From 749d3464f35fab13266430482429ddd885da1ab2 Mon Sep 17 00:00:00 2001 From: Justin Frahm Date: Tue, 27 Jan 2026 10:40:42 -0700 Subject: [PATCH 01/52] Expiry notification first pass --- IMPLEMENTATION_PLAN.md | 339 ++++++++++++++++++ .../email-notification-service/lambda.ts | 17 + .../lib/email/email-notification-service.ts | 50 +++ .../email/email-notification-service.test.ts | 91 +++++ .../cognito-backup/requirements-dev.txt | 4 +- .../python/cognito-backup/requirements.txt | 4 +- .../common/cc_common/email_service_client.py | 39 ++ .../python/common/requirements-dev.txt | 10 +- .../lambdas/python/common/requirements.txt | 4 +- .../tests/unit/test_email_service_client.py | 45 ++- .../requirements-dev.txt | 4 +- .../custom-resources/requirements-dev.txt | 4 +- .../python/data-events/requirements-dev.txt | 4 +- .../disaster-recovery/requirements-dev.txt | 4 +- .../provider-data-v1/requirements-dev.txt | 4 +- .../search/expiration_reminder_tracker.py | 150 ++++++++ .../search/handlers/expiration_reminders.py | 337 +++++++++++++++++ .../python/search/requirements-dev.txt | 4 +- .../function/test_expiration_reminders.py | 297 +++++++++++++++ .../tests/function/test_search_providers.py | 2 +- .../unit/test_expiration_reminder_tracker.py | 149 ++++++++ .../tests/unit/test_opensearch_client.py | 10 +- .../staff-user-pre-token/requirements-dev.txt | 4 +- .../python/staff-users/requirements-dev.txt | 4 +- 24 files changed, 1550 insertions(+), 30 deletions(-) create mode 100644 IMPLEMENTATION_PLAN.md create mode 100644 backend/compact-connect/lambdas/python/search/expiration_reminder_tracker.py create mode 100644 backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py create mode 100644 backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py create mode 100644 backend/compact-connect/lambdas/python/search/tests/unit/test_expiration_reminder_tracker.py diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md new file mode 100644 index 000000000..b8ba763b5 --- /dev/null +++ b/IMPLEMENTATION_PLAN.md @@ -0,0 +1,339 @@ +--- +name: Expiration Reminder Stack +overview: Implement a new ExpirationReminderStack that sends email notifications to practitioners about expiring privileges at 30 days, 7 days, and day of expiration. The solution uses OpenSearch for efficient querying and includes notification tracking for idempotency. All providers are processed in a single Lambda execution. +todos: + - id: stack-infrastructure + content: Create ExpirationReminderStack with Lambda, EventBridge rules, alarms + status: pending + - id: lambda-handler + content: Implement expiration_reminders Lambda with OpenSearch query (single execution) + status: pending + - id: notification-tracking + content: Add ExpirationReminderTracker using EventStateTable for idempotency + status: pending + - id: email-template-ts + content: Add privilegeExpirationReminder template to email-notification-service Lambda + status: pending + - id: email-client-py + content: Add send_privilege_expiration_reminder_email to EmailServiceClient + status: pending + - id: backend-stage + content: Integrate ExpirationReminderStack into BackendStage + status: pending + - id: unit-tests + content: Write unit tests for Lambda handler and notification tracking + status: pending +--- + +# Privilege Expiration Reminder Notification System + +## Architecture Overview + +```mermaid +flowchart TB + subgraph eventbridge [EventBridge Rules] + rule30[30-Day Rule] + rule7[7-Day Rule] + rule0[Expiration Day Rule] + end + + subgraph lambda [Lambda Function] + process[Process Reminders Lambda] + end + + subgraph deps [Dependencies] + opensearch[OpenSearch] + email[Email Notification Lambda] + eventstate[Event State Table] + end + + rule30 -->|targetDate: T+30| process + rule7 -->|targetDate: T+7| process + rule0 -->|targetDate: T| process + + process --> opensearch + process --> email + process --> eventstate +``` + +## Phased Implementation Plan + +### Phase 1 — OpenSearch query generator + handler skeleton + +- **Implement**: `iterate_privileges_by_expiration_date()` generator (page-on-demand) and a minimal `process_expiration_reminders()` handler that wires together: OpenSearch → per-provider processing loop → metrics output. +- **Add tests**: + - Unit tests for the generator pagination behavior (multiple pages, empty results, last page) using mocked OpenSearch responses. + - Unit tests for “extract privileges expiring on targetDate” from a provider document (including nested `inner_hits` vs full privileges list, if applicable). +- **Run locally**: + - `python -m pytest backend/compact-connect/lambdas/python/expiration-reminders/tests` + +### Phase 2 — Email template + Python email client call + +- **Implement**: + - Add `privilegeExpirationReminder` template to the Node email service and an exported method to send it. + - Add `send_privilege_expiration_reminder_email()` to `cc_common.email_service_client.EmailServiceClient` to invoke the email service lambda with the new template payload. +- **Add tests**: + - Node unit tests verifying template variable validation + rendered output shape for `privilegeExpirationReminder`. + - Python unit test verifying the `EmailServiceClient` payload for `privilegeExpirationReminder` (template name, recipientType `SPECIFIC`, variables). +- **Run locally**: + - `python -m pytest backend/compact-connect/lambdas/python/common/tests` + - Run the Node email notification service unit tests (from `backend/compact-connect/lambdas/nodejs/`, using the repo’s standard test command for that package). + +### Phase 3 — Idempotency tracking (EventStateTable) + +- **Implement**: `ExpirationReminderTracker` (or extend existing tracker pattern) so the handler can: + - Check “already notified for provider + expiration_date (+ compact)” before sending. + - Record success after sending so retries do not duplicate emails. +- **Add tests**: + - Unit tests for tracker keying and “already sent” behavior. + - Handler unit tests covering: already-sent → skipped, missing email → skipped, send failure → failed + recorded (as applicable). +- **Run locally**: + - `python -m pytest backend/compact-connect/lambdas/python/expiration-reminders/tests` + +### Phase 4 — CDK stack + scheduling + alarms (incl. duration alarm) + +- **Implement**: + - `ExpirationReminderStack` with the Lambda, 3 EventBridge rules (30/7/0 days), and log retention. + - CloudWatch alarms: + - Lambda errors/throttles (as appropriate). + - **Duration alarm**: triggers if Lambda execution duration exceeds **10 minutes** (with 15-minute Lambda timeout). + - Integrate stack into `backend/compact-connect/pipeline/backend_stage.py` (consistent with other optional stacks). +- **Add tests**: + - CDK assertions tests validating: rules exist, Lambda timeout is 15 minutes, and the duration alarm threshold is 10 minutes. +- **Run locally**: + - `python -m pytest backend/compact-connect/tests/app -k expiration_reminder` + +## Implementation Components + +### 1. New Stack: ExpirationReminderStack + +Create [`stacks/expiration_reminder_stack/__init__.py`](backend/compact-connect/stacks/expiration_reminder_stack/__init__.py) + +**Dependencies:** + +- `PersistentStack` - provider table, email service lambda, encryption key, alarm topic +- `EventStateStack` - notification tracking table +- `SearchPersistentStack` - OpenSearch domain for querying privileges by expiration date + +**Resources created:** + +- Lambda function for processing reminders (15-minute timeout to handle all providers in single execution) +- Three EventBridge rules (30-day, 7-day, day-of) +- CloudWatch alarm for Lambda execution time (triggers if execution exceeds 10 minutes) +- CloudWatch alarms for Lambda failures +- Log groups with appropriate retention + +**Note on execution time:** The Lambda is configured with a 15-minute timeout. A CloudWatch alarm will trigger if the execution time ever exceeds 10 minutes, alerting us before we approach the timeout limit. This allows us to monitor if we need to revisit the single-execution approach in the future. + +### 2. New Lambda: expiration-reminders + +Create new lambda directory: `lambdas/python/expiration-reminders/` + +**Handler:** `handlers/expiration_reminders.py` + +```python +def process_expiration_reminders(event: dict, context): + """ + Input: + { + "targetDate": "2026-02-16", # Expiration date to process + "daysBefore": 30, # Days before expiration (30, 7, or 0) + "scheduledTime": "2026-01-17...", # When rule triggered (for logging) + } + + Output: + { + "targetDate": "2026-02-16", + "daysBefore": 30, + "metrics": { "sent": N, "skipped": N, "failed": N, "alreadySent": N, "noEmail": N } + } + """ +``` + +**Core logic:** + +1. Query OpenSearch for active privileges expiring on `targetDate` + - Use `iterate_privileges_by_expiration_date()` generator which handles pagination internally + - The generator yields provider documents one at a time, fetching subsequent pages on demand + - This avoids loading all results into memory at once +2. As each provider document is yielded from the generator: + - Extract privileges expiring on `targetDate` from the provider document + - Check `EventStateTable` if notification already sent for this provider/date + - Skip if already sent (idempotency) + - Get provider's registered email from provider record + - Skip if no registered email + - Send email via Email Notification Service Lambda (consolidating all expiring privileges for this provider) + - Record notification sent in `EventStateTable` + +3. Return metrics summary with counts of sent, skipped, and failed notifications + +**Note:** The Lambda processes all providers in a single execution. OpenSearch pagination happens within the Lambda function (not across multiple invocations) using a generator that fetches pages on demand, keeping memory usage low. + +### 3. Email Template Addition + +Update [`lambdas/nodejs/email-notification-service/lambda.ts`](backend/compact-connect/lambdas/nodejs/email-notification-service/lambda.ts) + +Add new case for `privilegeExpirationReminder` template. + +Update [`lambdas/nodejs/lib/email/email-notification-service.ts`](backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts) + +Add `sendPrivilegeExpirationReminderEmail()` method. + +**Template variables:** + +- `providerFirstName` - Practitioner's first name +- `expirationDate` - The actual expiration date (formatted nicely) +- `privileges` - Array of `{ jurisdiction, licenseType, privilegeId }` + +**Email content:** + +- Subject: "Your Compact Connect Privileges Expire on [Date]" +- Body: Lists all privileges expiring on that date with jurisdiction and license type + +### 4. Python Email Client Extension + +Update [`lambdas/python/common/cc_common/email_service_client.py`](backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py) + +Add `send_privilege_expiration_reminder_email()` method following existing patterns. + +### 5. Notification Tracking + +Use existing `EventStateTable` with new key pattern (implemented in `cc_common.event_state_client`): + +``` +pk: {compact}#EXPIRATION_REMINDER#{provider_id} +sk: {event_type}#{expiration_date} +ttl: 90 days after expiration (auto-cleanup) +``` + +Where `event_type` is one of: +- `privilege.expiration.30day` +- `privilege.expiration.7day` +- `privilege.expiration.dayOf` + +This allows tracking each reminder type (30-day, 7-day, day-of) independently per provider/expiration date. + +`ExpirationReminderTracker` class in `cc_common.event_state_client` provides the interface for checking/recording sent notifications. + +### 6. OpenSearch Query Helper + +Add generator method to find privileges by expiration date. This method handles OpenSearch pagination internally and yields results one at a time, fetching subsequent pages on demand: + +```python +def iterate_privileges_by_expiration_date(compact: str, expiration_date: date): + """Generator that yields provider documents with privileges expiring on a specific date. + + Uses search_after pagination internally, fetching pages on demand as results are consumed. + This keeps memory usage low by not loading all results into memory at once. + + Yields provider documents (dict) one at a time. Each document contains the provider + data and a nested privileges array with matching privileges. + + :param compact: Compact identifier + :param expiration_date: Date to match against privilege expiration dates + :yield: Provider document dict with matching privileges + """ + index_name = f'compact_{compact}_providers' + search_after = None + current_page_hits = [] + is_last_page = False + + while True: + # Fetch next page if current page is exhausted + if not current_page_hits: + # If we've already processed the last page, we're done + if is_last_page: + break + + search_body = { + "query": { + "nested": { + "path": "privileges", + "query": { + "bool": { + "must": [ + {"term": {"privileges.dateOfExpiration": expiration_date.isoformat()}}, + {"term": {"privileges.status": "active"}} + ] + } + }, + "inner_hits": {"size": 100} + } + }, + "sort": [{"providerId": "asc"}], # Required for search_after pagination + "size": 100 + } + + if search_after: + search_body["search_after"] = search_after + + response = opensearch_client.search(index_name=index_name, body=search_body) + hits = response.get('hits', {}).get('hits', []) + + if not hits: + # No more results + break + + # Store hits as a list that we'll pop from + current_page_hits = hits + + # If we got fewer results than requested, this is the last page + if len(hits) < 100: + is_last_page = True + else: + # Get sort values from last hit for next page (before we start popping) + last_hit = hits[-1] + search_after = last_hit.get('sort') + + # Pop and yield next result from current page + # Using pop(0) removes the item from the list, freeing memory as we process + hit = current_page_hits.pop(0) + yield hit['_source'] +``` + +### 7. Backend Stage Integration + +Update [`pipeline/backend_stage.py`](backend/compact-connect/pipeline/backend_stage.py) + +Add `ExpirationReminderStack` instantiation, conditional on `self.persistent_stack.hosted_zone` (same as `NotificationStack` and `ReportingStack`). + +## File Structure + +``` +backend/compact-connect/ +├── stacks/ +│ └── expiration_reminder_stack/ +│ └── __init__.py # New stack definition +├── lambdas/ +│ ├── python/ +│ │ └── expiration-reminders/ # New lambda package +│ │ ├── handlers/ +│ │ │ └── expiration_reminders.py +│ │ ├── requirements.in +│ │ ├── requirements.txt +│ │ ├── requirements-dev.in +│ │ ├── requirements-dev.txt +│ │ └── tests/ +│ │ └── (unit tests) +│ └── nodejs/ +│ ├── email-notification-service/ +│ │ └── lambda.ts # Add template case +│ └── lib/email/ +│ └── email-notification-service.ts # Add email method +└── pipeline/ + └── backend_stage.py # Add stack to stage +``` + +## Testing Strategy + +1. **Unit tests** for the Lambda handler with mocked OpenSearch and email service +2. **Manual testing** via AWS Console - invoke Lambda directly with specific `targetDate`: + +```json +{ + "targetDate": "2026-02-16", + "scheduledTime": "2026-01-20T10:00:00Z" +} +``` + +The notification tracker ensures no duplicate emails are sent when retrying. 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 e147de605..4cc809242 100644 --- a/backend/compact-connect/lambdas/nodejs/email-notification-service/lambda.ts +++ b/backend/compact-connect/lambdas/nodejs/email-notification-service/lambda.ts @@ -386,6 +386,23 @@ export class Lambda implements LambdaInterface { event.templateVariables?.auditNote || '' ); break; + case 'privilegeExpirationReminder': + if (!event.specificEmails?.length) { + throw new Error('No recipients found for privilege expiration reminder email'); + } + if (!event.templateVariables?.providerFirstName + || !event.templateVariables?.expirationDate + || !event.templateVariables?.privileges) { + throw new Error('Missing required template variables for privilegeExpirationReminder template.'); + } + await this.emailService.sendPrivilegeExpirationReminderEmail( + event.compact, + event.specificEmails, + event.templateVariables.providerFirstName, + event.templateVariables.expirationDate, + event.templateVariables.privileges + ); + break; case 'licenseInvestigationStateNotification': if (!event.jurisdiction) { throw new Error('No jurisdiction provided for license investigation state notification email'); diff --git a/backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts b/backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts index 1b348f5a3..a871ff868 100644 --- a/backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts +++ b/backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts @@ -629,4 +629,54 @@ export class EmailNotificationService extends BaseEmailService { await this.sendEmail({ htmlContent, subject, recipients, errorMessage: 'Unable to send home jurisdiction change state notification email' }); } + + /** + * Sends a reminder email to a provider about expiring privileges + * @param compact - The compact name + * @param specificEmails - The email address(es) to send the notification to (provider's email) + * @param providerFirstName - The provider's first name + * @param expirationDate - The formatted expiration date string + * @param privileges - Array of expiring privileges with jurisdiction, licenseType, and privilegeId + */ + public async sendPrivilegeExpirationReminderEmail( + compact: string, + specificEmails: string[], + providerFirstName: string, + expirationDate: string, + privileges: { jurisdiction: string; licenseType: string; privilegeId: string }[] + ): Promise { + this.logger.info('Sending privilege expiration reminder email', { compact: compact, privilegeCount: privileges.length }); + + const recipients = specificEmails; + + if (recipients.length === 0) { + throw new Error('No recipients found for privilege expiration reminder email'); + } + + if (privileges.length === 0) { + throw new Error('No privileges provided for privilege expiration reminder email'); + } + + const emailContent = this.getNewEmailTemplate(); + const subject = `Your Compact Connect Privileges Expire on ${expirationDate}`; + const headerText = 'Privilege Expiration Reminder'; + const bodyText = `Hello ${providerFirstName},\n\nThis is a reminder that the following privilege(s) will expire on ${expirationDate}:`; + + this.insertHeader(emailContent, headerText); + this.insertBody(emailContent, bodyText, 'center'); + + privileges.forEach((privilege) => { + const titleText = `${privilege.licenseType.toUpperCase()} - ${privilege.jurisdiction.toUpperCase()}`; + const privilegeIdText = `Privilege Id: ${privilege.privilegeId}`; + + this.insertTuple(emailContent, titleText, privilegeIdText); + }); + + this.insertBody(emailContent, '\nPlease visit Compact Connect to renew your privileges before they expire.', 'center'); + this.insertFooter(emailContent); + + const htmlContent = this.renderTemplate(emailContent); + + await this.sendEmail({ htmlContent, subject, recipients, errorMessage: 'Unable to send privilege expiration reminder email' }); + } } 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 a43ee8193..b4d4e3b45 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 @@ -1067,4 +1067,95 @@ describe('EmailNotificationService', () => { expect(htmlContent).not.toContain('from TX to other'); }); }); + + describe('Privilege Expiration Reminder', () => { + it('should send privilege expiration reminder email with expected subject and content', async () => { + await emailService.sendPrivilegeExpirationReminderEmail( + 'aslp', + ['provider@example.com'], + 'John', + 'February 16, 2026', + [ + { jurisdiction: 'oh', licenseType: 'aud', privilegeId: 'AUD-OH-001' }, + { jurisdiction: 'ky', licenseType: 'slp', privilegeId: 'SLP-KY-002' } + ] + ); + + expect(mockSESClient).toHaveReceivedCommandWith( + SendEmailCommand, + { + Destination: { + ToAddresses: ['provider@example.com'] + }, + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('Privilege Expiration Reminder') + } + }, + Subject: { + Charset: 'UTF-8', + Data: 'Your Compact Connect Privileges Expire on February 16, 2026' + } + } + }, + FromEmailAddress: 'Compact Connect ' + } + ); + + // Get the actual HTML content for detailed validation + const emailCall = mockSESClient.commandCalls(SendEmailCommand)[0]; + const htmlContent = emailCall.args[0].input.Content?.Simple?.Body?.Html?.Data; + + expect(htmlContent).toBeDefined(); + expect(htmlContent).toContain('Hello John'); + expect(htmlContent).toContain('will expire on February 16, 2026'); + expect(htmlContent).toContain('AUD - OH'); + expect(htmlContent).toContain('Privilege Id: AUD-OH-001'); + expect(htmlContent).toContain('SLP - KY'); + expect(htmlContent).toContain('Privilege Id: SLP-KY-002'); + expect(htmlContent).toContain('Please visit Compact Connect to renew your privileges before they expire'); + }); + + it('should throw error when no recipients provided', async () => { + await expect(emailService.sendPrivilegeExpirationReminderEmail( + 'aslp', + [], + 'John', + 'February 16, 2026', + [{ jurisdiction: 'oh', licenseType: 'aud', privilegeId: 'AUD-OH-001' }] + )).rejects.toThrow('No recipients found for privilege expiration reminder email'); + }); + + it('should throw error when no privileges provided', async () => { + await expect(emailService.sendPrivilegeExpirationReminderEmail( + 'aslp', + ['provider@example.com'], + 'John', + 'February 16, 2026', + [] + )).rejects.toThrow('No privileges provided for privilege expiration reminder email'); + }); + + it('should send email with single privilege', async () => { + await emailService.sendPrivilegeExpirationReminderEmail( + 'aslp', + ['provider@example.com'], + 'Jane', + 'March 1, 2026', + [{ jurisdiction: 'ne', licenseType: 'slp', privilegeId: 'SLP-NE-123' }] + ); + + const emailCall = mockSESClient.commandCalls(SendEmailCommand)[0]; + const htmlContent = emailCall.args[0].input.Content?.Simple?.Body?.Html?.Data; + + expect(htmlContent).toBeDefined(); + expect(htmlContent).toContain('Hello Jane'); + expect(htmlContent).toContain('will expire on March 1, 2026'); + expect(htmlContent).toContain('SLP - NE'); + expect(htmlContent).toContain('Privilege Id: SLP-NE-123'); + }); + }); }); diff --git a/backend/compact-connect/lambdas/python/cognito-backup/requirements-dev.txt b/backend/compact-connect/lambdas/python/cognito-backup/requirements-dev.txt index 2eeb7f2b6..40bb11577 100644 --- a/backend/compact-connect/lambdas/python/cognito-backup/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/cognito-backup/requirements-dev.txt @@ -6,11 +6,11 @@ # aws-lambda-powertools==3.24.0 # via -r lambdas/python/cognito-backup/requirements-dev.in -boto3==1.42.34 +boto3==1.42.35 # via # -r lambdas/python/cognito-backup/requirements-dev.in # moto -botocore==1.42.34 +botocore==1.42.35 # via # -r lambdas/python/cognito-backup/requirements-dev.in # boto3 diff --git a/backend/compact-connect/lambdas/python/cognito-backup/requirements.txt b/backend/compact-connect/lambdas/python/cognito-backup/requirements.txt index e9c49dbf9..e151217a8 100644 --- a/backend/compact-connect/lambdas/python/cognito-backup/requirements.txt +++ b/backend/compact-connect/lambdas/python/cognito-backup/requirements.txt @@ -6,9 +6,9 @@ # aws-lambda-powertools==3.24.0 # via -r lambdas/python/cognito-backup/requirements.in -boto3==1.42.34 +boto3==1.42.35 # via -r lambdas/python/cognito-backup/requirements.in -botocore==1.42.34 +botocore==1.42.35 # via # -r lambdas/python/cognito-backup/requirements.in # boto3 diff --git a/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py b/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py index 248824aa2..778a164ea 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py @@ -37,6 +37,17 @@ class InvestigationNotificationTemplateVariables: provider_id: UUID +@dataclass +class PrivilegeExpirationReminderTemplateVariables: + """ + Template variables for privilege expiration reminder emails. + """ + + provider_first_name: str + expiration_date: date + privileges: list[dict] # Each dict has: jurisdiction, licenseType, privilegeId + + class ProviderNotificationMethod(Protocol): """Protocol for provider encumbrance notification methods.""" @@ -809,6 +820,34 @@ def send_military_audit_declined_notification( } return self._invoke_lambda(payload) + def send_privilege_expiration_reminder_email( + self, + *, + compact: str, + provider_email: str, + template_variables: 'PrivilegeExpirationReminderTemplateVariables', + ) -> dict[str, str]: + """ + Send a privilege expiration reminder email to a provider. + + :param compact: Compact name + :param provider_email: Email address of the provider + :param template_variables: Template variables for the email (provider name, expiration date, privileges) + :return: Response from the email notification service + """ + payload = { + 'compact': compact, + 'template': 'privilegeExpirationReminder', + 'recipientType': 'SPECIFIC', + 'specificEmails': [provider_email], + 'templateVariables': { + 'providerFirstName': template_variables.provider_first_name, + 'expirationDate': template_variables.expiration_date.strftime('%B %d, %Y'), + 'privileges': template_variables.privileges, + }, + } + return self._invoke_lambda(payload) + def send_home_jurisdiction_change_old_state_notification( self, *, diff --git a/backend/compact-connect/lambdas/python/common/requirements-dev.txt b/backend/compact-connect/lambdas/python/common/requirements-dev.txt index 938c03016..60c2779b4 100644 --- a/backend/compact-connect/lambdas/python/common/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/common/requirements-dev.txt @@ -18,21 +18,21 @@ aws-sam-translator==1.103.0 # moto aws-xray-sdk==2.15.0 # via moto -boto3==1.42.34 +boto3==1.42.35 # via # aws-sam-translator # moto -boto3-stubs[full]==1.42.34 +boto3-stubs[full]==1.42.35 # via -r lambdas/python/common/requirements-dev.in -boto3-stubs-full==1.42.34 +boto3-stubs-full==1.42.35 # via boto3-stubs -botocore==1.42.34 +botocore==1.42.35 # via # aws-xray-sdk # boto3 # moto # s3transfer -botocore-stubs==1.42.34 +botocore-stubs==1.42.35 # via boto3-stubs certifi==2026.1.4 # via requests diff --git a/backend/compact-connect/lambdas/python/common/requirements.txt b/backend/compact-connect/lambdas/python/common/requirements.txt index 00633f799..4709620df 100644 --- a/backend/compact-connect/lambdas/python/common/requirements.txt +++ b/backend/compact-connect/lambdas/python/common/requirements.txt @@ -10,9 +10,9 @@ argon2-cffi-bindings==25.1.0 # via argon2-cffi aws-lambda-powertools==3.24.0 # via -r lambdas/python/common/requirements.in -boto3==1.42.34 +boto3==1.42.35 # via -r lambdas/python/common/requirements.in -botocore==1.42.34 +botocore==1.42.35 # via # boto3 # s3transfer diff --git a/backend/compact-connect/lambdas/python/common/tests/unit/test_email_service_client.py b/backend/compact-connect/lambdas/python/common/tests/unit/test_email_service_client.py index ed72cc823..043dbb573 100644 --- a/backend/compact-connect/lambdas/python/common/tests/unit/test_email_service_client.py +++ b/backend/compact-connect/lambdas/python/common/tests/unit/test_email_service_client.py @@ -1,5 +1,5 @@ import json -from datetime import UTC, datetime +from datetime import UTC, date, datetime from unittest.mock import MagicMock from cc_common.config import logger @@ -222,3 +222,46 @@ def test_provider_account_recovery_confirmation_should_invoke_lambda_client_with } ), ) + + def test_privilege_expiration_reminder_should_invoke_lambda_client_with_expected_parameters(self): + from cc_common.email_service_client import PrivilegeExpirationReminderTemplateVariables + + mock_lambda_client = MagicMock() + test_model = self._generate_test_model(mock_lambda_client) + + privileges = [ + {'jurisdiction': 'oh', 'licenseType': 'aud', 'privilegeId': 'AUD-OH-001'}, + {'jurisdiction': 'ky', 'licenseType': 'slp', 'privilegeId': 'SLP-KY-002'}, + ] + + template_variables = PrivilegeExpirationReminderTemplateVariables( + provider_first_name='John', + expiration_date=date(2026, 2, 16), + privileges=privileges, + ) + + test_model.send_privilege_expiration_reminder_email( + compact=TEST_COMPACT, + provider_email='provider@example.com', + template_variables=template_variables, + ) + + mock_lambda_client.invoke.assert_called_once_with( + FunctionName='test-lambda-name', + InvocationType='RequestResponse', + Payload=json.dumps( + { + 'compact': TEST_COMPACT, + 'template': 'privilegeExpirationReminder', + 'recipientType': 'SPECIFIC', + 'specificEmails': [ + 'provider@example.com', + ], + 'templateVariables': { + 'providerFirstName': 'John', + 'expirationDate': 'February 16, 2026', + 'privileges': privileges, + }, + } + ), + ) diff --git a/backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt b/backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt index 5ae718d36..ddcddbcb2 100644 --- a/backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt @@ -4,9 +4,9 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/compact-configuration/requirements-dev.in # -boto3==1.42.34 +boto3==1.42.35 # via moto -botocore==1.42.34 +botocore==1.42.35 # via # boto3 # moto diff --git a/backend/compact-connect/lambdas/python/custom-resources/requirements-dev.txt b/backend/compact-connect/lambdas/python/custom-resources/requirements-dev.txt index 0da85c5f1..07d138a17 100644 --- a/backend/compact-connect/lambdas/python/custom-resources/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/custom-resources/requirements-dev.txt @@ -4,9 +4,9 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/custom-resources/requirements-dev.in # -boto3==1.42.34 +boto3==1.42.35 # via moto -botocore==1.42.34 +botocore==1.42.35 # via # boto3 # moto diff --git a/backend/compact-connect/lambdas/python/data-events/requirements-dev.txt b/backend/compact-connect/lambdas/python/data-events/requirements-dev.txt index f7465902c..1c867ffcc 100644 --- a/backend/compact-connect/lambdas/python/data-events/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/data-events/requirements-dev.txt @@ -4,9 +4,9 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/data-events/requirements-dev.in # -boto3==1.42.34 +boto3==1.42.35 # via moto -botocore==1.42.34 +botocore==1.42.35 # via # boto3 # moto diff --git a/backend/compact-connect/lambdas/python/disaster-recovery/requirements-dev.txt b/backend/compact-connect/lambdas/python/disaster-recovery/requirements-dev.txt index d3f57b027..174b7006b 100644 --- a/backend/compact-connect/lambdas/python/disaster-recovery/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/disaster-recovery/requirements-dev.txt @@ -4,9 +4,9 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/disaster-recovery/requirements-dev.in # -boto3==1.42.34 +boto3==1.42.35 # via moto -botocore==1.42.34 +botocore==1.42.35 # via # boto3 # moto diff --git a/backend/compact-connect/lambdas/python/provider-data-v1/requirements-dev.txt b/backend/compact-connect/lambdas/python/provider-data-v1/requirements-dev.txt index d1819cd74..8c6e6f9fa 100644 --- a/backend/compact-connect/lambdas/python/provider-data-v1/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/provider-data-v1/requirements-dev.txt @@ -4,9 +4,9 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/provider-data-v1/requirements-dev.in # -boto3==1.42.34 +boto3==1.42.35 # via moto -botocore==1.42.34 +botocore==1.42.35 # via # boto3 # moto diff --git a/backend/compact-connect/lambdas/python/search/expiration_reminder_tracker.py b/backend/compact-connect/lambdas/python/search/expiration_reminder_tracker.py new file mode 100644 index 000000000..7624e958d --- /dev/null +++ b/backend/compact-connect/lambdas/python/search/expiration_reminder_tracker.py @@ -0,0 +1,150 @@ +""" +Idempotency tracking for expiration reminder notifications. + +This module provides tracking for 30-day, 7-day, and day-of expiration reminders +to ensure each provider receives at most one reminder per expiration date per reminder type. +""" + +import time +from datetime import timedelta +from enum import StrEnum +from uuid import UUID + +from cc_common.config import config, logger + + +class ExpirationEventType(StrEnum): + """Event types for privilege expiration reminders.""" + + PRIVILEGE_EXPIRATION_30_DAY = 'privilege.expiration.30day' + PRIVILEGE_EXPIRATION_7_DAY = 'privilege.expiration.7day' + PRIVILEGE_EXPIRATION_DAY_OF = 'privilege.expiration.dayOf' + + +class ExpirationReminderTracker: + """ + Tracks expiration reminder notifications for providers. + + Uses a key pattern based on provider_id + expiration_date + event_type to track + whether 30-day, 7-day, or day-of reminders have been sent for each expiring privilege. + + Key pattern: + pk: {compact}#EXPIRATION_REMINDER#{provider_id} + sk: {event_type}#{expiration_date} + ttl: 90 days after the record is written (auto-cleanup) + """ + + # TTL: 90 days after the record is written + _TTL_DAYS = 90 + _SUCCESS_STATUS = 'SUCCESS' + + def __init__( + self, *, compact: str, provider_id: UUID, expiration_date: str, event_type: ExpirationEventType + ): + """ + Initialize the tracker for a specific provider, expiration date, and reminder type. + + :param compact: The compact identifier + :param provider_id: The provider's UUID + :param expiration_date: The privilege expiration date (ISO format string) + :param event_type: The reminder type (30-day, 7-day, or day-of) + """ + self.compact = compact + self.provider_id = provider_id + self.expiration_date = expiration_date + self.event_type = event_type + self._event_state_table = config.event_state_table + self._cached_record: dict | None = None + self._cache_loaded: bool = False + + def _build_pk(self) -> str: + return f'{self.compact}#EXPIRATION_REMINDER#{self.provider_id}' + + def _build_sk(self) -> str: + return f'{self.event_type}#{self.expiration_date}' + + def _get_record(self) -> dict | None: + """Get the existing record, with caching.""" + if not self._cache_loaded: + pk = self._build_pk() + sk = self._build_sk() + try: + response = self._event_state_table.get_item( + Key={'pk': pk, 'sk': sk}, + ConsistentRead=True, + ) + self._cached_record = response.get('Item') + except Exception as e: # noqa: BLE001 + # Fail open on read errors - allow notification to proceed + logger.warning( + 'Failed to check expiration reminder status', + **self._log_context(), + error=str(e), + ) + self._cached_record = None + self._cache_loaded = True + return self._cached_record + + def was_already_sent(self) -> bool: + """ + Check if this notification was already successfully sent. + + :return: True if already sent successfully, False otherwise + """ + record = self._get_record() + if record is None: + return False + return record.get('status') == self._SUCCESS_STATUS + + def record_success(self) -> None: + """Record a successful notification.""" + self._write_record(status=self._SUCCESS_STATUS) + + def record_failure(self, *, error_message: str) -> None: + """ + Record a failed notification attempt. + + :param error_message: Description of the failure + """ + self._write_record(status='FAILED', error_message=error_message) + + def _write_record(self, *, status: str, error_message: str | None = None) -> None: + """Write the record, swallowing and logging any DynamoDB errors.""" + pk = self._build_pk() + sk = self._build_sk() + ttl = int(time.time()) + int(timedelta(days=self._TTL_DAYS).total_seconds()) + + item = { + 'pk': pk, + 'sk': sk, + 'status': status, + 'compact': self.compact, + 'providerId': str(self.provider_id), + 'expirationDate': self.expiration_date, + 'eventType': self.event_type, + 'ttl': ttl, + } + + if error_message: + item['errorMessage'] = error_message + + try: + self._event_state_table.put_item(Item=item) + logger.debug('Recorded expiration reminder state', pk=pk, sk=sk, status=status) + except Exception as e: # noqa: BLE001 + # Swallow DynamoDB errors - notification was sent, tracking is secondary + logger.error( + 'Unable to record expiration reminder state.', + status=status, + **self._log_context(), + error=str(e), + ) + + def _log_context(self) -> dict: + """Return context dict for logging.""" + return { + 'compact': self.compact, + 'provider_id': str(self.provider_id), + 'expiration_date': self.expiration_date, + 'event_type': self.event_type, + } diff --git a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py new file mode 100644 index 000000000..6aee5e41e --- /dev/null +++ b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py @@ -0,0 +1,337 @@ +from __future__ import annotations + +from collections.abc import Generator +from dataclasses import dataclass, replace +from datetime import date +from uuid import UUID + +from aws_lambda_powertools.utilities.typing import LambdaContext +from cc_common.config import config, logger +from cc_common.email_service_client import PrivilegeExpirationReminderTemplateVariables +from cc_common.exceptions import CCInvalidRequestException +from expiration_reminder_tracker import ExpirationEventType, ExpirationReminderTracker +from opensearch_client import OpenSearchClient + +DEFAULT_PAGE_SIZE = 100 + +# Map days before expiration to ExpirationEventType +DAYS_BEFORE_TO_EVENT_TYPE = { + 30: ExpirationEventType.PRIVILEGE_EXPIRATION_30_DAY, + 7: ExpirationEventType.PRIVILEGE_EXPIRATION_7_DAY, + 0: ExpirationEventType.PRIVILEGE_EXPIRATION_DAY_OF, +} + + +# Instantiate outside handler for connection reuse across invocations +opensearch_client = OpenSearchClient(timeout=30) + + +@dataclass(frozen=True) +class Metrics: + """Tracks notification processing statistics.""" + + sent: int = 0 + skipped: int = 0 + failed: int = 0 + already_sent: int = 0 + no_email: int = 0 + matched_privileges: int = 0 + providers_with_matches: int = 0 + + def as_dict(self) -> dict[str, int]: + return { + 'sent': self.sent, + 'skipped': self.skipped, + 'failed': self.failed, + 'alreadySent': self.already_sent, + 'noEmail': self.no_email, + 'matchedPrivileges': self.matched_privileges, + 'providersWithMatches': self.providers_with_matches, + } + + +def process_expiration_reminders(event: dict, context: LambdaContext): # noqa: ARG001 unused-argument + """ + Process privilege expiration reminders: + - Query OpenSearch (paginated) for privileges expiring on target date + - Check idempotency tracker for each provider + - Send email notification if not already sent + - Record success/failure for idempotency + + Event format: + { + "targetDate": "2026-02-16", # Expiration date to process + "daysBefore": 30, # Days before expiration (30, 7, or 0) + "scheduledTime": "2026-01-17..." # When rule triggered (for logging) + } + """ + try: + target_date_str = event['targetDate'] + days_before = event['daysBefore'] + scheduled_time = event['scheduledTime'] + except KeyError as e: + raise CCInvalidRequestException(f'Missing required field: {e.args[0]}') from e + + if days_before not in DAYS_BEFORE_TO_EVENT_TYPE: + raise CCInvalidRequestException(f'Invalid daysBefore value: {days_before}. Must be 30, 7, or 0.') + + event_type = DAYS_BEFORE_TO_EVENT_TYPE[days_before] + expiration_date = _parse_iso_date(target_date_str) + + logger.info( + 'Processing privilege expiration reminders', + target_date=target_date_str, + days_before=days_before, + event_type=event_type, + scheduled_time=scheduled_time, + ) + + metrics = Metrics() + + for compact in config.compacts: + for provider_doc in iterate_privileges_by_expiration_date( + compact=compact, + expiration_date=expiration_date, + page_size=DEFAULT_PAGE_SIZE, + ): + matched_privileges = extract_expiring_privileges_from_provider_document( + provider_document=provider_doc, expiration_date=expiration_date + ) + if not matched_privileges: + continue + + metrics = replace( + metrics, + matched_privileges=metrics.matched_privileges + len(matched_privileges), + providers_with_matches=metrics.providers_with_matches + 1, + ) + + # Process this provider's notification + result = _process_provider_notification( + compact=compact, + provider_doc=provider_doc, + expiration_date=expiration_date, + event_type=event_type, + matched_privileges=matched_privileges, + ) + metrics = replace( + metrics, + sent=metrics.sent + result['sent'], + skipped=metrics.skipped + result['skipped'], + failed=metrics.failed + result['failed'], + already_sent=metrics.already_sent + result['already_sent'], + no_email=metrics.no_email + result['no_email'], + ) + + logger.info('Completed processing expiration reminders', metrics=metrics.as_dict()) + return {'targetDate': target_date_str, 'daysBefore': days_before, 'metrics': metrics.as_dict()} + + +def _process_provider_notification( + *, + compact: str, + provider_doc: dict, + expiration_date: date, + event_type: ExpirationEventType, + matched_privileges: list[dict], +) -> dict[str, int]: + """ + Process a single provider's expiration reminder notification. + + :return: Dict with counts for sent, skipped, failed, already_sent, no_email + """ + result = {'sent': 0, 'skipped': 0, 'failed': 0, 'already_sent': 0, 'no_email': 0} + + provider_id_str = provider_doc.get('providerId') + if not provider_id_str: + logger.warning('Provider document missing providerId', compact=compact) + result['skipped'] = 1 + return result + + try: + provider_id = UUID(provider_id_str) + except ValueError: + logger.warning('Invalid providerId format', provider_id=provider_id_str, compact=compact) + result['skipped'] = 1 + return result + + # Check for registered email - providers with privileges should always be registered + provider_email = provider_doc.get('compactConnectRegisteredEmailAddress') + if not provider_email: + logger.error( + 'Provider with privileges has no registered email address', + provider_id=str(provider_id), + compact=compact, + ) + result['no_email'] = 1 + return result + + # Check idempotency tracker + tracker = ExpirationReminderTracker( + compact=compact, + provider_id=provider_id, + expiration_date=expiration_date.isoformat(), + event_type=event_type, + ) + + if tracker.was_already_sent(): + logger.debug( + 'Reminder already sent, skipping', + provider_id=str(provider_id), + compact=compact, + event_type=event_type, + ) + result['already_sent'] = 1 + return result + + # Prepare and send email + provider_first_name = provider_doc.get('givenName', 'Provider') + + # Format privileges for email template + email_privileges = [ + { + 'jurisdiction': p.get('jurisdiction', ''), + 'licenseType': p.get('licenseType', ''), + 'privilegeId': p.get('privilegeId', ''), + } + for p in matched_privileges + ] + + template_variables = PrivilegeExpirationReminderTemplateVariables( + provider_first_name=provider_first_name, + expiration_date=expiration_date, + privileges=email_privileges, + ) + + try: + config.email_service_client.send_privilege_expiration_reminder_email( + compact=compact, + provider_email=provider_email, + template_variables=template_variables, + ) + tracker.record_success() + logger.info( + 'Sent expiration reminder', + provider_id=str(provider_id), + compact=compact, + event_type=event_type, + ) + result['sent'] = 1 + except Exception as e: # noqa: BLE001 catching all errors intentionally to record failures + tracker.record_failure(error_message=str(e)) + logger.error( + 'Failed to send expiration reminder', + provider_id=str(provider_id), + compact=compact, + event_type=event_type, + error=str(e), + ) + result['failed'] = 1 + + return result + + +def iterate_privileges_by_expiration_date( + *, + compact: str, + expiration_date: date, + page_size: int = DEFAULT_PAGE_SIZE, +) -> Generator[dict, None, None]: + """ + Generator yielding provider documents with (potentially) matching privileges. + + OpenSearch pagination is handled internally using `search_after`. Results are yielded + one provider at a time, and the current page is consumed by popping a single hit + per iteration so memory usage decreases as the page is processed. + """ + index_name = f'compact_{compact}_providers' + search_after = None + current_page_hits: list[dict] = [] + is_last_page = False + + while True: + if not current_page_hits: + if is_last_page: + break + + search_body = _build_expiration_query(expiration_date=expiration_date, page_size=page_size) + if search_after is not None: + search_body['search_after'] = search_after + + response = opensearch_client.search(index_name=index_name, body=search_body) + hits = response.get('hits', {}).get('hits', []) + if not hits: + break + + # capture next cursor BEFORE mutating / consuming hits + if len(hits) < page_size: + is_last_page = True + else: + search_after = hits[-1].get('sort') + + # Reverse the list so we can pop() (O(1)) while maintaining original ordering. + current_page_hits = list(reversed(hits)) + + hit = current_page_hits.pop() + yield _provider_document_from_hit(hit) + + +def extract_expiring_privileges_from_provider_document(*, provider_document: dict, expiration_date: date) -> list[dict]: + """ + Return privileges in the provider document expiring on expiration_date and active. + + If the generator merged `inner_hits` into `provider_document['privileges']`, this + will typically be a tight list already; we still filter defensively. + """ + privileges = provider_document.get('privileges', []) or [] + expiration_date_str = expiration_date.isoformat() + return [ + p + for p in privileges + if p.get('dateOfExpiration') == expiration_date_str and str(p.get('status', '')).lower() == 'active' + ] + + +def _provider_document_from_hit(hit: dict) -> dict: + """ + Normalize an OpenSearch hit into a provider document shape for downstream processing. + + If `inner_hits.privileges` is present, replace the provider's `privileges` list with + only the matched privileges so we don't have to scan the full provider privileges list. + """ + provider_doc = dict(hit.get('_source', {}) or {}) + + inner_hits = (hit.get('inner_hits') or {}).get('privileges', {}).get('hits', {}).get('hits', []) + if inner_hits: + provider_doc['privileges'] = [ih.get('_source', {}) for ih in inner_hits] + + return provider_doc + + +def _build_expiration_query(*, expiration_date: date, page_size: int) -> dict: + return { + 'query': { + 'nested': { + 'path': 'privileges', + 'query': { + 'bool': { + 'must': [ + {'term': {'privileges.dateOfExpiration': expiration_date.isoformat()}}, + {'term': {'privileges.status': 'active'}}, + ], + }, + }, + 'inner_hits': {'size': 100}, + }, + }, + # Required for search_after pagination + 'sort': [{'providerId': 'asc'}], + 'size': page_size, + } + + +def _parse_iso_date(value: str) -> date: + try: + return date.fromisoformat(value) + except Exception as e: + raise CCInvalidRequestException(f'Invalid ISO date for targetDate: {value}') from e diff --git a/backend/compact-connect/lambdas/python/search/requirements-dev.txt b/backend/compact-connect/lambdas/python/search/requirements-dev.txt index f478cfc14..e70b4bd12 100644 --- a/backend/compact-connect/lambdas/python/search/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/search/requirements-dev.txt @@ -4,9 +4,9 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/search/requirements-dev.in # -boto3==1.42.34 +boto3==1.42.35 # via moto -botocore==1.42.34 +botocore==1.42.35 # via # boto3 # moto diff --git a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py new file mode 100644 index 000000000..e0bef12b8 --- /dev/null +++ b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py @@ -0,0 +1,297 @@ +from datetime import date +from unittest.mock import MagicMock, patch +from uuid import uuid4 + +from tests import TstLambdas + + +class TestExpirationRemindersOpenSearch(TstLambdas): + """Tests for OpenSearch query and data extraction.""" + + def test_extract_expiring_privileges_filters_by_date_and_active_status(self): + from handlers.expiration_reminders import extract_expiring_privileges_from_provider_document + + expiration_date = date(2026, 2, 16) + provider_doc = { + 'providerId': 'p1', + 'privileges': [ + {'privilegeId': 'a', 'dateOfExpiration': '2026-02-16', 'status': 'active'}, + {'privilegeId': 'b', 'dateOfExpiration': '2026-02-16', 'status': 'inactive'}, + {'privilegeId': 'c', 'dateOfExpiration': '2026-02-15', 'status': 'active'}, + ], + } + + matched = extract_expiring_privileges_from_provider_document( + provider_document=provider_doc, + expiration_date=expiration_date, + ) + + self.assertEqual([{'privilegeId': 'a', 'dateOfExpiration': '2026-02-16', 'status': 'active'}], matched) + + def test_iterate_privileges_by_expiration_date_paginates_with_search_after_and_yields_in_order(self): + # Patch the module-level opensearch_client used by the generator + with patch('handlers.expiration_reminders.opensearch_client') as mock_client: + mock_client.search = MagicMock() + + # Page 1: p1, p2 + mock_client.search.side_effect = [ + { + 'hits': { + 'hits': [ + { + '_source': {'providerId': 'p1', 'privileges': []}, + 'sort': ['p1'], + }, + { + '_source': {'providerId': 'p2', 'privileges': []}, + 'sort': ['p2'], + }, + ] + } + }, + # Page 2: p3 + { + 'hits': { + 'hits': [ + { + '_source': {'providerId': 'p3', 'privileges': []}, + 'sort': ['p3'], + } + ] + } + }, + ] + + from handlers.expiration_reminders import iterate_privileges_by_expiration_date + + results = list( + iterate_privileges_by_expiration_date( + compact='aslp', + expiration_date=date(2026, 2, 16), + page_size=2, + ) + ) + + self.assertEqual([r['providerId'] for r in results], ['p1', 'p2', 'p3']) + + # Verify pagination: second call includes search_after from last hit in page 1 + first_call_kwargs = mock_client.search.call_args_list[0].kwargs + second_call_kwargs = mock_client.search.call_args_list[1].kwargs + + self.assertEqual(first_call_kwargs['index_name'], 'compact_aslp_providers') + self.assertNotIn('search_after', first_call_kwargs['body']) + + self.assertEqual(second_call_kwargs['index_name'], 'compact_aslp_providers') + self.assertEqual(second_call_kwargs['body']['search_after'], ['p2']) + + def test_iterate_privileges_by_expiration_date_merges_inner_hits_privileges_into_provider_document(self): + with patch('handlers.expiration_reminders.opensearch_client') as mock_client: + mock_client.search = MagicMock( + return_value={ + 'hits': { + 'hits': [ + { + '_source': { + 'providerId': 'p1', + 'privileges': [{'privilegeId': 'should_be_replaced'}], + }, + 'sort': ['p1'], + 'inner_hits': { + 'privileges': { + 'hits': { + 'hits': [ + {'_source': {'privilegeId': 'a'}}, + {'_source': {'privilegeId': 'b'}}, + ] + } + } + }, + } + ] + } + } + ) + + from handlers.expiration_reminders import iterate_privileges_by_expiration_date + + provider_doc = next( + iterate_privileges_by_expiration_date( + compact='aslp', + expiration_date=date(2026, 2, 16), + page_size=100, + ) + ) + + self.assertEqual(provider_doc['providerId'], 'p1') + self.assertEqual(provider_doc['privileges'], [{'privilegeId': 'a'}, {'privilegeId': 'b'}]) + + +class TestProcessExpirationReminders(TstLambdas): + """Tests for the main handler with email sending and idempotency.""" + + def _make_provider_doc( + self, + *, + provider_id: str | None = None, + email: str | None = 'test@example.com', + given_name: str = 'John', + privileges: list | None = None, + ) -> dict: + """Helper to create a provider document for testing.""" + doc = { + 'providerId': provider_id or str(uuid4()), + 'givenName': given_name, + 'privileges': privileges or [ + { + 'privilegeId': 'a', + 'dateOfExpiration': '2026-02-16', + 'status': 'active', + 'jurisdiction': 'oh', + 'licenseType': 'aud', + }, + ], + } + if email: + doc['compactConnectRegisteredEmailAddress'] = email + return doc + + def _make_event(self, days_before: int = 30) -> dict: + """Helper to create a valid event for testing.""" + return { + 'targetDate': '2026-02-16', + 'daysBefore': days_before, + 'scheduledTime': '2026-01-17T10:00:00Z', + } + + def test_handler_sends_email_and_records_success(self): + with ( + patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') as mock_iter, + patch('handlers.expiration_reminders.config') as mock_config, + patch('handlers.expiration_reminders.ExpirationReminderTracker') as mock_tracker_class, + ): + mock_config.compacts = ['aslp'] + mock_email_client = MagicMock() + mock_config.email_service_client = mock_email_client + + mock_tracker_instance = MagicMock() + mock_tracker_instance.was_already_sent.return_value = False + mock_tracker_class.return_value = mock_tracker_instance + + mock_iter.return_value = iter([self._make_provider_doc()]) + + from handlers.expiration_reminders import process_expiration_reminders + + resp = process_expiration_reminders(self._make_event(days_before=30), self.mock_context) + + self.assertEqual(resp['metrics']['sent'], 1) + self.assertEqual(resp['metrics']['failed'], 0) + self.assertEqual(resp['daysBefore'], 30) + mock_email_client.send_privilege_expiration_reminder_email.assert_called_once() + mock_tracker_instance.record_success.assert_called_once() + + # Verify tracker was created with correct event_type + from expiration_reminder_tracker import ExpirationEventType + + mock_tracker_class.assert_called_once() + tracker_call = mock_tracker_class.call_args.kwargs + self.assertEqual(tracker_call['event_type'], ExpirationEventType.PRIVILEGE_EXPIRATION_30_DAY) + + def test_handler_skips_when_already_sent(self): + with ( + patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') as mock_iter, + patch('handlers.expiration_reminders.config') as mock_config, + patch('handlers.expiration_reminders.ExpirationReminderTracker') as mock_tracker_class, + ): + mock_config.compacts = ['aslp'] + mock_email_client = MagicMock() + mock_config.email_service_client = mock_email_client + + mock_tracker_instance = MagicMock() + mock_tracker_instance.was_already_sent.return_value = True + mock_tracker_class.return_value = mock_tracker_instance + + mock_iter.return_value = iter([self._make_provider_doc()]) + + from handlers.expiration_reminders import process_expiration_reminders + + resp = process_expiration_reminders(self._make_event(days_before=7), self.mock_context) + + self.assertEqual(resp['metrics']['sent'], 0) + self.assertEqual(resp['metrics']['alreadySent'], 1) + mock_email_client.send_privilege_expiration_reminder_email.assert_not_called() + + def test_handler_logs_error_for_provider_without_email(self): + with ( + patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') as mock_iter, + patch('handlers.expiration_reminders.config') as mock_config, + patch('handlers.expiration_reminders.ExpirationReminderTracker') as mock_tracker_class, + patch('handlers.expiration_reminders.logger') as mock_logger, + ): + mock_config.compacts = ['aslp'] + mock_email_client = MagicMock() + mock_config.email_service_client = mock_email_client + + # Provider without email + mock_iter.return_value = iter([self._make_provider_doc(email=None)]) + + from handlers.expiration_reminders import process_expiration_reminders + + resp = process_expiration_reminders(self._make_event(days_before=0), self.mock_context) + + self.assertEqual(resp['metrics']['sent'], 0) + self.assertEqual(resp['metrics']['noEmail'], 1) + mock_email_client.send_privilege_expiration_reminder_email.assert_not_called() + mock_tracker_class.assert_not_called() + # Verify error was logged (not just debug) + mock_logger.error.assert_called() + + def test_handler_records_failure_on_email_error(self): + with ( + patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') as mock_iter, + patch('handlers.expiration_reminders.config') as mock_config, + patch('handlers.expiration_reminders.ExpirationReminderTracker') as mock_tracker_class, + ): + mock_config.compacts = ['aslp'] + mock_email_client = MagicMock() + mock_email_client.send_privilege_expiration_reminder_email.side_effect = Exception('Email service down') + mock_config.email_service_client = mock_email_client + + mock_tracker_instance = MagicMock() + mock_tracker_instance.was_already_sent.return_value = False + mock_tracker_class.return_value = mock_tracker_instance + + mock_iter.return_value = iter([self._make_provider_doc()]) + + from handlers.expiration_reminders import process_expiration_reminders + + resp = process_expiration_reminders(self._make_event(), self.mock_context) + + self.assertEqual(resp['metrics']['sent'], 0) + self.assertEqual(resp['metrics']['failed'], 1) + mock_tracker_instance.record_failure.assert_called_once() + self.assertIn('Email service down', mock_tracker_instance.record_failure.call_args.kwargs['error_message']) + + def test_handler_validates_days_before_value(self): + from cc_common.exceptions import CCInvalidRequestException + from handlers.expiration_reminders import process_expiration_reminders + + with self.assertRaises(CCInvalidRequestException) as ctx: + process_expiration_reminders( + {'targetDate': '2026-02-16', 'daysBefore': 15, 'scheduledTime': '2026-01-17T10:00:00Z'}, + self.mock_context, + ) + + self.assertIn('15', str(ctx.exception)) + self.assertIn('Must be 30, 7, or 0', str(ctx.exception)) + + def test_handler_requires_days_before_field(self): + from cc_common.exceptions import CCInvalidRequestException + from handlers.expiration_reminders import process_expiration_reminders + + with self.assertRaises(CCInvalidRequestException) as ctx: + process_expiration_reminders( + {'targetDate': '2026-02-16', 'scheduledTime': '2026-01-17T10:00:00Z'}, + self.mock_context, + ) + + self.assertIn('daysBefore', str(ctx.exception)) diff --git a/backend/compact-connect/lambdas/python/search/tests/function/test_search_providers.py b/backend/compact-connect/lambdas/python/search/tests/function/test_search_providers.py index 5e4ef2dd7..02ae82529 100644 --- a/backend/compact-connect/lambdas/python/search/tests/function/test_search_providers.py +++ b/backend/compact-connect/lambdas/python/search/tests/function/test_search_providers.py @@ -1,7 +1,6 @@ import json from unittest.mock import patch -from cc_common.exceptions import CCInvalidRequestException from moto import mock_aws from . import TstFunction @@ -446,6 +445,7 @@ def test_query_with_nested_index_key_returns_400(self): @patch('handlers.search.opensearch_client') def test_opensearch_request_error_returns_400_with_error_message(self, mock_opensearch_client): """Test that OpenSearch RequestError with status 400 returns error message to caller.""" + from cc_common.exceptions import CCInvalidRequestException from handlers.search import search_api_handler # Create a RequestError with realistic OpenSearch error structure diff --git a/backend/compact-connect/lambdas/python/search/tests/unit/test_expiration_reminder_tracker.py b/backend/compact-connect/lambdas/python/search/tests/unit/test_expiration_reminder_tracker.py new file mode 100644 index 000000000..470a111d6 --- /dev/null +++ b/backend/compact-connect/lambdas/python/search/tests/unit/test_expiration_reminder_tracker.py @@ -0,0 +1,149 @@ +from unittest.mock import MagicMock, patch +from uuid import uuid4 + +from tests import TstLambdas + + +class TestExpirationReminderTracker(TstLambdas): + """Tests for the ExpirationReminderTracker idempotency logic.""" + + def test_was_already_sent_returns_false_when_no_record_exists(self): + mock_event_state_table = MagicMock() + mock_event_state_table.get_item.return_value = {} + + with patch('expiration_reminder_tracker.config') as mock_config: + mock_config.event_state_table = mock_event_state_table + + from expiration_reminder_tracker import ExpirationEventType, ExpirationReminderTracker + + tracker = ExpirationReminderTracker( + compact='aslp', + provider_id=uuid4(), + expiration_date='2026-02-16', + event_type=ExpirationEventType.PRIVILEGE_EXPIRATION_30_DAY, + ) + + self.assertFalse(tracker.was_already_sent()) + + def test_was_already_sent_returns_true_when_success_record_exists(self): + mock_event_state_table = MagicMock() + mock_event_state_table.get_item.return_value = {'Item': {'status': 'SUCCESS'}} + + with patch('expiration_reminder_tracker.config') as mock_config: + mock_config.event_state_table = mock_event_state_table + + from expiration_reminder_tracker import ExpirationEventType, ExpirationReminderTracker + + tracker = ExpirationReminderTracker( + compact='aslp', + provider_id=uuid4(), + expiration_date='2026-02-16', + event_type=ExpirationEventType.PRIVILEGE_EXPIRATION_30_DAY, + ) + + self.assertTrue(tracker.was_already_sent()) + + def test_was_already_sent_returns_false_when_failed_record_exists(self): + mock_event_state_table = MagicMock() + mock_event_state_table.get_item.return_value = {'Item': {'status': 'FAILED'}} + + with patch('expiration_reminder_tracker.config') as mock_config: + mock_config.event_state_table = mock_event_state_table + + from expiration_reminder_tracker import ExpirationEventType, ExpirationReminderTracker + + tracker = ExpirationReminderTracker( + compact='aslp', + provider_id=uuid4(), + expiration_date='2026-02-16', + event_type=ExpirationEventType.PRIVILEGE_EXPIRATION_7_DAY, + ) + + # FAILED status should allow retry + self.assertFalse(tracker.was_already_sent()) + + def test_record_success_calls_put_item(self): + mock_event_state_table = MagicMock() + mock_event_state_table.get_item.return_value = {} + + with patch('expiration_reminder_tracker.config') as mock_config: + mock_config.event_state_table = mock_event_state_table + + from expiration_reminder_tracker import ExpirationEventType, ExpirationReminderTracker + + provider_id = uuid4() + tracker = ExpirationReminderTracker( + compact='aslp', + provider_id=provider_id, + expiration_date='2026-02-16', + event_type=ExpirationEventType.PRIVILEGE_EXPIRATION_DAY_OF, + ) + + tracker.record_success() + + mock_event_state_table.put_item.assert_called_once() + call_kwargs = mock_event_state_table.put_item.call_args.kwargs + item = call_kwargs['Item'] + self.assertEqual(item['pk'], f'aslp#EXPIRATION_REMINDER#{provider_id}') + self.assertEqual(item['sk'], f'{ExpirationEventType.PRIVILEGE_EXPIRATION_DAY_OF}#2026-02-16') + self.assertEqual(item['status'], 'SUCCESS') + self.assertIn('ttl', item) + + def test_record_failure_includes_error_message(self): + mock_event_state_table = MagicMock() + mock_event_state_table.get_item.return_value = {} + + with patch('expiration_reminder_tracker.config') as mock_config: + mock_config.event_state_table = mock_event_state_table + + from expiration_reminder_tracker import ExpirationEventType, ExpirationReminderTracker + + tracker = ExpirationReminderTracker( + compact='aslp', + provider_id=uuid4(), + expiration_date='2026-02-16', + event_type=ExpirationEventType.PRIVILEGE_EXPIRATION_30_DAY, + ) + + tracker.record_failure(error_message='Connection timeout') + + mock_event_state_table.put_item.assert_called_once() + call_kwargs = mock_event_state_table.put_item.call_args.kwargs + item = call_kwargs['Item'] + self.assertEqual(item['status'], 'FAILED') + self.assertEqual(item['errorMessage'], 'Connection timeout') + + def test_different_event_types_are_tracked_separately(self): + """Verify that 30-day, 7-day, and day-of reminders use different keys.""" + from expiration_reminder_tracker import ExpirationEventType, ExpirationReminderTracker + + mock_event_state_table = MagicMock() + + # 30-day was sent, but 7-day was not + def mock_get_item(*, Key, ConsistentRead): # noqa: ARG001, N803 + if ExpirationEventType.PRIVILEGE_EXPIRATION_30_DAY in Key['sk']: + return {'Item': {'status': 'SUCCESS'}} + return {} + + mock_event_state_table.get_item.side_effect = mock_get_item + + with patch('expiration_reminder_tracker.config') as mock_config: + mock_config.event_state_table = mock_event_state_table + + provider_id = uuid4() + + tracker_30 = ExpirationReminderTracker( + compact='aslp', + provider_id=provider_id, + expiration_date='2026-02-16', + event_type=ExpirationEventType.PRIVILEGE_EXPIRATION_30_DAY, + ) + tracker_7 = ExpirationReminderTracker( + compact='aslp', + provider_id=provider_id, + expiration_date='2026-02-16', + event_type=ExpirationEventType.PRIVILEGE_EXPIRATION_7_DAY, + ) + + self.assertTrue(tracker_30.was_already_sent()) + self.assertFalse(tracker_7.was_already_sent()) diff --git a/backend/compact-connect/lambdas/python/search/tests/unit/test_opensearch_client.py b/backend/compact-connect/lambdas/python/search/tests/unit/test_opensearch_client.py index 9678cfe75..f817579b3 100644 --- a/backend/compact-connect/lambdas/python/search/tests/unit/test_opensearch_client.py +++ b/backend/compact-connect/lambdas/python/search/tests/unit/test_opensearch_client.py @@ -2,7 +2,6 @@ from unittest import TestCase from unittest.mock import MagicMock, patch -from cc_common.exceptions import CCInternalException, CCInvalidRequestException from opensearchpy.exceptions import ConnectionTimeout, RequestError, TransportError @@ -146,6 +145,8 @@ def test_search_calls_internal_client_with_expected_arguments(self): def test_search_raises_cc_invalid_request_exception_on_400_request_error(self): """Test that search raises CCInvalidRequestException when OpenSearch returns a 400 RequestError.""" + from cc_common.exceptions import CCInvalidRequestException + client, mock_internal_client = self._create_client_with_mock() index_name = 'test_index' @@ -182,6 +183,8 @@ def test_search_raises_cc_invalid_request_exception_on_400_request_error(self): def test_search_raises_cc_invalid_request_exception_with_fallback_on_missing_root_cause(self): """Test that search falls back to error type when root_cause is missing.""" + from cc_common.exceptions import CCInvalidRequestException + client, mock_internal_client = self._create_client_with_mock() index_name = 'test_index' @@ -216,6 +219,8 @@ def test_search_reraises_non_400_request_error(self): def test_search_raises_cc_invalid_request_exception_on_timeout(self): """Test that search raises CCInvalidRequestException when the request times out.""" + from cc_common.exceptions import CCInvalidRequestException + client, mock_internal_client = self._create_client_with_mock() index_name = 'test_index' @@ -370,6 +375,7 @@ def test_bulk_index_raises_cc_internal_exception_after_max_retries(self, mock_sl @patch('opensearch_client.time.sleep') def test_bulk_index_exponential_backoff_caps_at_max(self, mock_sleep): """Test that exponential backoff is capped at MAX_BACKOFF_SECONDS.""" + from cc_common.exceptions import CCInternalException from opensearch_client import MAX_BACKOFF_SECONDS client, mock_internal_client = self._create_client_with_mock() @@ -433,6 +439,7 @@ def test_create_index_retries_on_connection_timeout_and_succeeds(self, mock_slee @patch('opensearch_client.time.sleep') def test_create_index_raises_after_max_retries(self, mock_sleep): """Test that create_index raises CCInternalException after max retries.""" + from cc_common.exceptions import CCInternalException from opensearch_client import MAX_RETRY_ATTEMPTS client, mock_internal_client = self._create_client_with_mock() @@ -516,6 +523,7 @@ def test_cluster_health_retries_on_connection_timeout_and_succeeds(self, mock_sl @patch('opensearch_client.time.sleep') def test_cluster_health_raises_after_max_retries(self, mock_sleep): """Test that cluster_health raises CCInternalException after max retries.""" + from cc_common.exceptions import CCInternalException from opensearch_client import MAX_RETRY_ATTEMPTS client, mock_internal_client = self._create_client_with_mock() diff --git a/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements-dev.txt b/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements-dev.txt index 3caa872c7..a24a50722 100644 --- a/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements-dev.txt @@ -4,9 +4,9 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/staff-user-pre-token/requirements-dev.in # -boto3==1.42.34 +boto3==1.42.35 # via moto -botocore==1.42.34 +botocore==1.42.35 # via # boto3 # moto diff --git a/backend/compact-connect/lambdas/python/staff-users/requirements-dev.txt b/backend/compact-connect/lambdas/python/staff-users/requirements-dev.txt index 5c8240b13..cb2cc8eb8 100644 --- a/backend/compact-connect/lambdas/python/staff-users/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/staff-users/requirements-dev.txt @@ -4,9 +4,9 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/staff-users/requirements-dev.in # -boto3==1.42.34 +boto3==1.42.35 # via moto -botocore==1.42.34 +botocore==1.42.35 # via # boto3 # moto From 8458af5b2b9d72b85c5ca92c79bbdf59010f8cff Mon Sep 17 00:00:00 2001 From: Justin Frahm Date: Tue, 27 Jan 2026 11:24:01 -0700 Subject: [PATCH 02/52] Add expiration reminder stack --- .../search/handlers/expiration_reminders.py | 29 ++- .../function/test_expiration_reminders.py | 42 ++++- .../compact-connect/pipeline/backend_stage.py | 38 ++-- .../expiration_reminder_stack/__init__.py | 176 ++++++++++++++++++ backend/compact-connect/tests/app/base.py | 1 + .../app/test_expiration_reminder_stack.py | 142 ++++++++++++++ 6 files changed, 406 insertions(+), 22 deletions(-) create mode 100644 backend/compact-connect/stacks/expiration_reminder_stack/__init__.py create mode 100644 backend/compact-connect/tests/app/test_expiration_reminder_stack.py diff --git a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py index 6aee5e41e..a1b5f55d7 100644 --- a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py @@ -2,7 +2,7 @@ from collections.abc import Generator from dataclasses import dataclass, replace -from datetime import date +from datetime import UTC, date, datetime, timedelta from uuid import UUID from aws_lambda_powertools.utilities.typing import LambdaContext @@ -60,23 +60,34 @@ def process_expiration_reminders(event: dict, context: LambdaContext): # noqa: Event format: { - "targetDate": "2026-02-16", # Expiration date to process - "daysBefore": 30, # Days before expiration (30, 7, or 0) - "scheduledTime": "2026-01-17..." # When rule triggered (for logging) + "daysBefore": 30, # Days before expiration (30, 7, or 0) - required + "targetDate": "2026-02-16", # Optional: Expiration date to process + # (if not provided, calculated as today + daysBefore) + "scheduledTime": "2026-01-17..." # Optional: When rule triggered (for logging, defaults to current time) } """ try: - target_date_str = event['targetDate'] days_before = event['daysBefore'] - scheduled_time = event['scheduledTime'] - except KeyError as e: - raise CCInvalidRequestException(f'Missing required field: {e.args[0]}') from e + except KeyError: + raise CCInvalidRequestException('Missing required field: daysBefore') from None if days_before not in DAYS_BEFORE_TO_EVENT_TYPE: raise CCInvalidRequestException(f'Invalid daysBefore value: {days_before}. Must be 30, 7, or 0.') + # Calculate targetDate if not provided (today + daysBefore) + if 'targetDate' in event: + target_date_str = event['targetDate'] + expiration_date = _parse_iso_date(target_date_str) + else: + # We will be running ~ midnight UTC-4, so now(UTC) should be a few hours into the next day + today = datetime.now(UTC).date() + expiration_date = today + timedelta(days=days_before) + target_date_str = expiration_date.isoformat() + + # Get scheduledTime for logging (default to current time if not provided) + scheduled_time = event.get('scheduledTime', datetime.now(UTC).isoformat()) + event_type = DAYS_BEFORE_TO_EVENT_TYPE[days_before] - expiration_date = _parse_iso_date(target_date_str) logger.info( 'Processing privilege expiration reminders', diff --git a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py index e0bef12b8..d6c8db5bc 100644 --- a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py @@ -1,4 +1,4 @@ -from datetime import date +from datetime import UTC, date, datetime, timedelta from unittest.mock import MagicMock, patch from uuid import uuid4 @@ -295,3 +295,43 @@ def test_handler_requires_days_before_field(self): ) self.assertIn('daysBefore', str(ctx.exception)) + + def test_handler_calculates_target_date_from_days_before_when_not_provided(self): + """Test that handler calculates targetDate from daysBefore when targetDate is not provided.""" + + with ( + patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') as mock_iter, + patch('handlers.expiration_reminders.config') as mock_config, + patch('handlers.expiration_reminders.ExpirationReminderTracker') as mock_tracker_class, + ): + mock_config.compacts = ['aslp'] + mock_email_client = MagicMock() + mock_config.email_service_client = mock_email_client + + mock_tracker_instance = MagicMock() + mock_tracker_instance.was_already_sent.return_value = False + mock_tracker_class.return_value = mock_tracker_instance + + # Create provider doc with expiration date = today + 30 days + today = datetime.now(UTC).date() + target_date = today + timedelta(days=30) + provider_doc = self._make_provider_doc() + provider_doc['privileges'][0]['dateOfExpiration'] = target_date.isoformat() + + mock_iter.return_value = iter([provider_doc]) + + from handlers.expiration_reminders import process_expiration_reminders + + # Event with only daysBefore (no targetDate) + event = {'daysBefore': 30} + resp = process_expiration_reminders(event, self.mock_context) + + # Verify it calculated the correct target date + expected_target_date = (today + timedelta(days=30)).isoformat() + self.assertEqual(resp['targetDate'], expected_target_date) + self.assertEqual(resp['daysBefore'], 30) + + # Verify the generator was called with the calculated date + mock_iter.assert_called_once() + call_kwargs = mock_iter.call_args.kwargs + self.assertEqual(call_kwargs['expiration_date'], target_date) diff --git a/backend/compact-connect/pipeline/backend_stage.py b/backend/compact-connect/pipeline/backend_stage.py index b53a8b7c4..cb2fd7059 100644 --- a/backend/compact-connect/pipeline/backend_stage.py +++ b/backend/compact-connect/pipeline/backend_stage.py @@ -7,6 +7,7 @@ from stacks.disaster_recovery_stack import DisasterRecoveryStack from stacks.event_listener_stack import EventListenerStack from stacks.event_state_stack import EventStateStack +from stacks.expiration_reminder_stack import ExpirationReminderStack from stacks.feature_flag_stack import FeatureFlagStack from stacks.ingest_stack import IngestStack from stacks.managed_login_stack import ManagedLoginStack @@ -162,6 +163,18 @@ def __init__( persistent_stack=self.persistent_stack, ) + # Search Persistent Stack - OpenSearch Domain for advanced provider search + self.search_persistent_stack = SearchPersistentStack( + self, + 'SearchPersistentStack', + env=environment, + environment_context=environment_context, + standard_tags=standard_tags, + environment_name=environment_name, + vpc_stack=self.vpc_stack, + persistent_stack=self.persistent_stack, + ) + # Reporting and notifications depend on emails, which depend on having a domain name. If we don't configure # a HostedZone we won't bother with these whole stacks. if self.persistent_stack.hosted_zone: @@ -186,6 +199,19 @@ def __init__( persistent_stack=self.persistent_stack, ) + self.expiration_reminder_stack = ExpirationReminderStack( + self, + 'ExpirationReminderStack', + env=environment, + environment_context=environment_context, + standard_tags=standard_tags, + environment_name=environment_name, + persistent_stack=self.persistent_stack, + event_state_stack=self.event_state_stack, + search_persistent_stack=self.search_persistent_stack, + vpc_stack=self.vpc_stack, + ) + self.transaction_monitoring_stack = TransactionMonitoringStack( self, 'TransactionMonitoringStack', @@ -217,18 +243,6 @@ def __init__( standard_tags=standard_tags, ) - # Search Persistent Stack - OpenSearch Domain for advanced provider search - self.search_persistent_stack = SearchPersistentStack( - self, - 'SearchPersistentStack', - env=environment, - environment_context=environment_context, - standard_tags=standard_tags, - environment_name=environment_name, - vpc_stack=self.vpc_stack, - persistent_stack=self.persistent_stack, - ) - self.search_api_stack = SearchApiStack( self, 'SearchAPIStack', diff --git a/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py b/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py new file mode 100644 index 000000000..eb8ee8fcb --- /dev/null +++ b/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py @@ -0,0 +1,176 @@ +import os + +from aws_cdk import Duration +from aws_cdk.aws_cloudwatch import Alarm, ComparisonOperator, Stats, TreatMissingData +from aws_cdk.aws_cloudwatch_actions import SnsAction +from aws_cdk.aws_ec2 import SubnetSelection +from aws_cdk.aws_events import Rule, RuleTargetInput, Schedule +from aws_cdk.aws_events_targets import LambdaFunction +from aws_cdk.aws_logs import QueryDefinition, QueryString, RetentionDays +from cdk_nag import NagSuppressions +from common_constructs.stack import AppStack +from constructs import Construct + +from common_constructs.python_function import PythonFunction +from stacks import event_state_stack as ess +from stacks import persistent_stack as ps +from stacks import search_persistent_stack as sps +from stacks.vpc_stack import VpcStack + + +class ExpirationReminderStack(AppStack): + """ + Stack for privilege expiration reminder notifications. + + This stack provides scheduled email notifications to providers about expiring privileges: + - Lambda function for processing expiration reminders + - Three EventBridge rules (30-day, 7-day, day-of) that run daily + - CloudWatch alarms for errors and execution duration + - OpenSearch integration for querying privileges by expiration date + """ + + def __init__( + self, + scope: Construct, + construct_id: str, + *, + environment_name: str, + persistent_stack: ps.PersistentStack, + event_state_stack: ess.EventStateStack, + search_persistent_stack: sps.SearchPersistentStack, + vpc_stack: VpcStack, + **kwargs, + ): + super().__init__(scope, construct_id, environment_name=environment_name, **kwargs) + + # Create Lambda function for processing expiration reminders + self.expiration_reminder_handler = PythonFunction( + self, + 'ExpirationReminderHandler', + description='Processes privilege expiration reminders and sends email notifications', + index=os.path.join('handlers', 'expiration_reminders.py'), + lambda_dir='search', + handler='process_expiration_reminders', + timeout=Duration.minutes(15), # 15-minute timeout to handle all providers in single execution + memory_size=2048, # Higher memory for processing large result sets + log_retention=RetentionDays.ONE_MONTH, + environment={ + 'OPENSEARCH_HOST_ENDPOINT': search_persistent_stack.domain.domain_endpoint, + 'EMAIL_NOTIFICATION_SERVICE_LAMBDA_NAME': ( + persistent_stack.email_notification_service_lambda.function_name + ), + 'EVENT_STATE_TABLE_NAME': event_state_stack.event_state_table.table_name, + **self.common_env_vars, + }, + vpc=vpc_stack.vpc, + vpc_subnets=SubnetSelection(subnets=search_persistent_stack.provider_search_domain.vpc_subnets.subnets), + security_groups=[vpc_stack.lambda_security_group], + alarm_topic=persistent_stack.alarm_topic, + ) + + # Grant necessary permissions + # Read access to OpenSearch domain for querying privileges + search_persistent_stack.domain.grant_read(self.expiration_reminder_handler) + + # Read/write access to EventStateTable for idempotency tracking + event_state_stack.event_state_table.grant_read_write_data(self.expiration_reminder_handler) + + # Invoke permission for email notification service + persistent_stack.email_notification_service_lambda.grant_invoke(self.expiration_reminder_handler) + + # Add CDK Nag suppressions for the Lambda function's IAM role + NagSuppressions.add_resource_suppressions_by_path( + self, + f'{self.expiration_reminder_handler.role.node.path}/DefaultPolicy/Resource', + [ + { + 'id': 'AwsSolutions-IAM5', + 'reason': 'The grant_read method requires wildcard permissions on the OpenSearch domain to ' + 'read from indices. This is appropriate for a function that needs to query ' + 'provider indices by expiration date. Additionally, grant_read_write_data on the ' + 'EventStateTable uses wildcard permissions for item-level operations which is required ' + 'for tracking notification state.', + }, + ], + ) + + # Create EventBridge rules for each reminder type + # All rules run daily at midnight UTC-4 (4:00 AM UTC) to process reminders for privileges expiring on the + # calculated target date + Rule( + self, + 'ExpirationReminder30DayRule', + description='Daily rule to send 30-day expiration reminders', + schedule=Schedule.cron(week_day='*', hour='4', minute='0', month='*', year='*'), + targets=[ + LambdaFunction( + handler=self.expiration_reminder_handler, + event=RuleTargetInput.from_object({'daysBefore': 30}), + ) + ], + ) + + Rule( + self, + 'ExpirationReminder7DayRule', + description='Daily rule to send 7-day expiration reminders', + schedule=Schedule.cron(week_day='*', hour='4', minute='0', month='*', year='*'), + targets=[ + LambdaFunction( + handler=self.expiration_reminder_handler, + event=RuleTargetInput.from_object({'daysBefore': 7}), + ) + ], + ) + + Rule( + self, + 'ExpirationReminderDayOfRule', + description='Daily rule to send day-of expiration reminders', + schedule=Schedule.cron(week_day='*', hour='4', minute='0', month='*', year='*'), + targets=[ + LambdaFunction( + handler=self.expiration_reminder_handler, + event=RuleTargetInput.from_object({'daysBefore': 0}), + ) + ], + ) + + # CloudWatch alarm for Lambda errors + Alarm( + self, + 'ExpirationReminderErrorAlarm', + metric=self.expiration_reminder_handler.metric_errors(statistic=Stats.SUM), + evaluation_periods=1, + threshold=1, + actions_enabled=True, + alarm_description=f'{self.expiration_reminder_handler.node.path} failed to process expiration reminders', + comparison_operator=ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + treat_missing_data=TreatMissingData.NOT_BREACHING, + ).add_alarm_action(SnsAction(persistent_stack.alarm_topic)) + + # CloudWatch alarm for Lambda execution duration (triggers if exceeds 10 minutes) + Alarm( + self, + 'ExpirationReminderDurationAlarm', + metric=self.expiration_reminder_handler.metric_duration(statistic=Stats.MAXIMUM, period=Duration.days(1)), + evaluation_periods=1, + threshold=600_000, # 10 minutes in milliseconds + actions_enabled=True, + alarm_description=f'{self.expiration_reminder_handler.node.path} Lambda Duration exceeded 10 minutes', + comparison_operator=ComparisonOperator.GREATER_THAN_THRESHOLD, + treat_missing_data=TreatMissingData.NOT_BREACHING, + ).add_alarm_action(SnsAction(persistent_stack.alarm_topic)) + + # CloudWatch Logs Insights query definition + QueryDefinition( + self, + 'ExpirationReminderQuery', + query_definition_name=f'{self.node.id}/ExpirationReminderHandler', + query_string=QueryString( + fields=['@timestamp', '@log', 'level', 'message', 'compact', 'provider_id', 'event_type', '@message'], + filter_statements=['level in ["INFO", "WARNING", "ERROR"]'], + sort='@timestamp desc', + ), + log_groups=[self.expiration_reminder_handler.log_group], + ) diff --git a/backend/compact-connect/tests/app/base.py b/backend/compact-connect/tests/app/base.py index 12ff282ef..351ddcb86 100644 --- a/backend/compact-connect/tests/app/base.py +++ b/backend/compact-connect/tests/app/base.py @@ -579,6 +579,7 @@ def _check_no_backend_stage_annotations(self, stage: BackendStage): if stage.persistent_stack.hosted_zone: self._check_no_stack_annotations(stage.notification_stack) self._check_no_stack_annotations(stage.reporting_stack) + self._check_no_stack_annotations(stage.expiration_reminder_stack) # No backup stack here, because nexted stack annotations are checked in the parent stack def _count_stack_resources(self, stack: Stack) -> int: diff --git a/backend/compact-connect/tests/app/test_expiration_reminder_stack.py b/backend/compact-connect/tests/app/test_expiration_reminder_stack.py new file mode 100644 index 000000000..6da7b1fa6 --- /dev/null +++ b/backend/compact-connect/tests/app/test_expiration_reminder_stack.py @@ -0,0 +1,142 @@ +import json +from unittest import TestCase + +from aws_cdk.assertions import Template +from aws_cdk.aws_cloudwatch import CfnAlarm +from aws_cdk.aws_events import CfnRule +from aws_cdk.aws_lambda import CfnFunction + +from tests.app.base import TstAppABC + + +class TestExpirationReminderStack(TstAppABC, TestCase): + """ + Test cases for the ExpirationReminderStack to ensure proper resource configuration + for privilege expiration reminder notifications. + """ + + @classmethod + def get_context(cls): + with open('cdk.json') as f: + context = json.load(f)['context'] + with open('cdk.context.sandbox-example.json') as f: + context.update(json.load(f)) + + # Suppresses lambda bundling for tests + context['aws:cdk:bundling-stacks'] = [] + return context + + def test_lambda_function_created_with_correct_timeout(self): + """Test that the Lambda function is created with a 15-minute timeout.""" + # Stack is only created if hosted_zone is configured + if not hasattr(self.app.sandbox_backend_stage, 'expiration_reminder_stack'): + self.skipTest('ExpirationReminderStack not created (hosted_zone not configured)') + + expiration_stack = self.app.sandbox_backend_stage.expiration_reminder_stack + expiration_template = Template.from_stack(expiration_stack) + + # Verify the lambda function is created + handler_logical_id = expiration_stack.get_logical_id( + expiration_stack.expiration_reminder_handler.node.default_child + ) + handler_properties = TestExpirationReminderStack.get_resource_properties_by_logical_id( + handler_logical_id, + resources=expiration_template.find_resources(CfnFunction.CFN_RESOURCE_TYPE_NAME), + ) + + # Verify timeout is 15 minutes (900 seconds) + self.assertEqual(handler_properties['Timeout'], 900) + + # Verify handler is correct + self.assertEqual(handler_properties['Handler'], 'handlers.expiration_reminders.process_expiration_reminders') + + def test_eventbridge_rules_created(self): + """Test that all three EventBridge rules (30-day, 7-day, day-of) are created.""" + # Stack is only created if hosted_zone is configured + if not hasattr(self.app.sandbox_backend_stage, 'expiration_reminder_stack'): + self.skipTest('ExpirationReminderStack not created (hosted_zone not configured)') + + expiration_stack = self.app.sandbox_backend_stage.expiration_reminder_stack + expiration_template = Template.from_stack(expiration_stack) + + # Get all EventBridge rules + rules = expiration_template.find_resources(CfnRule.CFN_RESOURCE_TYPE_NAME) + + # Verify we have exactly 3 rules + self.assertEqual(len(rules), 3, 'Should have exactly 3 EventBridge rules') + + # Verify all rules have the same schedule (daily at 10:00 AM UTC) and are enabled + handler_logical_id = expiration_stack.get_logical_id( + expiration_stack.expiration_reminder_handler.node.default_child + ) + + for rule_name in ['ExpirationReminder30DayRule', 'ExpirationReminder7DayRule', 'ExpirationReminderDayOfRule']: + rule_logical_id = expiration_stack.get_logical_id( + expiration_stack.node.find_child(rule_name).node.default_child + ) + rule = TestExpirationReminderStack.get_resource_properties_by_logical_id(rule_logical_id, resources=rules) + + self.assertEqual(rule['ScheduleExpression'], 'cron(0 4 ? * * *)') # Daily at midnight UTC-4 (4:00 AM UTC) + self.assertEqual(rule['State'], 'ENABLED') + # Verify the rule targets the Lambda function + # The Arn is a GetAtt reference to the Lambda function + target_arn = rule['Targets'][0]['Arn'] + if isinstance(target_arn, dict) and 'Fn::GetAtt' in target_arn: + self.assertEqual(target_arn['Fn::GetAtt'][0], handler_logical_id) + else: + # Fallback: just verify the target exists + self.assertIn('Arn', rule['Targets'][0]) + + def test_duration_alarm_configured(self): + """Test that the duration alarm is configured with a 10-minute threshold.""" + # Stack is only created if hosted_zone is configured + if not hasattr(self.app.sandbox_backend_stage, 'expiration_reminder_stack'): + self.skipTest('ExpirationReminderStack not created (hosted_zone not configured)') + + expiration_stack = self.app.sandbox_backend_stage.expiration_reminder_stack + expiration_template = Template.from_stack(expiration_stack) + + # Get all CloudWatch alarms + alarms = expiration_template.find_resources(CfnAlarm.CFN_RESOURCE_TYPE_NAME) + + # Find the duration alarm + duration_alarm_logical_id = expiration_stack.get_logical_id( + expiration_stack.node.find_child('ExpirationReminderDurationAlarm').node.default_child + ) + duration_alarm = TestExpirationReminderStack.get_resource_properties_by_logical_id( + duration_alarm_logical_id, resources=alarms + ) + + # Verify threshold is 10 minutes (600,000 milliseconds) + self.assertEqual(duration_alarm['Threshold'], 600_000) + self.assertEqual(duration_alarm['ComparisonOperator'], 'GreaterThanThreshold') + self.assertEqual(duration_alarm['EvaluationPeriods'], 1) + self.assertEqual(duration_alarm['MetricName'], 'Duration') + self.assertEqual(duration_alarm['Statistic'], 'Maximum') + + def test_error_alarm_configured(self): + """Test that the error alarm is configured.""" + # Stack is only created if hosted_zone is configured + if not hasattr(self.app.sandbox_backend_stage, 'expiration_reminder_stack'): + self.skipTest('ExpirationReminderStack not created (hosted_zone not configured)') + + expiration_stack = self.app.sandbox_backend_stage.expiration_reminder_stack + expiration_template = Template.from_stack(expiration_stack) + + # Get all CloudWatch alarms + alarms = expiration_template.find_resources(CfnAlarm.CFN_RESOURCE_TYPE_NAME) + + # Find the error alarm + error_alarm_logical_id = expiration_stack.get_logical_id( + expiration_stack.node.find_child('ExpirationReminderErrorAlarm').node.default_child + ) + error_alarm = TestExpirationReminderStack.get_resource_properties_by_logical_id( + error_alarm_logical_id, resources=alarms + ) + + # Verify error alarm configuration + self.assertEqual(error_alarm['Threshold'], 1) + self.assertEqual(error_alarm['ComparisonOperator'], 'GreaterThanOrEqualToThreshold') + self.assertEqual(error_alarm['EvaluationPeriods'], 1) + self.assertEqual(error_alarm['MetricName'], 'Errors') + self.assertEqual(error_alarm['Statistic'], 'Sum') From 60aa657c00fd136f3e095f3eefc55dfc3b466c3c Mon Sep 17 00:00:00 2001 From: Justin Frahm Date: Wed, 28 Jan 2026 14:18:35 -0700 Subject: [PATCH 03/52] Smoke test notifications --- .../common/cc_common/email_service_client.py | 4 +- .../search/handlers/expiration_reminders.py | 20 + .../expiration_reminder_stack/__init__.py | 27 +- .../stacks/vpc_stack/__init__.py | 33 +- .../smoke/expiration_reminder_load_test.py | 569 ++++++++++++++++++ 5 files changed, 632 insertions(+), 21 deletions(-) create mode 100755 backend/compact-connect/tests/smoke/expiration_reminder_load_test.py diff --git a/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py b/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py index 778a164ea..d5803003a 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py @@ -100,8 +100,8 @@ def _invoke_lambda(self, payload: dict[str, Any]) -> dict[str, Any]: ) if response.get('FunctionError'): - error_message = f'Failed to send email notification: {response.get("FunctionError")}' - self._logger.error(error_message, payload=payload) + error_message = 'Failed to send email notification' + self._logger.error(error_message, payload=payload, response=response) raise CCInternalException(error_message) return response diff --git a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py index a1b5f55d7..7f47b15e8 100644 --- a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py @@ -100,6 +100,9 @@ def process_expiration_reminders(event: dict, context: LambdaContext): # noqa: metrics = Metrics() for compact in config.compacts: + compact_provider_count = 0 + logger.info('Starting processing for compact', compact=compact, target_date=target_date_str) + for provider_doc in iterate_privileges_by_expiration_date( compact=compact, expiration_date=expiration_date, @@ -111,6 +114,7 @@ def process_expiration_reminders(event: dict, context: LambdaContext): # noqa: if not matched_privileges: continue + compact_provider_count += 1 metrics = replace( metrics, matched_privileges=metrics.matched_privileges + len(matched_privileges), @@ -134,6 +138,22 @@ def process_expiration_reminders(event: dict, context: LambdaContext): # noqa: no_email=metrics.no_email + result['no_email'], ) + # Log progress every 100 providers per compact + if compact_provider_count % 100 == 0: + logger.info( + 'Progress update', + compact=compact, + providers_processed=compact_provider_count, + metrics=metrics.as_dict(), + ) + + logger.info( + 'Completed processing for compact', + compact=compact, + total_providers_processed=compact_provider_count, + metrics=metrics.as_dict(), + ) + logger.info('Completed processing expiration reminders', metrics=metrics.as_dict()) return {'targetDate': target_date_str, 'daysBefore': days_before, 'metrics': metrics.as_dict()} diff --git a/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py b/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py index eb8ee8fcb..65bd4aa19 100644 --- a/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py +++ b/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py @@ -7,7 +7,6 @@ from aws_cdk.aws_events import Rule, RuleTargetInput, Schedule from aws_cdk.aws_events_targets import LambdaFunction from aws_cdk.aws_logs import QueryDefinition, QueryString, RetentionDays -from cdk_nag import NagSuppressions from common_constructs.stack import AppStack from constructs import Construct @@ -44,6 +43,8 @@ def __init__( super().__init__(scope, construct_id, environment_name=environment_name, **kwargs) # Create Lambda function for processing expiration reminders + # Use the search_api_lambda_role which already has OpenSearch read access configured + # in the domain's resource-based access policies self.expiration_reminder_handler = PythonFunction( self, 'ExpirationReminderHandler', @@ -51,6 +52,7 @@ def __init__( index=os.path.join('handlers', 'expiration_reminders.py'), lambda_dir='search', handler='process_expiration_reminders', + role=search_persistent_stack.search_api_lambda_role, timeout=Duration.minutes(15), # 15-minute timeout to handle all providers in single execution memory_size=2048, # Higher memory for processing large result sets log_retention=RetentionDays.ONE_MONTH, @@ -69,8 +71,8 @@ def __init__( ) # Grant necessary permissions - # Read access to OpenSearch domain for querying privileges - search_persistent_stack.domain.grant_read(self.expiration_reminder_handler) + # OpenSearch access is already configured via search_api_lambda_role which is included + # in the domain's resource-based access policies # Read/write access to EventStateTable for idempotency tracking event_state_stack.event_state_table.grant_read_write_data(self.expiration_reminder_handler) @@ -78,21 +80,10 @@ def __init__( # Invoke permission for email notification service persistent_stack.email_notification_service_lambda.grant_invoke(self.expiration_reminder_handler) - # Add CDK Nag suppressions for the Lambda function's IAM role - NagSuppressions.add_resource_suppressions_by_path( - self, - f'{self.expiration_reminder_handler.role.node.path}/DefaultPolicy/Resource', - [ - { - 'id': 'AwsSolutions-IAM5', - 'reason': 'The grant_read method requires wildcard permissions on the OpenSearch domain to ' - 'read from indices. This is appropriate for a function that needs to query ' - 'provider indices by expiration date. Additionally, grant_read_write_data on the ' - 'EventStateTable uses wildcard permissions for item-level operations which is required ' - 'for tracking notification state.', - }, - ], - ) + # Note: Since we're using the shared search_api_lambda_role, NagSuppressions for OpenSearch + # permissions are already applied in SearchPersistentStack. The EventStateTable permissions + # added by grant_read_write_data use wildcard permissions which is expected for DynamoDB + # item-level operations and is consistent with other Lambda functions in the codebase. # Create EventBridge rules for each reminder type # All rules run daily at midnight UTC-4 (4:00 AM UTC) to process reminders for privileges expiring on the diff --git a/backend/compact-connect/stacks/vpc_stack/__init__.py b/backend/compact-connect/stacks/vpc_stack/__init__.py index 650889e9f..78e2320ee 100644 --- a/backend/compact-connect/stacks/vpc_stack/__init__.py +++ b/backend/compact-connect/stacks/vpc_stack/__init__.py @@ -29,7 +29,7 @@ class VpcStack(AppStack): This stack provides network infrastructure including: - VPC with private subnets across multiple availability zones - - VPC endpoints for AWS services (CloudWatch Logs, DynamoDB) + - VPC endpoints for AWS services (CloudWatch Logs, Lambda, DynamoDB, S3) - Security groups for OpenSearch and Lambda functions - VPC Flow Logs for network monitoring @@ -161,6 +161,37 @@ def __init__( apply_to_children=True, ) + # VPC Endpoint for Lambda + # This allows Lambda functions in the VPC to invoke other Lambda functions without internet access + self.lambda_vpc_endpoint = self.vpc.add_interface_endpoint( + 'LambdaVpcEndpoint', + service=InterfaceVpcEndpointAwsService.LAMBDA_, + ) + + # Suppress CdkNag warnings for the Lambda VPC endpoint security group + NagSuppressions.add_resource_suppressions_by_path( + self, + path=self.lambda_vpc_endpoint.node.path, + suppressions=[ + { + 'id': 'AwsSolutions-EC23', + 'reason': 'VPC endpoint security groups are automatically managed by CDK. Inbound rules are ' + 'appropriately restricted to HTTPS (port 443) from VPC CIDR block.', + }, + { + 'id': 'HIPAA.Security-EC2RestrictedCommonPorts', + 'reason': 'VPC endpoint security groups are automatically managed by CDK. Only HTTPS (port 443) ' + 'is allowed for Lambda service communication.', + }, + { + 'id': 'HIPAA.Security-EC2RestrictedSSH', + 'reason': 'VPC endpoint security groups are automatically managed by CDK. SSH is not enabled on ' + 'this security group.', + }, + ], + apply_to_children=True, + ) + # VPC Endpoint for DynamoDB # This allows Lambda functions to access DynamoDB without internet access self.dynamodb_vpc_endpoint = self.vpc.add_gateway_endpoint( diff --git a/backend/compact-connect/tests/smoke/expiration_reminder_load_test.py b/backend/compact-connect/tests/smoke/expiration_reminder_load_test.py new file mode 100755 index 000000000..8a86adf95 --- /dev/null +++ b/backend/compact-connect/tests/smoke/expiration_reminder_load_test.py @@ -0,0 +1,569 @@ +#!/usr/bin/env python3 +""" +Load test for privilege expiration reminder notifications. + +This script creates a large number of providers with privileges expiring at specific dates, +indexes them into OpenSearch, and triggers the expiration reminder Lambda to test its +performance and capacity. + +Usage: + python expiration_reminder_load_test.py + python expiration_reminder_load_test.py --skip-data-load + +The script will: +1. Create 20,000 providers with privileges expiring in 30 days (unless --skip-data-load) +2. Create 10,000 providers with privileges expiring in 10 days (unless --skip-data-load) +3. Wait for DynamoDB stream events to index providers into OpenSearch (unless --skip-data-load) +4. Invoke the expiration reminder Lambda for the 30-day event +5. Display metrics from the Lambda execution + +Options: + --skip-data-load Skip creating providers and indexing steps. Use existing data in the database. + Useful for re-running the Lambda invocation test without recreating all providers. +""" + +import argparse +import json +import os +import sys +import time +import uuid +from datetime import UTC, date, datetime, timedelta + +import boto3 +from botocore.exceptions import ClientError +from smoke_common import ( + COMPACTS, + JURISDICTIONS, + LICENSE_TYPES, + SmokeTestFailureException, + config, + get_license_type_abbreviation, + load_smoke_test_env, + logger, +) + +# Load environment variables +load_smoke_test_env() + +# Add common lib path for test data generator (similar to smoke_common.py) +common_lib_path = os.path.join('lambdas', 'python', 'common') +sys.path.append(common_lib_path) +from common_test.test_data_generator import TestDataGenerator # noqa: E402 + +# Initialize AWS clients +lambda_client = boto3.client('lambda') +logs_client = boto3.client('logs') +dynamodb_table = config.provider_user_dynamodb_table + +# Test configuration +PROVIDERS_30_DAYS = 20_000 +PROVIDERS_10_DAYS = 10_000 +COMPACT = COMPACTS[0] # Use first compact +JURISDICTION = JURISDICTIONS[0] # Use first jurisdiction +# LICENSE_TYPES is a dict keyed by compact, get the first license type for the compact +if COMPACT in LICENSE_TYPES and LICENSE_TYPES[COMPACT]: + LICENSE_TYPE = LICENSE_TYPES[COMPACT][0]['name'] +else: + raise SmokeTestFailureException(f'No license types found for compact {COMPACT}') + + +class DynamoDBBatchWriter: + """Utility class to batch DynamoDB put_item operations for better efficiency.""" + + def __init__(self, table, batch_size: int = 25): + """ + Initialize the batch writer. + + :param table: DynamoDB table resource (boto3 resource Table) + :param batch_size: Batch size to use for API calls, default: 25 (DynamoDB max) + """ + self._table = table + self._batch_size = batch_size + self._batch = None + self._count = 0 + self.failed_item_count = 0 + self.failed_items = None + + def _do_batch_write(self): + """Execute the batch write operation.""" + # DynamoDB batch_write_item has a hard limit of 25 items per request + # Slice out exactly 25 items (or fewer if batch is smaller) and keep remainder + max_items_per_request = 25 + items_to_write = self._batch[:max_items_per_request] + remaining_items = self._batch[max_items_per_request:] + + if not items_to_write: + return + + # DynamoDB batch_write_item requires a dict keyed by table name + table_name = self._table.name + response = self._table.meta.client.batch_write_item( + RequestItems={ + table_name: [ + {'PutRequest': {'Item': item}} for item in items_to_write + ] + } + ) + + # Check for unprocessed items (shouldn't happen with proper batch sizing, but handle it) + unprocessed = response.get('UnprocessedItems', {}) + if unprocessed: + unprocessed_count = len(unprocessed.get(table_name, [])) + self.failed_item_count += unprocessed_count + logger.warning(f'Unprocessed items in batch write: {unprocessed_count}') + + # Keep remaining items for next batch write + self._batch = remaining_items + self._count = len(remaining_items) + + def __enter__(self): + self._batch = [] + self._count = 0 + self.failed_items = [] + self.failed_item_count = 0 + return self + + def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): + # Flush any remaining items (may require multiple calls if > 25 items) + while len(self._batch) > 0: + self._do_batch_write() + if exc_val is not None: + raise exc_val + + def put_item(self, item: dict): + """ + Add an item to the batch. Will automatically flush when batch size is reached. + + :param item: Dictionary representing the DynamoDB item to write + """ + if self._batch is None: + raise RuntimeError('This object must be used as a context manager') + self._batch.append(item) + self._count += 1 + if self._count >= self._batch_size: + self._do_batch_write() + + +def create_provider_records( + provider_id: str, + compact: str, + jurisdiction: str, + license_jurisdiction: str, + license_type: str, + expiration_date: date, + email: str, +) -> tuple[dict, dict, dict]: + """ + Create provider, license, and privilege record dictionaries (without writing to DynamoDB). + + Uses TestDataGenerator to ensure records match current schema requirements. + + :param provider_id: The provider's UUID + :param compact: The compact abbreviation + :param jurisdiction: The privilege jurisdiction + :param license_jurisdiction: The license jurisdiction (home state) + :param license_type: The license type + :param expiration_date: The privilege expiration date + :param email: The provider's email address + :return: Tuple of (provider_record, license_record, privilege_record) + """ + license_type_abbr = get_license_type_abbreviation(license_type) + if not license_type_abbr: + raise SmokeTestFailureException(f'Could not find abbreviation for license type: {license_type}') + + now = datetime.now(tz=UTC) + transaction_id = str(uuid.uuid4()) + given_name = f'TestProvider{provider_id[:8]}' + family_name = 'LoadTest' + + # Generate provider record using TestDataGenerator + provider_data = TestDataGenerator.generate_default_provider( + value_overrides={ + 'providerId': provider_id, + 'compact': compact, + 'licenseJurisdiction': license_jurisdiction, + 'privilegeJurisdictions': {jurisdiction}, + 'givenName': given_name, + 'familyName': family_name, + 'compactConnectRegisteredEmailAddress': email, + 'dateOfExpiration': expiration_date, + 'currentHomeJurisdiction': license_jurisdiction, + }, + is_registered=True, + ) + provider_record = provider_data.serialize_to_database_record() + + # Generate license record using TestDataGenerator + license_data = TestDataGenerator.generate_default_license( + value_overrides={ + 'providerId': provider_id, + 'compact': compact, + 'jurisdiction': license_jurisdiction, + 'licenseType': license_type, + 'givenName': given_name, + 'familyName': family_name, + 'emailAddress': email, + 'homeAddressState': license_jurisdiction, + } + ) + license_record = license_data.serialize_to_database_record() + + # Generate privilege record using TestDataGenerator + privilege_data = TestDataGenerator.generate_default_privilege( + value_overrides={ + 'providerId': provider_id, + 'compact': compact, + 'jurisdiction': jurisdiction, + 'licenseJurisdiction': license_jurisdiction, + 'licenseType': license_type, + 'dateOfExpiration': expiration_date, + 'dateOfIssuance': now, + 'dateOfRenewal': now, + 'dateOfUpdate': now, + 'compactTransactionId': transaction_id, + 'compactTransactionIdGSIPK': f'COMPACT#{compact}#TX#{transaction_id}#', + 'privilegeId': f'{license_type_abbr.upper()}-{jurisdiction.upper()}-{provider_id[:8]}', + 'administratorSetStatus': 'active', + 'attestations': [], + } + ) + privilege_record = privilege_data.serialize_to_database_record() + + return provider_record, license_record, privilege_record + + +def create_providers_batch(num_providers: int, days_until_expiration: int, progress_log_interval: int = 1000): + """ + Create a batch of providers with privileges expiring in the specified number of days. + + Uses DynamoDB batch_write_item for efficient bulk writes. + The first provider will use the registered email address from smoke_tests_env.json + to allow validation that emails are being sent. + + :param num_providers: Total number of providers to create + :param days_until_expiration: Number of days until privilege expiration + :param progress_log_interval: Interval for progress logging (number of providers) + """ + expiration_date = datetime.now(UTC).date() + timedelta(days=days_until_expiration) + expiration_date_str = expiration_date.isoformat() + logger.info( + f'Creating {num_providers} providers with privileges expiring in {days_until_expiration} days ' + f'(expiration date: {expiration_date_str})', + expiration_date=expiration_date_str, + ) + + # Get the registered email address for validation + registered_email = config.test_provider_user_username + logger.info( + f'Using registered email for first provider: {registered_email}', + registered_email=registered_email, + ) + + created_count = 0 + + # Use batch writer for efficient DynamoDB writes + # Each provider creates 3 items (provider, license, privilege) + # DynamoDB batch_write_item supports up to 25 items per request + # So we'll use batch_size=24 (8 providers * 3 items = 24 items per batch) + with DynamoDBBatchWriter(dynamodb_table, batch_size=24) as batch_writer: + for i in range(num_providers): + provider_id = str(uuid.uuid4()) + # Use registered email for the first provider to validate email delivery + if i == 0: + email = registered_email + else: + email = f'loadtest-{days_until_expiration}days-{provider_id[:8]}@example.com' + + provider_record, license_record, privilege_record = create_provider_records( + provider_id=provider_id, + compact=COMPACT, + jurisdiction=JURISDICTION, + license_jurisdiction=JURISDICTION, + license_type=LICENSE_TYPE, + expiration_date=expiration_date, + email=email, + ) + + # Add all three records to the batch + batch_writer.put_item(provider_record) + batch_writer.put_item(license_record) + batch_writer.put_item(privilege_record) + + created_count += 1 + if created_count % progress_log_interval == 0: + logger.info( + f'Created {created_count}/{num_providers} providers for {days_until_expiration}-day expiration ' + f'(expiration date: {expiration_date_str})' + ) + + if batch_writer.failed_item_count > 0: + logger.warning(f'Failed to write {batch_writer.failed_item_count} items during batch write') + + logger.info( + f'Completed creating {num_providers} providers for {days_until_expiration}-day expiration ' + f'(expiration date: {expiration_date_str})' + ) + + +def find_lambda_function_name(partial_name: str) -> str: + """ + Find a Lambda function by partial name match. + + :param partial_name: Partial function name to search for + :return: Full Lambda function name + :raises SmokeTestFailureException: If function not found + """ + logger.info(f'Searching for Lambda function containing: {partial_name}') + try: + paginator = lambda_client.get_paginator('list_functions') + for page in paginator.paginate(): + for func in page['Functions']: + if partial_name.lower() in func['FunctionName'].lower(): + function_name = func['FunctionName'] + logger.info(f'Found Lambda function: {function_name}') + return function_name + + raise SmokeTestFailureException( + f'Lambda function containing "{partial_name}" not found. ' + ) + except ClientError as e: + raise SmokeTestFailureException(f'Failed to list Lambda functions: {str(e)}') from e + + +def invoke_expiration_reminder_lambda(days_before: int): + """ + Invoke the expiration reminder Lambda asynchronously and poll CloudWatch Logs for completion. + + Uses async invocation to avoid keeping a TCP connection open for the entire execution duration. + Polls CloudWatch Logs to find the completion message with metrics. + + :param days_before: Days before expiration (30, 7, or 0) + :return: Dict containing targetDate, daysBefore, and metrics + """ + # Search for "ExpirationReminder" since CDK truncates function names + lambda_name = find_lambda_function_name('ExpirationReminder') + + # Get the Lambda function details to find the log group name + try: + function_response = lambda_client.get_function(FunctionName=lambda_name) + configuration = function_response['Configuration'] + + # Extract log group name from the function configuration + # The log group name is available in the LoggingConfig.LogGroup field (as ARN) + logging_config = configuration.get('LoggingConfig', {}) + log_group_arn = logging_config.get('LogGroup') + + if not log_group_arn: + raise SmokeTestFailureException( + f'LogGroup not found in Lambda function configuration for {lambda_name}. ' + 'The function may not have logging configured.' + ) + + # Extract log group name from ARN: arn:aws:logs:region:account:log-group:/aws/lambda/function-name + # The log group name is the last part after 'log-group:' + log_group_name = log_group_arn.split('log-group:')[-1] + + logger.info('Found log group for Lambda', log_group=log_group_name, function_name=lambda_name) + except ClientError as e: + raise SmokeTestFailureException( + f'Failed to get Lambda function details: {str(e)}' + ) from e + + event = {'daysBefore': days_before} + + logger.info(f'Invoking expiration reminder Lambda asynchronously for {days_before}-day reminder', event=event) + try: + # Invoke asynchronously - this returns immediately + response = lambda_client.invoke( + FunctionName=lambda_name, + InvocationType='Event', # Asynchronous invocation + Payload=json.dumps(event), + ) + + if response.get('FunctionError'): + error_payload = json.loads(response['Payload'].read()) + raise SmokeTestFailureException( + f'expiration_reminder Lambda invocation failed: {response.get("FunctionError")}, ' + f'error: {error_payload}' + ) + + logger.info('Lambda invocation accepted, polling CloudWatch Logs for completion...', log_group=log_group_name) + + # Poll CloudWatch Logs for the completion message + # The Lambda logs "Completed processing expiration reminders" with metrics + max_wait_time = 960 # 16 minutes (Lambda timeout is 15 minutes) + check_interval = 10 # Check every 10 seconds + start_time = time.time() + seen_event_ids = set() # Track which log events we've already processed + + while time.time() - start_time < max_wait_time: + # Get recent log events + try: + log_events_response = logs_client.filter_log_events( + logGroupName=log_group_name, + limit=100, + startTime=int((time.time() - 300) * 1000), # Last 5 minutes + ) + + # Process new log events + for event in log_events_response.get('events', []): + event_id = event.get('eventId') + if event_id in seen_event_ids: + continue # Skip events we've already processed + + seen_event_ids.add(event_id) + message = event.get('message', '') + + # Log all new messages to relay progress to CLI + # Lambda Powertools logs JSON format + try: + log_json = json.loads(message.strip()) + log_level = log_json.get('level', 'INFO') + log_message = log_json.get('message', '') + + # Relay log messages to CLI (skip DEBUG level) + if log_level != 'DEBUG': + logger.info(f'Lambda log [{log_level}]: {log_message}') + + # Check for completion message + if 'Completed processing expiration reminders' in log_message: + metrics = log_json.get('metrics', {}) + + if metrics: + target_date = log_json.get('targetDate', '') + days_before_logged = log_json.get('daysBefore', days_before) + + logger.info('Lambda completed successfully', metrics=metrics) + return { + 'targetDate': target_date, + 'daysBefore': days_before_logged, + 'metrics': metrics, + } + logger.warning('Completion message found but no metrics in log entry') + except json.JSONDecodeError: + logger.info(f'Lambda log: {message[:200]}') + except (ValueError, KeyError) as e: + logger.warning(f'Error processing log event: {str(e)}', message=message[:200]) + + except ClientError as e: + if e.response['Error']['Code'] == 'ResourceNotFoundException': + logger.warning(f'Log group not found yet: {log_group_name}, waiting...') + else: + logger.warning(f'Error querying logs: {str(e)}') + + time.sleep(check_interval) + elapsed = time.time() - start_time + if int(elapsed) % 60 == 0: # Log every minute + logger.info(f'Still waiting for Lambda completion... ({int(elapsed)}s elapsed)') + + raise SmokeTestFailureException( + f'Lambda did not complete within {max_wait_time}s timeout. ' + 'Check CloudWatch Logs for details.' + ) + + except ClientError as e: + if e.response['Error']['Code'] == 'ResourceNotFoundException': + raise SmokeTestFailureException( + f'Lambda function not found: {lambda_name}. ' + 'Please ensure the function is deployed and the name is correct.' + ) from e + raise + + +def run_load_test(skip_data_load: bool = False): + """ + Run the complete load test. + + :param skip_data_load: If True, skip creating providers and indexing steps + """ + logger.info('=' * 80) + logger.info('Starting Expiration Reminder Load Test') + logger.info('=' * 80) + + try: + if not skip_data_load: + # Step 1: Create providers with privileges expiring in 30 days + logger.info(f'Step 1: Creating {PROVIDERS_30_DAYS} providers with privileges expiring in 30 days...') + create_providers_batch(PROVIDERS_30_DAYS, days_until_expiration=30, progress_log_interval=1000) + logger.info(f'✓ Created {PROVIDERS_30_DAYS} providers for 30-day expiration') + + # Step 2: Create providers with privileges expiring in 10 days + logger.info(f'Step 2: Creating {PROVIDERS_10_DAYS} providers with privileges expiring in 10 days...') + create_providers_batch(PROVIDERS_10_DAYS, days_until_expiration=10, progress_log_interval=1000) + logger.info(f'✓ Created {PROVIDERS_10_DAYS} providers for 10-day expiration') + + total_providers = PROVIDERS_30_DAYS + PROVIDERS_10_DAYS + logger.info(f'Total providers created: {total_providers}') + + # Step 3: Wait for DynamoDB stream events to process and index providers into OpenSearch + logger.info('Step 3: Waiting 60 seconds for DynamoDB stream events to process and index providers...') + time.sleep(60) + logger.info('✓ Waiting complete - providers should now be indexed in OpenSearch') + else: + logger.info('Skipping data load - using existing providers in database') + + # Step 4: Invoke expiration reminder Lambda for 30-day event + logger.info('Step 4: Invoking expiration reminder Lambda for 30-day event...') + lambda_start_time = datetime.now(UTC) + lambda_response = invoke_expiration_reminder_lambda(days_before=30) + lambda_end_time = datetime.now(UTC) + lambda_duration = (lambda_end_time - lambda_start_time).total_seconds() + + logger.info( + '✓ Expiration reminder Lambda completed', + duration_seconds=lambda_duration, + response=lambda_response, + ) + + # Extract and display metrics + metrics = lambda_response.get('metrics', {}) + logger.info('=' * 80) + logger.info('LOAD TEST RESULTS') + logger.info('=' * 80) + logger.info(f'Lambda Execution Duration: {lambda_duration:.2f} seconds') + logger.info(f'Notifications Sent: {metrics.get("sent", 0)}') + logger.info(f'Notifications Skipped: {metrics.get("skipped", 0)}') + logger.info(f'Notifications Failed: {metrics.get("failed", 0)}') + logger.info(f'Already Sent (idempotency): {metrics.get("alreadySent", 0)}') + logger.info(f'No Email Address: {metrics.get("noEmail", 0)}') + logger.info(f'Matched Privileges: {metrics.get("matchedPrivileges", 0)}') + logger.info(f'Providers With Matches: {metrics.get("providersWithMatches", 0)}') + + expected_sent = PROVIDERS_30_DAYS + actual_sent = metrics.get('sent', 0) + + if actual_sent < expected_sent: + logger.warning( + f'⚠️ Only {actual_sent}/{expected_sent} notifications were sent. ' + 'This may indicate the Lambda timed out or encountered errors.' + ) + else: + logger.info(f'✓ Successfully sent {actual_sent} notifications as expected') + + if metrics.get('failed', 0) > 0: + logger.warning(f'⚠️ {metrics.get("failed", 0)} notifications failed to send') + + # Remind user to check email for validation + registered_email = config.test_provider_user_username + logger.info( + f'📧 Check email inbox for {registered_email} to validate that expiration reminder emails are being sent' + ) + + logger.info('=' * 80) + + except Exception as e: + logger.error('Load test failed', exc_info=e) + raise + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Load test for privilege expiration reminder notifications') + parser.add_argument( + '--skip-data-load', + action='store_true', + help='Skip creating providers and indexing steps. Use existing data in the database.', + ) + args = parser.parse_args() + + run_load_test(skip_data_load=args.skip_data_load) From 5e0b0d595acbf6a3fbcb76564b13bbc65a64f719 Mon Sep 17 00:00:00 2001 From: Justin Frahm Date: Wed, 28 Jan 2026 16:23:31 -0700 Subject: [PATCH 04/52] Delegate search permissions to search api, notification principal policies --- .../expiration_reminder_stack/__init__.py | 21 ++++-- .../search_persistent_stack/__init__.py | 2 + .../provider_search_domain.py | 64 ++++++++++++------- .../smoke/expiration_reminder_load_test.py | 17 ++--- 4 files changed, 64 insertions(+), 40 deletions(-) diff --git a/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py b/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py index 65bd4aa19..6417c2b74 100644 --- a/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py +++ b/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py @@ -7,6 +7,7 @@ from aws_cdk.aws_events import Rule, RuleTargetInput, Schedule from aws_cdk.aws_events_targets import LambdaFunction from aws_cdk.aws_logs import QueryDefinition, QueryString, RetentionDays +from cdk_nag import NagSuppressions from common_constructs.stack import AppStack from constructs import Construct @@ -52,7 +53,6 @@ def __init__( index=os.path.join('handlers', 'expiration_reminders.py'), lambda_dir='search', handler='process_expiration_reminders', - role=search_persistent_stack.search_api_lambda_role, timeout=Duration.minutes(15), # 15-minute timeout to handle all providers in single execution memory_size=2048, # Higher memory for processing large result sets log_retention=RetentionDays.ONE_MONTH, @@ -71,8 +71,7 @@ def __init__( ) # Grant necessary permissions - # OpenSearch access is already configured via search_api_lambda_role which is included - # in the domain's resource-based access policies + search_persistent_stack.provider_search_domain.grant_search_providers(self.expiration_reminder_handler) # Read/write access to EventStateTable for idempotency tracking event_state_stack.event_state_table.grant_read_write_data(self.expiration_reminder_handler) @@ -80,10 +79,18 @@ def __init__( # Invoke permission for email notification service persistent_stack.email_notification_service_lambda.grant_invoke(self.expiration_reminder_handler) - # Note: Since we're using the shared search_api_lambda_role, NagSuppressions for OpenSearch - # permissions are already applied in SearchPersistentStack. The EventStateTable permissions - # added by grant_read_write_data use wildcard permissions which is expected for DynamoDB - # item-level operations and is consistent with other Lambda functions in the codebase. + NagSuppressions.add_resource_suppressions_by_path( + self, + f'{self.expiration_reminder_handler.role.node.path}/DefaultPolicy/Resource', + [ + { + 'id': 'AwsSolutions-IAM5', + 'reason': 'The grant_read method requires wildcard permissions on the OpenSearch domain to ' + 'read from indices. This is appropriate for a function that needs to query ' + 'provider indices by expiration date.', + }, + ], + ) # Create EventBridge rules for each reminder type # All rules run daily at midnight UTC-4 (4:00 AM UTC) to process reminders for privileges expiring on the diff --git a/backend/compact-connect/stacks/search_persistent_stack/__init__.py b/backend/compact-connect/stacks/search_persistent_stack/__init__.py index 5c5f9a8a3..5b8952c47 100644 --- a/backend/compact-connect/stacks/search_persistent_stack/__init__.py +++ b/backend/compact-connect/stacks/search_persistent_stack/__init__.py @@ -63,6 +63,8 @@ def __init__( assumed_by=ServicePrincipal('lambda.amazonaws.com'), description='IAM role for Search API Lambda functions that need read access to OpenSearch Domain', ) + # Prevent any deady-embrace with cross-stack dependencies + self.export_value(self.search_api_lambda_role.role_arn) # Create the OpenSearch domain and associated resources self.provider_search_domain = ProviderSearchDomain( diff --git a/backend/compact-connect/stacks/search_persistent_stack/provider_search_domain.py b/backend/compact-connect/stacks/search_persistent_stack/provider_search_domain.py index 0afa708ba..a362a0fb1 100644 --- a/backend/compact-connect/stacks/search_persistent_stack/provider_search_domain.py +++ b/backend/compact-connect/stacks/search_persistent_stack/provider_search_domain.py @@ -2,7 +2,7 @@ from aws_cdk.aws_cloudwatch import Alarm, ComparisonOperator, Metric, TreatMissingData from aws_cdk.aws_cloudwatch_actions import SnsAction from aws_cdk.aws_ec2 import EbsDeviceVolumeType, SubnetSelection, SubnetType -from aws_cdk.aws_iam import Effect, IRole, PolicyStatement, ServicePrincipal +from aws_cdk.aws_iam import AccountRootPrincipal, Effect, IPrincipal, IRole, PolicyStatement, ServicePrincipal from aws_cdk.aws_kms import Key from aws_cdk.aws_logs import LogGroup, ResourcePolicy, RetentionDays from aws_cdk.aws_opensearchservice import ( @@ -79,7 +79,7 @@ def __init__( self._ingest_lambda_role = ingest_lambda_role self._index_manager_lambda_role = index_manager_lambda_role self._search_api_lambda_role = search_api_lambda_role - + self._compact_abbreviations = compact_abbreviations self._is_prod_environment = environment_name == PROD_ENV_NAME # Determine removal policy based on environment @@ -207,9 +207,10 @@ def __init__( ) # Configure access policies - self._configure_access_policies(compact_abbreviations) + self._configure_access_policies() # Grant lambda roles access to domain + self.grant_search_providers(self._search_api_lambda_role) self.domain.grant_read(self._search_api_lambda_role) self.domain.grant_write(self._ingest_lambda_role) self.domain.grant_read_write(self._index_manager_lambda_role) @@ -224,7 +225,7 @@ def __init__( # Add capacity monitoring alarms self._add_capacity_alarms(alarm_topic) - def _configure_access_policies(self, compact_abbreviations: list[str]): + def _configure_access_policies(self): """ Configure access policies for the OpenSearch domain. @@ -255,25 +256,9 @@ def _configure_access_policies(self, compact_abbreviations: list[str]): ], resources=[Fn.join('', [self.domain.domain_arn, '/compact*'])], ) - # Search API policy - restricted to _search endpoint only - # POST is required for _search queries even though they are read-only operations - # because OpenSearch's search API uses POST to send the query DSL body. - # By restricting the resource to /_search, we prevent POST from being used - # for document indexing or other write operations. - # See: https://docs.aws.amazon.com/opensearch-service/latest/developerguide/ac.html - search_api_policy = PolicyStatement( - effect=Effect.ALLOW, - principals=[self._search_api_lambda_role], - actions=[ - 'es:ESHttpPost', - ], - # define all compact indices to restrict the policy to the search operation - resources=[ - Fn.join(delimiter='', list_of_values=[self.domain.domain_arn, f'/compact_{compact}_providers/_search']) - for compact in compact_abbreviations - ], - ) - # Add access policy to restrict access to set of roles + + # Delegate read-only search access to account root, so access can be managed by principal policies. + search_api_policy = self._get_search_policy(AccountRootPrincipal()) self.domain.add_access_policies( ingest_access_policy, index_manager_access_policy, @@ -640,3 +625,36 @@ def _add_lambda_role_suppressions(self, lambda_role: IRole): ], apply_to_children=True, ) + + def grant_search_providers(self, principal: IPrincipal): + """ + Grant search access to the principal policy. + """ + # Add access policy to restrict access to set of roles + principal.grant_principal.add_to_principal_policy( + self._get_search_policy(), + ) + + def _get_search_policy(self, principal: IPrincipal = None): + """ + Generate search access policy. Specifies a principal, if provided. + + Search API policy is restricted to _search endpoint only POST is required for _search queries even though they + are read-only operations because OpenSearch's search API uses POST to send the query DSL body. + By restricting the resource to /_search, we prevent POST from being used for document indexing or other write + operations. + + See: https://docs.aws.amazon.com/opensearch-service/latest/developerguide/ac.html + """ + return PolicyStatement( + effect=Effect.ALLOW, + actions=[ + 'es:ESHttpPost', + ], + resources=[ + Fn.join(delimiter='', list_of_values=[self.domain.domain_arn, f'/compact_{compact}_providers/_search']) + for compact in self._compact_abbreviations + ], + # Can't specify principals if this policy is going on a principal policy + **({'principals': [principal.grant_principal]} if principal else {}), + ) diff --git a/backend/compact-connect/tests/smoke/expiration_reminder_load_test.py b/backend/compact-connect/tests/smoke/expiration_reminder_load_test.py index 8a86adf95..97ac0d7ea 100755 --- a/backend/compact-connect/tests/smoke/expiration_reminder_load_test.py +++ b/backend/compact-connect/tests/smoke/expiration_reminder_load_test.py @@ -238,8 +238,8 @@ def create_providers_batch(num_providers: int, days_until_expiration: int, progr Create a batch of providers with privileges expiring in the specified number of days. Uses DynamoDB batch_write_item for efficient bulk writes. - The first provider will use the registered email address from smoke_tests_env.json - to allow validation that emails are being sent. + All providers will use the registered email address from smoke_tests_env.json + to avoid spamming external email addresses during testing. :param num_providers: Total number of providers to create :param days_until_expiration: Number of days until privilege expiration @@ -253,10 +253,10 @@ def create_providers_batch(num_providers: int, days_until_expiration: int, progr expiration_date=expiration_date_str, ) - # Get the registered email address for validation + # Get the registered email address for all providers to avoid spamming external addresses registered_email = config.test_provider_user_username logger.info( - f'Using registered email for first provider: {registered_email}', + f'Using registered email for all providers: {registered_email}', registered_email=registered_email, ) @@ -267,13 +267,10 @@ def create_providers_batch(num_providers: int, days_until_expiration: int, progr # DynamoDB batch_write_item supports up to 25 items per request # So we'll use batch_size=24 (8 providers * 3 items = 24 items per batch) with DynamoDBBatchWriter(dynamodb_table, batch_size=24) as batch_writer: - for i in range(num_providers): + for _ in range(num_providers): provider_id = str(uuid.uuid4()) - # Use registered email for the first provider to validate email delivery - if i == 0: - email = registered_email - else: - email = f'loadtest-{days_until_expiration}days-{provider_id[:8]}@example.com' + # Use registered email for all providers to avoid spamming external email addresses + email = registered_email provider_record, license_record, privilege_record = create_provider_records( provider_id=provider_id, From 7a4bfe9c7c633debf84a974b7ef4db35a8672ddc Mon Sep 17 00:00:00 2001 From: Justin Frahm Date: Wed, 28 Jan 2026 16:52:00 -0700 Subject: [PATCH 05/52] Move search api role permissions back to original state --- .../lambdas/python/search/handlers/expiration_reminders.py | 1 + .../stacks/search_persistent_stack/__init__.py | 2 -- .../search_persistent_stack/provider_search_domain.py | 6 ++---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py index 7f47b15e8..341d23634 100644 --- a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py @@ -290,6 +290,7 @@ def iterate_privileges_by_expiration_date( search_body['search_after'] = search_after response = opensearch_client.search(index_name=index_name, body=search_body) + logger.info('Received response from OpenSearch', hits=response.get('hits', {}).get('total', {})) hits = response.get('hits', {}).get('hits', []) if not hits: break diff --git a/backend/compact-connect/stacks/search_persistent_stack/__init__.py b/backend/compact-connect/stacks/search_persistent_stack/__init__.py index 5b8952c47..5c5f9a8a3 100644 --- a/backend/compact-connect/stacks/search_persistent_stack/__init__.py +++ b/backend/compact-connect/stacks/search_persistent_stack/__init__.py @@ -63,8 +63,6 @@ def __init__( assumed_by=ServicePrincipal('lambda.amazonaws.com'), description='IAM role for Search API Lambda functions that need read access to OpenSearch Domain', ) - # Prevent any deady-embrace with cross-stack dependencies - self.export_value(self.search_api_lambda_role.role_arn) # Create the OpenSearch domain and associated resources self.provider_search_domain = ProviderSearchDomain( diff --git a/backend/compact-connect/stacks/search_persistent_stack/provider_search_domain.py b/backend/compact-connect/stacks/search_persistent_stack/provider_search_domain.py index a362a0fb1..55df55a54 100644 --- a/backend/compact-connect/stacks/search_persistent_stack/provider_search_domain.py +++ b/backend/compact-connect/stacks/search_persistent_stack/provider_search_domain.py @@ -2,7 +2,7 @@ from aws_cdk.aws_cloudwatch import Alarm, ComparisonOperator, Metric, TreatMissingData from aws_cdk.aws_cloudwatch_actions import SnsAction from aws_cdk.aws_ec2 import EbsDeviceVolumeType, SubnetSelection, SubnetType -from aws_cdk.aws_iam import AccountRootPrincipal, Effect, IPrincipal, IRole, PolicyStatement, ServicePrincipal +from aws_cdk.aws_iam import Effect, IPrincipal, IRole, PolicyStatement, ServicePrincipal from aws_cdk.aws_kms import Key from aws_cdk.aws_logs import LogGroup, ResourcePolicy, RetentionDays from aws_cdk.aws_opensearchservice import ( @@ -210,7 +210,6 @@ def __init__( self._configure_access_policies() # Grant lambda roles access to domain - self.grant_search_providers(self._search_api_lambda_role) self.domain.grant_read(self._search_api_lambda_role) self.domain.grant_write(self._ingest_lambda_role) self.domain.grant_read_write(self._index_manager_lambda_role) @@ -257,8 +256,7 @@ def _configure_access_policies(self): resources=[Fn.join('', [self.domain.domain_arn, '/compact*'])], ) - # Delegate read-only search access to account root, so access can be managed by principal policies. - search_api_policy = self._get_search_policy(AccountRootPrincipal()) + search_api_policy = self._get_search_policy(self._search_api_lambda_role) self.domain.add_access_policies( ingest_access_policy, index_manager_access_policy, From 691d64df86752ca0162eeb7fc227482dc5506b23 Mon Sep 17 00:00:00 2001 From: Justin Frahm Date: Thu, 29 Jan 2026 12:20:25 -0700 Subject: [PATCH 06/52] Revise information passed through to email --- backend/.gitignore | 2 +- .../email-notification-service/lambda.ts | 16 +- .../lib/email/email-notification-service.ts | 41 ++- .../lambdas/nodejs/lib/email/index.ts | 2 +- .../tests/email-notification-service.test.ts | 112 +++++++ .../email/email-notification-service.test.ts | 96 ++++-- .../common/cc_common/email_service_client.py | 6 +- .../tests/unit/test_email_service_client.py | 16 +- .../search/handlers/expiration_reminders.py | 113 +++---- .../function/test_expiration_reminders.py | 116 ++++--- .../smoke/expiration_reminder_load_test.py | 299 ++++++++++-------- 11 files changed, 534 insertions(+), 285 deletions(-) diff --git a/backend/.gitignore b/backend/.gitignore index 6e3dad9cc..0be4c814f 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -13,4 +13,4 @@ cdk.out cdk.context.json *-cdk.context.json # smoke test env -smoke_tests_env.json +*smoke_tests_env.json 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 4cc809242..514bd66ad 100644 --- a/backend/compact-connect/lambdas/nodejs/email-notification-service/lambda.ts +++ b/backend/compact-connect/lambdas/nodejs/email-notification-service/lambda.ts @@ -8,7 +8,7 @@ import { Context } from 'aws-lambda'; import { EnvironmentVariablesService } from '../lib/environment-variables-service'; import { CompactConfigurationClient } from '../lib/compact-configuration-client'; import { JurisdictionClient } from '../lib/jurisdiction-client'; -import { EmailNotificationService, EncumbranceNotificationService, InvestigationNotificationService } from '../lib/email'; +import { EmailNotificationService, EncumbranceNotificationService, InvestigationNotificationService, type PrivilegeExpirationReminderRow } from '../lib/email'; import { EmailNotificationEvent, EmailNotificationResponse } from '../lib/models/email-notification-service-event'; const environmentVariables = new EnvironmentVariablesService(); @@ -395,12 +395,24 @@ export class Lambda implements LambdaInterface { || !event.templateVariables?.privileges) { throw new Error('Missing required template variables for privilegeExpirationReminder template.'); } + const privileges = event.templateVariables.privileges as Array<{ jurisdiction?: string; licenseType?: string; privilegeId?: string; dateOfExpiration?: string }>; + if (!Array.isArray(privileges) || privileges.length === 0) { + throw new Error('privilegeExpirationReminder template requires a non-empty privileges array.'); + } + for (let i = 0; i < privileges.length; i++) { + const p = privileges[i]; + if (!p?.jurisdiction || !p?.licenseType || !p?.privilegeId || !p?.dateOfExpiration) { + throw new Error( + `privilegeExpirationReminder template requires each privilege to have jurisdiction, licenseType, privilegeId, and dateOfExpiration (ISO 8601). Invalid privilege at index ${i}.` + ); + } + } await this.emailService.sendPrivilegeExpirationReminderEmail( event.compact, event.specificEmails, event.templateVariables.providerFirstName, event.templateVariables.expirationDate, - event.templateVariables.privileges + privileges as PrivilegeExpirationReminderRow[] ); break; case 'licenseInvestigationStateNotification': diff --git a/backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts b/backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts index a871ff868..7da74ba00 100644 --- a/backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts +++ b/backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts @@ -5,6 +5,25 @@ import { RecipientType } from '../models/email-notification-service-event'; const environmentVariableService = new EnvironmentVariablesService(); +const MONTH_NAMES = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] as const; + +/** Format an ISO 8601 date string (YYYY-MM-DD) for display (e.g. "February 16, 2026"). Timezone-neutral. */ +function formatIsoDateForDisplay(isoDate: string): string { + const [year, month, day] = isoDate.split('-').map(Number); + const monthName = MONTH_NAMES[month - 1]; + return `${monthName} ${day}, ${year}`; +} + +/** + * Privilege row for expiration reminder: jurisdiction and licenseType are full names; dates are ISO 8601. + */ +export interface PrivilegeExpirationReminderRow { + jurisdiction: string; + licenseType: string; + privilegeId: string; + dateOfExpiration: string; +} + /** * Email service for handling email notifications */ @@ -631,21 +650,21 @@ export class EmailNotificationService extends BaseEmailService { } /** - * Sends a reminder email to a provider about expiring privileges + * Sends a reminder email to a provider about expiring privileges. * @param compact - The compact name * @param specificEmails - The email address(es) to send the notification to (provider's email) * @param providerFirstName - The provider's first name - * @param expirationDate - The formatted expiration date string - * @param privileges - Array of expiring privileges with jurisdiction, licenseType, and privilegeId + * @param expirationDate - ISO 8601 date string (YYYY-MM-DD) for the main expiration date + * @param privileges - Full list of privileges (jurisdiction and licenseType full names, dateOfExpiration ISO) */ public async sendPrivilegeExpirationReminderEmail( compact: string, specificEmails: string[], providerFirstName: string, expirationDate: string, - privileges: { jurisdiction: string; licenseType: string; privilegeId: string }[] + privileges: PrivilegeExpirationReminderRow[] ): Promise { - this.logger.info('Sending privilege expiration reminder email', { compact: compact, privilegeCount: privileges.length }); + this.logger.info('Sending privilege expiration reminder email', { compact, privilegeCount: privileges.length }); const recipients = specificEmails; @@ -657,19 +676,21 @@ export class EmailNotificationService extends BaseEmailService { throw new Error('No privileges provided for privilege expiration reminder email'); } + const expirationDateDisplay = formatIsoDateForDisplay(expirationDate); + const emailContent = this.getNewEmailTemplate(); - const subject = `Your Compact Connect Privileges Expire on ${expirationDate}`; + const subject = `Your Compact Connect Privileges Expire on ${expirationDateDisplay}`; const headerText = 'Privilege Expiration Reminder'; - const bodyText = `Hello ${providerFirstName},\n\nThis is a reminder that the following privilege(s) will expire on ${expirationDate}:`; + const bodyText = `Hello ${providerFirstName},\n\nThis is a reminder that the following privilege(s) will expire on ${expirationDateDisplay}:`; this.insertHeader(emailContent, headerText); this.insertBody(emailContent, bodyText, 'center'); privileges.forEach((privilege) => { - const titleText = `${privilege.licenseType.toUpperCase()} - ${privilege.jurisdiction.toUpperCase()}`; - const privilegeIdText = `Privilege Id: ${privilege.privilegeId}`; + const titleText = `${privilege.jurisdiction}, ${privilege.licenseType}`; + const detailText = `#${privilege.privilegeId} Expires: ${formatIsoDateForDisplay(privilege.dateOfExpiration)}`; - this.insertTuple(emailContent, titleText, privilegeIdText); + this.insertTuple(emailContent, titleText, detailText); }); this.insertBody(emailContent, '\nPlease visit Compact Connect to renew your privileges before they expire.', 'center'); diff --git a/backend/compact-connect/lambdas/nodejs/lib/email/index.ts b/backend/compact-connect/lambdas/nodejs/lib/email/index.ts index 66e5ba1e8..5c4df39b1 100644 --- a/backend/compact-connect/lambdas/nodejs/lib/email/index.ts +++ b/backend/compact-connect/lambdas/nodejs/lib/email/index.ts @@ -1,5 +1,5 @@ export { CognitoEmailService } from './cognito-email-service'; -export { EmailNotificationService } from './email-notification-service'; +export { EmailNotificationService, type PrivilegeExpirationReminderRow } from './email-notification-service'; export { EncumbranceNotificationService } from './encumbrance-notification-service'; export { InvestigationNotificationService } from './investigation-notification-service'; export { IngestEventEmailService } from './ingest-event-email-service'; 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 4bc6ebe32..96f6d3566 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 @@ -631,6 +631,118 @@ describe('EmailNotificationServiceLambda', () => { }); }); + describe('Privilege Expiration Reminder', () => { + const SAMPLE_PRIVILEGE_EXPIRATION_REMINDER_EVENT: EmailNotificationEvent = { + template: 'privilegeExpirationReminder', + recipientType: 'SPECIFIC', + compact: 'aslp', + specificEmails: ['provider@example.com'], + templateVariables: { + providerFirstName: 'Mary', + expirationDate: '2026-02-16', + privileges: [ + { + jurisdiction: 'Ohio', + licenseType: 'audiologist', + privilegeId: 'AUD-OH-001', + dateOfExpiration: '2026-02-16', + }, + { + jurisdiction: 'Kentucky', + licenseType: 'speech-language pathologist', + privilegeId: 'SLP-KY-002', + dateOfExpiration: '2026-03-01', + }, + ], + }, + }; + + it('should successfully send privilege expiration reminder email with new payload', async () => { + const response = await lambda.handler(SAMPLE_PRIVILEGE_EXPIRATION_REMINDER_EVENT, {} as any); + + expect(response).toEqual({ + message: 'Email message sent' + }); + + expect(mockSESClient).toHaveReceivedCommandTimes(SendEmailCommand, 1); + const sendCall = mockSESClient.commandCalls(SendEmailCommand)[0]; + const input = sendCall.args[0].input; + expect(input.Destination?.ToAddresses).toEqual(['provider@example.com']); + expect(input.Content?.Simple?.Subject?.Data).toBe('Your Compact Connect Privileges Expire on February 16, 2026'); + const htmlData = input.Content?.Simple?.Body?.Html?.Data ?? ''; + expect(htmlData).toContain('Privilege Expiration Reminder'); + expect(htmlData).toContain('Hello Mary'); + expect(htmlData).toContain('February 16, 2026'); + expect(htmlData).toContain('Ohio, audiologist'); + expect(htmlData).toContain('#AUD-OH-001'); + expect(htmlData).toContain('Kentucky, speech-language pathologist'); + expect(htmlData).toContain('#SLP-KY-002'); + }); + + it('should throw error when no recipients found', async () => { + const eventWithNoRecipients: EmailNotificationEvent = { + ...SAMPLE_PRIVILEGE_EXPIRATION_REMINDER_EVENT, + specificEmails: [] + }; + + await expect(lambda.handler(eventWithNoRecipients, {} as any)) + .rejects + .toThrow('No recipients found for privilege expiration reminder email'); + }); + + it('should throw error when required template variables missing', async () => { + const eventMissingVars: EmailNotificationEvent = { + ...SAMPLE_PRIVILEGE_EXPIRATION_REMINDER_EVENT, + templateVariables: {} + }; + + await expect(lambda.handler(eventMissingVars, {} as any)) + .rejects + .toThrow('Missing required template variables for privilegeExpirationReminder template'); + }); + + it('should throw error when privileges array is empty', async () => { + const eventEmptyPrivileges: EmailNotificationEvent = { + ...SAMPLE_PRIVILEGE_EXPIRATION_REMINDER_EVENT, + templateVariables: { + ...SAMPLE_PRIVILEGE_EXPIRATION_REMINDER_EVENT.templateVariables, + privileges: [] + } + }; + + await expect(lambda.handler(eventEmptyPrivileges, {} as any)) + .rejects + .toThrow('privilegeExpirationReminder template requires a non-empty privileges array'); + }); + + it('should throw error when privilege row missing required fields', async () => { + const eventInvalidPrivilege: EmailNotificationEvent = { + ...SAMPLE_PRIVILEGE_EXPIRATION_REMINDER_EVENT, + templateVariables: { + ...SAMPLE_PRIVILEGE_EXPIRATION_REMINDER_EVENT.templateVariables, + privileges: [ + { + jurisdiction: 'Ohio', + licenseType: 'audiologist', + privilegeId: 'AUD-OH-001', + dateOfExpiration: '2026-02-16', + }, + { + jurisdiction: 'Kentucky', + privilegeId: 'SLP-KY-002', + dateOfExpiration: '2026-03-01', + // missing licenseType + }, + ], + } + }; + + await expect(lambda.handler(eventInvalidPrivilege, {} as any)) + .rejects + .toThrow(/Invalid privilege at index 1/); + }); + }); + describe('Multiple Registration Attempt Notification', () => { const SAMPLE_MULTIPLE_REGISTRATION_ATTEMPT_NOTIFICATION_EVENT: EmailNotificationEvent = { template: 'multipleRegistrationAttemptNotification', 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 b4d4e3b45..8e50a79e1 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 @@ -1074,10 +1074,20 @@ describe('EmailNotificationService', () => { 'aslp', ['provider@example.com'], 'John', - 'February 16, 2026', + '2026-02-16', [ - { jurisdiction: 'oh', licenseType: 'aud', privilegeId: 'AUD-OH-001' }, - { jurisdiction: 'ky', licenseType: 'slp', privilegeId: 'SLP-KY-002' } + { + jurisdiction: 'Ohio', + licenseType: 'audiologist', + privilegeId: 'AUD-OH-001', + dateOfExpiration: '2026-02-16', + }, + { + jurisdiction: 'Kentucky', + licenseType: 'speech-language pathologist', + privilegeId: 'SLP-KY-002', + dateOfExpiration: '2026-03-01', + }, ] ); @@ -1085,58 +1095,70 @@ describe('EmailNotificationService', () => { SendEmailCommand, { Destination: { - ToAddresses: ['provider@example.com'] + ToAddresses: ['provider@example.com'], }, Content: { Simple: { Body: { Html: { Charset: 'UTF-8', - Data: expect.stringContaining('Privilege Expiration Reminder') - } + Data: expect.stringContaining('Privilege Expiration Reminder'), + }, }, Subject: { Charset: 'UTF-8', - Data: 'Your Compact Connect Privileges Expire on February 16, 2026' - } - } + Data: 'Your Compact Connect Privileges Expire on February 16, 2026', + }, + }, }, - FromEmailAddress: '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.Content?.Simple?.Body?.Html?.Data; expect(htmlContent).toBeDefined(); expect(htmlContent).toContain('Hello John'); expect(htmlContent).toContain('will expire on February 16, 2026'); - expect(htmlContent).toContain('AUD - OH'); - expect(htmlContent).toContain('Privilege Id: AUD-OH-001'); - expect(htmlContent).toContain('SLP - KY'); - expect(htmlContent).toContain('Privilege Id: SLP-KY-002'); + expect(htmlContent).toContain('Ohio, audiologist'); + expect(htmlContent).toContain('#AUD-OH-001'); + expect(htmlContent).toContain('Expires: February 16, 2026'); + expect(htmlContent).toContain('Kentucky, speech-language pathologist'); + expect(htmlContent).toContain('#SLP-KY-002'); + expect(htmlContent).toContain('Expires: March 1, 2026'); expect(htmlContent).toContain('Please visit Compact Connect to renew your privileges before they expire'); }); it('should throw error when no recipients provided', async () => { - await expect(emailService.sendPrivilegeExpirationReminderEmail( - 'aslp', - [], - 'John', - 'February 16, 2026', - [{ jurisdiction: 'oh', licenseType: 'aud', privilegeId: 'AUD-OH-001' }] - )).rejects.toThrow('No recipients found for privilege expiration reminder email'); + await expect( + emailService.sendPrivilegeExpirationReminderEmail( + 'aslp', + [], + 'John', + '2026-02-16', + [ + { + jurisdiction: 'Ohio', + licenseType: 'aud', + privilegeId: 'AUD-OH-001', + dateOfExpiration: '2026-02-16', + }, + ] + ) + ).rejects.toThrow('No recipients found for privilege expiration reminder email'); }); it('should throw error when no privileges provided', async () => { - await expect(emailService.sendPrivilegeExpirationReminderEmail( - 'aslp', - ['provider@example.com'], - 'John', - 'February 16, 2026', - [] - )).rejects.toThrow('No privileges provided for privilege expiration reminder email'); + await expect( + emailService.sendPrivilegeExpirationReminderEmail( + 'aslp', + ['provider@example.com'], + 'John', + '2026-02-16', + [] + ) + ).rejects.toThrow('No privileges provided for privilege expiration reminder email'); }); it('should send email with single privilege', async () => { @@ -1144,8 +1166,15 @@ describe('EmailNotificationService', () => { 'aslp', ['provider@example.com'], 'Jane', - 'March 1, 2026', - [{ jurisdiction: 'ne', licenseType: 'slp', privilegeId: 'SLP-NE-123' }] + '2026-03-01', + [ + { + jurisdiction: 'Nebraska', + licenseType: 'speech-language pathologist', + privilegeId: 'SLP-NE-123', + dateOfExpiration: '2026-03-01', + }, + ] ); const emailCall = mockSESClient.commandCalls(SendEmailCommand)[0]; @@ -1154,8 +1183,9 @@ describe('EmailNotificationService', () => { expect(htmlContent).toBeDefined(); expect(htmlContent).toContain('Hello Jane'); expect(htmlContent).toContain('will expire on March 1, 2026'); - expect(htmlContent).toContain('SLP - NE'); - expect(htmlContent).toContain('Privilege Id: SLP-NE-123'); + expect(htmlContent).toContain('Nebraska, speech-language pathologist'); + expect(htmlContent).toContain('#SLP-NE-123'); + expect(htmlContent).toContain('Expires: March 1, 2026'); }); }); }); diff --git a/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py b/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py index d5803003a..510808344 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py @@ -45,7 +45,7 @@ class PrivilegeExpirationReminderTemplateVariables: provider_first_name: str expiration_date: date - privileges: list[dict] # Each dict has: jurisdiction, licenseType, privilegeId + privileges: list[dict] class ProviderNotificationMethod(Protocol): @@ -832,7 +832,7 @@ def send_privilege_expiration_reminder_email( :param compact: Compact name :param provider_email: Email address of the provider - :param template_variables: Template variables for the email (provider name, expiration date, privileges) + :param template_variables: Provider name, expiration date, privileges (state, license type, id, date). :return: Response from the email notification service """ payload = { @@ -842,7 +842,7 @@ def send_privilege_expiration_reminder_email( 'specificEmails': [provider_email], 'templateVariables': { 'providerFirstName': template_variables.provider_first_name, - 'expirationDate': template_variables.expiration_date.strftime('%B %d, %Y'), + 'expirationDate': template_variables.expiration_date.isoformat(), 'privileges': template_variables.privileges, }, } diff --git a/backend/compact-connect/lambdas/python/common/tests/unit/test_email_service_client.py b/backend/compact-connect/lambdas/python/common/tests/unit/test_email_service_client.py index 043dbb573..f8cc8262f 100644 --- a/backend/compact-connect/lambdas/python/common/tests/unit/test_email_service_client.py +++ b/backend/compact-connect/lambdas/python/common/tests/unit/test_email_service_client.py @@ -230,8 +230,18 @@ def test_privilege_expiration_reminder_should_invoke_lambda_client_with_expected test_model = self._generate_test_model(mock_lambda_client) privileges = [ - {'jurisdiction': 'oh', 'licenseType': 'aud', 'privilegeId': 'AUD-OH-001'}, - {'jurisdiction': 'ky', 'licenseType': 'slp', 'privilegeId': 'SLP-KY-002'}, + { + 'jurisdiction': 'Ohio', + 'licenseType': 'aud', + 'privilegeId': 'AUD-OH-001', + 'dateOfExpiration': '2026-02-16', + }, + { + 'jurisdiction': 'Kentucky', + 'licenseType': 'slp', + 'privilegeId': 'SLP-KY-002', + 'dateOfExpiration': '2026-03-01', + }, ] template_variables = PrivilegeExpirationReminderTemplateVariables( @@ -259,7 +269,7 @@ def test_privilege_expiration_reminder_should_invoke_lambda_client_with_expected ], 'templateVariables': { 'providerFirstName': 'John', - 'expirationDate': 'February 16, 2026', + 'expirationDate': '2026-02-16', 'privileges': privileges, }, } diff --git a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py index 341d23634..ce4a82f45 100644 --- a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py @@ -7,12 +7,14 @@ from aws_lambda_powertools.utilities.typing import LambdaContext from cc_common.config import config, logger +from cc_common.data_model.compact_configuration_utils import CompactConfigUtility +from cc_common.data_model.schema.provider.api import ProviderGeneralResponseSchema from cc_common.email_service_client import PrivilegeExpirationReminderTemplateVariables from cc_common.exceptions import CCInvalidRequestException from expiration_reminder_tracker import ExpirationEventType, ExpirationReminderTracker from opensearch_client import OpenSearchClient -DEFAULT_PAGE_SIZE = 100 +DEFAULT_PAGE_SIZE = 1000 # Map days before expiration to ExpirationEventType DAYS_BEFORE_TO_EVENT_TYPE = { @@ -108,26 +110,17 @@ def process_expiration_reminders(event: dict, context: LambdaContext): # noqa: expiration_date=expiration_date, page_size=DEFAULT_PAGE_SIZE, ): - matched_privileges = extract_expiring_privileges_from_provider_document( - provider_document=provider_doc, expiration_date=expiration_date - ) - if not matched_privileges: - continue - compact_provider_count += 1 metrics = replace( metrics, - matched_privileges=metrics.matched_privileges + len(matched_privileges), providers_with_matches=metrics.providers_with_matches + 1, ) - # Process this provider's notification result = _process_provider_notification( compact=compact, provider_doc=provider_doc, expiration_date=expiration_date, event_type=event_type, - matched_privileges=matched_privileges, ) metrics = replace( metrics, @@ -164,7 +157,6 @@ def _process_provider_notification( provider_doc: dict, expiration_date: date, event_type: ExpirationEventType, - matched_privileges: list[dict], ) -> dict[str, int]: """ Process a single provider's expiration reminder notification. @@ -173,15 +165,11 @@ def _process_provider_notification( """ result = {'sent': 0, 'skipped': 0, 'failed': 0, 'already_sent': 0, 'no_email': 0} - provider_id_str = provider_doc.get('providerId') - if not provider_id_str: - logger.warning('Provider document missing providerId', compact=compact) - result['skipped'] = 1 - return result + provider_id_str = provider_doc['providerId'] try: provider_id = UUID(provider_id_str) - except ValueError: + except (ValueError, TypeError): logger.warning('Invalid providerId format', provider_id=provider_id_str, compact=compact) result['skipped'] = 1 return result @@ -215,24 +203,40 @@ def _process_provider_notification( result['already_sent'] = 1 return result - # Prepare and send email - provider_first_name = provider_doc.get('givenName', 'Provider') - - # Format privileges for email template - email_privileges = [ - { - 'jurisdiction': p.get('jurisdiction', ''), - 'licenseType': p.get('licenseType', ''), - 'privilegeId': p.get('privilegeId', ''), - } - for p in matched_privileges - ] + # Prepare and send email (provider_doc is schema-validated) + provider_first_name = provider_doc['givenName'] - template_variables = PrivilegeExpirationReminderTemplateVariables( - provider_first_name=provider_first_name, - expiration_date=expiration_date, - privileges=email_privileges, - ) + try: + # Build full privileges list for email (state name, license type, privilege id, ISO date per privilege) + email_privileges = [] + for privilege in provider_doc['privileges']: + jurisdiction_code = privilege['jurisdiction'] + jurisdiction_display = CompactConfigUtility.get_jurisdiction_name(jurisdiction_code) + if jurisdiction_display is None: + raise ValueError(f'Unknown jurisdiction code for display name: {jurisdiction_code!r}') + email_privileges.append({ + 'jurisdiction': jurisdiction_display, + 'licenseType': privilege['licenseType'], + 'privilegeId': privilege['privilegeId'], + 'dateOfExpiration': privilege['dateOfExpiration'], + }) + + template_variables = PrivilegeExpirationReminderTemplateVariables( + provider_first_name=provider_first_name, + expiration_date=expiration_date, + privileges=email_privileges, + ) + except ValueError as e: # invalid template data (missing/unknown jurisdiction, date, etc.) + tracker.record_failure(error_message=str(e)) + logger.error( + 'Failed to build expiration reminder template', + provider_id=str(provider_id), + compact=compact, + event_type=event_type, + error=str(e), + ) + result['failed'] = 1 + return result try: config.email_service_client.send_privilege_expiration_reminder_email( @@ -290,8 +294,8 @@ def iterate_privileges_by_expiration_date( search_body['search_after'] = search_after response = opensearch_client.search(index_name=index_name, body=search_body) - logger.info('Received response from OpenSearch', hits=response.get('hits', {}).get('total', {})) - hits = response.get('hits', {}).get('hits', []) + logger.info('Received response from OpenSearch', hits=response['hits']['total']) + hits = response['hits']['hits'] if not hits: break @@ -299,7 +303,7 @@ def iterate_privileges_by_expiration_date( if len(hits) < page_size: is_last_page = True else: - search_after = hits[-1].get('sort') + search_after = hits[-1]['sort'] # Reverse the list so we can pop() (O(1)) while maintaining original ordering. current_page_hits = list(reversed(hits)) @@ -308,42 +312,24 @@ def iterate_privileges_by_expiration_date( yield _provider_document_from_hit(hit) -def extract_expiring_privileges_from_provider_document(*, provider_document: dict, expiration_date: date) -> list[dict]: - """ - Return privileges in the provider document expiring on expiration_date and active. - - If the generator merged `inner_hits` into `provider_document['privileges']`, this - will typically be a tight list already; we still filter defensively. - """ - privileges = provider_document.get('privileges', []) or [] - expiration_date_str = expiration_date.isoformat() - return [ - p - for p in privileges - if p.get('dateOfExpiration') == expiration_date_str and str(p.get('status', '')).lower() == 'active' - ] - - def _provider_document_from_hit(hit: dict) -> dict: """ - Normalize an OpenSearch hit into a provider document shape for downstream processing. + Validate an OpenSearch hit's _source as a provider document via ProviderGeneralResponseSchema. - If `inner_hits.privileges` is present, replace the provider's `privileges` list with - only the matched privileges so we don't have to scan the full provider privileges list. + Returns the full provider document (including the complete privileges list) for the notification email. + Raises ValidationError if the document does not conform to the schema. """ - provider_doc = dict(hit.get('_source', {}) or {}) - - inner_hits = (hit.get('inner_hits') or {}).get('privileges', {}).get('hits', {}).get('hits', []) - if inner_hits: - provider_doc['privileges'] = [ih.get('_source', {}) for ih in inner_hits] - - return provider_doc + return ProviderGeneralResponseSchema().load(dict(hit['_source'])) def _build_expiration_query(*, expiration_date: date, page_size: int) -> dict: return { 'query': { 'nested': { + # Nested query for privileges + # This query is applied to each inner object (privilege) individually, so only privileges that match + # the _entire_ query are included in the results. This means that an individual privilege must be both + # active _and_ expire on the specified date to be included in the results. 'path': 'privileges', 'query': { 'bool': { @@ -353,7 +339,6 @@ def _build_expiration_query(*, expiration_date: date, page_size: int) -> dict: ], }, }, - 'inner_hits': {'size': 100}, }, }, # Required for search_after pagination diff --git a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py index d6c8db5bc..b4cf5377e 100644 --- a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py @@ -8,35 +8,20 @@ class TestExpirationRemindersOpenSearch(TstLambdas): """Tests for OpenSearch query and data extraction.""" - def test_extract_expiring_privileges_filters_by_date_and_active_status(self): - from handlers.expiration_reminders import extract_expiring_privileges_from_provider_document - - expiration_date = date(2026, 2, 16) - provider_doc = { - 'providerId': 'p1', - 'privileges': [ - {'privilegeId': 'a', 'dateOfExpiration': '2026-02-16', 'status': 'active'}, - {'privilegeId': 'b', 'dateOfExpiration': '2026-02-16', 'status': 'inactive'}, - {'privilegeId': 'c', 'dateOfExpiration': '2026-02-15', 'status': 'active'}, - ], - } - - matched = extract_expiring_privileges_from_provider_document( - provider_document=provider_doc, - expiration_date=expiration_date, - ) - - self.assertEqual([{'privilegeId': 'a', 'dateOfExpiration': '2026-02-16', 'status': 'active'}], matched) - def test_iterate_privileges_by_expiration_date_paginates_with_search_after_and_yields_in_order(self): - # Patch the module-level opensearch_client used by the generator - with patch('handlers.expiration_reminders.opensearch_client') as mock_client: + # Patch the module-level opensearch_client and schema load (validation would require full fixture) + with ( + patch('handlers.expiration_reminders.opensearch_client') as mock_client, + patch('handlers.expiration_reminders.ProviderGeneralResponseSchema') as mock_schema_class, + ): mock_client.search = MagicMock() + mock_schema_class.return_value.load.side_effect = lambda doc: doc # Page 1: p1, p2 mock_client.search.side_effect = [ { 'hits': { + 'total': {'value': 3, 'relation': 'eq'}, 'hits': [ { '_source': {'providerId': 'p1', 'privileges': []}, @@ -46,18 +31,19 @@ def test_iterate_privileges_by_expiration_date_paginates_with_search_after_and_y '_source': {'providerId': 'p2', 'privileges': []}, 'sort': ['p2'], }, - ] + ], } }, # Page 2: p3 { 'hits': { + 'total': {'value': 3, 'relation': 'eq'}, 'hits': [ { '_source': {'providerId': 'p3', 'privileges': []}, 'sort': ['p3'], } - ] + ], } }, ] @@ -84,33 +70,45 @@ def test_iterate_privileges_by_expiration_date_paginates_with_search_after_and_y self.assertEqual(second_call_kwargs['index_name'], 'compact_aslp_providers') self.assertEqual(second_call_kwargs['body']['search_after'], ['p2']) - def test_iterate_privileges_by_expiration_date_merges_inner_hits_privileges_into_provider_document(self): - with patch('handlers.expiration_reminders.opensearch_client') as mock_client: + def test_iterate_privileges_by_expiration_date_returns_full_provider_document(self): + """Provider document includes full privileges list from _source (no inner_hits filtering).""" + with ( + patch('handlers.expiration_reminders.opensearch_client') as mock_client, + patch('handlers.expiration_reminders.ProviderGeneralResponseSchema') as mock_schema_class, + ): + full_privileges = [ + { + 'privilegeId': 'a', + 'jurisdiction': 'oh', + 'licenseType': 'aud', + 'dateOfExpiration': '2026-02-16', + 'status': 'active', + }, + { + 'privilegeId': 'b', + 'jurisdiction': 'ky', + 'licenseType': 'slp', + 'dateOfExpiration': '2026-03-01', + 'status': 'active', + }, + ] mock_client.search = MagicMock( return_value={ 'hits': { + 'total': {'value': 1, 'relation': 'eq'}, 'hits': [ { '_source': { 'providerId': 'p1', - 'privileges': [{'privilegeId': 'should_be_replaced'}], + 'privileges': full_privileges, }, 'sort': ['p1'], - 'inner_hits': { - 'privileges': { - 'hits': { - 'hits': [ - {'_source': {'privilegeId': 'a'}}, - {'_source': {'privilegeId': 'b'}}, - ] - } - } - }, } - ] + ], } } ) + mock_schema_class.return_value.load.side_effect = lambda doc: doc from handlers.expiration_reminders import iterate_privileges_by_expiration_date @@ -123,7 +121,7 @@ def test_iterate_privileges_by_expiration_date_merges_inner_hits_privileges_into ) self.assertEqual(provider_doc['providerId'], 'p1') - self.assertEqual(provider_doc['privileges'], [{'privilegeId': 'a'}, {'privilegeId': 'b'}]) + self.assertEqual(provider_doc['privileges'], full_privileges) class TestProcessExpirationReminders(TstLambdas): @@ -187,6 +185,16 @@ def test_handler_sends_email_and_records_success(self): self.assertEqual(resp['metrics']['failed'], 0) self.assertEqual(resp['daysBefore'], 30) mock_email_client.send_privilege_expiration_reminder_email.assert_called_once() + call_kwargs = mock_email_client.send_privilege_expiration_reminder_email.call_args.kwargs + tv = call_kwargs['template_variables'] + self.assertEqual(tv.provider_first_name, 'John') + self.assertEqual(tv.expiration_date.isoformat(), '2026-02-16') + self.assertEqual(len(tv.privileges), 1) + priv = tv.privileges[0] + self.assertEqual(priv['jurisdiction'], 'Ohio', 'jurisdiction must be full state name') + self.assertEqual(priv['licenseType'], 'aud') + self.assertEqual(priv['privilegeId'], 'a') + self.assertEqual(priv['dateOfExpiration'], '2026-02-16', 'dateOfExpiration must be ISO 8601') mock_tracker_instance.record_success.assert_called_once() # Verify tracker was created with correct event_type @@ -271,6 +279,36 @@ def test_handler_records_failure_on_email_error(self): mock_tracker_instance.record_failure.assert_called_once() self.assertIn('Email service down', mock_tracker_instance.record_failure.call_args.kwargs['error_message']) + def test_handler_records_failure_when_privilege_has_unknown_jurisdiction(self): + """Unknown jurisdiction code raises when building template; handler records failure.""" + with ( + patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') as mock_iter, + patch('handlers.expiration_reminders.config') as mock_config, + patch('handlers.expiration_reminders.ExpirationReminderTracker') as mock_tracker_class, + ): + mock_config.compacts = ['aslp'] + mock_email_client = MagicMock() + mock_config.email_service_client = mock_email_client + + mock_tracker_instance = MagicMock() + mock_tracker_instance.was_already_sent.return_value = False + mock_tracker_class.return_value = mock_tracker_instance + + # Privilege with jurisdiction not in CompactConfigUtility.JURISDICTION_NAME_MAPPING + doc = self._make_provider_doc() + doc['privileges'][0]['jurisdiction'] = 'xx' + mock_iter.return_value = iter([doc]) + + from handlers.expiration_reminders import process_expiration_reminders + + resp = process_expiration_reminders(self._make_event(), self.mock_context) + + self.assertEqual(resp['metrics']['sent'], 0) + self.assertEqual(resp['metrics']['failed'], 1) + mock_tracker_instance.record_failure.assert_called_once() + failure_msg = mock_tracker_instance.record_failure.call_args.kwargs['error_message'] + self.assertIn('Unknown jurisdiction', failure_msg) + def test_handler_validates_days_before_value(self): from cc_common.exceptions import CCInvalidRequestException from handlers.expiration_reminders import process_expiration_reminders diff --git a/backend/compact-connect/tests/smoke/expiration_reminder_load_test.py b/backend/compact-connect/tests/smoke/expiration_reminder_load_test.py index 97ac0d7ea..7562d9e37 100755 --- a/backend/compact-connect/tests/smoke/expiration_reminder_load_test.py +++ b/backend/compact-connect/tests/smoke/expiration_reminder_load_test.py @@ -2,22 +2,33 @@ """ Load test for privilege expiration reminder notifications. -This script creates a large number of providers with privileges expiring at specific dates, -indexes them into OpenSearch, and triggers the expiration reminder Lambda to test its -performance and capacity. +This script creates providers with privileges expiring at specific dates, +indexes them into OpenSearch, and triggers the expiration reminder Lambda to test +performance and capacity (load test) or a small smoke test. Usage: + # Full load test (20k matching + 10k non-matching providers, two privileges per provider) python expiration_reminder_load_test.py python expiration_reminder_load_test.py --skip-data-load + # Smoke test (N providers, two privileges per provider, ~2/3 expiring / ~1/3 non-expiring) + python expiration_reminder_load_test.py --providers 3 + python expiration_reminder_load_test.py --providers 6 --skip-data-load + The script will: -1. Create 20,000 providers with privileges expiring in 30 days (unless --skip-data-load) -2. Create 10,000 providers with privileges expiring in 10 days (unless --skip-data-load) -3. Wait for DynamoDB stream events to index providers into OpenSearch (unless --skip-data-load) -4. Invoke the expiration reminder Lambda for the 30-day event -5. Display metrics from the Lambda execution +- Compute matching and non-matching provider counts: with --providers N (smoke), use ~2/3 and ~1/3 of N; + without --providers (load), use MATCHING_PROVIDERS (20k) and NON_MATCHING_PROVIDERS (10k). +- Create that many providers in a single batch (two privileges each; matching = one privilege on target + date, non-matching = neither on target date), unless --skip-data-load. +- Wait for DynamoDB stream events to index providers into OpenSearch (unless --skip-data-load). +- Invoke the expiration reminder Lambda for the 30-day event. +- Display metrics from the Lambda execution. Options: + --providers N Total number of fake providers to load (smoke mode). Allocates ~2/3 to + expiring and ~1/3 to non-expiring. Each provider has two privileges; for + expiring providers, one privilege matches the target date so the email + shows multiple privileges. Works for N >= 1. --skip-data-load Skip creating providers and indexing steps. Use existing data in the database. Useful for re-running the Lambda invocation test without recreating all providers. """ @@ -56,14 +67,20 @@ logs_client = boto3.client('logs') dynamodb_table = config.provider_user_dynamodb_table -# Test configuration -PROVIDERS_30_DAYS = 20_000 -PROVIDERS_10_DAYS = 10_000 +# Test configuration: counts of matching vs non-matching providers for the 30-day expiration run +MATCHING_PROVIDERS = 20_000 # Providers with one privilege expiring on target date (receive email) +NON_MATCHING_PROVIDERS = 10_000 # Providers with neither privilege on target date (no email) COMPACT = COMPACTS[0] # Use first compact JURISDICTION = JURISDICTIONS[0] # Use first jurisdiction -# LICENSE_TYPES is a dict keyed by compact, get the first license type for the compact +# Second jurisdiction and license type for two-privilege-per-provider (smoke mode) +JURISDICTION_2 = JURISDICTIONS[1] if len(JURISDICTIONS) > 1 else JURISDICTIONS[0] if COMPACT in LICENSE_TYPES and LICENSE_TYPES[COMPACT]: LICENSE_TYPE = LICENSE_TYPES[COMPACT][0]['name'] + LICENSE_TYPE_2 = ( + LICENSE_TYPES[COMPACT][1]['name'] + if len(LICENSE_TYPES[COMPACT]) > 1 + else LICENSE_TYPES[COMPACT][0]['name'] + ) else: raise SmokeTestFailureException(f'No license types found for compact {COMPACT}') @@ -148,59 +165,59 @@ def put_item(self, item: dict): def create_provider_records( provider_id: str, compact: str, - jurisdiction: str, + jurisdiction_1: str, + jurisdiction_2: str, license_jurisdiction: str, - license_type: str, - expiration_date: date, + license_type_1: str, + license_type_2: str, + expiration_date_1: date, + expiration_date_2: date, email: str, -) -> tuple[dict, dict, dict]: +) -> tuple[dict, dict, dict, dict]: """ - Create provider, license, and privilege record dictionaries (without writing to DynamoDB). + Create provider, license, and two privilege record dictionaries (without writing to DynamoDB). + Each provider has two privileges (different jurisdiction/license type so distinct records). Uses TestDataGenerator to ensure records match current schema requirements. - :param provider_id: The provider's UUID - :param compact: The compact abbreviation - :param jurisdiction: The privilege jurisdiction - :param license_jurisdiction: The license jurisdiction (home state) - :param license_type: The license type - :param expiration_date: The privilege expiration date - :param email: The provider's email address - :return: Tuple of (provider_record, license_record, privilege_record) + :return: Tuple of (provider_record, license_record, privilege_record_1, privilege_record_2) """ - license_type_abbr = get_license_type_abbreviation(license_type) - if not license_type_abbr: - raise SmokeTestFailureException(f'Could not find abbreviation for license type: {license_type}') + for _jurisdiction, _license_type in ( + (jurisdiction_1, license_type_1), + (jurisdiction_2, license_type_2), + ): + abbr = get_license_type_abbreviation(_license_type) + if not abbr: + raise SmokeTestFailureException( + f'Could not find abbreviation for license type: {_license_type}' + ) now = datetime.now(tz=UTC) - transaction_id = str(uuid.uuid4()) given_name = f'TestProvider{provider_id[:8]}' family_name = 'LoadTest' - # Generate provider record using TestDataGenerator provider_data = TestDataGenerator.generate_default_provider( value_overrides={ 'providerId': provider_id, 'compact': compact, 'licenseJurisdiction': license_jurisdiction, - 'privilegeJurisdictions': {jurisdiction}, + 'privilegeJurisdictions': {jurisdiction_1, jurisdiction_2}, 'givenName': given_name, 'familyName': family_name, 'compactConnectRegisteredEmailAddress': email, - 'dateOfExpiration': expiration_date, + 'dateOfExpiration': max(expiration_date_1, expiration_date_2), 'currentHomeJurisdiction': license_jurisdiction, }, is_registered=True, ) provider_record = provider_data.serialize_to_database_record() - # Generate license record using TestDataGenerator license_data = TestDataGenerator.generate_default_license( value_overrides={ 'providerId': provider_id, 'compact': compact, 'jurisdiction': license_jurisdiction, - 'licenseType': license_type, + 'licenseType': license_type_1, 'givenName': given_name, 'familyName': family_name, 'emailAddress': email, @@ -209,98 +226,101 @@ def create_provider_records( ) license_record = license_data.serialize_to_database_record() - # Generate privilege record using TestDataGenerator - privilege_data = TestDataGenerator.generate_default_privilege( - value_overrides={ - 'providerId': provider_id, - 'compact': compact, - 'jurisdiction': jurisdiction, - 'licenseJurisdiction': license_jurisdiction, - 'licenseType': license_type, - 'dateOfExpiration': expiration_date, - 'dateOfIssuance': now, - 'dateOfRenewal': now, - 'dateOfUpdate': now, - 'compactTransactionId': transaction_id, - 'compactTransactionIdGSIPK': f'COMPACT#{compact}#TX#{transaction_id}#', - 'privilegeId': f'{license_type_abbr.upper()}-{jurisdiction.upper()}-{provider_id[:8]}', - 'administratorSetStatus': 'active', - 'attestations': [], - } - ) - privilege_record = privilege_data.serialize_to_database_record() + def make_privilege(jurisdiction: str, license_type: str, expiration_date: date, suffix: str) -> dict: + abbr = get_license_type_abbreviation(license_type) + tx_id = str(uuid.uuid4()) + return TestDataGenerator.generate_default_privilege( + value_overrides={ + 'providerId': provider_id, + 'compact': compact, + 'jurisdiction': jurisdiction, + 'licenseJurisdiction': license_jurisdiction, + 'licenseType': license_type, + 'dateOfExpiration': expiration_date, + 'dateOfIssuance': now, + 'dateOfRenewal': now, + 'dateOfUpdate': now, + 'compactTransactionId': tx_id, + 'compactTransactionIdGSIPK': f'COMPACT#{compact}#TX#{tx_id}#', + 'privilegeId': f'{abbr.upper()}-{jurisdiction.upper()}-{provider_id[:8]}-{suffix}', + 'administratorSetStatus': 'active', + 'attestations': [], + } + ).serialize_to_database_record() + + privilege_record_1 = make_privilege(jurisdiction_1, license_type_1, expiration_date_1, '1') + privilege_record_2 = make_privilege(jurisdiction_2, license_type_2, expiration_date_2, '2') - return provider_record, license_record, privilege_record + return provider_record, license_record, privilege_record_1, privilege_record_2 -def create_providers_batch(num_providers: int, days_until_expiration: int, progress_log_interval: int = 1000): +def create_providers_batch( + matching_count: int, + non_matching_count: int, + target_days_until_expiration: int = 30, + progress_log_interval: int = 1000, +) -> int: """ - Create a batch of providers with privileges expiring in the specified number of days. + Create providers with two privileges each: matching_count match the target date (receive email), + non_matching_count do not (neither privilege on target date). - Uses DynamoDB batch_write_item for efficient bulk writes. - All providers will use the registered email address from smoke_tests_env.json - to avoid spamming external email addresses during testing. + Matching providers have one privilege expiring on the target date and one later, so the reminder + email shows both privileges. Non-matching providers have both privileges expiring on other dates. - :param num_providers: Total number of providers to create - :param days_until_expiration: Number of days until privilege expiration - :param progress_log_interval: Interval for progress logging (number of providers) + :param matching_count: Number of providers that match the 30-day search (will receive email). + :param non_matching_count: Number of providers that do not match (no email). + :param target_days_until_expiration: Days until the target expiration date (default 30). + :param progress_log_interval: Interval for progress logging. + :return: matching_count for use as expected_sent. """ - expiration_date = datetime.now(UTC).date() + timedelta(days=days_until_expiration) - expiration_date_str = expiration_date.isoformat() - logger.info( - f'Creating {num_providers} providers with privileges expiring in {days_until_expiration} days ' - f'(expiration date: {expiration_date_str})', - expiration_date=expiration_date_str, - ) + target_date = datetime.now(UTC).date() + timedelta(days=target_days_until_expiration) + other_date_matching = target_date + timedelta(days=30) + other_date_non_matching = target_date + timedelta(days=60) - # Get the registered email address for all providers to avoid spamming external addresses - registered_email = config.test_provider_user_username + total = matching_count + non_matching_count logger.info( - f'Using registered email for all providers: {registered_email}', - registered_email=registered_email, + f'Creating {total} providers (two privileges each): {matching_count} matching target date, ' + f'{non_matching_count} non-matching (target: {target_date.isoformat()})', ) - created_count = 0 + registered_email = config.test_provider_user_username + created = 0 - # Use batch writer for efficient DynamoDB writes - # Each provider creates 3 items (provider, license, privilege) - # DynamoDB batch_write_item supports up to 25 items per request - # So we'll use batch_size=24 (8 providers * 3 items = 24 items per batch) with DynamoDBBatchWriter(dynamodb_table, batch_size=24) as batch_writer: - for _ in range(num_providers): + for i in range(total): provider_id = str(uuid.uuid4()) - # Use registered email for all providers to avoid spamming external email addresses - email = registered_email + if i < matching_count: + exp_1, exp_2 = target_date, other_date_matching + else: + exp_1, exp_2 = other_date_non_matching, other_date_non_matching + timedelta(days=30) - provider_record, license_record, privilege_record = create_provider_records( + provider_record, license_record, priv_1, priv_2 = create_provider_records( provider_id=provider_id, compact=COMPACT, - jurisdiction=JURISDICTION, + jurisdiction_1=JURISDICTION, + jurisdiction_2=JURISDICTION_2, license_jurisdiction=JURISDICTION, - license_type=LICENSE_TYPE, - expiration_date=expiration_date, - email=email, + license_type_1=LICENSE_TYPE, + license_type_2=LICENSE_TYPE_2, + expiration_date_1=exp_1, + expiration_date_2=exp_2, + email=registered_email, ) - # Add all three records to the batch batch_writer.put_item(provider_record) batch_writer.put_item(license_record) - batch_writer.put_item(privilege_record) + batch_writer.put_item(priv_1) + batch_writer.put_item(priv_2) + created += 1 - created_count += 1 - if created_count % progress_log_interval == 0: - logger.info( - f'Created {created_count}/{num_providers} providers for {days_until_expiration}-day expiration ' - f'(expiration date: {expiration_date_str})' - ) + if created % progress_log_interval == 0: + logger.info(f'Created {created}/{total} providers') if batch_writer.failed_item_count > 0: logger.warning(f'Failed to write {batch_writer.failed_item_count} items during batch write') - logger.info( - f'Completed creating {num_providers} providers for {days_until_expiration}-day expiration ' - f'(expiration date: {expiration_date_str})' - ) + logger.info(f'Completed creating {total} providers (target date: {target_date.isoformat()})') + return matching_count def find_lambda_function_name(partial_name: str) -> str: @@ -468,40 +488,50 @@ def invoke_expiration_reminder_lambda(days_before: int): raise -def run_load_test(skip_data_load: bool = False): +def run_load_test(skip_data_load: bool = False, providers: int | None = None): """ - Run the complete load test. + Run the complete load test or smoke test. - :param skip_data_load: If True, skip creating providers and indexing steps + :param skip_data_load: If True, skip creating providers and indexing steps. + :param providers: If set, smoke mode: create this many providers (~2/3 matching, ~1/3 non-matching). + If None, load mode: use MATCHING_PROVIDERS and NON_MATCHING_PROVIDERS constants. """ logger.info('=' * 80) - logger.info('Starting Expiration Reminder Load Test') + title = 'Starting Expiration Reminder Load Test' if providers is None else 'Starting Expiration Reminder Smoke Test' + logger.info(title) logger.info('=' * 80) + expected_sent = None try: if not skip_data_load: - # Step 1: Create providers with privileges expiring in 30 days - logger.info(f'Step 1: Creating {PROVIDERS_30_DAYS} providers with privileges expiring in 30 days...') - create_providers_batch(PROVIDERS_30_DAYS, days_until_expiration=30, progress_log_interval=1000) - logger.info(f'✓ Created {PROVIDERS_30_DAYS} providers for 30-day expiration') - - # Step 2: Create providers with privileges expiring in 10 days - logger.info(f'Step 2: Creating {PROVIDERS_10_DAYS} providers with privileges expiring in 10 days...') - create_providers_batch(PROVIDERS_10_DAYS, days_until_expiration=10, progress_log_interval=1000) - logger.info(f'✓ Created {PROVIDERS_10_DAYS} providers for 10-day expiration') - - total_providers = PROVIDERS_30_DAYS + PROVIDERS_10_DAYS - logger.info(f'Total providers created: {total_providers}') + if providers is not None: + matching_count = max(1, round(providers * 2 / 3)) + non_matching_count = providers - matching_count + else: + matching_count = MATCHING_PROVIDERS + non_matching_count = NON_MATCHING_PROVIDERS + + total = matching_count + non_matching_count + progress_interval = max(1, total // 10) if providers is not None else 1000 + + logger.info( + f'Step 1: Creating {total} providers ({matching_count} matching, {non_matching_count} non-matching)...' + ) + expected_sent = create_providers_batch( + matching_count=matching_count, + non_matching_count=non_matching_count, + target_days_until_expiration=30, + progress_log_interval=progress_interval, + ) + logger.info(f'✓ Created {total} providers') - # Step 3: Wait for DynamoDB stream events to process and index providers into OpenSearch - logger.info('Step 3: Waiting 60 seconds for DynamoDB stream events to process and index providers...') + logger.info('Step 2: Waiting 60 seconds for DynamoDB stream events to process and index providers...') time.sleep(60) logger.info('✓ Waiting complete - providers should now be indexed in OpenSearch') else: logger.info('Skipping data load - using existing providers in database') - # Step 4: Invoke expiration reminder Lambda for 30-day event - logger.info('Step 4: Invoking expiration reminder Lambda for 30-day event...') + logger.info('Step 3: Invoking expiration reminder Lambda for 30-day event...') lambda_start_time = datetime.now(UTC) lambda_response = invoke_expiration_reminder_lambda(days_before=30) lambda_end_time = datetime.now(UTC) @@ -516,7 +546,7 @@ def run_load_test(skip_data_load: bool = False): # Extract and display metrics metrics = lambda_response.get('metrics', {}) logger.info('=' * 80) - logger.info('LOAD TEST RESULTS') + logger.info('LOAD TEST RESULTS' if providers is None else 'SMOKE TEST RESULTS') logger.info('=' * 80) logger.info(f'Lambda Execution Duration: {lambda_duration:.2f} seconds') logger.info(f'Notifications Sent: {metrics.get("sent", 0)}') @@ -527,16 +557,16 @@ def run_load_test(skip_data_load: bool = False): logger.info(f'Matched Privileges: {metrics.get("matchedPrivileges", 0)}') logger.info(f'Providers With Matches: {metrics.get("providersWithMatches", 0)}') - expected_sent = PROVIDERS_30_DAYS actual_sent = metrics.get('sent', 0) - if actual_sent < expected_sent: - logger.warning( - f'⚠️ Only {actual_sent}/{expected_sent} notifications were sent. ' - 'This may indicate the Lambda timed out or encountered errors.' - ) - else: - logger.info(f'✓ Successfully sent {actual_sent} notifications as expected') + if expected_sent is not None: + if actual_sent < expected_sent: + logger.warning( + f'⚠️ Only {actual_sent}/{expected_sent} notifications were sent. ' + 'This may indicate the Lambda timed out or encountered errors.' + ) + else: + logger.info(f'✓ Successfully sent {actual_sent} notifications as expected') if metrics.get('failed', 0) > 0: logger.warning(f'⚠️ {metrics.get("failed", 0)} notifications failed to send') @@ -556,6 +586,14 @@ def run_load_test(skip_data_load: bool = False): if __name__ == '__main__': parser = argparse.ArgumentParser(description='Load test for privilege expiration reminder notifications') + parser.add_argument( + '--providers', + type=int, + metavar='N', + default=None, + help='Smoke mode: total number of fake providers to load (two privileges each). ' + 'Roughly 2/3 will have one privilege expiring on the target date. Works for N >= 1.', + ) parser.add_argument( '--skip-data-load', action='store_true', @@ -563,4 +601,7 @@ def run_load_test(skip_data_load: bool = False): ) args = parser.parse_args() - run_load_test(skip_data_load=args.skip_data_load) + if args.providers is not None and args.providers < 1: + parser.error('--providers must be >= 1') + + run_load_test(skip_data_load=args.skip_data_load, providers=args.providers) From beca60bf3be62f681fccc361cc1a2b9f68892b26 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 30 Jan 2026 12:59:15 -0600 Subject: [PATCH 07/52] WIP - add pagination and break out rules per compact/cycle --- .../search/handlers/expiration_reminders.py | 214 ++++++++++--- .../lambdas/python/search/tests/__init__.py | 2 + .../function/test_expiration_reminders.py | 302 +++++++++++++++--- .../expiration_reminder_stack/__init__.py | 80 ++--- .../app/test_expiration_reminder_stack.py | 57 ++-- 5 files changed, 498 insertions(+), 157 deletions(-) diff --git a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py index ce4a82f45..562a459f9 100644 --- a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json from collections.abc import Generator from dataclasses import dataclass, replace from datetime import UTC, date, datetime, timedelta @@ -16,6 +17,10 @@ DEFAULT_PAGE_SIZE = 1000 +# Pagination / continuation: invoke self when remaining time is below this (ms) +TIMEOUT_BUFFER_MS = 120_000 # 2 minutes +MAX_CONTINUATION_DEPTH = 100 # Safety limit + # Map days before expiration to ExpirationEventType DAYS_BEFORE_TO_EVENT_TYPE = { 30: ExpirationEventType.PRIVILEGE_EXPIRATION_30_DAY, @@ -52,30 +57,71 @@ def as_dict(self) -> dict[str, int]: } -def process_expiration_reminders(event: dict, context: LambdaContext): # noqa: ARG001 unused-argument +@dataclass +class PaginatedProviderResult: + """Provider document with pagination cursor for continuation support.""" + + provider_doc: dict + search_after: list + + +def _initialize_metrics(accumulated: dict[str, int] | None) -> Metrics: + """Initialize metrics, merging any accumulated metrics from previous invocations.""" + if not accumulated: + return Metrics() + return Metrics( + sent=accumulated.get('sent', 0), + skipped=accumulated.get('skipped', 0), + failed=accumulated.get('failed', 0), + already_sent=accumulated.get('alreadySent', 0), + no_email=accumulated.get('noEmail', 0), + matched_privileges=accumulated.get('matchedPrivileges', 0), + providers_with_matches=accumulated.get('providersWithMatches', 0), + ) + + +def process_expiration_reminders(event: dict, context: LambdaContext): """ Process privilege expiration reminders: - Query OpenSearch (paginated) for privileges expiring on target date - Check idempotency tracker for each provider - Send email notification if not already sent - Record success/failure for idempotency + - Invoke self with pagination state when approaching 15-minute timeout Event format: { "daysBefore": 30, # Days before expiration (30, 7, or 0) - required + "compact": "aslp", # Compact to process - required "targetDate": "2026-02-16", # Optional: Expiration date to process # (if not provided, calculated as today + daysBefore) "scheduledTime": "2026-01-17..." # Optional: When rule triggered (for logging, defaults to current time) + "_continuation": { ... } # Internal: set when self-invoking for pagination + } """ try: days_before = event['daysBefore'] - except KeyError: - raise CCInvalidRequestException('Missing required field: daysBefore') from None + compact = event['compact'] + except KeyError as e: + raise CCInvalidRequestException(f'Missing required field: {e.args[0]}') from None if days_before not in DAYS_BEFORE_TO_EVENT_TYPE: raise CCInvalidRequestException(f'Invalid daysBefore value: {days_before}. Must be 30, 7, or 0.') + if compact not in config.compacts: + raise CCInvalidRequestException(f'Invalid compact: {compact}. Must be one of {config.compacts}.') + + # Parse continuation state (if this is a continuation invocation) + continuation = event.get('_continuation', {}) + initial_search_after = continuation.get('searchAfter') + continuation_depth = continuation.get('depth', 0) + accumulated_metrics = continuation.get('accumulatedMetrics') + + if continuation_depth >= MAX_CONTINUATION_DEPTH: + logger.error('Max continuation depth exceeded', depth=continuation_depth, compact=compact) + raise RuntimeError(f'Exceeded maximum continuation depth of {MAX_CONTINUATION_DEPTH}') from None + # Calculate targetDate if not provided (today + daysBefore) if 'targetDate' in event: target_date_str = event['targetDate'] @@ -88,67 +134,133 @@ def process_expiration_reminders(event: dict, context: LambdaContext): # noqa: # Get scheduledTime for logging (default to current time if not provided) scheduled_time = event.get('scheduledTime', datetime.now(UTC).isoformat()) - event_type = DAYS_BEFORE_TO_EVENT_TYPE[days_before] logger.info( 'Processing privilege expiration reminders', + compact=compact, target_date=target_date_str, days_before=days_before, event_type=event_type, scheduled_time=scheduled_time, + continuation_depth=continuation_depth, ) - metrics = Metrics() + metrics = _initialize_metrics(accumulated_metrics) + compact_provider_count = 0 + logger.info('Starting processing for compact', compact=compact, target_date=target_date_str) - for compact in config.compacts: - compact_provider_count = 0 - logger.info('Starting processing for compact', compact=compact, target_date=target_date_str) + for result in iterate_privileges_by_expiration_date( + compact=compact, + expiration_date=expiration_date, + page_size=DEFAULT_PAGE_SIZE, + initial_search_after=initial_search_after, + ): + compact_provider_count += 1 + metrics = replace( + metrics, + providers_with_matches=metrics.providers_with_matches + 1, + ) - for provider_doc in iterate_privileges_by_expiration_date( + provider_result = _process_provider_notification( compact=compact, + provider_doc=result.provider_doc, expiration_date=expiration_date, - page_size=DEFAULT_PAGE_SIZE, - ): - compact_provider_count += 1 - metrics = replace( - metrics, - providers_with_matches=metrics.providers_with_matches + 1, - ) + event_type=event_type, + ) + metrics = replace( + metrics, + sent=metrics.sent + provider_result['sent'], + skipped=metrics.skipped + provider_result['skipped'], + failed=metrics.failed + provider_result['failed'], + already_sent=metrics.already_sent + provider_result['already_sent'], + no_email=metrics.no_email + provider_result['no_email'], + ) - result = _process_provider_notification( + # Check if approaching timeout; invoke continuation if so + if context.get_remaining_time_in_millis() < TIMEOUT_BUFFER_MS: + logger.info( + 'Approaching timeout, invoking continuation', compact=compact, - provider_doc=provider_doc, - expiration_date=expiration_date, - event_type=event_type, + providers_processed=compact_provider_count, + remaining_time_ms=context.get_remaining_time_in_millis(), ) - metrics = replace( - metrics, - sent=metrics.sent + result['sent'], - skipped=metrics.skipped + result['skipped'], - failed=metrics.failed + result['failed'], - already_sent=metrics.already_sent + result['already_sent'], - no_email=metrics.no_email + result['no_email'], + return _invoke_continuation( + event=event, + context=context, + search_after=result.search_after, + metrics=metrics, + depth=continuation_depth, + ) + + # Log progress every 100 providers processed + if compact_provider_count % 100 == 0: + logger.info( + 'Progress update', + compact=compact, + providers_processed=compact_provider_count, + metrics=metrics.as_dict(), ) - # Log progress every 100 providers per compact - if compact_provider_count % 100 == 0: - logger.info( - 'Progress update', - compact=compact, - providers_processed=compact_provider_count, - metrics=metrics.as_dict(), - ) + logger.info( + 'Completed processing for compact', + compact=compact, + total_providers_processed=compact_provider_count, + metrics=metrics.as_dict(), + ) + return { + 'status': 'complete', + 'targetDate': target_date_str, + 'daysBefore': days_before, + 'compact': compact, + 'metrics': metrics.as_dict(), + 'totalInvocations': continuation_depth + 1, + } - logger.info( - 'Completed processing for compact', - compact=compact, - total_providers_processed=compact_provider_count, - metrics=metrics.as_dict(), - ) - logger.info('Completed processing expiration reminders', metrics=metrics.as_dict()) - return {'targetDate': target_date_str, 'daysBefore': days_before, 'metrics': metrics.as_dict()} +def _invoke_continuation( + *, + event: dict, + context: LambdaContext, + search_after: list, + metrics: Metrics, + depth: int, +) -> dict: + """Invoke this Lambda asynchronously to continue processing with pagination state.""" + continuation_event = { + 'daysBefore': event['daysBefore'], + 'compact': event['compact'], + 'targetDate': event.get('targetDate'), + 'scheduledTime': event.get('scheduledTime'), + '_continuation': { + 'searchAfter': search_after, + 'depth': depth + 1, + 'accumulatedMetrics': metrics.as_dict(), + }, + } + continuation_event = {k: v for k, v in continuation_event.items() if v is not None} + + logger.info( + 'Invoking continuation', + compact=event['compact'], + next_depth=depth + 1, + current_metrics=metrics.as_dict(), + remaining_time_ms=context.get_remaining_time_in_millis(), + ) + + config.lambda_client.invoke( + FunctionName=context.function_name, + InvocationType='Event', + Payload=json.dumps(continuation_event), + ) + + return { + 'status': 'continued', + 'compact': event['compact'], + 'nextInvocationDepth': depth + 1, + 'resumeFrom': {'searchAfter': search_after}, + 'metricsAtContinuation': metrics.as_dict(), + } def _process_provider_notification( @@ -271,16 +383,17 @@ def iterate_privileges_by_expiration_date( compact: str, expiration_date: date, page_size: int = DEFAULT_PAGE_SIZE, -) -> Generator[dict, None, None]: + initial_search_after: list | None = None, +) -> Generator[PaginatedProviderResult, None, None]: """ - Generator yielding provider documents with (potentially) matching privileges. + Generator yielding provider documents with pagination cursors. OpenSearch pagination is handled internally using `search_after`. Results are yielded - one provider at a time, and the current page is consumed by popping a single hit - per iteration so memory usage decreases as the page is processed. + one provider at a time with their cursor for continuation support. Use + initial_search_after to resume from a previous invocation. """ index_name = f'compact_{compact}_providers' - search_after = None + search_after = initial_search_after current_page_hits: list[dict] = [] is_last_page = False @@ -309,7 +422,10 @@ def iterate_privileges_by_expiration_date( current_page_hits = list(reversed(hits)) hit = current_page_hits.pop() - yield _provider_document_from_hit(hit) + yield PaginatedProviderResult( + provider_doc=_provider_document_from_hit(hit), + search_after=hit['sort'], + ) def _provider_document_from_hit(hit: dict) -> dict: diff --git a/backend/compact-connect/lambdas/python/search/tests/__init__.py b/backend/compact-connect/lambdas/python/search/tests/__init__.py index d00640792..96c10c577 100644 --- a/backend/compact-connect/lambdas/python/search/tests/__init__.py +++ b/backend/compact-connect/lambdas/python/search/tests/__init__.py @@ -104,3 +104,5 @@ def setUpClass(cls): cls.config = cc_common.config._Config() # noqa: SLF001 protected-access cc_common.config.config = cls.config cls.mock_context = MagicMock(name='MockLambdaContext', spec=LambdaContext) + # Configure mock_context.get_remaining_time_in_millis to return large value by default + cls.mock_context.get_remaining_time_in_millis.return_value = 900_000 # 15 minutes in ms diff --git a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py index b4cf5377e..2f96d1e24 100644 --- a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py @@ -1,3 +1,4 @@ +import json from datetime import UTC, date, datetime, timedelta from unittest.mock import MagicMock, patch from uuid import uuid4 @@ -58,17 +59,55 @@ def test_iterate_privileges_by_expiration_date_paginates_with_search_after_and_y ) ) - self.assertEqual([r['providerId'] for r in results], ['p1', 'p2', 'p3']) + self.assertEqual([r.provider_doc['providerId'] for r in results], ['p1', 'p2', 'p3']) + self.assertEqual([r.search_after for r in results], [['p1'], ['p2'], ['p3']]) # Verify pagination: second call includes search_after from last hit in page 1 first_call_kwargs = mock_client.search.call_args_list[0].kwargs second_call_kwargs = mock_client.search.call_args_list[1].kwargs - self.assertEqual(first_call_kwargs['index_name'], 'compact_aslp_providers') + self.assertEqual('compact_aslp_providers', first_call_kwargs['index_name']) self.assertNotIn('search_after', first_call_kwargs['body']) - self.assertEqual(second_call_kwargs['index_name'], 'compact_aslp_providers') - self.assertEqual(second_call_kwargs['body']['search_after'], ['p2']) + self.assertEqual('compact_aslp_providers', second_call_kwargs['index_name']) + self.assertEqual(['p2'], second_call_kwargs['body']['search_after']) + + def test_iterate_privileges_by_expiration_date_resumes_with_initial_search_after(self): + """When initial_search_after is provided, first OpenSearch query uses it.""" + with ( + patch('handlers.expiration_reminders.opensearch_client') as mock_client, + patch('handlers.expiration_reminders.ProviderGeneralResponseSchema') as mock_schema_class, + ): + mock_client.search = MagicMock( + return_value={ + 'hits': { + 'total': {'value': 1, 'relation': 'eq'}, + 'hits': [ + { + '_source': {'providerId': 'p2', 'privileges': []}, + 'sort': ['p2'], + } + ], + } + } + ) + mock_schema_class.return_value.load.side_effect = lambda doc: doc + + from handlers.expiration_reminders import iterate_privileges_by_expiration_date + + results = list( + iterate_privileges_by_expiration_date( + compact='aslp', + expiration_date=date(2026, 2, 16), + page_size=2, + initial_search_after=['p1'], + ) + ) + + self.assertEqual(1, len(results)) + self.assertEqual('p2', results[0].provider_doc['providerId']) + mock_client.search.assert_called_once() + self.assertEqual(['p1'], mock_client.search.call_args.kwargs['body']['search_after']) def test_iterate_privileges_by_expiration_date_returns_full_provider_document(self): """Provider document includes full privileges list from _source (no inner_hits filtering).""" @@ -112,7 +151,7 @@ def test_iterate_privileges_by_expiration_date_returns_full_provider_document(se from handlers.expiration_reminders import iterate_privileges_by_expiration_date - provider_doc = next( + result = next( iterate_privileges_by_expiration_date( compact='aslp', expiration_date=date(2026, 2, 16), @@ -120,8 +159,9 @@ def test_iterate_privileges_by_expiration_date_returns_full_provider_document(se ) ) - self.assertEqual(provider_doc['providerId'], 'p1') - self.assertEqual(provider_doc['privileges'], full_privileges) + self.assertEqual(result.provider_doc['providerId'], 'p1') + self.assertEqual(result.provider_doc['privileges'], full_privileges) + self.assertEqual(result.search_after, ['p1']) class TestProcessExpirationReminders(TstLambdas): @@ -153,11 +193,12 @@ def _make_provider_doc( doc['compactConnectRegisteredEmailAddress'] = email return doc - def _make_event(self, days_before: int = 30) -> dict: + def _make_event(self, days_before: int = 30, compact: str = 'aslp') -> dict: """Helper to create a valid event for testing.""" return { 'targetDate': '2026-02-16', 'daysBefore': days_before, + 'compact': compact, 'scheduledTime': '2026-01-17T10:00:00Z', } @@ -175,26 +216,30 @@ def test_handler_sends_email_and_records_success(self): mock_tracker_instance.was_already_sent.return_value = False mock_tracker_class.return_value = mock_tracker_instance - mock_iter.return_value = iter([self._make_provider_doc()]) + from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders - from handlers.expiration_reminders import process_expiration_reminders + mock_iter.return_value = iter([ + PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor1']), + ]) resp = process_expiration_reminders(self._make_event(days_before=30), self.mock_context) - self.assertEqual(resp['metrics']['sent'], 1) - self.assertEqual(resp['metrics']['failed'], 0) - self.assertEqual(resp['daysBefore'], 30) + self.assertEqual('complete', resp['status']) + self.assertEqual(1, resp['metrics']['sent']) + self.assertEqual(0, resp['metrics']['failed']) + self.assertEqual(30, resp['daysBefore']) + self.assertEqual('aslp', resp['compact']) mock_email_client.send_privilege_expiration_reminder_email.assert_called_once() call_kwargs = mock_email_client.send_privilege_expiration_reminder_email.call_args.kwargs tv = call_kwargs['template_variables'] - self.assertEqual(tv.provider_first_name, 'John') - self.assertEqual(tv.expiration_date.isoformat(), '2026-02-16') - self.assertEqual(len(tv.privileges), 1) + self.assertEqual('John', tv.provider_first_name) + self.assertEqual('2026-02-16', tv.expiration_date.isoformat()) + self.assertEqual(1, len(tv.privileges)) priv = tv.privileges[0] - self.assertEqual(priv['jurisdiction'], 'Ohio', 'jurisdiction must be full state name') - self.assertEqual(priv['licenseType'], 'aud') - self.assertEqual(priv['privilegeId'], 'a') - self.assertEqual(priv['dateOfExpiration'], '2026-02-16', 'dateOfExpiration must be ISO 8601') + self.assertEqual('Ohio', priv['jurisdiction'], 'jurisdiction must be full state name') + self.assertEqual('aud', priv['licenseType']) + self.assertEqual('a', priv['privilegeId']) + self.assertEqual('2026-02-16', priv['dateOfExpiration'], 'dateOfExpiration must be ISO 8601') mock_tracker_instance.record_success.assert_called_once() # Verify tracker was created with correct event_type @@ -202,7 +247,7 @@ def test_handler_sends_email_and_records_success(self): mock_tracker_class.assert_called_once() tracker_call = mock_tracker_class.call_args.kwargs - self.assertEqual(tracker_call['event_type'], ExpirationEventType.PRIVILEGE_EXPIRATION_30_DAY) + self.assertEqual(ExpirationEventType.PRIVILEGE_EXPIRATION_30_DAY, tracker_call['event_type']) def test_handler_skips_when_already_sent(self): with ( @@ -218,14 +263,17 @@ def test_handler_skips_when_already_sent(self): mock_tracker_instance.was_already_sent.return_value = True mock_tracker_class.return_value = mock_tracker_instance - mock_iter.return_value = iter([self._make_provider_doc()]) + from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders - from handlers.expiration_reminders import process_expiration_reminders + mock_iter.return_value = iter([ + PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor1']), + ]) resp = process_expiration_reminders(self._make_event(days_before=7), self.mock_context) - self.assertEqual(resp['metrics']['sent'], 0) - self.assertEqual(resp['metrics']['alreadySent'], 1) + self.assertEqual('complete', resp['status']) + self.assertEqual(0, resp['metrics']['sent']) + self.assertEqual(1, resp['metrics']['alreadySent']) mock_email_client.send_privilege_expiration_reminder_email.assert_not_called() def test_handler_logs_error_for_provider_without_email(self): @@ -240,14 +288,20 @@ def test_handler_logs_error_for_provider_without_email(self): mock_config.email_service_client = mock_email_client # Provider without email - mock_iter.return_value = iter([self._make_provider_doc(email=None)]) + from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders - from handlers.expiration_reminders import process_expiration_reminders + mock_iter.return_value = iter([ + PaginatedProviderResult( + provider_doc=self._make_provider_doc(email=None), + search_after=['cursor1'], + ), + ]) resp = process_expiration_reminders(self._make_event(days_before=0), self.mock_context) - self.assertEqual(resp['metrics']['sent'], 0) - self.assertEqual(resp['metrics']['noEmail'], 1) + self.assertEqual('complete', resp['status']) + self.assertEqual(0, resp['metrics']['sent']) + self.assertEqual(1, resp['metrics']['noEmail']) mock_email_client.send_privilege_expiration_reminder_email.assert_not_called() mock_tracker_class.assert_not_called() # Verify error was logged (not just debug) @@ -268,14 +322,17 @@ def test_handler_records_failure_on_email_error(self): mock_tracker_instance.was_already_sent.return_value = False mock_tracker_class.return_value = mock_tracker_instance - mock_iter.return_value = iter([self._make_provider_doc()]) + from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders - from handlers.expiration_reminders import process_expiration_reminders + mock_iter.return_value = iter([ + PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor1']), + ]) resp = process_expiration_reminders(self._make_event(), self.mock_context) - self.assertEqual(resp['metrics']['sent'], 0) - self.assertEqual(resp['metrics']['failed'], 1) + self.assertEqual('complete', resp['status']) + self.assertEqual(0, resp['metrics']['sent']) + self.assertEqual(1, resp['metrics']['failed']) mock_tracker_instance.record_failure.assert_called_once() self.assertIn('Email service down', mock_tracker_instance.record_failure.call_args.kwargs['error_message']) @@ -295,16 +352,19 @@ def test_handler_records_failure_when_privilege_has_unknown_jurisdiction(self): mock_tracker_class.return_value = mock_tracker_instance # Privilege with jurisdiction not in CompactConfigUtility.JURISDICTION_NAME_MAPPING + from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders + doc = self._make_provider_doc() doc['privileges'][0]['jurisdiction'] = 'xx' - mock_iter.return_value = iter([doc]) - - from handlers.expiration_reminders import process_expiration_reminders + mock_iter.return_value = iter([ + PaginatedProviderResult(provider_doc=doc, search_after=['cursor1']), + ]) resp = process_expiration_reminders(self._make_event(), self.mock_context) - self.assertEqual(resp['metrics']['sent'], 0) - self.assertEqual(resp['metrics']['failed'], 1) + self.assertEqual('complete', resp['status']) + self.assertEqual(0, resp['metrics']['sent']) + self.assertEqual(1, resp['metrics']['failed']) mock_tracker_instance.record_failure.assert_called_once() failure_msg = mock_tracker_instance.record_failure.call_args.kwargs['error_message'] self.assertIn('Unknown jurisdiction', failure_msg) @@ -315,7 +375,12 @@ def test_handler_validates_days_before_value(self): with self.assertRaises(CCInvalidRequestException) as ctx: process_expiration_reminders( - {'targetDate': '2026-02-16', 'daysBefore': 15, 'scheduledTime': '2026-01-17T10:00:00Z'}, + { + 'targetDate': '2026-02-16', + 'daysBefore': 15, + 'compact': 'aslp', + 'scheduledTime': '2026-01-17T10:00:00Z', + }, self.mock_context, ) @@ -328,12 +393,42 @@ def test_handler_requires_days_before_field(self): with self.assertRaises(CCInvalidRequestException) as ctx: process_expiration_reminders( - {'targetDate': '2026-02-16', 'scheduledTime': '2026-01-17T10:00:00Z'}, + {'targetDate': '2026-02-16', 'compact': 'aslp', 'scheduledTime': '2026-01-17T10:00:00Z'}, self.mock_context, ) self.assertIn('daysBefore', str(ctx.exception)) + def test_handler_requires_compact_field(self): + from cc_common.exceptions import CCInvalidRequestException + from handlers.expiration_reminders import process_expiration_reminders + + with self.assertRaises(CCInvalidRequestException) as ctx: + process_expiration_reminders( + {'targetDate': '2026-02-16', 'daysBefore': 30, 'scheduledTime': '2026-01-17T10:00:00Z'}, + self.mock_context, + ) + + self.assertIn('compact', str(ctx.exception)) + + def test_handler_validates_compact_value(self): + from cc_common.exceptions import CCInvalidRequestException + from handlers.expiration_reminders import process_expiration_reminders + + with ( + patch('handlers.expiration_reminders.config') as mock_config, + ): + mock_config.compacts = ['aslp', 'coun'] + + with self.assertRaises(CCInvalidRequestException) as ctx: + process_expiration_reminders( + self._make_event(compact='invalid'), + self.mock_context, + ) + + self.assertIn('invalid', str(ctx.exception)) + self.assertIn('Must be one of', str(ctx.exception)) + def test_handler_calculates_target_date_from_days_before_when_not_provided(self): """Test that handler calculates targetDate from daysBefore when targetDate is not provided.""" @@ -356,20 +451,131 @@ def test_handler_calculates_target_date_from_days_before_when_not_provided(self) provider_doc = self._make_provider_doc() provider_doc['privileges'][0]['dateOfExpiration'] = target_date.isoformat() - mock_iter.return_value = iter([provider_doc]) + from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders - from handlers.expiration_reminders import process_expiration_reminders + mock_iter.return_value = iter([ + PaginatedProviderResult(provider_doc=provider_doc, search_after=['cursor1']), + ]) - # Event with only daysBefore (no targetDate) - event = {'daysBefore': 30} + # Event with only daysBefore and compact (no targetDate) + event = {'daysBefore': 30, 'compact': 'aslp'} resp = process_expiration_reminders(event, self.mock_context) # Verify it calculated the correct target date expected_target_date = (today + timedelta(days=30)).isoformat() - self.assertEqual(resp['targetDate'], expected_target_date) - self.assertEqual(resp['daysBefore'], 30) + self.assertEqual(expected_target_date, resp['targetDate']) + self.assertEqual(30, resp['daysBefore']) - # Verify the generator was called with the calculated date + # Verify the generator was called with the calculated date and no initial_search_after mock_iter.assert_called_once() call_kwargs = mock_iter.call_args.kwargs - self.assertEqual(call_kwargs['expiration_date'], target_date) + self.assertEqual(target_date, call_kwargs['expiration_date']) + self.assertIsNone(call_kwargs.get('initial_search_after')) + + def test_handler_continuation_parses_accumulated_metrics_and_search_after(self): + """Continuation event restores metrics and passes initial_search_after to generator.""" + with ( + patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') as mock_iter, + patch('handlers.expiration_reminders.config') as mock_config, + patch('handlers.expiration_reminders.ExpirationReminderTracker') as mock_tracker_class, + ): + mock_config.compacts = ['aslp'] + mock_config.email_service_client = MagicMock() + + mock_tracker_instance = MagicMock() + mock_tracker_instance.was_already_sent.return_value = False + mock_tracker_class.return_value = mock_tracker_instance + + from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders + + mock_iter.return_value = iter([ + PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor2']), + ]) + + event = { + 'targetDate': '2026-02-16', + 'daysBefore': 30, + 'compact': 'aslp', + '_continuation': { + 'searchAfter': ['cursor1'], + 'depth': 1, + 'accumulatedMetrics': { + 'sent': 100, + 'skipped': 2, + 'failed': 1, + 'alreadySent': 5, + 'noEmail': 0, + 'matchedPrivileges': 108, + 'providersWithMatches': 108, + }, + }, + } + resp = process_expiration_reminders(event, self.mock_context) + + self.assertEqual('complete', resp['status']) + self.assertEqual(101, resp['metrics']['sent']) + self.assertEqual(2, resp['metrics']['skipped']) + self.assertEqual(5, resp['metrics']['alreadySent']) + self.assertEqual(2, resp['totalInvocations']) + mock_iter.assert_called_once() + call_kwargs = mock_iter.call_args.kwargs + self.assertEqual(['cursor1'], call_kwargs['initial_search_after']) + + def test_handler_invokes_itself_with_pagination_values_when_reaching_limit(self): + """When remaining time is below threshold, handler invokes self and returns continued.""" + with ( + patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') as mock_iter, + patch('handlers.expiration_reminders.config') as mock_config, + patch('handlers.expiration_reminders.ExpirationReminderTracker') as mock_tracker_class, + ): + mock_config.compacts = ['aslp'] + mock_config.email_service_client = MagicMock() + mock_lambda_client = MagicMock() + mock_config.lambda_client = mock_lambda_client + + mock_tracker_instance = MagicMock() + mock_tracker_instance.was_already_sent.return_value = False + mock_tracker_class.return_value = mock_tracker_instance + + from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders + + mock_iter.return_value = iter([ + PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor1']), + ]) + + mock_context = MagicMock() + mock_context.get_remaining_time_in_millis.return_value = 60_000 # 1 minute left + + resp = process_expiration_reminders(self._make_event(), mock_context) + + self.assertEqual('continued', resp['status']) + self.assertEqual(1, resp['nextInvocationDepth']) + self.assertEqual(['cursor1'], resp['resumeFrom']['searchAfter']) + self.assertEqual(1, resp['metricsAtContinuation']['sent']) + mock_lambda_client.invoke.assert_called_once() + call_kwargs = mock_lambda_client.invoke.call_args.kwargs + self.assertEqual('Event', call_kwargs['InvocationType']) + payload = json.loads(call_kwargs['Payload']) + self.assertEqual(30, payload['daysBefore']) + self.assertEqual('aslp', payload['compact']) + self.assertEqual(['cursor1'], payload['_continuation']['searchAfter']) + self.assertEqual(1, payload['_continuation']['depth']) + self.assertEqual(1, payload['_continuation']['accumulatedMetrics']['sent']) + + def test_handler_max_continuation_depth_raises(self): + """When continuation depth >= MAX_CONTINUATION_DEPTH, handler raises RuntimeError.""" + from handlers.expiration_reminders import MAX_CONTINUATION_DEPTH, process_expiration_reminders + + event = { + 'targetDate': '2026-02-16', + 'daysBefore': 30, + 'compact': 'aslp', + '_continuation': { + 'searchAfter': ['cursor1'], + 'depth': MAX_CONTINUATION_DEPTH, + 'accumulatedMetrics': {}, + }, + } + with self.assertRaises(RuntimeError) as ctx: + process_expiration_reminders(event, self.mock_context) + self.assertIn(str(MAX_CONTINUATION_DEPTH), str(ctx.exception)) diff --git a/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py b/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py index 6417c2b74..77aa644a6 100644 --- a/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py +++ b/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py @@ -1,6 +1,8 @@ +import json import os from aws_cdk import Duration +from aws_cdk import aws_iam as iam from aws_cdk.aws_cloudwatch import Alarm, ComparisonOperator, Stats, TreatMissingData from aws_cdk.aws_cloudwatch_actions import SnsAction from aws_cdk.aws_ec2 import SubnetSelection @@ -24,7 +26,7 @@ class ExpirationReminderStack(AppStack): This stack provides scheduled email notifications to providers about expiring privileges: - Lambda function for processing expiration reminders - - Three EventBridge rules (30-day, 7-day, day-of) that run daily + - EventBridge rules per compact and reminder type (30-day, 7-day, day-of) that run daily - CloudWatch alarms for errors and execution duration - OpenSearch integration for querying privileges by expiration date """ @@ -79,6 +81,20 @@ def __init__( # Invoke permission for email notification service persistent_stack.email_notification_service_lambda.grant_invoke(self.expiration_reminder_handler) + # Self-invocation permission for pagination when execution approaches 15-minute timeout + # Use standalone policy to avoid circular dependency when Lambda invokes itself + # see https://github.com/aws/aws-cdk/issues/11020#issuecomment-842946562 + self_invoke_statement = iam.PolicyStatement( + actions=['lambda:InvokeFunction'], + resources=[self.expiration_reminder_handler.function_arn], + ) + self_invoke_policy = iam.Policy( + self, + 'ExpirationReminderSelfInvokePolicy', + statements=[self_invoke_statement], + ) + self_invoke_policy.attach_to_role(self.expiration_reminder_handler.role) + NagSuppressions.add_resource_suppressions_by_path( self, f'{self.expiration_reminder_handler.role.node.path}/DefaultPolicy/Resource', @@ -92,47 +108,31 @@ def __init__( ], ) - # Create EventBridge rules for each reminder type + # Create EventBridge rules per compact and reminder type # All rules run daily at midnight UTC-4 (4:00 AM UTC) to process reminders for privileges expiring on the - # calculated target date - Rule( - self, - 'ExpirationReminder30DayRule', - description='Daily rule to send 30-day expiration reminders', - schedule=Schedule.cron(week_day='*', hour='4', minute='0', month='*', year='*'), - targets=[ - LambdaFunction( - handler=self.expiration_reminder_handler, - event=RuleTargetInput.from_object({'daysBefore': 30}), + # calculated target date. Each invocation processes a single compact. + reminder_configs = [ + {'days_before': 30, 'suffix': '30Day'}, + {'days_before': 7, 'suffix': '7Day'}, + {'days_before': 0, 'suffix': 'DayOf'}, + ] + for compact in json.loads(self.common_env_vars['COMPACTS']): + for reminder_config in reminder_configs: + Rule( + self, + f'ExpirationReminder{reminder_config["suffix"]}Rule{compact.upper()}', + description=f'Daily rule to send {reminder_config["days_before"]}-day expiration reminders for {compact}', + schedule=Schedule.cron(week_day='*', hour='4', minute='0', month='*', year='*'), + targets=[ + LambdaFunction( + handler=self.expiration_reminder_handler, + event=RuleTargetInput.from_object({ + 'daysBefore': reminder_config['days_before'], + 'compact': compact, + }), + ) + ], ) - ], - ) - - Rule( - self, - 'ExpirationReminder7DayRule', - description='Daily rule to send 7-day expiration reminders', - schedule=Schedule.cron(week_day='*', hour='4', minute='0', month='*', year='*'), - targets=[ - LambdaFunction( - handler=self.expiration_reminder_handler, - event=RuleTargetInput.from_object({'daysBefore': 7}), - ) - ], - ) - - Rule( - self, - 'ExpirationReminderDayOfRule', - description='Daily rule to send day-of expiration reminders', - schedule=Schedule.cron(week_day='*', hour='4', minute='0', month='*', year='*'), - targets=[ - LambdaFunction( - handler=self.expiration_reminder_handler, - event=RuleTargetInput.from_object({'daysBefore': 0}), - ) - ], - ) # CloudWatch alarm for Lambda errors Alarm( diff --git a/backend/compact-connect/tests/app/test_expiration_reminder_stack.py b/backend/compact-connect/tests/app/test_expiration_reminder_stack.py index 6da7b1fa6..adbebb541 100644 --- a/backend/compact-connect/tests/app/test_expiration_reminder_stack.py +++ b/backend/compact-connect/tests/app/test_expiration_reminder_stack.py @@ -51,7 +51,7 @@ def test_lambda_function_created_with_correct_timeout(self): self.assertEqual(handler_properties['Handler'], 'handlers.expiration_reminders.process_expiration_reminders') def test_eventbridge_rules_created(self): - """Test that all three EventBridge rules (30-day, 7-day, day-of) are created.""" + """Test that EventBridge rules per compact and reminder type (30-day, 7-day, day-of) are created.""" # Stack is only created if hosted_zone is configured if not hasattr(self.app.sandbox_backend_stage, 'expiration_reminder_stack'): self.skipTest('ExpirationReminderStack not created (hosted_zone not configured)') @@ -62,30 +62,47 @@ def test_eventbridge_rules_created(self): # Get all EventBridge rules rules = expiration_template.find_resources(CfnRule.CFN_RESOURCE_TYPE_NAME) - # Verify we have exactly 3 rules - self.assertEqual(len(rules), 3, 'Should have exactly 3 EventBridge rules') + # Number of rules = compacts * 3 reminder types (30-day, 7-day, day-of) + context = self.get_context() + compacts = context['compacts'] + expected_rule_count = len(compacts) * 3 + self.assertEqual( + len(rules), + expected_rule_count, + f'Should have {expected_rule_count} EventBridge rules (one per compact per reminder type)', + ) - # Verify all rules have the same schedule (daily at 10:00 AM UTC) and are enabled + # Verify all rules have the same schedule (daily at 4:00 AM UTC) and are enabled handler_logical_id = expiration_stack.get_logical_id( expiration_stack.expiration_reminder_handler.node.default_child ) - for rule_name in ['ExpirationReminder30DayRule', 'ExpirationReminder7DayRule', 'ExpirationReminderDayOfRule']: - rule_logical_id = expiration_stack.get_logical_id( - expiration_stack.node.find_child(rule_name).node.default_child - ) - rule = TestExpirationReminderStack.get_resource_properties_by_logical_id(rule_logical_id, resources=rules) - - self.assertEqual(rule['ScheduleExpression'], 'cron(0 4 ? * * *)') # Daily at midnight UTC-4 (4:00 AM UTC) - self.assertEqual(rule['State'], 'ENABLED') - # Verify the rule targets the Lambda function - # The Arn is a GetAtt reference to the Lambda function - target_arn = rule['Targets'][0]['Arn'] - if isinstance(target_arn, dict) and 'Fn::GetAtt' in target_arn: - self.assertEqual(target_arn['Fn::GetAtt'][0], handler_logical_id) - else: - # Fallback: just verify the target exists - self.assertIn('Arn', rule['Targets'][0]) + reminder_configs = [ + {'suffix': '30Day'}, + {'suffix': '7Day'}, + {'suffix': 'DayOf'}, + ] + for compact in compacts: + for reminder_config in reminder_configs: + rule_name = f'ExpirationReminder{reminder_config["suffix"]}Rule{compact.upper()}' + rule_logical_id = expiration_stack.get_logical_id( + expiration_stack.node.find_child(rule_name).node.default_child + ) + rule = TestExpirationReminderStack.get_resource_properties_by_logical_id( + rule_logical_id, resources=rules + ) + + self.assertEqual( + rule['ScheduleExpression'], + 'cron(0 4 ? * * *)', # Daily at midnight UTC-4 (4:00 AM UTC) + ) + self.assertEqual(rule['State'], 'ENABLED') + # Verify the rule targets the Lambda function + target_arn = rule['Targets'][0]['Arn'] + if isinstance(target_arn, dict) and 'Fn::GetAtt' in target_arn: + self.assertEqual(target_arn['Fn::GetAtt'][0], handler_logical_id) + else: + self.assertIn('Arn', rule['Targets'][0]) def test_duration_alarm_configured(self): """Test that the duration alarm is configured with a 10-minute threshold.""" From 7e36a51b4edb10c5d318135f063e533b5a22a52f Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 30 Jan 2026 13:32:00 -0600 Subject: [PATCH 08/52] Update test patches/get tests passing --- .../function/test_expiration_reminders.py | 898 +++++++++--------- 1 file changed, 437 insertions(+), 461 deletions(-) diff --git a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py index 2f96d1e24..afb334682 100644 --- a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py @@ -1,169 +1,171 @@ import json -from datetime import UTC, date, datetime, timedelta +from datetime import date, timedelta from unittest.mock import MagicMock, patch from uuid import uuid4 +from moto import mock_aws + from tests import TstLambdas +@mock_aws class TestExpirationRemindersOpenSearch(TstLambdas): """Tests for OpenSearch query and data extraction.""" - def test_iterate_privileges_by_expiration_date_paginates_with_search_after_and_yields_in_order(self): - # Patch the module-level opensearch_client and schema load (validation would require full fixture) - with ( - patch('handlers.expiration_reminders.opensearch_client') as mock_client, - patch('handlers.expiration_reminders.ProviderGeneralResponseSchema') as mock_schema_class, - ): - mock_client.search = MagicMock() - mock_schema_class.return_value.load.side_effect = lambda doc: doc - - # Page 1: p1, p2 - mock_client.search.side_effect = [ - { - 'hits': { - 'total': {'value': 3, 'relation': 'eq'}, - 'hits': [ - { - '_source': {'providerId': 'p1', 'privileges': []}, - 'sort': ['p1'], - }, - { - '_source': {'providerId': 'p2', 'privileges': []}, - 'sort': ['p2'], - }, - ], - } - }, - # Page 2: p3 - { - 'hits': { - 'total': {'value': 3, 'relation': 'eq'}, - 'hits': [ - { - '_source': {'providerId': 'p3', 'privileges': []}, - 'sort': ['p3'], - } - ], - } - }, - ] + @patch('handlers.expiration_reminders.ProviderGeneralResponseSchema') + @patch('handlers.expiration_reminders.opensearch_client') + def test_iterate_privileges_by_expiration_date_paginates_with_search_after_and_yields_in_order( + self, mock_client, mock_schema_class + ): + mock_client.search = MagicMock() + mock_schema_class.return_value.load.side_effect = lambda doc: doc + + # Page 1: p1, p2 + mock_client.search.side_effect = [ + { + 'hits': { + 'total': {'value': 3, 'relation': 'eq'}, + 'hits': [ + { + '_source': {'providerId': 'p1', 'privileges': []}, + 'sort': ['p1'], + }, + { + '_source': {'providerId': 'p2', 'privileges': []}, + 'sort': ['p2'], + }, + ], + } + }, + # Page 2: p3 + { + 'hits': { + 'total': {'value': 3, 'relation': 'eq'}, + 'hits': [ + { + '_source': {'providerId': 'p3', 'privileges': []}, + 'sort': ['p3'], + } + ], + } + }, + ] - from handlers.expiration_reminders import iterate_privileges_by_expiration_date + from handlers.expiration_reminders import iterate_privileges_by_expiration_date - results = list( - iterate_privileges_by_expiration_date( - compact='aslp', - expiration_date=date(2026, 2, 16), - page_size=2, - ) + results = list( + iterate_privileges_by_expiration_date( + compact='aslp', + expiration_date=date(2026, 2, 16), + page_size=2, ) + ) - self.assertEqual([r.provider_doc['providerId'] for r in results], ['p1', 'p2', 'p3']) - self.assertEqual([r.search_after for r in results], [['p1'], ['p2'], ['p3']]) + self.assertEqual([r.provider_doc['providerId'] for r in results], ['p1', 'p2', 'p3']) + self.assertEqual([r.search_after for r in results], [['p1'], ['p2'], ['p3']]) - # Verify pagination: second call includes search_after from last hit in page 1 - first_call_kwargs = mock_client.search.call_args_list[0].kwargs - second_call_kwargs = mock_client.search.call_args_list[1].kwargs + # Verify pagination: second call includes search_after from last hit in page 1 + first_call_kwargs = mock_client.search.call_args_list[0].kwargs + second_call_kwargs = mock_client.search.call_args_list[1].kwargs - self.assertEqual('compact_aslp_providers', first_call_kwargs['index_name']) - self.assertNotIn('search_after', first_call_kwargs['body']) + self.assertEqual('compact_aslp_providers', first_call_kwargs['index_name']) + self.assertNotIn('search_after', first_call_kwargs['body']) - self.assertEqual('compact_aslp_providers', second_call_kwargs['index_name']) - self.assertEqual(['p2'], second_call_kwargs['body']['search_after']) + self.assertEqual('compact_aslp_providers', second_call_kwargs['index_name']) + self.assertEqual(['p2'], second_call_kwargs['body']['search_after']) - def test_iterate_privileges_by_expiration_date_resumes_with_initial_search_after(self): + @patch('handlers.expiration_reminders.ProviderGeneralResponseSchema') + @patch('handlers.expiration_reminders.opensearch_client') + def test_iterate_privileges_by_expiration_date_resumes_with_initial_search_after( + self, mock_client, mock_schema_class + ): """When initial_search_after is provided, first OpenSearch query uses it.""" - with ( - patch('handlers.expiration_reminders.opensearch_client') as mock_client, - patch('handlers.expiration_reminders.ProviderGeneralResponseSchema') as mock_schema_class, - ): - mock_client.search = MagicMock( - return_value={ - 'hits': { - 'total': {'value': 1, 'relation': 'eq'}, - 'hits': [ - { - '_source': {'providerId': 'p2', 'privileges': []}, - 'sort': ['p2'], - } - ], - } + mock_client.search = MagicMock( + return_value={ + 'hits': { + 'total': {'value': 1, 'relation': 'eq'}, + 'hits': [ + { + '_source': {'providerId': 'p2', 'privileges': []}, + 'sort': ['p2'], + } + ], } - ) - mock_schema_class.return_value.load.side_effect = lambda doc: doc + } + ) + mock_schema_class.return_value.load.side_effect = lambda doc: doc - from handlers.expiration_reminders import iterate_privileges_by_expiration_date + from handlers.expiration_reminders import iterate_privileges_by_expiration_date - results = list( - iterate_privileges_by_expiration_date( - compact='aslp', - expiration_date=date(2026, 2, 16), - page_size=2, - initial_search_after=['p1'], - ) + results = list( + iterate_privileges_by_expiration_date( + compact='aslp', + expiration_date=date(2026, 2, 16), + page_size=2, + initial_search_after=['p1'], ) - - self.assertEqual(1, len(results)) - self.assertEqual('p2', results[0].provider_doc['providerId']) - mock_client.search.assert_called_once() - self.assertEqual(['p1'], mock_client.search.call_args.kwargs['body']['search_after']) - - def test_iterate_privileges_by_expiration_date_returns_full_provider_document(self): + ) + + self.assertEqual(1, len(results)) + self.assertEqual('p2', results[0].provider_doc['providerId']) + mock_client.search.assert_called_once() + self.assertEqual(['p1'], mock_client.search.call_args.kwargs['body']['search_after']) + + @patch('handlers.expiration_reminders.ProviderGeneralResponseSchema') + @patch('handlers.expiration_reminders.opensearch_client') + def test_iterate_privileges_by_expiration_date_returns_full_provider_document( + self, mock_client, mock_schema_class + ): """Provider document includes full privileges list from _source (no inner_hits filtering).""" - with ( - patch('handlers.expiration_reminders.opensearch_client') as mock_client, - patch('handlers.expiration_reminders.ProviderGeneralResponseSchema') as mock_schema_class, - ): - full_privileges = [ - { - 'privilegeId': 'a', - 'jurisdiction': 'oh', - 'licenseType': 'aud', - 'dateOfExpiration': '2026-02-16', - 'status': 'active', - }, - { - 'privilegeId': 'b', - 'jurisdiction': 'ky', - 'licenseType': 'slp', - 'dateOfExpiration': '2026-03-01', - 'status': 'active', - }, - ] - mock_client.search = MagicMock( - return_value={ - 'hits': { - 'total': {'value': 1, 'relation': 'eq'}, - 'hits': [ - { - '_source': { - 'providerId': 'p1', - 'privileges': full_privileges, - }, - 'sort': ['p1'], - } - ], - } + full_privileges = [ + { + 'privilegeId': 'a', + 'jurisdiction': 'oh', + 'licenseType': 'aud', + 'dateOfExpiration': '2026-02-16', + 'status': 'active', + }, + { + 'privilegeId': 'b', + 'jurisdiction': 'ky', + 'licenseType': 'slp', + 'dateOfExpiration': '2026-03-01', + 'status': 'active', + }, + ] + mock_client.search = MagicMock( + return_value={ + 'hits': { + 'total': {'value': 1, 'relation': 'eq'}, + 'hits': [ + { + '_source': { + 'providerId': 'p1', + 'privileges': full_privileges, + }, + 'sort': ['p1'], + } + ], } - ) - mock_schema_class.return_value.load.side_effect = lambda doc: doc + } + ) + mock_schema_class.return_value.load.side_effect = lambda doc: doc - from handlers.expiration_reminders import iterate_privileges_by_expiration_date + from handlers.expiration_reminders import iterate_privileges_by_expiration_date - result = next( - iterate_privileges_by_expiration_date( - compact='aslp', - expiration_date=date(2026, 2, 16), - page_size=100, - ) + result = next( + iterate_privileges_by_expiration_date( + compact='aslp', + expiration_date=date(2026, 2, 16), + page_size=100, ) + ) - self.assertEqual(result.provider_doc['providerId'], 'p1') - self.assertEqual(result.provider_doc['privileges'], full_privileges) - self.assertEqual(result.search_after, ['p1']) + self.assertEqual('p1', result.provider_doc['providerId']) + self.assertEqual(full_privileges, result.provider_doc['privileges']) +@mock_aws class TestProcessExpirationReminders(TstLambdas): """Tests for the main handler with email sending and idempotency.""" @@ -202,172 +204,162 @@ def _make_event(self, days_before: int = 30, compact: str = 'aslp') -> dict: 'scheduledTime': '2026-01-17T10:00:00Z', } - def test_handler_sends_email_and_records_success(self): - with ( - patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') as mock_iter, - patch('handlers.expiration_reminders.config') as mock_config, - patch('handlers.expiration_reminders.ExpirationReminderTracker') as mock_tracker_class, - ): - mock_config.compacts = ['aslp'] - mock_email_client = MagicMock() - mock_config.email_service_client = mock_email_client - - mock_tracker_instance = MagicMock() - mock_tracker_instance.was_already_sent.return_value = False - mock_tracker_class.return_value = mock_tracker_instance - - from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders - - mock_iter.return_value = iter([ - PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor1']), - ]) - - resp = process_expiration_reminders(self._make_event(days_before=30), self.mock_context) - - self.assertEqual('complete', resp['status']) - self.assertEqual(1, resp['metrics']['sent']) - self.assertEqual(0, resp['metrics']['failed']) - self.assertEqual(30, resp['daysBefore']) - self.assertEqual('aslp', resp['compact']) - mock_email_client.send_privilege_expiration_reminder_email.assert_called_once() - call_kwargs = mock_email_client.send_privilege_expiration_reminder_email.call_args.kwargs - tv = call_kwargs['template_variables'] - self.assertEqual('John', tv.provider_first_name) - self.assertEqual('2026-02-16', tv.expiration_date.isoformat()) - self.assertEqual(1, len(tv.privileges)) - priv = tv.privileges[0] - self.assertEqual('Ohio', priv['jurisdiction'], 'jurisdiction must be full state name') - self.assertEqual('aud', priv['licenseType']) - self.assertEqual('a', priv['privilegeId']) - self.assertEqual('2026-02-16', priv['dateOfExpiration'], 'dateOfExpiration must be ISO 8601') - mock_tracker_instance.record_success.assert_called_once() - - # Verify tracker was created with correct event_type - from expiration_reminder_tracker import ExpirationEventType - - mock_tracker_class.assert_called_once() - tracker_call = mock_tracker_class.call_args.kwargs - self.assertEqual(ExpirationEventType.PRIVILEGE_EXPIRATION_30_DAY, tracker_call['event_type']) - - def test_handler_skips_when_already_sent(self): - with ( - patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') as mock_iter, - patch('handlers.expiration_reminders.config') as mock_config, - patch('handlers.expiration_reminders.ExpirationReminderTracker') as mock_tracker_class, - ): - mock_config.compacts = ['aslp'] - mock_email_client = MagicMock() - mock_config.email_service_client = mock_email_client - - mock_tracker_instance = MagicMock() - mock_tracker_instance.was_already_sent.return_value = True - mock_tracker_class.return_value = mock_tracker_instance - - from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders - - mock_iter.return_value = iter([ - PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor1']), - ]) - - resp = process_expiration_reminders(self._make_event(days_before=7), self.mock_context) - - self.assertEqual('complete', resp['status']) - self.assertEqual(0, resp['metrics']['sent']) - self.assertEqual(1, resp['metrics']['alreadySent']) - mock_email_client.send_privilege_expiration_reminder_email.assert_not_called() - - def test_handler_logs_error_for_provider_without_email(self): - with ( - patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') as mock_iter, - patch('handlers.expiration_reminders.config') as mock_config, - patch('handlers.expiration_reminders.ExpirationReminderTracker') as mock_tracker_class, - patch('handlers.expiration_reminders.logger') as mock_logger, - ): - mock_config.compacts = ['aslp'] - mock_email_client = MagicMock() - mock_config.email_service_client = mock_email_client - - # Provider without email - from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders - - mock_iter.return_value = iter([ - PaginatedProviderResult( - provider_doc=self._make_provider_doc(email=None), - search_after=['cursor1'], - ), - ]) - - resp = process_expiration_reminders(self._make_event(days_before=0), self.mock_context) - - self.assertEqual('complete', resp['status']) - self.assertEqual(0, resp['metrics']['sent']) - self.assertEqual(1, resp['metrics']['noEmail']) - mock_email_client.send_privilege_expiration_reminder_email.assert_not_called() - mock_tracker_class.assert_not_called() - # Verify error was logged (not just debug) - mock_logger.error.assert_called() - - def test_handler_records_failure_on_email_error(self): - with ( - patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') as mock_iter, - patch('handlers.expiration_reminders.config') as mock_config, - patch('handlers.expiration_reminders.ExpirationReminderTracker') as mock_tracker_class, - ): - mock_config.compacts = ['aslp'] - mock_email_client = MagicMock() - mock_email_client.send_privilege_expiration_reminder_email.side_effect = Exception('Email service down') - mock_config.email_service_client = mock_email_client - - mock_tracker_instance = MagicMock() - mock_tracker_instance.was_already_sent.return_value = False - mock_tracker_class.return_value = mock_tracker_instance - - from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders - - mock_iter.return_value = iter([ - PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor1']), - ]) - - resp = process_expiration_reminders(self._make_event(), self.mock_context) - - self.assertEqual('complete', resp['status']) - self.assertEqual(0, resp['metrics']['sent']) - self.assertEqual(1, resp['metrics']['failed']) - mock_tracker_instance.record_failure.assert_called_once() - self.assertIn('Email service down', mock_tracker_instance.record_failure.call_args.kwargs['error_message']) - - def test_handler_records_failure_when_privilege_has_unknown_jurisdiction(self): + @patch('handlers.expiration_reminders.ExpirationReminderTracker') + @patch('handlers.expiration_reminders.config') + @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') + def test_handler_sends_email_and_records_success(self, mock_iter, mock_config, mock_tracker_class): + mock_config.compacts = ['aslp'] + mock_email_client = MagicMock() + mock_config.email_service_client = mock_email_client + + mock_tracker_instance = MagicMock() + mock_tracker_instance.was_already_sent.return_value = False + mock_tracker_class.return_value = mock_tracker_instance + + from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders + + mock_iter.return_value = iter([ + PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor1']), + ]) + + resp = process_expiration_reminders(self._make_event(days_before=30), self.mock_context) + + self.assertEqual('complete', resp['status']) + self.assertEqual(1, resp['metrics']['sent']) + self.assertEqual(0, resp['metrics']['failed']) + self.assertEqual(30, resp['daysBefore']) + self.assertEqual('aslp', resp['compact']) + mock_email_client.send_privilege_expiration_reminder_email.assert_called_once() + call_kwargs = mock_email_client.send_privilege_expiration_reminder_email.call_args.kwargs + tv = call_kwargs['template_variables'] + self.assertEqual('John', tv.provider_first_name) + self.assertEqual('2026-02-16', tv.expiration_date.isoformat()) + self.assertEqual(1, len(tv.privileges)) + priv = tv.privileges[0] + self.assertEqual('Ohio', priv['jurisdiction'], 'jurisdiction must be full state name') + self.assertEqual('aud', priv['licenseType']) + self.assertEqual('a', priv['privilegeId']) + self.assertEqual('2026-02-16', priv['dateOfExpiration'], 'dateOfExpiration must be ISO 8601') + mock_tracker_instance.record_success.assert_called_once() + + # Verify tracker was created with correct event_type + from expiration_reminder_tracker import ExpirationEventType + + mock_tracker_class.assert_called_once() + tracker_call = mock_tracker_class.call_args.kwargs + self.assertEqual(ExpirationEventType.PRIVILEGE_EXPIRATION_30_DAY, tracker_call['event_type']) + + @patch('handlers.expiration_reminders.ExpirationReminderTracker') + @patch('handlers.expiration_reminders.config') + @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') + def test_handler_skips_when_already_sent(self, mock_iter, mock_config, mock_tracker_class): + mock_config.compacts = ['aslp'] + mock_email_client = MagicMock() + mock_config.email_service_client = mock_email_client + + mock_tracker_instance = MagicMock() + mock_tracker_instance.was_already_sent.return_value = True + mock_tracker_class.return_value = mock_tracker_instance + + from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders + + mock_iter.return_value = iter([ + PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor1']), + ]) + + resp = process_expiration_reminders(self._make_event(days_before=7), self.mock_context) + + self.assertEqual('complete', resp['status']) + self.assertEqual(0, resp['metrics']['sent']) + self.assertEqual(1, resp['metrics']['alreadySent']) + mock_email_client.send_privilege_expiration_reminder_email.assert_not_called() + + @patch('handlers.expiration_reminders.ExpirationReminderTracker') + @patch('handlers.expiration_reminders.config') + @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') + def test_handler_logs_error_for_provider_without_email(self, mock_iter, mock_config, mock_tracker_class): + """Provider without email address is skipped and logged as noEmail.""" + mock_config.compacts = ['aslp'] + mock_email_client = MagicMock() + mock_config.email_service_client = mock_email_client + + mock_tracker_instance = MagicMock() + mock_tracker_instance.was_already_sent.return_value = False + mock_tracker_class.return_value = mock_tracker_instance + + from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders + + mock_iter.return_value = iter([ + PaginatedProviderResult(provider_doc=self._make_provider_doc(email=None), search_after=['cursor1']), + ]) + + resp = process_expiration_reminders(self._make_event(days_before=0), self.mock_context) + + self.assertEqual('complete', resp['status']) + self.assertEqual(0, resp['metrics']['sent']) + self.assertEqual(1, resp['metrics']['noEmail']) + mock_email_client.send_privilege_expiration_reminder_email.assert_not_called() + + @patch('handlers.expiration_reminders.ExpirationReminderTracker') + @patch('handlers.expiration_reminders.config') + @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') + def test_handler_records_failure_on_email_error(self, mock_iter, mock_config, mock_tracker_class): + """Email service raises exception; handler records failure.""" + mock_config.compacts = ['aslp'] + mock_email_client = MagicMock() + mock_email_client.send_privilege_expiration_reminder_email.side_effect = Exception('Email service down') + mock_config.email_service_client = mock_email_client + + mock_tracker_instance = MagicMock() + mock_tracker_instance.was_already_sent.return_value = False + mock_tracker_class.return_value = mock_tracker_instance + + from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders + + mock_iter.return_value = iter([ + PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor1']), + ]) + + resp = process_expiration_reminders(self._make_event(), self.mock_context) + + self.assertEqual('complete', resp['status']) + self.assertEqual(0, resp['metrics']['sent']) + self.assertEqual(1, resp['metrics']['failed']) + mock_tracker_instance.record_failure.assert_called_once() + self.assertIn('Email service down', mock_tracker_instance.record_failure.call_args.kwargs['error_message']) + + @patch('handlers.expiration_reminders.ExpirationReminderTracker') + @patch('handlers.expiration_reminders.config') + @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') + def test_handler_records_failure_when_privilege_has_unknown_jurisdiction( + self, mock_iter, mock_config, mock_tracker_class + ): """Unknown jurisdiction code raises when building template; handler records failure.""" - with ( - patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') as mock_iter, - patch('handlers.expiration_reminders.config') as mock_config, - patch('handlers.expiration_reminders.ExpirationReminderTracker') as mock_tracker_class, - ): - mock_config.compacts = ['aslp'] - mock_email_client = MagicMock() - mock_config.email_service_client = mock_email_client - - mock_tracker_instance = MagicMock() - mock_tracker_instance.was_already_sent.return_value = False - mock_tracker_class.return_value = mock_tracker_instance - - # Privilege with jurisdiction not in CompactConfigUtility.JURISDICTION_NAME_MAPPING - from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders - - doc = self._make_provider_doc() - doc['privileges'][0]['jurisdiction'] = 'xx' - mock_iter.return_value = iter([ - PaginatedProviderResult(provider_doc=doc, search_after=['cursor1']), - ]) - - resp = process_expiration_reminders(self._make_event(), self.mock_context) - - self.assertEqual('complete', resp['status']) - self.assertEqual(0, resp['metrics']['sent']) - self.assertEqual(1, resp['metrics']['failed']) - mock_tracker_instance.record_failure.assert_called_once() - failure_msg = mock_tracker_instance.record_failure.call_args.kwargs['error_message'] - self.assertIn('Unknown jurisdiction', failure_msg) + mock_config.compacts = ['aslp'] + mock_email_client = MagicMock() + mock_config.email_service_client = mock_email_client + + mock_tracker_instance = MagicMock() + mock_tracker_instance.was_already_sent.return_value = False + mock_tracker_class.return_value = mock_tracker_instance + + # Privilege with jurisdiction not in CompactConfigUtility.JURISDICTION_NAME_MAPPING + from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders + + doc = self._make_provider_doc() + doc['privileges'][0]['jurisdiction'] = 'xx' + mock_iter.return_value = iter([ + PaginatedProviderResult(provider_doc=doc, search_after=['cursor1']), + ]) + + resp = process_expiration_reminders(self._make_event(), self.mock_context) + + self.assertEqual('complete', resp['status']) + self.assertEqual(0, resp['metrics']['sent']) + self.assertEqual(1, resp['metrics']['failed']) + mock_tracker_instance.record_failure.assert_called_once() + failure_msg = mock_tracker_instance.record_failure.call_args.kwargs['error_message'] + self.assertIn('Unknown jurisdiction', failure_msg) def test_handler_validates_days_before_value(self): from cc_common.exceptions import CCInvalidRequestException @@ -377,26 +369,19 @@ def test_handler_validates_days_before_value(self): process_expiration_reminders( { 'targetDate': '2026-02-16', - 'daysBefore': 15, + 'daysBefore': 999, # Invalid 'compact': 'aslp', - 'scheduledTime': '2026-01-17T10:00:00Z', }, self.mock_context, ) - - self.assertIn('15', str(ctx.exception)) - self.assertIn('Must be 30, 7, or 0', str(ctx.exception)) + self.assertIn('daysBefore', str(ctx.exception)) def test_handler_requires_days_before_field(self): from cc_common.exceptions import CCInvalidRequestException from handlers.expiration_reminders import process_expiration_reminders with self.assertRaises(CCInvalidRequestException) as ctx: - process_expiration_reminders( - {'targetDate': '2026-02-16', 'compact': 'aslp', 'scheduledTime': '2026-01-17T10:00:00Z'}, - self.mock_context, - ) - + process_expiration_reminders({'targetDate': '2026-02-16', 'compact': 'aslp'}, self.mock_context) self.assertIn('daysBefore', str(ctx.exception)) def test_handler_requires_compact_field(self): @@ -404,163 +389,154 @@ def test_handler_requires_compact_field(self): from handlers.expiration_reminders import process_expiration_reminders with self.assertRaises(CCInvalidRequestException) as ctx: - process_expiration_reminders( - {'targetDate': '2026-02-16', 'daysBefore': 30, 'scheduledTime': '2026-01-17T10:00:00Z'}, - self.mock_context, - ) - + process_expiration_reminders({'targetDate': '2026-02-16', 'daysBefore': 30}, self.mock_context) self.assertIn('compact', str(ctx.exception)) def test_handler_validates_compact_value(self): from cc_common.exceptions import CCInvalidRequestException from handlers.expiration_reminders import process_expiration_reminders - with ( - patch('handlers.expiration_reminders.config') as mock_config, - ): - mock_config.compacts = ['aslp', 'coun'] - - with self.assertRaises(CCInvalidRequestException) as ctx: - process_expiration_reminders( - self._make_event(compact='invalid'), - self.mock_context, - ) - - self.assertIn('invalid', str(ctx.exception)) - self.assertIn('Must be one of', str(ctx.exception)) - - def test_handler_calculates_target_date_from_days_before_when_not_provided(self): - """Test that handler calculates targetDate from daysBefore when targetDate is not provided.""" - - with ( - patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') as mock_iter, - patch('handlers.expiration_reminders.config') as mock_config, - patch('handlers.expiration_reminders.ExpirationReminderTracker') as mock_tracker_class, - ): - mock_config.compacts = ['aslp'] - mock_email_client = MagicMock() - mock_config.email_service_client = mock_email_client - - mock_tracker_instance = MagicMock() - mock_tracker_instance.was_already_sent.return_value = False - mock_tracker_class.return_value = mock_tracker_instance - - # Create provider doc with expiration date = today + 30 days - today = datetime.now(UTC).date() - target_date = today + timedelta(days=30) - provider_doc = self._make_provider_doc() - provider_doc['privileges'][0]['dateOfExpiration'] = target_date.isoformat() - - from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders - - mock_iter.return_value = iter([ - PaginatedProviderResult(provider_doc=provider_doc, search_after=['cursor1']), - ]) - - # Event with only daysBefore and compact (no targetDate) - event = {'daysBefore': 30, 'compact': 'aslp'} - resp = process_expiration_reminders(event, self.mock_context) - - # Verify it calculated the correct target date - expected_target_date = (today + timedelta(days=30)).isoformat() - self.assertEqual(expected_target_date, resp['targetDate']) - self.assertEqual(30, resp['daysBefore']) - - # Verify the generator was called with the calculated date and no initial_search_after - mock_iter.assert_called_once() - call_kwargs = mock_iter.call_args.kwargs - self.assertEqual(target_date, call_kwargs['expiration_date']) - self.assertIsNone(call_kwargs.get('initial_search_after')) - - def test_handler_continuation_parses_accumulated_metrics_and_search_after(self): - """Continuation event restores metrics and passes initial_search_after to generator.""" - with ( - patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') as mock_iter, - patch('handlers.expiration_reminders.config') as mock_config, - patch('handlers.expiration_reminders.ExpirationReminderTracker') as mock_tracker_class, - ): - mock_config.compacts = ['aslp'] - mock_config.email_service_client = MagicMock() - - mock_tracker_instance = MagicMock() - mock_tracker_instance.was_already_sent.return_value = False - mock_tracker_class.return_value = mock_tracker_instance - - from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders - - mock_iter.return_value = iter([ - PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor2']), - ]) - - event = { - 'targetDate': '2026-02-16', - 'daysBefore': 30, - 'compact': 'aslp', - '_continuation': { - 'searchAfter': ['cursor1'], - 'depth': 1, - 'accumulatedMetrics': { - 'sent': 100, - 'skipped': 2, - 'failed': 1, - 'alreadySent': 5, - 'noEmail': 0, - 'matchedPrivileges': 108, - 'providersWithMatches': 108, - }, - }, - } - resp = process_expiration_reminders(event, self.mock_context) - - self.assertEqual('complete', resp['status']) - self.assertEqual(101, resp['metrics']['sent']) - self.assertEqual(2, resp['metrics']['skipped']) - self.assertEqual(5, resp['metrics']['alreadySent']) - self.assertEqual(2, resp['totalInvocations']) - mock_iter.assert_called_once() - call_kwargs = mock_iter.call_args.kwargs - self.assertEqual(['cursor1'], call_kwargs['initial_search_after']) - - def test_handler_invokes_itself_with_pagination_values_when_reaching_limit(self): - """When remaining time is below threshold, handler invokes self and returns continued.""" - with ( - patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') as mock_iter, - patch('handlers.expiration_reminders.config') as mock_config, - patch('handlers.expiration_reminders.ExpirationReminderTracker') as mock_tracker_class, - ): - mock_config.compacts = ['aslp'] - mock_config.email_service_client = MagicMock() - mock_lambda_client = MagicMock() - mock_config.lambda_client = mock_lambda_client - - mock_tracker_instance = MagicMock() - mock_tracker_instance.was_already_sent.return_value = False - mock_tracker_class.return_value = mock_tracker_instance - - from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders - - mock_iter.return_value = iter([ - PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor1']), - ]) - - mock_context = MagicMock() - mock_context.get_remaining_time_in_millis.return_value = 60_000 # 1 minute left - - resp = process_expiration_reminders(self._make_event(), mock_context) - - self.assertEqual('continued', resp['status']) - self.assertEqual(1, resp['nextInvocationDepth']) - self.assertEqual(['cursor1'], resp['resumeFrom']['searchAfter']) - self.assertEqual(1, resp['metricsAtContinuation']['sent']) - mock_lambda_client.invoke.assert_called_once() - call_kwargs = mock_lambda_client.invoke.call_args.kwargs - self.assertEqual('Event', call_kwargs['InvocationType']) - payload = json.loads(call_kwargs['Payload']) - self.assertEqual(30, payload['daysBefore']) - self.assertEqual('aslp', payload['compact']) - self.assertEqual(['cursor1'], payload['_continuation']['searchAfter']) - self.assertEqual(1, payload['_continuation']['depth']) - self.assertEqual(1, payload['_continuation']['accumulatedMetrics']['sent']) + with self.assertRaises(CCInvalidRequestException) as ctx: + process_expiration_reminders( + {'targetDate': '2026-02-16', 'daysBefore': 30, 'compact': 'invalid'}, self.mock_context + ) + self.assertIn('compact', str(ctx.exception)) + + @patch('handlers.expiration_reminders.ExpirationReminderTracker') + @patch('handlers.expiration_reminders.config') + @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') + @patch('handlers.expiration_reminders.datetime') + def test_handler_calculates_target_date_from_days_before_when_not_provided( + self, mock_datetime_class, mock_iter, mock_config, mock_tracker_class + ): + """When targetDate is not provided, handler calculates it based on daysBefore.""" + mock_config.compacts = ['aslp'] + mock_email_client = MagicMock() + mock_config.email_service_client = mock_email_client + + mock_tracker_instance = MagicMock() + mock_tracker_instance.was_already_sent.return_value = False + mock_tracker_class.return_value = mock_tracker_instance + + # Mock datetime.now(UTC).date() to return fixed date + today = date(2026, 1, 17) + mock_now = MagicMock() + mock_now.date.return_value = today + mock_datetime_class.now.return_value = mock_now + # Passthrough other datetime methods + mock_datetime_class.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs) + + from handlers.expiration_reminders import process_expiration_reminders + + mock_iter.return_value = iter([]) + + resp = process_expiration_reminders({'daysBefore': 30, 'compact': 'aslp'}, self.mock_context) + + self.assertEqual('complete', resp['status']) + # Verify it calculated the correct target date + expected_target_date = (today + timedelta(days=30)).isoformat() + self.assertEqual(expected_target_date, resp['targetDate']) + self.assertEqual(30, resp['daysBefore']) + + # Verify the generator was called with the calculated date and no initial_search_after + mock_iter.assert_called_once() + call_kwargs = mock_iter.call_args.kwargs + self.assertEqual(date(2026, 2, 16), call_kwargs['expiration_date']) + self.assertIsNone(call_kwargs.get('initial_search_after')) + + @patch('handlers.expiration_reminders.ExpirationReminderTracker') + @patch('handlers.expiration_reminders.config') + @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') + def test_handler_continuation_parses_accumulated_metrics_and_search_after( + self, mock_iter, mock_config, mock_tracker_class + ): + """Continuation event with _continuation state merges accumulated metrics and resumes from searchAfter.""" + mock_config.compacts = ['aslp'] + mock_email_client = MagicMock() + mock_config.email_service_client = mock_email_client + + mock_tracker_instance = MagicMock() + mock_tracker_instance.was_already_sent.side_effect = [False, True, True, False, False] + mock_tracker_class.return_value = mock_tracker_instance + + from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders + + # Simulate a continuation invocation + event_with_continuation = { + 'targetDate': '2026-02-16', + 'daysBefore': 30, + 'compact': 'aslp', + '_continuation': { + 'searchAfter': ['cursor1'], + 'depth': 1, + 'accumulatedMetrics': {'sent': 100, 'skipped': 2, 'alreadySent': 5}, + }, + } + + # This invocation processes 1 new email, 2 already sent + mock_iter.return_value = iter([ + PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor2']), + PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor3']), + PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor4']), + ]) + + resp = process_expiration_reminders(event_with_continuation, self.mock_context) + + self.assertEqual('complete', resp['status']) + self.assertEqual(101, resp['metrics']['sent']) + self.assertEqual(2, resp['metrics']['skipped']) + self.assertEqual(7, resp['metrics']['alreadySent']) + self.assertEqual(2, resp['totalInvocations']) + mock_iter.assert_called_once() + call_kwargs = mock_iter.call_args.kwargs + self.assertEqual(['cursor1'], call_kwargs['initial_search_after']) + + @patch('handlers.expiration_reminders.ExpirationReminderTracker') + @patch('handlers.expiration_reminders.config') + @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') + def test_handler_invokes_itself_with_pagination_values_when_reaching_limit( + self, mock_iter, mock_config, mock_tracker_class + ): + """Handler detects remaining time is low, invokes itself with continuation state.""" + mock_config.compacts = ['aslp'] + mock_email_client = MagicMock() + mock_config.email_service_client = mock_email_client + mock_lambda_client = MagicMock() + mock_config.lambda_client = mock_lambda_client + mock_config.lambda_function_name = 'test-function' + + mock_tracker_instance = MagicMock() + mock_tracker_instance.was_already_sent.return_value = False + mock_tracker_class.return_value = mock_tracker_instance + + from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders + + # Return one provider doc + mock_iter.return_value = iter([ + PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor1']), + ]) + + # Mock the context to simulate approaching timeout + # TIMEOUT_BUFFER_MS is 120,000 (2 minutes), so set remaining time to 100,000 (< buffer) + mock_context_below_time_threshold = MagicMock() + mock_context_below_time_threshold.get_remaining_time_in_millis.return_value = 100_000 + + resp = process_expiration_reminders(self._make_event(), mock_context_below_time_threshold) + + self.assertEqual('continued', resp['status']) + self.assertEqual(1, resp['nextInvocationDepth']) + self.assertEqual(['cursor1'], resp['resumeFrom']['searchAfter']) + self.assertEqual(1, resp['metricsAtContinuation']['sent']) + + mock_lambda_client.invoke.assert_called_once() + call_kwargs = mock_lambda_client.invoke.call_args.kwargs + self.assertEqual('Event', call_kwargs['InvocationType']) + payload = json.loads(call_kwargs['Payload']) + self.assertEqual(30, payload['daysBefore']) + self.assertEqual('aslp', payload['compact']) + self.assertEqual(['cursor1'], payload['_continuation']['searchAfter']) + self.assertEqual(1, payload['_continuation']['depth']) + self.assertEqual(1, payload['_continuation']['accumulatedMetrics']['sent']) def test_handler_max_continuation_depth_raises(self): """When continuation depth >= MAX_CONTINUATION_DEPTH, handler raises RuntimeError.""" From 87ff2c1408379733ab36a5b8af8acfffe05f64eb Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 30 Jan 2026 14:20:21 -0600 Subject: [PATCH 09/52] fix comment --- .../search/handlers/expiration_reminders.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py index 562a459f9..af8b86166 100644 --- a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py @@ -277,21 +277,14 @@ def _process_provider_notification( """ result = {'sent': 0, 'skipped': 0, 'failed': 0, 'already_sent': 0, 'no_email': 0} - provider_id_str = provider_doc['providerId'] - - try: - provider_id = UUID(provider_id_str) - except (ValueError, TypeError): - logger.warning('Invalid providerId format', provider_id=provider_id_str, compact=compact) - result['skipped'] = 1 - return result + provider_id = provider_doc['providerId'] # Check for registered email - providers with privileges should always be registered provider_email = provider_doc.get('compactConnectRegisteredEmailAddress') if not provider_email: logger.error( 'Provider with privileges has no registered email address', - provider_id=str(provider_id), + provider_id=provider_id, compact=compact, ) result['no_email'] = 1 @@ -308,7 +301,7 @@ def _process_provider_notification( if tracker.was_already_sent(): logger.debug( 'Reminder already sent, skipping', - provider_id=str(provider_id), + provider_id=provider_id, compact=compact, event_type=event_type, ) @@ -443,9 +436,8 @@ def _build_expiration_query(*, expiration_date: date, page_size: int) -> dict: 'query': { 'nested': { # Nested query for privileges - # This query is applied to each inner object (privilege) individually, so only privileges that match - # the _entire_ query are included in the results. This means that an individual privilege must be both - # active _and_ expire on the specified date to be included in the results. + # This query will match any privilege that matches the _entire_ query, so only providers with at least + # one privilege that is active and expires on the specified date will be included in the results. 'path': 'privileges', 'query': { 'bool': { From 1873e2bb82b4be77849d59ede7e75b3b645b3595 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 30 Jan 2026 14:30:30 -0600 Subject: [PATCH 10/52] WIP - update smoke test --- .../tests/smoke/expiration_reminder_load_test.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/backend/compact-connect/tests/smoke/expiration_reminder_load_test.py b/backend/compact-connect/tests/smoke/expiration_reminder_load_test.py index 7562d9e37..5bffebea4 100755 --- a/backend/compact-connect/tests/smoke/expiration_reminder_load_test.py +++ b/backend/compact-connect/tests/smoke/expiration_reminder_load_test.py @@ -348,7 +348,7 @@ def find_lambda_function_name(partial_name: str) -> str: raise SmokeTestFailureException(f'Failed to list Lambda functions: {str(e)}') from e -def invoke_expiration_reminder_lambda(days_before: int): +def invoke_expiration_reminder_lambda(days_before: int, compact: str = 'aslp'): """ Invoke the expiration reminder Lambda asynchronously and poll CloudWatch Logs for completion. @@ -356,7 +356,8 @@ def invoke_expiration_reminder_lambda(days_before: int): Polls CloudWatch Logs to find the completion message with metrics. :param days_before: Days before expiration (30, 7, or 0) - :return: Dict containing targetDate, daysBefore, and metrics + :param compact: Compact to process (e.g. 'aslp', 'coun', 'octp') + :return: Dict containing targetDate, daysBefore, compact, and metrics """ # Search for "ExpirationReminder" since CDK truncates function names lambda_name = find_lambda_function_name('ExpirationReminder') @@ -387,7 +388,7 @@ def invoke_expiration_reminder_lambda(days_before: int): f'Failed to get Lambda function details: {str(e)}' ) from e - event = {'daysBefore': days_before} + event = {'daysBefore': days_before, 'compact': compact} logger.info(f'Invoking expiration reminder Lambda asynchronously for {days_before}-day reminder', event=event) try: @@ -408,7 +409,7 @@ def invoke_expiration_reminder_lambda(days_before: int): logger.info('Lambda invocation accepted, polling CloudWatch Logs for completion...', log_group=log_group_name) # Poll CloudWatch Logs for the completion message - # The Lambda logs "Completed processing expiration reminders" with metrics + # The Lambda logs "Completed processing for compact" (or "Completed processing expiration reminders") with metrics max_wait_time = 960 # 16 minutes (Lambda timeout is 15 minutes) check_interval = 10 # Check every 10 seconds start_time = time.time() @@ -443,8 +444,8 @@ def invoke_expiration_reminder_lambda(days_before: int): if log_level != 'DEBUG': logger.info(f'Lambda log [{log_level}]: {log_message}') - # Check for completion message - if 'Completed processing expiration reminders' in log_message: + # Check for completion message (handler logs "Completed processing for compact" with metrics) + if 'Completed processing' in log_message and log_json.get('metrics'): metrics = log_json.get('metrics', {}) if metrics: From 027ffa42e447fea6548fb72819982cc27a45ee39 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 2 Feb 2026 12:54:48 -0600 Subject: [PATCH 11/52] Update email template to match design --- .../email-notification-service/lambda.ts | 7 +- .../nodejs/lib/email/base-email-service.ts | 324 ++++++++++++++++++ .../lib/email/email-notification-service.ts | 59 +++- .../tests/email-notification-service.test.ts | 12 +- .../email/email-notification-service.test.ts | 20 +- .../lambdas/python/search/tests/__init__.py | 2 +- 6 files changed, 401 insertions(+), 23 deletions(-) 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 514bd66ad..79eb83e87 100644 --- a/backend/compact-connect/lambdas/nodejs/email-notification-service/lambda.ts +++ b/backend/compact-connect/lambdas/nodejs/email-notification-service/lambda.ts @@ -395,12 +395,17 @@ export class Lambda implements LambdaInterface { || !event.templateVariables?.privileges) { throw new Error('Missing required template variables for privilegeExpirationReminder template.'); } - const privileges = event.templateVariables.privileges as Array<{ jurisdiction?: string; licenseType?: string; privilegeId?: string; dateOfExpiration?: string }>; + const privileges = event.templateVariables.privileges as Array<{ jurisdiction?: string; + licenseType?: string; privilegeId?: + string; dateOfExpiration?: string; + formattedExpirationDate?: string }>; + if (!Array.isArray(privileges) || privileges.length === 0) { throw new Error('privilegeExpirationReminder template requires a non-empty privileges array.'); } for (let i = 0; i < privileges.length; i++) { const p = privileges[i]; + if (!p?.jurisdiction || !p?.licenseType || !p?.privilegeId || !p?.dateOfExpiration) { throw new Error( `privilegeExpirationReminder template requires each privilege to have jurisdiction, licenseType, privilegeId, and dateOfExpiration (ISO 8601). Invalid privilege at index ${i}.` 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 44634ae66..cddbfde20 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 @@ -331,6 +331,45 @@ export abstract class BaseEmailService { report['root']['data']['childrenIds'].push(blockHeaderId); } + /** + * Inserts just the logo without a heading. Useful for emails that have + * a different layout where the greeting comes right after the logo. + */ + protected insertLogo(report: TReaderDocument): void { + // Insert environment banner first (above logo) + if (this.shouldShowEnvironmentBannerIfNonProdEnvironment) { + this.environmentBannerService.insertEnvironmentBannerIfNonProd(report); + } + + const blockLogoId = 'block-logo'; + + report[blockLogoId] = { + 'type': 'Image', + 'data': { + 'style': { + 'padding': { + 'top': 40, + 'bottom': 8, + 'right': 68, + 'left': 68 + }, + 'backgroundColor': null, + 'textAlign': 'center' + }, + 'props': { + 'width': null, + 'height': 24, + 'url': `${BaseEmailService.getEmailImageBaseUrl()}/compact-connect-logo-final.png`, + 'alt': '', + 'linkHref': null, + 'contentAlignment': 'middle' + } + } + }; + + report['root']['data']['childrenIds'].push(blockLogoId); + } + protected insertBody( report: TReaderDocument, bodyText: string, @@ -730,4 +769,289 @@ export abstract class BaseEmailService { protected renderTemplate(template: TReaderDocument): string { return renderToStaticMarkup(template, { rootBlockId: 'root' }); } + + /** + * Inserts a privilege expiration list section with a two-column table layout. + * Displays privileges with their expiration dates in a styled table format. + * + * @param report - The email template document to modify + * @param privileges - Array of privilege objects with jurisdiction, licenseType, privilegeId, and formattedExpirationDate + */ + protected insertPrivilegeExpirationListSection( + report: TReaderDocument, + privileges: { + jurisdiction: string; + licenseType: string; + privilegeId: string; + formattedExpirationDate: string; + }[] + ): void { + // Insert the table header row + this.insertPrivilegeTableHeader(report); + + // Insert divider after header + this.insertPrivilegeTableDivider(report); + + // Insert each privilege row with a divider after it + privileges.forEach((privilege) => { + this.insertPrivilegeTableRow( + report, + `${privilege.jurisdiction}, ${privilege.licenseType}`, + `#${privilege.privilegeId}`, + privilege.formattedExpirationDate + ); + + // Add divider after each row + this.insertPrivilegeTableDivider(report); + }); + + // Add bottom spacing container + this.insertPrivilegeTableBottomSpacer(report); + } + + /** + * Inserts the header row for the privilege expiration table + */ + private insertPrivilegeTableHeader(report: TReaderDocument): void { + const containerId = `block-${crypto.randomUUID()}`; + const privilegeHeaderId = `block-${crypto.randomUUID()}`; + const expiresHeaderId = `block-${crypto.randomUUID()}`; + + report[privilegeHeaderId] = { + 'type': 'Text', + 'data': { + 'style': { + 'color': '#767676', + 'fontSize': 14, + 'fontWeight': 'bold', + 'padding': { + 'top': 0, + 'bottom': 0, + 'right': 24, + 'left': 24 + } + }, + 'props': { + 'text': 'Privilege' + } + } + }; + + report[expiresHeaderId] = { + 'type': 'Text', + 'data': { + 'style': { + 'color': '#767676', + 'backgroundColor': null, + 'fontSize': 14, + 'fontWeight': 'bold', + 'padding': { + 'top': 0, + 'bottom': 0, + 'right': 24, + 'left': 24 + } + }, + 'props': { + 'text': 'Expires' + } + } + }; + + report[containerId] = { + 'type': 'ColumnsContainer', + 'data': { + 'style': { + 'backgroundColor': '#F4F6F8', + 'padding': { + 'top': 20, + 'bottom': 0, + 'right': 24, + 'left': 24 + } + }, + 'props': { + 'fixedWidths': [190, 100, null], + 'columnsCount': 2, + 'columnsGap': 8, + 'contentAlignment': 'top', + 'columns': [ + { 'childrenIds': [privilegeHeaderId]}, + { 'childrenIds': [expiresHeaderId]}, + { 'childrenIds': []} + ] + } + } + }; + + report['root']['data']['childrenIds'].push(containerId); + } + + /** + * Inserts a divider within the privilege expiration table + */ + private insertPrivilegeTableDivider(report: TReaderDocument): void { + const dividerId = `block-${crypto.randomUUID()}`; + + report[dividerId] = { + 'type': 'Divider', + 'data': { + 'style': { + 'backgroundColor': '#F4F6F8', + 'padding': { + 'top': 8, + 'bottom': 0, + 'right': 0, + 'left': 0 + } + }, + 'props': { + 'lineColor': '#CCCCCC' + } + } + }; + + report['root']['data']['childrenIds'].push(dividerId); + } + + /** + * Inserts a single privilege row in the expiration table + */ + private insertPrivilegeTableRow( + report: TReaderDocument, + title: string, + privilegeId: string, + expirationDate: string + ): void { + const containerId = `block-${crypto.randomUUID()}`; + const titleId = `block-${crypto.randomUUID()}`; + const privilegeIdTextId = `block-${crypto.randomUUID()}`; + const dateId = `block-${crypto.randomUUID()}`; + + // Privilege title (jurisdiction + license type) + report[titleId] = { + 'type': 'Text', + 'data': { + 'style': { + 'color': '#09122B', + 'fontWeight': 'bold', + 'padding': { + 'top': 12, + 'bottom': 4, + 'right': 24, + 'left': 24 + } + }, + 'props': { + 'text': title + } + } + }; + + // Privilege ID (gray subtitle) + report[privilegeIdTextId] = { + 'type': 'Text', + 'data': { + 'style': { + 'color': '#767676', + 'fontWeight': 'normal', + 'padding': { + 'top': 0, + 'bottom': 0, + 'right': 24, + 'left': 24 + } + }, + 'props': { + 'text': privilegeId + } + } + }; + + // Expiration date + report[dateId] = { + 'type': 'Text', + 'data': { + 'style': { + 'color': '#09122B', + 'fontWeight': 'bold', + 'padding': { + 'top': 12, + 'bottom': 0, + 'right': 24, + 'left': 24 + } + }, + 'props': { + 'text': expirationDate + } + } + }; + + // Two-column container + report[containerId] = { + 'type': 'ColumnsContainer', + 'data': { + 'style': { + 'backgroundColor': '#F4F6F8', + 'padding': { + 'top': 4, + 'bottom': 4, + 'right': 24, + 'left': 24 + } + }, + 'props': { + 'fixedWidths': [190, 100, null], + 'columnsCount': 2, + 'columnsGap': 8, + 'contentAlignment': 'top', + 'columns': [ + { 'childrenIds': [titleId, privilegeIdTextId]}, + { 'childrenIds': [dateId]}, + { 'childrenIds': []} + ] + } + } + }; + + report['root']['data']['childrenIds'].push(containerId); + } + + /** + * Inserts bottom spacing for the privilege expiration table + */ + private insertPrivilegeTableBottomSpacer(report: TReaderDocument): void { + const containerId = `block-${crypto.randomUUID()}`; + const spacerId = `block-${crypto.randomUUID()}`; + + report[spacerId] = { + 'type': 'Spacer', + 'data': { + 'props': { + 'height': 4 + } + } + }; + + report[containerId] = { + 'type': 'Container', + 'data': { + 'style': { + 'backgroundColor': '#F4F6F8', + 'padding': { + 'top': 12, + 'bottom': 12, + 'right': 24, + 'left': 24 + } + }, + 'props': { + 'childrenIds': [spacerId] + } + } + }; + + report['root']['data']['childrenIds'].push(containerId); + } } diff --git a/backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts b/backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts index 7da74ba00..5de8de290 100644 --- a/backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts +++ b/backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts @@ -11,9 +11,19 @@ const MONTH_NAMES = ['January', 'February', 'March', 'April', 'May', 'June', 'Ju function formatIsoDateForDisplay(isoDate: string): string { const [year, month, day] = isoDate.split('-').map(Number); const monthName = MONTH_NAMES[month - 1]; + return `${monthName} ${day}, ${year}`; } +/** Format an ISO 8601 date string (YYYY-MM-DD) for display as MM/DD/YYYY (e.g. "02/16/2026"). Timezone-neutral. */ +function formatIsoDateAsSlashFormat(isoDate: string): string { + const [year, month, day] = isoDate.split('-').map(Number); + const paddedMonth = String(month).padStart(2, '0'); + const paddedDay = String(day).padStart(2, '0'); + + return `${paddedMonth}/${paddedDay}/${year}`; +} + /** * Privilege row for expiration reminder: jurisdiction and licenseType are full names; dates are ISO 8601. */ @@ -677,23 +687,54 @@ export class EmailNotificationService extends BaseEmailService { } const expirationDateDisplay = formatIsoDateForDisplay(expirationDate); + const expirationDateSlash = formatIsoDateAsSlashFormat(expirationDate); const emailContent = this.getNewEmailTemplate(); const subject = `Your Compact Connect Privileges Expire on ${expirationDateDisplay}`; - const headerText = 'Privilege Expiration Reminder'; - const bodyText = `Hello ${providerFirstName},\n\nThis is a reminder that the following privilege(s) will expire on ${expirationDateDisplay}:`; - this.insertHeader(emailContent, headerText); - this.insertBody(emailContent, bodyText, 'center'); + // Logo at the top + this.insertLogo(emailContent); - privileges.forEach((privilege) => { - const titleText = `${privilege.jurisdiction}, ${privilege.licenseType}`; - const detailText = `#${privilege.privilegeId} Expires: ${formatIsoDateForDisplay(privilege.dateOfExpiration)}`; + // Greeting text + this.insertBody(emailContent, `Hi ${providerFirstName},`, 'center', false, { + 'padding': { 'top': 44, 'bottom': 0, 'right': 76, 'left': 76 } + }); + + // "This message is to inform you..." text + this.insertBody( + emailContent, + 'This message is to inform you that one or more of your privileges will expire on:', + 'center', + false, + { 'padding': { 'top': 24, 'bottom': 0, 'right': 32, 'left': 32 }} + ); - this.insertTuple(emailContent, titleText, detailText); + // Bold expiration date + this.insertBody(emailContent, expirationDateSlash, 'center', false, { + 'fontSize': 19, + 'fontWeight': 'bold', + 'padding': { 'top': 4, 'bottom': 0, 'right': 60, 'left': 60 } }); - this.insertBody(emailContent, '\nPlease visit Compact Connect to renew your privileges before they expire.', 'center'); + // Explanation text + this.insertBody( + emailContent, + 'When a privilege is expired, you can no longer use it to practice. In order to renew your privileges, you must first renew your home state license. A full list of the privileges and their expiration dates is below:', + 'center', + false, + { 'padding': { 'top': 24, 'bottom': 24, 'right': 32, 'left': 32 }} + ); + + // Insert the two-column privilege expiration list table + const privilegesForTable = privileges.map((privilege) => ({ + jurisdiction: privilege.jurisdiction, + licenseType: privilege.licenseType, + privilegeId: privilege.privilegeId, + formattedExpirationDate: formatIsoDateAsSlashFormat(privilege.dateOfExpiration) + })); + + this.insertPrivilegeExpirationListSection(emailContent, privilegesForTable); + this.insertFooter(emailContent); const htmlContent = this.renderTemplate(emailContent); 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 96f6d3566..fa0f30683 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 @@ -667,16 +667,22 @@ describe('EmailNotificationServiceLambda', () => { expect(mockSESClient).toHaveReceivedCommandTimes(SendEmailCommand, 1); const sendCall = mockSESClient.commandCalls(SendEmailCommand)[0]; const input = sendCall.args[0].input; + expect(input.Destination?.ToAddresses).toEqual(['provider@example.com']); expect(input.Content?.Simple?.Subject?.Data).toBe('Your Compact Connect Privileges Expire on February 16, 2026'); const htmlData = input.Content?.Simple?.Body?.Html?.Data ?? ''; - expect(htmlData).toContain('Privilege Expiration Reminder'); - expect(htmlData).toContain('Hello Mary'); - expect(htmlData).toContain('February 16, 2026'); + + expect(htmlData).toContain('Hi Mary,'); + expect(htmlData).toContain('one or more of your privileges will expire on'); + expect(htmlData).toContain('02/16/2026'); expect(htmlData).toContain('Ohio, audiologist'); expect(htmlData).toContain('#AUD-OH-001'); expect(htmlData).toContain('Kentucky, speech-language pathologist'); expect(htmlData).toContain('#SLP-KY-002'); + expect(htmlData).toContain('03/01/2026'); + // Verify two-column table headers are present + expect(htmlData).toContain('Privilege'); + expect(htmlData).toContain('Expires'); }); it('should throw error when no recipients found', async () => { 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 8e50a79e1..9c13af673 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 @@ -1102,7 +1102,7 @@ describe('EmailNotificationService', () => { Body: { Html: { Charset: 'UTF-8', - Data: expect.stringContaining('Privilege Expiration Reminder'), + Data: expect.stringContaining('Hi John,'), }, }, Subject: { @@ -1119,15 +1119,17 @@ describe('EmailNotificationService', () => { const htmlContent = emailCall.args[0].input.Content?.Simple?.Body?.Html?.Data; expect(htmlContent).toBeDefined(); - expect(htmlContent).toContain('Hello John'); - expect(htmlContent).toContain('will expire on February 16, 2026'); + expect(htmlContent).toContain('Hi John,'); + expect(htmlContent).toContain('one or more of your privileges will expire on'); + expect(htmlContent).toContain('02/16/2026'); expect(htmlContent).toContain('Ohio, audiologist'); expect(htmlContent).toContain('#AUD-OH-001'); - expect(htmlContent).toContain('Expires: February 16, 2026'); expect(htmlContent).toContain('Kentucky, speech-language pathologist'); expect(htmlContent).toContain('#SLP-KY-002'); - expect(htmlContent).toContain('Expires: March 1, 2026'); - expect(htmlContent).toContain('Please visit Compact Connect to renew your privileges before they expire'); + expect(htmlContent).toContain('03/01/2026'); + // Verify two-column table headers + expect(htmlContent).toContain('Privilege'); + expect(htmlContent).toContain('Expires'); }); it('should throw error when no recipients provided', async () => { @@ -1181,11 +1183,11 @@ describe('EmailNotificationService', () => { const htmlContent = emailCall.args[0].input.Content?.Simple?.Body?.Html?.Data; expect(htmlContent).toBeDefined(); - expect(htmlContent).toContain('Hello Jane'); - expect(htmlContent).toContain('will expire on March 1, 2026'); + expect(htmlContent).toContain('Hi Jane,'); + expect(htmlContent).toContain('one or more of your privileges will expire on'); + expect(htmlContent).toContain('03/01/2026'); expect(htmlContent).toContain('Nebraska, speech-language pathologist'); expect(htmlContent).toContain('#SLP-NE-123'); - expect(htmlContent).toContain('Expires: March 1, 2026'); }); }); }); diff --git a/backend/compact-connect/lambdas/python/search/tests/__init__.py b/backend/compact-connect/lambdas/python/search/tests/__init__.py index 96c10c577..c481991d8 100644 --- a/backend/compact-connect/lambdas/python/search/tests/__init__.py +++ b/backend/compact-connect/lambdas/python/search/tests/__init__.py @@ -105,4 +105,4 @@ def setUpClass(cls): cc_common.config.config = cls.config cls.mock_context = MagicMock(name='MockLambdaContext', spec=LambdaContext) # Configure mock_context.get_remaining_time_in_millis to return large value by default - cls.mock_context.get_remaining_time_in_millis.return_value = 900_000 # 15 minutes in ms + cls.mock_context.get_remaining_time_in_millis.return_value = 900_000 # 15 minutes in ms \ No newline at end of file From cd3021db1be0a1520b42f8d9e3388c26922c009d Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 2 Feb 2026 16:36:31 -0600 Subject: [PATCH 12/52] Fix logo size --- .../lambdas/nodejs/lib/email/base-email-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 cddbfde20..b775518de 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 @@ -358,7 +358,7 @@ export abstract class BaseEmailService { }, 'props': { 'width': null, - 'height': 24, + 'height': 100, 'url': `${BaseEmailService.getEmailImageBaseUrl()}/compact-connect-logo-final.png`, 'alt': '', 'linkHref': null, From f74e30221339ef61ae4148ce4d1934ec758037f5 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 4 Feb 2026 09:41:44 -0600 Subject: [PATCH 13/52] Update power user permission policy --- backend/multi-account/README.md | 65 +++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/backend/multi-account/README.md b/backend/multi-account/README.md index 93ad638fe..2495bc383 100644 --- a/backend/multi-account/README.md +++ b/backend/multi-account/README.md @@ -107,8 +107,8 @@ new AWS organization that we will set up here. Have them: ### Configure Permission Set Inline Policies To enhance security, configure inline policies on IAM Identity Center permission sets to restrict certain actions: -#### Lambda Function Code Update Protection -We do not want users updating runtime code out of bands of our CI/CD reviewal and deployment process. Apply the following inline policy to the `AWSPowerUserAccess` permission set to prevent unauthorized Lambda function updates, as well as updates to our backup resources: +#### Lambda Function Code Update Protection and Resource Deletion Prevention +We do not want users updating runtime code or deleting critical resources outside of our CI/CD review and deployment process. Apply the following inline policy to the `AWSPowerUserAccess` permission set. 1. Log into the AWS Management account console via your IAM Identity Center user 2. Go to the IAM Identity Center service, Permission sets view @@ -150,16 +150,73 @@ We do not want users updating runtime code out of bands of our CI/CD reviewal an "Resource": [ "*" ] + }, + { + "Sid": "DenyResourceModification", + "Effect": "Deny", + "Action": [ + "dynamodb:Delete*", + "s3:Delete*", + "s3:Create*", + "s3:Put*", + "s3:Replicate*", + "s3:Update*", + "events:Delete*", + "sqs:DeleteQueue", + "sns:Delete*", + "ses:Delete*", + "ses:Update*", + "cognito-idp:DeleteUserPool", + "cognito-idp:DeleteUserPoolDomain", + "cognito-idp:DeleteGroup", + "cognito-idp:DeleteIdentityProvider", + "cognito-idp:DeleteResourceServer", + "cognito-idp:DeleteManagedLoginBranding", + "ec2:DeleteVpc", + "ec2:DeleteSubnet", + "ec2:DeleteSecurityGroup", + "ec2:DeleteInternetGateway", + "ec2:DeleteNatGateway", + "ec2:DeleteRouteTable", + "ec2:DeleteRoute", + "ec2:DeleteNetworkAcl", + "ec2:DeleteNetworkAclEntry", + "ec2:DeleteVpnConnection", + "ec2:DeleteVpnGateway", + "ec2:DeleteVpcEndpoint", + "ec2:DeleteVpcEndpointServiceConfigurations", + "ec2:DeleteVpcPeeringConnection", + "ec2:DeleteFlowLogs", + "ec2:DeleteEgressOnlyInternetGateway", + "kms:ScheduleKeyDeletion", + "kms:Disable*", + "kms:Delete*", + "secretsmanager:Delete*", + "apigateway:DELETE", + "apigateway:PATCH", + "apigateway:PUT", + "apigateway:POST", + "apigateway:RemoveCertificateFromDomain", + "apigateway:SetWebACL", + "apigateway:Update*", + "es:Delete*" + ], + "Resource": [ + "*" + ] } ] } ``` -6. Name the policy `DenyComputeAndBackupUpdates` +6. Name the policy `DenyComputeBackupAndResourceModifications` 7. Select "Create policy" 8. The policy will automatically apply to all users assigned to the `AWSPowerUserAccess` permission set -This policy prevents power users from modifying Lambda functions and backup resources while still allowing CloudFormation deployments to make updates during CI/CD processes. +This policy prevents power users from: +- Modifying Lambda functions, Step Functions, and backup resources +- Deleting critical infrastructure resources +- Modifying S3 bucket configurations and API Gateway resources ### Disallow Root - Log into the AWS Management account console via your IAM Identity Center user From 317c04f84c825aa32831eea691ed7af6410db3e3 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 4 Feb 2026 12:45:18 -0600 Subject: [PATCH 14/52] Only include privileges in email that are active This required adding a pre-load hook to the API response schemas to check for licenses/privileges that may have expired in OpenSearch since their last update in DynamoDB. --- .../data_model/schema/license/api.py | 44 +++++- .../data_model/schema/privilege/api.py | 49 ++++++- .../test_schema/test_license.py | 99 +++++++++++++ .../test_schema/test_privilege.py | 94 ++++++++++++ .../tests/unit/test_sanitize_provider_data.py | 10 +- .../search/handlers/expiration_reminders.py | 28 ++-- .../function/test_expiration_reminders.py | 135 ++++++++++++++---- 7 files changed, 413 insertions(+), 46 deletions(-) diff --git a/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/license/api.py b/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/license/api.py index 8e8c271ff..12ec805b0 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/license/api.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/license/api.py @@ -3,7 +3,9 @@ Schema for API objects. """ -from marshmallow import ValidationError, validates_schema +from datetime import date + +from marshmallow import ValidationError, pre_load, validates_schema from marshmallow.fields import Date, Email, List, Nested, Raw, String from marshmallow.validate import Length @@ -24,6 +26,42 @@ from cc_common.data_model.schema.investigation.api import InvestigationGeneralResponseSchema +class LicenseExpirationStatusMixin: + """ + Mixin that checks for stale 'licenseStatus' values when loading license data. + + OpenSearch documents may have stale status values because the licenseStatus field is + calculated at write time. If the dateOfExpiration has passed since the last update, + the licenseStatus should be 'inactive' even if the stored value says 'active'. + + This mixin should be applied to license API response schemas that load data from + OpenSearch or other sources where the status may be stale. + """ + + @pre_load + def check_for_stale_license_status(self, in_data, **kwargs): + """Set licenseStatus to inactive if the license has expired.""" + if in_data.get('licenseStatus') != ActiveInactiveStatus.ACTIVE: + # Already inactive, no check needed + return in_data + + date_of_expiration = in_data.get('dateOfExpiration') + if date_of_expiration is None: + return in_data + + # Parse the expiration date (handle both string and date objects) + if isinstance(date_of_expiration, str): + expiration_date = date.fromisoformat(date_of_expiration) + else: + expiration_date = date_of_expiration + + # If expired, correct the status to inactive + if expiration_date < config.expiration_resolution_date: + in_data['licenseStatus'] = ActiveInactiveStatus.INACTIVE + + return in_data + + class LicensePostRequestSchema(CCRequestSchema, StrictSchema): """ Schema for license data as posted by a board staff-user @@ -111,7 +149,7 @@ class LicenseReportResponseSchema(ForgivingSchema): dateOfExpiration = Raw(required=True, allow_none=False) -class LicenseGeneralResponseSchema(ForgivingSchema): +class LicenseGeneralResponseSchema(LicenseExpirationStatusMixin, ForgivingSchema): """ License object fields, as seen by staff users with only the 'readGeneral' permission. @@ -152,7 +190,7 @@ class LicenseGeneralResponseSchema(ForgivingSchema): investigationStatus = InvestigationStatusField(required=False, allow_none=False) -class LicenseReadPrivateResponseSchema(ForgivingSchema): +class LicenseReadPrivateResponseSchema(LicenseExpirationStatusMixin, ForgivingSchema): """ License object fields, as seen by staff users with only the 'readPrivate' permission. diff --git a/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/privilege/api.py b/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/privilege/api.py index f8eeabd32..2a0114a21 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/privilege/api.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/privilege/api.py @@ -1,14 +1,17 @@ # ruff: noqa: N801, N815, ARG002 invalid-name unused-argument -from marshmallow import Schema +from datetime import date + +from marshmallow import Schema, pre_load from marshmallow.fields import List, Nested, Raw, String from marshmallow.validate import ContainsNoneOf, Length +from cc_common.config import config from cc_common.data_model.schema.adverse_action.api import ( AdverseActionGeneralResponseSchema, AdverseActionPublicResponseSchema, ) from cc_common.data_model.schema.base_record import ForgivingSchema -from cc_common.data_model.schema.common import UpdateCategory +from cc_common.data_model.schema.common import ActiveInactiveStatus, UpdateCategory from cc_common.data_model.schema.fields import ( ActiveInactive, Compact, @@ -19,6 +22,42 @@ from cc_common.data_model.schema.investigation.api import InvestigationGeneralResponseSchema +class PrivilegeExpirationStatusMixin: + """ + Mixin that checks for stale 'status' values when loading privilege data. + + OpenSearch documents may have stale status values because the status field is calculated + at write time. If the dateOfExpiration has passed since the last update, the status + should be 'inactive' even if the stored value says 'active'. + + This mixin should be applied to privilege API response schemas that load data from + OpenSearch or other sources where the status may be stale. + """ + + @pre_load + def check_for_stale_status(self, in_data, **kwargs): + """Set status to inactive if the privilege has expired.""" + if in_data.get('status') != ActiveInactiveStatus.ACTIVE: + # Already inactive, no check needed + return in_data + + date_of_expiration = in_data.get('dateOfExpiration') + if date_of_expiration is None: + return in_data + + # Parse the expiration date (handle both string and date objects) + if isinstance(date_of_expiration, str): + expiration_date = date.fromisoformat(date_of_expiration) + else: + expiration_date = date_of_expiration + + # If expired, set the status to inactive + if expiration_date < config.expiration_resolution_date: + in_data['status'] = ActiveInactiveStatus.INACTIVE + + return in_data + + class AttestationVersionResponseSchema(Schema): """ This schema is intended to be used by any api response in the system which needs to track which attestations have @@ -57,7 +96,7 @@ class PrivilegeUpdatePreviousGeneralResponseSchema(ForgivingSchema): privilegeId = String(required=False, allow_none=False) -class PrivilegeGeneralResponseSchema(ForgivingSchema): +class PrivilegeGeneralResponseSchema(PrivilegeExpirationStatusMixin, ForgivingSchema): """ Schema defining fields available to all staff users with only the 'readGeneral' permission. @@ -92,7 +131,7 @@ class PrivilegeGeneralResponseSchema(ForgivingSchema): investigationStatus = InvestigationStatusField(required=False, allow_none=False) -class PrivilegeReadPrivateResponseSchema(ForgivingSchema): +class PrivilegeReadPrivateResponseSchema(PrivilegeExpirationStatusMixin, ForgivingSchema): """ Schema defining fields available to staff users with the 'readPrivate' or higher permission. @@ -131,7 +170,7 @@ class PrivilegeReadPrivateResponseSchema(ForgivingSchema): ssnLastFour = String(required=False, allow_none=False, validate=Length(equal=4)) -class PrivilegePublicResponseSchema(ForgivingSchema): +class PrivilegePublicResponseSchema(PrivilegeExpirationStatusMixin, ForgivingSchema): """ Privilege object fields, as seen by the public lookup endpoints. diff --git a/backend/compact-connect/lambdas/python/common/tests/unit/test_data_model/test_schema/test_license.py b/backend/compact-connect/lambdas/python/common/tests/unit/test_data_model/test_schema/test_license.py index 73f239035..64a515d57 100644 --- a/backend/compact-connect/lambdas/python/common/tests/unit/test_data_model/test_schema/test_license.py +++ b/backend/compact-connect/lambdas/python/common/tests/unit/test_data_model/test_schema/test_license.py @@ -301,3 +301,102 @@ def test_compact_eligible_with_inactive_license_not_allowed(self): with self.assertRaises(ValidationError): LicenseIngestSchema().load({'compact': 'aslp', 'jurisdiction': 'oh', **license_record}) + + +class TestLicenseGeneralResponseSchemaExpirationCheck(TstLambdas): + """ + Tests for the LicenseExpirationStatusMixin applied to LicenseGeneralResponseSchema. + + This mixin checks for stale 'licenseStatus' values when loading license data from sources + like OpenSearch where the status may not have been updated after expiration. + """ + + def _make_license_data(self, *, license_status='active', date_of_expiration='2100-01-01'): + """Create minimal valid license data for testing.""" + return { + 'providerId': 'a4182428-d061-701c-82e5-a3d1d547d797', + 'type': 'license', + 'dateOfUpdate': '2024-01-01T00:00:00+00:00', + 'compact': 'aslp', + 'jurisdiction': 'oh', + 'licenseType': 'audiologist', + 'licenseStatus': license_status, + 'jurisdictionUploadedLicenseStatus': 'active', + 'compactEligibility': 'eligible', + 'jurisdictionUploadedCompactEligibility': 'eligible', + 'givenName': 'John', + 'familyName': 'Doe', + 'dateOfIssuance': '2024-01-01', + 'dateOfExpiration': date_of_expiration, + 'homeAddressStreet1': '123 Main St', + 'homeAddressCity': 'Columbus', + 'homeAddressState': 'OH', + 'homeAddressPostalCode': '43215', + } + + def test_expired_license_status_corrected_to_inactive(self): + """When licenseStatus is 'active' but dateOfExpiration is in the past, licenseStatus becomes 'inactive'.""" + from cc_common.data_model.schema.license.api import LicenseGeneralResponseSchema + + license_data = self._make_license_data( + license_status='active', + date_of_expiration='2020-01-01', # Expired + ) + + result = LicenseGeneralResponseSchema().load(license_data) + + self.assertEqual('inactive', result['licenseStatus']) + + def test_unexpired_license_status_remains_active(self): + """When licenseStatus is 'active' and dateOfExpiration is in the future, licenseStatus stays 'active'.""" + from cc_common.data_model.schema.license.api import LicenseGeneralResponseSchema + + license_data = self._make_license_data( + license_status='active', + date_of_expiration='2100-01-01', # Far in the future + ) + + result = LicenseGeneralResponseSchema().load(license_data) + + self.assertEqual('active', result['licenseStatus']) + + def test_already_inactive_license_status_remains_inactive(self): + """When licenseStatus is already 'inactive', it stays 'inactive' regardless of expiration.""" + from cc_common.data_model.schema.license.api import LicenseGeneralResponseSchema + + license_data = self._make_license_data( + license_status='inactive', + date_of_expiration='2100-01-01', # Not expired, but status is inactive + ) + + result = LicenseGeneralResponseSchema().load(license_data) + + self.assertEqual('inactive', result['licenseStatus']) + + @patch('cc_common.config._Config.current_standard_datetime', datetime.fromisoformat('2024-11-09T03:59:59+00:00')) + def test_license_status_active_right_before_expiration_utc_minus_four(self): + """License status remains 'active' right before midnight UTC-4 on expiration day.""" + from cc_common.data_model.schema.license.api import LicenseGeneralResponseSchema + + license_data = self._make_license_data( + license_status='active', + date_of_expiration='2024-11-08', # Expires at midnight UTC-4 on Nov 9 + ) + + result = LicenseGeneralResponseSchema().load(license_data) + + self.assertEqual('active', result['licenseStatus']) + + @patch('cc_common.config._Config.current_standard_datetime', datetime.fromisoformat('2024-11-09T04:00:00+00:00')) + def test_license_status_corrected_to_inactive_at_expiration_utc_minus_four(self): + """License status corrected to 'inactive' at midnight UTC-4 on the day after expiration.""" + from cc_common.data_model.schema.license.api import LicenseGeneralResponseSchema + + license_data = self._make_license_data( + license_status='active', + date_of_expiration='2024-11-08', # Expired at midnight UTC-4 on Nov 9 + ) + + result = LicenseGeneralResponseSchema().load(license_data) + + self.assertEqual('inactive', result['licenseStatus']) diff --git a/backend/compact-connect/lambdas/python/common/tests/unit/test_data_model/test_schema/test_privilege.py b/backend/compact-connect/lambdas/python/common/tests/unit/test_data_model/test_schema/test_privilege.py index e58a86270..68146b214 100644 --- a/backend/compact-connect/lambdas/python/common/tests/unit/test_data_model/test_schema/test_privilege.py +++ b/backend/compact-connect/lambdas/python/common/tests/unit/test_data_model/test_schema/test_privilege.py @@ -222,3 +222,97 @@ def test_valid_if_deactivation_details_present_when_update_type_is_deactivation( } PrivilegeUpdateRecordSchema().load(privilege_data) + + +class TestPrivilegeGeneralResponseSchemaExpirationCheck(TstLambdas): + """ + Tests for the PrivilegeExpirationStatusMixin applied to PrivilegeGeneralResponseSchema. + + This mixin checks for stale 'status' values when loading privilege data from sources + like OpenSearch where the status may not have been updated after expiration. + """ + + def _make_privilege_data(self, *, status='active', date_of_expiration='2100-01-01'): + """Create minimal valid privilege data for testing.""" + return { + 'type': 'privilege', + 'providerId': 'a4182428-d061-701c-82e5-a3d1d547d797', + 'compact': 'aslp', + 'jurisdiction': 'oh', + 'licenseJurisdiction': 'ne', + 'licenseType': 'audiologist', + 'dateOfIssuance': '2024-01-01', + 'dateOfRenewal': '2024-01-01', + 'dateOfExpiration': date_of_expiration, + 'dateOfUpdate': '2024-01-01T00:00:00+00:00', + 'administratorSetStatus': 'active', + 'privilegeId': 'test-priv-123', + 'status': status, + } + + def test_expired_privilege_status_corrected_to_inactive(self): + """When status is 'active' but dateOfExpiration is in the past, status becomes 'inactive'.""" + from cc_common.data_model.schema.privilege.api import PrivilegeGeneralResponseSchema + + privilege_data = self._make_privilege_data( + status='active', + date_of_expiration='2020-01-01', # Expired + ) + + result = PrivilegeGeneralResponseSchema().load(privilege_data) + + self.assertEqual('inactive', result['status']) + + def test_unexpired_privilege_status_remains_active(self): + """When status is 'active' and dateOfExpiration is in the future, status stays 'active'.""" + from cc_common.data_model.schema.privilege.api import PrivilegeGeneralResponseSchema + + privilege_data = self._make_privilege_data( + status='active', + date_of_expiration='2100-01-01', # Far in the future + ) + + result = PrivilegeGeneralResponseSchema().load(privilege_data) + + self.assertEqual('active', result['status']) + + def test_already_inactive_status_remains_inactive(self): + """When status is already 'inactive', it stays 'inactive' regardless of expiration.""" + from cc_common.data_model.schema.privilege.api import PrivilegeGeneralResponseSchema + + privilege_data = self._make_privilege_data( + status='inactive', + date_of_expiration='2100-01-01', # Not expired, but status is inactive + ) + + result = PrivilegeGeneralResponseSchema().load(privilege_data) + + self.assertEqual('inactive', result['status']) + + @patch('cc_common.config._Config.current_standard_datetime', datetime.fromisoformat('2024-11-09T03:59:59+00:00')) + def test_status_active_right_before_expiration_utc_minus_four(self): + """Status remains 'active' right before midnight UTC-4 on expiration day.""" + from cc_common.data_model.schema.privilege.api import PrivilegeGeneralResponseSchema + + privilege_data = self._make_privilege_data( + status='active', + date_of_expiration='2024-11-08', # Expires at midnight UTC-4 on Nov 9 + ) + + result = PrivilegeGeneralResponseSchema().load(privilege_data) + + self.assertEqual('active', result['status']) + + @patch('cc_common.config._Config.current_standard_datetime', datetime.fromisoformat('2024-11-09T04:00:00+00:00')) + def test_status_corrected_to_inactive_at_expiration_utc_minus_four(self): + """Status corrected to 'inactive' at midnight UTC-4 on the day after expiration.""" + from cc_common.data_model.schema.privilege.api import PrivilegeGeneralResponseSchema + + privilege_data = self._make_privilege_data( + status='active', + date_of_expiration='2024-11-08', # Expired at midnight UTC-4 on Nov 9 + ) + + result = PrivilegeGeneralResponseSchema().load(privilege_data) + + self.assertEqual('inactive', result['status']) diff --git a/backend/compact-connect/lambdas/python/common/tests/unit/test_sanitize_provider_data.py b/backend/compact-connect/lambdas/python/common/tests/unit/test_sanitize_provider_data.py index aa771032e..05c592606 100644 --- a/backend/compact-connect/lambdas/python/common/tests/unit/test_sanitize_provider_data.py +++ b/backend/compact-connect/lambdas/python/common/tests/unit/test_sanitize_provider_data.py @@ -1,4 +1,6 @@ import json +from datetime import datetime +from unittest.mock import patch from tests import TstLambdas @@ -72,10 +74,16 @@ def when_testing_general_provider_info_returned(self, scopes: set[str]): self.assertEqual(expected_provider, resp) - def test_sanitized_provider_record_returned_if_caller_does_not_have_read_private_permissions_for_jurisdiction(self): + @patch('cc_common.config._Config.current_standard_datetime', datetime.fromisoformat('2025-04-01T12:00:00+00:00')) + def test_sanitized_provider_record_returned_if_caller_does_not_have_read_private_permissions_for_jurisdiction( + self, + ): + # Mock datetime so schema expiration checks keep license/privilege status active (test data expires 2025-04-04) self.when_testing_general_provider_info_returned( scopes={'openid', 'email', 'aslp/readGeneral', 'az/aslp.readPrivate'} ) + @patch('cc_common.config._Config.current_standard_datetime', datetime.fromisoformat('2025-04-01T12:00:00+00:00')) def test_sanitized_provider_record_returned_if_caller_does_not_have_any_read_private_permissions(self): + # Mock datetime so schema expiration checks keep license/privilege status active (test data expires 2025-04-04) self.when_testing_general_provider_info_returned(scopes={'openid', 'email', 'aslp/readGeneral'}) diff --git a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py index af8b86166..5bc045de6 100644 --- a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py @@ -4,7 +4,6 @@ from collections.abc import Generator from dataclasses import dataclass, replace from datetime import UTC, date, datetime, timedelta -from uuid import UUID from aws_lambda_powertools.utilities.typing import LambdaContext from cc_common.config import config, logger @@ -312,19 +311,30 @@ def _process_provider_notification( provider_first_name = provider_doc['givenName'] try: - # Build full privileges list for email (state name, license type, privilege id, ISO date per privilege) + # Build privileges list for email, filtering to only active privileges. email_privileges = [] for privilege in provider_doc['privileges']: + # Only include active privileges in the email + if privilege.get('status') != 'active': + logger.info( + 'Skipping inactive privilege', + provider_id=provider_id, + compact=compact, + privilege_id=privilege['privilegeId'], + ) + continue jurisdiction_code = privilege['jurisdiction'] jurisdiction_display = CompactConfigUtility.get_jurisdiction_name(jurisdiction_code) if jurisdiction_display is None: raise ValueError(f'Unknown jurisdiction code for display name: {jurisdiction_code!r}') - email_privileges.append({ - 'jurisdiction': jurisdiction_display, - 'licenseType': privilege['licenseType'], - 'privilegeId': privilege['privilegeId'], - 'dateOfExpiration': privilege['dateOfExpiration'], - }) + email_privileges.append( + { + 'jurisdiction': jurisdiction_display, + 'licenseType': privilege['licenseType'], + 'privilegeId': privilege['privilegeId'], + 'dateOfExpiration': privilege['dateOfExpiration'], + } + ) template_variables = PrivilegeExpirationReminderTemplateVariables( provider_first_name=provider_first_name, @@ -436,7 +446,7 @@ def _build_expiration_query(*, expiration_date: date, page_size: int) -> dict: 'query': { 'nested': { # Nested query for privileges - # This query will match any privilege that matches the _entire_ query, so only providers with at least + # This query will match any privilege that matches the _entire_ query, so only providers with at least # one privilege that is active and expires on the specified date will be included in the results. 'path': 'privileges', 'query': { diff --git a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py index afb334682..95864edfe 100644 --- a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py @@ -1,5 +1,5 @@ import json -from datetime import date, timedelta +from datetime import date, datetime, timedelta from unittest.mock import MagicMock, patch from uuid import uuid4 @@ -113,9 +113,7 @@ def test_iterate_privileges_by_expiration_date_resumes_with_initial_search_after @patch('handlers.expiration_reminders.ProviderGeneralResponseSchema') @patch('handlers.expiration_reminders.opensearch_client') - def test_iterate_privileges_by_expiration_date_returns_full_provider_document( - self, mock_client, mock_schema_class - ): + def test_iterate_privileges_by_expiration_date_returns_full_provider_document(self, mock_client, mock_schema_class): """Provider document includes full privileges list from _source (no inner_hits filtering).""" full_privileges = [ { @@ -181,7 +179,8 @@ def _make_provider_doc( doc = { 'providerId': provider_id or str(uuid4()), 'givenName': given_name, - 'privileges': privileges or [ + 'privileges': privileges + or [ { 'privilegeId': 'a', 'dateOfExpiration': '2026-02-16', @@ -218,9 +217,11 @@ def test_handler_sends_email_and_records_success(self, mock_iter, mock_config, m from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders - mock_iter.return_value = iter([ - PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor1']), - ]) + mock_iter.return_value = iter( + [ + PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor1']), + ] + ) resp = process_expiration_reminders(self._make_event(days_before=30), self.mock_context) @@ -263,9 +264,11 @@ def test_handler_skips_when_already_sent(self, mock_iter, mock_config, mock_trac from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders - mock_iter.return_value = iter([ - PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor1']), - ]) + mock_iter.return_value = iter( + [ + PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor1']), + ] + ) resp = process_expiration_reminders(self._make_event(days_before=7), self.mock_context) @@ -289,9 +292,11 @@ def test_handler_logs_error_for_provider_without_email(self, mock_iter, mock_con from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders - mock_iter.return_value = iter([ - PaginatedProviderResult(provider_doc=self._make_provider_doc(email=None), search_after=['cursor1']), - ]) + mock_iter.return_value = iter( + [ + PaginatedProviderResult(provider_doc=self._make_provider_doc(email=None), search_after=['cursor1']), + ] + ) resp = process_expiration_reminders(self._make_event(days_before=0), self.mock_context) @@ -316,9 +321,11 @@ def test_handler_records_failure_on_email_error(self, mock_iter, mock_config, mo from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders - mock_iter.return_value = iter([ - PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor1']), - ]) + mock_iter.return_value = iter( + [ + PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor1']), + ] + ) resp = process_expiration_reminders(self._make_event(), self.mock_context) @@ -348,9 +355,11 @@ def test_handler_records_failure_when_privilege_has_unknown_jurisdiction( doc = self._make_provider_doc() doc['privileges'][0]['jurisdiction'] = 'xx' - mock_iter.return_value = iter([ - PaginatedProviderResult(provider_doc=doc, search_after=['cursor1']), - ]) + mock_iter.return_value = iter( + [ + PaginatedProviderResult(provider_doc=doc, search_after=['cursor1']), + ] + ) resp = process_expiration_reminders(self._make_event(), self.mock_context) @@ -474,11 +483,13 @@ def test_handler_continuation_parses_accumulated_metrics_and_search_after( } # This invocation processes 1 new email, 2 already sent - mock_iter.return_value = iter([ - PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor2']), - PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor3']), - PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor4']), - ]) + mock_iter.return_value = iter( + [ + PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor2']), + PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor3']), + PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor4']), + ] + ) resp = process_expiration_reminders(event_with_continuation, self.mock_context) @@ -512,9 +523,11 @@ def test_handler_invokes_itself_with_pagination_values_when_reaching_limit( from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders # Return one provider doc - mock_iter.return_value = iter([ - PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor1']), - ]) + mock_iter.return_value = iter( + [ + PaginatedProviderResult(provider_doc=self._make_provider_doc(), search_after=['cursor1']), + ] + ) # Mock the context to simulate approaching timeout # TIMEOUT_BUFFER_MS is 120,000 (2 minutes), so set remaining time to 100,000 (< buffer) @@ -555,3 +568,69 @@ def test_handler_max_continuation_depth_raises(self): with self.assertRaises(RuntimeError) as ctx: process_expiration_reminders(event, self.mock_context) self.assertIn(str(MAX_CONTINUATION_DEPTH), str(ctx.exception)) + + @patch('handlers.expiration_reminders.ExpirationReminderTracker') + @patch('handlers.expiration_reminders.config') + @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') + def test_handler_only_includes_active_privileges_in_email(self, mock_iter, mock_config, mock_tracker_class): + """ + Only active privileges are included in the email; inactive privileges are filtered out. + """ + mock_config.compacts = ['aslp'] + mock_email_client = MagicMock() + mock_config.email_service_client = mock_email_client + + mock_tracker_instance = MagicMock() + mock_tracker_instance.was_already_sent.return_value = False + mock_tracker_class.return_value = mock_tracker_instance + + from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders + + # Create provider with three privileges: + # 1. Inactive (encumbered) + # 2. Inactive (was stale-expired in OpenSearch, schema corrected status to inactive) + # 3. Active and expiring on target date + privileges = [ + { + 'privilegeId': 'encumbered-priv', + 'dateOfExpiration': '2026-02-16', + 'status': 'inactive', # Encumbered - results in inactive status + 'jurisdiction': 'oh', + 'licenseType': 'aud', + }, + { + 'privilegeId': 'active-expiring-priv', + 'dateOfExpiration': '2026-02-16', + 'status': 'active', # Active privilege expiring on target date + 'jurisdiction': 'ne', + 'licenseType': 'aud', + }, + ] + provider_doc = self._make_provider_doc(privileges=privileges) + + mock_iter.return_value = iter( + [ + PaginatedProviderResult(provider_doc=provider_doc, search_after=['cursor1']), + ] + ) + + # Use daysBefore=0 to simulate day-of expiration notification + resp = process_expiration_reminders(self._make_event(days_before=0), self.mock_context) + + self.assertEqual('complete', resp['status']) + self.assertEqual(1, resp['metrics']['sent']) + + # Verify only the active privilege was passed to the email client + mock_email_client.send_privilege_expiration_reminder_email.assert_called_once() + call_kwargs = mock_email_client.send_privilege_expiration_reminder_email.call_args.kwargs + tv = call_kwargs['template_variables'] + + # Should only have 1 privilege (the active one) + self.assertEqual(1, len(tv.privileges), 'Only active privileges should be included in email') + + # Verify it's the correct privilege + priv = tv.privileges[0] + self.assertEqual('active-expiring-priv', priv['privilegeId']) + self.assertEqual('Nebraska', priv['jurisdiction']) + self.assertEqual('aud', priv['licenseType']) + self.assertEqual('2026-02-16', priv['dateOfExpiration']) From e479480fbd52003de5fe2a4ac3158bc3b7c1a49b Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 4 Feb 2026 12:46:10 -0600 Subject: [PATCH 15/52] Add expiration check to cosm codebase --- .../expiration_reminder_stack/__init__.py | 10 +- .../data_model/schema/license/api.py | 44 ++++++++- .../test_schema/test_license.py | 99 +++++++++++++++++++ .../tests/unit/test_sanitize_provider_data.py | 10 +- 4 files changed, 155 insertions(+), 8 deletions(-) diff --git a/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py b/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py index 77aa644a6..80b5d97f2 100644 --- a/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py +++ b/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py @@ -126,10 +126,12 @@ def __init__( targets=[ LambdaFunction( handler=self.expiration_reminder_handler, - event=RuleTargetInput.from_object({ - 'daysBefore': reminder_config['days_before'], - 'compact': compact, - }), + event=RuleTargetInput.from_object( + { + 'daysBefore': reminder_config['days_before'], + 'compact': compact, + } + ), ) ], ) diff --git a/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/license/api.py b/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/license/api.py index 8e8c271ff..2a1d39d56 100644 --- a/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/license/api.py +++ b/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/license/api.py @@ -3,7 +3,9 @@ Schema for API objects. """ -from marshmallow import ValidationError, validates_schema +from datetime import date + +from marshmallow import ValidationError, pre_load, validates_schema from marshmallow.fields import Date, Email, List, Nested, Raw, String from marshmallow.validate import Length @@ -24,6 +26,42 @@ from cc_common.data_model.schema.investigation.api import InvestigationGeneralResponseSchema +class LicenseExpirationStatusMixin: + """ + Mixin that corrects stale 'licenseStatus' values when loading license data. + + OpenSearch documents may have stale status values because the licenseStatus field is + calculated at write time. If the dateOfExpiration has passed since the last update, + the licenseStatus should be 'inactive' even if the stored value says 'active'. + + This mixin should be applied to license API response schemas that load data from + OpenSearch or other sources where the status may be stale. + """ + + @pre_load + def correct_expired_license_status(self, in_data, **kwargs): + """Set licenseStatus to inactive if the license has expired.""" + if in_data.get('licenseStatus') != ActiveInactiveStatus.ACTIVE: + # Already inactive, no correction needed + return in_data + + date_of_expiration = in_data.get('dateOfExpiration') + if date_of_expiration is None: + return in_data + + # Parse the expiration date (handle both string and date objects) + if isinstance(date_of_expiration, str): + expiration_date = date.fromisoformat(date_of_expiration) + else: + expiration_date = date_of_expiration + + # If expired, correct the status to inactive + if expiration_date < config.expiration_resolution_date: + in_data['licenseStatus'] = ActiveInactiveStatus.INACTIVE + + return in_data + + class LicensePostRequestSchema(CCRequestSchema, StrictSchema): """ Schema for license data as posted by a board staff-user @@ -111,7 +149,7 @@ class LicenseReportResponseSchema(ForgivingSchema): dateOfExpiration = Raw(required=True, allow_none=False) -class LicenseGeneralResponseSchema(ForgivingSchema): +class LicenseGeneralResponseSchema(LicenseExpirationStatusMixin, ForgivingSchema): """ License object fields, as seen by staff users with only the 'readGeneral' permission. @@ -152,7 +190,7 @@ class LicenseGeneralResponseSchema(ForgivingSchema): investigationStatus = InvestigationStatusField(required=False, allow_none=False) -class LicenseReadPrivateResponseSchema(ForgivingSchema): +class LicenseReadPrivateResponseSchema(LicenseExpirationStatusMixin, ForgivingSchema): """ License object fields, as seen by staff users with only the 'readPrivate' permission. diff --git a/backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_license.py b/backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_license.py index 95286fa3c..2b9de2dfd 100644 --- a/backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_license.py +++ b/backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_license.py @@ -301,3 +301,102 @@ def test_compact_eligible_with_inactive_license_not_allowed(self): with self.assertRaises(ValidationError): LicenseIngestSchema().load({'compact': 'cosm', 'jurisdiction': 'oh', **license_record}) + + +class TestLicenseGeneralResponseSchemaExpirationCheck(TstLambdas): + """ + Tests for the LicenseExpirationStatusMixin applied to LicenseGeneralResponseSchema. + + This mixin corrects stale 'licenseStatus' values when loading license data from sources + like OpenSearch where the status may not have been updated after expiration. + """ + + def _make_license_data(self, *, license_status='active', date_of_expiration='2100-01-01'): + """Create minimal valid license data for testing.""" + return { + 'providerId': 'a4182428-d061-701c-82e5-a3d1d547d797', + 'type': 'license', + 'dateOfUpdate': '2024-01-01T00:00:00+00:00', + 'compact': 'cosm', + 'jurisdiction': 'oh', + 'licenseType': 'cosmetologist', + 'licenseStatus': license_status, + 'jurisdictionUploadedLicenseStatus': 'active', + 'compactEligibility': 'eligible', + 'jurisdictionUploadedCompactEligibility': 'eligible', + 'givenName': 'John', + 'familyName': 'Doe', + 'dateOfIssuance': '2024-01-01', + 'dateOfExpiration': date_of_expiration, + 'homeAddressStreet1': '123 Main St', + 'homeAddressCity': 'Columbus', + 'homeAddressState': 'OH', + 'homeAddressPostalCode': '43215', + } + + def test_expired_license_status_corrected_to_inactive(self): + """When licenseStatus is 'active' but dateOfExpiration is in the past, licenseStatus becomes 'inactive'.""" + from cc_common.data_model.schema.license.api import LicenseGeneralResponseSchema + + license_data = self._make_license_data( + license_status='active', + date_of_expiration='2020-01-01', # Expired + ) + + result = LicenseGeneralResponseSchema().load(license_data) + + self.assertEqual('inactive', result['licenseStatus']) + + def test_unexpired_license_status_remains_active(self): + """When licenseStatus is 'active' and dateOfExpiration is in the future, licenseStatus stays 'active'.""" + from cc_common.data_model.schema.license.api import LicenseGeneralResponseSchema + + license_data = self._make_license_data( + license_status='active', + date_of_expiration='2100-01-01', # Far in the future + ) + + result = LicenseGeneralResponseSchema().load(license_data) + + self.assertEqual('active', result['licenseStatus']) + + def test_already_inactive_license_status_remains_inactive(self): + """When licenseStatus is already 'inactive', it stays 'inactive' regardless of expiration.""" + from cc_common.data_model.schema.license.api import LicenseGeneralResponseSchema + + license_data = self._make_license_data( + license_status='inactive', + date_of_expiration='2100-01-01', # Not expired, but status is inactive + ) + + result = LicenseGeneralResponseSchema().load(license_data) + + self.assertEqual('inactive', result['licenseStatus']) + + @patch('cc_common.config._Config.current_standard_datetime', datetime.fromisoformat('2024-11-09T03:59:59+00:00')) + def test_license_status_active_right_before_expiration_utc_minus_four(self): + """License status remains 'active' right before midnight UTC-4 on expiration day.""" + from cc_common.data_model.schema.license.api import LicenseGeneralResponseSchema + + license_data = self._make_license_data( + license_status='active', + date_of_expiration='2024-11-08', # Expires at midnight UTC-4 on Nov 9 + ) + + result = LicenseGeneralResponseSchema().load(license_data) + + self.assertEqual('active', result['licenseStatus']) + + @patch('cc_common.config._Config.current_standard_datetime', datetime.fromisoformat('2024-11-09T04:00:00+00:00')) + def test_license_status_corrected_to_inactive_at_expiration_utc_minus_four(self): + """License status corrected to 'inactive' at midnight UTC-4 on the day after expiration.""" + from cc_common.data_model.schema.license.api import LicenseGeneralResponseSchema + + license_data = self._make_license_data( + license_status='active', + date_of_expiration='2024-11-08', # Expired at midnight UTC-4 on Nov 9 + ) + + result = LicenseGeneralResponseSchema().load(license_data) + + self.assertEqual('inactive', result['licenseStatus']) diff --git a/backend/cosmetology-app/lambdas/python/common/tests/unit/test_sanitize_provider_data.py b/backend/cosmetology-app/lambdas/python/common/tests/unit/test_sanitize_provider_data.py index f35b5f2a0..91d853f6a 100644 --- a/backend/cosmetology-app/lambdas/python/common/tests/unit/test_sanitize_provider_data.py +++ b/backend/cosmetology-app/lambdas/python/common/tests/unit/test_sanitize_provider_data.py @@ -1,4 +1,6 @@ import json +from datetime import datetime +from unittest.mock import patch from tests import TstLambdas @@ -66,10 +68,16 @@ def when_testing_general_provider_info_returned(self, scopes: set[str]): self.assertEqual(expected_provider, resp) - def test_sanitized_provider_record_returned_if_caller_does_not_have_read_private_permissions_for_jurisdiction(self): + @patch('cc_common.config._Config.current_standard_datetime', datetime.fromisoformat('2025-04-01T12:00:00+00:00')) + def test_sanitized_provider_record_returned_if_caller_does_not_have_read_private_permissions_for_jurisdiction( + self, + ): + # Mock datetime so schema expiration checks keep license/privilege status active (test data expires 2025-04-04) self.when_testing_general_provider_info_returned( scopes={'openid', 'email', 'cosm/readGeneral', 'az/cosm.readPrivate'} ) + @patch('cc_common.config._Config.current_standard_datetime', datetime.fromisoformat('2025-04-01T12:00:00+00:00')) def test_sanitized_provider_record_returned_if_caller_does_not_have_any_read_private_permissions(self): + # Mock datetime so schema expiration checks keep license/privilege status active (test data expires 2025-04-04) self.when_testing_general_provider_info_returned(scopes={'openid', 'email', 'cosm/readGeneral'}) From fc64812663381ae70fc2c677792bfa163f6beead Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 4 Feb 2026 13:10:08 -0600 Subject: [PATCH 16/52] Refactor lambda/tests to use/mock config properties consistently --- .../search/expiration_reminder_tracker.py | 4 +- .../search/handlers/expiration_reminders.py | 6 +- .../function/test_expiration_reminders.py | 87 ++++++------------- 3 files changed, 30 insertions(+), 67 deletions(-) diff --git a/backend/compact-connect/lambdas/python/search/expiration_reminder_tracker.py b/backend/compact-connect/lambdas/python/search/expiration_reminder_tracker.py index 7624e958d..2c001a51c 100644 --- a/backend/compact-connect/lambdas/python/search/expiration_reminder_tracker.py +++ b/backend/compact-connect/lambdas/python/search/expiration_reminder_tracker.py @@ -38,9 +38,7 @@ class ExpirationReminderTracker: _TTL_DAYS = 90 _SUCCESS_STATUS = 'SUCCESS' - def __init__( - self, *, compact: str, provider_id: UUID, expiration_date: str, event_type: ExpirationEventType - ): + def __init__(self, *, compact: str, provider_id: UUID, expiration_date: str, event_type: ExpirationEventType): """ Initialize the tracker for a specific provider, expiration date, and reminder type. diff --git a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py index 5bc045de6..2ccd5e694 100644 --- a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py @@ -3,7 +3,7 @@ import json from collections.abc import Generator from dataclasses import dataclass, replace -from datetime import UTC, date, datetime, timedelta +from datetime import date, timedelta from aws_lambda_powertools.utilities.typing import LambdaContext from cc_common.config import config, logger @@ -127,12 +127,12 @@ def process_expiration_reminders(event: dict, context: LambdaContext): expiration_date = _parse_iso_date(target_date_str) else: # We will be running ~ midnight UTC-4, so now(UTC) should be a few hours into the next day - today = datetime.now(UTC).date() + today = config.current_standard_datetime.date() expiration_date = today + timedelta(days=days_before) target_date_str = expiration_date.isoformat() # Get scheduledTime for logging (default to current time if not provided) - scheduled_time = event.get('scheduledTime', datetime.now(UTC).isoformat()) + scheduled_time = event.get('scheduledTime', config.current_standard_datetime.isoformat()) event_type = DAYS_BEFORE_TO_EVENT_TYPE[days_before] logger.info( diff --git a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py index 95864edfe..32416e667 100644 --- a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py @@ -204,12 +204,10 @@ def _make_event(self, days_before: int = 30, compact: str = 'aslp') -> dict: } @patch('handlers.expiration_reminders.ExpirationReminderTracker') - @patch('handlers.expiration_reminders.config') + @patch('cc_common.config._Config.email_service_client') @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') - def test_handler_sends_email_and_records_success(self, mock_iter, mock_config, mock_tracker_class): - mock_config.compacts = ['aslp'] - mock_email_client = MagicMock() - mock_config.email_service_client = mock_email_client + def test_handler_sends_email_and_records_success(self, mock_iter, mock_email_service_client, mock_tracker_class): + mock_email_client = mock_email_service_client mock_tracker_instance = MagicMock() mock_tracker_instance.was_already_sent.return_value = False @@ -251,12 +249,10 @@ def test_handler_sends_email_and_records_success(self, mock_iter, mock_config, m self.assertEqual(ExpirationEventType.PRIVILEGE_EXPIRATION_30_DAY, tracker_call['event_type']) @patch('handlers.expiration_reminders.ExpirationReminderTracker') - @patch('handlers.expiration_reminders.config') + @patch('cc_common.config._Config.email_service_client') @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') - def test_handler_skips_when_already_sent(self, mock_iter, mock_config, mock_tracker_class): - mock_config.compacts = ['aslp'] - mock_email_client = MagicMock() - mock_config.email_service_client = mock_email_client + def test_handler_skips_when_already_sent(self, mock_iter, mock_email_service_client, mock_tracker_class): + mock_email_client = mock_email_service_client mock_tracker_instance = MagicMock() mock_tracker_instance.was_already_sent.return_value = True @@ -278,13 +274,11 @@ def test_handler_skips_when_already_sent(self, mock_iter, mock_config, mock_trac mock_email_client.send_privilege_expiration_reminder_email.assert_not_called() @patch('handlers.expiration_reminders.ExpirationReminderTracker') - @patch('handlers.expiration_reminders.config') + @patch('cc_common.config._Config.email_service_client') @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') - def test_handler_logs_error_for_provider_without_email(self, mock_iter, mock_config, mock_tracker_class): + def test_handler_logs_error_for_provider_without_email(self, mock_iter, mock_email_service_client, mock_tracker_class): """Provider without email address is skipped and logged as noEmail.""" - mock_config.compacts = ['aslp'] - mock_email_client = MagicMock() - mock_config.email_service_client = mock_email_client + mock_email_client = mock_email_service_client mock_tracker_instance = MagicMock() mock_tracker_instance.was_already_sent.return_value = False @@ -306,14 +300,11 @@ def test_handler_logs_error_for_provider_without_email(self, mock_iter, mock_con mock_email_client.send_privilege_expiration_reminder_email.assert_not_called() @patch('handlers.expiration_reminders.ExpirationReminderTracker') - @patch('handlers.expiration_reminders.config') + @patch('cc_common.config._Config.email_service_client') @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') - def test_handler_records_failure_on_email_error(self, mock_iter, mock_config, mock_tracker_class): + def test_handler_records_failure_on_email_error(self, mock_iter, mock_email_client, mock_tracker_class): """Email service raises exception; handler records failure.""" - mock_config.compacts = ['aslp'] - mock_email_client = MagicMock() mock_email_client.send_privilege_expiration_reminder_email.side_effect = Exception('Email service down') - mock_config.email_service_client = mock_email_client mock_tracker_instance = MagicMock() mock_tracker_instance.was_already_sent.return_value = False @@ -336,15 +327,12 @@ def test_handler_records_failure_on_email_error(self, mock_iter, mock_config, mo self.assertIn('Email service down', mock_tracker_instance.record_failure.call_args.kwargs['error_message']) @patch('handlers.expiration_reminders.ExpirationReminderTracker') - @patch('handlers.expiration_reminders.config') + @patch('cc_common.config._Config.email_service_client') @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') def test_handler_records_failure_when_privilege_has_unknown_jurisdiction( - self, mock_iter, mock_config, mock_tracker_class + self, mock_iter, mock_email_client, mock_tracker_class ): """Unknown jurisdiction code raises when building template; handler records failure.""" - mock_config.compacts = ['aslp'] - mock_email_client = MagicMock() - mock_config.email_service_client = mock_email_client mock_tracker_instance = MagicMock() mock_tracker_instance.was_already_sent.return_value = False @@ -411,30 +399,18 @@ def test_handler_validates_compact_value(self): ) self.assertIn('compact', str(ctx.exception)) + @patch('cc_common.config._Config.email_service_client') @patch('handlers.expiration_reminders.ExpirationReminderTracker') - @patch('handlers.expiration_reminders.config') @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') - @patch('handlers.expiration_reminders.datetime') + @patch('cc_common.config._Config.current_standard_datetime', datetime.fromisoformat('2026-01-17T12:00:00+00:00')) def test_handler_calculates_target_date_from_days_before_when_not_provided( - self, mock_datetime_class, mock_iter, mock_config, mock_tracker_class + self, mock_iter, mock_tracker_class, mock_email_client ): """When targetDate is not provided, handler calculates it based on daysBefore.""" - mock_config.compacts = ['aslp'] - mock_email_client = MagicMock() - mock_config.email_service_client = mock_email_client - mock_tracker_instance = MagicMock() mock_tracker_instance.was_already_sent.return_value = False mock_tracker_class.return_value = mock_tracker_instance - # Mock datetime.now(UTC).date() to return fixed date - today = date(2026, 1, 17) - mock_now = MagicMock() - mock_now.date.return_value = today - mock_datetime_class.now.return_value = mock_now - # Passthrough other datetime methods - mock_datetime_class.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs) - from handlers.expiration_reminders import process_expiration_reminders mock_iter.return_value = iter([]) @@ -442,9 +418,8 @@ def test_handler_calculates_target_date_from_days_before_when_not_provided( resp = process_expiration_reminders({'daysBefore': 30, 'compact': 'aslp'}, self.mock_context) self.assertEqual('complete', resp['status']) - # Verify it calculated the correct target date - expected_target_date = (today + timedelta(days=30)).isoformat() - self.assertEqual(expected_target_date, resp['targetDate']) + # Verify it calculated the correct target date (2026-01-17 + 30 days = 2026-02-16) + self.assertEqual('2026-02-16', resp['targetDate']) self.assertEqual(30, resp['daysBefore']) # Verify the generator was called with the calculated date and no initial_search_after @@ -454,15 +429,12 @@ def test_handler_calculates_target_date_from_days_before_when_not_provided( self.assertIsNone(call_kwargs.get('initial_search_after')) @patch('handlers.expiration_reminders.ExpirationReminderTracker') - @patch('handlers.expiration_reminders.config') + @patch('cc_common.config._Config.email_service_client') @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') def test_handler_continuation_parses_accumulated_metrics_and_search_after( - self, mock_iter, mock_config, mock_tracker_class + self, mock_iter, mock_email_client, mock_tracker_class ): """Continuation event with _continuation state merges accumulated metrics and resumes from searchAfter.""" - mock_config.compacts = ['aslp'] - mock_email_client = MagicMock() - mock_config.email_service_client = mock_email_client mock_tracker_instance = MagicMock() mock_tracker_instance.was_already_sent.side_effect = [False, True, True, False, False] @@ -503,18 +475,13 @@ def test_handler_continuation_parses_accumulated_metrics_and_search_after( self.assertEqual(['cursor1'], call_kwargs['initial_search_after']) @patch('handlers.expiration_reminders.ExpirationReminderTracker') - @patch('handlers.expiration_reminders.config') + @patch('cc_common.config._Config.email_service_client') + @patch('cc_common.config._Config.lambda_client') @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') def test_handler_invokes_itself_with_pagination_values_when_reaching_limit( - self, mock_iter, mock_config, mock_tracker_class + self, mock_iter, mock_lambda_client, mock_email_client, mock_tracker_class ): """Handler detects remaining time is low, invokes itself with continuation state.""" - mock_config.compacts = ['aslp'] - mock_email_client = MagicMock() - mock_config.email_service_client = mock_email_client - mock_lambda_client = MagicMock() - mock_config.lambda_client = mock_lambda_client - mock_config.lambda_function_name = 'test-function' mock_tracker_instance = MagicMock() mock_tracker_instance.was_already_sent.return_value = False @@ -533,6 +500,7 @@ def test_handler_invokes_itself_with_pagination_values_when_reaching_limit( # TIMEOUT_BUFFER_MS is 120,000 (2 minutes), so set remaining time to 100,000 (< buffer) mock_context_below_time_threshold = MagicMock() mock_context_below_time_threshold.get_remaining_time_in_millis.return_value = 100_000 + mock_context_below_time_threshold.function_name = 'test-expiration-reminders' resp = process_expiration_reminders(self._make_event(), mock_context_below_time_threshold) @@ -570,15 +538,12 @@ def test_handler_max_continuation_depth_raises(self): self.assertIn(str(MAX_CONTINUATION_DEPTH), str(ctx.exception)) @patch('handlers.expiration_reminders.ExpirationReminderTracker') - @patch('handlers.expiration_reminders.config') + @patch('cc_common.config._Config.email_service_client') @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') - def test_handler_only_includes_active_privileges_in_email(self, mock_iter, mock_config, mock_tracker_class): + def test_handler_only_includes_active_privileges_in_email(self, mock_iter, mock_email_client, mock_tracker_class): """ Only active privileges are included in the email; inactive privileges are filtered out. """ - mock_config.compacts = ['aslp'] - mock_email_client = MagicMock() - mock_config.email_service_client = mock_email_client mock_tracker_instance = MagicMock() mock_tracker_instance.was_already_sent.return_value = False From ea6c89377dedd5fdf0c3e284bf5d92b454bef77e Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 4 Feb 2026 13:12:32 -0600 Subject: [PATCH 17/52] Formatting --- .../lambdas/python/search/tests/__init__.py | 2 +- .../function/test_expiration_reminders.py | 7 ++-- .../expiration_reminder_stack/__init__.py | 3 +- .../smoke/expiration_reminder_load_test.py | 33 ++++++------------- 4 files changed, 18 insertions(+), 27 deletions(-) diff --git a/backend/compact-connect/lambdas/python/search/tests/__init__.py b/backend/compact-connect/lambdas/python/search/tests/__init__.py index c481991d8..96c10c577 100644 --- a/backend/compact-connect/lambdas/python/search/tests/__init__.py +++ b/backend/compact-connect/lambdas/python/search/tests/__init__.py @@ -105,4 +105,4 @@ def setUpClass(cls): cc_common.config.config = cls.config cls.mock_context = MagicMock(name='MockLambdaContext', spec=LambdaContext) # Configure mock_context.get_remaining_time_in_millis to return large value by default - cls.mock_context.get_remaining_time_in_millis.return_value = 900_000 # 15 minutes in ms \ No newline at end of file + cls.mock_context.get_remaining_time_in_millis.return_value = 900_000 # 15 minutes in ms diff --git a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py index 32416e667..289ef8388 100644 --- a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py @@ -1,5 +1,6 @@ +# ruff: noqa ARG002 unused args import json -from datetime import date, datetime, timedelta +from datetime import date, datetime from unittest.mock import MagicMock, patch from uuid import uuid4 @@ -276,7 +277,9 @@ def test_handler_skips_when_already_sent(self, mock_iter, mock_email_service_cli @patch('handlers.expiration_reminders.ExpirationReminderTracker') @patch('cc_common.config._Config.email_service_client') @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') - def test_handler_logs_error_for_provider_without_email(self, mock_iter, mock_email_service_client, mock_tracker_class): + def test_handler_logs_error_for_provider_without_email( + self, mock_iter, mock_email_service_client, mock_tracker_class + ): """Provider without email address is skipped and logged as noEmail.""" mock_email_client = mock_email_service_client diff --git a/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py b/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py index 80b5d97f2..8a1a94820 100644 --- a/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py +++ b/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py @@ -121,7 +121,8 @@ def __init__( Rule( self, f'ExpirationReminder{reminder_config["suffix"]}Rule{compact.upper()}', - description=f'Daily rule to send {reminder_config["days_before"]}-day expiration reminders for {compact}', + description=f'Daily rule to send {reminder_config["days_before"]}-day expiration ' + f'reminders for {compact}', schedule=Schedule.cron(week_day='*', hour='4', minute='0', month='*', year='*'), targets=[ LambdaFunction( diff --git a/backend/compact-connect/tests/smoke/expiration_reminder_load_test.py b/backend/compact-connect/tests/smoke/expiration_reminder_load_test.py index 5bffebea4..3c10e1370 100755 --- a/backend/compact-connect/tests/smoke/expiration_reminder_load_test.py +++ b/backend/compact-connect/tests/smoke/expiration_reminder_load_test.py @@ -68,7 +68,7 @@ dynamodb_table = config.provider_user_dynamodb_table # Test configuration: counts of matching vs non-matching providers for the 30-day expiration run -MATCHING_PROVIDERS = 20_000 # Providers with one privilege expiring on target date (receive email) +MATCHING_PROVIDERS = 20_000 # Providers with one privilege expiring on target date (receive email) NON_MATCHING_PROVIDERS = 10_000 # Providers with neither privilege on target date (no email) COMPACT = COMPACTS[0] # Use first compact JURISDICTION = JURISDICTIONS[0] # Use first jurisdiction @@ -77,9 +77,7 @@ if COMPACT in LICENSE_TYPES and LICENSE_TYPES[COMPACT]: LICENSE_TYPE = LICENSE_TYPES[COMPACT][0]['name'] LICENSE_TYPE_2 = ( - LICENSE_TYPES[COMPACT][1]['name'] - if len(LICENSE_TYPES[COMPACT]) > 1 - else LICENSE_TYPES[COMPACT][0]['name'] + LICENSE_TYPES[COMPACT][1]['name'] if len(LICENSE_TYPES[COMPACT]) > 1 else LICENSE_TYPES[COMPACT][0]['name'] ) else: raise SmokeTestFailureException(f'No license types found for compact {COMPACT}') @@ -116,11 +114,7 @@ def _do_batch_write(self): # DynamoDB batch_write_item requires a dict keyed by table name table_name = self._table.name response = self._table.meta.client.batch_write_item( - RequestItems={ - table_name: [ - {'PutRequest': {'Item': item}} for item in items_to_write - ] - } + RequestItems={table_name: [{'PutRequest': {'Item': item}} for item in items_to_write]} ) # Check for unprocessed items (shouldn't happen with proper batch sizing, but handle it) @@ -188,9 +182,7 @@ def create_provider_records( ): abbr = get_license_type_abbreviation(_license_type) if not abbr: - raise SmokeTestFailureException( - f'Could not find abbreviation for license type: {_license_type}' - ) + raise SmokeTestFailureException(f'Could not find abbreviation for license type: {_license_type}') now = datetime.now(tz=UTC) given_name = f'TestProvider{provider_id[:8]}' @@ -341,9 +333,7 @@ def find_lambda_function_name(partial_name: str) -> str: logger.info(f'Found Lambda function: {function_name}') return function_name - raise SmokeTestFailureException( - f'Lambda function containing "{partial_name}" not found. ' - ) + raise SmokeTestFailureException(f'Lambda function containing "{partial_name}" not found. ') except ClientError as e: raise SmokeTestFailureException(f'Failed to list Lambda functions: {str(e)}') from e @@ -384,9 +374,7 @@ def invoke_expiration_reminder_lambda(days_before: int, compact: str = 'aslp'): logger.info('Found log group for Lambda', log_group=log_group_name, function_name=lambda_name) except ClientError as e: - raise SmokeTestFailureException( - f'Failed to get Lambda function details: {str(e)}' - ) from e + raise SmokeTestFailureException(f'Failed to get Lambda function details: {str(e)}') from e event = {'daysBefore': days_before, 'compact': compact} @@ -402,14 +390,14 @@ def invoke_expiration_reminder_lambda(days_before: int, compact: str = 'aslp'): if response.get('FunctionError'): error_payload = json.loads(response['Payload'].read()) raise SmokeTestFailureException( - f'expiration_reminder Lambda invocation failed: {response.get("FunctionError")}, ' - f'error: {error_payload}' + f'expiration_reminder Lambda invocation failed: {response.get("FunctionError")}, error: {error_payload}' ) logger.info('Lambda invocation accepted, polling CloudWatch Logs for completion...', log_group=log_group_name) # Poll CloudWatch Logs for the completion message - # The Lambda logs "Completed processing for compact" (or "Completed processing expiration reminders") with metrics + # The Lambda logs "Completed processing for compact" + # (or "Completed processing expiration reminders") with metrics max_wait_time = 960 # 16 minutes (Lambda timeout is 15 minutes) check_interval = 10 # Check every 10 seconds start_time = time.time() @@ -476,8 +464,7 @@ def invoke_expiration_reminder_lambda(days_before: int, compact: str = 'aslp'): logger.info(f'Still waiting for Lambda completion... ({int(elapsed)}s elapsed)') raise SmokeTestFailureException( - f'Lambda did not complete within {max_wait_time}s timeout. ' - 'Check CloudWatch Logs for details.' + f'Lambda did not complete within {max_wait_time}s timeout. Check CloudWatch Logs for details.' ) except ClientError as e: From c931cb55fe39d7116c6657a57dacf75e17cdf69f Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 4 Feb 2026 16:04:48 -0600 Subject: [PATCH 18/52] Update email wording according to management feedback --- .../lambdas/nodejs/lib/email/email-notification-service.ts | 4 ++-- .../lambdas/nodejs/tests/email-notification-service.test.ts | 2 +- .../nodejs/tests/lib/email/email-notification-service.test.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts b/backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts index 5de8de290..9420a8e5c 100644 --- a/backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts +++ b/backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts @@ -703,7 +703,7 @@ export class EmailNotificationService extends BaseEmailService { // "This message is to inform you..." text this.insertBody( emailContent, - 'This message is to inform you that one or more of your privileges will expire on:', + 'Your privilege(s) to practice expires on:', 'center', false, { 'padding': { 'top': 24, 'bottom': 0, 'right': 32, 'left': 32 }} @@ -719,7 +719,7 @@ export class EmailNotificationService extends BaseEmailService { // Explanation text this.insertBody( emailContent, - 'When a privilege is expired, you can no longer use it to practice. In order to renew your privileges, you must first renew your home state license. A full list of the privileges and their expiration dates is below:', + 'Upon expiration, you may not use your Compact privilege to provide patient/client services. You must renew your home state license in order to renew your Compact privilege to practice.', 'center', false, { 'padding': { 'top': 24, 'bottom': 24, 'right': 32, 'left': 32 }} 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 fa0f30683..ed93d9a8a 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 @@ -673,7 +673,7 @@ describe('EmailNotificationServiceLambda', () => { const htmlData = input.Content?.Simple?.Body?.Html?.Data ?? ''; expect(htmlData).toContain('Hi Mary,'); - expect(htmlData).toContain('one or more of your privileges will expire on'); + expect(htmlData).toContain('Your privilege(s) to practice expires on'); expect(htmlData).toContain('02/16/2026'); expect(htmlData).toContain('Ohio, audiologist'); expect(htmlData).toContain('#AUD-OH-001'); 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 9c13af673..2aeeddbfe 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 @@ -1120,7 +1120,7 @@ describe('EmailNotificationService', () => { expect(htmlContent).toBeDefined(); expect(htmlContent).toContain('Hi John,'); - expect(htmlContent).toContain('one or more of your privileges will expire on'); + expect(htmlContent).toContain('Your privilege(s) to practice expires on'); expect(htmlContent).toContain('02/16/2026'); expect(htmlContent).toContain('Ohio, audiologist'); expect(htmlContent).toContain('#AUD-OH-001'); @@ -1184,7 +1184,7 @@ describe('EmailNotificationService', () => { expect(htmlContent).toBeDefined(); expect(htmlContent).toContain('Hi Jane,'); - expect(htmlContent).toContain('one or more of your privileges will expire on'); + expect(htmlContent).toContain('Your privilege(s) to practice expires on'); expect(htmlContent).toContain('03/01/2026'); expect(htmlContent).toContain('Nebraska, speech-language pathologist'); expect(htmlContent).toContain('#SLP-NE-123'); From 8f06e94c09d16068fb911443d1e5de8944e8df84 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 4 Feb 2026 16:37:11 -0600 Subject: [PATCH 19/52] Update compact connect requirements to latest --- .github/workflows/check-compact-connect-ui-app.yml | 2 +- .github/workflows/check-compact-connect.yml | 6 ++---- .github/workflows/check-cosmetology-app.yml | 6 ++---- .../python/cognito-backup/requirements-dev.txt | 6 +++--- .../lambdas/python/cognito-backup/requirements.txt | 4 ++-- .../lambdas/python/common/requirements-dev.txt | 14 +++++++------- .../lambdas/python/common/requirements.txt | 6 +++--- .../compact-configuration/requirements-dev.txt | 6 +++--- .../python/custom-resources/requirements-dev.txt | 6 +++--- .../python/data-events/requirements-dev.txt | 6 +++--- .../python/disaster-recovery/requirements-dev.txt | 6 +++--- .../python/provider-data-v1/requirements-dev.txt | 6 +++--- .../lambdas/python/search/requirements-dev.txt | 6 +++--- .../lambdas/python/search/requirements.txt | 2 +- .../staff-user-pre-token/requirements-dev.txt | 6 +++--- .../python/staff-users/requirements-dev.txt | 6 +++--- backend/compact-connect/requirements-dev.txt | 6 +++--- backend/compact-connect/requirements.txt | 4 ++-- 18 files changed, 50 insertions(+), 54 deletions(-) diff --git a/.github/workflows/check-compact-connect-ui-app.yml b/.github/workflows/check-compact-connect-ui-app.yml index f544e5907..ae5fa68b9 100644 --- a/.github/workflows/check-compact-connect-ui-app.yml +++ b/.github/workflows/check-compact-connect-ui-app.yml @@ -103,7 +103,7 @@ jobs: run: "pip install -r backend/compact-connect-ui-app/requirements-dev.txt" - name: Install all Python dependencies - run: "cd backend/compact-connect; pip install -U 'pip<25.3'; bin/sync_deps.sh" + run: "cd backend/compact-connect; bin/sync_deps.sh" - name: Test backend run: "cd backend/compact-connect-ui-app; bin/run_tests.sh -l all -no" diff --git a/.github/workflows/check-compact-connect.yml b/.github/workflows/check-compact-connect.yml index ad776b5ae..272e55ddb 100644 --- a/.github/workflows/check-compact-connect.yml +++ b/.github/workflows/check-compact-connect.yml @@ -101,11 +101,9 @@ jobs: - name: Install dev dependencies run: "pip install -r backend/compact-connect/requirements-dev.txt" - # Note we are currently pinning the pip version to deal with compatibility issues released with pip 25.3 - # see https://stackoverflow.com/a/79802727 If this issues is addressed in a later version, we can remove the - # extra pip install command so we stop pinning the pip version + - name: Install all Python dependencies - run: "cd backend/compact-connect; pip install -U 'pip<25.3'; bin/sync_deps.sh" + run: "cd backend/compact-connect; bin/sync_deps.sh" - name: Test backend run: "cd backend/compact-connect; bin/run_tests.sh -l all -no" diff --git a/.github/workflows/check-cosmetology-app.yml b/.github/workflows/check-cosmetology-app.yml index cfb3162a8..97360e972 100644 --- a/.github/workflows/check-cosmetology-app.yml +++ b/.github/workflows/check-cosmetology-app.yml @@ -100,11 +100,9 @@ jobs: - name: Install dev dependencies run: "pip install -r backend/cosmetology-app/requirements-dev.txt" - # Note we are currently pinning the pip version to deal with compatibility issues released with pip 25.3 - # see https://stackoverflow.com/a/79802727 If this issues is addressed in a later version, we can remove the - # extra pip install command so we stop pinning the pip version + - name: Install all Python dependencies - run: "cd backend/cosmetology-app; pip install -U 'pip<25.3'; bin/sync_deps.sh" + run: "cd backend/cosmetology-app; bin/sync_deps.sh" - name: Test backend run: "cd backend/cosmetology-app; bin/run_tests.sh -l all -no" diff --git a/backend/compact-connect/lambdas/python/cognito-backup/requirements-dev.txt b/backend/compact-connect/lambdas/python/cognito-backup/requirements-dev.txt index 40bb11577..8690117bf 100644 --- a/backend/compact-connect/lambdas/python/cognito-backup/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/cognito-backup/requirements-dev.txt @@ -6,11 +6,11 @@ # aws-lambda-powertools==3.24.0 # via -r lambdas/python/cognito-backup/requirements-dev.in -boto3==1.42.35 +boto3==1.42.42 # via # -r lambdas/python/cognito-backup/requirements-dev.in # moto -botocore==1.42.35 +botocore==1.42.42 # via # -r lambdas/python/cognito-backup/requirements-dev.in # boto3 @@ -22,7 +22,7 @@ cffi==2.0.0 # via cryptography charset-normalizer==3.4.4 # via requests -cryptography==46.0.3 +cryptography==46.0.4 # via # joserfc # moto diff --git a/backend/compact-connect/lambdas/python/cognito-backup/requirements.txt b/backend/compact-connect/lambdas/python/cognito-backup/requirements.txt index e151217a8..7312dff97 100644 --- a/backend/compact-connect/lambdas/python/cognito-backup/requirements.txt +++ b/backend/compact-connect/lambdas/python/cognito-backup/requirements.txt @@ -6,9 +6,9 @@ # aws-lambda-powertools==3.24.0 # via -r lambdas/python/cognito-backup/requirements.in -boto3==1.42.35 +boto3==1.42.42 # via -r lambdas/python/cognito-backup/requirements.in -botocore==1.42.35 +botocore==1.42.42 # via # -r lambdas/python/cognito-backup/requirements.in # boto3 diff --git a/backend/compact-connect/lambdas/python/common/requirements-dev.txt b/backend/compact-connect/lambdas/python/common/requirements-dev.txt index 60c2779b4..e7d219bcf 100644 --- a/backend/compact-connect/lambdas/python/common/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/common/requirements-dev.txt @@ -18,21 +18,21 @@ aws-sam-translator==1.103.0 # moto aws-xray-sdk==2.15.0 # via moto -boto3==1.42.35 +boto3==1.42.42 # via # aws-sam-translator # moto -boto3-stubs[full]==1.42.35 +boto3-stubs[full]==1.42.42 # via -r lambdas/python/common/requirements-dev.in -boto3-stubs-full==1.42.35 +boto3-stubs-full==1.42.41 # via boto3-stubs -botocore==1.42.35 +botocore==1.42.42 # via # aws-xray-sdk # boto3 # moto # s3transfer -botocore-stubs==1.42.35 +botocore-stubs==1.42.41 # via boto3-stubs certifi==2026.1.4 # via requests @@ -42,7 +42,7 @@ cfn-lint==1.41.0 # via moto charset-normalizer==3.4.4 # via requests -cryptography==46.0.3 +cryptography==46.0.4 # via # -r lambdas/python/common/requirements-dev.in # joserfc @@ -177,7 +177,7 @@ urllib3==2.6.3 # responses werkzeug==3.1.5 # via moto -wrapt==2.0.1 +wrapt==2.1.1 # via aws-xray-sdk xmltodict==1.0.2 # via moto diff --git a/backend/compact-connect/lambdas/python/common/requirements.txt b/backend/compact-connect/lambdas/python/common/requirements.txt index 4709620df..6ff410463 100644 --- a/backend/compact-connect/lambdas/python/common/requirements.txt +++ b/backend/compact-connect/lambdas/python/common/requirements.txt @@ -10,9 +10,9 @@ argon2-cffi-bindings==25.1.0 # via argon2-cffi aws-lambda-powertools==3.24.0 # via -r lambdas/python/common/requirements.in -boto3==1.42.35 +boto3==1.42.42 # via -r lambdas/python/common/requirements.in -botocore==1.42.35 +botocore==1.42.42 # via # boto3 # s3transfer @@ -24,7 +24,7 @@ cffi==2.0.0 # cryptography charset-normalizer==3.4.4 # via requests -cryptography==46.0.3 +cryptography==46.0.4 # via -r lambdas/python/common/requirements.in idna==3.11 # via requests diff --git a/backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt b/backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt index ddcddbcb2..ce862d86b 100644 --- a/backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt @@ -4,9 +4,9 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/compact-configuration/requirements-dev.in # -boto3==1.42.35 +boto3==1.42.42 # via moto -botocore==1.42.35 +botocore==1.42.42 # via # boto3 # moto @@ -17,7 +17,7 @@ cffi==2.0.0 # via cryptography charset-normalizer==3.4.4 # via requests -cryptography==46.0.3 +cryptography==46.0.4 # via moto docker==7.1.0 # via moto diff --git a/backend/compact-connect/lambdas/python/custom-resources/requirements-dev.txt b/backend/compact-connect/lambdas/python/custom-resources/requirements-dev.txt index 07d138a17..52f5198ea 100644 --- a/backend/compact-connect/lambdas/python/custom-resources/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/custom-resources/requirements-dev.txt @@ -4,9 +4,9 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/custom-resources/requirements-dev.in # -boto3==1.42.35 +boto3==1.42.42 # via moto -botocore==1.42.35 +botocore==1.42.42 # via # boto3 # moto @@ -17,7 +17,7 @@ cffi==2.0.0 # via cryptography charset-normalizer==3.4.4 # via requests -cryptography==46.0.3 +cryptography==46.0.4 # via moto docker==7.1.0 # via moto diff --git a/backend/compact-connect/lambdas/python/data-events/requirements-dev.txt b/backend/compact-connect/lambdas/python/data-events/requirements-dev.txt index 1c867ffcc..072d67ec2 100644 --- a/backend/compact-connect/lambdas/python/data-events/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/data-events/requirements-dev.txt @@ -4,9 +4,9 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/data-events/requirements-dev.in # -boto3==1.42.35 +boto3==1.42.42 # via moto -botocore==1.42.35 +botocore==1.42.42 # via # boto3 # moto @@ -17,7 +17,7 @@ cffi==2.0.0 # via cryptography charset-normalizer==3.4.4 # via requests -cryptography==46.0.3 +cryptography==46.0.4 # via moto docker==7.1.0 # via moto diff --git a/backend/compact-connect/lambdas/python/disaster-recovery/requirements-dev.txt b/backend/compact-connect/lambdas/python/disaster-recovery/requirements-dev.txt index 174b7006b..553a070a6 100644 --- a/backend/compact-connect/lambdas/python/disaster-recovery/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/disaster-recovery/requirements-dev.txt @@ -4,9 +4,9 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/disaster-recovery/requirements-dev.in # -boto3==1.42.35 +boto3==1.42.42 # via moto -botocore==1.42.35 +botocore==1.42.42 # via # boto3 # moto @@ -17,7 +17,7 @@ cffi==2.0.0 # via cryptography charset-normalizer==3.4.4 # via requests -cryptography==46.0.3 +cryptography==46.0.4 # via moto docker==7.1.0 # via moto diff --git a/backend/compact-connect/lambdas/python/provider-data-v1/requirements-dev.txt b/backend/compact-connect/lambdas/python/provider-data-v1/requirements-dev.txt index 8c6e6f9fa..3c2661b1b 100644 --- a/backend/compact-connect/lambdas/python/provider-data-v1/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/provider-data-v1/requirements-dev.txt @@ -4,9 +4,9 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/provider-data-v1/requirements-dev.in # -boto3==1.42.35 +boto3==1.42.42 # via moto -botocore==1.42.35 +botocore==1.42.42 # via # boto3 # moto @@ -17,7 +17,7 @@ cffi==2.0.0 # via cryptography charset-normalizer==3.4.4 # via requests -cryptography==46.0.3 +cryptography==46.0.4 # via moto docker==7.1.0 # via moto diff --git a/backend/compact-connect/lambdas/python/search/requirements-dev.txt b/backend/compact-connect/lambdas/python/search/requirements-dev.txt index e70b4bd12..a8ef8fb85 100644 --- a/backend/compact-connect/lambdas/python/search/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/search/requirements-dev.txt @@ -4,9 +4,9 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/search/requirements-dev.in # -boto3==1.42.35 +boto3==1.42.42 # via moto -botocore==1.42.35 +botocore==1.42.42 # via # boto3 # moto @@ -17,7 +17,7 @@ cffi==2.0.0 # via cryptography charset-normalizer==3.4.4 # via requests -cryptography==46.0.3 +cryptography==46.0.4 # via moto docker==7.1.0 # via moto diff --git a/backend/compact-connect/lambdas/python/search/requirements.txt b/backend/compact-connect/lambdas/python/search/requirements.txt index 13e8f3a95..c16489c7c 100644 --- a/backend/compact-connect/lambdas/python/search/requirements.txt +++ b/backend/compact-connect/lambdas/python/search/requirements.txt @@ -20,7 +20,7 @@ opensearch-protobufs==0.19.0 # via opensearch-py opensearch-py==3.1.0 # via -r lambdas/python/search/requirements.in -protobuf==6.33.4 +protobuf==6.33.5 # via opensearch-protobufs python-dateutil==2.9.0.post0 # via opensearch-py diff --git a/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements-dev.txt b/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements-dev.txt index a24a50722..65523ffcd 100644 --- a/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements-dev.txt @@ -4,9 +4,9 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/staff-user-pre-token/requirements-dev.in # -boto3==1.42.35 +boto3==1.42.42 # via moto -botocore==1.42.35 +botocore==1.42.42 # via # boto3 # moto @@ -17,7 +17,7 @@ cffi==2.0.0 # via cryptography charset-normalizer==3.4.4 # via requests -cryptography==46.0.3 +cryptography==46.0.4 # via moto docker==7.1.0 # via moto diff --git a/backend/compact-connect/lambdas/python/staff-users/requirements-dev.txt b/backend/compact-connect/lambdas/python/staff-users/requirements-dev.txt index cb2cc8eb8..0c39fa9b6 100644 --- a/backend/compact-connect/lambdas/python/staff-users/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/staff-users/requirements-dev.txt @@ -4,9 +4,9 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/staff-users/requirements-dev.in # -boto3==1.42.35 +boto3==1.42.42 # via moto -botocore==1.42.35 +botocore==1.42.42 # via # boto3 # moto @@ -17,7 +17,7 @@ cffi==2.0.0 # via cryptography charset-normalizer==3.4.4 # via requests -cryptography==46.0.3 +cryptography==46.0.4 # via # joserfc # moto diff --git a/backend/compact-connect/requirements-dev.txt b/backend/compact-connect/requirements-dev.txt index 78f375389..0cd970211 100644 --- a/backend/compact-connect/requirements-dev.txt +++ b/backend/compact-connect/requirements-dev.txt @@ -18,7 +18,7 @@ charset-normalizer==3.4.4 # via requests click==8.3.1 # via pip-tools -coverage[toml]==7.13.2 +coverage[toml]==7.13.3 # via # -r requirements-dev.in # pytest-cov @@ -87,9 +87,9 @@ requests==2.32.5 # via # cachecontrol # pip-audit -rich==14.3.1 +rich==14.3.2 # via pip-audit -ruff==0.14.14 +ruff==0.15.0 # via -r requirements-dev.in sortedcontainers==2.4.0 # via cyclonedx-python-lib diff --git a/backend/compact-connect/requirements.txt b/backend/compact-connect/requirements.txt index a775c2cfc..c73efbc3c 100644 --- a/backend/compact-connect/requirements.txt +++ b/backend/compact-connect/requirements.txt @@ -12,11 +12,11 @@ aws-cdk-asset-awscli-v1==2.2.263 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib -aws-cdk-aws-lambda-python-alpha==2.236.0a0 +aws-cdk-aws-lambda-python-alpha==2.237.1a0 # via -r requirements.in aws-cdk-cloud-assembly-schema==48.20.0 # via aws-cdk-lib -aws-cdk-lib==2.236.0 +aws-cdk-lib==2.237.1 # via # -r requirements.in # aws-cdk-aws-lambda-python-alpha From 17f6c8f0ae7694b86ac0d07fc566e3669d98a10b Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 4 Feb 2026 16:38:37 -0600 Subject: [PATCH 20/52] Reduce size of load test --- ...der_load_test.py => expiration_reminder_load_tests.py} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename backend/compact-connect/tests/smoke/{expiration_reminder_load_test.py => expiration_reminder_load_tests.py} (98%) diff --git a/backend/compact-connect/tests/smoke/expiration_reminder_load_test.py b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py similarity index 98% rename from backend/compact-connect/tests/smoke/expiration_reminder_load_test.py rename to backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py index 3c10e1370..767d1a087 100755 --- a/backend/compact-connect/tests/smoke/expiration_reminder_load_test.py +++ b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py @@ -7,7 +7,7 @@ performance and capacity (load test) or a small smoke test. Usage: - # Full load test (20k matching + 10k non-matching providers, two privileges per provider) + # Full load test (10k matching + 5k non-matching providers, two privileges per provider) python expiration_reminder_load_test.py python expiration_reminder_load_test.py --skip-data-load @@ -17,7 +17,7 @@ The script will: - Compute matching and non-matching provider counts: with --providers N (smoke), use ~2/3 and ~1/3 of N; - without --providers (load), use MATCHING_PROVIDERS (20k) and NON_MATCHING_PROVIDERS (10k). + without --providers (load), use MATCHING_PROVIDERS (10k) and NON_MATCHING_PROVIDERS (5k). - Create that many providers in a single batch (two privileges each; matching = one privilege on target date, non-matching = neither on target date), unless --skip-data-load. - Wait for DynamoDB stream events to index providers into OpenSearch (unless --skip-data-load). @@ -68,8 +68,8 @@ dynamodb_table = config.provider_user_dynamodb_table # Test configuration: counts of matching vs non-matching providers for the 30-day expiration run -MATCHING_PROVIDERS = 20_000 # Providers with one privilege expiring on target date (receive email) -NON_MATCHING_PROVIDERS = 10_000 # Providers with neither privilege on target date (no email) +MATCHING_PROVIDERS = 10_000 # Providers with one privilege expiring on target date (receive email) +NON_MATCHING_PROVIDERS = 5_000 # Providers with neither privilege on target date (no email) COMPACT = COMPACTS[0] # Use first compact JURISDICTION = JURISDICTIONS[0] # Use first jurisdiction # Second jurisdiction and license type for two-privilege-per-provider (smoke mode) From a516125d71fc4601d62253cdb1b561ab257888da Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 4 Feb 2026 17:04:44 -0600 Subject: [PATCH 21/52] upgrade pip version in GH actions --- .github/workflows/check-compact-connect-ui-app.yml | 8 ++++++++ .github/workflows/check-compact-connect.yml | 12 ++++++++++++ .github/workflows/check-cosmetology-app.yml | 8 ++++++++ 3 files changed, 28 insertions(+) diff --git a/.github/workflows/check-compact-connect-ui-app.yml b/.github/workflows/check-compact-connect-ui-app.yml index ae5fa68b9..a85349961 100644 --- a/.github/workflows/check-compact-connect-ui-app.yml +++ b/.github/workflows/check-compact-connect-ui-app.yml @@ -24,6 +24,10 @@ jobs: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v5 + - name: Upgrade pip + # Runner image ships pip 25.3; upgrade to 26.0+ so pip-audit passes (CVE-2026-1703 fixed in 26.0) + run: pip install --upgrade 'pip>=26.0' + - name: Install dev dependencies run: "pip install -r backend/compact-connect-ui-app/requirements-dev.txt" @@ -82,6 +86,10 @@ jobs: with: python-version: '3.14' + - name: Upgrade pip + # Runner image ships pip 25.3; upgrade to 26.0+ so pip-audit passes (CVE-2026-1703 fixed in 26.0) + run: pip install --upgrade 'pip>=26.0' + # Setup Node - name: Setup Node uses: actions/setup-node@v6 diff --git a/.github/workflows/check-compact-connect.yml b/.github/workflows/check-compact-connect.yml index 272e55ddb..52907c7f4 100644 --- a/.github/workflows/check-compact-connect.yml +++ b/.github/workflows/check-compact-connect.yml @@ -24,6 +24,10 @@ jobs: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v5 + - name: Upgrade pip + # Runner image ships pip 25.3; upgrade to 26.0+ so pip-audit passes (CVE-2026-1703 fixed in 26.0) + run: pip install --upgrade 'pip>=26.0' + - name: Install dev dependencies run: "pip install -r backend/compact-connect/requirements-dev.txt" @@ -82,6 +86,10 @@ jobs: with: python-version: '3.14' + - name: Upgrade pip + # Runner image ships pip 25.3; upgrade to 26.0+ so pip-audit passes (CVE-2026-1703 fixed in 26.0) + run: pip install --upgrade 'pip>=26.0' + # Setup Node - name: Setup Node uses: actions/setup-node@v6 @@ -120,6 +128,10 @@ jobs: with: python-version: '3.12' + - name: Upgrade pip + # Runner image ships pip 25.3; upgrade to 26.0+ so pip-audit passes (CVE-2026-1703 fixed in 26.0) + run: pip install --upgrade 'pip>=26.0' + - name: Install dependencies run: "cd backend/compact-connect/lambdas/python/purchases; pip install -r requirements.txt" diff --git a/.github/workflows/check-cosmetology-app.yml b/.github/workflows/check-cosmetology-app.yml index 97360e972..8ba1d8d6c 100644 --- a/.github/workflows/check-cosmetology-app.yml +++ b/.github/workflows/check-cosmetology-app.yml @@ -24,6 +24,10 @@ jobs: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v5 + - name: Upgrade pip + # Runner image ships pip 25.3; upgrade to 26.0+ so pip-audit passes (CVE-2026-1703 fixed in 26.0) + run: pip install --upgrade 'pip>=26.0' + - name: Install dev dependencies run: "pip install -r backend/cosmetology-app/requirements-dev.txt" @@ -81,6 +85,10 @@ jobs: with: python-version: '3.14' + - name: Upgrade pip + # Runner image ships pip 25.3; upgrade to 26.0+ so pip-audit passes (CVE-2026-1703 fixed in 26.0) + run: pip install --upgrade 'pip>=26.0' + # Setup Node - name: Setup Node uses: actions/setup-node@v6 From 6d210bcacd54340fac72f47d6c2941c28ef440a2 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 5 Feb 2026 07:47:10 -0600 Subject: [PATCH 22/52] remove ref to scope in doc --- backend/compact-connect/app_clients/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/compact-connect/app_clients/README.md b/backend/compact-connect/app_clients/README.md index d1e04dd9f..b306f89cf 100644 --- a/backend/compact-connect/app_clients/README.md +++ b/backend/compact-connect/app_clients/README.md @@ -52,7 +52,6 @@ The following scopes are available at the jurisdiction level: {jurisdiction}/{compact}.admin {jurisdiction}/{compact}.write {jurisdiction}/{compact}.readPrivate -{jurisdiction}/{compact}.readSSN ``` Currently, the most common scope needed by app clients is `{jurisdiction}/{compact}.write`, which allows uploading From 62c05bff0f4e3b9acd0ea961a38928de123c46e8 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 5 Feb 2026 09:48:21 -0600 Subject: [PATCH 23/52] Add search with retry This will make background processes more robust to temp connection timeouts --- .../search/handlers/expiration_reminders.py | 2 +- .../python/search/opensearch_client.py | 63 ++++++ .../function/test_expiration_reminders.py | 16 +- .../tests/unit/test_opensearch_client.py | 181 ++++++++++++++++++ 4 files changed, 253 insertions(+), 9 deletions(-) diff --git a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py index 2ccd5e694..61b194799 100644 --- a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py @@ -409,7 +409,7 @@ def iterate_privileges_by_expiration_date( if search_after is not None: search_body['search_after'] = search_after - response = opensearch_client.search(index_name=index_name, body=search_body) + response = opensearch_client.search_with_retry(index_name=index_name, body=search_body) logger.info('Received response from OpenSearch', hits=response['hits']['total']) hits = response['hits']['hits'] if not hits: diff --git a/backend/compact-connect/lambdas/python/search/opensearch_client.py b/backend/compact-connect/lambdas/python/search/opensearch_client.py index 474b3ff6c..cff213b77 100644 --- a/backend/compact-connect/lambdas/python/search/opensearch_client.py +++ b/backend/compact-connect/lambdas/python/search/opensearch_client.py @@ -178,6 +178,9 @@ def search(self, index_name: str, body: dict) -> dict: """ Execute a search query against the specified index. + This method is intended to be used for user-facing search requests that need a response quickly (UI/API). + For background/batch operations, use search_with_retry instead. + :param index_name: The name of the index to search :param body: The OpenSearch query body :return: The search response from OpenSearch @@ -210,6 +213,66 @@ def search(self, index_name: str, body: dict) -> dict: # Re-raise non-400 RequestErrors raise + def search_with_retry(self, index_name: str, body: dict) -> dict: + """ + Execute a search query with retry logic and exponential backoff. + + Use this method for background/batch operations where retrying on transient + failures is preferred over immediately returning an error to a user. + + :param index_name: The name of the index to search + :param body: The OpenSearch query body + :return: The search response from OpenSearch + :raises CCInternalException: If all retry attempts fail due to connection issues + :raises CCInvalidRequestException: If the query is invalid (400 error) - not retried + """ + last_exception = None + backoff_seconds = INITIAL_BACKOFF_SECONDS + + for attempt in range(1, MAX_RETRY_ATTEMPTS + 1): + try: + return self._client.search(index=index_name, body=body) + except RequestError as e: + # Don't retry invalid queries (400 errors) - they will fail again + # Note: RequestError is a subclass of TransportError, so must be caught first + if e.status_code == 400: + error_message = self._extract_opensearch_error_reason(e) + logger.warning( + 'OpenSearch search request failed with invalid query', + index_name=index_name, + status_code=e.status_code, + error_message=error_message, + ) + raise CCInvalidRequestException(f'Invalid search query: {error_message}') from e + # Re-raise non-400 RequestErrors + raise + except (ConnectionTimeout, TransportError) as e: + last_exception = e + if attempt < MAX_RETRY_ATTEMPTS: + logger.warning( + 'Search request failed, retrying with backoff', + attempt=attempt, + max_attempts=MAX_RETRY_ATTEMPTS, + backoff_seconds=backoff_seconds, + index_name=index_name, + error=str(e), + ) + time.sleep(backoff_seconds) + # Exponential backoff with cap + backoff_seconds = min(backoff_seconds * 2, MAX_BACKOFF_SECONDS) + else: + logger.error( + 'Search request failed after max retry attempts', + attempts=MAX_RETRY_ATTEMPTS, + index_name=index_name, + error=str(e), + ) + + # All retry attempts failed + raise CCInternalException( + f'Search request to {index_name} failed after {MAX_RETRY_ATTEMPTS} attempts. Last error: {last_exception}' + ) + @staticmethod def _extract_opensearch_error_reason(e: RequestError) -> str: """ diff --git a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py index 289ef8388..b0b94c93b 100644 --- a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py @@ -18,11 +18,11 @@ class TestExpirationRemindersOpenSearch(TstLambdas): def test_iterate_privileges_by_expiration_date_paginates_with_search_after_and_yields_in_order( self, mock_client, mock_schema_class ): - mock_client.search = MagicMock() + mock_client.search_with_retry = MagicMock() mock_schema_class.return_value.load.side_effect = lambda doc: doc # Page 1: p1, p2 - mock_client.search.side_effect = [ + mock_client.search_with_retry.side_effect = [ { 'hits': { 'total': {'value': 3, 'relation': 'eq'}, @@ -66,8 +66,8 @@ def test_iterate_privileges_by_expiration_date_paginates_with_search_after_and_y self.assertEqual([r.search_after for r in results], [['p1'], ['p2'], ['p3']]) # Verify pagination: second call includes search_after from last hit in page 1 - first_call_kwargs = mock_client.search.call_args_list[0].kwargs - second_call_kwargs = mock_client.search.call_args_list[1].kwargs + first_call_kwargs = mock_client.search_with_retry.call_args_list[0].kwargs + second_call_kwargs = mock_client.search_with_retry.call_args_list[1].kwargs self.assertEqual('compact_aslp_providers', first_call_kwargs['index_name']) self.assertNotIn('search_after', first_call_kwargs['body']) @@ -81,7 +81,7 @@ def test_iterate_privileges_by_expiration_date_resumes_with_initial_search_after self, mock_client, mock_schema_class ): """When initial_search_after is provided, first OpenSearch query uses it.""" - mock_client.search = MagicMock( + mock_client.search_with_retry = MagicMock( return_value={ 'hits': { 'total': {'value': 1, 'relation': 'eq'}, @@ -109,8 +109,8 @@ def test_iterate_privileges_by_expiration_date_resumes_with_initial_search_after self.assertEqual(1, len(results)) self.assertEqual('p2', results[0].provider_doc['providerId']) - mock_client.search.assert_called_once() - self.assertEqual(['p1'], mock_client.search.call_args.kwargs['body']['search_after']) + mock_client.search_with_retry.assert_called_once() + self.assertEqual(['p1'], mock_client.search_with_retry.call_args.kwargs['body']['search_after']) @patch('handlers.expiration_reminders.ProviderGeneralResponseSchema') @patch('handlers.expiration_reminders.opensearch_client') @@ -132,7 +132,7 @@ def test_iterate_privileges_by_expiration_date_returns_full_provider_document(se 'status': 'active', }, ] - mock_client.search = MagicMock( + mock_client.search_with_retry = MagicMock( return_value={ 'hits': { 'total': {'value': 1, 'relation': 'eq'}, diff --git a/backend/compact-connect/lambdas/python/search/tests/unit/test_opensearch_client.py b/backend/compact-connect/lambdas/python/search/tests/unit/test_opensearch_client.py index f817579b3..b30489848 100644 --- a/backend/compact-connect/lambdas/python/search/tests/unit/test_opensearch_client.py +++ b/backend/compact-connect/lambdas/python/search/tests/unit/test_opensearch_client.py @@ -537,3 +537,184 @@ def test_cluster_health_raises_after_max_retries(self, mock_sleep): # Verify health was called MAX_RETRY_ATTEMPTS times self.assertEqual(MAX_RETRY_ATTEMPTS, mock_internal_client.cluster.health.call_count) self.assertIn('cluster_health', str(context.exception)) + + +class TestOpenSearchClientSearchWithRetry(TestCase): + """Test suite for OpenSearchClient.search_with_retry method.""" + + def _create_client_with_mock(self): + """Create an OpenSearchClient with a mocked internal client.""" + with ( + patch('opensearch_client.boto3'), + patch('opensearch_client.config'), + patch('opensearch_client.OpenSearch') as mock_opensearch_class, + ): + mock_internal_client = MagicMock() + mock_opensearch_class.return_value = mock_internal_client + + from opensearch_client import OpenSearchClient + + client = OpenSearchClient() + return client, mock_internal_client + + def test_search_with_retry_calls_internal_client_with_expected_arguments(self): + """Test that search_with_retry calls the internal client's search method correctly.""" + client, mock_internal_client = self._create_client_with_mock() + + index_name = 'test_index' + query_body = {'query': {'match': {'givenName': 'John'}}} + expected_response = { + 'hits': { + 'total': {'value': 1}, + 'hits': [{'_source': {'givenName': 'John', 'familyName': 'Doe'}}], + }, + } + mock_internal_client.search.return_value = expected_response + + result = client.search_with_retry(index_name=index_name, body=query_body) + + mock_internal_client.search.assert_called_once_with(index=index_name, body=query_body) + self.assertEqual(expected_response, result) + + @patch('opensearch_client.time.sleep') + def test_search_with_retry_retries_on_connection_timeout_and_succeeds(self, mock_sleep): + """Test that search_with_retry retries on ConnectionTimeout and eventually succeeds.""" + from opensearch_client import INITIAL_BACKOFF_SECONDS + + client, mock_internal_client = self._create_client_with_mock() + + index_name = 'test_index' + query_body = {'query': {'match_all': {}}} + expected_response = {'hits': {'total': {'value': 0}, 'hits': []}} + + # First two calls fail with ConnectionTimeout, third succeeds + mock_internal_client.search.side_effect = [ + ConnectionTimeout('Connection timed out', 503, 'some error'), + ConnectionTimeout('Connection timed out', 503, 'some error'), + expected_response, + ] + + result = client.search_with_retry(index_name=index_name, body=query_body) + + # Verify search was called 3 times + self.assertEqual(3, mock_internal_client.search.call_count) + # Verify sleep was called with exponential backoff + self.assertEqual(2, mock_sleep.call_count) + mock_sleep.assert_any_call(INITIAL_BACKOFF_SECONDS) + mock_sleep.assert_any_call(INITIAL_BACKOFF_SECONDS * 2) + # Verify we got the successful response + self.assertEqual(expected_response, result) + + @patch('opensearch_client.time.sleep') + def test_search_with_retry_retries_on_transport_error_and_succeeds(self, mock_sleep): + """Test that search_with_retry retries on TransportError and eventually succeeds.""" + client, mock_internal_client = self._create_client_with_mock() + + index_name = 'test_index' + query_body = {'query': {'match_all': {}}} + expected_response = {'hits': {'total': {'value': 0}, 'hits': []}} + + # First call fails with TransportError, second succeeds + mock_internal_client.search.side_effect = [ + TransportError(503, 'ReadTimeout'), + expected_response, + ] + + result = client.search_with_retry(index_name=index_name, body=query_body) + + # Verify search was called 2 times + self.assertEqual(2, mock_internal_client.search.call_count) + # Verify sleep was called once + self.assertEqual(1, mock_sleep.call_count) + self.assertEqual(expected_response, result) + + @patch('opensearch_client.time.sleep') + def test_search_with_retry_raises_cc_internal_exception_after_max_retries(self, mock_sleep): + """Test that search_with_retry raises CCInternalException after all retry attempts fail.""" + from cc_common.exceptions import CCInternalException + from opensearch_client import MAX_RETRY_ATTEMPTS + + client, mock_internal_client = self._create_client_with_mock() + + index_name = 'test_index' + query_body = {'query': {'match_all': {}}} + + # All calls fail with ConnectionTimeout + mock_internal_client.search.side_effect = ConnectionTimeout('Connection timed out', 503, 'some error') + + with self.assertRaises(CCInternalException) as context: + client.search_with_retry(index_name=index_name, body=query_body) + + # Verify search was called MAX_RETRY_ATTEMPTS times + self.assertEqual(MAX_RETRY_ATTEMPTS, mock_internal_client.search.call_count) + # Verify sleep was called MAX_RETRY_ATTEMPTS - 1 times (no sleep after last failure) + self.assertEqual(MAX_RETRY_ATTEMPTS - 1, mock_sleep.call_count) + # Verify the exception message contains useful info + self.assertIn('Search request', str(context.exception)) + self.assertIn(index_name, str(context.exception)) + self.assertIn(str(MAX_RETRY_ATTEMPTS), str(context.exception)) + + def test_search_with_retry_raises_cc_invalid_request_exception_on_400_error_without_retrying(self): + """Test that search_with_retry raises CCInvalidRequestException on 400 error without retrying.""" + from cc_common.exceptions import CCInvalidRequestException + + client, mock_internal_client = self._create_client_with_mock() + + index_name = 'test_index' + query_body = {'query': {'invalid_query': {}}} + + error_reason = 'Unknown query type [invalid_query]' + error_info = { + 'error': { + 'root_cause': [{'type': 'parsing_exception', 'reason': error_reason}], + 'type': 'parsing_exception', + 'reason': error_reason, + }, + 'status': 400, + } + mock_internal_client.search.side_effect = RequestError(400, 'parsing_exception', error_info) + + with self.assertRaises(CCInvalidRequestException) as context: + client.search_with_retry(index_name=index_name, body=query_body) + + # Verify search was only called once (no retry on 400) + self.assertEqual(1, mock_internal_client.search.call_count) + # Verify the exception message extracts the reason + self.assertEqual(f'Invalid search query: {error_reason}', str(context.exception)) + + def test_search_with_retry_reraises_non_400_request_error(self): + """Test that search_with_retry re-raises RequestError for non-400 status codes.""" + client, mock_internal_client = self._create_client_with_mock() + + index_name = 'test_index' + query_body = {'query': {'match_all': {}}} + + # Simulate OpenSearch returning a 500 error + mock_internal_client.search.side_effect = RequestError(500, 'internal_error', 'Something went wrong') + + with self.assertRaises(RequestError) as context: + client.search_with_retry(index_name=index_name, body=query_body) + + self.assertEqual(500, context.exception.status_code) + + @patch('opensearch_client.time.sleep') + def test_search_with_retry_exponential_backoff_caps_at_max(self, mock_sleep): + """Test that exponential backoff is capped at MAX_BACKOFF_SECONDS.""" + from cc_common.exceptions import CCInternalException + from opensearch_client import MAX_BACKOFF_SECONDS + + client, mock_internal_client = self._create_client_with_mock() + + index_name = 'test_index' + query_body = {'query': {'match_all': {}}} + + # All calls fail + mock_internal_client.search.side_effect = ConnectionTimeout('Connection timed out', 503, 'some error') + + with self.assertRaises(CCInternalException): + client.search_with_retry(index_name=index_name, body=query_body) + + # Verify backoff values: 2, 4, 8, 16 (all should be <= MAX_BACKOFF_SECONDS) + sleep_calls = [call[0][0] for call in mock_sleep.call_args_list] + for sleep_value in sleep_calls: + self.assertLessEqual(sleep_value, MAX_BACKOFF_SECONDS) From e0a9b4599f05f470ce3f266b5a5e52e65f52015c Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 5 Feb 2026 17:12:18 -0600 Subject: [PATCH 24/52] update example in cosm script --- backend/cosmetology-app/bin/create_staff_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/cosmetology-app/bin/create_staff_user.py b/backend/cosmetology-app/bin/create_staff_user.py index dbbd2ef8e..3775beefd 100755 --- a/backend/cosmetology-app/bin/create_staff_user.py +++ b/backend/cosmetology-app/bin/create_staff_user.py @@ -140,7 +140,7 @@ def get_sub_from_attributes(user_attributes: list): parser = ArgumentParser( description='Create a staff user', - epilog='example: bin/create_staff_user.py -e justin@example.org -f williams -g justin -t board-ed -c octp -j oh', # noqa: E501 line-too-long + epilog='example: bin/create_staff_user.py -e justin@example.org -f williams -g justin -t board-ed -c cosm -j oh', # noqa: E501 line-too-long ) parser.add_argument('-e', '--email', help="The new user's email address", required=True) parser.add_argument('-f', '--family-name', help="The new user's family name", required=True) From 2c4523410aeec53dc5c810c3e4c2f00bc07e6963 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 6 Feb 2026 13:29:12 -0600 Subject: [PATCH 25/52] PR feedback --- .../check-compact-connect-ui-app.yml | 2 +- IMPLEMENTATION_PLAN.md | 339 ------------------ .../common/cc_common/email_service_client.py | 4 +- .../search/handlers/expiration_reminders.py | 2 +- 4 files changed, 4 insertions(+), 343 deletions(-) delete mode 100644 IMPLEMENTATION_PLAN.md diff --git a/.github/workflows/check-compact-connect-ui-app.yml b/.github/workflows/check-compact-connect-ui-app.yml index a85349961..b05bbaeb1 100644 --- a/.github/workflows/check-compact-connect-ui-app.yml +++ b/.github/workflows/check-compact-connect-ui-app.yml @@ -111,7 +111,7 @@ jobs: run: "pip install -r backend/compact-connect-ui-app/requirements-dev.txt" - name: Install all Python dependencies - run: "cd backend/compact-connect; bin/sync_deps.sh" + run: "cd backend/compact-connect-ui-app; bin/sync_deps.sh" - name: Test backend run: "cd backend/compact-connect-ui-app; bin/run_tests.sh -l all -no" diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md deleted file mode 100644 index b8ba763b5..000000000 --- a/IMPLEMENTATION_PLAN.md +++ /dev/null @@ -1,339 +0,0 @@ ---- -name: Expiration Reminder Stack -overview: Implement a new ExpirationReminderStack that sends email notifications to practitioners about expiring privileges at 30 days, 7 days, and day of expiration. The solution uses OpenSearch for efficient querying and includes notification tracking for idempotency. All providers are processed in a single Lambda execution. -todos: - - id: stack-infrastructure - content: Create ExpirationReminderStack with Lambda, EventBridge rules, alarms - status: pending - - id: lambda-handler - content: Implement expiration_reminders Lambda with OpenSearch query (single execution) - status: pending - - id: notification-tracking - content: Add ExpirationReminderTracker using EventStateTable for idempotency - status: pending - - id: email-template-ts - content: Add privilegeExpirationReminder template to email-notification-service Lambda - status: pending - - id: email-client-py - content: Add send_privilege_expiration_reminder_email to EmailServiceClient - status: pending - - id: backend-stage - content: Integrate ExpirationReminderStack into BackendStage - status: pending - - id: unit-tests - content: Write unit tests for Lambda handler and notification tracking - status: pending ---- - -# Privilege Expiration Reminder Notification System - -## Architecture Overview - -```mermaid -flowchart TB - subgraph eventbridge [EventBridge Rules] - rule30[30-Day Rule] - rule7[7-Day Rule] - rule0[Expiration Day Rule] - end - - subgraph lambda [Lambda Function] - process[Process Reminders Lambda] - end - - subgraph deps [Dependencies] - opensearch[OpenSearch] - email[Email Notification Lambda] - eventstate[Event State Table] - end - - rule30 -->|targetDate: T+30| process - rule7 -->|targetDate: T+7| process - rule0 -->|targetDate: T| process - - process --> opensearch - process --> email - process --> eventstate -``` - -## Phased Implementation Plan - -### Phase 1 — OpenSearch query generator + handler skeleton - -- **Implement**: `iterate_privileges_by_expiration_date()` generator (page-on-demand) and a minimal `process_expiration_reminders()` handler that wires together: OpenSearch → per-provider processing loop → metrics output. -- **Add tests**: - - Unit tests for the generator pagination behavior (multiple pages, empty results, last page) using mocked OpenSearch responses. - - Unit tests for “extract privileges expiring on targetDate” from a provider document (including nested `inner_hits` vs full privileges list, if applicable). -- **Run locally**: - - `python -m pytest backend/compact-connect/lambdas/python/expiration-reminders/tests` - -### Phase 2 — Email template + Python email client call - -- **Implement**: - - Add `privilegeExpirationReminder` template to the Node email service and an exported method to send it. - - Add `send_privilege_expiration_reminder_email()` to `cc_common.email_service_client.EmailServiceClient` to invoke the email service lambda with the new template payload. -- **Add tests**: - - Node unit tests verifying template variable validation + rendered output shape for `privilegeExpirationReminder`. - - Python unit test verifying the `EmailServiceClient` payload for `privilegeExpirationReminder` (template name, recipientType `SPECIFIC`, variables). -- **Run locally**: - - `python -m pytest backend/compact-connect/lambdas/python/common/tests` - - Run the Node email notification service unit tests (from `backend/compact-connect/lambdas/nodejs/`, using the repo’s standard test command for that package). - -### Phase 3 — Idempotency tracking (EventStateTable) - -- **Implement**: `ExpirationReminderTracker` (or extend existing tracker pattern) so the handler can: - - Check “already notified for provider + expiration_date (+ compact)” before sending. - - Record success after sending so retries do not duplicate emails. -- **Add tests**: - - Unit tests for tracker keying and “already sent” behavior. - - Handler unit tests covering: already-sent → skipped, missing email → skipped, send failure → failed + recorded (as applicable). -- **Run locally**: - - `python -m pytest backend/compact-connect/lambdas/python/expiration-reminders/tests` - -### Phase 4 — CDK stack + scheduling + alarms (incl. duration alarm) - -- **Implement**: - - `ExpirationReminderStack` with the Lambda, 3 EventBridge rules (30/7/0 days), and log retention. - - CloudWatch alarms: - - Lambda errors/throttles (as appropriate). - - **Duration alarm**: triggers if Lambda execution duration exceeds **10 minutes** (with 15-minute Lambda timeout). - - Integrate stack into `backend/compact-connect/pipeline/backend_stage.py` (consistent with other optional stacks). -- **Add tests**: - - CDK assertions tests validating: rules exist, Lambda timeout is 15 minutes, and the duration alarm threshold is 10 minutes. -- **Run locally**: - - `python -m pytest backend/compact-connect/tests/app -k expiration_reminder` - -## Implementation Components - -### 1. New Stack: ExpirationReminderStack - -Create [`stacks/expiration_reminder_stack/__init__.py`](backend/compact-connect/stacks/expiration_reminder_stack/__init__.py) - -**Dependencies:** - -- `PersistentStack` - provider table, email service lambda, encryption key, alarm topic -- `EventStateStack` - notification tracking table -- `SearchPersistentStack` - OpenSearch domain for querying privileges by expiration date - -**Resources created:** - -- Lambda function for processing reminders (15-minute timeout to handle all providers in single execution) -- Three EventBridge rules (30-day, 7-day, day-of) -- CloudWatch alarm for Lambda execution time (triggers if execution exceeds 10 minutes) -- CloudWatch alarms for Lambda failures -- Log groups with appropriate retention - -**Note on execution time:** The Lambda is configured with a 15-minute timeout. A CloudWatch alarm will trigger if the execution time ever exceeds 10 minutes, alerting us before we approach the timeout limit. This allows us to monitor if we need to revisit the single-execution approach in the future. - -### 2. New Lambda: expiration-reminders - -Create new lambda directory: `lambdas/python/expiration-reminders/` - -**Handler:** `handlers/expiration_reminders.py` - -```python -def process_expiration_reminders(event: dict, context): - """ - Input: - { - "targetDate": "2026-02-16", # Expiration date to process - "daysBefore": 30, # Days before expiration (30, 7, or 0) - "scheduledTime": "2026-01-17...", # When rule triggered (for logging) - } - - Output: - { - "targetDate": "2026-02-16", - "daysBefore": 30, - "metrics": { "sent": N, "skipped": N, "failed": N, "alreadySent": N, "noEmail": N } - } - """ -``` - -**Core logic:** - -1. Query OpenSearch for active privileges expiring on `targetDate` - - Use `iterate_privileges_by_expiration_date()` generator which handles pagination internally - - The generator yields provider documents one at a time, fetching subsequent pages on demand - - This avoids loading all results into memory at once -2. As each provider document is yielded from the generator: - - Extract privileges expiring on `targetDate` from the provider document - - Check `EventStateTable` if notification already sent for this provider/date - - Skip if already sent (idempotency) - - Get provider's registered email from provider record - - Skip if no registered email - - Send email via Email Notification Service Lambda (consolidating all expiring privileges for this provider) - - Record notification sent in `EventStateTable` - -3. Return metrics summary with counts of sent, skipped, and failed notifications - -**Note:** The Lambda processes all providers in a single execution. OpenSearch pagination happens within the Lambda function (not across multiple invocations) using a generator that fetches pages on demand, keeping memory usage low. - -### 3. Email Template Addition - -Update [`lambdas/nodejs/email-notification-service/lambda.ts`](backend/compact-connect/lambdas/nodejs/email-notification-service/lambda.ts) - -Add new case for `privilegeExpirationReminder` template. - -Update [`lambdas/nodejs/lib/email/email-notification-service.ts`](backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts) - -Add `sendPrivilegeExpirationReminderEmail()` method. - -**Template variables:** - -- `providerFirstName` - Practitioner's first name -- `expirationDate` - The actual expiration date (formatted nicely) -- `privileges` - Array of `{ jurisdiction, licenseType, privilegeId }` - -**Email content:** - -- Subject: "Your Compact Connect Privileges Expire on [Date]" -- Body: Lists all privileges expiring on that date with jurisdiction and license type - -### 4. Python Email Client Extension - -Update [`lambdas/python/common/cc_common/email_service_client.py`](backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py) - -Add `send_privilege_expiration_reminder_email()` method following existing patterns. - -### 5. Notification Tracking - -Use existing `EventStateTable` with new key pattern (implemented in `cc_common.event_state_client`): - -``` -pk: {compact}#EXPIRATION_REMINDER#{provider_id} -sk: {event_type}#{expiration_date} -ttl: 90 days after expiration (auto-cleanup) -``` - -Where `event_type` is one of: -- `privilege.expiration.30day` -- `privilege.expiration.7day` -- `privilege.expiration.dayOf` - -This allows tracking each reminder type (30-day, 7-day, day-of) independently per provider/expiration date. - -`ExpirationReminderTracker` class in `cc_common.event_state_client` provides the interface for checking/recording sent notifications. - -### 6. OpenSearch Query Helper - -Add generator method to find privileges by expiration date. This method handles OpenSearch pagination internally and yields results one at a time, fetching subsequent pages on demand: - -```python -def iterate_privileges_by_expiration_date(compact: str, expiration_date: date): - """Generator that yields provider documents with privileges expiring on a specific date. - - Uses search_after pagination internally, fetching pages on demand as results are consumed. - This keeps memory usage low by not loading all results into memory at once. - - Yields provider documents (dict) one at a time. Each document contains the provider - data and a nested privileges array with matching privileges. - - :param compact: Compact identifier - :param expiration_date: Date to match against privilege expiration dates - :yield: Provider document dict with matching privileges - """ - index_name = f'compact_{compact}_providers' - search_after = None - current_page_hits = [] - is_last_page = False - - while True: - # Fetch next page if current page is exhausted - if not current_page_hits: - # If we've already processed the last page, we're done - if is_last_page: - break - - search_body = { - "query": { - "nested": { - "path": "privileges", - "query": { - "bool": { - "must": [ - {"term": {"privileges.dateOfExpiration": expiration_date.isoformat()}}, - {"term": {"privileges.status": "active"}} - ] - } - }, - "inner_hits": {"size": 100} - } - }, - "sort": [{"providerId": "asc"}], # Required for search_after pagination - "size": 100 - } - - if search_after: - search_body["search_after"] = search_after - - response = opensearch_client.search(index_name=index_name, body=search_body) - hits = response.get('hits', {}).get('hits', []) - - if not hits: - # No more results - break - - # Store hits as a list that we'll pop from - current_page_hits = hits - - # If we got fewer results than requested, this is the last page - if len(hits) < 100: - is_last_page = True - else: - # Get sort values from last hit for next page (before we start popping) - last_hit = hits[-1] - search_after = last_hit.get('sort') - - # Pop and yield next result from current page - # Using pop(0) removes the item from the list, freeing memory as we process - hit = current_page_hits.pop(0) - yield hit['_source'] -``` - -### 7. Backend Stage Integration - -Update [`pipeline/backend_stage.py`](backend/compact-connect/pipeline/backend_stage.py) - -Add `ExpirationReminderStack` instantiation, conditional on `self.persistent_stack.hosted_zone` (same as `NotificationStack` and `ReportingStack`). - -## File Structure - -``` -backend/compact-connect/ -├── stacks/ -│ └── expiration_reminder_stack/ -│ └── __init__.py # New stack definition -├── lambdas/ -│ ├── python/ -│ │ └── expiration-reminders/ # New lambda package -│ │ ├── handlers/ -│ │ │ └── expiration_reminders.py -│ │ ├── requirements.in -│ │ ├── requirements.txt -│ │ ├── requirements-dev.in -│ │ ├── requirements-dev.txt -│ │ └── tests/ -│ │ └── (unit tests) -│ └── nodejs/ -│ ├── email-notification-service/ -│ │ └── lambda.ts # Add template case -│ └── lib/email/ -│ └── email-notification-service.ts # Add email method -└── pipeline/ - └── backend_stage.py # Add stack to stage -``` - -## Testing Strategy - -1. **Unit tests** for the Lambda handler with mocked OpenSearch and email service -2. **Manual testing** via AWS Console - invoke Lambda directly with specific `targetDate`: - -```json -{ - "targetDate": "2026-02-16", - "scheduledTime": "2026-01-20T10:00:00Z" -} -``` - -The notification tracker ensures no duplicate emails are sent when retrying. diff --git a/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py b/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py index 510808344..4ed5c43d5 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py @@ -100,8 +100,8 @@ def _invoke_lambda(self, payload: dict[str, Any]) -> dict[str, Any]: ) if response.get('FunctionError'): - error_message = 'Failed to send email notification' - self._logger.error(error_message, payload=payload, response=response) + error_message = f'Failed to send email notification: {response.get("FunctionError")}' + self._logger.error(error_message) raise CCInternalException(error_message) return response diff --git a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py index 61b194799..b4e21d2fc 100644 --- a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py @@ -314,7 +314,7 @@ def _process_provider_notification( # Build privileges list for email, filtering to only active privileges. email_privileges = [] for privilege in provider_doc['privileges']: - # Only include active privileges in the email + # Only include active privileges in the email per the feature request requirements if privilege.get('status') != 'active': logger.info( 'Skipping inactive privilege', From c353a5dc25cdaeb09628590d954e17be68ff6145 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 6 Feb 2026 14:52:21 -0600 Subject: [PATCH 26/52] Update node dependencies to latest --- .../email-notification-service/lambda.ts | 7 +- .../lambdas/nodejs/package.json | 2 +- .../compact-connect/lambdas/nodejs/yarn.lock | 4266 +++++++++-------- 3 files changed, 2179 insertions(+), 2096 deletions(-) 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 79eb83e87..4d2e8f8d1 100644 --- a/backend/compact-connect/lambdas/nodejs/email-notification-service/lambda.ts +++ b/backend/compact-connect/lambdas/nodejs/email-notification-service/lambda.ts @@ -386,7 +386,7 @@ export class Lambda implements LambdaInterface { event.templateVariables?.auditNote || '' ); break; - case 'privilegeExpirationReminder': + case 'privilegeExpirationReminder': { if (!event.specificEmails?.length) { throw new Error('No recipients found for privilege expiration reminder email'); } @@ -395,8 +395,8 @@ export class Lambda implements LambdaInterface { || !event.templateVariables?.privileges) { throw new Error('Missing required template variables for privilegeExpirationReminder template.'); } - const privileges = event.templateVariables.privileges as Array<{ jurisdiction?: string; - licenseType?: string; privilegeId?: + const privileges = event.templateVariables.privileges as Array<{ jurisdiction?: string; + licenseType?: string; privilegeId?: string; dateOfExpiration?: string; formattedExpirationDate?: string }>; @@ -420,6 +420,7 @@ export class Lambda implements LambdaInterface { privileges as PrivilegeExpirationReminderRow[] ); break; + } case 'licenseInvestigationStateNotification': if (!event.jurisdiction) { throw new Error('No jurisdiction provided for license investigation state notification email'); diff --git a/backend/compact-connect/lambdas/nodejs/package.json b/backend/compact-connect/lambdas/nodejs/package.json index b54e9be68..6771d8d53 100644 --- a/backend/compact-connect/lambdas/nodejs/package.json +++ b/backend/compact-connect/lambdas/nodejs/package.json @@ -17,7 +17,7 @@ "@types/aws-lambda": "8.10.145", "@types/jest": "^29.5.12", "@types/node": "22.5.4", - "@types/nodemailer": "^6.4.14", + "@types/nodemailer": "^7.0.9", "@types/react": "^18.3.12", "@typescript-eslint/eslint-plugin": "^8.12.2", "@typescript-eslint/parser": "^8.12.2", diff --git a/backend/compact-connect/lambdas/nodejs/yarn.lock b/backend/compact-connect/lambdas/nodejs/yarn.lock index d2455e780..ed6e7113e 100644 --- a/backend/compact-connect/lambdas/nodejs/yarn.lock +++ b/backend/compact-connect/lambdas/nodejs/yarn.lock @@ -2,17 +2,9 @@ # yarn lockfile v1 -"@ampproject/remapping@^2.2.0": - version "2.3.0" - resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz" - 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.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz" + resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-5.2.0.tgz#cfcc22570949c98c6689cfcbd2d693d36cdae2e1" integrity sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg== dependencies: "@aws-crypto/util" "^5.2.0" @@ -21,7 +13,7 @@ "@aws-crypto/crc32c@5.2.0": version "5.2.0" - resolved "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz" + resolved "https://registry.yarnpkg.com/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz#4e34aab7f419307821509a98b9b08e84e0c1917e" integrity sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag== dependencies: "@aws-crypto/util" "^5.2.0" @@ -30,7 +22,7 @@ "@aws-crypto/sha1-browser@5.2.0": version "5.2.0" - resolved "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz#b0ee2d2821d3861f017e965ef3b4cb38e3b6a0f4" integrity sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg== dependencies: "@aws-crypto/supports-web-crypto" "^5.2.0" @@ -42,7 +34,7 @@ "@aws-crypto/sha256-browser@5.2.0": version "5.2.0" - resolved "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz#153895ef1dba6f9fce38af550e0ef58988eb649e" integrity sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw== dependencies: "@aws-crypto/sha256-js" "^5.2.0" @@ -55,7 +47,7 @@ "@aws-crypto/sha256-js@5.2.0", "@aws-crypto/sha256-js@^5.2.0": version "5.2.0" - resolved "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz#c4fdb773fdbed9a664fc1a95724e206cf3860042" integrity sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA== dependencies: "@aws-crypto/util" "^5.2.0" @@ -64,937 +56,972 @@ "@aws-crypto/supports-web-crypto@^5.2.0": version "5.2.0" - resolved "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz" + resolved "https://registry.yarnpkg.com/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz#a1e399af29269be08e695109aa15da0a07b5b5fb" integrity sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg== dependencies: tslib "^2.6.2" "@aws-crypto/util@5.2.0", "@aws-crypto/util@^5.2.0": version "5.2.0" - resolved "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz" + resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-5.2.0.tgz#71284c9cffe7927ddadac793c14f14886d3876da" integrity sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ== dependencies: "@aws-sdk/types" "^3.222.0" "@smithy/util-utf8" "^2.0.0" tslib "^2.6.2" -"@aws-lambda-powertools/commons@2.30.0": - version "2.30.0" - resolved "https://registry.yarnpkg.com/@aws-lambda-powertools/commons/-/commons-2.30.0.tgz#920d00d560511e76ee7da767f9eeec396a2a527a" - integrity sha512-5kjamCPR/dIEHIk4BAFDSO/MgF21qL/q/f5AKi3GEOdvULuLcbw40xneMmPXo4ETaBSe+BHvovWLuI0Jl9BEnw== +"@aws-lambda-powertools/commons@2.30.2": + version "2.30.2" + resolved "https://registry.yarnpkg.com/@aws-lambda-powertools/commons/-/commons-2.30.2.tgz#147624901d08886be77e2a380feff1e8a1f1e026" + integrity sha512-bhhrpUdCfpBGKllJfGyi5faf12mh6tjYmOf5DALgIym89NkidFphPdZtWl5l6IN1YhgO8HWPX4Yvmi4tPBE/uw== dependencies: - "@aws/lambda-invoke-store" "0.2.2" + "@aws/lambda-invoke-store" "0.2.3" "@aws-lambda-powertools/logger@^2.30.0": - version "2.30.0" - resolved "https://registry.yarnpkg.com/@aws-lambda-powertools/logger/-/logger-2.30.0.tgz#9a8c74575cc19ed2c66e9f7c9f58c39b86c025ac" - integrity sha512-ejmuzn8KNi7CrhJSnl4a/SLAjcmlHJPvH55QpSZvj8K8aQLhOKcCpd0u+gXp0oYlj50d22h9ZV5qtIn7O8V4Lw== + version "2.30.2" + resolved "https://registry.yarnpkg.com/@aws-lambda-powertools/logger/-/logger-2.30.2.tgz#f85aa0315a032053365366e0716c1d2fcedd19be" + integrity sha512-BSh2hPcnYUJNn/Olwby4LpHbUWePKuSKtfNeTBXAn+op3OcC7kx7CHSqc5PYGW7x2T8uliE1IuGB5vQz2gBeDQ== dependencies: - "@aws-lambda-powertools/commons" "2.30.0" - "@aws/lambda-invoke-store" "0.2.2" - lodash.merge "^4.6.2" + "@aws-lambda-powertools/commons" "2.30.2" + "@aws/lambda-invoke-store" "0.2.3" "@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== + version "3.984.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-dynamodb/-/client-dynamodb-3.984.0.tgz#e6e03af181339d0bca1e30d905a51950d1442239" + integrity sha512-8/Oft9MWQtbG6p9f8eY5fsKC2CcO5YVDlwive8eUYS9mEbgnyQxm68OyH26WvsSTykQ9QkIbR+fOG56RsIBODw== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@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" + "@aws-sdk/core" "^3.973.6" + "@aws-sdk/credential-provider-node" "^3.972.5" + "@aws-sdk/dynamodb-codec" "^3.972.7" + "@aws-sdk/middleware-endpoint-discovery" "^3.972.3" + "@aws-sdk/middleware-host-header" "^3.972.3" + "@aws-sdk/middleware-logger" "^3.972.3" + "@aws-sdk/middleware-recursion-detection" "^3.972.3" + "@aws-sdk/middleware-user-agent" "^3.972.6" + "@aws-sdk/region-config-resolver" "^3.972.3" + "@aws-sdk/types" "^3.973.1" + "@aws-sdk/util-endpoints" "3.984.0" + "@aws-sdk/util-user-agent-browser" "^3.972.3" + "@aws-sdk/util-user-agent-node" "^3.972.4" + "@smithy/config-resolver" "^4.4.6" + "@smithy/core" "^3.22.0" + "@smithy/fetch-http-handler" "^5.3.9" + "@smithy/hash-node" "^4.2.8" + "@smithy/invalid-dependency" "^4.2.8" + "@smithy/middleware-content-length" "^4.2.8" + "@smithy/middleware-endpoint" "^4.4.12" + "@smithy/middleware-retry" "^4.4.29" + "@smithy/middleware-serde" "^4.2.9" + "@smithy/middleware-stack" "^4.2.8" + "@smithy/node-config-provider" "^4.3.8" + "@smithy/node-http-handler" "^4.4.8" + "@smithy/protocol-http" "^5.3.8" + "@smithy/smithy-client" "^4.11.1" + "@smithy/types" "^4.12.0" + "@smithy/url-parser" "^4.2.8" + "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" - "@smithy/util-body-length-node" "^4.2.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-body-length-node" "^4.2.1" + "@smithy/util-defaults-mode-browser" "^4.3.28" + "@smithy/util-defaults-mode-node" "^4.2.31" + "@smithy/util-endpoints" "^3.2.8" + "@smithy/util-middleware" "^4.2.8" + "@smithy/util-retry" "^4.2.8" "@smithy/util-utf8" "^4.2.0" - "@smithy/util-waiter" "^4.2.0" - "@smithy/uuid" "^1.1.0" + "@smithy/util-waiter" "^4.2.8" tslib "^2.6.2" "@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== + version "3.984.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.984.0.tgz#ca8726d321d383c9b5f0f7c2fb48f69e5d583997" + integrity sha512-7ny2Slr93Y+QniuluvcfWwyDi32zWQfznynL56Tk0vVh7bWrvS/odm8WP2nInKicRVNipcJHY2YInur6Q/9V0A== dependencies: "@aws-crypto/sha1-browser" "5.2.0" "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "3.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" + "@aws-sdk/core" "^3.973.6" + "@aws-sdk/credential-provider-node" "^3.972.5" + "@aws-sdk/middleware-bucket-endpoint" "^3.972.3" + "@aws-sdk/middleware-expect-continue" "^3.972.3" + "@aws-sdk/middleware-flexible-checksums" "^3.972.4" + "@aws-sdk/middleware-host-header" "^3.972.3" + "@aws-sdk/middleware-location-constraint" "^3.972.3" + "@aws-sdk/middleware-logger" "^3.972.3" + "@aws-sdk/middleware-recursion-detection" "^3.972.3" + "@aws-sdk/middleware-sdk-s3" "^3.972.6" + "@aws-sdk/middleware-ssec" "^3.972.3" + "@aws-sdk/middleware-user-agent" "^3.972.6" + "@aws-sdk/region-config-resolver" "^3.972.3" + "@aws-sdk/signature-v4-multi-region" "3.984.0" + "@aws-sdk/types" "^3.973.1" + "@aws-sdk/util-endpoints" "3.984.0" + "@aws-sdk/util-user-agent-browser" "^3.972.3" + "@aws-sdk/util-user-agent-node" "^3.972.4" + "@smithy/config-resolver" "^4.4.6" + "@smithy/core" "^3.22.0" + "@smithy/eventstream-serde-browser" "^4.2.8" + "@smithy/eventstream-serde-config-resolver" "^4.3.8" + "@smithy/eventstream-serde-node" "^4.2.8" + "@smithy/fetch-http-handler" "^5.3.9" + "@smithy/hash-blob-browser" "^4.2.9" + "@smithy/hash-node" "^4.2.8" + "@smithy/hash-stream-node" "^4.2.8" + "@smithy/invalid-dependency" "^4.2.8" + "@smithy/md5-js" "^4.2.8" + "@smithy/middleware-content-length" "^4.2.8" + "@smithy/middleware-endpoint" "^4.4.12" + "@smithy/middleware-retry" "^4.4.29" + "@smithy/middleware-serde" "^4.2.9" + "@smithy/middleware-stack" "^4.2.8" + "@smithy/node-config-provider" "^4.3.8" + "@smithy/node-http-handler" "^4.4.8" + "@smithy/protocol-http" "^5.3.8" + "@smithy/smithy-client" "^4.11.1" + "@smithy/types" "^4.12.0" + "@smithy/url-parser" "^4.2.8" + "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" - "@smithy/util-body-length-node" "^4.2.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-body-length-node" "^4.2.1" + "@smithy/util-defaults-mode-browser" "^4.3.28" + "@smithy/util-defaults-mode-node" "^4.2.31" + "@smithy/util-endpoints" "^3.2.8" + "@smithy/util-middleware" "^4.2.8" + "@smithy/util-retry" "^4.2.8" + "@smithy/util-stream" "^4.5.10" "@smithy/util-utf8" "^4.2.0" - "@smithy/util-waiter" "^4.2.0" - "@smithy/uuid" "^1.1.0" + "@smithy/util-waiter" "^4.2.8" tslib "^2.6.2" "@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== + version "3.984.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sesv2/-/client-sesv2-3.984.0.tgz#416c9d00b6def6f4f04098674deadf7c45d474de" + integrity sha512-39TuwXcP5i7/WG1KZuGkXwdFu8Hu0ecUZv5ib4Yyr88MZ1rceyxnxkZEjKatuF+xJU5OjuQvdkWjtD3k7rlI4w== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@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" + "@aws-sdk/core" "^3.973.6" + "@aws-sdk/credential-provider-node" "^3.972.5" + "@aws-sdk/middleware-host-header" "^3.972.3" + "@aws-sdk/middleware-logger" "^3.972.3" + "@aws-sdk/middleware-recursion-detection" "^3.972.3" + "@aws-sdk/middleware-user-agent" "^3.972.6" + "@aws-sdk/region-config-resolver" "^3.972.3" + "@aws-sdk/signature-v4-multi-region" "3.984.0" + "@aws-sdk/types" "^3.973.1" + "@aws-sdk/util-endpoints" "3.984.0" + "@aws-sdk/util-user-agent-browser" "^3.972.3" + "@aws-sdk/util-user-agent-node" "^3.972.4" + "@smithy/config-resolver" "^4.4.6" + "@smithy/core" "^3.22.0" + "@smithy/fetch-http-handler" "^5.3.9" + "@smithy/hash-node" "^4.2.8" + "@smithy/invalid-dependency" "^4.2.8" + "@smithy/middleware-content-length" "^4.2.8" + "@smithy/middleware-endpoint" "^4.4.12" + "@smithy/middleware-retry" "^4.4.29" + "@smithy/middleware-serde" "^4.2.9" + "@smithy/middleware-stack" "^4.2.8" + "@smithy/node-config-provider" "^4.3.8" + "@smithy/node-http-handler" "^4.4.8" + "@smithy/protocol-http" "^5.3.8" + "@smithy/smithy-client" "^4.11.1" + "@smithy/types" "^4.12.0" + "@smithy/url-parser" "^4.2.8" + "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" - "@smithy/util-body-length-node" "^4.2.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-body-length-node" "^4.2.1" + "@smithy/util-defaults-mode-browser" "^4.3.28" + "@smithy/util-defaults-mode-node" "^4.2.31" + "@smithy/util-endpoints" "^3.2.8" + "@smithy/util-middleware" "^4.2.8" + "@smithy/util-retry" "^4.2.8" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@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== +"@aws-sdk/client-sso@3.982.0": + version "3.982.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.982.0.tgz#36ea3868045c6d0ade03bf7a0119ac3a1abf79a8" + integrity sha512-qJrIiivmvujdGqJ0ldSUvhN3k3N7GtPesoOI1BSt0fNXovVnMz4C/JmnkhZihU7hJhDvxJaBROLYTU+lpild4w== dependencies: "@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" + "@aws-sdk/core" "^3.973.6" + "@aws-sdk/middleware-host-header" "^3.972.3" + "@aws-sdk/middleware-logger" "^3.972.3" + "@aws-sdk/middleware-recursion-detection" "^3.972.3" + "@aws-sdk/middleware-user-agent" "^3.972.6" + "@aws-sdk/region-config-resolver" "^3.972.3" + "@aws-sdk/types" "^3.973.1" + "@aws-sdk/util-endpoints" "3.982.0" + "@aws-sdk/util-user-agent-browser" "^3.972.3" + "@aws-sdk/util-user-agent-node" "^3.972.4" + "@smithy/config-resolver" "^4.4.6" + "@smithy/core" "^3.22.0" + "@smithy/fetch-http-handler" "^5.3.9" + "@smithy/hash-node" "^4.2.8" + "@smithy/invalid-dependency" "^4.2.8" + "@smithy/middleware-content-length" "^4.2.8" + "@smithy/middleware-endpoint" "^4.4.12" + "@smithy/middleware-retry" "^4.4.29" + "@smithy/middleware-serde" "^4.2.9" + "@smithy/middleware-stack" "^4.2.8" + "@smithy/node-config-provider" "^4.3.8" + "@smithy/node-http-handler" "^4.4.8" + "@smithy/protocol-http" "^5.3.8" + "@smithy/smithy-client" "^4.11.1" + "@smithy/types" "^4.12.0" + "@smithy/url-parser" "^4.2.8" + "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" - "@smithy/util-body-length-node" "^4.2.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-body-length-node" "^4.2.1" + "@smithy/util-defaults-mode-browser" "^4.3.28" + "@smithy/util-defaults-mode-node" "^4.2.31" + "@smithy/util-endpoints" "^3.2.8" + "@smithy/util-middleware" "^4.2.8" + "@smithy/util-retry" "^4.2.8" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@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" +"@aws-sdk/core@^3.973.6": + version "3.973.6" + resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.973.6.tgz#dd7ff2af60034da3e1af7926d2ce7efe0341ea64" + integrity sha512-pz4ZOw3BLG0NdF25HoB9ymSYyPbMiIjwQJ2aROXRhAzt+b+EOxStfFv8s5iZyP6Kiw7aYhyWxj5G3NhmkoOTKw== + dependencies: + "@aws-sdk/types" "^3.973.1" + "@aws-sdk/xml-builder" "^3.972.4" + "@smithy/core" "^3.22.0" + "@smithy/node-config-provider" "^4.3.8" + "@smithy/property-provider" "^4.2.8" + "@smithy/protocol-http" "^5.3.8" + "@smithy/signature-v4" "^5.3.8" + "@smithy/smithy-client" "^4.11.1" + "@smithy/types" "^4.12.0" + "@smithy/util-base64" "^4.3.0" + "@smithy/util-middleware" "^4.2.8" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@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== +"@aws-sdk/crc64-nvme@3.972.0": + version "3.972.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.0.tgz#c5e6d14428c9fb4e6bb0646b73a0fa68e6007e24" + integrity sha512-ThlLhTqX68jvoIVv+pryOdb5coP1cX1/MaTbB9xkGDCbWbsqQcLqzPxuSoW1DCnAAIacmXCWpzUNOB9pv+xXQw== + dependencies: + "@smithy/types" "^4.12.0" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-env@^3.972.4": + version "3.972.4" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.4.tgz#33b5ad1169ce5b7ac313ce2c27b939277795b9d0" + integrity sha512-/8dnc7+XNMmViEom2xsNdArQxQPSgy4Z/lm6qaFPTrMFesT1bV3PsBhb19n09nmxHdrtQskYmViddUIjUQElXg== dependencies: - "@aws-sdk/core" "3.901.0" - "@aws-sdk/types" "3.901.0" - "@smithy/property-provider" "^4.2.0" - "@smithy/types" "^4.6.0" + "@aws-sdk/core" "^3.973.6" + "@aws-sdk/types" "^3.973.1" + "@smithy/property-provider" "^4.2.8" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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" +"@aws-sdk/credential-provider-http@^3.972.6": + version "3.972.6" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.6.tgz#3a6720826cff7620690fc4fdf522a81e299a2ebf" + integrity sha512-5ERWqRljiZv44AIdvIRQ3k+EAV0Sq2WeJHvXuK7gL7bovSxOf8Al7MLH7Eh3rdovH4KHFnlIty7J71mzvQBl5Q== + dependencies: + "@aws-sdk/core" "^3.973.6" + "@aws-sdk/types" "^3.973.1" + "@smithy/fetch-http-handler" "^5.3.9" + "@smithy/node-http-handler" "^4.4.8" + "@smithy/property-provider" "^4.2.8" + "@smithy/protocol-http" "^5.3.8" + "@smithy/smithy-client" "^4.11.1" + "@smithy/types" "^4.12.0" + "@smithy/util-stream" "^4.5.10" tslib "^2.6.2" -"@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" +"@aws-sdk/credential-provider-ini@^3.972.4": + version "3.972.4" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.4.tgz#77df2d72984b51f51307914a543514414f780e19" + integrity sha512-eRUg+3HaUKuXWn/lEMirdiA5HOKmEl8hEHVuszIDt2MMBUKgVX5XNGmb3XmbgU17h6DZ+RtjbxQpjhz3SbTjZg== + dependencies: + "@aws-sdk/core" "^3.973.6" + "@aws-sdk/credential-provider-env" "^3.972.4" + "@aws-sdk/credential-provider-http" "^3.972.6" + "@aws-sdk/credential-provider-login" "^3.972.4" + "@aws-sdk/credential-provider-process" "^3.972.4" + "@aws-sdk/credential-provider-sso" "^3.972.4" + "@aws-sdk/credential-provider-web-identity" "^3.972.4" + "@aws-sdk/nested-clients" "3.982.0" + "@aws-sdk/types" "^3.973.1" + "@smithy/credential-provider-imds" "^4.2.8" + "@smithy/property-provider" "^4.2.8" + "@smithy/shared-ini-file-loader" "^4.4.3" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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" +"@aws-sdk/credential-provider-login@^3.972.4": + version "3.972.4" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.4.tgz#dc656fbcb3206e5bebbdc44a571503a5ba4e0e6d" + integrity sha512-nLGjXuvWWDlQAp505xIONI7Gam0vw2p7Qu3P6on/W2q7rjJXtYjtpHbcsaOjJ/pAju3eTvEQuSuRedcRHVQIAQ== + dependencies: + "@aws-sdk/core" "^3.973.6" + "@aws-sdk/nested-clients" "3.982.0" + "@aws-sdk/types" "^3.973.1" + "@smithy/property-provider" "^4.2.8" + "@smithy/protocol-http" "^5.3.8" + "@smithy/shared-ini-file-loader" "^4.4.3" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@aws-sdk/credential-provider-node@^3.972.5": + version "3.972.5" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.5.tgz#2f16620eee963c445a727d4a7b5e000df41fa7b6" + integrity sha512-VWXKgSISQCI2GKN3zakTNHSiZ0+mux7v6YHmmbLQp/o3fvYUQJmKGcLZZzg2GFA+tGGBStplra9VFNf/WwxpYg== + dependencies: + "@aws-sdk/credential-provider-env" "^3.972.4" + "@aws-sdk/credential-provider-http" "^3.972.6" + "@aws-sdk/credential-provider-ini" "^3.972.4" + "@aws-sdk/credential-provider-process" "^3.972.4" + "@aws-sdk/credential-provider-sso" "^3.972.4" + "@aws-sdk/credential-provider-web-identity" "^3.972.4" + "@aws-sdk/types" "^3.973.1" + "@smithy/credential-provider-imds" "^4.2.8" + "@smithy/property-provider" "^4.2.8" + "@smithy/shared-ini-file-loader" "^4.4.3" + "@smithy/types" "^4.12.0" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-process@^3.972.4": + version "3.972.4" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.4.tgz#4918ba11b88e0bc96e488f199e6d5605f2449c49" + integrity sha512-TCZpWUnBQN1YPk6grvd5x419OfXjHvhj5Oj44GYb84dOVChpg/+2VoEj+YVA4F4E/6huQPNnX7UYbTtxJqgihw== dependencies: - "@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" + "@aws-sdk/core" "^3.973.6" + "@aws-sdk/types" "^3.973.1" + "@smithy/property-provider" "^4.2.8" + "@smithy/shared-ini-file-loader" "^4.4.3" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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" +"@aws-sdk/credential-provider-sso@^3.972.4": + version "3.972.4" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.4.tgz#0a945243e26e76c7460e20ae6725e07aa03d75fb" + integrity sha512-wzsGwv9mKlwJ3vHLyembBvGE/5nPUIwRR2I51B1cBV4Cb4ql9nIIfpmHzm050XYTY5fqTOKJQnhLj7zj89VG8g== + dependencies: + "@aws-sdk/client-sso" "3.982.0" + "@aws-sdk/core" "^3.973.6" + "@aws-sdk/token-providers" "3.982.0" + "@aws-sdk/types" "^3.973.1" + "@smithy/property-provider" "^4.2.8" + "@smithy/shared-ini-file-loader" "^4.4.3" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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" +"@aws-sdk/credential-provider-web-identity@^3.972.4": + version "3.972.4" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.4.tgz#8dd394a0d1e1663fe0dec5ae9f2688cc5d3de410" + integrity sha512-hIzw2XzrG8jzsUSEatehmpkd5rWzASg5IHUfA+m01k/RtvfAML7ZJVVohuKdhAYx+wV2AThLiQJVzqn7F0khrw== + dependencies: + "@aws-sdk/core" "^3.973.6" + "@aws-sdk/nested-clients" "3.982.0" + "@aws-sdk/types" "^3.973.1" + "@smithy/property-provider" "^4.2.8" + "@smithy/shared-ini-file-loader" "^4.4.3" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@aws-sdk/dynamodb-codec@^3.972.7": + version "3.972.7" + resolved "https://registry.yarnpkg.com/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.972.7.tgz#73ad04468838aa6aef8306b9cfd8ecd32d18421b" + integrity sha512-oo1g22IhIvZiBNTqV2DwBh39+pwo7Tz6aBZjA+eTNFJ1jfxD84tTycVPOwaYLb2ZYhgsxOT9bTAj/EHEPoKbLQ== + dependencies: + "@aws-sdk/core" "^3.973.6" + "@smithy/core" "^3.22.0" + "@smithy/smithy-client" "^4.11.1" + "@smithy/types" "^4.12.0" + "@smithy/util-base64" "^4.3.0" + tslib "^2.6.2" + +"@aws-sdk/endpoint-cache@^3.972.2": + version "3.972.2" + resolved "https://registry.yarnpkg.com/@aws-sdk/endpoint-cache/-/endpoint-cache-3.972.2.tgz#87acd8ea94e29e57fd78f87bdaaa9e1f944fbacc" + integrity sha512-3L7mwqSLJ6ouZZKtCntoNF0HTYDNs1FDQqkGjoPWXcv1p0gnLotaDmLq1rIDqfu4ucOit0Re3ioLyYDUTpSroA== dependencies: mnemonist "0.38.3" tslib "^2.6.2" -"@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== +"@aws-sdk/middleware-bucket-endpoint@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.3.tgz#158507d55505e5e7b5b8cdac9f037f6aa326f202" + integrity sha512-fmbgWYirF67YF1GfD7cg5N6HHQ96EyRNx/rDIrTF277/zTWVuPI2qS/ZHgofwR1NZPe/NWvoppflQY01LrbVLg== 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" + "@aws-sdk/types" "^3.973.1" + "@aws-sdk/util-arn-parser" "^3.972.2" + "@smithy/node-config-provider" "^4.3.8" + "@smithy/protocol-http" "^5.3.8" + "@smithy/types" "^4.12.0" "@smithy/util-config-provider" "^4.2.0" tslib "^2.6.2" -"@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== +"@aws-sdk/middleware-endpoint-discovery@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.972.3.tgz#96e1cad8447d756ceaf143d96a367be24b1e6d4e" + integrity sha512-xAxA8/TOygQmMrzcw9CrlpTHCGWSG/lvzrHCySfSZpDN4/yVSfXO+gUwW9WxeskBmuv9IIFATOVpzc9EzfTZ0Q== dependencies: - "@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" + "@aws-sdk/endpoint-cache" "^3.972.2" + "@aws-sdk/types" "^3.973.1" + "@smithy/node-config-provider" "^4.3.8" + "@smithy/protocol-http" "^5.3.8" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@aws-sdk/middleware-expect-continue@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.3.tgz#c60bd81e81dde215b9f3f67e3c5448b608afd530" + integrity sha512-4msC33RZsXQpUKR5QR4HnvBSNCPLGHmB55oDiROqqgyOc+TOfVu2xgi5goA7ms6MdZLeEh2905UfWMnMMF4mRg== dependencies: - "@aws-sdk/types" "3.901.0" - "@smithy/protocol-http" "^5.3.0" - "@smithy/types" "^4.6.0" + "@aws-sdk/types" "^3.973.1" + "@smithy/protocol-http" "^5.3.8" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@aws-sdk/middleware-flexible-checksums@^3.972.4": + version "3.972.4" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.972.4.tgz#3d43d12a11a07a6660093d10b16d8e65bf242050" + integrity sha512-xOxsUkF3O3BtIe3tf54OpPo94eZepjFm3z0Dd2TZKbsPxMiRTFXurC04wJ58o/wPW9YHVO9VqZik3MfoPfrKlw== dependencies: "@aws-crypto/crc32" "5.2.0" "@aws-crypto/crc32c" "5.2.0" "@aws-crypto/util" "5.2.0" - "@aws-sdk/core" "3.901.0" - "@aws-sdk/types" "3.901.0" + "@aws-sdk/core" "^3.973.6" + "@aws-sdk/crc64-nvme" "3.972.0" + "@aws-sdk/types" "^3.973.1" "@smithy/is-array-buffer" "^4.2.0" - "@smithy/node-config-provider" "^4.3.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/node-config-provider" "^4.3.8" + "@smithy/protocol-http" "^5.3.8" + "@smithy/types" "^4.12.0" + "@smithy/util-middleware" "^4.2.8" + "@smithy/util-stream" "^4.5.10" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@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== +"@aws-sdk/middleware-host-header@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.3.tgz#47c161dec62d89c66c89f4d17ff4434021e04af5" + integrity sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA== dependencies: - "@aws-sdk/types" "3.901.0" - "@smithy/protocol-http" "^5.3.0" - "@smithy/types" "^4.6.0" + "@aws-sdk/types" "^3.973.1" + "@smithy/protocol-http" "^5.3.8" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@aws-sdk/middleware-location-constraint@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.3.tgz#b4f504f75baa19064b7457e5c6e3c8cecb4c32eb" + integrity sha512-nIg64CVrsXp67vbK0U1/Is8rik3huS3QkRHn2DRDx4NldrEFMgdkZGI/+cZMKD9k4YOS110Dfu21KZLHrFA/1g== dependencies: - "@aws-sdk/types" "3.901.0" - "@smithy/types" "^4.6.0" + "@aws-sdk/types" "^3.973.1" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@aws-sdk/middleware-logger@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.972.3.tgz#ef1afd4a0b70fe72cf5f7c817f82da9f35c7e836" + integrity sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA== dependencies: - "@aws-sdk/types" "3.901.0" - "@smithy/types" "^4.6.0" + "@aws-sdk/types" "^3.973.1" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@aws-sdk/middleware-recursion-detection@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.3.tgz#5b95dcecff76a0d2963bd954bdef87700d1b1c8c" + integrity sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q== dependencies: - "@aws-sdk/types" "3.901.0" - "@aws/lambda-invoke-store" "^0.0.1" - "@smithy/protocol-http" "^5.3.0" - "@smithy/types" "^4.6.0" + "@aws-sdk/types" "^3.973.1" + "@aws/lambda-invoke-store" "^0.2.2" + "@smithy/protocol-http" "^5.3.8" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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" +"@aws-sdk/middleware-sdk-s3@^3.972.6": + version "3.972.6" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.6.tgz#d3ca2f65298c6a1076f0244833f11019f5766cb2" + integrity sha512-Xq7wM6kbgJN1UO++8dvH/efPb1nTwWqFCpZCR7RCLOETP7xAUAhVo7JmsCnML5Di/iC4Oo5VrJ4QmkYcMZniLw== + dependencies: + "@aws-sdk/core" "^3.973.6" + "@aws-sdk/types" "^3.973.1" + "@aws-sdk/util-arn-parser" "^3.972.2" + "@smithy/core" "^3.22.0" + "@smithy/node-config-provider" "^4.3.8" + "@smithy/protocol-http" "^5.3.8" + "@smithy/signature-v4" "^5.3.8" + "@smithy/smithy-client" "^4.11.1" + "@smithy/types" "^4.12.0" "@smithy/util-config-provider" "^4.2.0" - "@smithy/util-middleware" "^4.2.0" - "@smithy/util-stream" "^4.4.0" + "@smithy/util-middleware" "^4.2.8" + "@smithy/util-stream" "^4.5.10" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@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== +"@aws-sdk/middleware-ssec@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.3.tgz#4f81d310fd91164e6e18ba3adab6bcf906920333" + integrity sha512-dU6kDuULN3o3jEHcjm0c4zWJlY1zWVkjG9NPe9qxYLLpcbdj5kRYBS2DdWYD+1B9f910DezRuws7xDEqKkHQIg== dependencies: - "@aws-sdk/types" "3.901.0" - "@smithy/types" "^4.6.0" + "@aws-sdk/types" "^3.973.1" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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" +"@aws-sdk/middleware-user-agent@^3.972.6": + version "3.972.6" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.6.tgz#8abf3fae980f80834460d3345937e5843a59082d" + integrity sha512-TehLN8W/kivl0U9HcS+keryElEWORROpghDXZBLfnb40DXM7hx/i+7OOjkogXQOF3QtUraJVRkHQ07bPhrWKlw== + dependencies: + "@aws-sdk/core" "^3.973.6" + "@aws-sdk/types" "^3.973.1" + "@aws-sdk/util-endpoints" "3.982.0" + "@smithy/core" "^3.22.0" + "@smithy/protocol-http" "^5.3.8" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@aws-sdk/nested-clients@3.982.0": + version "3.982.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.982.0.tgz#b7d50bb7c273ed688fab0e52d5430dc6b0167d6d" + integrity sha512-VVkaH27digrJfdVrT64rjkllvOp4oRiZuuJvrylLXAKl18ujToJR7AqpDldL/LS63RVne3QWIpkygIymxFtliQ== dependencies: "@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" + "@aws-sdk/core" "^3.973.6" + "@aws-sdk/middleware-host-header" "^3.972.3" + "@aws-sdk/middleware-logger" "^3.972.3" + "@aws-sdk/middleware-recursion-detection" "^3.972.3" + "@aws-sdk/middleware-user-agent" "^3.972.6" + "@aws-sdk/region-config-resolver" "^3.972.3" + "@aws-sdk/types" "^3.973.1" + "@aws-sdk/util-endpoints" "3.982.0" + "@aws-sdk/util-user-agent-browser" "^3.972.3" + "@aws-sdk/util-user-agent-node" "^3.972.4" + "@smithy/config-resolver" "^4.4.6" + "@smithy/core" "^3.22.0" + "@smithy/fetch-http-handler" "^5.3.9" + "@smithy/hash-node" "^4.2.8" + "@smithy/invalid-dependency" "^4.2.8" + "@smithy/middleware-content-length" "^4.2.8" + "@smithy/middleware-endpoint" "^4.4.12" + "@smithy/middleware-retry" "^4.4.29" + "@smithy/middleware-serde" "^4.2.9" + "@smithy/middleware-stack" "^4.2.8" + "@smithy/node-config-provider" "^4.3.8" + "@smithy/node-http-handler" "^4.4.8" + "@smithy/protocol-http" "^5.3.8" + "@smithy/smithy-client" "^4.11.1" + "@smithy/types" "^4.12.0" + "@smithy/url-parser" "^4.2.8" + "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" - "@smithy/util-body-length-node" "^4.2.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-body-length-node" "^4.2.1" + "@smithy/util-defaults-mode-browser" "^4.3.28" + "@smithy/util-defaults-mode-node" "^4.2.31" + "@smithy/util-endpoints" "^3.2.8" + "@smithy/util-middleware" "^4.2.8" + "@smithy/util-retry" "^4.2.8" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@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== +"@aws-sdk/region-config-resolver@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.3.tgz#25af64235ca6f4b6b21f85d4b3c0b432efc4ae04" + integrity sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow== dependencies: - "@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" + "@aws-sdk/types" "^3.973.1" + "@smithy/config-resolver" "^4.4.6" + "@smithy/node-config-provider" "^4.3.8" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@aws-sdk/signature-v4-multi-region@3.984.0": + version "3.984.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.984.0.tgz#ddcd91add853425f818e2478f13158fb6545ee27" + integrity sha512-TaWbfYCwnuOSvDSrgs7QgoaoXse49E7LzUkVOUhoezwB7bkmhp+iojADm7UepCEu4021SquD7NG1xA+WCvmldA== dependencies: - "@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" + "@aws-sdk/middleware-sdk-s3" "^3.972.6" + "@aws-sdk/types" "^3.973.1" + "@smithy/protocol-http" "^5.3.8" + "@smithy/signature-v4" "^5.3.8" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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" +"@aws-sdk/token-providers@3.982.0": + version "3.982.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.982.0.tgz#c7a65d4f286c69ef10918fe7758bbe8dc7a064e9" + integrity sha512-v3M0KYp2TVHYHNBT7jHD9lLTWAdS9CaWJ2jboRKt0WAB65bA7iUEpR+k4VqKYtpQN4+8kKSc4w+K6kUNZkHKQw== + dependencies: + "@aws-sdk/core" "^3.973.6" + "@aws-sdk/nested-clients" "3.982.0" + "@aws-sdk/types" "^3.973.1" + "@smithy/property-provider" "^4.2.8" + "@smithy/shared-ini-file-loader" "^4.4.3" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@aws-sdk/types@^3.222.0", "@aws-sdk/types@^3.973.1": + version "3.973.1" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.973.1.tgz#1b2992ec6c8380c3e74c9bd2c74703e9a807d6e0" + integrity sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg== dependencies: - "@smithy/types" "^4.6.0" + "@smithy/types" "^4.12.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== +"@aws-sdk/util-arn-parser@^3.972.2": + version "3.972.2" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.2.tgz#ef18ba889e8ef35f083f1e962018bc0ce70acef3" + integrity sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg== dependencies: - "@smithy/types" "^3.7.1" tslib "^2.6.2" -"@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== +"@aws-sdk/util-dynamodb@^3.901.0": + version "3.984.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-dynamodb/-/util-dynamodb-3.984.0.tgz#45388393feaf87b643d1e91f140ac7554aba0130" + integrity sha512-Yg3MbD3UJr/lKCtplbKxcCVxn2aaNURNB3ahaCaTAddMWLKik7aswW9zfZw1LQXuUHQeIBv27votDbp2YYY4aw== dependencies: tslib "^2.6.2" -"@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== +"@aws-sdk/util-endpoints@3.982.0": + version "3.982.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.982.0.tgz#65674c566a8aa2d35b27dcd4132873e75f58dc76" + integrity sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ== dependencies: + "@aws-sdk/types" "^3.973.1" + "@smithy/types" "^4.12.0" + "@smithy/url-parser" "^4.2.8" + "@smithy/util-endpoints" "^3.2.8" tslib "^2.6.2" -"@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== +"@aws-sdk/util-endpoints@3.984.0": + version "3.984.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.984.0.tgz#a2cb22dbeee710be9558e8678370a44b75bbf162" + integrity sha512-9ebjLA0hMKHeVvXEtTDCCOBtwjb0bOXiuUV06HNeVdgAjH6gj4x4Zwt4IBti83TiyTGOCl5YfZqGx4ehVsasbQ== dependencies: - "@aws-sdk/types" "3.901.0" - "@smithy/types" "^4.6.0" - "@smithy/url-parser" "^4.2.0" - "@smithy/util-endpoints" "^3.2.0" + "@aws-sdk/types" "^3.973.1" + "@smithy/types" "^4.12.0" + "@smithy/url-parser" "^4.2.8" + "@smithy/util-endpoints" "^3.2.8" tslib "^2.6.2" "@aws-sdk/util-locate-window@^3.0.0": - version "3.693.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.693.0.tgz" - integrity sha512-ttrag6haJLWABhLqtg1Uf+4LgHWIMOVSYL+VYZmAp2v4PUGOwWmWQH0Zk8RM7YuQcLfH/EoR72/Yxz6A4FKcuw== + version "3.965.4" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-locate-window/-/util-locate-window-3.965.4.tgz#f62d279e1905f6939b6dffb0f76ab925440f72bf" + integrity sha512-H1onv5SkgPBK2P6JR2MjGgbOnttoNzSPIRoeZTNPZYyaplwGg50zS3amXvXqF0/qfXpWEC9rLWU564QTB9bSog== dependencies: tslib "^2.6.2" -"@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== +"@aws-sdk/util-user-agent-browser@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.3.tgz#1363b388cb3af86c5322ef752c0cf8d7d25efa8a" + integrity sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw== dependencies: - "@aws-sdk/types" "3.901.0" - "@smithy/types" "^4.6.0" + "@aws-sdk/types" "^3.973.1" + "@smithy/types" "^4.12.0" bowser "^2.11.0" tslib "^2.6.2" -"@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== +"@aws-sdk/util-user-agent-node@^3.972.4": + version "3.972.4" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.4.tgz#35cf669fa3e77973422da5a1df50b79b41d460b3" + integrity sha512-3WFCBLiM8QiHDfosQq3Py+lIMgWlFWwFQliUHUqwEiRqLnKyhgbU3AKa7AWJF7lW2Oc/2kFNY4MlAYVnVc0i8A== dependencies: - "@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" + "@aws-sdk/middleware-user-agent" "^3.972.6" + "@aws-sdk/types" "^3.973.1" + "@smithy/node-config-provider" "^4.3.8" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@aws-sdk/xml-builder@^3.972.4": + version "3.972.4" + resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.972.4.tgz#8115c8cf90c71cf484a52c82eac5344cd3a5e921" + integrity sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q== dependencies: - "@smithy/types" "^4.6.0" - fast-xml-parser "5.2.5" + "@smithy/types" "^4.12.0" + fast-xml-parser "5.3.4" tslib "^2.6.2" -"@aws/lambda-invoke-store@0.2.2": - version "0.2.2" - resolved "https://registry.yarnpkg.com/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.2.tgz#b00f7d6aedfe832ef6c84488f3a422cce6a47efa" - integrity sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg== - -"@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== +"@aws/lambda-invoke-store@0.2.3", "@aws/lambda-invoke-store@^0.2.2": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz#f1137f56209ccc69c15f826242cbf37f828617dd" + integrity sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw== -"@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" - integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.27.1", "@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" + integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== dependencies: - "@babel/helper-validator-identifier" "^7.25.9" + "@babel/helper-validator-identifier" "^7.28.5" js-tokens "^4.0.0" - picocolors "^1.0.0" + picocolors "^1.1.1" -"@babel/compat-data@^7.25.9": - version "7.26.3" - resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz" - integrity sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g== +"@babel/compat-data@^7.28.6": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.0.tgz#00d03e8c0ac24dd9be942c5370990cbe1f17d88d" + integrity sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg== "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9": - version "7.26.0" - resolved "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz" - 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" + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.0.tgz#5286ad785df7f79d656e88ce86e650d16ca5f322" + integrity sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helpers" "^7.28.6" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.29.0" + "@babel/types" "^7.29.0" + "@jridgewell/remapping" "^2.3.5" 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.npmjs.org/@babel/generator/-/generator-7.26.3.tgz" - integrity sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ== +"@babel/generator@^7.29.0", "@babel/generator@^7.7.2": + version "7.29.1" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.1.tgz#d09876290111abbb00ef962a7b83a5307fba0d50" + integrity sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw== dependencies: - "@babel/parser" "^7.26.3" - "@babel/types" "^7.26.3" - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.25" + "@babel/parser" "^7.29.0" + "@babel/types" "^7.29.0" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" -"@babel/helper-compilation-targets@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz" - integrity sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ== +"@babel/helper-compilation-targets@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz#32c4a3f41f12ed1532179b108a4d746e105c2b25" + integrity sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA== dependencies: - "@babel/compat-data" "^7.25.9" - "@babel/helper-validator-option" "^7.25.9" + "@babel/compat-data" "^7.28.6" + "@babel/helper-validator-option" "^7.27.1" 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.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz" - integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== +"@babel/helper-globals@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" + integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== + +"@babel/helper-module-imports@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz#60632cbd6ffb70b22823187201116762a03e2d5c" + integrity sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw== dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" + "@babel/traverse" "^7.28.6" + "@babel/types" "^7.28.6" -"@babel/helper-module-transforms@^7.26.0": - version "7.26.0" - resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz" - integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== +"@babel/helper-module-transforms@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz#9312d9d9e56edc35aeb6e95c25d4106b50b9eb1e" + integrity sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA== dependencies: - "@babel/helper-module-imports" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" - "@babel/traverse" "^7.25.9" + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-validator-identifier" "^7.28.5" + "@babel/traverse" "^7.28.6" -"@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.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz" - integrity sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw== +"@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.28.6", "@babel/helper-plugin-utils@^7.8.0": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz#6f13ea251b68c8532e985fd532f28741a8af9ac8" + integrity sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug== -"@babel/helper-string-parser@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz" - integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== -"@babel/helper-validator-identifier@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz" - integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== +"@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== -"@babel/helper-validator-option@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz" - integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== -"@babel/helpers@^7.26.0": - version "7.26.0" - resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz" - integrity sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw== +"@babel/helpers@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.6.tgz#fca903a313ae675617936e8998b814c415cbf5d7" + integrity sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw== dependencies: - "@babel/template" "^7.25.9" - "@babel/types" "^7.26.0" + "@babel/template" "^7.28.6" + "@babel/types" "^7.28.6" -"@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.npmjs.org/@babel/parser/-/parser-7.26.3.tgz" - integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA== +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.28.6", "@babel/parser@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.0.tgz#669ef345add7d057e92b7ed15f0bac07611831b6" + integrity sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww== dependencies: - "@babel/types" "^7.26.3" + "@babel/types" "^7.29.0" "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz" + 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.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz" + 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.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz" + 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.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz" + 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.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz" - integrity sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A== + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz#b71d5914665f60124e133696f17cd7669062c503" + integrity sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/plugin-syntax-import-meta@^7.10.4": version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz" + 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.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz" + 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.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz" - integrity sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA== + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz#f8ca28bbd84883b5fea0e447c635b81ba73997ee" + integrity sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz" + 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.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz" + 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.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz" + 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.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz" + 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.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz" + 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.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz" + 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.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz" + 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.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz" + 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.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz" - 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.npmjs.org/@babel/template/-/template-7.25.9.tgz" - 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.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz" - 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" + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz#c7b2ddf1d0a811145b1de800d1abd146af92e3a2" + integrity sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/template@^7.28.6", "@babel/template@^7.3.3": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57" + integrity sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ== + dependencies: + "@babel/code-frame" "^7.28.6" + "@babel/parser" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a" + integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/types" "^7.29.0" debug "^4.3.1" - 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.npmjs.org/@babel/types/-/types-7.26.3.tgz" - integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA== +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.28.2", "@babel/types@^7.28.6", "@babel/types@^7.29.0", "@babel/types@^7.3.3": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7" + integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A== dependencies: - "@babel/helper-string-parser" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" "@bcoe/v8-coverage@^0.2.3": version "0.2.3" - resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" + 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.npmjs.org/@colors/colors/-/colors-1.6.0.tgz" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== "@csg-org/block-avatar@^0.0.11": @@ -1074,292 +1101,309 @@ "@cspotcode/source-map-support@^0.8.0": version "0.8.1" - resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@dabh/diagnostics@^2.0.2": - version "2.0.3" - resolved "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz" - integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA== +"@dabh/diagnostics@^2.0.8": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.8.tgz#ead97e72ca312cf0e6dd7af0d300b58993a31a5e" + integrity sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q== dependencies: - colorspace "1.1.x" + "@so-ric/colorspace" "^1.1.6" enabled "2.0.x" kuler "^2.0.0" -"@esbuild/aix-ppc64@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz#51299374de171dbd80bb7d838e1cfce9af36f353" - integrity sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ== - "@esbuild/aix-ppc64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz#b57697945b50e99007b4c2521507dc613d4a648c" integrity sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw== -"@esbuild/android-arm64@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz#58565291a1fe548638adb9c584237449e5e14018" - integrity sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw== +"@esbuild/aix-ppc64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz#815b39267f9bffd3407ea6c376ac32946e24f8d2" + integrity sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg== "@esbuild/android-arm64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz#1add7e0af67acefd556e407f8497e81fddad79c0" integrity sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w== -"@esbuild/android-arm@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.23.1.tgz#5eb8c652d4c82a2421e3395b808e6d9c42c862ee" - integrity sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ== +"@esbuild/android-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz#19b882408829ad8e12b10aff2840711b2da361e8" + integrity sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg== "@esbuild/android-arm@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.0.tgz#ab7263045fa8e090833a8e3c393b60d59a789810" integrity sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew== -"@esbuild/android-x64@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.23.1.tgz#ae19d665d2f06f0f48a6ac9a224b3f672e65d517" - integrity sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg== +"@esbuild/android-arm@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.3.tgz#90be58de27915efa27b767fcbdb37a4470627d7b" + integrity sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA== "@esbuild/android-x64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.0.tgz#e8f8b196cfdfdd5aeaebbdb0110983460440e705" integrity sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ== -"@esbuild/darwin-arm64@0.23.1": - version "0.23.1" - resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz" - integrity sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q== +"@esbuild/android-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.3.tgz#d7dcc976f16e01a9aaa2f9b938fbec7389f895ac" + integrity sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ== "@esbuild/darwin-arm64@0.24.0": version "0.24.0" - resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz#2d0d9414f2acbffd2d86e98253914fca603a53dd" integrity sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw== -"@esbuild/darwin-x64@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz#c58353b982f4e04f0d022284b8ba2733f5ff0931" - integrity sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw== +"@esbuild/darwin-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz#9f6cac72b3a8532298a6a4493ed639a8988e8abd" + integrity sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg== "@esbuild/darwin-x64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz#33087aab31a1eb64c89daf3d2cf8ce1775656107" integrity sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA== -"@esbuild/freebsd-arm64@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz#f9220dc65f80f03635e1ef96cfad5da1f446f3bc" - integrity sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA== +"@esbuild/darwin-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz#ac61d645faa37fd650340f1866b0812e1fb14d6a" + integrity sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg== "@esbuild/freebsd-arm64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz#bb76e5ea9e97fa3c753472f19421075d3a33e8a7" integrity sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA== -"@esbuild/freebsd-x64@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz#69bd8511fa013b59f0226d1609ac43f7ce489730" - integrity sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g== +"@esbuild/freebsd-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz#b8625689d73cf1830fe58c39051acdc12474ea1b" + integrity sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w== "@esbuild/freebsd-x64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz#e0e2ce9249fdf6ee29e5dc3d420c7007fa579b93" integrity sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ== -"@esbuild/linux-arm64@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz#8050af6d51ddb388c75653ef9871f5ccd8f12383" - integrity sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g== +"@esbuild/freebsd-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz#07be7dd3c9d42fe0eccd2ab9f9ded780bc53bead" + integrity sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA== "@esbuild/linux-arm64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz#d1b2aa58085f73ecf45533c07c82d81235388e75" integrity sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g== -"@esbuild/linux-arm@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz#ecaabd1c23b701070484990db9a82f382f99e771" - integrity sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ== +"@esbuild/linux-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz#bf31918fe5c798586460d2b3d6c46ed2c01ca0b6" + integrity sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg== "@esbuild/linux-arm@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz#8e4915df8ea3e12b690a057e77a47b1d5935ef6d" integrity sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw== -"@esbuild/linux-ia32@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz#3ed2273214178109741c09bd0687098a0243b333" - integrity sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ== +"@esbuild/linux-arm@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz#28493ee46abec1dc3f500223cd9f8d2df08f9d11" + integrity sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw== "@esbuild/linux-ia32@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz#8200b1110666c39ab316572324b7af63d82013fb" integrity sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA== -"@esbuild/linux-loong64@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz#a0fdf440b5485c81b0fbb316b08933d217f5d3ac" - integrity sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw== +"@esbuild/linux-ia32@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz#750752a8b30b43647402561eea764d0a41d0ee29" + integrity sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg== "@esbuild/linux-loong64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz#6ff0c99cf647504df321d0640f0d32e557da745c" integrity sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g== -"@esbuild/linux-mips64el@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz#e11a2806346db8375b18f5e104c5a9d4e81807f6" - integrity sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q== +"@esbuild/linux-loong64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz#a5a92813a04e71198c50f05adfaf18fc1e95b9ed" + integrity sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA== "@esbuild/linux-mips64el@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz#3f720ccd4d59bfeb4c2ce276a46b77ad380fa1f3" integrity sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA== -"@esbuild/linux-ppc64@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz#06a2744c5eaf562b1a90937855b4d6cf7c75ec96" - integrity sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw== +"@esbuild/linux-mips64el@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz#deb45d7fd2d2161eadf1fbc593637ed766d50bb1" + integrity sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw== "@esbuild/linux-ppc64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz#9d6b188b15c25afd2e213474bf5f31e42e3aa09e" integrity sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ== -"@esbuild/linux-riscv64@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz#65b46a2892fc0d1af4ba342af3fe0fa4a8fe08e7" - integrity sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA== +"@esbuild/linux-ppc64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz#6f39ae0b8c4d3d2d61a65b26df79f6e12a1c3d78" + integrity sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA== "@esbuild/linux-riscv64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz#f989fdc9752dfda286c9cd87c46248e4dfecbc25" integrity sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw== -"@esbuild/linux-s390x@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz#e71ea18c70c3f604e241d16e4e5ab193a9785d6f" - integrity sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw== +"@esbuild/linux-riscv64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz#4c5c19c3916612ec8e3915187030b9df0b955c1d" + integrity sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ== "@esbuild/linux-s390x@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz#29ebf87e4132ea659c1489fce63cd8509d1c7319" integrity sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g== -"@esbuild/linux-x64@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz#d47f97391e80690d4dfe811a2e7d6927ad9eed24" - integrity sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ== +"@esbuild/linux-s390x@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz#9ed17b3198fa08ad5ccaa9e74f6c0aff7ad0156d" + integrity sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw== "@esbuild/linux-x64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz#4af48c5c0479569b1f359ffbce22d15f261c0cef" integrity sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA== -"@esbuild/netbsd-x64@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz#44e743c9778d57a8ace4b72f3c6b839a3b74a653" - integrity sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA== +"@esbuild/linux-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz#12383dcbf71b7cf6513e58b4b08d95a710bf52a5" + integrity sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA== + +"@esbuild/netbsd-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz#dd0cb2fa543205fcd931df44f4786bfcce6df7d7" + integrity sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA== "@esbuild/netbsd-x64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz#1ae73d23cc044a0ebd4f198334416fb26c31366c" integrity sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg== -"@esbuild/openbsd-arm64@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz#05c5a1faf67b9881834758c69f3e51b7dee015d7" - integrity sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q== +"@esbuild/netbsd-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz#028ad1807a8e03e155153b2d025b506c3787354b" + integrity sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA== "@esbuild/openbsd-arm64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz#5d904a4f5158c89859fd902c427f96d6a9e632e2" integrity sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg== -"@esbuild/openbsd-x64@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz#2e58ae511bacf67d19f9f2dcd9e8c5a93f00c273" - integrity sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA== +"@esbuild/openbsd-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz#e3c16ff3490c9b59b969fffca87f350ffc0e2af5" + integrity sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw== "@esbuild/openbsd-x64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz#4c8aa88c49187c601bae2971e71c6dc5e0ad1cdf" integrity sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q== -"@esbuild/sunos-x64@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz#adb022b959d18d3389ac70769cef5a03d3abd403" - integrity sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA== +"@esbuild/openbsd-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz#c5a4693fcb03d1cbecbf8b422422468dfc0d2a8b" + integrity sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ== + +"@esbuild/openharmony-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz#082082444f12db564a0775a41e1991c0e125055e" + integrity sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g== "@esbuild/sunos-x64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz#8ddc35a0ea38575fa44eda30a5ee01ae2fa54dd4" integrity sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA== -"@esbuild/win32-arm64@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz#84906f50c212b72ec360f48461d43202f4c8b9a2" - integrity sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A== +"@esbuild/sunos-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz#5ab036c53f929e8405c4e96e865a424160a1b537" + integrity sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA== "@esbuild/win32-arm64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz#6e79c8543f282c4539db684a207ae0e174a9007b" integrity sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA== -"@esbuild/win32-ia32@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz#5e3eacc515820ff729e90d0cb463183128e82fac" - integrity sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ== +"@esbuild/win32-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz#38de700ef4b960a0045370c171794526e589862e" + integrity sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA== "@esbuild/win32-ia32@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz#057af345da256b7192d18b676a02e95d0fa39103" integrity sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw== -"@esbuild/win32-x64@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz#81fd50d11e2c32b2d6241470e3185b70c7b30699" - integrity sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg== +"@esbuild/win32-ia32@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz#451b93dc03ec5d4f38619e6cd64d9f9eff06f55c" + integrity sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q== "@esbuild/win32-x64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz#168ab1c7e1c318b922637fad8f339d48b01e1244" integrity sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA== -"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": - version "4.4.1" - resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz" - integrity sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA== +"@esbuild/win32-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz#0eaf705c941a218a43dba8e09f1df1d6cd2f1f17" + integrity sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA== + +"@eslint-community/eslint-utils@^4.8.0", "@eslint-community/eslint-utils@^4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" + integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== dependencies: eslint-visitor-keys "^3.4.3" -"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.1": - version "4.12.1" - resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz" - integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== +"@eslint-community/regexpp@^4.12.1", "@eslint-community/regexpp@^4.12.2": + version "4.12.2" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" + integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== -"@eslint/config-array@^0.19.0": - version "0.19.1" - resolved "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz" - integrity sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA== +"@eslint/config-array@^0.21.1": + version "0.21.1" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.1.tgz#7d1b0060fea407f8301e932492ba8c18aff29713" + integrity sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA== dependencies: - "@eslint/object-schema" "^2.1.5" + "@eslint/object-schema" "^2.1.7" debug "^4.3.1" minimatch "^3.1.2" -"@eslint/core@^0.9.0": - version "0.9.1" - resolved "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz" - integrity sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q== +"@eslint/config-helpers@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz#1bd006ceeb7e2e55b2b773ab318d300e1a66aeda" + integrity sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw== + dependencies: + "@eslint/core" "^0.17.0" + +"@eslint/core@^0.17.0": + version "0.17.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.17.0.tgz#77225820413d9617509da9342190a2019e78761c" + integrity sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ== dependencies: "@types/json-schema" "^7.0.15" -"@eslint/eslintrc@^3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz" - integrity sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w== +"@eslint/eslintrc@^3.3.1": + version "3.3.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.3.tgz#26393a0806501b5e2b6a43aa588a4d8df67880ac" + integrity sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -1367,58 +1411,54 @@ globals "^14.0.0" ignore "^5.2.0" import-fresh "^3.2.1" - js-yaml "^4.1.0" + js-yaml "^4.1.1" minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@9.16.0": - version "9.16.0" - resolved "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz" - integrity sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg== +"@eslint/js@9.39.2": + version "9.39.2" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.2.tgz#2d4b8ec4c3ea13c1b3748e0c97ecd766bdd80599" + integrity sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA== -"@eslint/object-schema@^2.1.5": - version "2.1.5" - resolved "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz" - integrity sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ== +"@eslint/object-schema@^2.1.7": + version "2.1.7" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.7.tgz#6e2126a1347e86a4dedf8706ec67ff8e107ebbad" + integrity sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA== -"@eslint/plugin-kit@^0.2.3": - version "0.2.4" - resolved "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz" - integrity sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg== +"@eslint/plugin-kit@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz#9779e3fd9b7ee33571a57435cf4335a1794a6cb2" + integrity sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA== dependencies: + "@eslint/core" "^0.17.0" levn "^0.4.1" "@humanfs/core@^0.19.1": version "0.19.1" - resolved "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz" + resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== "@humanfs/node@^0.16.6": - version "0.16.6" - resolved "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz" - integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw== + version "0.16.7" + resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.7.tgz#822cb7b3a12c5a240a24f621b5a2413e27a45f26" + integrity sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ== dependencies: "@humanfs/core" "^0.19.1" - "@humanwhocodes/retry" "^0.3.0" + "@humanwhocodes/retry" "^0.4.0" "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" - resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/retry@^0.3.0": - version "0.3.1" - resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz" - integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA== - -"@humanwhocodes/retry@^0.4.1": - version "0.4.1" - resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz" - integrity sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA== +"@humanwhocodes/retry@^0.4.0", "@humanwhocodes/retry@^0.4.2": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" + integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== "@isaacs/cliui@^8.0.2": version "8.0.2" - resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== dependencies: string-width "^5.1.2" @@ -1430,7 +1470,7 @@ "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" - resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz" + 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" @@ -1441,12 +1481,12 @@ "@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": version "0.1.3" - resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz" + 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.npmjs.org/@jest/console/-/console-29.7.0.tgz" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.7.0.tgz#cd4822dbdb84529265c5a2bdb529a3c9cc950ffc" integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== dependencies: "@jest/types" "^29.6.3" @@ -1458,7 +1498,7 @@ "@jest/core@^29.7.0": version "29.7.0" - resolved "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.7.0.tgz#b6cccc239f30ff36609658c5a5e2291757ce448f" integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== dependencies: "@jest/console" "^29.7.0" @@ -1490,9 +1530,14 @@ slash "^3.0.0" strip-ansi "^6.0.0" +"@jest/diff-sequences@30.0.1": + version "30.0.1" + resolved "https://registry.yarnpkg.com/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz#0ededeae4d071f5c8ffe3678d15f3a1be09156be" + integrity sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw== + "@jest/environment@^29.7.0": version "29.7.0" - resolved "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz" + 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" @@ -1500,16 +1545,23 @@ "@types/node" "*" jest-mock "^29.7.0" +"@jest/expect-utils@30.2.0": + version "30.2.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-30.2.0.tgz#4f95413d4748454fdb17404bf1141827d15e6011" + integrity sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA== + dependencies: + "@jest/get-type" "30.1.0" + "@jest/expect-utils@^29.7.0": version "29.7.0" - resolved "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== dependencies: jest-get-type "^29.6.3" "@jest/expect@^29.7.0": version "29.7.0" - resolved "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.7.0.tgz#76a3edb0cb753b70dfbfe23283510d3d45432bf2" integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== dependencies: expect "^29.7.0" @@ -1517,7 +1569,7 @@ "@jest/fake-timers@^29.7.0": version "29.7.0" - resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz" + 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" @@ -1527,9 +1579,14 @@ jest-mock "^29.7.0" jest-util "^29.7.0" +"@jest/get-type@30.1.0": + version "30.1.0" + resolved "https://registry.yarnpkg.com/@jest/get-type/-/get-type-30.1.0.tgz#4fcb4dc2ebcf0811be1c04fd1cb79c2dba431cbc" + integrity sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA== + "@jest/globals@^29.7.0": version "29.7.0" - resolved "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.7.0.tgz#8d9290f9ec47ff772607fa864ca1d5a2efae1d4d" integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== dependencies: "@jest/environment" "^29.7.0" @@ -1537,9 +1594,17 @@ "@jest/types" "^29.6.3" jest-mock "^29.7.0" +"@jest/pattern@30.0.1": + version "30.0.1" + resolved "https://registry.yarnpkg.com/@jest/pattern/-/pattern-30.0.1.tgz#d5304147f49a052900b4b853dedb111d080e199f" + integrity sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA== + dependencies: + "@types/node" "*" + jest-regex-util "30.0.1" + "@jest/reporters@^29.7.0": version "29.7.0" - resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.7.0.tgz#04b262ecb3b8faa83b0b3d321623972393e8f4c7" integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== dependencies: "@bcoe/v8-coverage" "^0.2.3" @@ -1567,16 +1632,23 @@ strip-ansi "^6.0.0" v8-to-istanbul "^9.0.1" +"@jest/schemas@30.0.5": + version "30.0.5" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-30.0.5.tgz#7bdf69fc5a368a5abdb49fd91036c55225846473" + integrity sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA== + dependencies: + "@sinclair/typebox" "^0.34.0" + "@jest/schemas@^29.6.3": version "29.6.3" - resolved "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== dependencies: "@sinclair/typebox" "^0.27.8" "@jest/source-map@^29.6.3": version "29.6.3" - resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz" + 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" @@ -1585,7 +1657,7 @@ "@jest/test-result@^29.7.0": version "29.7.0" - resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz" + 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" @@ -1595,7 +1667,7 @@ "@jest/test-sequencer@^29.7.0": version "29.7.0" - resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz" + 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" @@ -1605,7 +1677,7 @@ "@jest/transform@^29.7.0": version "29.7.0" - resolved "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== dependencies: "@babel/core" "^7.11.6" @@ -1624,9 +1696,22 @@ slash "^3.0.0" write-file-atomic "^4.0.2" +"@jest/types@30.2.0": + version "30.2.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-30.2.0.tgz#1c678a7924b8f59eafd4c77d56b6d0ba976d62b8" + integrity sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg== + dependencies: + "@jest/pattern" "30.0.1" + "@jest/schemas" "30.0.5" + "@types/istanbul-lib-coverage" "^2.0.6" + "@types/istanbul-reports" "^3.0.4" + "@types/node" "*" + "@types/yargs" "^17.0.33" + chalk "^4.1.2" + "@jest/types@^29.6.3": version "29.6.3" - resolved "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== dependencies: "@jest/schemas" "^29.6.3" @@ -1636,133 +1721,118 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" -"@jridgewell/gen-mapping@^0.3.5": - version "0.3.5" - resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz" - integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== +"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== dependencies: - "@jridgewell/set-array" "^1.2.1" - "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" "@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" - resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" + 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.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz" - 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.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz" - integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== "@jridgewell/trace-mapping@0.3.9": version "0.3.9" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== dependencies: "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@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.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz" - integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.31" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== dependencies: "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - "@pkgjs/parseargs@^0.11.0": version "0.11.0" - resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== "@sinclair/typebox@^0.27.8": - version "0.27.8" - resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz" - integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + version "0.27.10" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.10.tgz#beefe675f1853f73676aecc915b2bd2ac98c4fc6" + integrity sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA== + +"@sinclair/typebox@^0.34.0": + version "0.34.48" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.34.48.tgz#75b0ead87e59e1adbd6dccdc42bad4fddee73b59" + integrity sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA== "@sinonjs/commons@^3.0.0", "@sinonjs/commons@^3.0.1": version "3.0.1" - resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== dependencies: type-detect "4.0.8" "@sinonjs/fake-timers@11.2.2": version "11.2.2" - resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz#50063cc3574f4a27bd8453180a04171c85cc9699" integrity sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw== dependencies: "@sinonjs/commons" "^3.0.0" "@sinonjs/fake-timers@^10.0.2": version "10.3.0" - resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz" + 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.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz#36b9dbc21ad5546486ea9173d6bea063eb1717d5" integrity sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw== dependencies: "@sinonjs/commons" "^3.0.1" "@sinonjs/samsam@^8.0.0": - version "8.0.2" - resolved "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz" - integrity sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw== + version "8.0.3" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-8.0.3.tgz#eb6ffaef421e1e27783cc9b52567de20cb28072d" + integrity sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ== dependencies: "@sinonjs/commons" "^3.0.1" - lodash.get "^4.4.2" type-detect "^4.1.0" "@sinonjs/text-encoding@^0.7.3": version "0.7.3" - resolved "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz#282046f03e886e352b2d5f5da5eb755e01457f3f" integrity sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA== -"@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== +"@smithy/abort-controller@^4.2.8": + version "4.2.8" + resolved "https://registry.yarnpkg.com/@smithy/abort-controller/-/abort-controller-4.2.8.tgz#3bfd7a51acce88eaec9a65c3382542be9f3a053a" + integrity sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw== dependencies: - "@smithy/types" "^4.6.0" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@smithy/chunked-blob-reader-native@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.1.tgz#380266951d746b522b4ab2b16bfea6b451147b41" + integrity sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ== dependencies: - "@smithy/util-base64" "^4.2.0" + "@smithy/util-base64" "^4.3.0" tslib "^2.6.2" "@smithy/chunked-blob-reader@^5.2.0": @@ -1772,140 +1842,141 @@ dependencies: tslib "^2.6.2" -"@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== +"@smithy/config-resolver@^4.4.6": + version "4.4.6" + resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-4.4.6.tgz#bd7f65b3da93f37f1c97a399ade0124635c02297" + integrity sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ== dependencies: - "@smithy/node-config-provider" "^4.3.0" - "@smithy/types" "^4.6.0" + "@smithy/node-config-provider" "^4.3.8" + "@smithy/types" "^4.12.0" "@smithy/util-config-provider" "^4.2.0" - "@smithy/util-middleware" "^4.2.0" + "@smithy/util-endpoints" "^3.2.8" + "@smithy/util-middleware" "^4.2.8" tslib "^2.6.2" -"@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== +"@smithy/core@^3.22.0", "@smithy/core@^3.22.1": + version "3.22.1" + resolved "https://registry.yarnpkg.com/@smithy/core/-/core-3.22.1.tgz#c34180d541c9dc5d29412809a6aa497ea47d74f8" + integrity sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g== 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/middleware-serde" "^4.2.9" + "@smithy/protocol-http" "^5.3.8" + "@smithy/types" "^4.12.0" + "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" - "@smithy/util-middleware" "^4.2.0" - "@smithy/util-stream" "^4.4.0" + "@smithy/util-middleware" "^4.2.8" + "@smithy/util-stream" "^4.5.11" "@smithy/util-utf8" "^4.2.0" "@smithy/uuid" "^1.1.0" tslib "^2.6.2" -"@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== +"@smithy/credential-provider-imds@^4.2.8": + version "4.2.8" + resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.8.tgz#b2f4bf759ab1c35c0dd00fa3470263c749ebf60f" + integrity sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw== dependencies: - "@smithy/node-config-provider" "^4.3.0" - "@smithy/property-provider" "^4.2.0" - "@smithy/types" "^4.6.0" - "@smithy/url-parser" "^4.2.0" + "@smithy/node-config-provider" "^4.3.8" + "@smithy/property-provider" "^4.2.8" + "@smithy/types" "^4.12.0" + "@smithy/url-parser" "^4.2.8" tslib "^2.6.2" -"@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== +"@smithy/eventstream-codec@^4.2.8": + version "4.2.8" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-codec/-/eventstream-codec-4.2.8.tgz#2f431f4bac22e40aa6565189ea350c6fcb5efafd" + integrity sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw== dependencies: "@aws-crypto/crc32" "5.2.0" - "@smithy/types" "^4.6.0" + "@smithy/types" "^4.12.0" "@smithy/util-hex-encoding" "^4.2.0" tslib "^2.6.2" -"@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== +"@smithy/eventstream-serde-browser@^4.2.8": + version "4.2.8" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.8.tgz#04e2e1fad18e286d5595fbc0bff22e71251fca38" + integrity sha512-MTfQT/CRQz5g24ayXdjg53V0mhucZth4PESoA5IhvaWVDTOQLfo8qI9vzqHcPsdd2v6sqfTYqF5L/l+pea5Uyw== dependencies: - "@smithy/eventstream-serde-universal" "^4.2.0" - "@smithy/types" "^4.6.0" + "@smithy/eventstream-serde-universal" "^4.2.8" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@smithy/eventstream-serde-config-resolver@^4.3.8": + version "4.3.8" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.8.tgz#b913d23834c6ebf1646164893e1bec89dffe4f3b" + integrity sha512-ah12+luBiDGzBruhu3efNy1IlbwSEdNiw8fOZksoKoWW1ZHvO/04MQsdnws/9Aj+5b0YXSSN2JXKy/ClIsW8MQ== dependencies: - "@smithy/types" "^4.6.0" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@smithy/eventstream-serde-node@^4.2.8": + version "4.2.8" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.8.tgz#5f2dfa2cbb30bf7564c8d8d82a9832e9313f5243" + integrity sha512-cYpCpp29z6EJHa5T9WL0KAlq3SOKUQkcgSoeRfRVwjGgSFl7Uh32eYGt7IDYCX20skiEdRffyDpvF2efEZPC0A== dependencies: - "@smithy/eventstream-serde-universal" "^4.2.0" - "@smithy/types" "^4.6.0" + "@smithy/eventstream-serde-universal" "^4.2.8" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@smithy/eventstream-serde-universal@^4.2.8": + version "4.2.8" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.8.tgz#a62b389941c28a8c3ab44a0c8ba595447e0258a7" + integrity sha512-iJ6YNJd0bntJYnX6s52NC4WFYcZeKrPUr1Kmmr5AwZcwCSzVpS7oavAmxMR7pMq7V+D1G4s9F5NJK0xwOsKAlQ== dependencies: - "@smithy/eventstream-codec" "^4.2.0" - "@smithy/types" "^4.6.0" + "@smithy/eventstream-codec" "^4.2.8" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@smithy/fetch-http-handler@^5.3.9": + version "5.3.9" + resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.9.tgz#edfc9e90e0c7538c81e22e748d62c0066cc91d58" + integrity sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA== dependencies: - "@smithy/protocol-http" "^5.3.0" - "@smithy/querystring-builder" "^4.2.0" - "@smithy/types" "^4.6.0" - "@smithy/util-base64" "^4.2.0" + "@smithy/protocol-http" "^5.3.8" + "@smithy/querystring-builder" "^4.2.8" + "@smithy/types" "^4.12.0" + "@smithy/util-base64" "^4.3.0" tslib "^2.6.2" -"@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== +"@smithy/hash-blob-browser@^4.2.9": + version "4.2.9" + resolved "https://registry.yarnpkg.com/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.9.tgz#4f8e19b12b5a1000b7292b30f5ee237d32216af3" + integrity sha512-m80d/iicI7DlBDxyQP6Th7BW/ejDGiF0bgI754+tiwK0lgMkcaIBgvwwVc7OFbY4eUzpGtnig52MhPAEJ7iNYg== dependencies: "@smithy/chunked-blob-reader" "^5.2.0" - "@smithy/chunked-blob-reader-native" "^4.2.0" - "@smithy/types" "^4.6.0" + "@smithy/chunked-blob-reader-native" "^4.2.1" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@smithy/hash-node@^4.2.8": + version "4.2.8" + resolved "https://registry.yarnpkg.com/@smithy/hash-node/-/hash-node-4.2.8.tgz#c21eb055041716cd492dda3a109852a94b6d47bb" + integrity sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA== dependencies: - "@smithy/types" "^4.6.0" + "@smithy/types" "^4.12.0" "@smithy/util-buffer-from" "^4.2.0" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@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== +"@smithy/hash-stream-node@^4.2.8": + version "4.2.8" + resolved "https://registry.yarnpkg.com/@smithy/hash-stream-node/-/hash-stream-node-4.2.8.tgz#d541a31c714ac9c85ae9fec91559e81286707ddb" + integrity sha512-v0FLTXgHrTeheYZFGhR+ehX5qUm4IQsjAiL9qehad2cyjMWcN2QG6/4mSwbSgEQzI7jwfoXj7z4fxZUx/Mhj2w== dependencies: - "@smithy/types" "^4.6.0" + "@smithy/types" "^4.12.0" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@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== +"@smithy/invalid-dependency@^4.2.8": + version "4.2.8" + resolved "https://registry.yarnpkg.com/@smithy/invalid-dependency/-/invalid-dependency-4.2.8.tgz#c578bc6d5540c877aaed5034b986b5f6bd896451" + integrity sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ== dependencies: - "@smithy/types" "^4.6.0" + "@smithy/types" "^4.12.0" tslib "^2.6.2" "@smithy/is-array-buffer@^2.2.0": version "2.2.0" - resolved "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz" + resolved "https://registry.yarnpkg.com/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz#f84f0d9f9a36601a9ca9381688bd1b726fd39111" integrity sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA== dependencies: tslib "^2.6.2" @@ -1917,193 +1988,186 @@ dependencies: tslib "^2.6.2" -"@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== +"@smithy/md5-js@^4.2.8": + version "4.2.8" + resolved "https://registry.yarnpkg.com/@smithy/md5-js/-/md5-js-4.2.8.tgz#d354dbf9aea7a580be97598a581e35eef324ce22" + integrity sha512-oGMaLj4tVZzLi3itBa9TCswgMBr7k9b+qKYowQ6x1rTyTuO1IU2YHdHUa+891OsOH+wCsH7aTPRsTJO3RMQmjQ== dependencies: - "@smithy/types" "^4.6.0" + "@smithy/types" "^4.12.0" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@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== +"@smithy/middleware-content-length@^4.2.8": + version "4.2.8" + resolved "https://registry.yarnpkg.com/@smithy/middleware-content-length/-/middleware-content-length-4.2.8.tgz#82c1df578fa70fe5800cf305b8788b9d2836a3e4" + integrity sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A== dependencies: - "@smithy/protocol-http" "^5.3.0" - "@smithy/types" "^4.6.0" + "@smithy/protocol-http" "^5.3.8" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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" +"@smithy/middleware-endpoint@^4.4.12", "@smithy/middleware-endpoint@^4.4.13": + version "4.4.13" + resolved "https://registry.yarnpkg.com/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.13.tgz#8a5dda67cbf8e63155a908a724e7ae09b763baad" + integrity sha512-x6vn0PjYmGdNuKh/juUJJewZh7MoQ46jYaJ2mvekF4EesMuFfrl4LaW/k97Zjf8PTCPQmPgMvwewg7eNoH9n5w== + dependencies: + "@smithy/core" "^3.22.1" + "@smithy/middleware-serde" "^4.2.9" + "@smithy/node-config-provider" "^4.3.8" + "@smithy/shared-ini-file-loader" "^4.4.3" + "@smithy/types" "^4.12.0" + "@smithy/url-parser" "^4.2.8" + "@smithy/util-middleware" "^4.2.8" tslib "^2.6.2" -"@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/middleware-retry@^4.4.29": + version "4.4.30" + resolved "https://registry.yarnpkg.com/@smithy/middleware-retry/-/middleware-retry-4.4.30.tgz#a0548803044069b53a332606d4b4f803f07f8963" + integrity sha512-CBGyFvN0f8hlnqKH/jckRDz78Snrp345+PVk8Ux7pnkUCW97Iinse59lY78hBt04h1GZ6hjBN94BRwZy1xC8Bg== + dependencies: + "@smithy/node-config-provider" "^4.3.8" + "@smithy/protocol-http" "^5.3.8" + "@smithy/service-error-classification" "^4.2.8" + "@smithy/smithy-client" "^4.11.2" + "@smithy/types" "^4.12.0" + "@smithy/util-middleware" "^4.2.8" + "@smithy/util-retry" "^4.2.8" "@smithy/uuid" "^1.1.0" tslib "^2.6.2" -"@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== +"@smithy/middleware-serde@^4.2.9": + version "4.2.9" + resolved "https://registry.yarnpkg.com/@smithy/middleware-serde/-/middleware-serde-4.2.9.tgz#fd9d9b02b265aef67c9a30f55c2a5038fc9ca791" + integrity sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ== dependencies: - "@smithy/protocol-http" "^5.3.0" - "@smithy/types" "^4.6.0" + "@smithy/protocol-http" "^5.3.8" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@smithy/middleware-stack@^4.2.8": + version "4.2.8" + resolved "https://registry.yarnpkg.com/@smithy/middleware-stack/-/middleware-stack-4.2.8.tgz#4fa9cfaaa05f664c9bb15d45608f3cb4f6da2b76" + integrity sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA== dependencies: - "@smithy/types" "^4.6.0" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@smithy/node-config-provider@^4.3.8": + version "4.3.8" + resolved "https://registry.yarnpkg.com/@smithy/node-config-provider/-/node-config-provider-4.3.8.tgz#85a0683448262b2eb822f64c14278d4887526377" + integrity sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg== dependencies: - "@smithy/property-provider" "^4.2.0" - "@smithy/shared-ini-file-loader" "^4.3.0" - "@smithy/types" "^4.6.0" + "@smithy/property-provider" "^4.2.8" + "@smithy/shared-ini-file-loader" "^4.4.3" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@smithy/node-http-handler@^4.4.8", "@smithy/node-http-handler@^4.4.9": + version "4.4.9" + resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.4.9.tgz#c167e5b8aed33c5edaf25b903ed9866858499c93" + integrity sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w== dependencies: - "@smithy/abort-controller" "^4.2.0" - "@smithy/protocol-http" "^5.3.0" - "@smithy/querystring-builder" "^4.2.0" - "@smithy/types" "^4.6.0" + "@smithy/abort-controller" "^4.2.8" + "@smithy/protocol-http" "^5.3.8" + "@smithy/querystring-builder" "^4.2.8" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@smithy/property-provider@^4.2.8": + version "4.2.8" + resolved "https://registry.yarnpkg.com/@smithy/property-provider/-/property-provider-4.2.8.tgz#6e37b30923d2d31370c50ce303a4339020031472" + integrity sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w== dependencies: - "@smithy/types" "^4.6.0" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@smithy/protocol-http@^5.3.8": + version "5.3.8" + resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-5.3.8.tgz#0938f69a3c3673694c2f489a640fce468ce75006" + integrity sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ== dependencies: - "@smithy/types" "^4.6.0" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@smithy/querystring-builder@^4.2.8": + version "4.2.8" + resolved "https://registry.yarnpkg.com/@smithy/querystring-builder/-/querystring-builder-4.2.8.tgz#2fa72d29eb1844a6a9933038bbbb14d6fe385e93" + integrity sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw== dependencies: - "@smithy/types" "^4.6.0" + "@smithy/types" "^4.12.0" "@smithy/util-uri-escape" "^4.2.0" tslib "^2.6.2" -"@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== +"@smithy/querystring-parser@^4.2.8": + version "4.2.8" + resolved "https://registry.yarnpkg.com/@smithy/querystring-parser/-/querystring-parser-4.2.8.tgz#aa3f2456180ce70242e89018d0b1ebd4782a6347" + integrity sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA== dependencies: - "@smithy/types" "^4.6.0" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@smithy/service-error-classification@^4.2.8": + version "4.2.8" + resolved "https://registry.yarnpkg.com/@smithy/service-error-classification/-/service-error-classification-4.2.8.tgz#6d89dbad4f4978d7b75a44af8c18c22455a16cdc" + integrity sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ== dependencies: - "@smithy/types" "^4.6.0" + "@smithy/types" "^4.12.0" -"@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== +"@smithy/shared-ini-file-loader@^4.4.3": + version "4.4.3" + resolved "https://registry.yarnpkg.com/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.3.tgz#6054215ecb3a6532b13aa49a9fbda640b63be50e" + integrity sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg== dependencies: - "@smithy/types" "^4.6.0" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@smithy/signature-v4@^5.3.8": + version "5.3.8" + resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-5.3.8.tgz#796619b10b7cc9467d0625b0ebd263ae04fdfb76" + integrity sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg== dependencies: "@smithy/is-array-buffer" "^4.2.0" - "@smithy/protocol-http" "^5.3.0" - "@smithy/types" "^4.6.0" + "@smithy/protocol-http" "^5.3.8" + "@smithy/types" "^4.12.0" "@smithy/util-hex-encoding" "^4.2.0" - "@smithy/util-middleware" "^4.2.0" + "@smithy/util-middleware" "^4.2.8" "@smithy/util-uri-escape" "^4.2.0" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.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": - version "3.7.2" - resolved "https://registry.yarnpkg.com/@smithy/types/-/types-3.7.2.tgz#05cb14840ada6f966de1bf9a9c7dd86027343e10" - integrity sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg== - dependencies: +"@smithy/smithy-client@^4.11.1", "@smithy/smithy-client@^4.11.2": + version "4.11.2" + resolved "https://registry.yarnpkg.com/@smithy/smithy-client/-/smithy-client-4.11.2.tgz#1f6a4d75625dbaa16bafbe9b10cf6a41c98fe3da" + integrity sha512-SCkGmFak/xC1n7hKRsUr6wOnBTJ3L22Qd4e8H1fQIuKTAjntwgU8lrdMe7uHdiT2mJAOWA/60qaW9tiMu69n1A== + dependencies: + "@smithy/core" "^3.22.1" + "@smithy/middleware-endpoint" "^4.4.13" + "@smithy/middleware-stack" "^4.2.8" + "@smithy/protocol-http" "^5.3.8" + "@smithy/types" "^4.12.0" + "@smithy/util-stream" "^4.5.11" 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== +"@smithy/types@^4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.12.0.tgz#55d2479080922bda516092dbf31916991d9c6fee" + integrity sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw== dependencies: tslib "^2.6.2" -"@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== +"@smithy/url-parser@^4.2.8": + version "4.2.8" + resolved "https://registry.yarnpkg.com/@smithy/url-parser/-/url-parser-4.2.8.tgz#b44267cd704abe114abcd00580acdd9e4acc1177" + integrity sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA== dependencies: - "@smithy/querystring-parser" "^4.2.0" - "@smithy/types" "^4.6.0" + "@smithy/querystring-parser" "^4.2.8" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@smithy/util-base64@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@smithy/util-base64/-/util-base64-4.3.0.tgz#5e287b528793aa7363877c1a02cd880d2e76241d" + integrity sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ== dependencies: "@smithy/util-buffer-from" "^4.2.0" "@smithy/util-utf8" "^4.2.0" @@ -2116,16 +2180,16 @@ dependencies: tslib "^2.6.2" -"@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== +"@smithy/util-body-length-node@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz#79c8a5d18e010cce6c42d5cbaf6c1958523e6fec" + integrity sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA== dependencies: tslib "^2.6.2" "@smithy/util-buffer-from@^2.2.0": version "2.2.0" - resolved "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz" + resolved "https://registry.yarnpkg.com/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz#6fc88585165ec73f8681d426d96de5d402021e4b" integrity sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA== dependencies: "@smithy/is-array-buffer" "^2.2.0" @@ -2146,37 +2210,36 @@ dependencies: tslib "^2.6.2" -"@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== +"@smithy/util-defaults-mode-browser@^4.3.28": + version "4.3.29" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.29.tgz#fd4f9563ffd1fb49d092e5b86bacc7796170763e" + integrity sha512-nIGy3DNRmOjaYaaKcQDzmWsro9uxlaqUOhZDHQed9MW/GmkBZPtnU70Pu1+GT9IBmUXwRdDuiyaeiy9Xtpn3+Q== dependencies: - "@smithy/property-provider" "^4.2.0" - "@smithy/smithy-client" "^4.7.0" - "@smithy/types" "^4.6.0" - bowser "^2.11.0" + "@smithy/property-provider" "^4.2.8" + "@smithy/smithy-client" "^4.11.2" + "@smithy/types" "^4.12.0" tslib "^2.6.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" +"@smithy/util-defaults-mode-node@^4.2.31": + version "4.2.32" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.32.tgz#bc3e9ee1711a9ac3b1c29ea0bef0e785c1da30da" + integrity sha512-7dtFff6pu5fsjqrVve0YMhrnzJtccCWDacNKOkiZjJ++fmjGExmmSu341x+WU6Oc1IccL7lDuaUj7SfrHpWc5Q== + dependencies: + "@smithy/config-resolver" "^4.4.6" + "@smithy/credential-provider-imds" "^4.2.8" + "@smithy/node-config-provider" "^4.3.8" + "@smithy/property-provider" "^4.2.8" + "@smithy/smithy-client" "^4.11.2" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@smithy/util-endpoints@^3.2.8": + version "3.2.8" + resolved "https://registry.yarnpkg.com/@smithy/util-endpoints/-/util-endpoints-3.2.8.tgz#5650bda2adac989ff2e562606088c5de3dcb1b36" + integrity sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw== dependencies: - "@smithy/node-config-provider" "^4.3.0" - "@smithy/types" "^4.6.0" + "@smithy/node-config-provider" "^4.3.8" + "@smithy/types" "^4.12.0" tslib "^2.6.2" "@smithy/util-hex-encoding@^4.2.0": @@ -2186,32 +2249,32 @@ dependencies: tslib "^2.6.2" -"@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== +"@smithy/util-middleware@^4.2.8": + version "4.2.8" + resolved "https://registry.yarnpkg.com/@smithy/util-middleware/-/util-middleware-4.2.8.tgz#1da33f29a74c7ebd9e584813cb7e12881600a80a" + integrity sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A== dependencies: - "@smithy/types" "^4.6.0" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@smithy/util-retry@^4.2.8": + version "4.2.8" + resolved "https://registry.yarnpkg.com/@smithy/util-retry/-/util-retry-4.2.8.tgz#23f3f47baf0681233fd0c37b259e60e268c73b11" + integrity sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg== dependencies: - "@smithy/service-error-classification" "^4.2.0" - "@smithy/types" "^4.6.0" + "@smithy/service-error-classification" "^4.2.8" + "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@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== +"@smithy/util-stream@^4.5.10", "@smithy/util-stream@^4.5.11": + version "4.5.11" + resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-4.5.11.tgz#69bf0816c2a396b389a48a64455dacdb57893984" + integrity sha512-lKmZ0S/3Qj2OF5H1+VzvDLb6kRxGzZHq6f3rAsoSu5cTLGsn3v3VQBA8czkNNXlLjoFEtVu3OQT2jEeOtOE2CA== 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/fetch-http-handler" "^5.3.9" + "@smithy/node-http-handler" "^4.4.9" + "@smithy/types" "^4.12.0" + "@smithy/util-base64" "^4.3.0" "@smithy/util-buffer-from" "^4.2.0" "@smithy/util-hex-encoding" "^4.2.0" "@smithy/util-utf8" "^4.2.0" @@ -2226,7 +2289,7 @@ "@smithy/util-utf8@^2.0.0": version "2.3.0" - resolved "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz" + resolved "https://registry.yarnpkg.com/@smithy/util-utf8/-/util-utf8-2.3.0.tgz#dd96d7640363259924a214313c3cf16e7dd329c5" integrity sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A== dependencies: "@smithy/util-buffer-from" "^2.2.0" @@ -2240,13 +2303,13 @@ "@smithy/util-buffer-from" "^4.2.0" tslib "^2.6.2" -"@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== +"@smithy/util-waiter@^4.2.8": + version "4.2.8" + resolved "https://registry.yarnpkg.com/@smithy/util-waiter/-/util-waiter-4.2.8.tgz#35d7bd8b2be7a2ebc12d8c38a0818c501b73e928" + integrity sha512-n+lahlMWk+aejGuax7DPWtqav8HYnWxQwR+LCG2BgCUmaGcTe9qZCFsmw8TMg9iG75HOwhrJCX9TCJRLH+Yzqg== dependencies: - "@smithy/abort-controller" "^4.2.0" - "@smithy/types" "^4.6.0" + "@smithy/abort-controller" "^4.2.8" + "@smithy/types" "^4.12.0" tslib "^2.6.2" "@smithy/uuid@^1.1.0": @@ -2256,34 +2319,47 @@ dependencies: tslib "^2.6.2" +"@so-ric/colorspace@^1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@so-ric/colorspace/-/colorspace-1.1.6.tgz#62515d8b9f27746b76950a83bde1af812d91923b" + integrity sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw== + dependencies: + color "^5.0.2" + text-hex "1.0.x" + +"@standard-schema/spec@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8" + integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w== + "@tsconfig/node10@^1.0.7": - version "1.0.11" - resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz" - integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== + version "1.0.12" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.12.tgz#be57ceac1e4692b41be9de6be8c32a106636dba4" + integrity sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ== "@tsconfig/node12@^1.0.7": version "1.0.11" - resolved "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== "@tsconfig/node14@^1.0.0": version "1.0.3" - resolved "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== "@tsconfig/node16@^1.0.2": version "1.0.4" - resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== "@types/aws-lambda@8.10.145": version "8.10.145" - resolved "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.145.tgz" + resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.145.tgz#b2d31a987f4888e5553ff1819f57cafa475594d9" integrity sha512-dtByW6WiFk5W5Jfgz1VM+YPA21xMXTuSFoLYIDY0L44jDLLflVPtZkYuu3/YxpGcvjzKFBZLU+GyKjR0HOYtyw== "@types/babel__core@^7.1.14": version "7.20.5" - resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" + 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" @@ -2293,61 +2369,74 @@ "@types/babel__traverse" "*" "@types/babel__generator@*": - version "7.6.8" - resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz" - integrity sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw== + version "7.27.0" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.27.0.tgz#b5819294c51179957afaec341442f9341e4108a9" + integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== dependencies: "@babel/types" "^7.0.0" "@types/babel__template@*": version "7.4.4" - resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz" + 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.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz" - integrity sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg== + version "7.28.0" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz#07d713d6cce0d265c9849db0cbe62d3f61f36f74" + integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q== dependencies: - "@babel/types" "^7.20.7" + "@babel/types" "^7.28.2" + +"@types/chai@^5.2.2": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.2.3.tgz#8e9cd9e1c3581fa6b341a5aed5588eb285be0b4a" + integrity sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA== + dependencies: + "@types/deep-eql" "*" + assertion-error "^2.0.1" + +"@types/deep-eql@*": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/deep-eql/-/deep-eql-4.0.2.tgz#334311971d3a07121e7eb91b684a605e7eea9cbd" + integrity sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw== "@types/estree@^1.0.6": - version "1.0.6" - resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz" - integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== "@types/graceful-fs@^4.1.3": version "4.1.9" - resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz" + 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", "@types/istanbul-lib-coverage@^2.0.1", "@types/istanbul-lib-coverage@^2.0.6": version "2.0.6" - resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz" + 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== "@types/istanbul-lib-report@*": version "3.0.3" - resolved "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf" integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== dependencies: "@types/istanbul-lib-coverage" "*" -"@types/istanbul-reports@^3.0.0": +"@types/istanbul-reports@^3.0.0", "@types/istanbul-reports@^3.0.4": version "3.0.4" - resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== dependencies: "@types/istanbul-lib-report" "*" "@types/jest@^29.5.12": version "29.5.14" - resolved "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.14.tgz#2b910912fa1d6856cadcd0c1f95af7df1d6049e5" integrity sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ== dependencies: expect "^29.0.0" @@ -2355,204 +2444,225 @@ "@types/json-schema@^7.0.15": version "7.0.15" - resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== -"@types/node@*", "@types/node@22.5.4": +"@types/node@*": + version "25.2.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.2.1.tgz#378021f9e765bb65ba36de16f3c3a8622c1fa03d" + integrity sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg== + dependencies: + undici-types "~7.16.0" + +"@types/node@22.5.4": version "22.5.4" - resolved "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.4.tgz#83f7d1f65bc2ed223bdbf57c7884f1d5a4fa84e8" integrity sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg== dependencies: undici-types "~6.19.2" -"@types/nodemailer@^6.4.14": - version "6.4.17" - resolved "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz" - integrity sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww== +"@types/nodemailer@^7.0.9": + version "7.0.9" + resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-7.0.9.tgz#a19e3fa222b21213b481cdbdbc70a06787ea49e8" + integrity sha512-vI8oF1M+8JvQhsId0Pc38BdUP2evenIIys7c7p+9OZXSPOH5c1dyINP1jT8xQ2xPuBUXmIC87s+91IZMDjH8Ow== dependencies: "@types/node" "*" "@types/prop-types@*": - version "15.7.14" - resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz" - integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ== + version "15.7.15" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7" + integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw== "@types/react@^18.3.12": - version "18.3.14" - resolved "https://registry.npmjs.org/@types/react/-/react-18.3.14.tgz" - integrity sha512-NzahNKvjNhVjuPBQ+2G7WlxstQ+47kXZNHlUvFakDViuIEfGY926GqhMueQFZ7woG+sPiQKlF36XfrIUVSUfFg== + version "18.3.28" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.28.tgz#0a85b1a7243b4258d9f626f43797ba18eb5f8781" + integrity sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw== dependencies: "@types/prop-types" "*" - csstype "^3.0.2" + csstype "^3.2.2" "@types/sinon@^17.0.3": - version "17.0.3" - resolved "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz" - integrity sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw== + version "17.0.4" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-17.0.4.tgz#fd9a3e8e07eea1a3f4a6f82a972c899e5778f369" + integrity sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew== dependencies: "@types/sinonjs__fake-timers" "*" "@types/sinonjs__fake-timers@*": - version "8.1.5" - resolved "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz" - integrity sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ== + version "15.0.1" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-15.0.1.tgz#49f731d9453f52d64dd79f5a5626c1cf1b81bea4" + integrity sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w== -"@types/stack-utils@^2.0.0": +"@types/stack-utils@^2.0.0", "@types/stack-utils@^2.0.3": version "2.0.3" - resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== "@types/triple-beam@^1.3.2": version "1.3.5" - resolved "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz" + resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c" integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw== "@types/yargs-parser@*": version "21.0.3" - resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== -"@types/yargs@^17.0.8": - version "17.0.33" - resolved "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz" - integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA== +"@types/yargs@^17.0.33", "@types/yargs@^17.0.8": + version "17.0.35" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.35.tgz#07013e46aa4d7d7d50a49e15604c1c5340d4eb24" + integrity sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg== dependencies: "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^8.12.2": - version "8.18.0" - resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz" - integrity sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw== - dependencies: - "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.18.0" - "@typescript-eslint/type-utils" "8.18.0" - "@typescript-eslint/utils" "8.18.0" - "@typescript-eslint/visitor-keys" "8.18.0" - graphemer "^1.4.0" - ignore "^5.3.1" + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz#d8899e5c2eccf5c4a20d01c036a193753748454d" + integrity sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ== + dependencies: + "@eslint-community/regexpp" "^4.12.2" + "@typescript-eslint/scope-manager" "8.54.0" + "@typescript-eslint/type-utils" "8.54.0" + "@typescript-eslint/utils" "8.54.0" + "@typescript-eslint/visitor-keys" "8.54.0" + ignore "^7.0.5" natural-compare "^1.4.0" - ts-api-utils "^1.3.0" + ts-api-utils "^2.4.0" "@typescript-eslint/parser@^8.12.2": - version "8.18.0" - resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.0.tgz" - integrity sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q== - dependencies: - "@typescript-eslint/scope-manager" "8.18.0" - "@typescript-eslint/types" "8.18.0" - "@typescript-eslint/typescript-estree" "8.18.0" - "@typescript-eslint/visitor-keys" "8.18.0" - debug "^4.3.4" - -"@typescript-eslint/scope-manager@8.18.0": - version "8.18.0" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz" - integrity sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw== - dependencies: - "@typescript-eslint/types" "8.18.0" - "@typescript-eslint/visitor-keys" "8.18.0" - -"@typescript-eslint/type-utils@8.18.0": - version "8.18.0" - resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.0.tgz" - integrity sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow== - dependencies: - "@typescript-eslint/typescript-estree" "8.18.0" - "@typescript-eslint/utils" "8.18.0" - debug "^4.3.4" - ts-api-utils "^1.3.0" - -"@typescript-eslint/types@8.18.0": - version "8.18.0" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.0.tgz" - integrity sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA== - -"@typescript-eslint/typescript-estree@8.18.0": - version "8.18.0" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz" - integrity sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg== - dependencies: - "@typescript-eslint/types" "8.18.0" - "@typescript-eslint/visitor-keys" "8.18.0" - debug "^4.3.4" - fast-glob "^3.3.2" - is-glob "^4.0.3" - minimatch "^9.0.4" - semver "^7.6.0" - ts-api-utils "^1.3.0" - -"@typescript-eslint/utils@8.18.0": - version "8.18.0" - resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.0.tgz" - integrity sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg== - dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.18.0" - "@typescript-eslint/types" "8.18.0" - "@typescript-eslint/typescript-estree" "8.18.0" - -"@typescript-eslint/visitor-keys@8.18.0": - version "8.18.0" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz" - integrity sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw== - dependencies: - "@typescript-eslint/types" "8.18.0" - eslint-visitor-keys "^4.2.0" + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.54.0.tgz#3d01a6f54ed247deb9982621f70e7abf1810bd97" + integrity sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA== + dependencies: + "@typescript-eslint/scope-manager" "8.54.0" + "@typescript-eslint/types" "8.54.0" + "@typescript-eslint/typescript-estree" "8.54.0" + "@typescript-eslint/visitor-keys" "8.54.0" + debug "^4.4.3" + +"@typescript-eslint/project-service@8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.54.0.tgz#f582aceb3d752544c8e1b11fea8d95d00cf9adc6" + integrity sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g== + dependencies: + "@typescript-eslint/tsconfig-utils" "^8.54.0" + "@typescript-eslint/types" "^8.54.0" + debug "^4.4.3" + +"@typescript-eslint/scope-manager@8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz#307dc8cbd80157e2772c2d36216857415a71ab33" + integrity sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg== + dependencies: + "@typescript-eslint/types" "8.54.0" + "@typescript-eslint/visitor-keys" "8.54.0" + +"@typescript-eslint/tsconfig-utils@8.54.0", "@typescript-eslint/tsconfig-utils@^8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz#71dd7ba1674bd48b172fc4c85b2f734b0eae3dbc" + integrity sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw== + +"@typescript-eslint/type-utils@8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz#64965317dd4118346c2fa5ee94492892200e9fb9" + integrity sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA== + dependencies: + "@typescript-eslint/types" "8.54.0" + "@typescript-eslint/typescript-estree" "8.54.0" + "@typescript-eslint/utils" "8.54.0" + debug "^4.4.3" + ts-api-utils "^2.4.0" + +"@typescript-eslint/types@8.54.0", "@typescript-eslint/types@^8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.54.0.tgz#c12d41f67a2e15a8a96fbc5f2d07b17331130889" + integrity sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA== + +"@typescript-eslint/typescript-estree@8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz#3c7716905b2b811fadbd2114804047d1bfc86527" + integrity sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA== + dependencies: + "@typescript-eslint/project-service" "8.54.0" + "@typescript-eslint/tsconfig-utils" "8.54.0" + "@typescript-eslint/types" "8.54.0" + "@typescript-eslint/visitor-keys" "8.54.0" + debug "^4.4.3" + minimatch "^9.0.5" + semver "^7.7.3" + tinyglobby "^0.2.15" + ts-api-utils "^2.4.0" + +"@typescript-eslint/utils@8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.54.0.tgz#c79a4bcbeebb4f571278c0183ed1cb601d84c6c8" + integrity sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA== + dependencies: + "@eslint-community/eslint-utils" "^4.9.1" + "@typescript-eslint/scope-manager" "8.54.0" + "@typescript-eslint/types" "8.54.0" + "@typescript-eslint/typescript-estree" "8.54.0" + +"@typescript-eslint/visitor-keys@8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz#0e4b50124b210b8600b245dd66cbad52deb15590" + integrity sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA== + dependencies: + "@typescript-eslint/types" "8.54.0" + eslint-visitor-keys "^4.2.1" "@vitest/expect@>1.6.0": - version "2.1.8" - resolved "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz" - integrity sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw== + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-4.0.18.tgz#361510d99fbf20eb814222e4afcb8539d79dc94d" + integrity sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ== dependencies: - "@vitest/spy" "2.1.8" - "@vitest/utils" "2.1.8" - chai "^5.1.2" - tinyrainbow "^1.2.0" + "@standard-schema/spec" "^1.0.0" + "@types/chai" "^5.2.2" + "@vitest/spy" "4.0.18" + "@vitest/utils" "4.0.18" + chai "^6.2.1" + tinyrainbow "^3.0.3" -"@vitest/pretty-format@2.1.8": - version "2.1.8" - resolved "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz" - integrity sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ== +"@vitest/pretty-format@4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.0.18.tgz#fbccd4d910774072ec15463553edb8ca5ce53218" + integrity sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw== dependencies: - tinyrainbow "^1.2.0" + tinyrainbow "^3.0.3" -"@vitest/spy@2.1.8": - version "2.1.8" - resolved "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz" - integrity sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg== - dependencies: - tinyspy "^3.0.2" +"@vitest/spy@4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-4.0.18.tgz#ba0f20503fb6d08baf3309d690b3efabdfa88762" + integrity sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw== -"@vitest/utils@2.1.8": - version "2.1.8" - resolved "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz" - integrity sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA== +"@vitest/utils@4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.0.18.tgz#9636b16d86a4152ec68a8d6859cff702896433d4" + integrity sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA== dependencies: - "@vitest/pretty-format" "2.1.8" - loupe "^3.1.2" - tinyrainbow "^1.2.0" + "@vitest/pretty-format" "4.0.18" + tinyrainbow "^3.0.3" acorn-jsx@^5.3.2: version "5.3.2" - resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn-walk@^8.1.1: version "8.3.4" - resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== dependencies: acorn "^8.11.0" -acorn@^8.11.0, acorn@^8.14.0, acorn@^8.4.1: - version "8.14.0" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz" - integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== +acorn@^8.11.0, acorn@^8.15.0, acorn@^8.4.1: + version "8.15.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== ajv@^6.12.4: version "6.12.6" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== dependencies: fast-deep-equal "^3.1.1" @@ -2560,48 +2670,43 @@ ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ansi-colors@^4.1.3: - version "4.1.3" - resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz" - integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== - ansi-escapes@^4.2.1: version "4.3.2" - resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" + 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.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-regex@^6.0.1: - version "6.1.0" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz" - integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + version "6.2.2" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.2.2.tgz#60216eea464d864597ce2832000738a0589650c1" + integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg== ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" -ansi-styles@^5.0.0: +ansi-styles@^5.0.0, ansi-styles@^5.2.0: version "5.2.0" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== ansi-styles@^6.1.0: - version "6.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + version "6.2.3" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.3.tgz#c044d5dcc521a076413472597a1acb1f103c4041" + integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== -anymatch@^3.0.3, anymatch@~3.1.2: +anymatch@^3.0.3: version "3.1.3" - resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== dependencies: normalize-path "^3.0.0" @@ -2609,39 +2714,39 @@ anymatch@^3.0.3, anymatch@~3.1.2: arg@^4.1.0: version "4.1.3" - resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== argparse@^1.0.7: version "1.0.10" - resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== dependencies: sprintf-js "~1.0.2" argparse@^2.0.1: version "2.0.1" - resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== assertion-error@^1.1.0: version "1.1.0" - resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== assertion-error@^2.0.1: version "2.0.1" - resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== async@^3.2.3: version "3.2.6" - resolved "https://registry.npmjs.org/async/-/async-3.2.6.tgz" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== aws-sdk-client-mock-jest@^4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/aws-sdk-client-mock-jest/-/aws-sdk-client-mock-jest-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/aws-sdk-client-mock-jest/-/aws-sdk-client-mock-jest-4.1.0.tgz#40a3bdedd8d551cf2a836b77239038c0ca10e25c" integrity sha512-+g4a5Hp+MmPqqNnvwfLitByggrqf+xSbk1pm6fBYHNcon6+aQjL5iB+3YB6HuGPemY+/mUKN34iP62S14R61bA== dependencies: "@vitest/expect" ">1.6.0" @@ -2650,7 +2755,7 @@ aws-sdk-client-mock-jest@^4.1.0: aws-sdk-client-mock@^4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/aws-sdk-client-mock/-/aws-sdk-client-mock-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/aws-sdk-client-mock/-/aws-sdk-client-mock-4.1.0.tgz#ae1950b2277f8e65f9a039975d79ff9fffab39e3" integrity sha512-h/tOYTkXEsAcV3//6C1/7U4ifSpKyJvb6auveAepqqNJl6TdZaPFEtKjBQNf8UxQdDP850knB2i/whq4zlsxJw== dependencies: "@types/sinon" "^17.0.3" @@ -2659,7 +2764,7 @@ aws-sdk-client-mock@^4.1.0: babel-jest@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz" + 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" @@ -2672,7 +2777,7 @@ babel-jest@^29.7.0: babel-plugin-istanbul@^6.1.1: version "6.1.1" - resolved "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz" + 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" @@ -2683,7 +2788,7 @@ babel-plugin-istanbul@^6.1.1: babel-plugin-jest-hoist@^29.6.3: version "29.6.3" - resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz" + 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" @@ -2692,9 +2797,9 @@ babel-plugin-jest-hoist@^29.6.3: "@types/babel__traverse" "^7.0.6" babel-preset-current-node-syntax@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz" - integrity sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw== + version "1.2.0" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz#20730d6cdc7dda5d89401cab10ac6a32067acde6" + integrity sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg== dependencies: "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-bigint" "^7.8.3" @@ -2714,7 +2819,7 @@ babel-preset-current-node-syntax@^1.0.0: babel-preset-jest@^29.6.3: version "29.6.3" - resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz" + 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" @@ -2722,105 +2827,106 @@ babel-preset-jest@^29.6.3: balanced-match@^1.0.0: version "1.0.2" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -binary-extensions@^2.0.0: - version "2.3.0" - resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz" - integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== +baseline-browser-mapping@^2.9.0: + version "2.9.19" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz#3e508c43c46d961eb4d7d2e5b8d1dd0f9ee4f488" + integrity sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg== bowser@^2.11.0: - version "2.11.0" - resolved "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz" - integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== + version "2.13.1" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.13.1.tgz#5a4c652de1d002f847dd011819f5fc729f308a7e" + integrity sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw== brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + version "1.1.12" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + version "2.0.2" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" + integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== dependencies: balanced-match "^1.0.0" -braces@^3.0.3, braces@~3.0.2: +braces@^3.0.3: version "3.0.3" - resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: fill-range "^7.1.1" browser-stdout@^1.3.1: version "1.3.1" - resolved "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz" + 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.npmjs.org/browserslist/-/browserslist-4.24.2.tgz" - integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg== + version "4.28.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95" + integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA== dependencies: - caniuse-lite "^1.0.30001669" - electron-to-chromium "^1.5.41" - node-releases "^2.0.18" - update-browserslist-db "^1.1.1" + baseline-browser-mapping "^2.9.0" + caniuse-lite "^1.0.30001759" + electron-to-chromium "^1.5.263" + node-releases "^2.0.27" + update-browserslist-db "^1.2.0" bs-logger@^0.2.6: version "0.2.6" - resolved "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== dependencies: fast-json-stable-stringify "2.x" bser@2.1.1: version "2.1.1" - resolved "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz" + 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.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== callsites@^3.0.0: version "3.1.0" - resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + 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.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" + 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: version "6.3.0" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" + 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.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001687.tgz" - integrity sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ== +caniuse-lite@^1.0.30001759: + version "1.0.30001769" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz#1ad91594fad7dc233777c2781879ab5409f7d9c2" + integrity sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg== chai-match-pattern@^1.1.0: version "1.3.0" - resolved "https://registry.npmjs.org/chai-match-pattern/-/chai-match-pattern-1.3.0.tgz" + resolved "https://registry.yarnpkg.com/chai-match-pattern/-/chai-match-pattern-1.3.0.tgz#cefd4437de465860f4f87922c31049eb9d979104" integrity sha512-DflyfI8lZ56YuYAZMTBPWghjqFQfqY1IR0ZZXrjlGZJuRvtN0TjJMBpLsrMfc45kjivXJ06iayuP7lzG6ij1bQ== dependencies: lodash-match-pattern "^2.3.1" chai@^4.1.2: version "4.5.0" - resolved "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.5.0.tgz#707e49923afdd9b13a8b0b47d33d732d13812fd8" integrity sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw== dependencies: assertion-error "^1.1.0" @@ -2831,20 +2937,14 @@ chai@^4.1.2: pathval "^1.1.1" type-detect "^4.1.0" -chai@^5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz" - integrity sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw== - dependencies: - assertion-error "^2.0.1" - check-error "^2.1.1" - deep-eql "^5.0.1" - loupe "^3.1.0" - pathval "^2.0.0" +chai@^6.2.1: + version "6.2.2" + resolved "https://registry.yarnpkg.com/chai/-/chai-6.2.2.tgz#ae41b52c9aca87734505362717f3255facda360e" + integrity sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg== -chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" @@ -2852,66 +2952,49 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: char-regex@^1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz" + 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.npmjs.org/check-error/-/check-error-1.0.3.tgz" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== dependencies: get-func-name "^2.0.2" -check-error@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz" - integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== - checkit@^0.7.0: version "0.7.0" - resolved "https://registry.npmjs.org/checkit/-/checkit-0.7.0.tgz" + resolved "https://registry.yarnpkg.com/checkit/-/checkit-0.7.0.tgz#14979abc93018346bfcfdcbabc19ab54c0bfd74a" integrity sha512-QgiWB8gMdF/CbmWyuxCk+f2MPQe0G1DfJfHCTbrfZlY3FnJWdnW+EGsRJctcYz/IrXxPYJmjRjdgmKUkyIZl/Q== dependencies: inherits "^2.0.1" lodash "^4.0.0" -chokidar@^3.5.3: - version "3.6.0" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" - integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" +chokidar@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" ci-info@^3.2.0: version "3.9.0" - resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz" + 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.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz" - integrity sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA== +ci-info@^4.2.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.4.0.tgz#7d54eff9f54b45b62401c26032696eb59c8bd18c" + integrity sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg== -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" +cjs-module-lexer@^1.0.0: + version "1.4.3" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz#0f79731eb8cfe1ec72acd4066efac9d61991b00d" + integrity sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q== cliui@^8.0.1: version "8.0.1" - resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz" + 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" @@ -2920,80 +3003,71 @@ cliui@^8.0.1: co@^4.6.0: version "4.6.0" - resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" + 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.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz" - integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== - -color-convert@^1.9.3: - version "1.9.3" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" + version "1.0.3" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz#cc1f01eb8d02298cbc9a437c74c70ab4e5210b80" + integrity sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw== color-convert@^2.0.1: version "2.0.1" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== dependencies: color-name "~1.1.4" -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== +color-convert@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-3.1.3.tgz#db6627b97181cb8facdfce755ae26f97ab0711f1" + integrity sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg== + dependencies: + color-name "^2.0.0" -color-name@^1.0.0, color-name@~1.1.4: +color-name@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-2.1.0.tgz#0b677385c1c4b4edfdeaf77e38fa338e3a40b693" + integrity sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg== + +color-name@~1.1.4: version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.6.0: - version "1.9.1" - resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz" - integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color@^3.1.3: - version "3.2.1" - resolved "https://registry.npmjs.org/color/-/color-3.2.1.tgz" - integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== +color-string@^2.1.3: + version "2.1.4" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-2.1.4.tgz#9dcf566ff976e23368c8bd673f5c35103ab41058" + integrity sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg== dependencies: - color-convert "^1.9.3" - color-string "^1.6.0" + color-name "^2.0.0" -colorspace@1.1.x: - version "1.1.4" - resolved "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz" - integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w== +color@^5.0.2: + version "5.0.3" + resolved "https://registry.yarnpkg.com/color/-/color-5.0.3.tgz#f79390b1b778e222ffbb54304d3dbeaef633f97f" + integrity sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA== dependencies: - color "^3.1.3" - text-hex "1.0.x" + color-convert "^3.1.3" + color-string "^2.1.3" commander@^10.0.1: version "10.0.1" - resolved "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== concat-map@0.0.1: version "0.0.1" - resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + 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.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" + 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.npmjs.org/create-jest/-/create-jest-29.7.0.tgz" + 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" @@ -3006,81 +3080,81 @@ create-jest@^29.7.0: create-require@^1.1.0: version "1.1.1" - resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-spawn@^7.0.0, cross-spawn@^7.0.3, cross-spawn@^7.0.5: +cross-spawn@^7.0.3, cross-spawn@^7.0.6: version "7.0.6" - resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" which "^2.0.1" -csstype@^3.0.2: - version "3.1.3" - resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz" - integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== +csstype@^3.2.2: + version "3.2.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" + integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== -debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5: - version "4.4.0" - resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz" - integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.5, debug@^4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== dependencies: ms "^2.1.3" decamelize@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz" + 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.npmjs.org/dedent/-/dedent-1.5.3.tgz" - integrity sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ== + version "1.7.1" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.7.1.tgz#364661eea3d73f3faba7089214420ec2f8f13e15" + integrity sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg== deep-eql@^4.1.3: version "4.1.4" - resolved "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.4.tgz#d0d3912865911bb8fac5afb4e3acfa6a28dc72b7" integrity sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg== dependencies: type-detect "^4.0.0" -deep-eql@^5.0.1: - version "5.0.2" - resolved "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz" - integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== - deep-is@^0.1.3: version "0.1.4" - resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" + 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.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== detect-newline@^3.0.0: version "3.1.0" - resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" + 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.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== diff@^4.0.1: - version "4.0.2" - resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + version "4.0.4" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.4.tgz#7a6dbfda325f25f07517e9b518f897c08332e07d" + integrity sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ== diff@^5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz" - integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== + version "5.2.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.2.tgz#0a4742797281d09cfa699b79ea32d27723623bad" + integrity sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A== + +diff@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-7.0.0.tgz#3fb34d387cd76d803f6eebea67b921dab0182a9a" + integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw== dom-serializer@^2.0.0: version "2.0.0" @@ -3113,45 +3187,38 @@ domutils@^3.0.1: domhandler "^5.0.3" dotenv@^16.3.1: - version "16.4.7" - resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz" - integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ== + version "16.6.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" + integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== eastasianwidth@^0.2.0: version "0.2.0" - resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== -ejs@^3.1.10: - version "3.1.10" - resolved "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz" - integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== - dependencies: - jake "^10.8.5" - -electron-to-chromium@^1.5.41: - version "1.5.71" - resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.71.tgz" - integrity sha512-dB68l59BI75W1BUGVTAEJy45CEVuEGy9qPVVQ8pnHyHMn36PLPPoE1mjLH+lo9rKulO3HC2OhbACI/8tCqJBcA== +electron-to-chromium@^1.5.263: + version "1.5.286" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz#142be1ab5e1cd5044954db0e5898f60a4960384e" + integrity sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A== emittery@^0.13.1: version "0.13.1" - resolved "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz" + 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.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== emoji-regex@^9.2.2: version "9.2.2" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== enabled@2.0.x: version "2.0.0" - resolved "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== entities@^4.2.0, entities@^4.4.0: @@ -3160,15 +3227,15 @@ entities@^4.2.0, entities@^4.4.0: integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + version "1.3.4" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414" + integrity sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ== dependencies: is-arrayish "^0.2.1" esbuild@0.24.0: version "0.24.0" - resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.24.0.tgz#f2d470596885fcb2e91c21eb3da3b3c89c0b55e7" integrity sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ== optionalDependencies: "@esbuild/aix-ppc64" "0.24.0" @@ -3196,94 +3263,96 @@ esbuild@0.24.0: "@esbuild/win32-ia32" "0.24.0" "@esbuild/win32-x64" "0.24.0" -esbuild@~0.23.0: - version "0.23.1" - resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz" - integrity sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg== +esbuild@~0.27.0: + version "0.27.3" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.27.3.tgz#5859ca8e70a3af956b26895ce4954d7e73bd27a8" + integrity sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg== optionalDependencies: - "@esbuild/aix-ppc64" "0.23.1" - "@esbuild/android-arm" "0.23.1" - "@esbuild/android-arm64" "0.23.1" - "@esbuild/android-x64" "0.23.1" - "@esbuild/darwin-arm64" "0.23.1" - "@esbuild/darwin-x64" "0.23.1" - "@esbuild/freebsd-arm64" "0.23.1" - "@esbuild/freebsd-x64" "0.23.1" - "@esbuild/linux-arm" "0.23.1" - "@esbuild/linux-arm64" "0.23.1" - "@esbuild/linux-ia32" "0.23.1" - "@esbuild/linux-loong64" "0.23.1" - "@esbuild/linux-mips64el" "0.23.1" - "@esbuild/linux-ppc64" "0.23.1" - "@esbuild/linux-riscv64" "0.23.1" - "@esbuild/linux-s390x" "0.23.1" - "@esbuild/linux-x64" "0.23.1" - "@esbuild/netbsd-x64" "0.23.1" - "@esbuild/openbsd-arm64" "0.23.1" - "@esbuild/openbsd-x64" "0.23.1" - "@esbuild/sunos-x64" "0.23.1" - "@esbuild/win32-arm64" "0.23.1" - "@esbuild/win32-ia32" "0.23.1" - "@esbuild/win32-x64" "0.23.1" + "@esbuild/aix-ppc64" "0.27.3" + "@esbuild/android-arm" "0.27.3" + "@esbuild/android-arm64" "0.27.3" + "@esbuild/android-x64" "0.27.3" + "@esbuild/darwin-arm64" "0.27.3" + "@esbuild/darwin-x64" "0.27.3" + "@esbuild/freebsd-arm64" "0.27.3" + "@esbuild/freebsd-x64" "0.27.3" + "@esbuild/linux-arm" "0.27.3" + "@esbuild/linux-arm64" "0.27.3" + "@esbuild/linux-ia32" "0.27.3" + "@esbuild/linux-loong64" "0.27.3" + "@esbuild/linux-mips64el" "0.27.3" + "@esbuild/linux-ppc64" "0.27.3" + "@esbuild/linux-riscv64" "0.27.3" + "@esbuild/linux-s390x" "0.27.3" + "@esbuild/linux-x64" "0.27.3" + "@esbuild/netbsd-arm64" "0.27.3" + "@esbuild/netbsd-x64" "0.27.3" + "@esbuild/openbsd-arm64" "0.27.3" + "@esbuild/openbsd-x64" "0.27.3" + "@esbuild/openharmony-arm64" "0.27.3" + "@esbuild/sunos-x64" "0.27.3" + "@esbuild/win32-arm64" "0.27.3" + "@esbuild/win32-ia32" "0.27.3" + "@esbuild/win32-x64" "0.27.3" escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" - resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== escape-string-regexp@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== escape-string-regexp@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-scope@^8.2.0: - version "8.2.0" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz" - integrity sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A== +eslint-scope@^8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.4.0.tgz#88e646a207fad61436ffa39eb505147200655c82" + integrity sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" eslint-visitor-keys@^3.4.3: version "3.4.3" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint-visitor-keys@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz" - integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== +eslint-visitor-keys@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1" + integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== eslint@^9.13.0: - version "9.16.0" - resolved "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz" - integrity sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA== + version "9.39.2" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.2.tgz#cb60e6d16ab234c0f8369a3fe7cc87967faf4b6c" + integrity sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw== dependencies: - "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/eslint-utils" "^4.8.0" "@eslint-community/regexpp" "^4.12.1" - "@eslint/config-array" "^0.19.0" - "@eslint/core" "^0.9.0" - "@eslint/eslintrc" "^3.2.0" - "@eslint/js" "9.16.0" - "@eslint/plugin-kit" "^0.2.3" + "@eslint/config-array" "^0.21.1" + "@eslint/config-helpers" "^0.4.2" + "@eslint/core" "^0.17.0" + "@eslint/eslintrc" "^3.3.1" + "@eslint/js" "9.39.2" + "@eslint/plugin-kit" "^0.4.1" "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" - "@humanwhocodes/retry" "^0.4.1" + "@humanwhocodes/retry" "^0.4.2" "@types/estree" "^1.0.6" - "@types/json-schema" "^7.0.15" ajv "^6.12.4" chalk "^4.0.0" - cross-spawn "^7.0.5" + cross-spawn "^7.0.6" debug "^4.3.2" escape-string-regexp "^4.0.0" - eslint-scope "^8.2.0" - eslint-visitor-keys "^4.2.0" - espree "^10.3.0" + eslint-scope "^8.4.0" + eslint-visitor-keys "^4.2.1" + espree "^10.4.0" esquery "^1.5.0" esutils "^2.0.2" fast-deep-equal "^3.1.3" @@ -3299,47 +3368,47 @@ eslint@^9.13.0: natural-compare "^1.4.0" optionator "^0.9.3" -espree@^10.0.1, espree@^10.3.0: - version "10.3.0" - resolved "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz" - integrity sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg== +espree@^10.0.1, espree@^10.4.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.4.0.tgz#d54f4949d4629005a1fa168d937c3ff1f7e2a837" + integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ== dependencies: - acorn "^8.14.0" + acorn "^8.15.0" acorn-jsx "^5.3.2" - eslint-visitor-keys "^4.2.0" + eslint-visitor-keys "^4.2.1" esprima@^4.0.0: version "4.0.1" - resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== esquery@^1.5.0: - version "1.6.0" - resolved "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz" - integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + version "1.7.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.7.0.tgz#08d048f261f0ddedb5bae95f46809463d9c9496d" + integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g== dependencies: estraverse "^5.1.0" esrecurse@^4.3.0: version "4.3.0" - resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== dependencies: estraverse "^5.2.0" estraverse@^5.1.0, estraverse@^5.2.0: version "5.3.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== esutils@^2.0.2: version "2.0.3" - resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== execa@^5.0.0: version "5.1.1" - resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== dependencies: cross-spawn "^7.0.3" @@ -3354,12 +3423,24 @@ execa@^5.0.0: exit@^0.1.2: version "0.1.2" - resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== -expect@>28.1.3, expect@^29.0.0, expect@^29.7.0: +expect@>28.1.3: + version "30.2.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-30.2.0.tgz#d4013bed267013c14bc1199cec8aa57cee9b5869" + integrity sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw== + dependencies: + "@jest/expect-utils" "30.2.0" + "@jest/get-type" "30.1.0" + jest-matcher-utils "30.2.0" + jest-message-util "30.2.0" + jest-mock "30.2.0" + jest-util "30.2.0" + +expect@^29.0.0, expect@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== dependencies: "@jest/expect-utils" "^29.7.0" @@ -3370,80 +3451,60 @@ expect@>28.1.3, expect@^29.0.0, expect@^29.7.0: fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" - resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.3.2: - version "3.3.2" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz" - integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== fast-levenshtein@^2.0.6: version "2.0.6" - resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -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== +fast-xml-parser@5.3.4: + version "5.3.4" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz#06f39aafffdbc97bef0321e626c7ddd06a043ecf" + integrity sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA== dependencies: strnum "^2.1.0" -fastq@^1.6.0: - version "1.17.1" - resolved "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz" - integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== - dependencies: - reusify "^1.0.4" - fb-watchman@^2.0.0: version "2.0.2" - resolved "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== dependencies: bser "2.1.1" +fdir@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + fecha@^4.2.0: version "4.2.3" - resolved "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz" + resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd" integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== file-entry-cache@^8.0.0: version "8.0.0" - resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== dependencies: flat-cache "^4.0.0" -filelist@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz" - integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== - dependencies: - minimatch "^5.0.1" - fill-range@^7.1.1: version "7.1.1" - resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" + 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" @@ -3451,7 +3512,7 @@ find-up@^4.0.0, find-up@^4.1.0: find-up@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== dependencies: locate-path "^6.0.0" @@ -3459,7 +3520,7 @@ find-up@^5.0.0: flat-cache@^4.0.0: version "4.0.1" - resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== dependencies: flatted "^3.2.9" @@ -3467,92 +3528,85 @@ flat-cache@^4.0.0: flat@^5.0.2: version "5.0.2" - resolved "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== flatted@^3.2.9: - version "3.3.2" - resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz" - integrity sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA== + version "3.3.3" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" + integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== fn.name@1.x.x: version "1.1.0" - resolved "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== foreground-child@^3.1.0: - version "3.3.0" - resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz" - integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== + version "3.3.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" + integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== dependencies: - cross-spawn "^7.0.0" + cross-spawn "^7.0.6" signal-exit "^4.0.1" fs.realpath@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + 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.3: +fsevents@^2.3.2, fsevents@~2.3.3: version "2.3.3" - resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== function-bind@^1.1.2: version "1.1.2" - resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" + 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.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" + 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.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-func-name@^2.0.1, get-func-name@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== get-package-type@^0.1.0: version "0.1.0" - resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz" + 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.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== get-tsconfig@^4.7.5: - version "4.8.1" - resolved "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz" - integrity sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg== + version "4.13.6" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.13.6.tgz#2fbfda558a98a691a798f123afd95915badce876" + integrity sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw== dependencies: resolve-pkg-maps "^1.0.0" -glob-parent@^5.1.2, glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - glob-parent@^6.0.2: version "6.0.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== dependencies: is-glob "^4.0.3" glob@^10.4.5: - version "10.4.5" - resolved "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz" - integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + version "10.5.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c" + integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg== dependencies: foreground-child "^3.1.0" jackspeak "^3.1.2" @@ -3563,7 +3617,7 @@ glob@^10.4.5: glob@^7.1.3, glob@^7.1.4: version "7.2.3" - resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" @@ -3573,46 +3627,48 @@ glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - globals@^14.0.0: version "14.0.0" - resolved "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== -graceful-fs@^4.2.9: +graceful-fs@^4.2.11, graceful-fs@^4.2.9: version "4.2.11" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -graphemer@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz" - integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +handlebars@^4.7.8: + version "4.7.8" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9" + integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.2" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" has-flag@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== hasown@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: function-bind "^1.1.2" he@^1.2.0: version "1.2.0" - resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" + 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.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== htmlparser2@^8.0.0: @@ -3627,25 +3683,30 @@ htmlparser2@^8.0.0: human-signals@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -ignore@^5.2.0, ignore@^5.3.1: +ignore@^5.2.0: version "5.3.2" - resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== +ignore@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9" + integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== + import-fresh@^3.2.1: - version "3.3.0" - resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + version "3.3.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" + integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== dependencies: parent-module "^1.0.0" resolve-from "^4.0.0" import-local@^3.0.2: version "3.2.0" - resolved "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz" + 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" @@ -3653,12 +3714,12 @@ import-local@^3.0.2: imurmurhash@^0.1.4: version "0.1.4" - resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + 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.npmjs.org/inflight/-/inflight-1.0.6.tgz" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: once "^1.3.0" @@ -3666,63 +3727,56 @@ inflight@^1.0.4: inherits@2, inherits@^2.0.1, inherits@^2.0.3: version "2.0.4" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== is-arrayish@^0.2.1: version "0.2.1" - resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" + 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.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz" - integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-core-module@^2.13.0: - version "2.15.1" - resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz" - integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== +is-core-module@^2.16.1: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== dependencies: hasown "^2.0.2" is-extglob@^2.1.1: version "2.1.1" - resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== is-fullwidth-code-point@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" + 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.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: +is-glob@^4.0.0, is-glob@^4.0.3: version "4.0.3" - resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: is-extglob "^2.1.1" is-number@^7.0.0: version "7.0.0" - resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + is-plain-obj@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== is-plain-object@^5.0.0: @@ -3732,27 +3786,27 @@ is-plain-object@^5.0.0: is-stream@^2.0.0: version "2.0.1" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== is-unicode-supported@^0.1.0: version "0.1.0" - resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== isexe@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + 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.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz" + 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.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz" + 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" @@ -3763,7 +3817,7 @@ istanbul-lib-instrument@^5.0.4: istanbul-lib-instrument@^6.0.0: version "6.0.3" - resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz" + 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" @@ -3774,7 +3828,7 @@ istanbul-lib-instrument@^6.0.0: istanbul-lib-report@^3.0.0: version "3.0.1" - resolved "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz" + 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" @@ -3783,7 +3837,7 @@ istanbul-lib-report@^3.0.0: istanbul-lib-source-maps@^4.0.0: version "4.0.1" - resolved "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz" + 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" @@ -3791,35 +3845,25 @@ istanbul-lib-source-maps@^4.0.0: source-map "^0.6.1" istanbul-reports@^3.1.3: - version "3.1.7" - resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz" - integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz#cb4535162b5784aa623cee21a7252cf2c807ac93" + integrity sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA== dependencies: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" jackspeak@^3.1.2: version "3.4.3" - resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== dependencies: "@isaacs/cliui" "^8.0.2" optionalDependencies: "@pkgjs/parseargs" "^0.11.0" -jake@^10.8.5: - version "10.9.2" - resolved "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz" - integrity sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA== - dependencies: - async "^3.2.3" - chalk "^4.0.2" - filelist "^1.0.4" - minimatch "^3.1.2" - jest-changed-files@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz" + 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" @@ -3828,7 +3872,7 @@ jest-changed-files@^29.7.0: jest-circus@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz" + 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" @@ -3854,7 +3898,7 @@ jest-circus@^29.7.0: jest-cli@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz" + 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" @@ -3871,7 +3915,7 @@ jest-cli@^29.7.0: jest-config@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz" + 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" @@ -3897,9 +3941,19 @@ jest-config@^29.7.0: slash "^3.0.0" strip-json-comments "^3.1.1" +jest-diff@30.2.0: + version "30.2.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-30.2.0.tgz#e3ec3a6ea5c5747f605c9e874f83d756cba36825" + integrity sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A== + dependencies: + "@jest/diff-sequences" "30.0.1" + "@jest/get-type" "30.1.0" + chalk "^4.1.2" + pretty-format "30.2.0" + jest-diff@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== dependencies: chalk "^4.0.0" @@ -3909,14 +3963,14 @@ jest-diff@^29.7.0: jest-docblock@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz" + 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.npmjs.org/jest-each/-/jest-each-29.7.0.tgz" + 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" @@ -3927,7 +3981,7 @@ jest-each@^29.7.0: jest-environment-node@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz" + 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" @@ -3939,12 +3993,12 @@ jest-environment-node@^29.7.0: jest-get-type@^29.6.3: version "29.6.3" - resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz" + 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.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz" + 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" @@ -3963,15 +4017,25 @@ jest-haste-map@^29.7.0: jest-leak-detector@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz" + 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@30.2.0: + version "30.2.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz#69a0d4c271066559ec8b0d8174829adc3f23a783" + integrity sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg== + dependencies: + "@jest/get-type" "30.1.0" + chalk "^4.1.2" + jest-diff "30.2.0" + pretty-format "30.2.0" + jest-matcher-utils@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== dependencies: chalk "^4.0.0" @@ -3979,9 +4043,24 @@ jest-matcher-utils@^29.7.0: jest-get-type "^29.6.3" pretty-format "^29.7.0" +jest-message-util@30.2.0: + version "30.2.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-30.2.0.tgz#fc97bf90d11f118b31e6131e2b67fc4f39f92152" + integrity sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@jest/types" "30.2.0" + "@types/stack-utils" "^2.0.3" + chalk "^4.1.2" + graceful-fs "^4.2.11" + micromatch "^4.0.8" + pretty-format "30.2.0" + slash "^3.0.0" + stack-utils "^2.0.6" + jest-message-util@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== dependencies: "@babel/code-frame" "^7.12.13" @@ -3994,9 +4073,18 @@ jest-message-util@^29.7.0: slash "^3.0.0" stack-utils "^2.0.3" +jest-mock@30.2.0: + version "30.2.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-30.2.0.tgz#69f991614eeb4060189459d3584f710845bff45e" + integrity sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw== + dependencies: + "@jest/types" "30.2.0" + "@types/node" "*" + jest-util "30.2.0" + jest-mock@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz" + 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" @@ -4005,17 +4093,22 @@ jest-mock@^29.7.0: jest-pnp-resolver@^1.2.2: version "1.2.3" - resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== +jest-regex-util@30.0.1: + version "30.0.1" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-30.0.1.tgz#f17c1de3958b67dfe485354f5a10093298f2a49b" + integrity sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA== + jest-regex-util@^29.6.3: version "29.6.3" - resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz" + 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.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz" + 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" @@ -4023,7 +4116,7 @@ jest-resolve-dependencies@^29.7.0: jest-resolve@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.7.0.tgz#64d6a8992dd26f635ab0c01e5eef4399c6bcbc30" integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== dependencies: chalk "^4.0.0" @@ -4038,7 +4131,7 @@ jest-resolve@^29.7.0: jest-runner@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.7.0.tgz#809af072d408a53dcfd2e849a4c976d3132f718e" integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== dependencies: "@jest/console" "^29.7.0" @@ -4065,7 +4158,7 @@ jest-runner@^29.7.0: jest-runtime@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz" + 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" @@ -4093,7 +4186,7 @@ jest-runtime@^29.7.0: jest-snapshot@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz" + 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" @@ -4117,9 +4210,21 @@ jest-snapshot@^29.7.0: pretty-format "^29.7.0" semver "^7.5.3" -jest-util@^29.0.0, jest-util@^29.7.0: +jest-util@30.2.0: + version "30.2.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-30.2.0.tgz#5142adbcad6f4e53c2776c067a4db3c14f913705" + integrity sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA== + dependencies: + "@jest/types" "30.2.0" + "@types/node" "*" + chalk "^4.1.2" + ci-info "^4.2.0" + graceful-fs "^4.2.11" + picomatch "^4.0.2" + +jest-util@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== dependencies: "@jest/types" "^29.6.3" @@ -4131,7 +4236,7 @@ jest-util@^29.0.0, jest-util@^29.7.0: jest-validate@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz" + 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" @@ -4143,7 +4248,7 @@ jest-validate@^29.7.0: jest-watcher@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz" + 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" @@ -4157,7 +4262,7 @@ jest-watcher@^29.7.0: jest-worker@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== dependencies: "@types/node" "*" @@ -4167,7 +4272,7 @@ jest-worker@^29.7.0: jest@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613" integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== dependencies: "@jest/core" "^29.7.0" @@ -4177,79 +4282,79 @@ jest@^29.7.0: "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + version "3.14.2" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.2.tgz#77485ce1dd7f33c061fd1b16ecea23b55fcb04b0" + integrity sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg== dependencies: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== +js-yaml@^4.1.0, js-yaml@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== dependencies: argparse "^2.0.1" jsesc@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz" - integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== json-buffer@3.0.1: version "3.0.1" - resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" + 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.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" + 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.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" + 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.npmjs.org/json5/-/json5-2.2.3.tgz" + 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.npmjs.org/just-extend/-/just-extend-6.2.0.tgz" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-6.2.0.tgz#b816abfb3d67ee860482e7401564672558163947" integrity sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw== keyv@^4.5.4: version "4.5.4" - resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== dependencies: json-buffer "3.0.1" kleur@^3.0.3: version "3.0.3" - resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" + 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.npmjs.org/kuler/-/kuler-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== lambda-local@^2.2.0: version "2.2.0" - resolved "https://registry.npmjs.org/lambda-local/-/lambda-local-2.2.0.tgz" + resolved "https://registry.yarnpkg.com/lambda-local/-/lambda-local-2.2.0.tgz#733d183a4c3f2b16c6499b9ea72cec2f13278eef" integrity sha512-bPcgpIXbHnVGfI/omZIlgucDqlf4LrsunwoKue5JdZeGybt8L6KyJz2Zu19ffuZwIwLj2NAI2ZyaqNT6/cetcg== dependencies: commander "^10.0.1" @@ -4258,12 +4363,12 @@ lambda-local@^2.2.0: leven@^3.1.0: version "3.1.0" - resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz" + 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.npmjs.org/levn/-/levn-0.4.1.tgz" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== dependencies: prelude-ls "^1.2.1" @@ -4271,26 +4376,26 @@ levn@^0.4.1: lines-and-columns@^1.1.6: version "1.2.4" - resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" + 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.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" + 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.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== dependencies: p-locate "^5.0.0" lodash-checkit@^2.4.1: version "2.4.1" - resolved "https://registry.npmjs.org/lodash-checkit/-/lodash-checkit-2.4.1.tgz" + resolved "https://registry.yarnpkg.com/lodash-checkit/-/lodash-checkit-2.4.1.tgz#8b09c6b359a5d4de86f752ff9c231f1db5d23fd4" integrity sha512-OAg5CqY04/dnsO8izxXqlleuj7z/dOk6yV0pm0TVtRaUwG5v2PGw4XWSIG/dLK0UWYk7g0/TCk8OCf50oVwv6w== dependencies: checkit "^0.7.0" @@ -4298,36 +4403,31 @@ lodash-checkit@^2.4.1: lodash-match-pattern@^2.3.1: version "2.3.1" - resolved "https://registry.npmjs.org/lodash-match-pattern/-/lodash-match-pattern-2.3.1.tgz" + resolved "https://registry.yarnpkg.com/lodash-match-pattern/-/lodash-match-pattern-2.3.1.tgz#d38f455a8b310bd91f7b2b4378297102a9b473c8" integrity sha512-dpltpxoTqs94gGFm24VwHDyFh3/eNtqNjKrlnifIBLtnzYq0nAlNM6BIeLdGAfCWC/BwNtiLL1eKZTQpLVnY6A== dependencies: chalk "^4.1.0" he "^1.2.0" lodash-checkit "^2.4.1" -lodash.get@^4.4.2: - version "4.4.2" - resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz" - integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== - lodash.memoize@^4.1.2: version "4.1.2" - resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== lodash.merge@^4.6.2: version "4.6.2" - resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== lodash@^4.0.0, lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + version "4.17.23" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a" + integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w== log-symbols@^4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== dependencies: chalk "^4.1.0" @@ -4335,7 +4435,7 @@ log-symbols@^4.1.0: logform@^2.7.0: version "2.7.0" - resolved "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.7.0.tgz#cfca97528ef290f2e125a08396805002b2d060d1" integrity sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ== dependencies: "@colors/colors" "1.6.0" @@ -4347,72 +4447,62 @@ logform@^2.7.0: loose-envify@^1.1.0: version "1.4.0" - resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" + 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.npmjs.org/loupe/-/loupe-2.3.7.tgz" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== dependencies: get-func-name "^2.0.1" -loupe@^3.1.0, loupe@^3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz" - integrity sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg== - lru-cache@^10.2.0: version "10.4.3" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz" + 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.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" + 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.npmjs.org/make-dir/-/make-dir-4.0.0.tgz" + 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" make-error@^1.1.1, make-error@^1.3.6: version "1.3.6" - resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== makeerror@1.0.12: version "1.0.12" - resolved "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== dependencies: tmpl "1.0.5" marked@^12.0.2: version "12.0.2" - resolved "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz" + resolved "https://registry.yarnpkg.com/marked/-/marked-12.0.2.tgz#b31578fe608b599944c69807b00f18edab84647e" integrity sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q== merge-stream@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge2@^1.3.0: - version "1.4.1" - resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -micromatch@^4.0.4: +micromatch@^4.0.4, micromatch@^4.0.8: version "4.0.8" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: braces "^3.0.3" @@ -4420,71 +4510,70 @@ micromatch@^4.0.4: mimic-fn@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" + 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: version "3.1.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1, minimatch@^5.1.6: - version "5.1.6" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^9.0.4: +minimatch@^9.0.4, minimatch@^9.0.5: version "9.0.5" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: brace-expansion "^2.0.1" +minimist@^1.2.5: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + "minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: version "7.1.2" - resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== mnemonist@0.38.3: version "0.38.3" - resolved "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz" + resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.38.3.tgz#35ec79c1c1f4357cfda2fe264659c2775ccd7d9d" integrity sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw== dependencies: obliterator "^1.6.1" mocha@^11.0.0: - version "11.0.1" - resolved "https://registry.npmjs.org/mocha/-/mocha-11.0.1.tgz" - integrity sha512-+3GkODfsDG71KSCQhc4IekSW+ItCK/kiez1Z28ksWvYhKXV/syxMlerR/sC7whDp7IyreZ4YxceMLdTs5hQE8A== + version "11.7.5" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-11.7.5.tgz#58f5bbfa5e0211ce7e5ee6128107cefc2515a627" + integrity sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig== dependencies: - ansi-colors "^4.1.3" browser-stdout "^1.3.1" - chokidar "^3.5.3" + chokidar "^4.0.1" debug "^4.3.5" - diff "^5.2.0" + diff "^7.0.0" escape-string-regexp "^4.0.0" find-up "^5.0.0" glob "^10.4.5" he "^1.2.0" + is-path-inside "^3.0.3" js-yaml "^4.1.0" log-symbols "^4.1.0" - minimatch "^5.1.6" + minimatch "^9.0.5" ms "^2.1.3" + picocolors "^1.1.1" serialize-javascript "^6.0.2" strip-json-comments "^3.1.1" supports-color "^8.1.1" - workerpool "^6.5.1" - yargs "^16.2.0" - yargs-parser "^20.2.9" + workerpool "^9.2.0" + yargs "^17.7.2" + yargs-parser "^21.1.1" yargs-unparser "^2.0.0" ms@^2.1.1, ms@^2.1.3: version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== nanoid@^3.3.11: @@ -4494,12 +4583,17 @@ nanoid@^3.3.11: natural-compare@^1.4.0: version "1.4.0" - resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + nise@^6.0.0: version "6.1.1" - resolved "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz" + resolved "https://registry.yarnpkg.com/nise/-/nise-6.1.1.tgz#78ea93cc49be122e44cb7c8fdf597b0e8778b64a" integrity sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g== dependencies: "@sinonjs/commons" "^3.0.1" @@ -4510,60 +4604,60 @@ nise@^6.0.0: node-int64@^0.4.0: version "0.4.0" - resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" + 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.npmjs.org/node-releases/-/node-releases-2.0.18.tgz" - integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== +node-releases@^2.0.27: + version "2.0.27" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e" + integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== nodemailer@^7.0.11: - version "7.0.11" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-7.0.11.tgz#5f7b06afaec20073cff36bea92d1c7395cc3e512" - integrity sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw== + version "7.0.13" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-7.0.13.tgz#74acaa55f0c6f9476384c29f27f53e467e8483cd" + integrity sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw== -normalize-path@^3.0.0, normalize-path@~3.0.0: +normalize-path@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" + 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.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" + 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.npmjs.org/obliterator/-/obliterator-1.6.1.tgz" + 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.npmjs.org/once/-/once-1.4.0.tgz" + 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.npmjs.org/one-time/-/one-time-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== dependencies: fn.name "1.x.x" onetime@^5.1.2: version "5.1.2" - resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" + 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.npmjs.org/optionator/-/optionator-0.9.4.tgz" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== dependencies: deep-is "^0.1.3" @@ -4575,52 +4669,52 @@ optionator@^0.9.3: p-limit@^2.2.0: version "2.3.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" + 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: version "3.1.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" + 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.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" + 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.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== dependencies: p-limit "^3.0.2" p-try@^2.0.0: version "2.2.0" - resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" + 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.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== parent-module@^1.0.0: version "1.0.1" - resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== dependencies: callsites "^3.0.0" parse-json@^5.2.0: version "5.2.0" - resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" + 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" @@ -4635,65 +4729,65 @@ parse-srcset@^1.0.2: path-exists@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + 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.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + 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: version "3.1.1" - resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + 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.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" + 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.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== dependencies: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-to-regexp@^8.1.0: - version "8.2.0" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz" - integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ== + version "8.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.3.0.tgz#aa818a6981f99321003a08987d3cec9c3474cd1f" + integrity sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA== pathval@^1.1.1: version "1.1.1" - resolved "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== -pathval@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz" - integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA== - -picocolors@^1.0.0, picocolors@^1.1.0, picocolors@^1.1.1: +picocolors@^1.1.1: version "1.1.1" - resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^4.0.2, picomatch@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" + integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== + pirates@^4.0.4: - version "4.0.6" - resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz" - integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== + version "4.0.7" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.7.tgz#643b4a18c4257c8a65104b73f3049ce9a0a15e22" + integrity sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA== pkg-dir@^4.2.0: version "4.2.0" - resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" + 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" @@ -4709,12 +4803,21 @@ postcss@^8.3.11: prelude-ls@^1.2.1: version "1.2.1" - resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +pretty-format@30.2.0: + version "30.2.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-30.2.0.tgz#2d44fe6134529aed18506f6d11509d8a62775ebe" + integrity sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA== + dependencies: + "@jest/schemas" "30.0.5" + ansi-styles "^5.2.0" + react-is "^18.3.1" + pretty-format@^29.0.0, pretty-format@^29.7.0: version "29.7.0" - resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== dependencies: "@jest/schemas" "^29.6.3" @@ -4723,7 +4826,7 @@ pretty-format@^29.0.0, pretty-format@^29.7.0: prompts@^2.0.1: version "2.4.2" - resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== dependencies: kleur "^3.0.3" @@ -4736,118 +4839,99 @@ punycode@^2.1.0: pure-rand@^6.0.0: version "6.1.0" - resolved "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - randombytes@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" react-dom@^18.3.1: version "18.3.1" - resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== dependencies: loose-envify "^1.1.0" scheduler "^0.23.2" -react-is@^18.0.0: +react-is@^18.0.0, react-is@^18.3.1: version "18.3.1" - resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== react@^18.3.1: version "18.3.1" - resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz" + 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.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" util-deprecate "^1.0.1" -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== require-directory@^2.1.1: version "2.1.1" - resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" + 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.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" + 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.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" + 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.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== resolve-pkg-maps@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== resolve.exports@^2.0.0: version "2.0.3" - resolved "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz" + 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.npmjs.org/resolve/-/resolve-1.22.8.tgz" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + version "1.22.11" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262" + integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== dependencies: - is-core-module "^2.13.0" + is-core-module "^2.16.1" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== safe-stable-stringify@^2.3.1: version "2.5.0" - resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd" integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== sanitize-html@^2.17.0: @@ -4864,60 +4948,53 @@ sanitize-html@^2.17.0: scheduler@^0.23.2: version "0.23.2" - resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== dependencies: loose-envify "^1.1.0" semver@^6.3.0, semver@^6.3.1: version "6.3.1" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" + 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, semver@^7.6.0, semver@^7.6.3: - version "7.6.3" - resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" - integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== +semver@^7.5.3, semver@^7.5.4, semver@^7.7.3: + version "7.7.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" + integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== serialize-javascript@^6.0.2: version "6.0.2" - resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== dependencies: randombytes "^2.1.0" shebang-command@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== dependencies: shebang-regex "^3.0.0" shebang-regex@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + 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.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" + 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.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== -simple-swizzle@^0.2.2: - version "0.2.2" - resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz" - integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== - dependencies: - is-arrayish "^0.3.1" - sinon@^18.0.1: version "18.0.1" - resolved "https://registry.npmjs.org/sinon/-/sinon-18.0.1.tgz" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-18.0.1.tgz#464334cdfea2cddc5eda9a4ea7e2e3f0c7a91c5e" integrity sha512-a2N2TDY1uGviajJ6r4D1CyRAkzE9NNVlYOV1wX5xQDuAk0ONgzgRl0EjCQuRCPxOwp13ghsMwt9Gdldujs39qw== dependencies: "@sinonjs/commons" "^3.0.1" @@ -4929,12 +5006,12 @@ sinon@^18.0.1: sisteransi@^1.0.5: version "1.0.5" - resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" + 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.npmjs.org/slash/-/slash-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== source-map-js@^1.2.1: @@ -4944,7 +5021,7 @@ source-map-js@^1.2.1: source-map-support@0.5.13: version "0.5.13" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz" + 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" @@ -4952,29 +5029,29 @@ source-map-support@0.5.13: source-map@^0.6.0, source-map@^0.6.1: version "0.6.1" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + 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.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== stack-trace@0.0.x: version "0.0.10" - resolved "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== -stack-utils@^2.0.3: +stack-utils@^2.0.3, stack-utils@^2.0.6: version "2.0.6" - resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== dependencies: escape-string-regexp "^2.0.0" string-length@^4.0.1: version "4.0.2" - resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" + 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" @@ -4982,7 +5059,7 @@ string-length@^4.0.1: "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: emoji-regex "^8.0.0" @@ -4991,7 +5068,7 @@ string-length@^4.0.1: string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: emoji-regex "^8.0.0" @@ -5000,7 +5077,7 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" - resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== dependencies: eastasianwidth "^0.2.0" @@ -5009,74 +5086,74 @@ string-width@^5.0.1, string-width@^5.1.2: string_decoder@^1.1.1: version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== dependencies: safe-buffer "~5.2.0" "strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" strip-ansi@^7.0.1: - version "7.1.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" - integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + version "7.1.2" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba" + integrity sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA== dependencies: ansi-regex "^6.0.1" strip-bom@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz" + 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.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" + 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.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== 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== + version "2.1.2" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-2.1.2.tgz#a5e00ba66ab25f9cafa3726b567ce7a49170937a" + integrity sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ== supports-color@^7, supports-color@^7.1.0: version "7.2.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" supports-color@^8.0.0, supports-color@^8.1.1: version "8.1.1" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" + 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.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" + 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.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz" + 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" @@ -5085,59 +5162,62 @@ test-exclude@^6.0.0: text-hex@1.0.x: version "1.0.0" - resolved "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== -tinyrainbow@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz" - integrity sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ== +tinyglobby@^0.2.15: + version "0.2.15" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" + integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.3" -tinyspy@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz" - integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q== +tinyrainbow@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-3.0.3.tgz#984a5b1c1b25854a9b6bccbe77964d0593d1ea42" + integrity sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q== tmpl@1.0.5: version "1.0.5" - resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz" + 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.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== dependencies: is-number "^7.0.0" triple-beam@^1.3.0: version "1.4.1" - resolved "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz" + resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984" integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg== -ts-api-utils@^1.3.0: - version "1.4.3" - resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz" - integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw== +ts-api-utils@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.4.0.tgz#2690579f96d2790253bdcf1ca35d569ad78f9ad8" + integrity sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA== ts-jest@^29.2.5: - version "29.2.5" - resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz" - integrity sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA== + version "29.4.6" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.4.6.tgz#51cb7c133f227396818b71297ad7409bb77106e9" + integrity sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA== dependencies: bs-logger "^0.2.6" - ejs "^3.1.10" fast-json-stable-stringify "^2.1.0" - jest-util "^29.0.0" + handlebars "^4.7.8" json5 "^2.2.3" lodash.memoize "^4.1.2" make-error "^1.3.6" - semver "^7.6.3" + semver "^7.7.3" + type-fest "^4.41.0" yargs-parser "^21.1.1" ts-node@^10.9.2: version "10.9.2" - resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== dependencies: "@cspotcode/source-map-support" "^0.8.0" @@ -5156,79 +5236,94 @@ ts-node@^10.9.2: tslib@^2.1.0, tslib@^2.6.2, tslib@^2.8.0: version "2.8.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== tsx@^4.19.2: - version "4.19.2" - resolved "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz" - integrity sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g== + version "4.21.0" + resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.21.0.tgz#32aa6cf17481e336f756195e6fe04dae3e6308b1" + integrity sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw== dependencies: - esbuild "~0.23.0" + esbuild "~0.27.0" get-tsconfig "^4.7.5" optionalDependencies: fsevents "~2.3.3" type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" - resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== dependencies: prelude-ls "^1.2.1" type-detect@4.0.8: version "4.0.8" - resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== type-detect@^4.0.0, type-detect@^4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz" + 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.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== +type-fest@^4.41.0: + version "4.41.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58" + integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== + typescript@5.6.3: version "5.6.3" - resolved "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b" integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== +uglify-js@^3.1.4: + version "3.19.3" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f" + integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== + undici-types@~6.19.2: version "6.19.8" - resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== -update-browserslist-db@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz" - integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== +undici-types@~7.16.0: + version "7.16.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" + integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== + +update-browserslist-db@^1.2.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d" + integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w== dependencies: escalade "^3.2.0" - picocolors "^1.1.0" + picocolors "^1.1.1" uri-js@^4.2.2: version "4.4.1" - resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: punycode "^2.1.0" util-deprecate@^1.0.1: version "1.0.2" - resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== 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" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== v8-to-istanbul@^9.0.1: version "9.3.0" - resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz" + 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" @@ -5237,21 +5332,21 @@ v8-to-istanbul@^9.0.1: walker@^1.0.8: version "1.0.8" - resolved "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== dependencies: makeerror "1.0.12" which@^2.0.1: version "2.0.2" - resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" winston-transport@^4.9.0: version "4.9.0" - resolved "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz" + resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.9.0.tgz#3bba345de10297654ea6f33519424560003b3bf9" integrity sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A== dependencies: logform "^2.7.0" @@ -5259,12 +5354,12 @@ winston-transport@^4.9.0: triple-beam "^1.3.0" winston@^3.10.0: - version "3.17.0" - resolved "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz" - integrity sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw== + version "3.19.0" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.19.0.tgz#cc1d1262f5f45946904085cfffe73efb4b7a581d" + integrity sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA== dependencies: "@colors/colors" "^1.6.0" - "@dabh/diagnostics" "^2.0.2" + "@dabh/diagnostics" "^2.0.8" async "^3.2.3" is-stream "^2.0.0" logform "^2.7.0" @@ -5277,17 +5372,22 @@ winston@^3.10.0: word-wrap@^1.2.5: version "1.2.5" - resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -workerpool@^6.5.1: - version "6.5.1" - resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz" - integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== + +workerpool@^9.2.0: + version "9.3.4" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-9.3.4.tgz#f6c92395b2141afd78e2a889e80cb338fe9fca41" + integrity sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg== "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== dependencies: ansi-styles "^4.0.0" @@ -5296,7 +5396,7 @@ workerpool@^6.5.1: wrap-ansi@^7.0.0: version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== dependencies: ansi-styles "^4.0.0" @@ -5305,7 +5405,7 @@ wrap-ansi@^7.0.0: wrap-ansi@^8.1.0: version "8.1.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== dependencies: ansi-styles "^6.1.0" @@ -5314,12 +5414,12 @@ wrap-ansi@^8.1.0: wrappy@1: version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + 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.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz" + 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" @@ -5327,27 +5427,22 @@ write-file-atomic@^4.0.2: y18n@^5.0.5: version "5.0.8" - resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" + 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.npmjs.org/yallist/-/yallist-3.1.1.tgz" + 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.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - yargs-parser@^21.1.1: version "21.1.1" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" + 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.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== dependencies: camelcase "^6.0.0" @@ -5355,22 +5450,9 @@ yargs-unparser@^2.0.0: flat "^5.0.2" is-plain-obj "^2.1.0" -yargs@^16.2.0: - version "16.2.0" - resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - -yargs@^17.3.1: +yargs@^17.3.1, yargs@^17.7.2: version "17.7.2" - resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== dependencies: cliui "^8.0.1" @@ -5383,15 +5465,15 @@ yargs@^17.3.1: yn@3.1.1: version "3.1.1" - resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== yocto-queue@^0.1.0: version "0.1.0" - resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== zod@^3.23.8: - version "3.23.8" - resolved "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz" - integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== + version "3.25.76" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" + integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== From 1313cc982737f6857c368a1cd664f524aba31623 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 6 Feb 2026 14:57:48 -0600 Subject: [PATCH 27/52] update cosmetology node deps to latest --- .../cosmetology-app/lambdas/nodejs/yarn.lock | 1349 ++++++++--------- 1 file changed, 674 insertions(+), 675 deletions(-) diff --git a/backend/cosmetology-app/lambdas/nodejs/yarn.lock b/backend/cosmetology-app/lambdas/nodejs/yarn.lock index 0aeefce7a..9a5f70474 100644 --- a/backend/cosmetology-app/lambdas/nodejs/yarn.lock +++ b/backend/cosmetology-app/lambdas/nodejs/yarn.lock @@ -86,46 +86,46 @@ "@aws/lambda-invoke-store" "0.2.3" "@aws-sdk/client-dynamodb@^3.901.0": - version "3.971.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-dynamodb/-/client-dynamodb-3.971.0.tgz#6fef3c3f358d5d828d9c5ae68fffcaf386fc90f5" - integrity sha512-xSlYgrvonTatk09zjN5wJwIhSOS6Mjf4/ccP/Sws/t12BNH7AFIl9jJXfxIS5FYfhvCfFDzoZsdr/3XrdBpxEA== + version "3.985.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-dynamodb/-/client-dynamodb-3.985.0.tgz#2bcd54c9d215a72145de037efdec30d0ba219b0c" + integrity sha512-e11U2wheDF3hhYOQse3nA0ZsvKGJXf74bwrNnU3gnPhafK7mGfvu6WzJNGIrJBf1aDYsNJ/U5yZL7fe1mBS8fg== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "3.970.0" - "@aws-sdk/credential-provider-node" "3.971.0" - "@aws-sdk/dynamodb-codec" "3.970.0" - "@aws-sdk/middleware-endpoint-discovery" "3.971.0" - "@aws-sdk/middleware-host-header" "3.969.0" - "@aws-sdk/middleware-logger" "3.969.0" - "@aws-sdk/middleware-recursion-detection" "3.969.0" - "@aws-sdk/middleware-user-agent" "3.970.0" - "@aws-sdk/region-config-resolver" "3.969.0" - "@aws-sdk/types" "3.969.0" - "@aws-sdk/util-endpoints" "3.970.0" - "@aws-sdk/util-user-agent-browser" "3.969.0" - "@aws-sdk/util-user-agent-node" "3.971.0" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/credential-provider-node" "^3.972.6" + "@aws-sdk/dynamodb-codec" "^3.972.8" + "@aws-sdk/middleware-endpoint-discovery" "^3.972.3" + "@aws-sdk/middleware-host-header" "^3.972.3" + "@aws-sdk/middleware-logger" "^3.972.3" + "@aws-sdk/middleware-recursion-detection" "^3.972.3" + "@aws-sdk/middleware-user-agent" "^3.972.7" + "@aws-sdk/region-config-resolver" "^3.972.3" + "@aws-sdk/types" "^3.973.1" + "@aws-sdk/util-endpoints" "3.985.0" + "@aws-sdk/util-user-agent-browser" "^3.972.3" + "@aws-sdk/util-user-agent-node" "^3.972.5" "@smithy/config-resolver" "^4.4.6" - "@smithy/core" "^3.20.6" + "@smithy/core" "^3.22.1" "@smithy/fetch-http-handler" "^5.3.9" "@smithy/hash-node" "^4.2.8" "@smithy/invalid-dependency" "^4.2.8" "@smithy/middleware-content-length" "^4.2.8" - "@smithy/middleware-endpoint" "^4.4.7" - "@smithy/middleware-retry" "^4.4.23" + "@smithy/middleware-endpoint" "^4.4.13" + "@smithy/middleware-retry" "^4.4.30" "@smithy/middleware-serde" "^4.2.9" "@smithy/middleware-stack" "^4.2.8" "@smithy/node-config-provider" "^4.3.8" - "@smithy/node-http-handler" "^4.4.8" + "@smithy/node-http-handler" "^4.4.9" "@smithy/protocol-http" "^5.3.8" - "@smithy/smithy-client" "^4.10.8" + "@smithy/smithy-client" "^4.11.2" "@smithy/types" "^4.12.0" "@smithy/url-parser" "^4.2.8" "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" "@smithy/util-body-length-node" "^4.2.1" - "@smithy/util-defaults-mode-browser" "^4.3.22" - "@smithy/util-defaults-mode-node" "^4.2.25" + "@smithy/util-defaults-mode-browser" "^4.3.29" + "@smithy/util-defaults-mode-node" "^4.2.32" "@smithy/util-endpoints" "^3.2.8" "@smithy/util-middleware" "^4.2.8" "@smithy/util-retry" "^4.2.8" @@ -134,33 +134,33 @@ tslib "^2.6.2" "@aws-sdk/client-s3@^3.901.0": - version "3.971.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.971.0.tgz#2e12e17e75b186d563c2e203fb4b213b3a973942" - integrity sha512-BBUne390fKa4C4QvZlUZ5gKcu+Uyid4IyQ20N4jl0vS7SK2xpfXlJcgKqPW5ts6kx6hWTQBk6sH5Lf12RvuJxg== + version "3.985.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.985.0.tgz#e5ebe2f341c3d4225677cbfd173a8b04a07342ea" + integrity sha512-S9TqjzzZEEIKBnC7yFpvqM7CG9ALpY5qhQ5BnDBJtdG20NoGpjKLGUUfD2wmZItuhbrcM4Z8c6m6Fg0XYIOVvw== dependencies: "@aws-crypto/sha1-browser" "5.2.0" "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "3.970.0" - "@aws-sdk/credential-provider-node" "3.971.0" - "@aws-sdk/middleware-bucket-endpoint" "3.969.0" - "@aws-sdk/middleware-expect-continue" "3.969.0" - "@aws-sdk/middleware-flexible-checksums" "3.971.0" - "@aws-sdk/middleware-host-header" "3.969.0" - "@aws-sdk/middleware-location-constraint" "3.969.0" - "@aws-sdk/middleware-logger" "3.969.0" - "@aws-sdk/middleware-recursion-detection" "3.969.0" - "@aws-sdk/middleware-sdk-s3" "3.970.0" - "@aws-sdk/middleware-ssec" "3.971.0" - "@aws-sdk/middleware-user-agent" "3.970.0" - "@aws-sdk/region-config-resolver" "3.969.0" - "@aws-sdk/signature-v4-multi-region" "3.970.0" - "@aws-sdk/types" "3.969.0" - "@aws-sdk/util-endpoints" "3.970.0" - "@aws-sdk/util-user-agent-browser" "3.969.0" - "@aws-sdk/util-user-agent-node" "3.971.0" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/credential-provider-node" "^3.972.6" + "@aws-sdk/middleware-bucket-endpoint" "^3.972.3" + "@aws-sdk/middleware-expect-continue" "^3.972.3" + "@aws-sdk/middleware-flexible-checksums" "^3.972.5" + "@aws-sdk/middleware-host-header" "^3.972.3" + "@aws-sdk/middleware-location-constraint" "^3.972.3" + "@aws-sdk/middleware-logger" "^3.972.3" + "@aws-sdk/middleware-recursion-detection" "^3.972.3" + "@aws-sdk/middleware-sdk-s3" "^3.972.7" + "@aws-sdk/middleware-ssec" "^3.972.3" + "@aws-sdk/middleware-user-agent" "^3.972.7" + "@aws-sdk/region-config-resolver" "^3.972.3" + "@aws-sdk/signature-v4-multi-region" "3.985.0" + "@aws-sdk/types" "^3.973.1" + "@aws-sdk/util-endpoints" "3.985.0" + "@aws-sdk/util-user-agent-browser" "^3.972.3" + "@aws-sdk/util-user-agent-node" "^3.972.5" "@smithy/config-resolver" "^4.4.6" - "@smithy/core" "^3.20.6" + "@smithy/core" "^3.22.1" "@smithy/eventstream-serde-browser" "^4.2.8" "@smithy/eventstream-serde-config-resolver" "^4.3.8" "@smithy/eventstream-serde-node" "^4.2.8" @@ -171,568 +171,568 @@ "@smithy/invalid-dependency" "^4.2.8" "@smithy/md5-js" "^4.2.8" "@smithy/middleware-content-length" "^4.2.8" - "@smithy/middleware-endpoint" "^4.4.7" - "@smithy/middleware-retry" "^4.4.23" + "@smithy/middleware-endpoint" "^4.4.13" + "@smithy/middleware-retry" "^4.4.30" "@smithy/middleware-serde" "^4.2.9" "@smithy/middleware-stack" "^4.2.8" "@smithy/node-config-provider" "^4.3.8" - "@smithy/node-http-handler" "^4.4.8" + "@smithy/node-http-handler" "^4.4.9" "@smithy/protocol-http" "^5.3.8" - "@smithy/smithy-client" "^4.10.8" + "@smithy/smithy-client" "^4.11.2" "@smithy/types" "^4.12.0" "@smithy/url-parser" "^4.2.8" "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" "@smithy/util-body-length-node" "^4.2.1" - "@smithy/util-defaults-mode-browser" "^4.3.22" - "@smithy/util-defaults-mode-node" "^4.2.25" + "@smithy/util-defaults-mode-browser" "^4.3.29" + "@smithy/util-defaults-mode-node" "^4.2.32" "@smithy/util-endpoints" "^3.2.8" "@smithy/util-middleware" "^4.2.8" "@smithy/util-retry" "^4.2.8" - "@smithy/util-stream" "^4.5.10" + "@smithy/util-stream" "^4.5.11" "@smithy/util-utf8" "^4.2.0" "@smithy/util-waiter" "^4.2.8" tslib "^2.6.2" -"@aws-sdk/client-sesv2@^3.839.0", "@aws-sdk/client-sesv2@^3.901.0": - version "3.971.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-sesv2/-/client-sesv2-3.971.0.tgz#657bbf2ae78c39e6073e3e503b9e4f608a50de8a" - integrity sha512-NP/lbf3mfY10Txzl0ml2YnTjnZwflp1+faOotMCrXi4fb6kInosdW0ZSHXNlNulFo9cW+llq07lD59Sw3nny+A== +"@aws-sdk/client-sesv2@^3.901.0": + version "3.985.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sesv2/-/client-sesv2-3.985.0.tgz#27b9ca639d8b03b2011ae738ab0cc6b36e54197c" + integrity sha512-RqeSpVUFeg/fI874lNNdJP5nZ+3mUY5qRDDHYiOta3+2esOC/RAG1XcfYnupFR8wDDiIYsi6gHakRUYgiIW13w== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "3.970.0" - "@aws-sdk/credential-provider-node" "3.971.0" - "@aws-sdk/middleware-host-header" "3.969.0" - "@aws-sdk/middleware-logger" "3.969.0" - "@aws-sdk/middleware-recursion-detection" "3.969.0" - "@aws-sdk/middleware-user-agent" "3.970.0" - "@aws-sdk/region-config-resolver" "3.969.0" - "@aws-sdk/signature-v4-multi-region" "3.970.0" - "@aws-sdk/types" "3.969.0" - "@aws-sdk/util-endpoints" "3.970.0" - "@aws-sdk/util-user-agent-browser" "3.969.0" - "@aws-sdk/util-user-agent-node" "3.971.0" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/credential-provider-node" "^3.972.6" + "@aws-sdk/middleware-host-header" "^3.972.3" + "@aws-sdk/middleware-logger" "^3.972.3" + "@aws-sdk/middleware-recursion-detection" "^3.972.3" + "@aws-sdk/middleware-user-agent" "^3.972.7" + "@aws-sdk/region-config-resolver" "^3.972.3" + "@aws-sdk/signature-v4-multi-region" "3.985.0" + "@aws-sdk/types" "^3.973.1" + "@aws-sdk/util-endpoints" "3.985.0" + "@aws-sdk/util-user-agent-browser" "^3.972.3" + "@aws-sdk/util-user-agent-node" "^3.972.5" "@smithy/config-resolver" "^4.4.6" - "@smithy/core" "^3.20.6" + "@smithy/core" "^3.22.1" "@smithy/fetch-http-handler" "^5.3.9" "@smithy/hash-node" "^4.2.8" "@smithy/invalid-dependency" "^4.2.8" "@smithy/middleware-content-length" "^4.2.8" - "@smithy/middleware-endpoint" "^4.4.7" - "@smithy/middleware-retry" "^4.4.23" + "@smithy/middleware-endpoint" "^4.4.13" + "@smithy/middleware-retry" "^4.4.30" "@smithy/middleware-serde" "^4.2.9" "@smithy/middleware-stack" "^4.2.8" "@smithy/node-config-provider" "^4.3.8" - "@smithy/node-http-handler" "^4.4.8" + "@smithy/node-http-handler" "^4.4.9" "@smithy/protocol-http" "^5.3.8" - "@smithy/smithy-client" "^4.10.8" + "@smithy/smithy-client" "^4.11.2" "@smithy/types" "^4.12.0" "@smithy/url-parser" "^4.2.8" "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" "@smithy/util-body-length-node" "^4.2.1" - "@smithy/util-defaults-mode-browser" "^4.3.22" - "@smithy/util-defaults-mode-node" "^4.2.25" + "@smithy/util-defaults-mode-browser" "^4.3.29" + "@smithy/util-defaults-mode-node" "^4.2.32" "@smithy/util-endpoints" "^3.2.8" "@smithy/util-middleware" "^4.2.8" "@smithy/util-retry" "^4.2.8" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/client-sso@3.971.0": - version "3.971.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.971.0.tgz#0f81795a66ce4369676489f219d05472c05f49e7" - integrity sha512-Xx+w6DQqJxDdymYyIxyKJnRzPvVJ4e/Aw0czO7aC9L/iraaV7AG8QtRe93OGW6aoHSh72CIiinnpJJfLsQqP4g== +"@aws-sdk/client-sso@3.985.0": + version "3.985.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.985.0.tgz#67287e255389c73d45f8b3b6b55ba7b57a5f73f0" + integrity sha512-81J8iE8MuXhdbMfIz4sWFj64Pe41bFi/uqqmqOC5SlGv+kwoyLsyKS/rH2tW2t5buih4vTUxskRjxlqikTD4oQ== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "3.970.0" - "@aws-sdk/middleware-host-header" "3.969.0" - "@aws-sdk/middleware-logger" "3.969.0" - "@aws-sdk/middleware-recursion-detection" "3.969.0" - "@aws-sdk/middleware-user-agent" "3.970.0" - "@aws-sdk/region-config-resolver" "3.969.0" - "@aws-sdk/types" "3.969.0" - "@aws-sdk/util-endpoints" "3.970.0" - "@aws-sdk/util-user-agent-browser" "3.969.0" - "@aws-sdk/util-user-agent-node" "3.971.0" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/middleware-host-header" "^3.972.3" + "@aws-sdk/middleware-logger" "^3.972.3" + "@aws-sdk/middleware-recursion-detection" "^3.972.3" + "@aws-sdk/middleware-user-agent" "^3.972.7" + "@aws-sdk/region-config-resolver" "^3.972.3" + "@aws-sdk/types" "^3.973.1" + "@aws-sdk/util-endpoints" "3.985.0" + "@aws-sdk/util-user-agent-browser" "^3.972.3" + "@aws-sdk/util-user-agent-node" "^3.972.5" "@smithy/config-resolver" "^4.4.6" - "@smithy/core" "^3.20.6" + "@smithy/core" "^3.22.1" "@smithy/fetch-http-handler" "^5.3.9" "@smithy/hash-node" "^4.2.8" "@smithy/invalid-dependency" "^4.2.8" "@smithy/middleware-content-length" "^4.2.8" - "@smithy/middleware-endpoint" "^4.4.7" - "@smithy/middleware-retry" "^4.4.23" + "@smithy/middleware-endpoint" "^4.4.13" + "@smithy/middleware-retry" "^4.4.30" "@smithy/middleware-serde" "^4.2.9" "@smithy/middleware-stack" "^4.2.8" "@smithy/node-config-provider" "^4.3.8" - "@smithy/node-http-handler" "^4.4.8" + "@smithy/node-http-handler" "^4.4.9" "@smithy/protocol-http" "^5.3.8" - "@smithy/smithy-client" "^4.10.8" + "@smithy/smithy-client" "^4.11.2" "@smithy/types" "^4.12.0" "@smithy/url-parser" "^4.2.8" "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" "@smithy/util-body-length-node" "^4.2.1" - "@smithy/util-defaults-mode-browser" "^4.3.22" - "@smithy/util-defaults-mode-node" "^4.2.25" + "@smithy/util-defaults-mode-browser" "^4.3.29" + "@smithy/util-defaults-mode-node" "^4.2.32" "@smithy/util-endpoints" "^3.2.8" "@smithy/util-middleware" "^4.2.8" "@smithy/util-retry" "^4.2.8" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/core@3.970.0": - version "3.970.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.970.0.tgz#3dbd978d39de680b03bf00354032fec7df397f3b" - integrity sha512-klpzObldOq8HXzDjDlY6K8rMhYZU6mXRz6P9F9N+tWnjoYFfeBMra8wYApydElTUYQKP1O7RLHwH1OKFfKcqIA== +"@aws-sdk/core@^3.973.7": + version "3.973.7" + resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.973.7.tgz#b2b3e8f272ba283ff8c066560e15b26238fdb410" + integrity sha512-wNZZQQNlJ+hzD49cKdo+PY6rsTDElO8yDImnrI69p2PLBa7QomeUKAJWYp9xnaR38nlHqWhMHZuYLCQ3oSX+xg== dependencies: - "@aws-sdk/types" "3.969.0" - "@aws-sdk/xml-builder" "3.969.0" - "@smithy/core" "^3.20.6" + "@aws-sdk/types" "^3.973.1" + "@aws-sdk/xml-builder" "^3.972.4" + "@smithy/core" "^3.22.1" "@smithy/node-config-provider" "^4.3.8" "@smithy/property-provider" "^4.2.8" "@smithy/protocol-http" "^5.3.8" "@smithy/signature-v4" "^5.3.8" - "@smithy/smithy-client" "^4.10.8" + "@smithy/smithy-client" "^4.11.2" "@smithy/types" "^4.12.0" "@smithy/util-base64" "^4.3.0" "@smithy/util-middleware" "^4.2.8" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/crc64-nvme@3.969.0": - version "3.969.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/crc64-nvme/-/crc64-nvme-3.969.0.tgz#1c7d9ffb550c26d26376e3e6129ad9f77c473802" - integrity sha512-IGNkP54HD3uuLnrPCYsv3ZD478UYq+9WwKrIVJ9Pdi3hxPg8562CH3ZHf8hEgfePN31P9Kj+Zu9kq2Qcjjt61A== +"@aws-sdk/crc64-nvme@3.972.0": + version "3.972.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.0.tgz#c5e6d14428c9fb4e6bb0646b73a0fa68e6007e24" + integrity sha512-ThlLhTqX68jvoIVv+pryOdb5coP1cX1/MaTbB9xkGDCbWbsqQcLqzPxuSoW1DCnAAIacmXCWpzUNOB9pv+xXQw== dependencies: "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-env@3.970.0": - version "3.970.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.970.0.tgz#c8d97b001d896db418b905cb1f3564a855f73d44" - integrity sha512-rtVzXzEtAfZBfh+lq3DAvRar4c3jyptweOAJR2DweyXx71QSMY+O879hjpMwES7jl07a3O1zlnFIDo4KP/96kQ== +"@aws-sdk/credential-provider-env@^3.972.5": + version "3.972.5" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.5.tgz#33ccf5df157a2e14a502e2295996f830547b6346" + integrity sha512-LxJ9PEO4gKPXzkufvIESUysykPIdrV7+Ocb9yAhbhJLE4TiAYqbCVUE+VuKP1leGR1bBfjWjYgSV5MxprlX3mQ== dependencies: - "@aws-sdk/core" "3.970.0" - "@aws-sdk/types" "3.969.0" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/types" "^3.973.1" "@smithy/property-provider" "^4.2.8" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-http@3.970.0": - version "3.970.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.970.0.tgz#e4f17105d9a7908342defe91e2c616db9ea646be" - integrity sha512-CjDbWL7JxjLc9ZxQilMusWSw05yRvUJKRpz59IxDpWUnSMHC9JMMUUkOy5Izk8UAtzi6gupRWArp4NG4labt9Q== +"@aws-sdk/credential-provider-http@^3.972.7": + version "3.972.7" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.7.tgz#089412221a6ca6769e9fe9af95c12b0fbebcce93" + integrity sha512-L2uOGtvp2x3bTcxFTpSM+GkwFIPd8pHfGWO1764icMbo7e5xJh0nfhx1UwkXLnwvocTNEf8A7jISZLYjUSNaTg== dependencies: - "@aws-sdk/core" "3.970.0" - "@aws-sdk/types" "3.969.0" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/types" "^3.973.1" "@smithy/fetch-http-handler" "^5.3.9" - "@smithy/node-http-handler" "^4.4.8" + "@smithy/node-http-handler" "^4.4.9" "@smithy/property-provider" "^4.2.8" "@smithy/protocol-http" "^5.3.8" - "@smithy/smithy-client" "^4.10.8" + "@smithy/smithy-client" "^4.11.2" "@smithy/types" "^4.12.0" - "@smithy/util-stream" "^4.5.10" + "@smithy/util-stream" "^4.5.11" tslib "^2.6.2" -"@aws-sdk/credential-provider-ini@3.971.0": - version "3.971.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.971.0.tgz#470055fe0d575ab7edeadc0ff6adc127d2f75166" - integrity sha512-c0TGJG4xyfTZz3SInXfGU8i5iOFRrLmy4Bo7lMyH+IpngohYMYGYl61omXqf2zdwMbDv+YJ9AviQTcCaEUKi8w== - dependencies: - "@aws-sdk/core" "3.970.0" - "@aws-sdk/credential-provider-env" "3.970.0" - "@aws-sdk/credential-provider-http" "3.970.0" - "@aws-sdk/credential-provider-login" "3.971.0" - "@aws-sdk/credential-provider-process" "3.970.0" - "@aws-sdk/credential-provider-sso" "3.971.0" - "@aws-sdk/credential-provider-web-identity" "3.971.0" - "@aws-sdk/nested-clients" "3.971.0" - "@aws-sdk/types" "3.969.0" +"@aws-sdk/credential-provider-ini@^3.972.5": + version "3.972.5" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.5.tgz#520754da94d91e801ea16303806c93b86ef11444" + integrity sha512-SdDTYE6jkARzOeL7+kudMIM4DaFnP5dZVeatzw849k4bSXDdErDS188bgeNzc/RA2WGrlEpsqHUKP6G7sVXhZg== + dependencies: + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/credential-provider-env" "^3.972.5" + "@aws-sdk/credential-provider-http" "^3.972.7" + "@aws-sdk/credential-provider-login" "^3.972.5" + "@aws-sdk/credential-provider-process" "^3.972.5" + "@aws-sdk/credential-provider-sso" "^3.972.5" + "@aws-sdk/credential-provider-web-identity" "^3.972.5" + "@aws-sdk/nested-clients" "3.985.0" + "@aws-sdk/types" "^3.973.1" "@smithy/credential-provider-imds" "^4.2.8" "@smithy/property-provider" "^4.2.8" "@smithy/shared-ini-file-loader" "^4.4.3" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-login@3.971.0": - version "3.971.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-login/-/credential-provider-login-3.971.0.tgz#ac9c405402d235ab7da134b53190fad78d4fe9d9" - integrity sha512-yhbzmDOsk0RXD3rTPhZra4AWVnVAC4nFWbTp+sUty1hrOPurUmhuz8bjpLqYTHGnlMbJp+UqkQONhS2+2LzW2g== +"@aws-sdk/credential-provider-login@^3.972.5": + version "3.972.5" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.5.tgz#5677d7a829e5b9a14e37d5784f944cb2c513e082" + integrity sha512-uYq1ILyTSI6ZDCMY5+vUsRM0SOCVI7kaW4wBrehVVkhAxC6y+e9rvGtnoZqCOWL1gKjTMouvsf4Ilhc5NCg1Aw== dependencies: - "@aws-sdk/core" "3.970.0" - "@aws-sdk/nested-clients" "3.971.0" - "@aws-sdk/types" "3.969.0" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/nested-clients" "3.985.0" + "@aws-sdk/types" "^3.973.1" "@smithy/property-provider" "^4.2.8" "@smithy/protocol-http" "^5.3.8" "@smithy/shared-ini-file-loader" "^4.4.3" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-node@3.971.0": - version "3.971.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.971.0.tgz#817fe3b4730837fb970d7465106ec5b764c8dc8e" - integrity sha512-epUJBAKivtJqalnEBRsYIULKYV063o/5mXNJshZfyvkAgNIzc27CmmKRXTN4zaNOZg8g/UprFp25BGsi19x3nQ== - dependencies: - "@aws-sdk/credential-provider-env" "3.970.0" - "@aws-sdk/credential-provider-http" "3.970.0" - "@aws-sdk/credential-provider-ini" "3.971.0" - "@aws-sdk/credential-provider-process" "3.970.0" - "@aws-sdk/credential-provider-sso" "3.971.0" - "@aws-sdk/credential-provider-web-identity" "3.971.0" - "@aws-sdk/types" "3.969.0" +"@aws-sdk/credential-provider-node@^3.972.6": + version "3.972.6" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.6.tgz#ac44d928df3e4598263d8b35a6ad6785f65a3ecd" + integrity sha512-DZ3CnAAtSVtVz+G+ogqecaErMLgzph4JH5nYbHoBMgBkwTUV+SUcjsjOJwdBJTHu3Dm6l5LBYekZoU2nDqQk2A== + dependencies: + "@aws-sdk/credential-provider-env" "^3.972.5" + "@aws-sdk/credential-provider-http" "^3.972.7" + "@aws-sdk/credential-provider-ini" "^3.972.5" + "@aws-sdk/credential-provider-process" "^3.972.5" + "@aws-sdk/credential-provider-sso" "^3.972.5" + "@aws-sdk/credential-provider-web-identity" "^3.972.5" + "@aws-sdk/types" "^3.973.1" "@smithy/credential-provider-imds" "^4.2.8" "@smithy/property-provider" "^4.2.8" "@smithy/shared-ini-file-loader" "^4.4.3" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-process@3.970.0": - version "3.970.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.970.0.tgz#b4eddf6a544bf174b79f009a21ffea0e74cb8c12" - integrity sha512-0XeT8OaT9iMA62DFV9+m6mZfJhrD0WNKf4IvsIpj2Z7XbaYfz3CoDDvNoALf3rPY9NzyMHgDxOspmqdvXP00mw== +"@aws-sdk/credential-provider-process@^3.972.5": + version "3.972.5" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.5.tgz#6851a7170a1625e661600f5954b1cbe21dcf1eb4" + integrity sha512-HDKF3mVbLnuqGg6dMnzBf1VUOywE12/N286msI9YaK9mEIzdsGCtLTvrDhe3Up0R9/hGFbB+9l21/TwF5L1C6g== dependencies: - "@aws-sdk/core" "3.970.0" - "@aws-sdk/types" "3.969.0" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/types" "^3.973.1" "@smithy/property-provider" "^4.2.8" "@smithy/shared-ini-file-loader" "^4.4.3" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-sso@3.971.0": - version "3.971.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.971.0.tgz#b67232e3055dcc798b4aeeb75364a3d548aee4d8" - integrity sha512-dY0hMQ7dLVPQNJ8GyqXADxa9w5wNfmukgQniLxGVn+dMRx3YLViMp5ZpTSQpFhCWNF0oKQrYAI5cHhUJU1hETw== +"@aws-sdk/credential-provider-sso@^3.972.5": + version "3.972.5" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.5.tgz#a2ee1952f045ccfdef7527cf0f763533a23ba8ab" + integrity sha512-8urj3AoeNeQisjMmMBhFeiY2gxt6/7wQQbEGun0YV/OaOOiXrIudTIEYF8ZfD+NQI6X1FY5AkRsx6O/CaGiybA== dependencies: - "@aws-sdk/client-sso" "3.971.0" - "@aws-sdk/core" "3.970.0" - "@aws-sdk/token-providers" "3.971.0" - "@aws-sdk/types" "3.969.0" + "@aws-sdk/client-sso" "3.985.0" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/token-providers" "3.985.0" + "@aws-sdk/types" "^3.973.1" "@smithy/property-provider" "^4.2.8" "@smithy/shared-ini-file-loader" "^4.4.3" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-web-identity@3.971.0": - version "3.971.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.971.0.tgz#6d27da9193835ddd3c2c681827f9ed96a706e534" - integrity sha512-F1AwfNLr7H52T640LNON/h34YDiMuIqW/ZreGzhRR6vnFGaSPtNSKAKB2ssAMkLM8EVg8MjEAYD3NCUiEo+t/w== +"@aws-sdk/credential-provider-web-identity@^3.972.5": + version "3.972.5" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.5.tgz#c9aaaa412382802418d610828034b77051e98370" + integrity sha512-OK3cULuJl6c+RcDZfPpaK5o3deTOnKZbxm7pzhFNGA3fI2hF9yDih17fGRazJzGGWaDVlR9ejZrpDef4DJCEsw== dependencies: - "@aws-sdk/core" "3.970.0" - "@aws-sdk/nested-clients" "3.971.0" - "@aws-sdk/types" "3.969.0" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/nested-clients" "3.985.0" + "@aws-sdk/types" "^3.973.1" "@smithy/property-provider" "^4.2.8" "@smithy/shared-ini-file-loader" "^4.4.3" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/dynamodb-codec@3.970.0": - version "3.970.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.970.0.tgz#8fca48335605e5c3298424c8a1147de9cdfed412" - integrity sha512-MzNk7vu7aQcJ7dG33wS5DfW5JwOnXEnNYA6x8a/rA20dbiXZ+JctspeIk9rKetMU0Q3MougCs5AM+ddxJkbFXA== +"@aws-sdk/dynamodb-codec@^3.972.8": + version "3.972.8" + resolved "https://registry.yarnpkg.com/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.972.8.tgz#fc9d349984bcdda430acd44cbd94c317526c4d64" + integrity sha512-5ngfn6fQPSNc7G9LlingK4SXfzcJtv5pOP++erc7HmCq0LcDj//0pcpLgxpDII0sBTh0FcR/iw9i4fBZwSJ2Cg== dependencies: - "@aws-sdk/core" "3.970.0" - "@smithy/core" "^3.20.6" - "@smithy/smithy-client" "^4.10.8" + "@aws-sdk/core" "^3.973.7" + "@smithy/core" "^3.22.1" + "@smithy/smithy-client" "^4.11.2" "@smithy/types" "^4.12.0" "@smithy/util-base64" "^4.3.0" tslib "^2.6.2" -"@aws-sdk/endpoint-cache@3.971.0": - version "3.971.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/endpoint-cache/-/endpoint-cache-3.971.0.tgz#8f70755456e76c5944ef6b587170560ffde8acb2" - integrity sha512-bdDUWVMIe2DldrXubEqr5oGpV7VkNbKEPHltGH1D1XwOjSiHWker0HuUVNBGk31iBoWtA6RAfMhabyZGlNskqA== +"@aws-sdk/endpoint-cache@^3.972.2": + version "3.972.2" + resolved "https://registry.yarnpkg.com/@aws-sdk/endpoint-cache/-/endpoint-cache-3.972.2.tgz#87acd8ea94e29e57fd78f87bdaaa9e1f944fbacc" + integrity sha512-3L7mwqSLJ6ouZZKtCntoNF0HTYDNs1FDQqkGjoPWXcv1p0gnLotaDmLq1rIDqfu4ucOit0Re3ioLyYDUTpSroA== dependencies: mnemonist "0.38.3" tslib "^2.6.2" -"@aws-sdk/middleware-bucket-endpoint@3.969.0": - version "3.969.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.969.0.tgz#806dd79c406a689332c6f8b3d9b948eb8dae9bb8" - integrity sha512-MlbrlixtkTVhYhoasblKOkr7n2yydvUZjjxTnBhIuHmkyBS1619oGnTfq/uLeGYb4NYXdeQ5OYcqsRGvmWSuTw== +"@aws-sdk/middleware-bucket-endpoint@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.3.tgz#158507d55505e5e7b5b8cdac9f037f6aa326f202" + integrity sha512-fmbgWYirF67YF1GfD7cg5N6HHQ96EyRNx/rDIrTF277/zTWVuPI2qS/ZHgofwR1NZPe/NWvoppflQY01LrbVLg== dependencies: - "@aws-sdk/types" "3.969.0" - "@aws-sdk/util-arn-parser" "3.968.0" + "@aws-sdk/types" "^3.973.1" + "@aws-sdk/util-arn-parser" "^3.972.2" "@smithy/node-config-provider" "^4.3.8" "@smithy/protocol-http" "^5.3.8" "@smithy/types" "^4.12.0" "@smithy/util-config-provider" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/middleware-endpoint-discovery@3.971.0": - version "3.971.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.971.0.tgz#4a0ef37f7e278bcdd2a076bf7a7f3ce4ef33bb41" - integrity sha512-Q93j2R1mMydxrDA0WZ3JtgtIpW9YHdxV9GjlrBTSaeWKt/jncC4uy2MedEYOMbDTB+CUojxGOwK4FllDFeIs3g== +"@aws-sdk/middleware-endpoint-discovery@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.972.3.tgz#96e1cad8447d756ceaf143d96a367be24b1e6d4e" + integrity sha512-xAxA8/TOygQmMrzcw9CrlpTHCGWSG/lvzrHCySfSZpDN4/yVSfXO+gUwW9WxeskBmuv9IIFATOVpzc9EzfTZ0Q== dependencies: - "@aws-sdk/endpoint-cache" "3.971.0" - "@aws-sdk/types" "3.969.0" + "@aws-sdk/endpoint-cache" "^3.972.2" + "@aws-sdk/types" "^3.973.1" "@smithy/node-config-provider" "^4.3.8" "@smithy/protocol-http" "^5.3.8" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/middleware-expect-continue@3.969.0": - version "3.969.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.969.0.tgz#b040eca51f73681280ea9c39e20728558355e1e8" - integrity sha512-qXygzSi8osok7tH9oeuS3HoKw6jRfbvg5Me/X5RlHOvSSqQz8c5O9f3MjUApaCUSwbAU92KrbZWasw2PKiaVHg== +"@aws-sdk/middleware-expect-continue@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.3.tgz#c60bd81e81dde215b9f3f67e3c5448b608afd530" + integrity sha512-4msC33RZsXQpUKR5QR4HnvBSNCPLGHmB55oDiROqqgyOc+TOfVu2xgi5goA7ms6MdZLeEh2905UfWMnMMF4mRg== dependencies: - "@aws-sdk/types" "3.969.0" + "@aws-sdk/types" "^3.973.1" "@smithy/protocol-http" "^5.3.8" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/middleware-flexible-checksums@3.971.0": - version "3.971.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.971.0.tgz#8cb3a29bd0569e086c8c2393827b9b8682f845d3" - integrity sha512-+hGUDUxeIw8s2kkjfeXym0XZxdh0cqkHkDpEanWYdS1gnWkIR+gf9u/DKbKqGHXILPaqHXhWpLTQTVlaB4sI7Q== +"@aws-sdk/middleware-flexible-checksums@^3.972.5": + version "3.972.5" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.972.5.tgz#08391e6a407a6894e105dca37126dac1f969c107" + integrity sha512-SF/1MYWx67OyCrLA4icIpWUfCkdlOi8Y1KecQ9xYxkL10GMjVdPTGPnYhAg0dw5U43Y9PVUWhAV2ezOaG+0BLg== dependencies: "@aws-crypto/crc32" "5.2.0" "@aws-crypto/crc32c" "5.2.0" "@aws-crypto/util" "5.2.0" - "@aws-sdk/core" "3.970.0" - "@aws-sdk/crc64-nvme" "3.969.0" - "@aws-sdk/types" "3.969.0" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/crc64-nvme" "3.972.0" + "@aws-sdk/types" "^3.973.1" "@smithy/is-array-buffer" "^4.2.0" "@smithy/node-config-provider" "^4.3.8" "@smithy/protocol-http" "^5.3.8" "@smithy/types" "^4.12.0" "@smithy/util-middleware" "^4.2.8" - "@smithy/util-stream" "^4.5.10" + "@smithy/util-stream" "^4.5.11" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/middleware-host-header@3.969.0": - version "3.969.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.969.0.tgz#e9f09254c7c4122cd846b7a679823a6643f1fefc" - integrity sha512-AWa4rVsAfBR4xqm7pybQ8sUNJYnjyP/bJjfAw34qPuh3M9XrfGbAHG0aiAfQGrBnmS28jlO6Kz69o+c6PRw1dw== +"@aws-sdk/middleware-host-header@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.3.tgz#47c161dec62d89c66c89f4d17ff4434021e04af5" + integrity sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA== dependencies: - "@aws-sdk/types" "3.969.0" + "@aws-sdk/types" "^3.973.1" "@smithy/protocol-http" "^5.3.8" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/middleware-location-constraint@3.969.0": - version "3.969.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.969.0.tgz#6530b94097d22b5ef69fffda8d194a2f55f6980a" - integrity sha512-zH7pDfMLG/C4GWMOpvJEoYcSpj7XsNP9+irlgqwi667sUQ6doHQJ3yyDut3yiTk0maq1VgmriPFELyI9lrvH/g== +"@aws-sdk/middleware-location-constraint@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.3.tgz#b4f504f75baa19064b7457e5c6e3c8cecb4c32eb" + integrity sha512-nIg64CVrsXp67vbK0U1/Is8rik3huS3QkRHn2DRDx4NldrEFMgdkZGI/+cZMKD9k4YOS110Dfu21KZLHrFA/1g== dependencies: - "@aws-sdk/types" "3.969.0" + "@aws-sdk/types" "^3.973.1" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/middleware-logger@3.969.0": - version "3.969.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.969.0.tgz#9beec897cc10611ffda8d25da5fde7364d9e9cc1" - integrity sha512-xwrxfip7Y2iTtCMJ+iifN1E1XMOuhxIHY9DreMCvgdl4r7+48x2S1bCYPWH3eNY85/7CapBWdJ8cerpEl12sQQ== +"@aws-sdk/middleware-logger@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.972.3.tgz#ef1afd4a0b70fe72cf5f7c817f82da9f35c7e836" + integrity sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA== dependencies: - "@aws-sdk/types" "3.969.0" + "@aws-sdk/types" "^3.973.1" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/middleware-recursion-detection@3.969.0": - version "3.969.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.969.0.tgz#68fcfcde8f2fce448d754b45b2a0bec7158da0f8" - integrity sha512-2r3PuNquU3CcS1Am4vn/KHFwLi8QFjMdA/R+CRDXT4AFO/0qxevF/YStW3gAKntQIgWgQV8ZdEtKAoJvLI4UWg== +"@aws-sdk/middleware-recursion-detection@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.3.tgz#5b95dcecff76a0d2963bd954bdef87700d1b1c8c" + integrity sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q== dependencies: - "@aws-sdk/types" "3.969.0" + "@aws-sdk/types" "^3.973.1" "@aws/lambda-invoke-store" "^0.2.2" "@smithy/protocol-http" "^5.3.8" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/middleware-sdk-s3@3.970.0": - version "3.970.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.970.0.tgz#90aba70362f367e588d1e6735846056d94dce626" - integrity sha512-v/Y5F1lbFFY7vMeG5yYxuhnn0CAshz6KMxkz1pDyPxejNE9HtA0w8R6OTBh/bVdIm44QpjhbI7qeLdOE/PLzXQ== +"@aws-sdk/middleware-sdk-s3@^3.972.7": + version "3.972.7" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.7.tgz#eb77744a4533fb289e7278b8e0fa40816f53c309" + integrity sha512-VtZ7tMIw18VzjG+I6D6rh2eLkJfTtByiFoCIauGDtTTPBEUMQUiGaJ/zZrPlCY6BsvLLeFKz3+E5mntgiOWmIg== dependencies: - "@aws-sdk/core" "3.970.0" - "@aws-sdk/types" "3.969.0" - "@aws-sdk/util-arn-parser" "3.968.0" - "@smithy/core" "^3.20.6" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/types" "^3.973.1" + "@aws-sdk/util-arn-parser" "^3.972.2" + "@smithy/core" "^3.22.1" "@smithy/node-config-provider" "^4.3.8" "@smithy/protocol-http" "^5.3.8" "@smithy/signature-v4" "^5.3.8" - "@smithy/smithy-client" "^4.10.8" + "@smithy/smithy-client" "^4.11.2" "@smithy/types" "^4.12.0" "@smithy/util-config-provider" "^4.2.0" "@smithy/util-middleware" "^4.2.8" - "@smithy/util-stream" "^4.5.10" + "@smithy/util-stream" "^4.5.11" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/middleware-ssec@3.971.0": - version "3.971.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.971.0.tgz#71586813a653af64241ad850323f21ab687e7721" - integrity sha512-QGVhvRveYG64ZhnS/b971PxXM6N2NU79Fxck4EfQ7am8v1Br0ctoeDDAn9nXNblLGw87we9Z65F7hMxxiFHd3w== +"@aws-sdk/middleware-ssec@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.3.tgz#4f81d310fd91164e6e18ba3adab6bcf906920333" + integrity sha512-dU6kDuULN3o3jEHcjm0c4zWJlY1zWVkjG9NPe9qxYLLpcbdj5kRYBS2DdWYD+1B9f910DezRuws7xDEqKkHQIg== dependencies: - "@aws-sdk/types" "3.969.0" + "@aws-sdk/types" "^3.973.1" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/middleware-user-agent@3.970.0": - version "3.970.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.970.0.tgz#ebaa023ac89e91200dc5f0218c3c6bfa8ffba478" - integrity sha512-dnSJGGUGSFGEX2NzvjwSefH+hmZQ347AwbLhAsi0cdnISSge+pcGfOFrJt2XfBIypwFe27chQhlfuf/gWdzpZg== +"@aws-sdk/middleware-user-agent@^3.972.7": + version "3.972.7" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.7.tgz#3c49e740d6e7b594952a5e340925e0a0bfde4777" + integrity sha512-HUD+geASjXSCyL/DHPQc/Ua7JhldTcIglVAoCV8kiVm99IaFSlAbTvEnyhZwdE6bdFyTL+uIaWLaCFSRsglZBQ== dependencies: - "@aws-sdk/core" "3.970.0" - "@aws-sdk/types" "3.969.0" - "@aws-sdk/util-endpoints" "3.970.0" - "@smithy/core" "^3.20.6" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/types" "^3.973.1" + "@aws-sdk/util-endpoints" "3.985.0" + "@smithy/core" "^3.22.1" "@smithy/protocol-http" "^5.3.8" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/nested-clients@3.971.0": - version "3.971.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.971.0.tgz#391fe4c91357f088a4f2c301b9b58c117482a36a" - integrity sha512-TWaILL8GyYlhGrxxnmbkazM4QsXatwQgoWUvo251FXmUOsiXDFDVX3hoGIfB3CaJhV2pJPfebHUNJtY6TjZ11g== +"@aws-sdk/nested-clients@3.985.0": + version "3.985.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.985.0.tgz#b67ee7500dc3e2306e06ff7fa02badae154c3231" + integrity sha512-TsWwKzb/2WHafAY0CE7uXgLj0FmnkBTgfioG9HO+7z/zCPcl1+YU+i7dW4o0y+aFxFgxTMG+ExBQpqT/k2ao8g== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "3.970.0" - "@aws-sdk/middleware-host-header" "3.969.0" - "@aws-sdk/middleware-logger" "3.969.0" - "@aws-sdk/middleware-recursion-detection" "3.969.0" - "@aws-sdk/middleware-user-agent" "3.970.0" - "@aws-sdk/region-config-resolver" "3.969.0" - "@aws-sdk/types" "3.969.0" - "@aws-sdk/util-endpoints" "3.970.0" - "@aws-sdk/util-user-agent-browser" "3.969.0" - "@aws-sdk/util-user-agent-node" "3.971.0" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/middleware-host-header" "^3.972.3" + "@aws-sdk/middleware-logger" "^3.972.3" + "@aws-sdk/middleware-recursion-detection" "^3.972.3" + "@aws-sdk/middleware-user-agent" "^3.972.7" + "@aws-sdk/region-config-resolver" "^3.972.3" + "@aws-sdk/types" "^3.973.1" + "@aws-sdk/util-endpoints" "3.985.0" + "@aws-sdk/util-user-agent-browser" "^3.972.3" + "@aws-sdk/util-user-agent-node" "^3.972.5" "@smithy/config-resolver" "^4.4.6" - "@smithy/core" "^3.20.6" + "@smithy/core" "^3.22.1" "@smithy/fetch-http-handler" "^5.3.9" "@smithy/hash-node" "^4.2.8" "@smithy/invalid-dependency" "^4.2.8" "@smithy/middleware-content-length" "^4.2.8" - "@smithy/middleware-endpoint" "^4.4.7" - "@smithy/middleware-retry" "^4.4.23" + "@smithy/middleware-endpoint" "^4.4.13" + "@smithy/middleware-retry" "^4.4.30" "@smithy/middleware-serde" "^4.2.9" "@smithy/middleware-stack" "^4.2.8" "@smithy/node-config-provider" "^4.3.8" - "@smithy/node-http-handler" "^4.4.8" + "@smithy/node-http-handler" "^4.4.9" "@smithy/protocol-http" "^5.3.8" - "@smithy/smithy-client" "^4.10.8" + "@smithy/smithy-client" "^4.11.2" "@smithy/types" "^4.12.0" "@smithy/url-parser" "^4.2.8" "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" "@smithy/util-body-length-node" "^4.2.1" - "@smithy/util-defaults-mode-browser" "^4.3.22" - "@smithy/util-defaults-mode-node" "^4.2.25" + "@smithy/util-defaults-mode-browser" "^4.3.29" + "@smithy/util-defaults-mode-node" "^4.2.32" "@smithy/util-endpoints" "^3.2.8" "@smithy/util-middleware" "^4.2.8" "@smithy/util-retry" "^4.2.8" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/region-config-resolver@3.969.0": - version "3.969.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.969.0.tgz#fecbbeb688050a4ec59f32353f679a29c61f8e70" - integrity sha512-scj9OXqKpcjJ4jsFLtqYWz3IaNvNOQTFFvEY8XMJXTv+3qF5I7/x9SJtKzTRJEBF3spjzBUYPtGFbs9sj4fisQ== +"@aws-sdk/region-config-resolver@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.3.tgz#25af64235ca6f4b6b21f85d4b3c0b432efc4ae04" + integrity sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow== dependencies: - "@aws-sdk/types" "3.969.0" + "@aws-sdk/types" "^3.973.1" "@smithy/config-resolver" "^4.4.6" "@smithy/node-config-provider" "^4.3.8" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/signature-v4-multi-region@3.970.0": - version "3.970.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.970.0.tgz#f785fb5ff9f9914cbcbcbe8f9745d92f8d85a185" - integrity sha512-z3syXfuK/x/IsKf/AeYmgc2NT7fcJ+3fHaGO+fkghkV9WEba3fPyOwtTBX4KpFMNb2t50zDGZwbzW1/5ighcUQ== +"@aws-sdk/signature-v4-multi-region@3.985.0": + version "3.985.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.985.0.tgz#0c6ca19831602c39da427209eba189ca1f26961f" + integrity sha512-W6hTSOPiSbh4IdTYVxN7xHjpCh0qvfQU1GKGBzGQm0ZEIOaMmWqiDEvFfyGYKmfBvumT8vHKxQRTX0av9omtIg== dependencies: - "@aws-sdk/middleware-sdk-s3" "3.970.0" - "@aws-sdk/types" "3.969.0" + "@aws-sdk/middleware-sdk-s3" "^3.972.7" + "@aws-sdk/types" "^3.973.1" "@smithy/protocol-http" "^5.3.8" "@smithy/signature-v4" "^5.3.8" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/token-providers@3.971.0": - version "3.971.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.971.0.tgz#7d0ae8c0ca50ea5ef8cfbfeba180044e5d569603" - integrity sha512-4hKGWZbmuDdONMJV0HJ+9jwTDb0zLfKxcCLx2GEnBY31Gt9GeyIQ+DZ97Bb++0voawj6pnZToFikXTyrEq2x+w== +"@aws-sdk/token-providers@3.985.0": + version "3.985.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.985.0.tgz#bd700b624093352d25ff564457000ee3efdc6522" + integrity sha512-+hwpHZyEq8k+9JL2PkE60V93v2kNhUIv7STFt+EAez1UJsJOQDhc5LpzEX66pNjclI5OTwBROs/DhJjC/BtMjQ== dependencies: - "@aws-sdk/core" "3.970.0" - "@aws-sdk/nested-clients" "3.971.0" - "@aws-sdk/types" "3.969.0" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/nested-clients" "3.985.0" + "@aws-sdk/types" "^3.973.1" "@smithy/property-provider" "^4.2.8" "@smithy/shared-ini-file-loader" "^4.4.3" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/types@3.969.0", "@aws-sdk/types@^3.222.0": - version "3.969.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.969.0.tgz#d991e5d15b68a815e5cf739b7fab59212306a19c" - integrity sha512-7IIzM5TdiXn+VtgPdVLjmE6uUBUtnga0f4RiSEI1WW10RPuNvZ9U+pL3SwDiRDAdoGrOF9tSLJOFZmfuwYuVYQ== +"@aws-sdk/types@^3.222.0", "@aws-sdk/types@^3.973.1": + version "3.973.1" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.973.1.tgz#1b2992ec6c8380c3e74c9bd2c74703e9a807d6e0" + integrity sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg== dependencies: "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/util-arn-parser@3.968.0": - version "3.968.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.968.0.tgz#ca8d7d26ffafa340a9e441a40db886ddb587743f" - integrity sha512-gqqvYcitIIM2K4lrDX9de9YvOfXBcVdxfT/iLnvHJd4YHvSXlt+gs+AsL4FfPCxG4IG9A+FyulP9Sb1MEA75vw== +"@aws-sdk/util-arn-parser@^3.972.2": + version "3.972.2" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.2.tgz#ef18ba889e8ef35f083f1e962018bc0ce70acef3" + integrity sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg== dependencies: tslib "^2.6.2" "@aws-sdk/util-dynamodb@^3.901.0": - version "3.971.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-dynamodb/-/util-dynamodb-3.971.0.tgz#36cc22535fe18dd7680971ea989738e053e4920c" - integrity sha512-Q/ngcS9xYyi1Gg1WIO8Z1h9vuOVVSJxBiwlIflAfK3Rfd+4MMnhiNyG98PHPNmHvX/a9K/5ikg/gzZUDPS7Tug== + version "3.985.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-dynamodb/-/util-dynamodb-3.985.0.tgz#1f7cb9eeb3ff02dddd7c8517c4d4b6ae31309cbc" + integrity sha512-bf+DvndbrtbNgGtFT4kqDRC5Udi3W1C3Q4028n1i1+PRbRGPLVaXkfsis393YmiyIHca0WPRsCH9/dE9ROlbuw== dependencies: tslib "^2.6.2" -"@aws-sdk/util-endpoints@3.970.0": - version "3.970.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.970.0.tgz#3c94f9482091b35b0497962c6d03e2d6e7f8f86a" - integrity sha512-TZNZqFcMUtjvhZoZRtpEGQAdULYiy6rcGiXAbLU7e9LSpIYlRqpLa207oMNfgbzlL2PnHko+eVg8rajDiSOYCg== +"@aws-sdk/util-endpoints@3.985.0": + version "3.985.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.985.0.tgz#a6393811e86aaf71e17aa3313551c19e54ed59f8" + integrity sha512-vth7UfGSUR3ljvaq8V4Rc62FsM7GUTH/myxPWkaEgOrprz1/Pc72EgTXxj+cPPPDAfHFIpjhkB7T7Td0RJx+BA== dependencies: - "@aws-sdk/types" "3.969.0" + "@aws-sdk/types" "^3.973.1" "@smithy/types" "^4.12.0" "@smithy/url-parser" "^4.2.8" "@smithy/util-endpoints" "^3.2.8" tslib "^2.6.2" "@aws-sdk/util-locate-window@^3.0.0": - version "3.965.2" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-locate-window/-/util-locate-window-3.965.2.tgz#e3fde1227b2f76b94e33650cb4bfa391738a26dc" - integrity sha512-qKgO7wAYsXzhwCHhdbaKFyxd83Fgs8/1Ka+jjSPrv2Ll7mB55Wbwlo0kkfMLh993/yEc8aoDIAc1Fz9h4Spi4Q== + version "3.965.4" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-locate-window/-/util-locate-window-3.965.4.tgz#f62d279e1905f6939b6dffb0f76ab925440f72bf" + integrity sha512-H1onv5SkgPBK2P6JR2MjGgbOnttoNzSPIRoeZTNPZYyaplwGg50zS3amXvXqF0/qfXpWEC9rLWU564QTB9bSog== dependencies: tslib "^2.6.2" -"@aws-sdk/util-user-agent-browser@3.969.0": - version "3.969.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.969.0.tgz#5d86cbf8346b93427a4e1d7dddc1237c4fbe3f75" - integrity sha512-bpJGjuKmFr0rA6UKUCmN8D19HQFMLXMx5hKBXqBlPFdalMhxJSjcxzX9DbQh0Fn6bJtxCguFmRGOBdQqNOt49g== +"@aws-sdk/util-user-agent-browser@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.3.tgz#1363b388cb3af86c5322ef752c0cf8d7d25efa8a" + integrity sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw== dependencies: - "@aws-sdk/types" "3.969.0" + "@aws-sdk/types" "^3.973.1" "@smithy/types" "^4.12.0" bowser "^2.11.0" tslib "^2.6.2" -"@aws-sdk/util-user-agent-node@3.971.0": - version "3.971.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.971.0.tgz#ba1bcd65fa6bb66e3dd896805ebdcfdb58f7f0a8" - integrity sha512-Eygjo9mFzQYjbGY3MYO6CsIhnTwAMd3WmuFalCykqEmj2r5zf0leWrhPaqvA5P68V5JdGfPYgj7vhNOd6CtRBQ== +"@aws-sdk/util-user-agent-node@^3.972.5": + version "3.972.5" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.5.tgz#fccbe3b707a9d9bb7a755f7164f4f87dbeb81f84" + integrity sha512-GsUDF+rXyxDZkkJxUsDxnA67FG+kc5W1dnloCFLl6fWzceevsCYzJpASBzT+BPjwUgREE6FngfJYYYMQUY5fZQ== dependencies: - "@aws-sdk/middleware-user-agent" "3.970.0" - "@aws-sdk/types" "3.969.0" + "@aws-sdk/middleware-user-agent" "^3.972.7" + "@aws-sdk/types" "^3.973.1" "@smithy/node-config-provider" "^4.3.8" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/xml-builder@3.969.0": - version "3.969.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.969.0.tgz#907de7ca729c80351de4e5837fd30926fd84366f" - integrity sha512-BSe4Lx/qdRQQdX8cSSI7Et20vqBspzAjBy8ZmXVoyLkol3y4sXBXzn+BiLtR+oh60ExQn6o2DU4QjdOZbXaKIQ== +"@aws-sdk/xml-builder@^3.972.4": + version "3.972.4" + resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.972.4.tgz#8115c8cf90c71cf484a52c82eac5344cd3a5e921" + integrity sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q== dependencies: "@smithy/types" "^4.12.0" - fast-xml-parser "5.2.5" + fast-xml-parser "5.3.4" tslib "^2.6.2" "@aws/lambda-invoke-store@0.2.3", "@aws/lambda-invoke-store@^0.2.2": @@ -740,34 +740,34 @@ resolved "https://registry.yarnpkg.com/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz#f1137f56209ccc69c15f826242cbf37f828617dd" integrity sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw== -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.27.1", "@babel/code-frame@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.28.6.tgz#72499312ec58b1e2245ba4a4f550c132be4982f7" - integrity sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.27.1", "@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" + integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== dependencies: "@babel/helper-validator-identifier" "^7.28.5" js-tokens "^4.0.0" picocolors "^1.1.1" "@babel/compat-data@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.6.tgz#103f466803fa0f059e82ccac271475470570d74c" - integrity sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg== + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.0.tgz#00d03e8c0ac24dd9be942c5370990cbe1f17d88d" + integrity sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg== "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.6.tgz#531bf883a1126e53501ba46eb3bb414047af507f" - integrity sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw== + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.0.tgz#5286ad785df7f79d656e88ce86e650d16ca5f322" + integrity sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA== dependencies: - "@babel/code-frame" "^7.28.6" - "@babel/generator" "^7.28.6" + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" "@babel/helper-compilation-targets" "^7.28.6" "@babel/helper-module-transforms" "^7.28.6" "@babel/helpers" "^7.28.6" - "@babel/parser" "^7.28.6" + "@babel/parser" "^7.29.0" "@babel/template" "^7.28.6" - "@babel/traverse" "^7.28.6" - "@babel/types" "^7.28.6" + "@babel/traverse" "^7.29.0" + "@babel/types" "^7.29.0" "@jridgewell/remapping" "^2.3.5" convert-source-map "^2.0.0" debug "^4.1.0" @@ -775,13 +775,13 @@ json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.28.6", "@babel/generator@^7.7.2": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.6.tgz#48dcc65d98fcc8626a48f72b62e263d25fc3c3f1" - integrity sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw== +"@babel/generator@^7.29.0", "@babel/generator@^7.7.2": + version "7.29.1" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.1.tgz#d09876290111abbb00ef962a7b83a5307fba0d50" + integrity sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw== dependencies: - "@babel/parser" "^7.28.6" - "@babel/types" "^7.28.6" + "@babel/parser" "^7.29.0" + "@babel/types" "^7.29.0" "@jridgewell/gen-mapping" "^0.3.12" "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" @@ -847,12 +847,12 @@ "@babel/template" "^7.28.6" "@babel/types" "^7.28.6" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.6.tgz#f01a8885b7fa1e56dd8a155130226cd698ef13fd" - integrity sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ== +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.28.6", "@babel/parser@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.0.tgz#669ef345add7d057e92b7ed15f0bac07611831b6" + integrity sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww== dependencies: - "@babel/types" "^7.28.6" + "@babel/types" "^7.29.0" "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" @@ -982,23 +982,23 @@ "@babel/parser" "^7.28.6" "@babel/types" "^7.28.6" -"@babel/traverse@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.6.tgz#871ddc79a80599a5030c53b1cc48cbe3a5583c2e" - integrity sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg== +"@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a" + integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA== dependencies: - "@babel/code-frame" "^7.28.6" - "@babel/generator" "^7.28.6" + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" "@babel/helper-globals" "^7.28.0" - "@babel/parser" "^7.28.6" + "@babel/parser" "^7.29.0" "@babel/template" "^7.28.6" - "@babel/types" "^7.28.6" + "@babel/types" "^7.29.0" debug "^4.3.1" -"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.28.2", "@babel/types@^7.28.6", "@babel/types@^7.3.3": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.6.tgz#c3e9377f1b155005bcc4c46020e7e394e13089df" - integrity sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg== +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.28.2", "@babel/types@^7.28.6", "@babel/types@^7.29.0", "@babel/types@^7.3.3": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7" + integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A== dependencies: "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.28.5" @@ -1072,9 +1072,9 @@ integrity sha512-lSefWvyrWiez2rNlGIAtELHWckjzh1KnzPo3m5+nHN7O6Krvx5W2RPlYQwKFjDbqHbZjuwgKLjn1LPhyYDQFHw== "@csg-org/email-builder@^0.0.9-alpha.4": - version "0.0.9-alpha.4" - resolved "https://registry.yarnpkg.com/@csg-org/email-builder/-/email-builder-0.0.9-alpha.4.tgz#3786044653abe710b7a72df55497de4ac72d8339" - integrity sha512-6Q7t1gCM45buiZyHDPpLawxZJMqXRXMi/eo1FAcpbJZh9iYXjc2PKbOfCBDoVrJPlwD9dCM1Ak7cIYd8EE1UTg== + version "0.0.9" + resolved "https://registry.yarnpkg.com/@csg-org/email-builder/-/email-builder-0.0.9.tgz#dfeb32adc49885e82cfc9c4f8ccc49182630196e" + integrity sha512-bRkE124gA8lLMSr4sXnEOSwI1/ZyZo2mYJ7oy6ZdhaS/aZ9LVTT+k97gTHnUQtyJ7nEjWLPzdkO7O9XrdC9TYw== dependencies: "@csg-org/block-avatar" "^0.0.4-alpha.4" "@csg-org/block-button" "^0.0.4-alpha.4" @@ -1109,250 +1109,250 @@ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz#b57697945b50e99007b4c2521507dc613d4a648c" integrity sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw== -"@esbuild/aix-ppc64@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz#521cbd968dcf362094034947f76fa1b18d2d403c" - integrity sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw== +"@esbuild/aix-ppc64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz#815b39267f9bffd3407ea6c376ac32946e24f8d2" + integrity sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg== "@esbuild/android-arm64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz#1add7e0af67acefd556e407f8497e81fddad79c0" integrity sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w== -"@esbuild/android-arm64@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz#61ea550962d8aa12a9b33194394e007657a6df57" - integrity sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA== +"@esbuild/android-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz#19b882408829ad8e12b10aff2840711b2da361e8" + integrity sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg== "@esbuild/android-arm@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.0.tgz#ab7263045fa8e090833a8e3c393b60d59a789810" integrity sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew== -"@esbuild/android-arm@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.2.tgz#554887821e009dd6d853f972fde6c5143f1de142" - integrity sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA== +"@esbuild/android-arm@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.3.tgz#90be58de27915efa27b767fcbdb37a4470627d7b" + integrity sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA== "@esbuild/android-x64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.0.tgz#e8f8b196cfdfdd5aeaebbdb0110983460440e705" integrity sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ== -"@esbuild/android-x64@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.2.tgz#a7ce9d0721825fc578f9292a76d9e53334480ba2" - integrity sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A== +"@esbuild/android-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.3.tgz#d7dcc976f16e01a9aaa2f9b938fbec7389f895ac" + integrity sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ== "@esbuild/darwin-arm64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz#2d0d9414f2acbffd2d86e98253914fca603a53dd" integrity sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw== -"@esbuild/darwin-arm64@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz#2cb7659bd5d109803c593cfc414450d5430c8256" - integrity sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg== +"@esbuild/darwin-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz#9f6cac72b3a8532298a6a4493ed639a8988e8abd" + integrity sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg== "@esbuild/darwin-x64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz#33087aab31a1eb64c89daf3d2cf8ce1775656107" integrity sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA== -"@esbuild/darwin-x64@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz#e741fa6b1abb0cd0364126ba34ca17fd5e7bf509" - integrity sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA== +"@esbuild/darwin-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz#ac61d645faa37fd650340f1866b0812e1fb14d6a" + integrity sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg== "@esbuild/freebsd-arm64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz#bb76e5ea9e97fa3c753472f19421075d3a33e8a7" integrity sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA== -"@esbuild/freebsd-arm64@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz#2b64e7116865ca172d4ce034114c21f3c93e397c" - integrity sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g== +"@esbuild/freebsd-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz#b8625689d73cf1830fe58c39051acdc12474ea1b" + integrity sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w== "@esbuild/freebsd-x64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz#e0e2ce9249fdf6ee29e5dc3d420c7007fa579b93" integrity sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ== -"@esbuild/freebsd-x64@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz#e5252551e66f499e4934efb611812f3820e990bb" - integrity sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA== +"@esbuild/freebsd-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz#07be7dd3c9d42fe0eccd2ab9f9ded780bc53bead" + integrity sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA== "@esbuild/linux-arm64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz#d1b2aa58085f73ecf45533c07c82d81235388e75" integrity sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g== -"@esbuild/linux-arm64@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz#dc4acf235531cd6984f5d6c3b13dbfb7ddb303cb" - integrity sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw== +"@esbuild/linux-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz#bf31918fe5c798586460d2b3d6c46ed2c01ca0b6" + integrity sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg== "@esbuild/linux-arm@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz#8e4915df8ea3e12b690a057e77a47b1d5935ef6d" integrity sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw== -"@esbuild/linux-arm@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz#56a900e39240d7d5d1d273bc053daa295c92e322" - integrity sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw== +"@esbuild/linux-arm@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz#28493ee46abec1dc3f500223cd9f8d2df08f9d11" + integrity sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw== "@esbuild/linux-ia32@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz#8200b1110666c39ab316572324b7af63d82013fb" integrity sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA== -"@esbuild/linux-ia32@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz#d4a36d473360f6870efcd19d52bbfff59a2ed1cc" - integrity sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w== +"@esbuild/linux-ia32@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz#750752a8b30b43647402561eea764d0a41d0ee29" + integrity sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg== "@esbuild/linux-loong64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz#6ff0c99cf647504df321d0640f0d32e557da745c" integrity sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g== -"@esbuild/linux-loong64@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz#fcf0ab8c3eaaf45891d0195d4961cb18b579716a" - integrity sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg== +"@esbuild/linux-loong64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz#a5a92813a04e71198c50f05adfaf18fc1e95b9ed" + integrity sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA== "@esbuild/linux-mips64el@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz#3f720ccd4d59bfeb4c2ce276a46b77ad380fa1f3" integrity sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA== -"@esbuild/linux-mips64el@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz#598b67d34048bb7ee1901cb12e2a0a434c381c10" - integrity sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw== +"@esbuild/linux-mips64el@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz#deb45d7fd2d2161eadf1fbc593637ed766d50bb1" + integrity sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw== "@esbuild/linux-ppc64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz#9d6b188b15c25afd2e213474bf5f31e42e3aa09e" integrity sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ== -"@esbuild/linux-ppc64@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz#3846c5df6b2016dab9bc95dde26c40f11e43b4c0" - integrity sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ== +"@esbuild/linux-ppc64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz#6f39ae0b8c4d3d2d61a65b26df79f6e12a1c3d78" + integrity sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA== "@esbuild/linux-riscv64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz#f989fdc9752dfda286c9cd87c46248e4dfecbc25" integrity sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw== -"@esbuild/linux-riscv64@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz#173d4475b37c8d2c3e1707e068c174bb3f53d07d" - integrity sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA== +"@esbuild/linux-riscv64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz#4c5c19c3916612ec8e3915187030b9df0b955c1d" + integrity sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ== "@esbuild/linux-s390x@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz#29ebf87e4132ea659c1489fce63cd8509d1c7319" integrity sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g== -"@esbuild/linux-s390x@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz#f7a4790105edcab8a5a31df26fbfac1aa3dacfab" - integrity sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w== +"@esbuild/linux-s390x@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz#9ed17b3198fa08ad5ccaa9e74f6c0aff7ad0156d" + integrity sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw== "@esbuild/linux-x64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz#4af48c5c0479569b1f359ffbce22d15f261c0cef" integrity sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA== -"@esbuild/linux-x64@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz#2ecc1284b1904aeb41e54c9ddc7fcd349b18f650" - integrity sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA== +"@esbuild/linux-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz#12383dcbf71b7cf6513e58b4b08d95a710bf52a5" + integrity sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA== -"@esbuild/netbsd-arm64@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz#e2863c2cd1501845995cb11adf26f7fe4be527b0" - integrity sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw== +"@esbuild/netbsd-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz#dd0cb2fa543205fcd931df44f4786bfcce6df7d7" + integrity sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA== "@esbuild/netbsd-x64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz#1ae73d23cc044a0ebd4f198334416fb26c31366c" integrity sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg== -"@esbuild/netbsd-x64@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz#93f7609e2885d1c0b5a1417885fba8d1fcc41272" - integrity sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA== +"@esbuild/netbsd-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz#028ad1807a8e03e155153b2d025b506c3787354b" + integrity sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA== "@esbuild/openbsd-arm64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz#5d904a4f5158c89859fd902c427f96d6a9e632e2" integrity sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg== -"@esbuild/openbsd-arm64@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz#a1985604a203cdc325fd47542e106fafd698f02e" - integrity sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA== +"@esbuild/openbsd-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz#e3c16ff3490c9b59b969fffca87f350ffc0e2af5" + integrity sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw== "@esbuild/openbsd-x64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz#4c8aa88c49187c601bae2971e71c6dc5e0ad1cdf" integrity sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q== -"@esbuild/openbsd-x64@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz#8209e46c42f1ffbe6e4ef77a32e1f47d404ad42a" - integrity sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg== +"@esbuild/openbsd-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz#c5a4693fcb03d1cbecbf8b422422468dfc0d2a8b" + integrity sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ== -"@esbuild/openharmony-arm64@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz#8fade4441893d9cc44cbd7dcf3776f508ab6fb2f" - integrity sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag== +"@esbuild/openharmony-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz#082082444f12db564a0775a41e1991c0e125055e" + integrity sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g== "@esbuild/sunos-x64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz#8ddc35a0ea38575fa44eda30a5ee01ae2fa54dd4" integrity sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA== -"@esbuild/sunos-x64@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz#980d4b9703a16f0f07016632424fc6d9a789dfc2" - integrity sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg== +"@esbuild/sunos-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz#5ab036c53f929e8405c4e96e865a424160a1b537" + integrity sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA== "@esbuild/win32-arm64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz#6e79c8543f282c4539db684a207ae0e174a9007b" integrity sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA== -"@esbuild/win32-arm64@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz#1c09a3633c949ead3d808ba37276883e71f6111a" - integrity sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg== +"@esbuild/win32-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz#38de700ef4b960a0045370c171794526e589862e" + integrity sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA== "@esbuild/win32-ia32@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz#057af345da256b7192d18b676a02e95d0fa39103" integrity sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw== -"@esbuild/win32-ia32@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz#1b1e3a63ad4bef82200fef4e369e0fff7009eee5" - integrity sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ== +"@esbuild/win32-ia32@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz#451b93dc03ec5d4f38619e6cd64d9f9eff06f55c" + integrity sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q== "@esbuild/win32-x64@0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz#168ab1c7e1c318b922637fad8f339d48b01e1244" integrity sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA== -"@esbuild/win32-x64@0.27.2": - version "0.27.2" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz#9e585ab6086bef994c6e8a5b3a0481219ada862b" - integrity sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ== +"@esbuild/win32-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz#0eaf705c941a218a43dba8e09f1df1d6cd2f1f17" + integrity sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA== "@eslint-community/eslint-utils@^4.8.0", "@eslint-community/eslint-utils@^4.9.1": version "4.9.1" @@ -1758,14 +1758,14 @@ integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== "@sinclair/typebox@^0.27.8": - version "0.27.8" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" - integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + version "0.27.10" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.10.tgz#beefe675f1853f73676aecc915b2bd2ac98c4fc6" + integrity sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA== "@sinclair/typebox@^0.34.0": - version "0.34.47" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.34.47.tgz#61b684d8a20d2890b9f1f7b0d4f76b4b39f5bc0d" - integrity sha512-ZGIBQ+XDvO5JQku9wmwtabcVTHJsgSWAHYtVuM9pBNNR5E88v6Jcj/llpmsjivig5X8A8HHOb4/mbEKPS5EvAw== + version "0.34.48" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.34.48.tgz#75b0ead87e59e1adbd6dccdc42bad4fddee73b59" + integrity sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA== "@sinonjs/commons@^3.0.0", "@sinonjs/commons@^3.0.1": version "3.0.1" @@ -1843,10 +1843,10 @@ "@smithy/util-middleware" "^4.2.8" tslib "^2.6.2" -"@smithy/core@^3.20.6", "@smithy/core@^3.20.7": - version "3.20.7" - resolved "https://registry.yarnpkg.com/@smithy/core/-/core-3.20.7.tgz#98a64db764c3fad60926c75ae6eebe407b90f2b3" - integrity sha512-aO7jmh3CtrmPsIJxUwYIzI5WVlMK8BMCPQ4D4nTzqTqBhbzvxHNzBMGcEg13yg/z9R2Qsz49NUFl0F0lVbTVFw== +"@smithy/core@^3.22.1": + version "3.22.1" + resolved "https://registry.yarnpkg.com/@smithy/core/-/core-3.22.1.tgz#c34180d541c9dc5d29412809a6aa497ea47d74f8" + integrity sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g== dependencies: "@smithy/middleware-serde" "^4.2.9" "@smithy/protocol-http" "^5.3.8" @@ -1854,7 +1854,7 @@ "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" "@smithy/util-middleware" "^4.2.8" - "@smithy/util-stream" "^4.5.10" + "@smithy/util-stream" "^4.5.11" "@smithy/util-utf8" "^4.2.0" "@smithy/uuid" "^1.1.0" tslib "^2.6.2" @@ -1995,12 +1995,12 @@ "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@smithy/middleware-endpoint@^4.4.7", "@smithy/middleware-endpoint@^4.4.8": - version "4.4.8" - resolved "https://registry.yarnpkg.com/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.8.tgz#86aef87c2e0d5f727f172a663b9d31be9919e66a" - integrity sha512-TV44qwB/T0OMMzjIuI+JeS0ort3bvlPJ8XIH0MSlGADraXpZqmyND27ueuAL3E14optleADWqtd7dUgc2w+qhQ== +"@smithy/middleware-endpoint@^4.4.13": + version "4.4.13" + resolved "https://registry.yarnpkg.com/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.13.tgz#8a5dda67cbf8e63155a908a724e7ae09b763baad" + integrity sha512-x6vn0PjYmGdNuKh/juUJJewZh7MoQ46jYaJ2mvekF4EesMuFfrl4LaW/k97Zjf8PTCPQmPgMvwewg7eNoH9n5w== dependencies: - "@smithy/core" "^3.20.7" + "@smithy/core" "^3.22.1" "@smithy/middleware-serde" "^4.2.9" "@smithy/node-config-provider" "^4.3.8" "@smithy/shared-ini-file-loader" "^4.4.3" @@ -2009,15 +2009,15 @@ "@smithy/util-middleware" "^4.2.8" tslib "^2.6.2" -"@smithy/middleware-retry@^4.4.23": - version "4.4.24" - resolved "https://registry.yarnpkg.com/@smithy/middleware-retry/-/middleware-retry-4.4.24.tgz#5b9c2ab9e64844bb19bdd2e2cac036e9a2d00bb1" - integrity sha512-yiUY1UvnbUFfP5izoKLtfxDSTRv724YRRwyiC/5HYY6vdsVDcDOXKSXmkJl/Hovcxt5r+8tZEUAdrOaCJwrl9Q== +"@smithy/middleware-retry@^4.4.30": + version "4.4.30" + resolved "https://registry.yarnpkg.com/@smithy/middleware-retry/-/middleware-retry-4.4.30.tgz#a0548803044069b53a332606d4b4f803f07f8963" + integrity sha512-CBGyFvN0f8hlnqKH/jckRDz78Snrp345+PVk8Ux7pnkUCW97Iinse59lY78hBt04h1GZ6hjBN94BRwZy1xC8Bg== dependencies: "@smithy/node-config-provider" "^4.3.8" "@smithy/protocol-http" "^5.3.8" "@smithy/service-error-classification" "^4.2.8" - "@smithy/smithy-client" "^4.10.9" + "@smithy/smithy-client" "^4.11.2" "@smithy/types" "^4.12.0" "@smithy/util-middleware" "^4.2.8" "@smithy/util-retry" "^4.2.8" @@ -2051,10 +2051,10 @@ "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@smithy/node-http-handler@^4.4.8": - version "4.4.8" - resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.4.8.tgz#298cc148c812b9a79f0ebd75e82bdab9e6d0bbcd" - integrity sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg== +"@smithy/node-http-handler@^4.4.9": + version "4.4.9" + resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.4.9.tgz#c167e5b8aed33c5edaf25b903ed9866858499c93" + integrity sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w== dependencies: "@smithy/abort-controller" "^4.2.8" "@smithy/protocol-http" "^5.3.8" @@ -2124,17 +2124,17 @@ "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@smithy/smithy-client@^4.10.8", "@smithy/smithy-client@^4.10.9": - version "4.10.9" - resolved "https://registry.yarnpkg.com/@smithy/smithy-client/-/smithy-client-4.10.9.tgz#fc19348ff77749ebb40f948d7f1b09daa14c313f" - integrity sha512-Je0EvGXVJ0Vrrr2lsubq43JGRIluJ/hX17aN/W/A0WfE+JpoMdI8kwk2t9F0zTX9232sJDGcoH4zZre6m6f/sg== +"@smithy/smithy-client@^4.11.2": + version "4.11.2" + resolved "https://registry.yarnpkg.com/@smithy/smithy-client/-/smithy-client-4.11.2.tgz#1f6a4d75625dbaa16bafbe9b10cf6a41c98fe3da" + integrity sha512-SCkGmFak/xC1n7hKRsUr6wOnBTJ3L22Qd4e8H1fQIuKTAjntwgU8lrdMe7uHdiT2mJAOWA/60qaW9tiMu69n1A== dependencies: - "@smithy/core" "^3.20.7" - "@smithy/middleware-endpoint" "^4.4.8" + "@smithy/core" "^3.22.1" + "@smithy/middleware-endpoint" "^4.4.13" "@smithy/middleware-stack" "^4.2.8" "@smithy/protocol-http" "^5.3.8" "@smithy/types" "^4.12.0" - "@smithy/util-stream" "^4.5.10" + "@smithy/util-stream" "^4.5.11" tslib "^2.6.2" "@smithy/types@^4.12.0": @@ -2199,26 +2199,26 @@ dependencies: tslib "^2.6.2" -"@smithy/util-defaults-mode-browser@^4.3.22": - version "4.3.23" - resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.23.tgz#3b6ff9939b1fd54fb3d95a57ca0f6b341c497096" - integrity sha512-mMg+r/qDfjfF/0psMbV4zd7F/i+rpyp7Hjh0Wry7eY15UnzTEId+xmQTGDU8IdZtDfbGQxuWNfgBZKBj+WuYbA== +"@smithy/util-defaults-mode-browser@^4.3.29": + version "4.3.29" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.29.tgz#fd4f9563ffd1fb49d092e5b86bacc7796170763e" + integrity sha512-nIGy3DNRmOjaYaaKcQDzmWsro9uxlaqUOhZDHQed9MW/GmkBZPtnU70Pu1+GT9IBmUXwRdDuiyaeiy9Xtpn3+Q== dependencies: "@smithy/property-provider" "^4.2.8" - "@smithy/smithy-client" "^4.10.9" + "@smithy/smithy-client" "^4.11.2" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@smithy/util-defaults-mode-node@^4.2.25": - version "4.2.26" - resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.26.tgz#310da280a7168847aff4d0317ae2591f44caf61c" - integrity sha512-EQqe/WkbCinah0h1lMWh9ICl0Ob4lyl20/10WTB35SC9vDQfD8zWsOT+x2FIOXKAoZQ8z/y0EFMoodbcqWJY/w== +"@smithy/util-defaults-mode-node@^4.2.32": + version "4.2.32" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.32.tgz#bc3e9ee1711a9ac3b1c29ea0bef0e785c1da30da" + integrity sha512-7dtFff6pu5fsjqrVve0YMhrnzJtccCWDacNKOkiZjJ++fmjGExmmSu341x+WU6Oc1IccL7lDuaUj7SfrHpWc5Q== dependencies: "@smithy/config-resolver" "^4.4.6" "@smithy/credential-provider-imds" "^4.2.8" "@smithy/node-config-provider" "^4.3.8" "@smithy/property-provider" "^4.2.8" - "@smithy/smithy-client" "^4.10.9" + "@smithy/smithy-client" "^4.11.2" "@smithy/types" "^4.12.0" tslib "^2.6.2" @@ -2255,13 +2255,13 @@ "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@smithy/util-stream@^4.5.10": - version "4.5.10" - resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-4.5.10.tgz#3a7b56f0bdc3833205f80fea67d8e76756ea055b" - integrity sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g== +"@smithy/util-stream@^4.5.11": + version "4.5.11" + resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-4.5.11.tgz#69bf0816c2a396b389a48a64455dacdb57893984" + integrity sha512-lKmZ0S/3Qj2OF5H1+VzvDLb6kRxGzZHq6f3rAsoSu5cTLGsn3v3VQBA8czkNNXlLjoFEtVu3OQT2jEeOtOE2CA== dependencies: "@smithy/fetch-http-handler" "^5.3.9" - "@smithy/node-http-handler" "^4.4.8" + "@smithy/node-http-handler" "^4.4.9" "@smithy/types" "^4.12.0" "@smithy/util-base64" "^4.3.0" "@smithy/util-buffer-from" "^4.2.0" @@ -2437,9 +2437,9 @@ integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@types/node@*": - version "25.0.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.9.tgz#81ce3579ddf67cae812a9d49c8a0ab90c82e7782" - integrity sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw== + version "25.2.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.2.1.tgz#378021f9e765bb65ba36de16f3c3a8622c1fa03d" + integrity sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg== dependencies: undici-types "~7.16.0" @@ -2451,11 +2451,10 @@ undici-types "~6.19.2" "@types/nodemailer@^7.0.4": - version "7.0.5" - resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-7.0.5.tgz#4109582cf0680a1a69457f66adabb9540c4dd19a" - integrity sha512-7WtR4MFJUNN2UFy0NIowBRJswj5KXjXDhlZY43Hmots5eGu5q/dTeFd/I6GgJA/qj3RqO6dDy4SvfcV3fOVeIA== + version "7.0.9" + resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-7.0.9.tgz#a19e3fa222b21213b481cdbdbc70a06787ea49e8" + integrity sha512-vI8oF1M+8JvQhsId0Pc38BdUP2evenIIys7c7p+9OZXSPOH5c1dyINP1jT8xQ2xPuBUXmIC87s+91IZMDjH8Ow== dependencies: - "@aws-sdk/client-sesv2" "^3.839.0" "@types/node" "*" "@types/prop-types@*": @@ -2464,9 +2463,9 @@ integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw== "@types/react@^18.3.12": - version "18.3.27" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.27.tgz#74a3b590ea183983dc65a474dc17553ae1415c34" - integrity sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w== + version "18.3.28" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.28.tgz#0a85b1a7243b4258d9f626f43797ba18eb5f8781" + integrity sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw== dependencies: "@types/prop-types" "*" csstype "^3.2.2" @@ -2506,131 +2505,131 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^8.12.2": - version "8.53.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz#f6640f6f8749b71d9ab457263939e8932a3c6b46" - integrity sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag== + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz#d8899e5c2eccf5c4a20d01c036a193753748454d" + integrity sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ== dependencies: "@eslint-community/regexpp" "^4.12.2" - "@typescript-eslint/scope-manager" "8.53.1" - "@typescript-eslint/type-utils" "8.53.1" - "@typescript-eslint/utils" "8.53.1" - "@typescript-eslint/visitor-keys" "8.53.1" + "@typescript-eslint/scope-manager" "8.54.0" + "@typescript-eslint/type-utils" "8.54.0" + "@typescript-eslint/utils" "8.54.0" + "@typescript-eslint/visitor-keys" "8.54.0" ignore "^7.0.5" natural-compare "^1.4.0" ts-api-utils "^2.4.0" "@typescript-eslint/parser@^8.12.2": - version "8.53.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.53.1.tgz#58d4a70cc2daee2becf7d4521d65ea1782d6ec68" - integrity sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg== - dependencies: - "@typescript-eslint/scope-manager" "8.53.1" - "@typescript-eslint/types" "8.53.1" - "@typescript-eslint/typescript-estree" "8.53.1" - "@typescript-eslint/visitor-keys" "8.53.1" + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.54.0.tgz#3d01a6f54ed247deb9982621f70e7abf1810bd97" + integrity sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA== + dependencies: + "@typescript-eslint/scope-manager" "8.54.0" + "@typescript-eslint/types" "8.54.0" + "@typescript-eslint/typescript-estree" "8.54.0" + "@typescript-eslint/visitor-keys" "8.54.0" debug "^4.4.3" -"@typescript-eslint/project-service@8.53.1": - version "8.53.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.53.1.tgz#4e47856a0b14a1ceb28b0294b4badef3be1e9734" - integrity sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog== +"@typescript-eslint/project-service@8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.54.0.tgz#f582aceb3d752544c8e1b11fea8d95d00cf9adc6" + integrity sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g== dependencies: - "@typescript-eslint/tsconfig-utils" "^8.53.1" - "@typescript-eslint/types" "^8.53.1" + "@typescript-eslint/tsconfig-utils" "^8.54.0" + "@typescript-eslint/types" "^8.54.0" debug "^4.4.3" -"@typescript-eslint/scope-manager@8.53.1": - version "8.53.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.53.1.tgz#6c4b8c82cd45ae3b365afc2373636e166743a8fa" - integrity sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ== +"@typescript-eslint/scope-manager@8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz#307dc8cbd80157e2772c2d36216857415a71ab33" + integrity sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg== dependencies: - "@typescript-eslint/types" "8.53.1" - "@typescript-eslint/visitor-keys" "8.53.1" + "@typescript-eslint/types" "8.54.0" + "@typescript-eslint/visitor-keys" "8.54.0" -"@typescript-eslint/tsconfig-utils@8.53.1", "@typescript-eslint/tsconfig-utils@^8.53.1": - version "8.53.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.1.tgz#efe80b8d019cd49e5a1cf46c2eb0cd2733076424" - integrity sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA== +"@typescript-eslint/tsconfig-utils@8.54.0", "@typescript-eslint/tsconfig-utils@^8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz#71dd7ba1674bd48b172fc4c85b2f734b0eae3dbc" + integrity sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw== -"@typescript-eslint/type-utils@8.53.1": - version "8.53.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.53.1.tgz#95de2651a96d580bf5c6c6089ddd694284d558ad" - integrity sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w== +"@typescript-eslint/type-utils@8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz#64965317dd4118346c2fa5ee94492892200e9fb9" + integrity sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA== dependencies: - "@typescript-eslint/types" "8.53.1" - "@typescript-eslint/typescript-estree" "8.53.1" - "@typescript-eslint/utils" "8.53.1" + "@typescript-eslint/types" "8.54.0" + "@typescript-eslint/typescript-estree" "8.54.0" + "@typescript-eslint/utils" "8.54.0" debug "^4.4.3" ts-api-utils "^2.4.0" -"@typescript-eslint/types@8.53.1", "@typescript-eslint/types@^8.53.1": - version "8.53.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.53.1.tgz#101f203f0807a63216cceceedb815fabe21d5793" - integrity sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A== +"@typescript-eslint/types@8.54.0", "@typescript-eslint/types@^8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.54.0.tgz#c12d41f67a2e15a8a96fbc5f2d07b17331130889" + integrity sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA== -"@typescript-eslint/typescript-estree@8.53.1": - version "8.53.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.1.tgz#b6dce2303c9e27e95b8dcd8c325868fff53e488f" - integrity sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg== +"@typescript-eslint/typescript-estree@8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz#3c7716905b2b811fadbd2114804047d1bfc86527" + integrity sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA== dependencies: - "@typescript-eslint/project-service" "8.53.1" - "@typescript-eslint/tsconfig-utils" "8.53.1" - "@typescript-eslint/types" "8.53.1" - "@typescript-eslint/visitor-keys" "8.53.1" + "@typescript-eslint/project-service" "8.54.0" + "@typescript-eslint/tsconfig-utils" "8.54.0" + "@typescript-eslint/types" "8.54.0" + "@typescript-eslint/visitor-keys" "8.54.0" debug "^4.4.3" minimatch "^9.0.5" semver "^7.7.3" tinyglobby "^0.2.15" ts-api-utils "^2.4.0" -"@typescript-eslint/utils@8.53.1": - version "8.53.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.53.1.tgz#81fe6c343de288701b774f4d078382f567e6edaa" - integrity sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg== +"@typescript-eslint/utils@8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.54.0.tgz#c79a4bcbeebb4f571278c0183ed1cb601d84c6c8" + integrity sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA== dependencies: "@eslint-community/eslint-utils" "^4.9.1" - "@typescript-eslint/scope-manager" "8.53.1" - "@typescript-eslint/types" "8.53.1" - "@typescript-eslint/typescript-estree" "8.53.1" + "@typescript-eslint/scope-manager" "8.54.0" + "@typescript-eslint/types" "8.54.0" + "@typescript-eslint/typescript-estree" "8.54.0" -"@typescript-eslint/visitor-keys@8.53.1": - version "8.53.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.1.tgz#405f04959be22b9be364939af8ac19c3649b6eb7" - integrity sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg== +"@typescript-eslint/visitor-keys@8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz#0e4b50124b210b8600b245dd66cbad52deb15590" + integrity sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA== dependencies: - "@typescript-eslint/types" "8.53.1" + "@typescript-eslint/types" "8.54.0" eslint-visitor-keys "^4.2.1" "@vitest/expect@>1.6.0": - version "4.0.17" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-4.0.17.tgz#67bb0d4a7d37054590a19dcf831f7936d14a8a02" - integrity sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ== + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-4.0.18.tgz#361510d99fbf20eb814222e4afcb8539d79dc94d" + integrity sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ== dependencies: "@standard-schema/spec" "^1.0.0" "@types/chai" "^5.2.2" - "@vitest/spy" "4.0.17" - "@vitest/utils" "4.0.17" + "@vitest/spy" "4.0.18" + "@vitest/utils" "4.0.18" chai "^6.2.1" tinyrainbow "^3.0.3" -"@vitest/pretty-format@4.0.17": - version "4.0.17" - resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.0.17.tgz#dde7cb2c01699d0943571137d1b482edff5fc000" - integrity sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw== +"@vitest/pretty-format@4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.0.18.tgz#fbccd4d910774072ec15463553edb8ca5ce53218" + integrity sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw== dependencies: tinyrainbow "^3.0.3" -"@vitest/spy@4.0.17": - version "4.0.17" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-4.0.17.tgz#d0936f8908b4dae091d9b948be3bae8e19d1878d" - integrity sha512-I1bQo8QaP6tZlTomQNWKJE6ym4SHf3oLS7ceNjozxxgzavRAgZDc06T7kD8gb9bXKEgcLNt00Z+kZO6KaJ62Ew== +"@vitest/spy@4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-4.0.18.tgz#ba0f20503fb6d08baf3309d690b3efabdfa88762" + integrity sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw== -"@vitest/utils@4.0.17": - version "4.0.17" - resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.0.17.tgz#48181deab273c87ac4ee20c1c454ffe9c4f453fe" - integrity sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w== +"@vitest/utils@4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.0.18.tgz#9636b16d86a4152ec68a8d6859cff702896433d4" + integrity sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA== dependencies: - "@vitest/pretty-format" "4.0.17" + "@vitest/pretty-format" "4.0.18" tinyrainbow "^3.0.3" acorn-jsx@^5.3.2: @@ -2821,9 +2820,9 @@ balanced-match@^1.0.0: integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== baseline-browser-mapping@^2.9.0: - version "2.9.15" - resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz#6baaa0069883f50a99cdb31b56646491f47c05d7" - integrity sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg== + version "2.9.19" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz#3e508c43c46d961eb4d7d2e5b8d1dd0f9ee4f488" + integrity sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg== bowser@^2.11.0: version "2.13.1" @@ -2903,9 +2902,9 @@ camelcase@^6.0.0, camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001759: - version "1.0.30001765" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz#4a78d8a797fd4124ebaab2043df942eb091648ee" - integrity sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ== + version "1.0.30001769" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz#1ad91594fad7dc233777c2781879ab5409f7d9c2" + integrity sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg== chai-match-pattern@^1.1.0: version "1.3.0" @@ -2973,9 +2972,9 @@ ci-info@^3.2.0: integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== ci-info@^4.2.0: - version "4.3.1" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.3.1.tgz#355ad571920810b5623e11d40232f443f16f1daa" - integrity sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA== + version "4.4.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.4.0.tgz#7d54eff9f54b45b62401c26032696eb59c8bd18c" + integrity sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg== cjs-module-lexer@^1.0.0: version "1.4.3" @@ -3132,14 +3131,14 @@ diff-sequences@^29.6.3: integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + version "4.0.4" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.4.tgz#7a6dbfda325f25f07517e9b518f897c08332e07d" + integrity sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ== diff@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" - integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== + version "5.2.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.2.tgz#0a4742797281d09cfa699b79ea32d27723623bad" + integrity sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A== diff@^7.0.0: version "7.0.0" @@ -3187,9 +3186,9 @@ eastasianwidth@^0.2.0: integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== electron-to-chromium@^1.5.263: - version "1.5.267" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz#5d84f2df8cdb6bfe7e873706bb21bd4bfb574dc7" - integrity sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw== + version "1.5.286" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz#142be1ab5e1cd5044954db0e5898f60a4960384e" + integrity sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A== emittery@^0.13.1: version "0.13.1" @@ -3254,36 +3253,36 @@ esbuild@0.24.0: "@esbuild/win32-x64" "0.24.0" esbuild@~0.27.0: - version "0.27.2" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.27.2.tgz#d83ed2154d5813a5367376bb2292a9296fc83717" - integrity sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw== + version "0.27.3" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.27.3.tgz#5859ca8e70a3af956b26895ce4954d7e73bd27a8" + integrity sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg== optionalDependencies: - "@esbuild/aix-ppc64" "0.27.2" - "@esbuild/android-arm" "0.27.2" - "@esbuild/android-arm64" "0.27.2" - "@esbuild/android-x64" "0.27.2" - "@esbuild/darwin-arm64" "0.27.2" - "@esbuild/darwin-x64" "0.27.2" - "@esbuild/freebsd-arm64" "0.27.2" - "@esbuild/freebsd-x64" "0.27.2" - "@esbuild/linux-arm" "0.27.2" - "@esbuild/linux-arm64" "0.27.2" - "@esbuild/linux-ia32" "0.27.2" - "@esbuild/linux-loong64" "0.27.2" - "@esbuild/linux-mips64el" "0.27.2" - "@esbuild/linux-ppc64" "0.27.2" - "@esbuild/linux-riscv64" "0.27.2" - "@esbuild/linux-s390x" "0.27.2" - "@esbuild/linux-x64" "0.27.2" - "@esbuild/netbsd-arm64" "0.27.2" - "@esbuild/netbsd-x64" "0.27.2" - "@esbuild/openbsd-arm64" "0.27.2" - "@esbuild/openbsd-x64" "0.27.2" - "@esbuild/openharmony-arm64" "0.27.2" - "@esbuild/sunos-x64" "0.27.2" - "@esbuild/win32-arm64" "0.27.2" - "@esbuild/win32-ia32" "0.27.2" - "@esbuild/win32-x64" "0.27.2" + "@esbuild/aix-ppc64" "0.27.3" + "@esbuild/android-arm" "0.27.3" + "@esbuild/android-arm64" "0.27.3" + "@esbuild/android-x64" "0.27.3" + "@esbuild/darwin-arm64" "0.27.3" + "@esbuild/darwin-x64" "0.27.3" + "@esbuild/freebsd-arm64" "0.27.3" + "@esbuild/freebsd-x64" "0.27.3" + "@esbuild/linux-arm" "0.27.3" + "@esbuild/linux-arm64" "0.27.3" + "@esbuild/linux-ia32" "0.27.3" + "@esbuild/linux-loong64" "0.27.3" + "@esbuild/linux-mips64el" "0.27.3" + "@esbuild/linux-ppc64" "0.27.3" + "@esbuild/linux-riscv64" "0.27.3" + "@esbuild/linux-s390x" "0.27.3" + "@esbuild/linux-x64" "0.27.3" + "@esbuild/netbsd-arm64" "0.27.3" + "@esbuild/netbsd-x64" "0.27.3" + "@esbuild/openbsd-arm64" "0.27.3" + "@esbuild/openbsd-x64" "0.27.3" + "@esbuild/openharmony-arm64" "0.27.3" + "@esbuild/sunos-x64" "0.27.3" + "@esbuild/win32-arm64" "0.27.3" + "@esbuild/win32-ia32" "0.27.3" + "@esbuild/win32-x64" "0.27.3" escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" @@ -3454,10 +3453,10 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -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== +fast-xml-parser@5.3.4: + version "5.3.4" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz#06f39aafffdbc97bef0321e626c7ddd06a043ecf" + integrity sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA== dependencies: strnum "^2.1.0" @@ -3580,9 +3579,9 @@ get-stream@^6.0.0: integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== get-tsconfig@^4.7.5: - version "4.13.0" - resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.13.0.tgz#fcdd991e6d22ab9a600f00e91c318707a5d9a0d7" - integrity sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ== + version "4.13.6" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.13.6.tgz#2fbfda558a98a691a798f123afd95915badce876" + integrity sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw== dependencies: resolve-pkg-maps "^1.0.0" @@ -4411,9 +4410,9 @@ lodash.merge@^4.6.2: integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== lodash@^4.0.0, lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + version "4.17.23" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a" + integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w== log-symbols@^4.1.0: version "4.1.0" @@ -4603,9 +4602,9 @@ node-releases@^2.0.27: integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== nodemailer@^7.0.11: - version "7.0.12" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-7.0.12.tgz#b6b7bb05566c6c8458ee360aa30a407a478d35b7" - integrity sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA== + version "7.0.13" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-7.0.13.tgz#74acaa55f0c6f9476384c29f27f53e467e8483cd" + integrity sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw== normalize-path@^3.0.0: version "3.0.0" @@ -4949,9 +4948,9 @@ semver@^6.3.0, semver@^6.3.1: integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^7.5.3, semver@^7.5.4, semver@^7.7.3: - version "7.7.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" - integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== + version "7.7.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" + integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== serialize-javascript@^6.0.2: version "6.0.2" From e551e9c9cb7dcde63853d2aed3e840fe34ddcb9d Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 6 Feb 2026 15:01:57 -0600 Subject: [PATCH 28/52] update multi-account python deps to latest --- .../backups/requirements-dev.txt | 20 ++++++++-------- .../multi-account/backups/requirements.txt | 8 +++---- .../control-tower/requirements-dev.txt | 23 ++++++++++--------- .../control-tower/requirements.txt | 8 +++---- .../log-aggregation/requirements-dev.txt | 2 +- .../log-aggregation/requirements.txt | 8 +++---- 6 files changed, 35 insertions(+), 34 deletions(-) diff --git a/backend/multi-account/backups/requirements-dev.txt b/backend/multi-account/backups/requirements-dev.txt index 5680962c6..4b3596255 100644 --- a/backend/multi-account/backups/requirements-dev.txt +++ b/backend/multi-account/backups/requirements-dev.txt @@ -4,20 +4,20 @@ # # pip-compile --no-emit-index-url --no-strip-extras backups/requirements-dev.in # -boto3==1.42.11 +boto3==1.42.44 # via moto -botocore==1.42.11 +botocore==1.42.44 # via # boto3 # moto # s3transfer -certifi==2025.11.12 +certifi==2026.1.4 # via requests cffi==2.0.0 # via cryptography charset-normalizer==3.4.4 # via requests -cryptography==46.0.3 +cryptography==46.0.4 # via moto idna==3.11 # via requests @@ -25,7 +25,7 @@ iniconfig==2.3.0 # via pytest jinja2==3.1.6 # via moto -jmespath==1.0.1 +jmespath==1.1.0 # via # boto3 # botocore @@ -33,13 +33,13 @@ markupsafe==3.0.3 # via # jinja2 # werkzeug -moto==5.1.18 +moto==5.1.20 # via -r backups/requirements-dev.in -packaging==25.0 +packaging==26.0 # via pytest pluggy==1.6.0 # via pytest -pycparser==2.23 +pycparser==3.0 # via cffi pygments==2.19.2 # via pytest @@ -61,12 +61,12 @@ s3transfer==0.16.0 # via boto3 six==1.17.0 # via python-dateutil -urllib3==2.6.2 +urllib3==2.6.3 # via # botocore # requests # responses -werkzeug==3.1.4 +werkzeug==3.1.5 # via moto xmltodict==1.0.2 # via moto diff --git a/backend/multi-account/backups/requirements.txt b/backend/multi-account/backups/requirements.txt index b681e8aac..cdd6ff9ff 100644 --- a/backend/multi-account/backups/requirements.txt +++ b/backend/multi-account/backups/requirements.txt @@ -8,23 +8,23 @@ attrs==25.4.0 # via # cattrs # jsii -aws-cdk-asset-awscli-v1==2.2.242 +aws-cdk-asset-awscli-v1==2.2.263 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib aws-cdk-cloud-assembly-schema==48.20.0 # via aws-cdk-lib -aws-cdk-lib==2.232.2 +aws-cdk-lib==2.237.1 # via -r backups/requirements.in cattrs==25.3.0 # via jsii -constructs==10.4.4 +constructs==10.4.5 # via # -r backups/requirements.in # aws-cdk-lib importlib-resources==6.5.2 # via jsii -jsii==1.121.0 +jsii==1.126.0 # via # aws-cdk-asset-awscli-v1 # aws-cdk-asset-node-proxy-agent-v6 diff --git a/backend/multi-account/control-tower/requirements-dev.txt b/backend/multi-account/control-tower/requirements-dev.txt index 62d01ed4f..9492750cb 100644 --- a/backend/multi-account/control-tower/requirements-dev.txt +++ b/backend/multi-account/control-tower/requirements-dev.txt @@ -6,19 +6,19 @@ # boolean-py==5.0 # via license-expression -build==1.3.0 +build==1.4.0 # via pip-tools cachecontrol[filecache]==0.14.4 # via # cachecontrol # pip-audit -certifi==2025.11.12 +certifi==2026.1.4 # via requests charset-normalizer==3.4.4 # via requests click==8.3.1 # via pip-tools -coverage[toml]==7.13.0 +coverage[toml]==7.13.3 # via # -r control-tower/requirements-dev.in # pytest-cov @@ -26,7 +26,7 @@ cyclonedx-python-lib==11.6.0 # via pip-audit defusedxml==0.7.1 # via py-serializable -filelock==3.20.1 +filelock==3.20.3 # via cachecontrol idna==3.11 # via requests @@ -42,12 +42,13 @@ msgpack==1.1.2 # via cachecontrol packageurl-python==0.17.6 # via cyclonedx-python-lib -packaging==25.0 +packaging==26.0 # via # build # pip-audit # pip-requirements-parser # pytest + # wheel pip-api==0.0.34 # via pip-audit pip-audit==2.10.0 @@ -68,7 +69,7 @@ pygments==2.19.2 # via # pytest # rich -pyparsing==3.2.5 +pyparsing==3.3.2 # via pip-requirements-parser pyproject-hooks==1.2.0 # via @@ -84,19 +85,19 @@ requests==2.32.5 # via # cachecontrol # pip-audit -rich==14.2.0 +rich==14.3.2 # via pip-audit -ruff==0.14.9 +ruff==0.15.0 # via -r control-tower/requirements-dev.in sortedcontainers==2.4.0 # via cyclonedx-python-lib -tomli==2.3.0 +tomli==2.4.0 # via pip-audit tomli-w==1.2.0 # via pip-audit -urllib3==2.6.2 +urllib3==2.6.3 # via requests -wheel==0.45.1 +wheel==0.46.3 # via pip-tools # The following packages are considered to be unsafe in a requirements file: diff --git a/backend/multi-account/control-tower/requirements.txt b/backend/multi-account/control-tower/requirements.txt index b85f9da0d..455f07ef6 100644 --- a/backend/multi-account/control-tower/requirements.txt +++ b/backend/multi-account/control-tower/requirements.txt @@ -8,13 +8,13 @@ attrs==25.4.0 # via # cattrs # jsii -aws-cdk-asset-awscli-v1==2.2.242 +aws-cdk-asset-awscli-v1==2.2.263 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib aws-cdk-cloud-assembly-schema==48.20.0 # via aws-cdk-lib -aws-cdk-lib==2.232.2 +aws-cdk-lib==2.237.1 # via # -r control-tower/requirements.in # cdk-nag @@ -22,14 +22,14 @@ cattrs==25.3.0 # via jsii cdk-nag==2.37.55 # via -r control-tower/requirements.in -constructs==10.4.4 +constructs==10.4.5 # via # -r control-tower/requirements.in # aws-cdk-lib # cdk-nag importlib-resources==6.5.2 # via jsii -jsii==1.121.0 +jsii==1.126.0 # via # aws-cdk-asset-awscli-v1 # aws-cdk-asset-node-proxy-agent-v6 diff --git a/backend/multi-account/log-aggregation/requirements-dev.txt b/backend/multi-account/log-aggregation/requirements-dev.txt index 255223201..0b5a8d2dd 100644 --- a/backend/multi-account/log-aggregation/requirements-dev.txt +++ b/backend/multi-account/log-aggregation/requirements-dev.txt @@ -6,7 +6,7 @@ # iniconfig==2.3.0 # via pytest -packaging==25.0 +packaging==26.0 # via pytest pluggy==1.6.0 # via pytest diff --git a/backend/multi-account/log-aggregation/requirements.txt b/backend/multi-account/log-aggregation/requirements.txt index c722156d4..54fe50746 100644 --- a/backend/multi-account/log-aggregation/requirements.txt +++ b/backend/multi-account/log-aggregation/requirements.txt @@ -8,23 +8,23 @@ attrs==25.4.0 # via # cattrs # jsii -aws-cdk-asset-awscli-v1==2.2.242 +aws-cdk-asset-awscli-v1==2.2.263 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib aws-cdk-cloud-assembly-schema==48.20.0 # via aws-cdk-lib -aws-cdk-lib==2.232.2 +aws-cdk-lib==2.237.1 # via -r log-aggregation/requirements.in cattrs==25.3.0 # via jsii -constructs==10.4.4 +constructs==10.4.5 # via # -r log-aggregation/requirements.in # aws-cdk-lib importlib-resources==6.5.2 # via jsii -jsii==1.121.0 +jsii==1.126.0 # via # aws-cdk-asset-awscli-v1 # aws-cdk-asset-node-proxy-agent-v6 From 48c4937d54bb2cb55409e642748426f1b3f8292b Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 6 Feb 2026 15:26:03 -0600 Subject: [PATCH 29/52] test CI/CD node test issue --- backend/compact-connect/lambdas/nodejs/package.json | 4 ++-- backend/compact-connect/lambdas/nodejs/yarn.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/compact-connect/lambdas/nodejs/package.json b/backend/compact-connect/lambdas/nodejs/package.json index 6771d8d53..a626912cb 100644 --- a/backend/compact-connect/lambdas/nodejs/package.json +++ b/backend/compact-connect/lambdas/nodejs/package.json @@ -21,8 +21,8 @@ "@types/react": "^18.3.12", "@typescript-eslint/eslint-plugin": "^8.12.2", "@typescript-eslint/parser": "^8.12.2", - "aws-sdk-client-mock": "^4.1.0", - "aws-sdk-client-mock-jest": "^4.1.0", + "aws-sdk-client-mock": "4.1.0", + "aws-sdk-client-mock-jest": "4.1.0", "chai": "^4.1.2", "chai-match-pattern": "^1.1.0", "chalk": "^4.1.2", diff --git a/backend/compact-connect/lambdas/nodejs/yarn.lock b/backend/compact-connect/lambdas/nodejs/yarn.lock index ed6e7113e..37778e051 100644 --- a/backend/compact-connect/lambdas/nodejs/yarn.lock +++ b/backend/compact-connect/lambdas/nodejs/yarn.lock @@ -2744,7 +2744,7 @@ async@^3.2.3: resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== -aws-sdk-client-mock-jest@^4.1.0: +aws-sdk-client-mock-jest@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/aws-sdk-client-mock-jest/-/aws-sdk-client-mock-jest-4.1.0.tgz#40a3bdedd8d551cf2a836b77239038c0ca10e25c" integrity sha512-+g4a5Hp+MmPqqNnvwfLitByggrqf+xSbk1pm6fBYHNcon6+aQjL5iB+3YB6HuGPemY+/mUKN34iP62S14R61bA== @@ -2753,7 +2753,7 @@ aws-sdk-client-mock-jest@^4.1.0: expect ">28.1.3" tslib "^2.1.0" -aws-sdk-client-mock@^4.1.0: +aws-sdk-client-mock@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/aws-sdk-client-mock/-/aws-sdk-client-mock-4.1.0.tgz#ae1950b2277f8e65f9a039975d79ff9fffab39e3" integrity sha512-h/tOYTkXEsAcV3//6C1/7U4ifSpKyJvb6auveAepqqNJl6TdZaPFEtKjBQNf8UxQdDP850knB2i/whq4zlsxJw== From 339970f2a351016b06f0630044e03cfb87cf4864 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 6 Feb 2026 15:36:51 -0600 Subject: [PATCH 30/52] Add jest setup for mocks --- backend/compact-connect/lambdas/nodejs/jest.config.mjs | 1 + backend/compact-connect/lambdas/nodejs/package.json | 4 ++-- backend/compact-connect/lambdas/nodejs/tests/jest.setup.ts | 7 +++++++ backend/compact-connect/lambdas/nodejs/yarn.lock | 4 ++-- 4 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 backend/compact-connect/lambdas/nodejs/tests/jest.setup.ts diff --git a/backend/compact-connect/lambdas/nodejs/jest.config.mjs b/backend/compact-connect/lambdas/nodejs/jest.config.mjs index e4e50d8a5..50e5fd068 100644 --- a/backend/compact-connect/lambdas/nodejs/jest.config.mjs +++ b/backend/compact-connect/lambdas/nodejs/jest.config.mjs @@ -5,6 +5,7 @@ export default { testMatch: ['**/tests/**/*.test.[jt]s?(x)'], moduleFileExtensions: ['ts', 'js'], verbose: true, + setupFilesAfterEnv: ['/tests/jest.setup.ts'], testPathIgnorePatterns: [ '/node_modules/', ], diff --git a/backend/compact-connect/lambdas/nodejs/package.json b/backend/compact-connect/lambdas/nodejs/package.json index a626912cb..6771d8d53 100644 --- a/backend/compact-connect/lambdas/nodejs/package.json +++ b/backend/compact-connect/lambdas/nodejs/package.json @@ -21,8 +21,8 @@ "@types/react": "^18.3.12", "@typescript-eslint/eslint-plugin": "^8.12.2", "@typescript-eslint/parser": "^8.12.2", - "aws-sdk-client-mock": "4.1.0", - "aws-sdk-client-mock-jest": "4.1.0", + "aws-sdk-client-mock": "^4.1.0", + "aws-sdk-client-mock-jest": "^4.1.0", "chai": "^4.1.2", "chai-match-pattern": "^1.1.0", "chalk": "^4.1.2", diff --git a/backend/compact-connect/lambdas/nodejs/tests/jest.setup.ts b/backend/compact-connect/lambdas/nodejs/tests/jest.setup.ts new file mode 100644 index 000000000..1c42717e8 --- /dev/null +++ b/backend/compact-connect/lambdas/nodejs/tests/jest.setup.ts @@ -0,0 +1,7 @@ +// Jest setup file for aws-sdk-client-mock-jest matchers +// This file registers the custom matchers like toHaveReceivedCommandWith globally + +// Triple-slash reference to include the Jest matcher type augmentations +/// + +import 'aws-sdk-client-mock-jest'; diff --git a/backend/compact-connect/lambdas/nodejs/yarn.lock b/backend/compact-connect/lambdas/nodejs/yarn.lock index 37778e051..ed6e7113e 100644 --- a/backend/compact-connect/lambdas/nodejs/yarn.lock +++ b/backend/compact-connect/lambdas/nodejs/yarn.lock @@ -2744,7 +2744,7 @@ async@^3.2.3: resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== -aws-sdk-client-mock-jest@4.1.0: +aws-sdk-client-mock-jest@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/aws-sdk-client-mock-jest/-/aws-sdk-client-mock-jest-4.1.0.tgz#40a3bdedd8d551cf2a836b77239038c0ca10e25c" integrity sha512-+g4a5Hp+MmPqqNnvwfLitByggrqf+xSbk1pm6fBYHNcon6+aQjL5iB+3YB6HuGPemY+/mUKN34iP62S14R61bA== @@ -2753,7 +2753,7 @@ aws-sdk-client-mock-jest@4.1.0: expect ">28.1.3" tslib "^2.1.0" -aws-sdk-client-mock@4.1.0: +aws-sdk-client-mock@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/aws-sdk-client-mock/-/aws-sdk-client-mock-4.1.0.tgz#ae1950b2277f8e65f9a039975d79ff9fffab39e3" integrity sha512-h/tOYTkXEsAcV3//6C1/7U4ifSpKyJvb6auveAepqqNJl6TdZaPFEtKjBQNf8UxQdDP850knB2i/whq4zlsxJw== From 40da34c7ddb1a0436693a89e4f732d0b19e51db0 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 6 Feb 2026 15:41:18 -0600 Subject: [PATCH 31/52] update multi-account runner --- .github/workflows/check-multi-account.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/check-multi-account.yml b/.github/workflows/check-multi-account.yml index e055f502f..e08c3f8c6 100644 --- a/.github/workflows/check-multi-account.yml +++ b/.github/workflows/check-multi-account.yml @@ -24,6 +24,10 @@ jobs: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 + - name: Upgrade pip + # Runner image ships pip 25.3; upgrade to 26.0+ so pip-audit passes (CVE-2026-1703 fixed in 26.0) + run: pip install --upgrade 'pip>=26.0' + - name: Install dev dependencies run: "pip install -r backend/multi-account/control-tower/requirements-dev.txt" @@ -45,6 +49,10 @@ jobs: with: python-version: '3.12' + - name: Upgrade pip + # Runner image ships pip 25.3; upgrade to 26.0+ so pip-audit passes (CVE-2026-1703 fixed in 26.0) + run: pip install --upgrade 'pip>=26.0' + - name: Install dev dependencies run: "pip install -r backend/multi-account/control-tower/requirements-dev.txt" From a4a1a175493f405387db139083fd4bdd32a2bbe9 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 6 Feb 2026 16:07:10 -0600 Subject: [PATCH 32/52] Remove expect import and rely on global jest method --- .../nodejs/tests/cognito-emails.test.ts | 2 +- .../tests/email-notification-service.test.ts | 2 +- .../lib/email/base-email-service.test.ts | 2 +- .../lib/email/cognito-email-service.test.ts | 2 +- .../email/email-notification-service.test.ts | 2 +- .../encumbrance-notification-service.test.ts | 2 +- .../email/ingest-event-email-service.test.ts | 2 +- ...investigation-notification-service.test.ts | 2 +- .../nodejs/tests/lib/event-client.test.ts | 2 +- .../tests/lib/jurisdiction-client.test.ts | 2 +- .../compact-connect/lambdas/nodejs/yarn.lock | 425 +++++++++--------- 11 files changed, 217 insertions(+), 228 deletions(-) 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 769f2542d..4e792a532 100644 --- a/backend/compact-connect/lambdas/nodejs/tests/cognito-emails.test.ts +++ b/backend/compact-connect/lambdas/nodejs/tests/cognito-emails.test.ts @@ -4,7 +4,7 @@ import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 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'; +import { describe, it, beforeAll, beforeEach, jest } from '@jest/globals'; const SAMPLE_COGNITO_EVENT = { version: '1', 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 ed93d9a8a..8f55c7175 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 @@ -7,7 +7,7 @@ import { Readable } from 'stream'; import { sdkStreamMixin } from '@smithy/util-stream'; import { Lambda } from '../email-notification-service/lambda'; import { EmailNotificationEvent } from '../lib/models/email-notification-service-event'; -import { describe, it, expect, beforeAll, beforeEach, jest } from '@jest/globals'; +import { describe, it, beforeAll, beforeEach, jest } from '@jest/globals'; const SAMPLE_EVENT: EmailNotificationEvent = { template: 'transactionBatchSettlementFailure', 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 ef90f28ab..0a064a233 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 @@ -4,7 +4,7 @@ import { Logger } from '@aws-lambda-powertools/logger'; 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'; +import { describe, it, beforeEach, jest } from '@jest/globals'; const asSESClient = (mock: ReturnType) => mock as unknown as SESv2Client; 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 4c9ce66ab..69a8c1072 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 @@ -5,7 +5,7 @@ import { SESv2Client } from '@aws-sdk/client-sesv2'; import { CognitoEmailService } from '../../../lib/email'; import { EmailTemplateCapture } from '../../utils/email-template-capture'; import { TReaderDocument } from '@csg-org/email-builder'; -import { describe, it, expect, beforeEach, beforeAll, afterAll, jest } from '@jest/globals'; +import { describe, it, beforeEach, beforeAll, afterAll, jest } from '@jest/globals'; const asSESClient = (mock: ReturnType) => mock as unknown as SESv2Client; 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 2aeeddbfe..887ca7872 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 @@ -11,7 +11,7 @@ import { CompactConfigurationClient } from '../../../lib/compact-configuration-c import { JurisdictionClient } from '../../../lib/jurisdiction-client'; import { EmailTemplateCapture } from '../../utils/email-template-capture'; import { TReaderDocument } from '@csg-org/email-builder'; -import { describe, it, expect, beforeEach, beforeAll, afterAll, jest } from '@jest/globals'; +import { describe, it, beforeEach, beforeAll, afterAll, jest } from '@jest/globals'; jest.mock('nodemailer'); 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 9e59b539d..94b849e38 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 @@ -8,7 +8,7 @@ import { CompactConfigurationClient } from '../../../lib/compact-configuration-c import { JurisdictionClient } from '../../../lib/jurisdiction-client'; import { EmailTemplateCapture } from '../../utils/email-template-capture'; import { TReaderDocument } from '@csg-org/email-builder'; -import { describe, it, expect, beforeEach, beforeAll, afterAll, jest } from '@jest/globals'; +import { describe, it, beforeEach, beforeAll, afterAll, jest } from '@jest/globals'; import { Compact } from '../../../lib/models/compact'; import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 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 49a955809..1917a5bc8 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 @@ -10,7 +10,7 @@ import { SAMPLE_UNMARSHALLED_INGEST_FAILURE_ERROR_RECORD, SAMPLE_UNMARSHALLED_VALIDATION_ERROR_RECORD } from '../../sample-records'; -import { describe, it, expect, beforeEach, beforeAll, afterAll, jest } from '@jest/globals'; +import { describe, it, beforeEach, beforeAll, afterAll, jest } from '@jest/globals'; const asSESClient = (mock: ReturnType) => mock as unknown as SESv2Client; diff --git a/backend/compact-connect/lambdas/nodejs/tests/lib/email/investigation-notification-service.test.ts b/backend/compact-connect/lambdas/nodejs/tests/lib/email/investigation-notification-service.test.ts index 052fcbe21..0c29afc59 100644 --- a/backend/compact-connect/lambdas/nodejs/tests/lib/email/investigation-notification-service.test.ts +++ b/backend/compact-connect/lambdas/nodejs/tests/lib/email/investigation-notification-service.test.ts @@ -8,7 +8,7 @@ import { CompactConfigurationClient } from '../../../lib/compact-configuration-c import { JurisdictionClient } from '../../../lib/jurisdiction-client'; import { EmailTemplateCapture } from '../../utils/email-template-capture'; import { TReaderDocument } from '@csg-org/email-builder'; -import { describe, it, expect, beforeEach, beforeAll, afterAll, jest } from '@jest/globals'; +import { describe, it, beforeEach, beforeAll, afterAll, jest } from '@jest/globals'; import { Compact } from '../../../lib/models/compact'; import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; diff --git a/backend/compact-connect/lambdas/nodejs/tests/lib/event-client.test.ts b/backend/compact-connect/lambdas/nodejs/tests/lib/event-client.test.ts index 19c924b8e..16667d1ee 100644 --- a/backend/compact-connect/lambdas/nodejs/tests/lib/event-client.test.ts +++ b/backend/compact-connect/lambdas/nodejs/tests/lib/event-client.test.ts @@ -2,7 +2,7 @@ import { Logger } from '@aws-lambda-powertools/logger'; import { DynamoDBClient, QueryCommand } from '@aws-sdk/client-dynamodb'; import { mockClient } from 'aws-sdk-client-mock'; import { EventClient } from '../../lib/event-client'; -import { describe, it, expect, beforeAll, beforeEach, jest } from '@jest/globals'; +import { describe, it, beforeAll, beforeEach, jest } from '@jest/globals'; /* diff --git a/backend/compact-connect/lambdas/nodejs/tests/lib/jurisdiction-client.test.ts b/backend/compact-connect/lambdas/nodejs/tests/lib/jurisdiction-client.test.ts index a05ffd7d4..04dcc4896 100644 --- a/backend/compact-connect/lambdas/nodejs/tests/lib/jurisdiction-client.test.ts +++ b/backend/compact-connect/lambdas/nodejs/tests/lib/jurisdiction-client.test.ts @@ -3,7 +3,7 @@ import 'aws-sdk-client-mock-jest'; import { Logger } from '@aws-lambda-powertools/logger'; import { DynamoDBClient, QueryCommand, GetItemCommand } from '@aws-sdk/client-dynamodb'; import { JurisdictionClient } from '../../lib/jurisdiction-client'; -import { describe, it, expect, beforeAll, beforeEach, jest } from '@jest/globals'; +import { describe, it, beforeAll, beforeEach, jest } from '@jest/globals'; const SAMPLE_JURISDICTION_ITEMS = [ diff --git a/backend/compact-connect/lambdas/nodejs/yarn.lock b/backend/compact-connect/lambdas/nodejs/yarn.lock index ed6e7113e..ecc333362 100644 --- a/backend/compact-connect/lambdas/nodejs/yarn.lock +++ b/backend/compact-connect/lambdas/nodejs/yarn.lock @@ -86,46 +86,46 @@ "@aws/lambda-invoke-store" "0.2.3" "@aws-sdk/client-dynamodb@^3.901.0": - version "3.984.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-dynamodb/-/client-dynamodb-3.984.0.tgz#e6e03af181339d0bca1e30d905a51950d1442239" - integrity sha512-8/Oft9MWQtbG6p9f8eY5fsKC2CcO5YVDlwive8eUYS9mEbgnyQxm68OyH26WvsSTykQ9QkIbR+fOG56RsIBODw== + version "3.985.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-dynamodb/-/client-dynamodb-3.985.0.tgz#2bcd54c9d215a72145de037efdec30d0ba219b0c" + integrity sha512-e11U2wheDF3hhYOQse3nA0ZsvKGJXf74bwrNnU3gnPhafK7mGfvu6WzJNGIrJBf1aDYsNJ/U5yZL7fe1mBS8fg== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "^3.973.6" - "@aws-sdk/credential-provider-node" "^3.972.5" - "@aws-sdk/dynamodb-codec" "^3.972.7" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/credential-provider-node" "^3.972.6" + "@aws-sdk/dynamodb-codec" "^3.972.8" "@aws-sdk/middleware-endpoint-discovery" "^3.972.3" "@aws-sdk/middleware-host-header" "^3.972.3" "@aws-sdk/middleware-logger" "^3.972.3" "@aws-sdk/middleware-recursion-detection" "^3.972.3" - "@aws-sdk/middleware-user-agent" "^3.972.6" + "@aws-sdk/middleware-user-agent" "^3.972.7" "@aws-sdk/region-config-resolver" "^3.972.3" "@aws-sdk/types" "^3.973.1" - "@aws-sdk/util-endpoints" "3.984.0" + "@aws-sdk/util-endpoints" "3.985.0" "@aws-sdk/util-user-agent-browser" "^3.972.3" - "@aws-sdk/util-user-agent-node" "^3.972.4" + "@aws-sdk/util-user-agent-node" "^3.972.5" "@smithy/config-resolver" "^4.4.6" - "@smithy/core" "^3.22.0" + "@smithy/core" "^3.22.1" "@smithy/fetch-http-handler" "^5.3.9" "@smithy/hash-node" "^4.2.8" "@smithy/invalid-dependency" "^4.2.8" "@smithy/middleware-content-length" "^4.2.8" - "@smithy/middleware-endpoint" "^4.4.12" - "@smithy/middleware-retry" "^4.4.29" + "@smithy/middleware-endpoint" "^4.4.13" + "@smithy/middleware-retry" "^4.4.30" "@smithy/middleware-serde" "^4.2.9" "@smithy/middleware-stack" "^4.2.8" "@smithy/node-config-provider" "^4.3.8" - "@smithy/node-http-handler" "^4.4.8" + "@smithy/node-http-handler" "^4.4.9" "@smithy/protocol-http" "^5.3.8" - "@smithy/smithy-client" "^4.11.1" + "@smithy/smithy-client" "^4.11.2" "@smithy/types" "^4.12.0" "@smithy/url-parser" "^4.2.8" "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" "@smithy/util-body-length-node" "^4.2.1" - "@smithy/util-defaults-mode-browser" "^4.3.28" - "@smithy/util-defaults-mode-node" "^4.2.31" + "@smithy/util-defaults-mode-browser" "^4.3.29" + "@smithy/util-defaults-mode-node" "^4.2.32" "@smithy/util-endpoints" "^3.2.8" "@smithy/util-middleware" "^4.2.8" "@smithy/util-retry" "^4.2.8" @@ -134,33 +134,33 @@ tslib "^2.6.2" "@aws-sdk/client-s3@^3.901.0": - version "3.984.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.984.0.tgz#ca8726d321d383c9b5f0f7c2fb48f69e5d583997" - integrity sha512-7ny2Slr93Y+QniuluvcfWwyDi32zWQfznynL56Tk0vVh7bWrvS/odm8WP2nInKicRVNipcJHY2YInur6Q/9V0A== + version "3.985.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.985.0.tgz#e5ebe2f341c3d4225677cbfd173a8b04a07342ea" + integrity sha512-S9TqjzzZEEIKBnC7yFpvqM7CG9ALpY5qhQ5BnDBJtdG20NoGpjKLGUUfD2wmZItuhbrcM4Z8c6m6Fg0XYIOVvw== dependencies: "@aws-crypto/sha1-browser" "5.2.0" "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "^3.973.6" - "@aws-sdk/credential-provider-node" "^3.972.5" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/credential-provider-node" "^3.972.6" "@aws-sdk/middleware-bucket-endpoint" "^3.972.3" "@aws-sdk/middleware-expect-continue" "^3.972.3" - "@aws-sdk/middleware-flexible-checksums" "^3.972.4" + "@aws-sdk/middleware-flexible-checksums" "^3.972.5" "@aws-sdk/middleware-host-header" "^3.972.3" "@aws-sdk/middleware-location-constraint" "^3.972.3" "@aws-sdk/middleware-logger" "^3.972.3" "@aws-sdk/middleware-recursion-detection" "^3.972.3" - "@aws-sdk/middleware-sdk-s3" "^3.972.6" + "@aws-sdk/middleware-sdk-s3" "^3.972.7" "@aws-sdk/middleware-ssec" "^3.972.3" - "@aws-sdk/middleware-user-agent" "^3.972.6" + "@aws-sdk/middleware-user-agent" "^3.972.7" "@aws-sdk/region-config-resolver" "^3.972.3" - "@aws-sdk/signature-v4-multi-region" "3.984.0" + "@aws-sdk/signature-v4-multi-region" "3.985.0" "@aws-sdk/types" "^3.973.1" - "@aws-sdk/util-endpoints" "3.984.0" + "@aws-sdk/util-endpoints" "3.985.0" "@aws-sdk/util-user-agent-browser" "^3.972.3" - "@aws-sdk/util-user-agent-node" "^3.972.4" + "@aws-sdk/util-user-agent-node" "^3.972.5" "@smithy/config-resolver" "^4.4.6" - "@smithy/core" "^3.22.0" + "@smithy/core" "^3.22.1" "@smithy/eventstream-serde-browser" "^4.2.8" "@smithy/eventstream-serde-config-resolver" "^4.3.8" "@smithy/eventstream-serde-node" "^4.2.8" @@ -171,132 +171,132 @@ "@smithy/invalid-dependency" "^4.2.8" "@smithy/md5-js" "^4.2.8" "@smithy/middleware-content-length" "^4.2.8" - "@smithy/middleware-endpoint" "^4.4.12" - "@smithy/middleware-retry" "^4.4.29" + "@smithy/middleware-endpoint" "^4.4.13" + "@smithy/middleware-retry" "^4.4.30" "@smithy/middleware-serde" "^4.2.9" "@smithy/middleware-stack" "^4.2.8" "@smithy/node-config-provider" "^4.3.8" - "@smithy/node-http-handler" "^4.4.8" + "@smithy/node-http-handler" "^4.4.9" "@smithy/protocol-http" "^5.3.8" - "@smithy/smithy-client" "^4.11.1" + "@smithy/smithy-client" "^4.11.2" "@smithy/types" "^4.12.0" "@smithy/url-parser" "^4.2.8" "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" "@smithy/util-body-length-node" "^4.2.1" - "@smithy/util-defaults-mode-browser" "^4.3.28" - "@smithy/util-defaults-mode-node" "^4.2.31" + "@smithy/util-defaults-mode-browser" "^4.3.29" + "@smithy/util-defaults-mode-node" "^4.2.32" "@smithy/util-endpoints" "^3.2.8" "@smithy/util-middleware" "^4.2.8" "@smithy/util-retry" "^4.2.8" - "@smithy/util-stream" "^4.5.10" + "@smithy/util-stream" "^4.5.11" "@smithy/util-utf8" "^4.2.0" "@smithy/util-waiter" "^4.2.8" tslib "^2.6.2" "@aws-sdk/client-sesv2@^3.901.0": - version "3.984.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-sesv2/-/client-sesv2-3.984.0.tgz#416c9d00b6def6f4f04098674deadf7c45d474de" - integrity sha512-39TuwXcP5i7/WG1KZuGkXwdFu8Hu0ecUZv5ib4Yyr88MZ1rceyxnxkZEjKatuF+xJU5OjuQvdkWjtD3k7rlI4w== + version "3.985.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sesv2/-/client-sesv2-3.985.0.tgz#27b9ca639d8b03b2011ae738ab0cc6b36e54197c" + integrity sha512-RqeSpVUFeg/fI874lNNdJP5nZ+3mUY5qRDDHYiOta3+2esOC/RAG1XcfYnupFR8wDDiIYsi6gHakRUYgiIW13w== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "^3.973.6" - "@aws-sdk/credential-provider-node" "^3.972.5" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/credential-provider-node" "^3.972.6" "@aws-sdk/middleware-host-header" "^3.972.3" "@aws-sdk/middleware-logger" "^3.972.3" "@aws-sdk/middleware-recursion-detection" "^3.972.3" - "@aws-sdk/middleware-user-agent" "^3.972.6" + "@aws-sdk/middleware-user-agent" "^3.972.7" "@aws-sdk/region-config-resolver" "^3.972.3" - "@aws-sdk/signature-v4-multi-region" "3.984.0" + "@aws-sdk/signature-v4-multi-region" "3.985.0" "@aws-sdk/types" "^3.973.1" - "@aws-sdk/util-endpoints" "3.984.0" + "@aws-sdk/util-endpoints" "3.985.0" "@aws-sdk/util-user-agent-browser" "^3.972.3" - "@aws-sdk/util-user-agent-node" "^3.972.4" + "@aws-sdk/util-user-agent-node" "^3.972.5" "@smithy/config-resolver" "^4.4.6" - "@smithy/core" "^3.22.0" + "@smithy/core" "^3.22.1" "@smithy/fetch-http-handler" "^5.3.9" "@smithy/hash-node" "^4.2.8" "@smithy/invalid-dependency" "^4.2.8" "@smithy/middleware-content-length" "^4.2.8" - "@smithy/middleware-endpoint" "^4.4.12" - "@smithy/middleware-retry" "^4.4.29" + "@smithy/middleware-endpoint" "^4.4.13" + "@smithy/middleware-retry" "^4.4.30" "@smithy/middleware-serde" "^4.2.9" "@smithy/middleware-stack" "^4.2.8" "@smithy/node-config-provider" "^4.3.8" - "@smithy/node-http-handler" "^4.4.8" + "@smithy/node-http-handler" "^4.4.9" "@smithy/protocol-http" "^5.3.8" - "@smithy/smithy-client" "^4.11.1" + "@smithy/smithy-client" "^4.11.2" "@smithy/types" "^4.12.0" "@smithy/url-parser" "^4.2.8" "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" "@smithy/util-body-length-node" "^4.2.1" - "@smithy/util-defaults-mode-browser" "^4.3.28" - "@smithy/util-defaults-mode-node" "^4.2.31" + "@smithy/util-defaults-mode-browser" "^4.3.29" + "@smithy/util-defaults-mode-node" "^4.2.32" "@smithy/util-endpoints" "^3.2.8" "@smithy/util-middleware" "^4.2.8" "@smithy/util-retry" "^4.2.8" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/client-sso@3.982.0": - version "3.982.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.982.0.tgz#36ea3868045c6d0ade03bf7a0119ac3a1abf79a8" - integrity sha512-qJrIiivmvujdGqJ0ldSUvhN3k3N7GtPesoOI1BSt0fNXovVnMz4C/JmnkhZihU7hJhDvxJaBROLYTU+lpild4w== +"@aws-sdk/client-sso@3.985.0": + version "3.985.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.985.0.tgz#67287e255389c73d45f8b3b6b55ba7b57a5f73f0" + integrity sha512-81J8iE8MuXhdbMfIz4sWFj64Pe41bFi/uqqmqOC5SlGv+kwoyLsyKS/rH2tW2t5buih4vTUxskRjxlqikTD4oQ== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "^3.973.6" + "@aws-sdk/core" "^3.973.7" "@aws-sdk/middleware-host-header" "^3.972.3" "@aws-sdk/middleware-logger" "^3.972.3" "@aws-sdk/middleware-recursion-detection" "^3.972.3" - "@aws-sdk/middleware-user-agent" "^3.972.6" + "@aws-sdk/middleware-user-agent" "^3.972.7" "@aws-sdk/region-config-resolver" "^3.972.3" "@aws-sdk/types" "^3.973.1" - "@aws-sdk/util-endpoints" "3.982.0" + "@aws-sdk/util-endpoints" "3.985.0" "@aws-sdk/util-user-agent-browser" "^3.972.3" - "@aws-sdk/util-user-agent-node" "^3.972.4" + "@aws-sdk/util-user-agent-node" "^3.972.5" "@smithy/config-resolver" "^4.4.6" - "@smithy/core" "^3.22.0" + "@smithy/core" "^3.22.1" "@smithy/fetch-http-handler" "^5.3.9" "@smithy/hash-node" "^4.2.8" "@smithy/invalid-dependency" "^4.2.8" "@smithy/middleware-content-length" "^4.2.8" - "@smithy/middleware-endpoint" "^4.4.12" - "@smithy/middleware-retry" "^4.4.29" + "@smithy/middleware-endpoint" "^4.4.13" + "@smithy/middleware-retry" "^4.4.30" "@smithy/middleware-serde" "^4.2.9" "@smithy/middleware-stack" "^4.2.8" "@smithy/node-config-provider" "^4.3.8" - "@smithy/node-http-handler" "^4.4.8" + "@smithy/node-http-handler" "^4.4.9" "@smithy/protocol-http" "^5.3.8" - "@smithy/smithy-client" "^4.11.1" + "@smithy/smithy-client" "^4.11.2" "@smithy/types" "^4.12.0" "@smithy/url-parser" "^4.2.8" "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" "@smithy/util-body-length-node" "^4.2.1" - "@smithy/util-defaults-mode-browser" "^4.3.28" - "@smithy/util-defaults-mode-node" "^4.2.31" + "@smithy/util-defaults-mode-browser" "^4.3.29" + "@smithy/util-defaults-mode-node" "^4.2.32" "@smithy/util-endpoints" "^3.2.8" "@smithy/util-middleware" "^4.2.8" "@smithy/util-retry" "^4.2.8" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/core@^3.973.6": - version "3.973.6" - resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.973.6.tgz#dd7ff2af60034da3e1af7926d2ce7efe0341ea64" - integrity sha512-pz4ZOw3BLG0NdF25HoB9ymSYyPbMiIjwQJ2aROXRhAzt+b+EOxStfFv8s5iZyP6Kiw7aYhyWxj5G3NhmkoOTKw== +"@aws-sdk/core@^3.973.7": + version "3.973.7" + resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.973.7.tgz#b2b3e8f272ba283ff8c066560e15b26238fdb410" + integrity sha512-wNZZQQNlJ+hzD49cKdo+PY6rsTDElO8yDImnrI69p2PLBa7QomeUKAJWYp9xnaR38nlHqWhMHZuYLCQ3oSX+xg== dependencies: "@aws-sdk/types" "^3.973.1" "@aws-sdk/xml-builder" "^3.972.4" - "@smithy/core" "^3.22.0" + "@smithy/core" "^3.22.1" "@smithy/node-config-provider" "^4.3.8" "@smithy/property-provider" "^4.2.8" "@smithy/protocol-http" "^5.3.8" "@smithy/signature-v4" "^5.3.8" - "@smithy/smithy-client" "^4.11.1" + "@smithy/smithy-client" "^4.11.2" "@smithy/types" "^4.12.0" "@smithy/util-base64" "^4.3.0" "@smithy/util-middleware" "^4.2.8" @@ -311,46 +311,46 @@ "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-env@^3.972.4": - version "3.972.4" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.4.tgz#33b5ad1169ce5b7ac313ce2c27b939277795b9d0" - integrity sha512-/8dnc7+XNMmViEom2xsNdArQxQPSgy4Z/lm6qaFPTrMFesT1bV3PsBhb19n09nmxHdrtQskYmViddUIjUQElXg== +"@aws-sdk/credential-provider-env@^3.972.5": + version "3.972.5" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.5.tgz#33ccf5df157a2e14a502e2295996f830547b6346" + integrity sha512-LxJ9PEO4gKPXzkufvIESUysykPIdrV7+Ocb9yAhbhJLE4TiAYqbCVUE+VuKP1leGR1bBfjWjYgSV5MxprlX3mQ== dependencies: - "@aws-sdk/core" "^3.973.6" + "@aws-sdk/core" "^3.973.7" "@aws-sdk/types" "^3.973.1" "@smithy/property-provider" "^4.2.8" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-http@^3.972.6": - version "3.972.6" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.6.tgz#3a6720826cff7620690fc4fdf522a81e299a2ebf" - integrity sha512-5ERWqRljiZv44AIdvIRQ3k+EAV0Sq2WeJHvXuK7gL7bovSxOf8Al7MLH7Eh3rdovH4KHFnlIty7J71mzvQBl5Q== +"@aws-sdk/credential-provider-http@^3.972.7": + version "3.972.7" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.7.tgz#089412221a6ca6769e9fe9af95c12b0fbebcce93" + integrity sha512-L2uOGtvp2x3bTcxFTpSM+GkwFIPd8pHfGWO1764icMbo7e5xJh0nfhx1UwkXLnwvocTNEf8A7jISZLYjUSNaTg== dependencies: - "@aws-sdk/core" "^3.973.6" + "@aws-sdk/core" "^3.973.7" "@aws-sdk/types" "^3.973.1" "@smithy/fetch-http-handler" "^5.3.9" - "@smithy/node-http-handler" "^4.4.8" + "@smithy/node-http-handler" "^4.4.9" "@smithy/property-provider" "^4.2.8" "@smithy/protocol-http" "^5.3.8" - "@smithy/smithy-client" "^4.11.1" + "@smithy/smithy-client" "^4.11.2" "@smithy/types" "^4.12.0" - "@smithy/util-stream" "^4.5.10" + "@smithy/util-stream" "^4.5.11" tslib "^2.6.2" -"@aws-sdk/credential-provider-ini@^3.972.4": - version "3.972.4" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.4.tgz#77df2d72984b51f51307914a543514414f780e19" - integrity sha512-eRUg+3HaUKuXWn/lEMirdiA5HOKmEl8hEHVuszIDt2MMBUKgVX5XNGmb3XmbgU17h6DZ+RtjbxQpjhz3SbTjZg== - dependencies: - "@aws-sdk/core" "^3.973.6" - "@aws-sdk/credential-provider-env" "^3.972.4" - "@aws-sdk/credential-provider-http" "^3.972.6" - "@aws-sdk/credential-provider-login" "^3.972.4" - "@aws-sdk/credential-provider-process" "^3.972.4" - "@aws-sdk/credential-provider-sso" "^3.972.4" - "@aws-sdk/credential-provider-web-identity" "^3.972.4" - "@aws-sdk/nested-clients" "3.982.0" +"@aws-sdk/credential-provider-ini@^3.972.5": + version "3.972.5" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.5.tgz#520754da94d91e801ea16303806c93b86ef11444" + integrity sha512-SdDTYE6jkARzOeL7+kudMIM4DaFnP5dZVeatzw849k4bSXDdErDS188bgeNzc/RA2WGrlEpsqHUKP6G7sVXhZg== + dependencies: + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/credential-provider-env" "^3.972.5" + "@aws-sdk/credential-provider-http" "^3.972.7" + "@aws-sdk/credential-provider-login" "^3.972.5" + "@aws-sdk/credential-provider-process" "^3.972.5" + "@aws-sdk/credential-provider-sso" "^3.972.5" + "@aws-sdk/credential-provider-web-identity" "^3.972.5" + "@aws-sdk/nested-clients" "3.985.0" "@aws-sdk/types" "^3.973.1" "@smithy/credential-provider-imds" "^4.2.8" "@smithy/property-provider" "^4.2.8" @@ -358,13 +358,13 @@ "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-login@^3.972.4": - version "3.972.4" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.4.tgz#dc656fbcb3206e5bebbdc44a571503a5ba4e0e6d" - integrity sha512-nLGjXuvWWDlQAp505xIONI7Gam0vw2p7Qu3P6on/W2q7rjJXtYjtpHbcsaOjJ/pAju3eTvEQuSuRedcRHVQIAQ== +"@aws-sdk/credential-provider-login@^3.972.5": + version "3.972.5" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.5.tgz#5677d7a829e5b9a14e37d5784f944cb2c513e082" + integrity sha512-uYq1ILyTSI6ZDCMY5+vUsRM0SOCVI7kaW4wBrehVVkhAxC6y+e9rvGtnoZqCOWL1gKjTMouvsf4Ilhc5NCg1Aw== dependencies: - "@aws-sdk/core" "^3.973.6" - "@aws-sdk/nested-clients" "3.982.0" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/nested-clients" "3.985.0" "@aws-sdk/types" "^3.973.1" "@smithy/property-provider" "^4.2.8" "@smithy/protocol-http" "^5.3.8" @@ -372,17 +372,17 @@ "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-node@^3.972.5": - version "3.972.5" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.5.tgz#2f16620eee963c445a727d4a7b5e000df41fa7b6" - integrity sha512-VWXKgSISQCI2GKN3zakTNHSiZ0+mux7v6YHmmbLQp/o3fvYUQJmKGcLZZzg2GFA+tGGBStplra9VFNf/WwxpYg== - dependencies: - "@aws-sdk/credential-provider-env" "^3.972.4" - "@aws-sdk/credential-provider-http" "^3.972.6" - "@aws-sdk/credential-provider-ini" "^3.972.4" - "@aws-sdk/credential-provider-process" "^3.972.4" - "@aws-sdk/credential-provider-sso" "^3.972.4" - "@aws-sdk/credential-provider-web-identity" "^3.972.4" +"@aws-sdk/credential-provider-node@^3.972.6": + version "3.972.6" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.6.tgz#ac44d928df3e4598263d8b35a6ad6785f65a3ecd" + integrity sha512-DZ3CnAAtSVtVz+G+ogqecaErMLgzph4JH5nYbHoBMgBkwTUV+SUcjsjOJwdBJTHu3Dm6l5LBYekZoU2nDqQk2A== + dependencies: + "@aws-sdk/credential-provider-env" "^3.972.5" + "@aws-sdk/credential-provider-http" "^3.972.7" + "@aws-sdk/credential-provider-ini" "^3.972.5" + "@aws-sdk/credential-provider-process" "^3.972.5" + "@aws-sdk/credential-provider-sso" "^3.972.5" + "@aws-sdk/credential-provider-web-identity" "^3.972.5" "@aws-sdk/types" "^3.973.1" "@smithy/credential-provider-imds" "^4.2.8" "@smithy/property-provider" "^4.2.8" @@ -390,53 +390,53 @@ "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-process@^3.972.4": - version "3.972.4" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.4.tgz#4918ba11b88e0bc96e488f199e6d5605f2449c49" - integrity sha512-TCZpWUnBQN1YPk6grvd5x419OfXjHvhj5Oj44GYb84dOVChpg/+2VoEj+YVA4F4E/6huQPNnX7UYbTtxJqgihw== +"@aws-sdk/credential-provider-process@^3.972.5": + version "3.972.5" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.5.tgz#6851a7170a1625e661600f5954b1cbe21dcf1eb4" + integrity sha512-HDKF3mVbLnuqGg6dMnzBf1VUOywE12/N286msI9YaK9mEIzdsGCtLTvrDhe3Up0R9/hGFbB+9l21/TwF5L1C6g== dependencies: - "@aws-sdk/core" "^3.973.6" + "@aws-sdk/core" "^3.973.7" "@aws-sdk/types" "^3.973.1" "@smithy/property-provider" "^4.2.8" "@smithy/shared-ini-file-loader" "^4.4.3" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-sso@^3.972.4": - version "3.972.4" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.4.tgz#0a945243e26e76c7460e20ae6725e07aa03d75fb" - integrity sha512-wzsGwv9mKlwJ3vHLyembBvGE/5nPUIwRR2I51B1cBV4Cb4ql9nIIfpmHzm050XYTY5fqTOKJQnhLj7zj89VG8g== +"@aws-sdk/credential-provider-sso@^3.972.5": + version "3.972.5" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.5.tgz#a2ee1952f045ccfdef7527cf0f763533a23ba8ab" + integrity sha512-8urj3AoeNeQisjMmMBhFeiY2gxt6/7wQQbEGun0YV/OaOOiXrIudTIEYF8ZfD+NQI6X1FY5AkRsx6O/CaGiybA== dependencies: - "@aws-sdk/client-sso" "3.982.0" - "@aws-sdk/core" "^3.973.6" - "@aws-sdk/token-providers" "3.982.0" + "@aws-sdk/client-sso" "3.985.0" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/token-providers" "3.985.0" "@aws-sdk/types" "^3.973.1" "@smithy/property-provider" "^4.2.8" "@smithy/shared-ini-file-loader" "^4.4.3" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-web-identity@^3.972.4": - version "3.972.4" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.4.tgz#8dd394a0d1e1663fe0dec5ae9f2688cc5d3de410" - integrity sha512-hIzw2XzrG8jzsUSEatehmpkd5rWzASg5IHUfA+m01k/RtvfAML7ZJVVohuKdhAYx+wV2AThLiQJVzqn7F0khrw== +"@aws-sdk/credential-provider-web-identity@^3.972.5": + version "3.972.5" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.5.tgz#c9aaaa412382802418d610828034b77051e98370" + integrity sha512-OK3cULuJl6c+RcDZfPpaK5o3deTOnKZbxm7pzhFNGA3fI2hF9yDih17fGRazJzGGWaDVlR9ejZrpDef4DJCEsw== dependencies: - "@aws-sdk/core" "^3.973.6" - "@aws-sdk/nested-clients" "3.982.0" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/nested-clients" "3.985.0" "@aws-sdk/types" "^3.973.1" "@smithy/property-provider" "^4.2.8" "@smithy/shared-ini-file-loader" "^4.4.3" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/dynamodb-codec@^3.972.7": - version "3.972.7" - resolved "https://registry.yarnpkg.com/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.972.7.tgz#73ad04468838aa6aef8306b9cfd8ecd32d18421b" - integrity sha512-oo1g22IhIvZiBNTqV2DwBh39+pwo7Tz6aBZjA+eTNFJ1jfxD84tTycVPOwaYLb2ZYhgsxOT9bTAj/EHEPoKbLQ== +"@aws-sdk/dynamodb-codec@^3.972.8": + version "3.972.8" + resolved "https://registry.yarnpkg.com/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.972.8.tgz#fc9d349984bcdda430acd44cbd94c317526c4d64" + integrity sha512-5ngfn6fQPSNc7G9LlingK4SXfzcJtv5pOP++erc7HmCq0LcDj//0pcpLgxpDII0sBTh0FcR/iw9i4fBZwSJ2Cg== dependencies: - "@aws-sdk/core" "^3.973.6" - "@smithy/core" "^3.22.0" - "@smithy/smithy-client" "^4.11.1" + "@aws-sdk/core" "^3.973.7" + "@smithy/core" "^3.22.1" + "@smithy/smithy-client" "^4.11.2" "@smithy/types" "^4.12.0" "@smithy/util-base64" "^4.3.0" tslib "^2.6.2" @@ -484,15 +484,15 @@ "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/middleware-flexible-checksums@^3.972.4": - version "3.972.4" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.972.4.tgz#3d43d12a11a07a6660093d10b16d8e65bf242050" - integrity sha512-xOxsUkF3O3BtIe3tf54OpPo94eZepjFm3z0Dd2TZKbsPxMiRTFXurC04wJ58o/wPW9YHVO9VqZik3MfoPfrKlw== +"@aws-sdk/middleware-flexible-checksums@^3.972.5": + version "3.972.5" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.972.5.tgz#08391e6a407a6894e105dca37126dac1f969c107" + integrity sha512-SF/1MYWx67OyCrLA4icIpWUfCkdlOi8Y1KecQ9xYxkL10GMjVdPTGPnYhAg0dw5U43Y9PVUWhAV2ezOaG+0BLg== dependencies: "@aws-crypto/crc32" "5.2.0" "@aws-crypto/crc32c" "5.2.0" "@aws-crypto/util" "5.2.0" - "@aws-sdk/core" "^3.973.6" + "@aws-sdk/core" "^3.973.7" "@aws-sdk/crc64-nvme" "3.972.0" "@aws-sdk/types" "^3.973.1" "@smithy/is-array-buffer" "^4.2.0" @@ -500,7 +500,7 @@ "@smithy/protocol-http" "^5.3.8" "@smithy/types" "^4.12.0" "@smithy/util-middleware" "^4.2.8" - "@smithy/util-stream" "^4.5.10" + "@smithy/util-stream" "^4.5.11" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" @@ -543,23 +543,23 @@ "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/middleware-sdk-s3@^3.972.6": - version "3.972.6" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.6.tgz#d3ca2f65298c6a1076f0244833f11019f5766cb2" - integrity sha512-Xq7wM6kbgJN1UO++8dvH/efPb1nTwWqFCpZCR7RCLOETP7xAUAhVo7JmsCnML5Di/iC4Oo5VrJ4QmkYcMZniLw== +"@aws-sdk/middleware-sdk-s3@^3.972.7": + version "3.972.7" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.7.tgz#eb77744a4533fb289e7278b8e0fa40816f53c309" + integrity sha512-VtZ7tMIw18VzjG+I6D6rh2eLkJfTtByiFoCIauGDtTTPBEUMQUiGaJ/zZrPlCY6BsvLLeFKz3+E5mntgiOWmIg== dependencies: - "@aws-sdk/core" "^3.973.6" + "@aws-sdk/core" "^3.973.7" "@aws-sdk/types" "^3.973.1" "@aws-sdk/util-arn-parser" "^3.972.2" - "@smithy/core" "^3.22.0" + "@smithy/core" "^3.22.1" "@smithy/node-config-provider" "^4.3.8" "@smithy/protocol-http" "^5.3.8" "@smithy/signature-v4" "^5.3.8" - "@smithy/smithy-client" "^4.11.1" + "@smithy/smithy-client" "^4.11.2" "@smithy/types" "^4.12.0" "@smithy/util-config-provider" "^4.2.0" "@smithy/util-middleware" "^4.2.8" - "@smithy/util-stream" "^4.5.10" + "@smithy/util-stream" "^4.5.11" "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" @@ -572,57 +572,57 @@ "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/middleware-user-agent@^3.972.6": - version "3.972.6" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.6.tgz#8abf3fae980f80834460d3345937e5843a59082d" - integrity sha512-TehLN8W/kivl0U9HcS+keryElEWORROpghDXZBLfnb40DXM7hx/i+7OOjkogXQOF3QtUraJVRkHQ07bPhrWKlw== +"@aws-sdk/middleware-user-agent@^3.972.7": + version "3.972.7" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.7.tgz#3c49e740d6e7b594952a5e340925e0a0bfde4777" + integrity sha512-HUD+geASjXSCyL/DHPQc/Ua7JhldTcIglVAoCV8kiVm99IaFSlAbTvEnyhZwdE6bdFyTL+uIaWLaCFSRsglZBQ== dependencies: - "@aws-sdk/core" "^3.973.6" + "@aws-sdk/core" "^3.973.7" "@aws-sdk/types" "^3.973.1" - "@aws-sdk/util-endpoints" "3.982.0" - "@smithy/core" "^3.22.0" + "@aws-sdk/util-endpoints" "3.985.0" + "@smithy/core" "^3.22.1" "@smithy/protocol-http" "^5.3.8" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/nested-clients@3.982.0": - version "3.982.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.982.0.tgz#b7d50bb7c273ed688fab0e52d5430dc6b0167d6d" - integrity sha512-VVkaH27digrJfdVrT64rjkllvOp4oRiZuuJvrylLXAKl18ujToJR7AqpDldL/LS63RVne3QWIpkygIymxFtliQ== +"@aws-sdk/nested-clients@3.985.0": + version "3.985.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.985.0.tgz#b67ee7500dc3e2306e06ff7fa02badae154c3231" + integrity sha512-TsWwKzb/2WHafAY0CE7uXgLj0FmnkBTgfioG9HO+7z/zCPcl1+YU+i7dW4o0y+aFxFgxTMG+ExBQpqT/k2ao8g== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "^3.973.6" + "@aws-sdk/core" "^3.973.7" "@aws-sdk/middleware-host-header" "^3.972.3" "@aws-sdk/middleware-logger" "^3.972.3" "@aws-sdk/middleware-recursion-detection" "^3.972.3" - "@aws-sdk/middleware-user-agent" "^3.972.6" + "@aws-sdk/middleware-user-agent" "^3.972.7" "@aws-sdk/region-config-resolver" "^3.972.3" "@aws-sdk/types" "^3.973.1" - "@aws-sdk/util-endpoints" "3.982.0" + "@aws-sdk/util-endpoints" "3.985.0" "@aws-sdk/util-user-agent-browser" "^3.972.3" - "@aws-sdk/util-user-agent-node" "^3.972.4" + "@aws-sdk/util-user-agent-node" "^3.972.5" "@smithy/config-resolver" "^4.4.6" - "@smithy/core" "^3.22.0" + "@smithy/core" "^3.22.1" "@smithy/fetch-http-handler" "^5.3.9" "@smithy/hash-node" "^4.2.8" "@smithy/invalid-dependency" "^4.2.8" "@smithy/middleware-content-length" "^4.2.8" - "@smithy/middleware-endpoint" "^4.4.12" - "@smithy/middleware-retry" "^4.4.29" + "@smithy/middleware-endpoint" "^4.4.13" + "@smithy/middleware-retry" "^4.4.30" "@smithy/middleware-serde" "^4.2.9" "@smithy/middleware-stack" "^4.2.8" "@smithy/node-config-provider" "^4.3.8" - "@smithy/node-http-handler" "^4.4.8" + "@smithy/node-http-handler" "^4.4.9" "@smithy/protocol-http" "^5.3.8" - "@smithy/smithy-client" "^4.11.1" + "@smithy/smithy-client" "^4.11.2" "@smithy/types" "^4.12.0" "@smithy/url-parser" "^4.2.8" "@smithy/util-base64" "^4.3.0" "@smithy/util-body-length-browser" "^4.2.0" "@smithy/util-body-length-node" "^4.2.1" - "@smithy/util-defaults-mode-browser" "^4.3.28" - "@smithy/util-defaults-mode-node" "^4.2.31" + "@smithy/util-defaults-mode-browser" "^4.3.29" + "@smithy/util-defaults-mode-node" "^4.2.32" "@smithy/util-endpoints" "^3.2.8" "@smithy/util-middleware" "^4.2.8" "@smithy/util-retry" "^4.2.8" @@ -640,25 +640,25 @@ "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/signature-v4-multi-region@3.984.0": - version "3.984.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.984.0.tgz#ddcd91add853425f818e2478f13158fb6545ee27" - integrity sha512-TaWbfYCwnuOSvDSrgs7QgoaoXse49E7LzUkVOUhoezwB7bkmhp+iojADm7UepCEu4021SquD7NG1xA+WCvmldA== +"@aws-sdk/signature-v4-multi-region@3.985.0": + version "3.985.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.985.0.tgz#0c6ca19831602c39da427209eba189ca1f26961f" + integrity sha512-W6hTSOPiSbh4IdTYVxN7xHjpCh0qvfQU1GKGBzGQm0ZEIOaMmWqiDEvFfyGYKmfBvumT8vHKxQRTX0av9omtIg== dependencies: - "@aws-sdk/middleware-sdk-s3" "^3.972.6" + "@aws-sdk/middleware-sdk-s3" "^3.972.7" "@aws-sdk/types" "^3.973.1" "@smithy/protocol-http" "^5.3.8" "@smithy/signature-v4" "^5.3.8" "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@aws-sdk/token-providers@3.982.0": - version "3.982.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.982.0.tgz#c7a65d4f286c69ef10918fe7758bbe8dc7a064e9" - integrity sha512-v3M0KYp2TVHYHNBT7jHD9lLTWAdS9CaWJ2jboRKt0WAB65bA7iUEpR+k4VqKYtpQN4+8kKSc4w+K6kUNZkHKQw== +"@aws-sdk/token-providers@3.985.0": + version "3.985.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.985.0.tgz#bd700b624093352d25ff564457000ee3efdc6522" + integrity sha512-+hwpHZyEq8k+9JL2PkE60V93v2kNhUIv7STFt+EAez1UJsJOQDhc5LpzEX66pNjclI5OTwBROs/DhJjC/BtMjQ== dependencies: - "@aws-sdk/core" "^3.973.6" - "@aws-sdk/nested-clients" "3.982.0" + "@aws-sdk/core" "^3.973.7" + "@aws-sdk/nested-clients" "3.985.0" "@aws-sdk/types" "^3.973.1" "@smithy/property-provider" "^4.2.8" "@smithy/shared-ini-file-loader" "^4.4.3" @@ -681,27 +681,16 @@ tslib "^2.6.2" "@aws-sdk/util-dynamodb@^3.901.0": - version "3.984.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-dynamodb/-/util-dynamodb-3.984.0.tgz#45388393feaf87b643d1e91f140ac7554aba0130" - integrity sha512-Yg3MbD3UJr/lKCtplbKxcCVxn2aaNURNB3ahaCaTAddMWLKik7aswW9zfZw1LQXuUHQeIBv27votDbp2YYY4aw== - dependencies: - tslib "^2.6.2" - -"@aws-sdk/util-endpoints@3.982.0": - version "3.982.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.982.0.tgz#65674c566a8aa2d35b27dcd4132873e75f58dc76" - integrity sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ== + version "3.985.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-dynamodb/-/util-dynamodb-3.985.0.tgz#1f7cb9eeb3ff02dddd7c8517c4d4b6ae31309cbc" + integrity sha512-bf+DvndbrtbNgGtFT4kqDRC5Udi3W1C3Q4028n1i1+PRbRGPLVaXkfsis393YmiyIHca0WPRsCH9/dE9ROlbuw== dependencies: - "@aws-sdk/types" "^3.973.1" - "@smithy/types" "^4.12.0" - "@smithy/url-parser" "^4.2.8" - "@smithy/util-endpoints" "^3.2.8" tslib "^2.6.2" -"@aws-sdk/util-endpoints@3.984.0": - version "3.984.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.984.0.tgz#a2cb22dbeee710be9558e8678370a44b75bbf162" - integrity sha512-9ebjLA0hMKHeVvXEtTDCCOBtwjb0bOXiuUV06HNeVdgAjH6gj4x4Zwt4IBti83TiyTGOCl5YfZqGx4ehVsasbQ== +"@aws-sdk/util-endpoints@3.985.0": + version "3.985.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.985.0.tgz#a6393811e86aaf71e17aa3313551c19e54ed59f8" + integrity sha512-vth7UfGSUR3ljvaq8V4Rc62FsM7GUTH/myxPWkaEgOrprz1/Pc72EgTXxj+cPPPDAfHFIpjhkB7T7Td0RJx+BA== dependencies: "@aws-sdk/types" "^3.973.1" "@smithy/types" "^4.12.0" @@ -726,12 +715,12 @@ bowser "^2.11.0" tslib "^2.6.2" -"@aws-sdk/util-user-agent-node@^3.972.4": - version "3.972.4" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.4.tgz#35cf669fa3e77973422da5a1df50b79b41d460b3" - integrity sha512-3WFCBLiM8QiHDfosQq3Py+lIMgWlFWwFQliUHUqwEiRqLnKyhgbU3AKa7AWJF7lW2Oc/2kFNY4MlAYVnVc0i8A== +"@aws-sdk/util-user-agent-node@^3.972.5": + version "3.972.5" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.5.tgz#fccbe3b707a9d9bb7a755f7164f4f87dbeb81f84" + integrity sha512-GsUDF+rXyxDZkkJxUsDxnA67FG+kc5W1dnloCFLl6fWzceevsCYzJpASBzT+BPjwUgREE6FngfJYYYMQUY5fZQ== dependencies: - "@aws-sdk/middleware-user-agent" "^3.972.6" + "@aws-sdk/middleware-user-agent" "^3.972.7" "@aws-sdk/types" "^3.973.1" "@smithy/node-config-provider" "^4.3.8" "@smithy/types" "^4.12.0" @@ -1854,7 +1843,7 @@ "@smithy/util-middleware" "^4.2.8" tslib "^2.6.2" -"@smithy/core@^3.22.0", "@smithy/core@^3.22.1": +"@smithy/core@^3.22.1": version "3.22.1" resolved "https://registry.yarnpkg.com/@smithy/core/-/core-3.22.1.tgz#c34180d541c9dc5d29412809a6aa497ea47d74f8" integrity sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g== @@ -2006,7 +1995,7 @@ "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@smithy/middleware-endpoint@^4.4.12", "@smithy/middleware-endpoint@^4.4.13": +"@smithy/middleware-endpoint@^4.4.13": version "4.4.13" resolved "https://registry.yarnpkg.com/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.13.tgz#8a5dda67cbf8e63155a908a724e7ae09b763baad" integrity sha512-x6vn0PjYmGdNuKh/juUJJewZh7MoQ46jYaJ2mvekF4EesMuFfrl4LaW/k97Zjf8PTCPQmPgMvwewg7eNoH9n5w== @@ -2020,7 +2009,7 @@ "@smithy/util-middleware" "^4.2.8" tslib "^2.6.2" -"@smithy/middleware-retry@^4.4.29": +"@smithy/middleware-retry@^4.4.30": version "4.4.30" resolved "https://registry.yarnpkg.com/@smithy/middleware-retry/-/middleware-retry-4.4.30.tgz#a0548803044069b53a332606d4b4f803f07f8963" integrity sha512-CBGyFvN0f8hlnqKH/jckRDz78Snrp345+PVk8Ux7pnkUCW97Iinse59lY78hBt04h1GZ6hjBN94BRwZy1xC8Bg== @@ -2062,7 +2051,7 @@ "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@smithy/node-http-handler@^4.4.8", "@smithy/node-http-handler@^4.4.9": +"@smithy/node-http-handler@^4.4.9": version "4.4.9" resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.4.9.tgz#c167e5b8aed33c5edaf25b903ed9866858499c93" integrity sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w== @@ -2135,7 +2124,7 @@ "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@smithy/smithy-client@^4.11.1", "@smithy/smithy-client@^4.11.2": +"@smithy/smithy-client@^4.11.2": version "4.11.2" resolved "https://registry.yarnpkg.com/@smithy/smithy-client/-/smithy-client-4.11.2.tgz#1f6a4d75625dbaa16bafbe9b10cf6a41c98fe3da" integrity sha512-SCkGmFak/xC1n7hKRsUr6wOnBTJ3L22Qd4e8H1fQIuKTAjntwgU8lrdMe7uHdiT2mJAOWA/60qaW9tiMu69n1A== @@ -2210,7 +2199,7 @@ dependencies: tslib "^2.6.2" -"@smithy/util-defaults-mode-browser@^4.3.28": +"@smithy/util-defaults-mode-browser@^4.3.29": version "4.3.29" resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.29.tgz#fd4f9563ffd1fb49d092e5b86bacc7796170763e" integrity sha512-nIGy3DNRmOjaYaaKcQDzmWsro9uxlaqUOhZDHQed9MW/GmkBZPtnU70Pu1+GT9IBmUXwRdDuiyaeiy9Xtpn3+Q== @@ -2220,7 +2209,7 @@ "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@smithy/util-defaults-mode-node@^4.2.31": +"@smithy/util-defaults-mode-node@^4.2.32": version "4.2.32" resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.32.tgz#bc3e9ee1711a9ac3b1c29ea0bef0e785c1da30da" integrity sha512-7dtFff6pu5fsjqrVve0YMhrnzJtccCWDacNKOkiZjJ++fmjGExmmSu341x+WU6Oc1IccL7lDuaUj7SfrHpWc5Q== @@ -2266,7 +2255,7 @@ "@smithy/types" "^4.12.0" tslib "^2.6.2" -"@smithy/util-stream@^4.5.10", "@smithy/util-stream@^4.5.11": +"@smithy/util-stream@^4.5.11": version "4.5.11" resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-4.5.11.tgz#69bf0816c2a396b389a48a64455dacdb57893984" integrity sha512-lKmZ0S/3Qj2OF5H1+VzvDLb6kRxGzZHq6f3rAsoSu5cTLGsn3v3VQBA8czkNNXlLjoFEtVu3OQT2jEeOtOE2CA== From 3a5dfff4d5d0e311c1aea5d733c16605e7764133 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 6 Feb 2026 16:26:02 -0600 Subject: [PATCH 33/52] PR feedback - increment matched privilege metric --- .../search/handlers/expiration_reminders.py | 13 +++- .../function/test_expiration_reminders.py | 63 +++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py index b4e21d2fc..510179253 100644 --- a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py @@ -174,6 +174,7 @@ def process_expiration_reminders(event: dict, context: LambdaContext): failed=metrics.failed + provider_result['failed'], already_sent=metrics.already_sent + provider_result['already_sent'], no_email=metrics.no_email + provider_result['no_email'], + matched_privileges=metrics.matched_privileges + provider_result['matched_privileges'], ) # Check if approaching timeout; invoke continuation if so @@ -272,12 +273,20 @@ def _process_provider_notification( """ Process a single provider's expiration reminder notification. - :return: Dict with counts for sent, skipped, failed, already_sent, no_email + :return: Dict with counts for sent, skipped, failed, already_sent, no_email, matched_privileges """ - result = {'sent': 0, 'skipped': 0, 'failed': 0, 'already_sent': 0, 'no_email': 0} + result = {'sent': 0, 'skipped': 0, 'failed': 0, 'already_sent': 0, 'no_email': 0, 'matched_privileges': 0} provider_id = provider_doc['providerId'] + # Count privileges that match the expiration query (active + expiring on target date) + expiration_date_str = expiration_date.isoformat() + result['matched_privileges'] = sum( + 1 + for p in provider_doc.get('privileges', []) + if p.get('status') == 'active' and p.get('dateOfExpiration') == expiration_date_str + ) + # Check for registered email - providers with privileges should always be registered provider_email = provider_doc.get('compactConnectRegisteredEmailAddress') if not provider_email: diff --git a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py index b0b94c93b..97a6583de 100644 --- a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py @@ -602,3 +602,66 @@ def test_handler_only_includes_active_privileges_in_email(self, mock_iter, mock_ self.assertEqual('Nebraska', priv['jurisdiction']) self.assertEqual('aud', priv['licenseType']) self.assertEqual('2026-02-16', priv['dateOfExpiration']) + + @patch('handlers.expiration_reminders.ExpirationReminderTracker') + @patch('cc_common.config._Config.email_service_client') + @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') + def test_handler_increments_matched_privileges_metric(self, mock_iter, mock_email_client, mock_tracker_class): + """Metrics.matchedPrivileges is incremented by the count of active privileges expiring on target date per provider.""" + mock_tracker_instance = MagicMock() + mock_tracker_instance.was_already_sent.return_value = False + mock_tracker_class.return_value = mock_tracker_instance + + from handlers.expiration_reminders import PaginatedProviderResult, process_expiration_reminders + + target = '2026-02-16' + # Provider 1: two privileges expiring on target date (both active) + privileges_p1 = [ + { + 'privilegeId': 'p1-priv-1', + 'dateOfExpiration': target, + 'status': 'active', + 'jurisdiction': 'oh', + 'licenseType': 'aud', + }, + { + 'privilegeId': 'p1-priv-2', + 'dateOfExpiration': target, + 'status': 'active', + 'jurisdiction': 'ky', + 'licenseType': 'slp', + }, + ] + # Provider 2: one privilege expiring on target, one on another date (only first counts) + privileges_p2 = [ + { + 'privilegeId': 'p2-priv-1', + 'dateOfExpiration': target, + 'status': 'active', + 'jurisdiction': 'ne', + 'licenseType': 'aud', + }, + { + 'privilegeId': 'p2-priv-2', + 'dateOfExpiration': '2026-03-01', + 'status': 'active', + 'jurisdiction': 'oh', + 'licenseType': 'slp', + }, + ] + doc1 = self._make_provider_doc(provider_id='p1', privileges=privileges_p1) + doc2 = self._make_provider_doc(provider_id='p2', privileges=privileges_p2) + + mock_iter.return_value = iter( + [ + PaginatedProviderResult(provider_doc=doc1, search_after=['cursor1']), + PaginatedProviderResult(provider_doc=doc2, search_after=['cursor2']), + ] + ) + + resp = process_expiration_reminders(self._make_event(), self.mock_context) + + self.assertEqual('complete', resp['status']) + # 2 + 1 = 3 matched privileges (active and expiring on target date) + self.assertEqual(3, resp['metrics']['matchedPrivileges']) + self.assertEqual(2, resp['metrics']['providersWithMatches']) From ad525462581eeda4554e9ab34871bbe1842076ef Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 6 Feb 2026 16:33:47 -0600 Subject: [PATCH 34/52] PR feedback - pass calculated target date into continuation payload --- .../lambdas/python/search/handlers/expiration_reminders.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py index 510179253..26ec2422d 100644 --- a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py @@ -188,6 +188,7 @@ def process_expiration_reminders(event: dict, context: LambdaContext): return _invoke_continuation( event=event, context=context, + target_date_str=target_date_str, search_after=result.search_after, metrics=metrics, depth=continuation_depth, @@ -222,6 +223,7 @@ def _invoke_continuation( *, event: dict, context: LambdaContext, + target_date_str: str, search_after: list, metrics: Metrics, depth: int, @@ -230,7 +232,7 @@ def _invoke_continuation( continuation_event = { 'daysBefore': event['daysBefore'], 'compact': event['compact'], - 'targetDate': event.get('targetDate'), + 'targetDate': target_date_str, 'scheduledTime': event.get('scheduledTime'), '_continuation': { 'searchAfter': search_after, From cdeebf8a4dfb0697aa6e35a80c371d04a4a99530 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 9 Feb 2026 09:02:55 -0600 Subject: [PATCH 35/52] Add cleanup to load testing script --- .../smoke/expiration_reminder_load_tests.py | 83 ++++++++++++++++++- 1 file changed, 79 insertions(+), 4 deletions(-) diff --git a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py index 767d1a087..15b49546d 100755 --- a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py +++ b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py @@ -23,6 +23,8 @@ - Wait for DynamoDB stream events to index providers into OpenSearch (unless --skip-data-load). - Invoke the expiration reminder Lambda for the 30-day event. - Display metrics from the Lambda execution. +- Clean up: when data load was performed, delete all created provider, license, and privilege records + from DynamoDB after the test completes (whether successful or not). Options: --providers N Total number of fake providers to load (smoke mode). Allocates ~2/3 to @@ -251,7 +253,7 @@ def create_providers_batch( non_matching_count: int, target_days_until_expiration: int = 30, progress_log_interval: int = 1000, -) -> int: +) -> tuple[int, list[str]]: """ Create providers with two privileges each: matching_count match the target date (receive email), non_matching_count do not (neither privilege on target date). @@ -263,7 +265,7 @@ def create_providers_batch( :param non_matching_count: Number of providers that do not match (no email). :param target_days_until_expiration: Days until the target expiration date (default 30). :param progress_log_interval: Interval for progress logging. - :return: matching_count for use as expected_sent. + :return: Tuple of (matching_count for use as expected_sent, list of created provider IDs for cleanup). """ target_date = datetime.now(UTC).date() + timedelta(days=target_days_until_expiration) other_date_matching = target_date + timedelta(days=30) @@ -277,10 +279,12 @@ def create_providers_batch( registered_email = config.test_provider_user_username created = 0 + provider_ids: list[str] = [] with DynamoDBBatchWriter(dynamodb_table, batch_size=24) as batch_writer: for i in range(total): provider_id = str(uuid.uuid4()) + provider_ids.append(provider_id) if i < matching_count: exp_1, exp_2 = target_date, other_date_matching else: @@ -312,7 +316,62 @@ def create_providers_batch( logger.warning(f'Failed to write {batch_writer.failed_item_count} items during batch write') logger.info(f'Completed creating {total} providers (target date: {target_date.isoformat()})') - return matching_count + return matching_count, provider_ids + + +def delete_provider_records_batch(provider_ids: list[str], compact: str, progress_log_interval: int = 500): + """ + Delete all DynamoDB records (provider, license, privilege) for the given provider IDs. + Uses paginated query per provider so all records are removed regardless of count. + + :param provider_ids: List of provider IDs to delete. + :param compact: Compact abbreviation (e.g. 'aslp'). + :param progress_log_interval: Log progress every N providers. + """ + if not provider_ids: + return + + logger.info(f'Cleaning up {len(provider_ids)} test providers (provider, license, privilege records)...') + + deleted_total = 0 + failed_count = 0 + + for i, provider_id in enumerate(provider_ids): + if progress_log_interval and (i + 1) % progress_log_interval == 0: + logger.info(f'Cleanup progress: {i + 1}/{len(provider_ids)} providers, {deleted_total} records deleted') + + pk = f'{compact}#PROVIDER#{provider_id}' + try: + # Paginate through all items for this provider (provider, licenses, privileges, updates) + last_evaluated_key = None + items_to_delete: list[dict] = [] + + while True: + kwargs = { + 'KeyConditionExpression': 'pk = :pk', + 'ExpressionAttributeValues': {':pk': pk}, + } + if last_evaluated_key: + kwargs['ExclusiveStartKey'] = last_evaluated_key + + response = dynamodb_table.query(**kwargs) + items_to_delete.extend(response.get('Items', [])) + last_evaluated_key = response.get('LastEvaluatedKey') + if not last_evaluated_key: + break + + with dynamodb_table.batch_writer() as batch: + for item in items_to_delete: + batch.delete_item(Key={'pk': item['pk'], 'sk': item['sk']}) + deleted_total += 1 + + except ClientError as e: + failed_count += 1 + logger.warning(f'Failed to delete records for provider {provider_id}: {e.response["Error"]["Code"]} - {e}') + + logger.info(f'✓ Cleanup complete: {deleted_total} records deleted for {len(provider_ids)} providers') + if failed_count > 0: + logger.warning(f'Failed to delete records for {failed_count} provider(s)') def find_lambda_function_name(partial_name: str) -> str: @@ -479,6 +538,7 @@ def invoke_expiration_reminder_lambda(days_before: int, compact: str = 'aslp'): def run_load_test(skip_data_load: bool = False, providers: int | None = None): """ Run the complete load test or smoke test. + Cleans up created provider, license, and privilege records when data load was performed. :param skip_data_load: If True, skip creating providers and indexing steps. :param providers: If set, smoke mode: create this many providers (~2/3 matching, ~1/3 non-matching). @@ -490,6 +550,8 @@ def run_load_test(skip_data_load: bool = False, providers: int | None = None): logger.info('=' * 80) expected_sent = None + created_provider_ids: list[str] = [] + try: if not skip_data_load: if providers is not None: @@ -505,7 +567,7 @@ def run_load_test(skip_data_load: bool = False, providers: int | None = None): logger.info( f'Step 1: Creating {total} providers ({matching_count} matching, {non_matching_count} non-matching)...' ) - expected_sent = create_providers_batch( + expected_sent, created_provider_ids = create_providers_batch( matching_count=matching_count, non_matching_count=non_matching_count, target_days_until_expiration=30, @@ -571,6 +633,19 @@ def run_load_test(skip_data_load: bool = False, providers: int | None = None): logger.error('Load test failed', exc_info=e) raise + finally: + if created_provider_ids: + logger.info('Step 4: Cleaning up test provider, license, and privilege records...') + try: + progress_interval = max(1, len(created_provider_ids) // 10) if len(created_provider_ids) > 10 else None + delete_provider_records_batch( + provider_ids=created_provider_ids, + compact=COMPACT, + progress_log_interval=progress_interval or 500, + ) + except Exception as e: # noqa: BLE001 + logger.error(f'Cleanup failed (test data may remain in DynamoDB): {e}', exc_info=e) + if __name__ == '__main__': parser = argparse.ArgumentParser(description='Load test for privilege expiration reminder notifications') From 96b39939e209ef034ba3bbfef3f4baeb6519c23d Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 9 Feb 2026 09:35:49 -0600 Subject: [PATCH 36/52] Improve timeout logic for load test script --- .../smoke/expiration_reminder_load_tests.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py index 15b49546d..0eba16961 100755 --- a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py +++ b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py @@ -454,21 +454,23 @@ def invoke_expiration_reminder_lambda(days_before: int, compact: str = 'aslp'): logger.info('Lambda invocation accepted, polling CloudWatch Logs for completion...', log_group=log_group_name) - # Poll CloudWatch Logs for the completion message - # The Lambda logs "Completed processing for compact" - # (or "Completed processing expiration reminders") with metrics - max_wait_time = 960 # 16 minutes (Lambda timeout is 15 minutes) + # Poll CloudWatch Logs for the completion message. + # Keep waiting as long as the log group is still emitting new events. + # Fail only if no new logs appear for inactivity_timeout_sec (Lambda likely crashed or stopped). + inactivity_timeout_sec = 5 * 60 # 5 minute with no new log events check_interval = 10 # Check every 10 seconds + log_lookback_sec = 600 # Fetch events from last 10 minutes start_time = time.time() + last_activity_time = time.time() seen_event_ids = set() # Track which log events we've already processed - while time.time() - start_time < max_wait_time: + while time.time() - last_activity_time <= inactivity_timeout_sec: # Get recent log events try: log_events_response = logs_client.filter_log_events( logGroupName=log_group_name, limit=100, - startTime=int((time.time() - 300) * 1000), # Last 5 minutes + startTime=int((time.time() - log_lookback_sec) * 1000), ) # Process new log events @@ -478,6 +480,7 @@ def invoke_expiration_reminder_lambda(days_before: int, compact: str = 'aslp'): continue # Skip events we've already processed seen_event_ids.add(event_id) + last_activity_time = time.time() # Any new log = Lambda still running message = event.get('message', '') # Log all new messages to relay progress to CLI @@ -523,7 +526,8 @@ def invoke_expiration_reminder_lambda(days_before: int, compact: str = 'aslp'): logger.info(f'Still waiting for Lambda completion... ({int(elapsed)}s elapsed)') raise SmokeTestFailureException( - f'Lambda did not complete within {max_wait_time}s timeout. Check CloudWatch Logs for details.' + f'Log group produced no new events for {inactivity_timeout_sec}s (inactivity timeout). ' + 'Lambda may have crashed or stopped. Check CloudWatch Logs for details.' ) except ClientError as e: From 00ee60635e6c0c4bb32713710e97fed6c8586b19 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 9 Feb 2026 10:00:50 -0600 Subject: [PATCH 37/52] Tweak search alarms --- .../provider_search_domain.py | 7 ++++--- .../tests/app/test_search_persistent_stack.py | 21 +++++++++++++++---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/backend/compact-connect/stacks/search_persistent_stack/provider_search_domain.py b/backend/compact-connect/stacks/search_persistent_stack/provider_search_domain.py index 55df55a54..9478663eb 100644 --- a/backend/compact-connect/stacks/search_persistent_stack/provider_search_domain.py +++ b/backend/compact-connect/stacks/search_persistent_stack/provider_search_domain.py @@ -447,7 +447,8 @@ def _add_capacity_alarms(self, alarm_topic: ITopic): ).add_alarm_action(SnsAction(alarm_topic)) # Alarm: Cluster Status YELLOW - Degraded - # Yellow status indicates degraded state that should be monitored + # Yellow status indicates degraded state that should be monitored. + # Only fire after 15 minutes sustained to reduce noise. Alarm( self, 'ClusterStatusYellowAlarm', @@ -458,7 +459,7 @@ def _add_capacity_alarms(self, alarm_topic: ITopic): period=Duration.minutes(5), statistic='Sum', ), - evaluation_periods=1, # Alert when yellow status is detected + evaluation_periods=3, # 15 minutes sustained (3 × 5 min) before alerting threshold=1, comparison_operator=ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, treat_missing_data=TreatMissingData.NOT_BREACHING, @@ -504,7 +505,7 @@ def _add_capacity_alarms(self, alarm_topic: ITopic): period=Duration.minutes(5), statistic='Minimum', ), - evaluation_periods=3, # set 3 periods to account for any temporary drops + evaluation_periods=6, # set 6 periods (30 minutes) to account for any temporary drops threshold=10, # set to 10 to account for any documents set by OpenSearch by default comparison_operator=ComparisonOperator.LESS_THAN_THRESHOLD, treat_missing_data=TreatMissingData.BREACHING, diff --git a/backend/compact-connect/tests/app/test_search_persistent_stack.py b/backend/compact-connect/tests/app/test_search_persistent_stack.py index cbe55f7bb..e4c1a945c 100644 --- a/backend/compact-connect/tests/app/test_search_persistent_stack.py +++ b/backend/compact-connect/tests/app/test_search_persistent_stack.py @@ -170,13 +170,14 @@ def test_capacity_alarms_configured(self): """ Test that capacity monitoring alarms are configured for proactive scaling. - Verifies six critical alarms: + Verifies seven critical alarms: 1. Free Storage Space < 50% threshold 2. JVM Memory Pressure > 85% threshold 3. CPU Utilization > 70% threshold 4. Cluster Status RED for critical issues - 5. Cluster Status YELLOW for degraded state + 5. Cluster Status YELLOW for degraded state (15 min sustained) 6. Automated Snapshot Failure for backup issues + 7. Searchable Documents < 10 for data loss detection (30 min sustained) These alarms give DevOps team time to plan scaling activities before hitting limits. """ @@ -232,7 +233,7 @@ def test_capacity_alarms_configured(self): }, ) - # Verify Cluster Status YELLOW Alarm + # Verify Cluster Status YELLOW Alarm (15 min sustained to reduce non-prod noise) search_template.has_resource_properties( 'AWS::CloudWatch::Alarm', { @@ -240,7 +241,7 @@ def test_capacity_alarms_configured(self): 'Namespace': 'AWS/ES', 'Threshold': 1, 'ComparisonOperator': 'GreaterThanOrEqualToThreshold', - 'EvaluationPeriods': 1, + 'EvaluationPeriods': 3, }, ) @@ -256,6 +257,18 @@ def test_capacity_alarms_configured(self): }, ) + # Verify Searchable Documents Alarm (30 min sustained to reduce noise) + search_template.has_resource_properties( + 'AWS::CloudWatch::Alarm', + { + 'MetricName': 'SearchableDocuments', + 'Namespace': 'AWS/ES', + 'Threshold': 10, + 'ComparisonOperator': 'LessThanThreshold', + 'EvaluationPeriods': 6, + }, + ) + def test_sandbox_uses_expected_private_subnet(self): """ Test that the OpenSearch Domain in sandbox uses expected private Subnet. From f1c8623a0ef29407955c3553063b5459069922e7 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 9 Feb 2026 11:22:52 -0600 Subject: [PATCH 38/52] optimize load test cleanup --- .../smoke/expiration_reminder_load_tests.py | 91 +++++++------------ 1 file changed, 34 insertions(+), 57 deletions(-) diff --git a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py index 0eba16961..812d0ab67 100755 --- a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py +++ b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py @@ -265,7 +265,7 @@ def create_providers_batch( :param non_matching_count: Number of providers that do not match (no email). :param target_days_until_expiration: Days until the target expiration date (default 30). :param progress_log_interval: Interval for progress logging. - :return: Tuple of (matching_count for use as expected_sent, list of created provider IDs for cleanup). + :return: Tuple of (matching_count for use as expected_sent, list of {'pk', 'sk'} for cleanup). """ target_date = datetime.now(UTC).date() + timedelta(days=target_days_until_expiration) other_date_matching = target_date + timedelta(days=30) @@ -279,12 +279,11 @@ def create_providers_batch( registered_email = config.test_provider_user_username created = 0 - provider_ids: list[str] = [] + created_keys: list[dict] = [] # (pk, sk) for every item created, for fast batch delete at cleanup with DynamoDBBatchWriter(dynamodb_table, batch_size=24) as batch_writer: for i in range(total): provider_id = str(uuid.uuid4()) - provider_ids.append(provider_id) if i < matching_count: exp_1, exp_2 = target_date, other_date_matching else: @@ -307,6 +306,10 @@ def create_providers_batch( batch_writer.put_item(license_record) batch_writer.put_item(priv_1) batch_writer.put_item(priv_2) + + # Track keys for cleanup (batch delete by pk/sk, no query needed) + for record in (provider_record, license_record, priv_1, priv_2): + created_keys.append({'pk': record['pk'], 'sk': record['sk']}) created += 1 if created % progress_log_interval == 0: @@ -316,62 +319,37 @@ def create_providers_batch( logger.warning(f'Failed to write {batch_writer.failed_item_count} items during batch write') logger.info(f'Completed creating {total} providers (target date: {target_date.isoformat()})') - return matching_count, provider_ids + return matching_count, created_keys -def delete_provider_records_batch(provider_ids: list[str], compact: str, progress_log_interval: int = 500): +def delete_provider_records_by_keys( + keys: list[dict], + progress_log_interval: int | None = 500, +): """ - Delete all DynamoDB records (provider, license, privilege) for the given provider IDs. - Uses paginated query per provider so all records are removed regardless of count. + Delete DynamoDB records by a list of (pk, sk) keys. Uses batch_writer for efficient deletes. - :param provider_ids: List of provider IDs to delete. - :param compact: Compact abbreviation (e.g. 'aslp'). - :param progress_log_interval: Log progress every N providers. + :param keys: List of dicts with 'pk' and 'sk' (e.g. [{'pk': '...', 'sk': '...'}, ...]). + :param progress_log_interval: Log progress every N keys (None to disable). """ - if not provider_ids: + if not keys: return - logger.info(f'Cleaning up {len(provider_ids)} test providers (provider, license, privilege records)...') - - deleted_total = 0 - failed_count = 0 - - for i, provider_id in enumerate(provider_ids): - if progress_log_interval and (i + 1) % progress_log_interval == 0: - logger.info(f'Cleanup progress: {i + 1}/{len(provider_ids)} providers, {deleted_total} records deleted') - - pk = f'{compact}#PROVIDER#{provider_id}' - try: - # Paginate through all items for this provider (provider, licenses, privileges, updates) - last_evaluated_key = None - items_to_delete: list[dict] = [] - - while True: - kwargs = { - 'KeyConditionExpression': 'pk = :pk', - 'ExpressionAttributeValues': {':pk': pk}, - } - if last_evaluated_key: - kwargs['ExclusiveStartKey'] = last_evaluated_key + logger.info(f'Cleaning up {len(keys)} test records (batch delete by pk/sk)...') - response = dynamodb_table.query(**kwargs) - items_to_delete.extend(response.get('Items', [])) - last_evaluated_key = response.get('LastEvaluatedKey') - if not last_evaluated_key: - break - - with dynamodb_table.batch_writer() as batch: - for item in items_to_delete: - batch.delete_item(Key={'pk': item['pk'], 'sk': item['sk']}) - deleted_total += 1 - - except ClientError as e: - failed_count += 1 - logger.warning(f'Failed to delete records for provider {provider_id}: {e.response["Error"]["Code"]} - {e}') + deleted = 0 + try: + with dynamodb_table.batch_writer() as batch: + for i, key in enumerate(keys): + batch.delete_item(Key=key) + deleted += 1 + if progress_log_interval and (i + 1) % progress_log_interval == 0: + logger.info(f'Cleanup progress: {deleted}/{len(keys)} records deleted') + except ClientError as e: + logger.warning(f'Batch delete error after {deleted} items: {e.response["Error"]["Code"]} - {e}') + raise - logger.info(f'✓ Cleanup complete: {deleted_total} records deleted for {len(provider_ids)} providers') - if failed_count > 0: - logger.warning(f'Failed to delete records for {failed_count} provider(s)') + logger.info(f'✓ Cleanup complete: {deleted} records deleted') def find_lambda_function_name(partial_name: str) -> str: @@ -554,7 +532,7 @@ def run_load_test(skip_data_load: bool = False, providers: int | None = None): logger.info('=' * 80) expected_sent = None - created_provider_ids: list[str] = [] + created_keys: list[dict] = [] try: if not skip_data_load: @@ -571,7 +549,7 @@ def run_load_test(skip_data_load: bool = False, providers: int | None = None): logger.info( f'Step 1: Creating {total} providers ({matching_count} matching, {non_matching_count} non-matching)...' ) - expected_sent, created_provider_ids = create_providers_batch( + expected_sent, created_keys = create_providers_batch( matching_count=matching_count, non_matching_count=non_matching_count, target_days_until_expiration=30, @@ -638,13 +616,12 @@ def run_load_test(skip_data_load: bool = False, providers: int | None = None): raise finally: - if created_provider_ids: + if created_keys: logger.info('Step 4: Cleaning up test provider, license, and privilege records...') try: - progress_interval = max(1, len(created_provider_ids) // 10) if len(created_provider_ids) > 10 else None - delete_provider_records_batch( - provider_ids=created_provider_ids, - compact=COMPACT, + progress_interval = max(1, len(created_keys) // 10) if len(created_keys) > 10 else None + delete_provider_records_by_keys( + keys=created_keys, progress_log_interval=progress_interval or 500, ) except Exception as e: # noqa: BLE001 From e4c60669fc34b458f3d210666c4159b71072aee1 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 9 Feb 2026 11:35:16 -0600 Subject: [PATCH 39/52] adjust duration alarm --- .../stacks/expiration_reminder_stack/__init__.py | 6 +++--- .../tests/app/test_expiration_reminder_stack.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py b/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py index 8a1a94820..1875ca939 100644 --- a/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py +++ b/backend/compact-connect/stacks/expiration_reminder_stack/__init__.py @@ -150,15 +150,15 @@ def __init__( treat_missing_data=TreatMissingData.NOT_BREACHING, ).add_alarm_action(SnsAction(persistent_stack.alarm_topic)) - # CloudWatch alarm for Lambda execution duration (triggers if exceeds 10 minutes) + # CloudWatch alarm for Lambda execution duration (triggers if exceeds 14 minutes) Alarm( self, 'ExpirationReminderDurationAlarm', metric=self.expiration_reminder_handler.metric_duration(statistic=Stats.MAXIMUM, period=Duration.days(1)), evaluation_periods=1, - threshold=600_000, # 10 minutes in milliseconds + threshold=840_000, # 14 minutes in milliseconds actions_enabled=True, - alarm_description=f'{self.expiration_reminder_handler.node.path} Lambda Duration exceeded 10 minutes', + alarm_description=f'{self.expiration_reminder_handler.node.path} Lambda Duration exceeded 14 minutes', comparison_operator=ComparisonOperator.GREATER_THAN_THRESHOLD, treat_missing_data=TreatMissingData.NOT_BREACHING, ).add_alarm_action(SnsAction(persistent_stack.alarm_topic)) diff --git a/backend/compact-connect/tests/app/test_expiration_reminder_stack.py b/backend/compact-connect/tests/app/test_expiration_reminder_stack.py index adbebb541..6dc0a4223 100644 --- a/backend/compact-connect/tests/app/test_expiration_reminder_stack.py +++ b/backend/compact-connect/tests/app/test_expiration_reminder_stack.py @@ -105,7 +105,7 @@ def test_eventbridge_rules_created(self): self.assertIn('Arn', rule['Targets'][0]) def test_duration_alarm_configured(self): - """Test that the duration alarm is configured with a 10-minute threshold.""" + """Test that the duration alarm is configured with a 14-minute threshold.""" # Stack is only created if hosted_zone is configured if not hasattr(self.app.sandbox_backend_stage, 'expiration_reminder_stack'): self.skipTest('ExpirationReminderStack not created (hosted_zone not configured)') @@ -125,7 +125,7 @@ def test_duration_alarm_configured(self): ) # Verify threshold is 10 minutes (600,000 milliseconds) - self.assertEqual(duration_alarm['Threshold'], 600_000) + self.assertEqual(duration_alarm['Threshold'], 840_000) self.assertEqual(duration_alarm['ComparisonOperator'], 'GreaterThanThreshold') self.assertEqual(duration_alarm['EvaluationPeriods'], 1) self.assertEqual(duration_alarm['MetricName'], 'Duration') From b5047fd1e412c544f791dbf8b6abd3f46364ec7d Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 9 Feb 2026 13:35:26 -0600 Subject: [PATCH 40/52] PR feedback - catch single validation errors to prevent batch failure --- .../search/handlers/expiration_reminders.py | 29 ++++-- .../function/test_expiration_reminders.py | 98 ++++++++++++++++++- .../smoke/expiration_reminder_load_tests.py | 1 - 3 files changed, 118 insertions(+), 10 deletions(-) diff --git a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py index 26ec2422d..511466d5f 100644 --- a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py @@ -6,6 +6,8 @@ from datetime import date, timedelta from aws_lambda_powertools.utilities.typing import LambdaContext +from marshmallow import ValidationError + from cc_common.config import config, logger from cc_common.data_model.compact_configuration_utils import CompactConfigUtility from cc_common.data_model.schema.provider.api import ProviderGeneralResponseSchema @@ -37,7 +39,6 @@ class Metrics: """Tracks notification processing statistics.""" sent: int = 0 - skipped: int = 0 failed: int = 0 already_sent: int = 0 no_email: int = 0 @@ -47,7 +48,6 @@ class Metrics: def as_dict(self) -> dict[str, int]: return { 'sent': self.sent, - 'skipped': self.skipped, 'failed': self.failed, 'alreadySent': self.already_sent, 'noEmail': self.no_email, @@ -70,7 +70,6 @@ def _initialize_metrics(accumulated: dict[str, int] | None) -> Metrics: return Metrics() return Metrics( sent=accumulated.get('sent', 0), - skipped=accumulated.get('skipped', 0), failed=accumulated.get('failed', 0), already_sent=accumulated.get('alreadySent', 0), no_email=accumulated.get('noEmail', 0), @@ -170,7 +169,6 @@ def process_expiration_reminders(event: dict, context: LambdaContext): metrics = replace( metrics, sent=metrics.sent + provider_result['sent'], - skipped=metrics.skipped + provider_result['skipped'], failed=metrics.failed + provider_result['failed'], already_sent=metrics.already_sent + provider_result['already_sent'], no_email=metrics.no_email + provider_result['no_email'], @@ -275,9 +273,9 @@ def _process_provider_notification( """ Process a single provider's expiration reminder notification. - :return: Dict with counts for sent, skipped, failed, already_sent, no_email, matched_privileges + :return: Dict with counts for sent, failed, already_sent, no_email, matched_privileges """ - result = {'sent': 0, 'skipped': 0, 'failed': 0, 'already_sent': 0, 'no_email': 0, 'matched_privileges': 0} + result = {'sent': 0, 'failed': 0, 'already_sent': 0, 'no_email': 0, 'matched_privileges': 0} provider_id = provider_doc['providerId'] @@ -405,6 +403,9 @@ def iterate_privileges_by_expiration_date( OpenSearch pagination is handled internally using `search_after`. Results are yielded one provider at a time with their cursor for continuation support. Use initial_search_after to resume from a previous invocation. + + If a hit fails schema validation (ValidationError), it is logged and skipped so a bad + document does not abort the run. """ index_name = f'compact_{compact}_providers' search_after = initial_search_after @@ -436,8 +437,22 @@ def iterate_privileges_by_expiration_date( current_page_hits = list(reversed(hits)) hit = current_page_hits.pop() + try: + provider_doc = _provider_document_from_hit(hit) + except ValidationError as e: + hit_id = hit.get('_id') + provider_id_from_source = (hit.get('_source') or {}).get('providerId') + logger.error( + 'Skipping hit due to provider document validation error', + hit_id=hit_id, + provider_id=provider_id_from_source, + compact=compact, + validation_messages=str(e.messages) if getattr(e, 'messages', None) else str(e), + ) + continue + yield PaginatedProviderResult( - provider_doc=_provider_document_from_hit(hit), + provider_doc=provider_doc, search_after=hit['sort'], ) diff --git a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py index 97a6583de..eb7854f2e 100644 --- a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py @@ -204,6 +204,101 @@ def _make_event(self, days_before: int = 30, compact: str = 'aslp') -> dict: 'scheduledTime': '2026-01-17T10:00:00Z', } + def _valid_provider_source_for_schema(self, provider_id: str = 'valid-provider-123') -> dict: + """Build a provider _source that passes ProviderGeneralResponseSchema (for OpenSearch hit).""" + return { + 'providerId': provider_id, + 'type': 'provider', + 'dateOfUpdate': '2026-01-01T00:00:00', + 'compact': 'aslp', + 'licenseJurisdiction': 'oh', + 'currentHomeJurisdiction': 'oh', + 'licenseStatus': 'active', + 'compactEligibility': 'eligible', + 'givenName': 'Jane', + 'familyName': 'Doe', + 'dateOfExpiration': '2026-02-16', + 'jurisdictionUploadedLicenseStatus': 'active', + 'jurisdictionUploadedCompactEligibility': 'eligible', + 'birthMonthDay': '01-15', + 'compactConnectRegisteredEmailAddress': 'valid@example.com', + 'privileges': [ + { + 'type': 'privilege', + 'providerId': provider_id, + 'compact': 'aslp', + 'jurisdiction': 'oh', + 'licenseJurisdiction': 'oh', + 'licenseType': 'aud', + 'dateOfIssuance': '2025-01-01', + 'dateOfRenewal': '2025-01-01', + 'dateOfExpiration': '2026-02-16', + 'dateOfUpdate': '2025-01-01T00:00:00', + 'administratorSetStatus': 'active', + 'privilegeId': 'priv-1', + 'status': 'active', + }, + ], + } + + @patch('handlers.expiration_reminders.ExpirationReminderTracker') + @patch('cc_common.config._Config.email_service_client') + @patch('handlers.expiration_reminders.opensearch_client') + def test_handler_skips_invalid_schema_hit_and_sends_notification_for_valid_hit( + self, mock_opensearch, mock_email_client, mock_tracker_class + ): + """When OpenSearch returns one invalid (missing required field) and one valid provider, only the valid one gets a notification.""" + mock_opensearch.search_with_retry = MagicMock( + return_value={ + 'hits': { + 'total': {'value': 2, 'relation': 'eq'}, + 'hits': [ + { + '_id': 'invalid-hit-id', + '_source': { + # Missing required 'givenName' -> ValidationError + 'providerId': 'invalid-provider-456', + 'type': 'provider', + 'dateOfUpdate': '2026-01-01T00:00:00', + 'compact': 'aslp', + 'licenseJurisdiction': 'oh', + 'licenseStatus': 'active', + 'compactEligibility': 'eligible', + 'familyName': 'Bad', + 'dateOfExpiration': '2026-02-16', + 'jurisdictionUploadedLicenseStatus': 'active', + 'jurisdictionUploadedCompactEligibility': 'eligible', + 'birthMonthDay': '06-20', + 'privileges': [], + }, + 'sort': ['invalid-provider-456'], + }, + { + '_id': 'valid-hit-id', + '_source': self._valid_provider_source_for_schema(), + 'sort': ['valid-provider-123'], + }, + ], + } + } + ) + + mock_tracker_instance = MagicMock() + mock_tracker_instance.was_already_sent.return_value = False + mock_tracker_class.return_value = mock_tracker_instance + + from handlers.expiration_reminders import process_expiration_reminders + + resp = process_expiration_reminders(self._make_event(days_before=30), self.mock_context) + + self.assertEqual('complete', resp['status']) + self.assertEqual(1, resp['metrics']['sent'], 'Exactly one notification should be sent for the valid provider') + self.assertEqual(1, resp['metrics']['providersWithMatches']) + mock_email_client.send_privilege_expiration_reminder_email.assert_called_once() + call_kwargs = mock_email_client.send_privilege_expiration_reminder_email.call_args.kwargs + self.assertEqual('valid@example.com', call_kwargs['provider_email']) + self.assertEqual('Jane', call_kwargs['template_variables'].provider_first_name) + @patch('handlers.expiration_reminders.ExpirationReminderTracker') @patch('cc_common.config._Config.email_service_client') @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') @@ -453,7 +548,7 @@ def test_handler_continuation_parses_accumulated_metrics_and_search_after( '_continuation': { 'searchAfter': ['cursor1'], 'depth': 1, - 'accumulatedMetrics': {'sent': 100, 'skipped': 2, 'alreadySent': 5}, + 'accumulatedMetrics': {'sent': 100, 'alreadySent': 5}, }, } @@ -470,7 +565,6 @@ def test_handler_continuation_parses_accumulated_metrics_and_search_after( self.assertEqual('complete', resp['status']) self.assertEqual(101, resp['metrics']['sent']) - self.assertEqual(2, resp['metrics']['skipped']) self.assertEqual(7, resp['metrics']['alreadySent']) self.assertEqual(2, resp['totalInvocations']) mock_iter.assert_called_once() diff --git a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py index 812d0ab67..f14b24d54 100755 --- a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py +++ b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py @@ -582,7 +582,6 @@ def run_load_test(skip_data_load: bool = False, providers: int | None = None): logger.info('=' * 80) logger.info(f'Lambda Execution Duration: {lambda_duration:.2f} seconds') logger.info(f'Notifications Sent: {metrics.get("sent", 0)}') - logger.info(f'Notifications Skipped: {metrics.get("skipped", 0)}') logger.info(f'Notifications Failed: {metrics.get("failed", 0)}') logger.info(f'Already Sent (idempotency): {metrics.get("alreadySent", 0)}') logger.info(f'No Email Address: {metrics.get("noEmail", 0)}') From b0fbb9731d7d5848b2b7f638145ec7770ad65f85 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 9 Feb 2026 14:43:59 -0600 Subject: [PATCH 41/52] Do not deploy reminder stack to beta --- .../compact-connect/pipeline/backend_stage.py | 78 ++++++++++--------- backend/compact-connect/tests/app/base.py | 4 +- 2 files changed, 44 insertions(+), 38 deletions(-) diff --git a/backend/compact-connect/pipeline/backend_stage.py b/backend/compact-connect/pipeline/backend_stage.py index cb2fd7059..0f479de90 100644 --- a/backend/compact-connect/pipeline/backend_stage.py +++ b/backend/compact-connect/pipeline/backend_stage.py @@ -37,6 +37,7 @@ def __init__( ): super().__init__(scope, construct_id, **kwargs) + self.environment_name = environment_name standard_tags = StandardTags(**self.node.get_context('tags'), environment=environment_name) environment = Environment(account=environment_context['account_id'], region=environment_context['region']) @@ -175,43 +176,6 @@ def __init__( persistent_stack=self.persistent_stack, ) - # Reporting and notifications depend on emails, which depend on having a domain name. If we don't configure - # a HostedZone we won't bother with these whole stacks. - if self.persistent_stack.hosted_zone: - self.notification_stack = NotificationStack( - self, - 'NotificationStack', - env=environment, - environment_context=environment_context, - standard_tags=standard_tags, - environment_name=environment_name, - persistent_stack=self.persistent_stack, - event_state_stack=self.event_state_stack, - ) - - self.reporting_stack = ReportingStack( - self, - 'ReportingStack', - env=environment, - environment_context=environment_context, - environment_name=environment_name, - standard_tags=standard_tags, - persistent_stack=self.persistent_stack, - ) - - self.expiration_reminder_stack = ExpirationReminderStack( - self, - 'ExpirationReminderStack', - env=environment, - environment_context=environment_context, - standard_tags=standard_tags, - environment_name=environment_name, - persistent_stack=self.persistent_stack, - event_state_stack=self.event_state_stack, - search_persistent_stack=self.search_persistent_stack, - vpc_stack=self.vpc_stack, - ) - self.transaction_monitoring_stack = TransactionMonitoringStack( self, 'TransactionMonitoringStack', @@ -253,3 +217,43 @@ def __init__( persistent_stack=self.persistent_stack, search_persistent_stack=self.search_persistent_stack, ) + + # Reporting and notifications depend on emails, which depend on having a domain name. If we don't configure + # a HostedZone we won't bother with these whole stacks. + if self.persistent_stack.hosted_zone: + self.notification_stack = NotificationStack( + self, + 'NotificationStack', + env=environment, + environment_context=environment_context, + standard_tags=standard_tags, + environment_name=environment_name, + persistent_stack=self.persistent_stack, + event_state_stack=self.event_state_stack, + ) + + self.reporting_stack = ReportingStack( + self, + 'ReportingStack', + env=environment, + environment_context=environment_context, + environment_name=environment_name, + standard_tags=standard_tags, + persistent_stack=self.persistent_stack, + ) + + # Expiration reminder stack is not deployed in beta + # to reduce noise in that environment. + if environment_name != 'beta': + self.expiration_reminder_stack = ExpirationReminderStack( + self, + 'ExpirationReminderStack', + env=environment, + environment_context=environment_context, + standard_tags=standard_tags, + environment_name=environment_name, + persistent_stack=self.persistent_stack, + event_state_stack=self.event_state_stack, + search_persistent_stack=self.search_persistent_stack, + vpc_stack=self.vpc_stack, + ) diff --git a/backend/compact-connect/tests/app/base.py b/backend/compact-connect/tests/app/base.py index 351ddcb86..3ea316664 100644 --- a/backend/compact-connect/tests/app/base.py +++ b/backend/compact-connect/tests/app/base.py @@ -579,7 +579,9 @@ def _check_no_backend_stage_annotations(self, stage: BackendStage): if stage.persistent_stack.hosted_zone: self._check_no_stack_annotations(stage.notification_stack) self._check_no_stack_annotations(stage.reporting_stack) - self._check_no_stack_annotations(stage.expiration_reminder_stack) + # Expiration reminder stack is not deployed in beta (same as pipeline) + if stage.environment_name != 'beta': + self._check_no_stack_annotations(stage.expiration_reminder_stack) # No backup stack here, because nexted stack annotations are checked in the parent stack def _count_stack_resources(self, stack: Stack) -> int: From cd9f7eac63685dc719434b98d94e32756b66fb8a Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 9 Feb 2026 14:55:32 -0600 Subject: [PATCH 42/52] remove unneeded custom class from load test --- .../smoke/expiration_reminder_load_tests.py | 86 ++----------------- 1 file changed, 5 insertions(+), 81 deletions(-) diff --git a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py index f14b24d54..c9a66e5bb 100755 --- a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py +++ b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py @@ -85,79 +85,6 @@ raise SmokeTestFailureException(f'No license types found for compact {COMPACT}') -class DynamoDBBatchWriter: - """Utility class to batch DynamoDB put_item operations for better efficiency.""" - - def __init__(self, table, batch_size: int = 25): - """ - Initialize the batch writer. - - :param table: DynamoDB table resource (boto3 resource Table) - :param batch_size: Batch size to use for API calls, default: 25 (DynamoDB max) - """ - self._table = table - self._batch_size = batch_size - self._batch = None - self._count = 0 - self.failed_item_count = 0 - self.failed_items = None - - def _do_batch_write(self): - """Execute the batch write operation.""" - # DynamoDB batch_write_item has a hard limit of 25 items per request - # Slice out exactly 25 items (or fewer if batch is smaller) and keep remainder - max_items_per_request = 25 - items_to_write = self._batch[:max_items_per_request] - remaining_items = self._batch[max_items_per_request:] - - if not items_to_write: - return - - # DynamoDB batch_write_item requires a dict keyed by table name - table_name = self._table.name - response = self._table.meta.client.batch_write_item( - RequestItems={table_name: [{'PutRequest': {'Item': item}} for item in items_to_write]} - ) - - # Check for unprocessed items (shouldn't happen with proper batch sizing, but handle it) - unprocessed = response.get('UnprocessedItems', {}) - if unprocessed: - unprocessed_count = len(unprocessed.get(table_name, [])) - self.failed_item_count += unprocessed_count - logger.warning(f'Unprocessed items in batch write: {unprocessed_count}') - - # Keep remaining items for next batch write - self._batch = remaining_items - self._count = len(remaining_items) - - def __enter__(self): - self._batch = [] - self._count = 0 - self.failed_items = [] - self.failed_item_count = 0 - return self - - def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): - # Flush any remaining items (may require multiple calls if > 25 items) - while len(self._batch) > 0: - self._do_batch_write() - if exc_val is not None: - raise exc_val - - def put_item(self, item: dict): - """ - Add an item to the batch. Will automatically flush when batch size is reached. - - :param item: Dictionary representing the DynamoDB item to write - """ - if self._batch is None: - raise RuntimeError('This object must be used as a context manager') - self._batch.append(item) - self._count += 1 - if self._count >= self._batch_size: - self._do_batch_write() - - def create_provider_records( provider_id: str, compact: str, @@ -281,7 +208,7 @@ def create_providers_batch( created = 0 created_keys: list[dict] = [] # (pk, sk) for every item created, for fast batch delete at cleanup - with DynamoDBBatchWriter(dynamodb_table, batch_size=24) as batch_writer: + with dynamodb_table.batch_writer() as batch: for i in range(total): provider_id = str(uuid.uuid4()) if i < matching_count: @@ -302,10 +229,10 @@ def create_providers_batch( email=registered_email, ) - batch_writer.put_item(provider_record) - batch_writer.put_item(license_record) - batch_writer.put_item(priv_1) - batch_writer.put_item(priv_2) + batch.put_item(Item=provider_record) + batch.put_item(Item=license_record) + batch.put_item(Item=priv_1) + batch.put_item(Item=priv_2) # Track keys for cleanup (batch delete by pk/sk, no query needed) for record in (provider_record, license_record, priv_1, priv_2): @@ -315,9 +242,6 @@ def create_providers_batch( if created % progress_log_interval == 0: logger.info(f'Created {created}/{total} providers') - if batch_writer.failed_item_count > 0: - logger.warning(f'Failed to write {batch_writer.failed_item_count} items during batch write') - logger.info(f'Completed creating {total} providers (target date: {target_date.isoformat()})') return matching_count, created_keys From bb021985943db8cb23e0bc8b75bb07571edbbd72 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 9 Feb 2026 14:59:30 -0600 Subject: [PATCH 43/52] formatting --- .../bin/generate_privilege_test_data.py | 5 ++-- .../cc_common/data_model/data_client.py | 23 +++++++++++-------- .../data_model/provider_record_util.py | 5 ++-- .../tests/function/__init__.py | 4 ++-- .../python/purchases/handlers/privileges.py | 13 +++++++---- .../search/handlers/expiration_reminders.py | 3 +-- .../cc_common/data_model/data_client.py | 2 +- .../tests/function/__init__.py | 4 ++-- 8 files changed, 34 insertions(+), 25 deletions(-) diff --git a/backend/compact-connect/bin/generate_privilege_test_data.py b/backend/compact-connect/bin/generate_privilege_test_data.py index 3ba561665..790da2444 100755 --- a/backend/compact-connect/bin/generate_privilege_test_data.py +++ b/backend/compact-connect/bin/generate_privilege_test_data.py @@ -470,8 +470,9 @@ def main(): # Check if provider already has a privilege for this specific license type in the target state existing_privileges = provider_user_records.get_privilege_records( - filter_condition=lambda p, license_type=target_license_type: p.jurisdiction == args.privilege_state - and p.licenseType == license_type + filter_condition=lambda p, license_type=target_license_type: ( + p.jurisdiction == args.privilege_state and p.licenseType == license_type + ) ) if existing_privileges: if args.provider_id: 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 fd734b641..84310d6e5 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 @@ -862,8 +862,9 @@ def end_military_affiliation(self, compact: str, provider_id: str) -> None: # Get all military affiliation records that are INITIALIZING or ACTIVE active_military_affiliation_records = provider_user_records.get_military_affiliation_records( - filter_condition=lambda record: record.status - in [MilitaryAffiliationStatus.INITIALIZING, MilitaryAffiliationStatus.ACTIVE] + filter_condition=lambda record: ( + record.status in [MilitaryAffiliationStatus.INITIALIZING, MilitaryAffiliationStatus.ACTIVE] + ) ) # Create provider update record to track the removal of military status fields @@ -2312,7 +2313,7 @@ def lift_privilege_encumbrance( # Get the privilege record privilege_records = provider_user_records.get_privilege_records( - filter_condition=lambda p: (p.jurisdiction == jurisdiction and p.licenseType == license_type_name) + filter_condition=lambda p: p.jurisdiction == jurisdiction and p.licenseType == license_type_name ) if not privilege_records: @@ -2611,8 +2612,9 @@ def update_provider_home_state_jurisdiction( # Get all privileges for the provider that were not deactivated previously all_active_privileges = provider_user_records.get_privilege_records( - filter_condition=lambda privilege: privilege.homeJurisdictionChangeStatus - != HomeJurisdictionChangeStatusEnum.INACTIVE + filter_condition=lambda privilege: ( + privilege.homeJurisdictionChangeStatus != HomeJurisdictionChangeStatusEnum.INACTIVE + ) ) if not all_active_privileges: @@ -2678,8 +2680,9 @@ def update_provider_home_state_jurisdiction( # Get licenses from the current home state current_home_state_licenses = provider_user_records.get_license_records( - filter_condition=lambda license_data: license_data.jurisdiction - == home_jurisdiction_before_update + filter_condition=lambda license_data: ( + license_data.jurisdiction == home_jurisdiction_before_update + ) ) # Get unique license types from all privileges @@ -2827,8 +2830,10 @@ def _get_privilege_transaction_items_resulting_from_home_jurisdiction_move( privileges_for_license_type = [ privilege for privilege in provider_user_records.get_privilege_records( - filter_condition=lambda p: p.licenseType == license_type - and p.homeJurisdictionChangeStatus != HomeJurisdictionChangeStatusEnum.INACTIVE + filter_condition=lambda p: ( + p.licenseType == license_type + and p.homeJurisdictionChangeStatus != HomeJurisdictionChangeStatusEnum.INACTIVE + ) ) ] diff --git a/backend/compact-connect/lambdas/python/common/cc_common/data_model/provider_record_util.py b/backend/compact-connect/lambdas/python/common/cc_common/data_model/provider_record_util.py index dd50a2252..0b0050ef4 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/data_model/provider_record_util.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/data_model/provider_record_util.py @@ -712,8 +712,9 @@ def find_best_license_in_current_known_licenses(self, jurisdiction: str | None = else: # if jurisdiction is not provided, we filter by the user's current home jurisdiction current_home_jurisdiction_license_records = self.get_license_records( - filter_condition=lambda license_data: license_data.jurisdiction - == self.get_provider_record().currentHomeJurisdiction + filter_condition=lambda license_data: ( + license_data.jurisdiction == self.get_provider_record().currentHomeJurisdiction + ) ) # if there are no licenses for their current home jurisdiction, we will search through all licenses license_records = ( diff --git a/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/__init__.py b/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/__init__.py index ba2061beb..71c752625 100644 --- a/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/__init__.py +++ b/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/__init__.py @@ -378,8 +378,8 @@ def _generate_providers( patch_kwargs['new_callable'] = lambda: date_of_update else: # Use random dates for variation - patch_kwargs['new_callable'] = lambda: datetime.now(tz=UTC).replace(microsecond=0) - timedelta( - days=randint(1, 365) + patch_kwargs['new_callable'] = lambda: ( + datetime.now(tz=UTC).replace(microsecond=0) - timedelta(days=randint(1, 365)) ) with patch( diff --git a/backend/compact-connect/lambdas/python/purchases/handlers/privileges.py b/backend/compact-connect/lambdas/python/purchases/handlers/privileges.py index 2922ecfd4..1ccc80757 100644 --- a/backend/compact-connect/lambdas/python/purchases/handlers/privileges.py +++ b/backend/compact-connect/lambdas/python/purchases/handlers/privileges.py @@ -283,8 +283,9 @@ def post_purchase_privileges(event: dict, context: LambdaContext): # noqa: ARG0 # Check for any adverse actions that have been lifted 2 years ago latest_allowed_encumbrance_lift = _get_latest_allowed_encumbrance_lift() blocking_encumbrance_records = provider_user_records.get_adverse_action_records( - filter_condition=lambda record: record.effectiveLiftDate is None - or record.effectiveLiftDate > latest_allowed_encumbrance_lift + filter_condition=lambda record: ( + record.effectiveLiftDate is None or record.effectiveLiftDate > latest_allowed_encumbrance_lift + ) ) if blocking_encumbrance_records: raise CCInvalidRequestException( @@ -293,9 +294,11 @@ def post_purchase_privileges(event: dict, context: LambdaContext): # noqa: ARG0 # we now validate that the license type matches one of the license types from the home state license records matching_license_records = provider_user_records.get_license_records( - filter_condition=lambda record: record.licenseType == body['licenseType'] - and record.jurisdiction == current_home_jurisdiction - and record.compactEligibility == CompactEligibilityStatus.ELIGIBLE, + filter_condition=lambda record: ( + record.licenseType == body['licenseType'] + and record.jurisdiction == current_home_jurisdiction + and record.compactEligibility == CompactEligibilityStatus.ELIGIBLE + ), ) if not matching_license_records: diff --git a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py index 511466d5f..3fadcf873 100644 --- a/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/handlers/expiration_reminders.py @@ -6,14 +6,13 @@ from datetime import date, timedelta from aws_lambda_powertools.utilities.typing import LambdaContext -from marshmallow import ValidationError - from cc_common.config import config, logger from cc_common.data_model.compact_configuration_utils import CompactConfigUtility from cc_common.data_model.schema.provider.api import ProviderGeneralResponseSchema from cc_common.email_service_client import PrivilegeExpirationReminderTemplateVariables from cc_common.exceptions import CCInvalidRequestException from expiration_reminder_tracker import ExpirationEventType, ExpirationReminderTracker +from marshmallow import ValidationError from opensearch_client import OpenSearchClient DEFAULT_PAGE_SIZE = 1000 diff --git a/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/data_client.py b/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/data_client.py index 34508bcef..b1d23d067 100644 --- a/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/data_client.py +++ b/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/data_client.py @@ -1811,7 +1811,7 @@ def lift_privilege_encumbrance( # Get the privilege record privilege_records = provider_user_records.get_privilege_records( - filter_condition=lambda p: (p.jurisdiction == jurisdiction and p.licenseType == license_type_name) + filter_condition=lambda p: p.jurisdiction == jurisdiction and p.licenseType == license_type_name ) if not privilege_records: diff --git a/backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/__init__.py b/backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/__init__.py index 93a405ec4..58552b183 100644 --- a/backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/__init__.py +++ b/backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/__init__.py @@ -344,8 +344,8 @@ def _generate_providers( patch_kwargs['new_callable'] = lambda: date_of_update else: # Use random dates for variation - patch_kwargs['new_callable'] = lambda: datetime.now(tz=UTC).replace(microsecond=0) - timedelta( - days=randint(1, 365) + patch_kwargs['new_callable'] = lambda: ( + datetime.now(tz=UTC).replace(microsecond=0) - timedelta(days=randint(1, 365)) ) with patch( From 1eb1a42bb046140784d91ca8457ced58c1498865 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 11 Feb 2026 09:55:59 -0600 Subject: [PATCH 44/52] PR feedback - reworded GH action - improved tests verification/comments for clarity - add details to schema docs --- .../check-compact-connect-ui-app.yml | 2 +- .../data_model/schema/license/api.py | 5 ++- .../data_model/schema/privilege/api.py | 4 +- .../common/cc_common/email_service_client.py | 2 +- .../function/test_expiration_reminders.py | 38 ++++++++++++------- .../provider_search_domain.py | 2 +- .../smoke/expiration_reminder_load_tests.py | 24 ++++++------ 7 files changed, 47 insertions(+), 30 deletions(-) diff --git a/.github/workflows/check-compact-connect-ui-app.yml b/.github/workflows/check-compact-connect-ui-app.yml index b05bbaeb1..45c52517e 100644 --- a/.github/workflows/check-compact-connect-ui-app.yml +++ b/.github/workflows/check-compact-connect-ui-app.yml @@ -113,5 +113,5 @@ jobs: - name: Install all Python dependencies run: "cd backend/compact-connect-ui-app; bin/sync_deps.sh" - - name: Test backend + - name: Test frontend run: "cd backend/compact-connect-ui-app; bin/run_tests.sh -l all -no" diff --git a/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/license/api.py b/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/license/api.py index 12ec805b0..9d48dac56 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/license/api.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/license/api.py @@ -32,7 +32,10 @@ class LicenseExpirationStatusMixin: OpenSearch documents may have stale status values because the licenseStatus field is calculated at write time. If the dateOfExpiration has passed since the last update, - the licenseStatus should be 'inactive' even if the stored value says 'active'. + the licenseStatus should be 'inactive' even if the stored value in OpenSearch says 'active'. + To account for this, this mixin performs the same expiration check that is performed when loading + them from DynamoDB, so the status will factor in the date of expiration and show the correct + active/inactive status. This mixin should be applied to license API response schemas that load data from OpenSearch or other sources where the status may be stale. diff --git a/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/privilege/api.py b/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/privilege/api.py index 2a0114a21..529ff4235 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/privilege/api.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/privilege/api.py @@ -28,7 +28,9 @@ class PrivilegeExpirationStatusMixin: OpenSearch documents may have stale status values because the status field is calculated at write time. If the dateOfExpiration has passed since the last update, the status - should be 'inactive' even if the stored value says 'active'. + should be 'inactive' even if the stored value in OpenSearch says 'active'. To account for this, + this mixin performs the same expiration check that is performed when loading them from DynamoDB, + so the status will factor in the date of expiration and show the correct active/inactive status. This mixin should be applied to privilege API response schemas that load data from OpenSearch or other sources where the status may be stale. diff --git a/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py b/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py index 4ed5c43d5..ecd8ac644 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py @@ -825,7 +825,7 @@ def send_privilege_expiration_reminder_email( *, compact: str, provider_email: str, - template_variables: 'PrivilegeExpirationReminderTemplateVariables', + template_variables: PrivilegeExpirationReminderTemplateVariables, ) -> dict[str, str]: """ Send a privilege expiration reminder email to a provider. diff --git a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py index eb7854f2e..3c1738139 100644 --- a/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py +++ b/backend/compact-connect/lambdas/python/search/tests/function/test_expiration_reminders.py @@ -6,6 +6,7 @@ from moto import mock_aws +from common_test.test_constants import DEFAULT_COMPACT from tests import TstLambdas @@ -56,7 +57,7 @@ def test_iterate_privileges_by_expiration_date_paginates_with_search_after_and_y results = list( iterate_privileges_by_expiration_date( - compact='aslp', + compact=DEFAULT_COMPACT, expiration_date=date(2026, 2, 16), page_size=2, ) @@ -100,7 +101,7 @@ def test_iterate_privileges_by_expiration_date_resumes_with_initial_search_after results = list( iterate_privileges_by_expiration_date( - compact='aslp', + compact=DEFAULT_COMPACT, expiration_date=date(2026, 2, 16), page_size=2, initial_search_after=['p1'], @@ -154,7 +155,7 @@ def test_iterate_privileges_by_expiration_date_returns_full_provider_document(se result = next( iterate_privileges_by_expiration_date( - compact='aslp', + compact=DEFAULT_COMPACT, expiration_date=date(2026, 2, 16), page_size=100, ) @@ -195,7 +196,7 @@ def _make_provider_doc( doc['compactConnectRegisteredEmailAddress'] = email return doc - def _make_event(self, days_before: int = 30, compact: str = 'aslp') -> dict: + def _make_event(self, days_before: int = 30, compact: str = DEFAULT_COMPACT) -> dict: """Helper to create a valid event for testing.""" return { 'targetDate': '2026-02-16', @@ -210,7 +211,7 @@ def _valid_provider_source_for_schema(self, provider_id: str = 'valid-provider-1 'providerId': provider_id, 'type': 'provider', 'dateOfUpdate': '2026-01-01T00:00:00', - 'compact': 'aslp', + 'compact': DEFAULT_COMPACT, 'licenseJurisdiction': 'oh', 'currentHomeJurisdiction': 'oh', 'licenseStatus': 'active', @@ -226,7 +227,7 @@ def _valid_provider_source_for_schema(self, provider_id: str = 'valid-provider-1 { 'type': 'privilege', 'providerId': provider_id, - 'compact': 'aslp', + 'compact': DEFAULT_COMPACT, 'jurisdiction': 'oh', 'licenseJurisdiction': 'oh', 'licenseType': 'aud', @@ -260,7 +261,7 @@ def test_handler_skips_invalid_schema_hit_and_sends_notification_for_valid_hit( 'providerId': 'invalid-provider-456', 'type': 'provider', 'dateOfUpdate': '2026-01-01T00:00:00', - 'compact': 'aslp', + 'compact': DEFAULT_COMPACT, 'licenseJurisdiction': 'oh', 'licenseStatus': 'active', 'compactEligibility': 'eligible', @@ -323,7 +324,7 @@ def test_handler_sends_email_and_records_success(self, mock_iter, mock_email_ser self.assertEqual(1, resp['metrics']['sent']) self.assertEqual(0, resp['metrics']['failed']) self.assertEqual(30, resp['daysBefore']) - self.assertEqual('aslp', resp['compact']) + self.assertEqual(DEFAULT_COMPACT, resp['compact']) mock_email_client.send_privilege_expiration_reminder_email.assert_called_once() call_kwargs = mock_email_client.send_privilege_expiration_reminder_email.call_args.kwargs tv = call_kwargs['template_variables'] @@ -465,7 +466,7 @@ def test_handler_validates_days_before_value(self): { 'targetDate': '2026-02-16', 'daysBefore': 999, # Invalid - 'compact': 'aslp', + 'compact': DEFAULT_COMPACT, }, self.mock_context, ) @@ -476,7 +477,7 @@ def test_handler_requires_days_before_field(self): from handlers.expiration_reminders import process_expiration_reminders with self.assertRaises(CCInvalidRequestException) as ctx: - process_expiration_reminders({'targetDate': '2026-02-16', 'compact': 'aslp'}, self.mock_context) + process_expiration_reminders({'targetDate': '2026-02-16', 'compact': DEFAULT_COMPACT}, self.mock_context) self.assertIn('daysBefore', str(ctx.exception)) def test_handler_requires_compact_field(self): @@ -513,7 +514,7 @@ def test_handler_calculates_target_date_from_days_before_when_not_provided( mock_iter.return_value = iter([]) - resp = process_expiration_reminders({'daysBefore': 30, 'compact': 'aslp'}, self.mock_context) + resp = process_expiration_reminders({'daysBefore': 30, 'compact': DEFAULT_COMPACT}, self.mock_context) self.assertEqual('complete', resp['status']) # Verify it calculated the correct target date (2026-01-17 + 30 days = 2026-02-16) @@ -544,7 +545,7 @@ def test_handler_continuation_parses_accumulated_metrics_and_search_after( event_with_continuation = { 'targetDate': '2026-02-16', 'daysBefore': 30, - 'compact': 'aslp', + 'compact': DEFAULT_COMPACT, '_continuation': { 'searchAfter': ['cursor1'], 'depth': 1, @@ -611,7 +612,7 @@ def test_handler_invokes_itself_with_pagination_values_when_reaching_limit( self.assertEqual('Event', call_kwargs['InvocationType']) payload = json.loads(call_kwargs['Payload']) self.assertEqual(30, payload['daysBefore']) - self.assertEqual('aslp', payload['compact']) + self.assertEqual(DEFAULT_COMPACT, payload['compact']) self.assertEqual(['cursor1'], payload['_continuation']['searchAfter']) self.assertEqual(1, payload['_continuation']['depth']) self.assertEqual(1, payload['_continuation']['accumulatedMetrics']['sent']) @@ -623,7 +624,7 @@ def test_handler_max_continuation_depth_raises(self): event = { 'targetDate': '2026-02-16', 'daysBefore': 30, - 'compact': 'aslp', + 'compact': DEFAULT_COMPACT, '_continuation': { 'searchAfter': ['cursor1'], 'depth': MAX_CONTINUATION_DEPTH, @@ -637,6 +638,7 @@ def test_handler_max_continuation_depth_raises(self): @patch('handlers.expiration_reminders.ExpirationReminderTracker') @patch('cc_common.config._Config.email_service_client') @patch('handlers.expiration_reminders.iterate_privileges_by_expiration_date') + @patch('cc_common.config._Config.current_standard_datetime', datetime.fromisoformat('2026-02-16T12:00:00+00:00')) def test_handler_only_includes_active_privileges_in_email(self, mock_iter, mock_email_client, mock_tracker_class): """ Only active privileges are included in the email; inactive privileges are filtered out. @@ -682,6 +684,14 @@ def test_handler_only_includes_active_privileges_in_email(self, mock_iter, mock_ self.assertEqual('complete', resp['status']) self.assertEqual(1, resp['metrics']['sent']) + # verify the iterator was called with expected params + mock_iter.assert_called_once_with( + compact=DEFAULT_COMPACT, + expiration_date=date.fromisoformat('2026-02-16'), + page_size=1000, + initial_search_after=None, + ) + # Verify only the active privilege was passed to the email client mock_email_client.send_privilege_expiration_reminder_email.assert_called_once() call_kwargs = mock_email_client.send_privilege_expiration_reminder_email.call_args.kwargs diff --git a/backend/compact-connect/stacks/search_persistent_stack/provider_search_domain.py b/backend/compact-connect/stacks/search_persistent_stack/provider_search_domain.py index 9478663eb..a0ec2da24 100644 --- a/backend/compact-connect/stacks/search_persistent_stack/provider_search_domain.py +++ b/backend/compact-connect/stacks/search_persistent_stack/provider_search_domain.py @@ -638,7 +638,7 @@ def _get_search_policy(self, principal: IPrincipal = None): """ Generate search access policy. Specifies a principal, if provided. - Search API policy is restricted to _search endpoint only POST is required for _search queries even though they + Search API policy is restricted to _search endpoint only. POST is required for _search queries even though they are read-only operations because OpenSearch's search API uses POST to send the query DSL body. By restricting the resource to /_search, we prevent POST from being used for document indexing or other write operations. diff --git a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py index c9a66e5bb..95878fc14 100755 --- a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py +++ b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py @@ -7,7 +7,8 @@ performance and capacity (load test) or a small smoke test. Usage: - # Full load test (10k matching + 5k non-matching providers, two privileges per provider) + # Full load test (10k matching providers with privileges expiring soon + 5k providers with privilege not expiring + soon, two privileges per provider) python expiration_reminder_load_test.py python expiration_reminder_load_test.py --skip-data-load @@ -182,11 +183,12 @@ def create_providers_batch( progress_log_interval: int = 1000, ) -> tuple[int, list[str]]: """ - Create providers with two privileges each: matching_count match the target date (receive email), + Create providers with two privileges each: matching_count match the target expiration date (receive email), non_matching_count do not (neither privilege on target date). - Matching providers have one privilege expiring on the target date and one later, so the reminder - email shows both privileges. Non-matching providers have both privileges expiring on other dates. + Matching providers have two active privileges, one privilege expiring on the target date and one later. The reminder + notification should include all the user's active privileges, so both privileges will be included. Non-matching + providers have both privileges expiring on other dates. :param matching_count: Number of providers that match the 30-day search (will receive email). :param non_matching_count: Number of providers that do not match (no email). @@ -194,14 +196,14 @@ def create_providers_batch( :param progress_log_interval: Interval for progress logging. :return: Tuple of (matching_count for use as expected_sent, list of {'pk', 'sk'} for cleanup). """ - target_date = datetime.now(UTC).date() + timedelta(days=target_days_until_expiration) - other_date_matching = target_date + timedelta(days=30) - other_date_non_matching = target_date + timedelta(days=60) + target_expiration_date = datetime.now(UTC).date() + timedelta(days=target_days_until_expiration) + other_date_matching = target_expiration_date + timedelta(days=30) + other_date_non_matching = target_expiration_date + timedelta(days=60) total = matching_count + non_matching_count logger.info( - f'Creating {total} providers (two privileges each): {matching_count} matching target date, ' - f'{non_matching_count} non-matching (target: {target_date.isoformat()})', + f'Creating {total} providers (two privileges each): {matching_count} matching target expiration date, ' + f'{non_matching_count} non-matching (target expiration: {target_expiration_date.isoformat()})', ) registered_email = config.test_provider_user_username @@ -212,7 +214,7 @@ def create_providers_batch( for i in range(total): provider_id = str(uuid.uuid4()) if i < matching_count: - exp_1, exp_2 = target_date, other_date_matching + exp_1, exp_2 = target_expiration_date, other_date_matching else: exp_1, exp_2 = other_date_non_matching, other_date_non_matching + timedelta(days=30) @@ -242,7 +244,7 @@ def create_providers_batch( if created % progress_log_interval == 0: logger.info(f'Created {created}/{total} providers') - logger.info(f'Completed creating {total} providers (target date: {target_date.isoformat()})') + logger.info(f'Completed creating {total} providers (target expiration: {target_expiration_date.isoformat()})') return matching_count, created_keys From f514e92a99d068d54c16af390138b22125303bcd Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 11 Feb 2026 10:03:14 -0600 Subject: [PATCH 45/52] comment rewording for clarity --- .../python/common/cc_common/data_model/schema/license/api.py | 2 +- .../common/cc_common/data_model/schema/privilege/api.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/license/api.py b/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/license/api.py index 9d48dac56..47aa8fd95 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/license/api.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/license/api.py @@ -34,7 +34,7 @@ class LicenseExpirationStatusMixin: calculated at write time. If the dateOfExpiration has passed since the last update, the licenseStatus should be 'inactive' even if the stored value in OpenSearch says 'active'. To account for this, this mixin performs the same expiration check that is performed when loading - them from DynamoDB, so the status will factor in the date of expiration and show the correct + license records from DynamoDB, so the status will factor in the date of expiration and show the correct active/inactive status. This mixin should be applied to license API response schemas that load data from diff --git a/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/privilege/api.py b/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/privilege/api.py index 529ff4235..4259ff951 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/privilege/api.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/privilege/api.py @@ -29,8 +29,8 @@ class PrivilegeExpirationStatusMixin: OpenSearch documents may have stale status values because the status field is calculated at write time. If the dateOfExpiration has passed since the last update, the status should be 'inactive' even if the stored value in OpenSearch says 'active'. To account for this, - this mixin performs the same expiration check that is performed when loading them from DynamoDB, - so the status will factor in the date of expiration and show the correct active/inactive status. + this mixin performs the same expiration check that is performed when loading privilege records from + DynamoDB, so the status will factor in the date of expiration and show the correct active/inactive status. This mixin should be applied to privilege API response schemas that load data from OpenSearch or other sources where the status may be stale. From ac94c3d2a53830c487e8dfc2947968d2422bb527 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 11 Feb 2026 11:50:15 -0600 Subject: [PATCH 46/52] PR feedback - Update cognito-backup module to use common-layer deps - update test comment --- .../python/cognito-backup/requirements.in | 4 +--- .../python/cognito-backup/requirements.txt | 24 ------------------- .../app/test_expiration_reminder_stack.py | 2 +- 3 files changed, 2 insertions(+), 28 deletions(-) diff --git a/backend/compact-connect/lambdas/python/cognito-backup/requirements.in b/backend/compact-connect/lambdas/python/cognito-backup/requirements.in index fba480ffa..68b7c56e7 100644 --- a/backend/compact-connect/lambdas/python/cognito-backup/requirements.in +++ b/backend/compact-connect/lambdas/python/cognito-backup/requirements.in @@ -1,3 +1 @@ -boto3>=1.26.0 -botocore>=1.29.0 -aws-lambda-powertools>=2.0.0 +# common requirements are managed in the common requirements.in file diff --git a/backend/compact-connect/lambdas/python/cognito-backup/requirements.txt b/backend/compact-connect/lambdas/python/cognito-backup/requirements.txt index 7312dff97..ccaa9152c 100644 --- a/backend/compact-connect/lambdas/python/cognito-backup/requirements.txt +++ b/backend/compact-connect/lambdas/python/cognito-backup/requirements.txt @@ -4,27 +4,3 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/cognito-backup/requirements.in # -aws-lambda-powertools==3.24.0 - # via -r lambdas/python/cognito-backup/requirements.in -boto3==1.42.42 - # via -r lambdas/python/cognito-backup/requirements.in -botocore==1.42.42 - # via - # -r lambdas/python/cognito-backup/requirements.in - # boto3 - # s3transfer -jmespath==1.1.0 - # via - # aws-lambda-powertools - # boto3 - # botocore -python-dateutil==2.9.0.post0 - # via botocore -s3transfer==0.16.0 - # via boto3 -six==1.17.0 - # via python-dateutil -typing-extensions==4.15.0 - # via aws-lambda-powertools -urllib3==2.6.3 - # via botocore diff --git a/backend/compact-connect/tests/app/test_expiration_reminder_stack.py b/backend/compact-connect/tests/app/test_expiration_reminder_stack.py index 6dc0a4223..cc519e8ff 100644 --- a/backend/compact-connect/tests/app/test_expiration_reminder_stack.py +++ b/backend/compact-connect/tests/app/test_expiration_reminder_stack.py @@ -124,7 +124,7 @@ def test_duration_alarm_configured(self): duration_alarm_logical_id, resources=alarms ) - # Verify threshold is 10 minutes (600,000 milliseconds) + # Verify threshold is 14 minutes (840,000 milliseconds) self.assertEqual(duration_alarm['Threshold'], 840_000) self.assertEqual(duration_alarm['ComparisonOperator'], 'GreaterThanThreshold') self.assertEqual(duration_alarm['EvaluationPeriods'], 1) From fbc44bf2bdacf1ec3b5c7184303dc71667bfd9ea Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 11 Feb 2026 11:56:57 -0600 Subject: [PATCH 47/52] update smoke test error handling --- .../tests/smoke/expiration_reminder_load_tests.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py index 95878fc14..84ff0b2ec 100755 --- a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py +++ b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py @@ -349,12 +349,8 @@ def invoke_expiration_reminder_lambda(days_before: int, compact: str = 'aslp'): InvocationType='Event', # Asynchronous invocation Payload=json.dumps(event), ) - - if response.get('FunctionError'): - error_payload = json.loads(response['Payload'].read()) - raise SmokeTestFailureException( - f'expiration_reminder Lambda invocation failed: {response.get("FunctionError")}, error: {error_payload}' - ) + if response.get('StatusCode') != 202: + raise SmokeTestFailureException(f'Unexpected status code: {response.get("StatusCode")}') logger.info('Lambda invocation accepted, polling CloudWatch Logs for completion...', log_group=log_group_name) From 23ec36bad41f9a761b2754184806c93e1dc846e4 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 11 Feb 2026 12:18:57 -0600 Subject: [PATCH 48/52] PR feedback - update smoke test cloudwatch log pagination --- .../smoke/expiration_reminder_load_tests.py | 106 ++++++++++-------- 1 file changed, 59 insertions(+), 47 deletions(-) diff --git a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py index 84ff0b2ec..de427f7e4 100755 --- a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py +++ b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py @@ -365,54 +365,66 @@ def invoke_expiration_reminder_lambda(days_before: int, compact: str = 'aslp'): seen_event_ids = set() # Track which log events we've already processed while time.time() - last_activity_time <= inactivity_timeout_sec: - # Get recent log events + # Get recent log events, paginating through all pages each cycle try: - log_events_response = logs_client.filter_log_events( - logGroupName=log_group_name, - limit=100, - startTime=int((time.time() - log_lookback_sec) * 1000), - ) - - # Process new log events - for event in log_events_response.get('events', []): - event_id = event.get('eventId') - if event_id in seen_event_ids: - continue # Skip events we've already processed - - seen_event_ids.add(event_id) - last_activity_time = time.time() # Any new log = Lambda still running - message = event.get('message', '') - - # Log all new messages to relay progress to CLI - # Lambda Powertools logs JSON format - try: - log_json = json.loads(message.strip()) - log_level = log_json.get('level', 'INFO') - log_message = log_json.get('message', '') - - # Relay log messages to CLI (skip DEBUG level) - if log_level != 'DEBUG': - logger.info(f'Lambda log [{log_level}]: {log_message}') - - # Check for completion message (handler logs "Completed processing for compact" with metrics) - if 'Completed processing' in log_message and log_json.get('metrics'): - metrics = log_json.get('metrics', {}) - - if metrics: - target_date = log_json.get('targetDate', '') - days_before_logged = log_json.get('daysBefore', days_before) - - logger.info('Lambda completed successfully', metrics=metrics) - return { - 'targetDate': target_date, - 'daysBefore': days_before_logged, - 'metrics': metrics, - } - logger.warning('Completion message found but no metrics in log entry') - except json.JSONDecodeError: - logger.info(f'Lambda log: {message[:200]}') - except (ValueError, KeyError) as e: - logger.warning(f'Error processing log event: {str(e)}', message=message[:200]) + next_token = None + while True: + filter_kwargs = { + 'logGroupName': log_group_name, + 'limit': 100, + 'startTime': int((time.time() - log_lookback_sec) * 1000), + } + if next_token is not None: + filter_kwargs['nextToken'] = next_token + + log_events_response = logs_client.filter_log_events(**filter_kwargs) + + # Process new log events from this page + for log_event in log_events_response.get('events', []): + event_id = log_event.get('eventId') + if event_id in seen_event_ids: + continue # Skip events we've already processed + + seen_event_ids.add(event_id) + last_activity_time = time.time() # Any new log = Lambda still running + message = log_event.get('message', '') + + # Log all new messages to relay progress to CLI + # Lambda Powertools logs JSON format + try: + log_json = json.loads(message.strip()) + log_level = log_json.get('level', 'INFO') + log_message = log_json.get('message', '') + + # Relay log messages to CLI (skip DEBUG level) + if log_level != 'DEBUG': + logger.info(f'Lambda log [{log_level}]: {log_message}') + + # Check for completion message (handler logs "Completed processing for compact" with metrics) + if 'Completed processing' in log_message and log_json.get('metrics'): + metrics = log_json.get('metrics', {}) + + if metrics: + target_date = log_json.get('targetDate', '') + days_before_logged = log_json.get('daysBefore', days_before) + + logger.info('Lambda completed successfully', metrics=metrics) + return { + 'targetDate': target_date, + 'daysBefore': days_before_logged, + 'metrics': metrics, + } + logger.warning('Completion message found but no metrics in log entry') + except json.JSONDecodeError: + logger.info(f'Lambda log: {message[:200]}') + except (ValueError, KeyError) as e: + logger.warning(f'Error processing log event: {str(e)}', message=message[:200]) + + next_token = log_events_response.get('nextToken') + # Must check for empty events (https://github.com/boto/boto3/issues/4258) + # empty events + nextToken can cause infinite loop + if not log_events_response.get('events') or next_token is None: + break except ClientError as e: if e.response['Error']['Code'] == 'ResourceNotFoundException': From 8405d033367b236b5c9cd004184b34bd17bd413b Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 11 Feb 2026 12:24:54 -0600 Subject: [PATCH 49/52] PR feedback - avoid duplicate license types/jurisdictions in smoke test --- .../tests/smoke/expiration_reminder_load_tests.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py index de427f7e4..3d3b8c545 100755 --- a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py +++ b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py @@ -47,7 +47,6 @@ import boto3 from botocore.exceptions import ClientError from smoke_common import ( - COMPACTS, JURISDICTIONS, LICENSE_TYPES, SmokeTestFailureException, @@ -73,14 +72,14 @@ # Test configuration: counts of matching vs non-matching providers for the 30-day expiration run MATCHING_PROVIDERS = 10_000 # Providers with one privilege expiring on target date (receive email) NON_MATCHING_PROVIDERS = 5_000 # Providers with neither privilege on target date (no email) -COMPACT = COMPACTS[0] # Use first compact +COMPACT = 'aslp' # Use aslp compact as it has two license types to test two privileges JURISDICTION = JURISDICTIONS[0] # Use first jurisdiction -# Second jurisdiction and license type for two-privilege-per-provider (smoke mode) -JURISDICTION_2 = JURISDICTIONS[1] if len(JURISDICTIONS) > 1 else JURISDICTIONS[0] +# Second jurisdiction and license type for two-privilege-per-provider +JURISDICTION_2 = JURISDICTIONS[1] if COMPACT in LICENSE_TYPES and LICENSE_TYPES[COMPACT]: LICENSE_TYPE = LICENSE_TYPES[COMPACT][0]['name'] LICENSE_TYPE_2 = ( - LICENSE_TYPES[COMPACT][1]['name'] if len(LICENSE_TYPES[COMPACT]) > 1 else LICENSE_TYPES[COMPACT][0]['name'] + LICENSE_TYPES[COMPACT][1]['name'] ) else: raise SmokeTestFailureException(f'No license types found for compact {COMPACT}') From c34082b5717a906fb066f21fff56aed7b1dda458 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 11 Feb 2026 12:25:30 -0600 Subject: [PATCH 50/52] formatting --- .../tests/smoke/expiration_reminder_load_tests.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py index 3d3b8c545..506323e24 100755 --- a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py +++ b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py @@ -78,9 +78,7 @@ JURISDICTION_2 = JURISDICTIONS[1] if COMPACT in LICENSE_TYPES and LICENSE_TYPES[COMPACT]: LICENSE_TYPE = LICENSE_TYPES[COMPACT][0]['name'] - LICENSE_TYPE_2 = ( - LICENSE_TYPES[COMPACT][1]['name'] - ) + LICENSE_TYPE_2 = LICENSE_TYPES[COMPACT][1]['name'] else: raise SmokeTestFailureException(f'No license types found for compact {COMPACT}') From 1be319fd36aa6abb16aead1edf398a4ad5b9a40d Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 11 Feb 2026 13:24:41 -0600 Subject: [PATCH 51/52] update purchase requirement to address lint check --- .../python/purchases/requirements-dev.txt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/compact-connect/lambdas/python/purchases/requirements-dev.txt b/backend/compact-connect/lambdas/python/purchases/requirements-dev.txt index 6985a6575..1cb28f926 100644 --- a/backend/compact-connect/lambdas/python/purchases/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/purchases/requirements-dev.txt @@ -12,11 +12,11 @@ aws-lambda-powertools==3.24.0 # via -r requirements-dev.in boolean-py==5.0 # via license-expression -boto3==1.42.34 +boto3==1.42.46 # via # -r requirements-dev.in # moto -botocore==1.42.34 +botocore==1.42.46 # via # boto3 # moto @@ -37,11 +37,11 @@ charset-normalizer==3.4.4 # via requests click==8.3.1 # via pip-tools -coverage[toml]==7.13.2 +coverage[toml]==7.13.4 # via # -r requirements-dev.in # pytest-cov -cryptography==46.0.3 +cryptography==46.0.5 # via # -r requirements-dev.in # moto @@ -78,7 +78,7 @@ marshmallow==3.26.2 # via -r requirements-dev.in mdurl==0.1.2 # via markdown-it-py -moto[dynamodb,s3]==5.1.20 +moto[dynamodb,s3]==5.1.21 # via -r requirements-dev.in msgpack==1.1.2 # via cachecontrol @@ -98,7 +98,7 @@ pip-audit==2.10.0 # via -r requirements-dev.in pip-requirements-parser==32.0.1 # via pip-audit -pip-tools==7.5.2 +pip-tools==7.5.3 # via -r requirements-dev.in platformdirs==4.5.1 # via pip-audit @@ -146,9 +146,9 @@ requests==2.32.5 # responses responses==0.25.8 # via moto -rich==14.3.1 +rich==14.3.2 # via pip-audit -ruff==0.14.14 +ruff==0.15.0 # via -r requirements-dev.in s3transfer==0.16.0 # via boto3 From 76333c2b6f4d0ab686a55871d8dfc7738a5b7f60 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 11 Feb 2026 13:26:37 -0600 Subject: [PATCH 52/52] reduce comment line for lint check --- .../tests/smoke/expiration_reminder_load_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py index 506323e24..49bdc9d78 100755 --- a/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py +++ b/backend/compact-connect/tests/smoke/expiration_reminder_load_tests.py @@ -397,7 +397,7 @@ def invoke_expiration_reminder_lambda(days_before: int, compact: str = 'aslp'): if log_level != 'DEBUG': logger.info(f'Lambda log [{log_level}]: {log_message}') - # Check for completion message (handler logs "Completed processing for compact" with metrics) + # Check for completion message (handler logs "Completed processing" with metrics) if 'Completed processing' in log_message and log_json.get('metrics'): metrics = log_json.get('metrics', {})