From 13a873c2e99b7d5f9f92691324889497d216011d Mon Sep 17 00:00:00 2001 From: Marcel Smuts Date: Sat, 8 Nov 2025 13:27:52 +0200 Subject: [PATCH 01/16] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20updated?= =?UTF-8?q?=20template=20with=20latest=20architecture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../.cursor/mcps/collection-module.mcp | 586 ++++++++++++++++++ 1 file changed, 586 insertions(+) create mode 100644 stripe_collection_module/.cursor/mcps/collection-module.mcp diff --git a/stripe_collection_module/.cursor/mcps/collection-module.mcp b/stripe_collection_module/.cursor/mcps/collection-module.mcp new file mode 100644 index 0000000..4303b1a --- /dev/null +++ b/stripe_collection_module/.cursor/mcps/collection-module.mcp @@ -0,0 +1,586 @@ +# Collection Module Architecture Patterns + +This MCP provides architectural patterns and guidelines for the Stripe Collection Module. + +## Architecture Overview + +### Layer Structure +``` +Entry Points (main.ts, webhook-hooks) + ↓ +Lifecycle Hooks & Controllers (orchestration) + ↓ +Services (business logic) + ↓ +Clients & Adapters (infrastructure) + ↓ +External APIs (Stripe, Root) +``` + +### Key Principles +- **Separation of Concerns**: Each layer has single responsibility +- **Dependency Injection**: All dependencies via constructor +- **Type Safety**: TypeScript throughout +- **Testability**: Unit tests with mocked dependencies + +## Clients Pattern + +### Purpose +Thin wrappers around external API SDKs. + +### Structure +```typescript +import Stripe from 'stripe'; +import { getConfigService } from '../services/config-instance'; + +export default class StripeClient { + public stripeSDK: Stripe; + + constructor() { + const config = getConfigService(); + this.stripeSDK = new Stripe(config.get('stripeSecretKey')); + } +} +``` + +### Usage +```typescript +import StripeClient from '../clients/stripe-client'; + +const stripeClient = new StripeClient(); + +// Direct SDK access +const customer = await stripeClient.stripeSDK.customers.create({ + email: 'customer@example.com', +}); +``` + +### Guidelines +- Keep clients thin - expose the SDK +- Use ConfigurationService for credentials +- Don't wrap every SDK method +- Error handling done in services + +## Services Pattern + +### Purpose +Business logic layer that orchestrates operations. + +### Structure +```typescript +import { LogService } from './log.service'; +import StripeClient from '../clients/stripe-client'; + +export class MyService { + constructor( + private readonly logService: LogService, + private readonly stripeClient: StripeClient + ) {} + + async performOperation(params: OperationParams): Promise { + this.logService.info('Starting operation', 'MyService', params); + + try { + // 1. Fetch data + const data = await this.stripeClient.stripeSDK.someMethod(); + + // 2. Transform data + const transformed = this.transformData(data); + + // 3. Update systems + await this.updateSomething(transformed); + + this.logService.info('Operation completed', 'MyService'); + return result; + } catch (error: any) { + this.logService.error('Operation failed', 'MyService', { params }, error); + throw error; + } + } + + private transformData(data: any) { + // Business logic here + return transformed; + } +} +``` + +### Available Services +- **LogService**: Structured JSON logging +- **ConfigurationService**: Type-safe configuration +- **RenderService**: HTML generation for dashboard +- **RootService**: Root Platform operations + +### Guidelines +- Inject all dependencies via constructor +- Use LogService for all logging +- Handle errors with context +- Keep focused (single responsibility) +- Write unit tests with mocked dependencies + +## Adapters Pattern + +### Purpose +Data transformation between Stripe and Root formats. + +### Structure +```typescript +import Stripe from 'stripe'; +import * as root from '@rootplatform/node-sdk'; + +export default class StripeToRootAdapter { + convertInvoiceToRootPayment( + invoice: Stripe.Invoice, + params: ConvertInvoiceParams + ) { + return { + status: params.status, + failure_reason: params.failureReason || invoice.last_finalization_error?.message, + failure_action: params.failureAction || root.FailureAction.BlockRetry, + }; + } + + convertCustomerToAppData(customer: Stripe.Customer) { + return { + stripe_customer_id: customer.id, + stripe_email: customer.email, + stripe_default_payment_method: customer.invoice_settings.default_payment_method, + stripe_created_at: new Date(customer.created * 1000).toISOString(), + }; + } +} +``` + +### Usage +```typescript +const adapter = new StripeToRootAdapter(); + +// Convert webhook data +const paymentUpdate = adapter.convertInvoiceToRootPayment(invoice, { + status: PaymentStatus.Successful, +}); + +// Store customer info +const appData = adapter.convertCustomerToAppData(customer); +await rootService.updatePolicyAppData(policyId, appData); +``` + +### Guidelines +- Keep adapters pure (no side effects) +- Handle null/undefined values gracefully +- Add comprehensive tests +- Don't make API calls in adapters +- Don't add business logic + +## Controllers Pattern + +### Purpose +Event processors that orchestrate service calls. + +### Structure +```typescript +import { LogService } from '../../services/log.service'; +import { RootService } from '../../services/root.service'; +import StripeClient from '../../clients/stripe-client'; + +export class InvoicePaidController { + constructor( + private readonly logService: LogService, + private readonly rootService: RootService, + private readonly stripeClient: StripeClient + ) {} + + async handle(invoice: Stripe.Invoice): Promise { + this.logService.info('Processing invoice.paid', 'InvoicePaidController', { + invoiceId: invoice.id, + }); + + // 1. Validate input + if (!invoice.metadata?.rootPaymentId) { + this.logService.warn('No payment ID in invoice', 'InvoicePaidController'); + return; + } + + // 2. Coordinate services + await this.rootService.updatePaymentStatus({ + paymentId: invoice.metadata.rootPaymentId, + status: PaymentStatus.Successful, + }); + + this.logService.info('Invoice processed successfully', 'InvoicePaidController'); + } +} +``` + +### Guidelines +- Keep controllers thin (< 100 lines) +- Inject all dependencies via constructor +- Use services for business logic +- Log important steps +- Handle errors gracefully +- Return early for validation failures + +## Lifecycle Hooks Pattern + +### Purpose +Root Platform callback functions. + +### Structure +```typescript +import { getLogService } from '../services/log-instance'; +import { RootService } from '../services/root.service'; +import StripeClient from '../clients/stripe-client'; + +export async function afterPolicyPaymentMethodAssigned({ policy }) { + const logService = getLogService(); + const rootService = new RootService(logService); + const stripeClient = new StripeClient(); + + logService.info('Payment method assigned', 'afterPolicyPaymentMethodAssigned', { + policyId: policy.policy_id, + }); + + try { + // 1. Get payment method + const paymentMethod = await rootService.getPolicyPaymentMethod(policy.policy_id); + + // 2. Process with Stripe + const customerId = policy.app_data?.stripe_customer_id; + await stripeClient.stripeSDK.paymentMethods.attach( + paymentMethod.module.payment_method, + { customer: customerId } + ); + + logService.info('Payment method attached', 'afterPolicyPaymentMethodAssigned'); + } catch (error: any) { + logService.error('Failed to attach payment method', 'afterPolicyPaymentMethodAssigned', {}, error); + throw error; + } +} +``` + +### Available Hooks +- **Payment Method**: renderCreatePaymentMethod, createPaymentMethod, renderViewPaymentMethod +- **Policy**: afterPolicyIssued, afterPolicyPaymentMethodAssigned, afterPolicyUpdated, afterPolicyCancelled +- **Payment**: afterPaymentCreated, afterPaymentUpdated +- **Alteration**: afterAlterationPackageApplied + +### Guidelines +- Log hook entry and exit +- Validate input parameters +- Handle errors gracefully +- Use services for business logic +- Keep hooks thin (orchestration only) + +## Dependency Injection Pattern + +### Container Registration +```typescript +// core/container.setup.ts +container.register( + ServiceToken.MY_SERVICE, + (c) => { + const logService = c.resolve(ServiceToken.LOG_SERVICE); + const config = c.resolve(ServiceToken.CONFIG_SERVICE); + + const { MyService } = require('../services/my.service'); + return new MyService(logService, config); + }, + ServiceLifetime.SINGLETON +); +``` + +### Service Usage +```typescript +import { getContainer } from './core/container.setup'; +import { ServiceToken } from './core/container'; + +const container = getContainer(); +const myService = container.resolve(ServiceToken.MY_SERVICE); +await myService.doSomething(); +``` + +### Service Lifetimes +- **SINGLETON**: LogService, ConfigService (shared state/expensive to create) +- **TRANSIENT**: Controllers (per-request instances) + +### Guidelines +- Use ServiceTokens (Symbols) for type safety +- Register services at startup +- Inject dependencies via constructor +- Test with mocked services + +## Logging Pattern + +### Usage +```typescript +import { getLogService } from '../services/log-instance'; + +const logService = getLogService(); + +// Basic logging +logService.info('Operation completed', 'MyService'); +logService.error('Operation failed', 'MyService', {}, error); + +// With metadata +logService.info('Payment created', 'PaymentService', { + paymentId: 'payment_123', + amount: 10000, +}); + +// With correlation ID +const correlationId = logService.generateCorrelationId(); +logService.info('Processing request', 'WebhookHandler'); +// ... all subsequent logs include this correlation ID +logService.clearCorrelationId(); +``` + +### Guidelines +- Always provide context (service/controller name) +- Include relevant metadata +- Use appropriate log levels (DEBUG, INFO, WARN, ERROR) +- Don't log sensitive data + +## Testing Patterns + +### Service Testing +```typescript +describe('MyService', () => { + let service: MyService; + let mockLogService: jest.Mocked; + let mockStripeClient: jest.Mocked; + + beforeEach(() => { + mockLogService = { + info: jest.fn(), + error: jest.fn(), + } as any; + + mockStripeClient = { + stripeSDK: { + customers: { + create: jest.fn().mockResolvedValue({ id: 'cus_123' }), + }, + }, + } as any; + + service = new MyService(mockLogService, mockStripeClient); + }); + + it('should perform operation successfully', async () => { + const result = await service.performOperation({ id: '123' }); + + expect(result).toBeDefined(); + expect(mockLogService.info).toHaveBeenCalledWith( + 'Starting operation', + 'MyService', + { id: '123' } + ); + }); +}); +``` + +### Controller Testing +```typescript +describe('InvoicePaidController', () => { + let controller: InvoicePaidController; + let mockRootService: jest.Mocked; + + beforeEach(() => { + mockRootService = { + updatePaymentStatus: jest.fn(), + } as any; + + controller = new InvoicePaidController( + mockLogService, + mockRootService, + mockStripeClient + ); + }); + + it('should update payment status', async () => { + const invoice = { id: 'in_123', metadata: { rootPaymentId: 'pay_123' } }; + + await controller.handle(invoice); + + expect(mockRootService.updatePaymentStatus).toHaveBeenCalledWith({ + paymentId: 'pay_123', + status: PaymentStatus.Successful, + }); + }); +}); +``` + +## Error Handling Pattern + +### In Services +```typescript +try { + const result = await externalApiCall(); + return result; +} catch (error: any) { + this.logService.error( + 'API call failed', + 'MyService', + { params }, + error + ); + throw error; // Re-throw after logging +} +``` + +### In Controllers +```typescript +async handle(event: Event): Promise { + try { + await this.processEvent(event); + } catch (error: any) { + this.logService.error('Event processing failed', 'MyController', { event }, error); + throw error; + } +} +``` + +### In Lifecycle Hooks +```typescript +export async function afterPolicyIssued({ policy }) { + const logService = getLogService(); + + try { + await processPolicyIssued(policy); + } catch (error: any) { + logService.error('Failed to process policy issued', 'afterPolicyIssued', {}, error); + throw error; // Root Platform will handle retry + } +} +``` + +## Common Patterns + +### Orchestration Pattern (Service) +```typescript +async createCustomerAndSubscription(policy: Policy) { + // 1. Create customer + const customer = await this.stripeClient.stripeSDK.customers.create({ + email: policy.email, + }); + + // 2. Attach payment method + await this.stripeClient.stripeSDK.paymentMethods.attach( + paymentMethodId, + { customer: customer.id } + ); + + // 3. Create subscription + const subscription = await this.stripeClient.stripeSDK.subscriptions.create({ + customer: customer.id, + items: [{ price: priceId }], + }); + + // 4. Update Root + await this.rootService.updatePolicyAppData(policy.policy_id, { + stripe_customer_id: customer.id, + stripe_subscription_id: subscription.id, + }); + + return { customer, subscription }; +} +``` + +### Transformation Pattern (Adapter) +```typescript +mapStripeStatusToRoot(stripeStatus: string): PaymentStatus { + const mapping = { + paid: PaymentStatus.Successful, + open: PaymentStatus.Pending, + void: PaymentStatus.Cancelled, + uncollectible: PaymentStatus.Failed, + }; + return mapping[stripeStatus] || PaymentStatus.Failed; +} +``` + +### Idempotency Pattern (Hook) +```typescript +export async function afterPaymentCreated({ policy, payment }) { + // Check if already processed + const existingPaymentIntent = payment.metadata?.stripe_payment_intent_id; + if (existingPaymentIntent) { + logService.info('Payment already processed', 'afterPaymentCreated'); + return; + } + + // Process payment + const paymentIntent = await createPaymentIntent(payment); + + // Store reference to prevent re-processing + await updatePaymentMetadata(payment.payment_id, { + stripe_payment_intent_id: paymentIntent.id, + }); +} +``` + +## Best Practices Summary + +### Do ✅ +- Use dependency injection +- Log with context and metadata +- Handle errors gracefully +- Write tests for all business logic +- Keep layers separated +- Use TypeScript types +- Return early for validation failures + +### Don't ❌ +- Put business logic in controllers or hooks +- Create dependencies with `new` (use DI) +- Skip error handling +- Make clients stateful +- Skip input validation +- Log sensitive data +- Mix layer responsibilities + +## Quick Reference + +### Creating New Features +1. Create Service (if needed) - `code/services/` +2. Create Controller (if handling events) - `code/controllers/` +3. Register in DI Container - `code/core/container.setup.ts` +4. Wire in Entry Point - `code/webhook-hooks.ts` or `code/lifecycle-hooks/` +5. Add Tests - `__tests__/` + +### File Locations +- **Services**: `code/services/` - Business logic +- **Controllers**: `code/controllers/` - Event processors +- **Clients**: `code/clients/` - API wrappers +- **Adapters**: `code/adapters/` - Data transformation +- **Lifecycle Hooks**: `code/lifecycle-hooks/` - Root callbacks +- **Core/DI**: `code/core/` - Dependency injection +- **Utils**: `code/utils/` - Reusable helpers + +### Common Imports +```typescript +// Logging +import { getLogService } from '../services/log-instance'; + +// Configuration +import { getConfigService } from '../services/config-instance'; + +// Clients +import StripeClient from '../clients/stripe-client'; +import rootClient from '../clients/root-client'; + +// Services +import { RootService } from '../services/root.service'; +import { StripeService } from '../services/stripe.service'; +import { RenderService } from '../services/render.service'; + +// DI Container +import { getContainer } from '../core/container.setup'; +import { ServiceToken } from '../core/container'; + +// Types +import * as root from '@rootplatform/node-sdk'; +import Stripe from 'stripe'; +``` + From 954794a6d3f854f431c4a9554191f02a86cf4bb6 Mon Sep 17 00:00:00 2001 From: Marcel Smuts Date: Sat, 8 Nov 2025 13:28:13 +0200 Subject: [PATCH 02/16] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20updated?= =?UTF-8?q?=20template=20with=20latest=20architecture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursorrules | 14 + README.md | 497 +- SETUP.md | 358 + setup.sh | 167 + stripe_collection_module/.eslintrc.js | 7 + stripe_collection_module/.nvmrc | 5 + stripe_collection_module/.prettierrc | 11 + stripe_collection_module/.root-config.json | 10 +- .../.root-config.json.sample | 12 + stripe_collection_module/.vscode/launch.json | 96 + .../.vscode/settings.json | 87 + stripe_collection_module/.vscode/tasks.json | 57 + .../__tests__/core/container.test.ts | 318 + .../__tests__/helpers/factories.ts | 175 + .../__tests__/helpers/test-utils.ts | 63 + .../__tests__/services/config.service.test.ts | 294 + .../__tests__/services/log.service.test.ts | 169 + .../__tests__/services/render.service.test.ts | 255 + .../__tests__/services/root.service.test.ts | 391 + .../__tests__/services/stripe.service.test.ts | 411 + .../__tests__/setup.test.ts | 30 + stripe_collection_module/__tests__/setup.ts | 19 + .../code/adapters/README.md | 356 + .../code/adapters/stripe-to-root-adapter.ts | 135 +- .../code/clients/README.md | 263 + .../code/clients/root-client.ts | 8 +- .../code/clients/stripe-client-types.ts | 34 - .../code/clients/stripe-client.ts | 118 +- stripe_collection_module/code/config.ts | 88 - .../code/controllers/README.md | 201 + .../payment-creation.controller.ts | 169 + .../processCreatePaymentEventController.ts | 106 - ...processPolicyAlterationEventsController.ts | 815 -- ...ocessPolicyCancellationEventsController.ts | 206 - ...icyPaymentMethodAssignedEventController.ts | 287 - .../processPolicyUpdatedEventController.ts | 294 - .../invoice-paid.controller.ts | 145 + .../processChargeDisputedEventController.ts | 86 - ...essInvoiceChargeRefundedEventController.ts | 100 - .../processInvoiceCreatedEventController.ts | 157 - ...voiceMarkedUncollectableEventController.ts | 68 - .../processInvoicePaidEventController.ts | 81 - ...cessInvoicePaymentFailedEventController.ts | 72 - .../processInvoiceVoidedEventController.ts | 86 - ...ocessPaymentIntentFailedEventController.ts | 54 - ...ssPaymentIntentSucceededEventController.ts | 51 - .../processStripeEventController.ts | 38 - ...scriptionScheduleUpdatedEventController.ts | 174 - stripe_collection_module/code/core/README.md | 516 + .../code/core/container.setup.ts | 164 + .../code/core/container.ts | 155 + stripe_collection_module/code/env.sample.ts | 64 +- .../code/interfaces/index.ts | 77 - .../code/interfaces/root-payment.ts | 50 - .../code/interfaces/root.ts | 60 - .../code/interfaces/stripe-events.ts | 19 + .../code/lifecycle-hooks.ts | 528 - .../code/lifecycle-hooks/README.md | 515 + .../code/lifecycle-hooks/index.ts | 340 + stripe_collection_module/code/main.ts | 16 +- stripe_collection_module/code/sample.env.ts | 24 - .../code/services/README.md | 516 + .../code/services/config-instance.ts | 43 + .../code/services/config.service.ts | 213 + .../code/services/log-instance.ts | 43 + .../code/services/log.service.ts | 212 + .../code/services/render.service.ts | 353 + .../code/services/root.service.ts | 79 + .../code/services/stripe.service.ts | 278 + stripe_collection_module/code/utils/README.md | 179 + .../code/utils/error-types.ts | 205 + stripe_collection_module/code/utils/error.ts | 5 +- stripe_collection_module/code/utils/index.ts | 143 +- stripe_collection_module/code/utils/logger.ts | 9 +- stripe_collection_module/code/utils/retry.ts | 176 + .../code/utils/stripe-utils.ts | 198 - .../code/utils/timeout.ts | 119 + .../code/webhook-hooks.ts | 364 +- stripe_collection_module/docs/ARCHITECTURE.md | 227 + .../docs/BEST_PRACTICES.md | 637 + .../docs/CODE_STRUCTURE.md | 395 + stripe_collection_module/docs/CUSTOMIZING.md | 635 + stripe_collection_module/docs/DEPLOYMENT.md | 565 + stripe_collection_module/docs/LOG_VIEWING.md | 321 + .../docs/ROOT_CONFIGURATION.md | 327 + stripe_collection_module/docs/SETUP.md | 465 + stripe_collection_module/docs/TESTING.md | 474 + stripe_collection_module/docs/WEBHOOKS.md | 678 + stripe_collection_module/jest.config.js | 37 + stripe_collection_module/package-lock.json | 10247 ++++++++++++---- stripe_collection_module/package.json | 48 +- stripe_collection_module/scripts/README.md | 310 + stripe_collection_module/scripts/deploy.sh | 475 + .../scripts/validate-config.sh | 157 + stripe_collection_module/tsconfig.build.json | 2 + stripe_collection_module/tsconfig.eslint.json | 5 +- stripe_collection_module/tsconfig.json | 11 +- 97 files changed, 22766 insertions(+), 6551 deletions(-) create mode 100644 .cursorrules create mode 100644 SETUP.md create mode 100755 setup.sh create mode 100644 stripe_collection_module/.nvmrc create mode 100644 stripe_collection_module/.prettierrc create mode 100644 stripe_collection_module/.root-config.json.sample create mode 100644 stripe_collection_module/.vscode/launch.json create mode 100644 stripe_collection_module/.vscode/settings.json create mode 100644 stripe_collection_module/.vscode/tasks.json create mode 100644 stripe_collection_module/__tests__/core/container.test.ts create mode 100644 stripe_collection_module/__tests__/helpers/factories.ts create mode 100644 stripe_collection_module/__tests__/helpers/test-utils.ts create mode 100644 stripe_collection_module/__tests__/services/config.service.test.ts create mode 100644 stripe_collection_module/__tests__/services/log.service.test.ts create mode 100644 stripe_collection_module/__tests__/services/render.service.test.ts create mode 100644 stripe_collection_module/__tests__/services/root.service.test.ts create mode 100644 stripe_collection_module/__tests__/services/stripe.service.test.ts create mode 100644 stripe_collection_module/__tests__/setup.test.ts create mode 100644 stripe_collection_module/__tests__/setup.ts create mode 100644 stripe_collection_module/code/adapters/README.md create mode 100644 stripe_collection_module/code/clients/README.md delete mode 100644 stripe_collection_module/code/clients/stripe-client-types.ts delete mode 100644 stripe_collection_module/code/config.ts create mode 100644 stripe_collection_module/code/controllers/README.md create mode 100644 stripe_collection_module/code/controllers/root-event-processors/payment-creation.controller.ts delete mode 100644 stripe_collection_module/code/controllers/root-event-processors/processCreatePaymentEventController.ts delete mode 100644 stripe_collection_module/code/controllers/root-event-processors/processPolicyAlterationEventsController.ts delete mode 100644 stripe_collection_module/code/controllers/root-event-processors/processPolicyCancellationEventsController.ts delete mode 100644 stripe_collection_module/code/controllers/root-event-processors/processPolicyPaymentMethodAssignedEventController.ts delete mode 100644 stripe_collection_module/code/controllers/root-event-processors/processPolicyUpdatedEventController.ts create mode 100644 stripe_collection_module/code/controllers/stripe-event-processors/invoice-paid.controller.ts delete mode 100644 stripe_collection_module/code/controllers/stripe-event-processors/processChargeDisputedEventController.ts delete mode 100644 stripe_collection_module/code/controllers/stripe-event-processors/processInvoiceChargeRefundedEventController.ts delete mode 100644 stripe_collection_module/code/controllers/stripe-event-processors/processInvoiceCreatedEventController.ts delete mode 100644 stripe_collection_module/code/controllers/stripe-event-processors/processInvoiceMarkedUncollectableEventController.ts delete mode 100644 stripe_collection_module/code/controllers/stripe-event-processors/processInvoicePaidEventController.ts delete mode 100644 stripe_collection_module/code/controllers/stripe-event-processors/processInvoicePaymentFailedEventController.ts delete mode 100644 stripe_collection_module/code/controllers/stripe-event-processors/processInvoiceVoidedEventController.ts delete mode 100644 stripe_collection_module/code/controllers/stripe-event-processors/processPaymentIntentFailedEventController.ts delete mode 100644 stripe_collection_module/code/controllers/stripe-event-processors/processPaymentIntentSucceededEventController.ts delete mode 100644 stripe_collection_module/code/controllers/stripe-event-processors/processStripeEventController.ts delete mode 100644 stripe_collection_module/code/controllers/stripe-event-processors/processSubscriptionScheduleUpdatedEventController.ts create mode 100644 stripe_collection_module/code/core/README.md create mode 100644 stripe_collection_module/code/core/container.setup.ts create mode 100644 stripe_collection_module/code/core/container.ts delete mode 100644 stripe_collection_module/code/interfaces/index.ts delete mode 100644 stripe_collection_module/code/interfaces/root-payment.ts delete mode 100644 stripe_collection_module/code/interfaces/root.ts delete mode 100644 stripe_collection_module/code/lifecycle-hooks.ts create mode 100644 stripe_collection_module/code/lifecycle-hooks/README.md create mode 100644 stripe_collection_module/code/lifecycle-hooks/index.ts delete mode 100644 stripe_collection_module/code/sample.env.ts create mode 100644 stripe_collection_module/code/services/README.md create mode 100644 stripe_collection_module/code/services/config-instance.ts create mode 100644 stripe_collection_module/code/services/config.service.ts create mode 100644 stripe_collection_module/code/services/log-instance.ts create mode 100644 stripe_collection_module/code/services/log.service.ts create mode 100644 stripe_collection_module/code/services/render.service.ts create mode 100644 stripe_collection_module/code/services/root.service.ts create mode 100644 stripe_collection_module/code/services/stripe.service.ts create mode 100644 stripe_collection_module/code/utils/README.md create mode 100644 stripe_collection_module/code/utils/error-types.ts create mode 100644 stripe_collection_module/code/utils/retry.ts delete mode 100644 stripe_collection_module/code/utils/stripe-utils.ts create mode 100644 stripe_collection_module/code/utils/timeout.ts create mode 100644 stripe_collection_module/docs/ARCHITECTURE.md create mode 100644 stripe_collection_module/docs/BEST_PRACTICES.md create mode 100644 stripe_collection_module/docs/CODE_STRUCTURE.md create mode 100644 stripe_collection_module/docs/CUSTOMIZING.md create mode 100644 stripe_collection_module/docs/DEPLOYMENT.md create mode 100644 stripe_collection_module/docs/LOG_VIEWING.md create mode 100644 stripe_collection_module/docs/ROOT_CONFIGURATION.md create mode 100644 stripe_collection_module/docs/SETUP.md create mode 100644 stripe_collection_module/docs/TESTING.md create mode 100644 stripe_collection_module/docs/WEBHOOKS.md create mode 100644 stripe_collection_module/jest.config.js create mode 100644 stripe_collection_module/scripts/README.md create mode 100755 stripe_collection_module/scripts/deploy.sh create mode 100755 stripe_collection_module/scripts/validate-config.sh diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..507c062 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,14 @@ +# Collection Module Template Project Rules + +## Architecture Context + +For every conversation about code architecture, building features, or refactoring, load the Collection Module architecture patterns: + +stripe_collection_module/.cursor/mcps/collection-module.mcp + +This file contains the architectural patterns for: +- Services, Controllers, Clients, and Adapters +- Dependency Injection patterns +- Logging patterns +- Testing patterns +- Error handling patterns diff --git a/README.md b/README.md index ac9340b..171025f 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,466 @@ -# Stripe Collection Module -This repository contains the Collection Module example template for integrating -Stripe with the Root platform. +# Stripe Collection Module Template -For a template to be available to be used by developers, it has to be assigned -to the Template Organization in that stack. Every stack has a Template -Organization, and those are the details you need to update in the targets.yaml -file. +A production-ready template for building collection modules that integrate Stripe with the Root Platform. Includes comprehensive testing, structured logging, and clear architecture patterns. -## Getting started with a new template +--- -If the template is brand new and doesn't exist on the target Template -Organisation, start by creating the Collection Module on the Template -Organisation. Remember to update the key in the payload below. +## 🚀 Quick Start -Endpoint +### Automated Setup (Recommended) ```bash -> POST /v1/insurance/collection-modules HTTP/1.1 +./setup.sh ``` -Payload +This will automatically: +- ✓ Install all dependencies +- ✓ Configure Root Platform settings (`.root-config.json` and `.root-auth`) +- ✓ Create your environment file from template +- ✓ Run validation checks +- ✓ Show you next steps -```json -{ - "key": "payment_provider_cm_template", - "name": "Payment Provider Collection Module Template", - "key_of_collection_module_to_clone": "blank_starter_template" -} +### Manual Setup + +```bash +cd stripe_collection_module +npm install +cp code/env.sample.ts code/env.ts +# Edit code/env.ts with your Stripe API keys +npm run validate +npm test ``` -For updating existing collection modules, you can go ahead and deploy any -changes using the instructions below. +### Start Implementing + +See [docs/CUSTOMIZING.md](./stripe_collection_module/docs/CUSTOMIZING.md) to implement Stripe integration. + +--- + +## 📋 What's Included + +This template provides: + +- ✅ **Root Platform Ready** - Optimized for Root Platform deployment +- ✅ **Comprehensive Testing** - Jest with 60+ tests, factories, and utilities +- ✅ **Structured Logging** - JSON logging integrated with Root Platform +- ✅ **Dependency Injection** - Clean, testable service architecture +- ✅ **Type-Safe Configuration** - Injectable, validated configuration service +- ✅ **Stripe Integration** - Ready-to-implement Stripe service stubs +- ✅ **Production Best Practices** - Error handling, retries, monitoring +- ✅ **Complete Documentation** - Setup, deployment, customization guides + +--- + +## 📚 Documentation + +### Getting Started + +- **[Getting Started](./SETUP.md)** - Complete walkthrough for new users +- **[Setup Guide](./stripe_collection_module/docs/SETUP.md)** - Detailed setup reference +- **[Root Configuration](./stripe_collection_module/docs/ROOT_CONFIGURATION.md)** - Root Platform config files +- **[Implementation Guide](./stripe_collection_module/docs/CUSTOMIZING.md)** - Stripe implementation guide + +### Development + +- **[Architecture](./stripe_collection_module/docs/ARCHITECTURE.md)** - System design and patterns +- **[Testing Guide](./stripe_collection_module/docs/TESTING.md)** - Writing and running tests +- **[Best Practices](./stripe_collection_module/docs/BEST_PRACTICES.md)** - Production patterns + +### Deployment + +- **[Deployment Guide](./stripe_collection_module/docs/DEPLOYMENT.md)** - Root Platform deployment +- **[Webhooks Setup](./stripe_collection_module/docs/WEBHOOKS.md)** - Webhook configuration +- **[Log Viewing](./stripe_collection_module/docs/LOG_VIEWING.md)** - Root Platform log access + +--- + +## 🏗️ Architecture + +``` +Stripe → Root Platform → Stripe Collection Module → Root API + ↓ + Root Platform Logs +``` + +### Key Components + +**Core Services:** +- `ConfigurationService` - Type-safe, validated configuration +- `LogService` - Dual-output structured logging +- `RenderService` - HTML generation for dashboards + +**Stripe Integration:** +- Stripe SDK client wrapper +- Stripe service layer for business logic +- Stripe webhook event processors +- Stripe to Root data adapters + +**Root Integration:** +- Root API client wrapper +- Policy and payment management +- Lifecycle hook handlers + +### Project Structure + +``` +stripe_collection_module/ +├── code/ +│ ├── core/ # DI container & domain models +│ ├── services/ # Business logic +│ ├── clients/ # API wrappers +│ ├── controllers/ # Event processors +│ ├── lifecycle-hooks/ # Root platform hooks +│ ├── utils/ # Utilities +│ └── env.sample.ts # Configuration template +├── __tests__/ # Comprehensive test suite +├── docs/ # Detailed documentation +├── infrastructure/ # AWS templates (SAM/CloudFormation) +└── scripts/ # Build and deployment scripts +``` + +--- + +## 🔧 Configuration + +### Required Environment Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `ENVIRONMENT` | Environment name | `production` or `development` | +| `STRIPE_SECRET_KEY_LIVE` | Stripe API secret key | `sk_live_...` | +| `STRIPE_PUBLISHABLE_KEY_LIVE` | Stripe publishable key | `pk_live_...` | +| `STRIPE_WEBHOOK_SIGNING_SECRET_LIVE` | Stripe webhook secret | `whsec_...` | +| `ROOT_API_KEY_LIVE` | Root API key | `production_...` | +| `ROOT_BASE_URL_LIVE` | Root API URL | `https://api.rootplatform.com/v1/insurance` | +| `ROOT_COLLECTION_MODULE_KEY` | Module identifier | `cm_stripe` | + +See `stripe_collection_module/code/env.sample.ts` for complete configuration template. + +### Configuration Files + +- `stripe_collection_module/code/env.ts` - Environment variables (gitignored) +- `stripe_collection_module/.root-config.json` - Root module config +- `stripe_collection_module/package.json` - Dependencies and scripts + +--- + +## 🧪 Testing + +```bash +cd stripe_collection_module + +# Run all tests +npm test + +# Watch mode +npm run test:watch + +# With coverage +npm run test:coverage + +# Validate configuration +npm run validate +``` + +### Test Coverage + +- **Services**: 80%+ coverage +- **Core**: 90%+ coverage +- **Overall**: 70%+ coverage + +See [docs/TESTING.md](./stripe_collection_module/docs/TESTING.md) for testing guide. + +--- + +## 🚢 Deployment + +### Root Platform Deployment + +Deployment is handled through the Root Platform API: + +```bash +# Prepare for deployment +cd stripe_collection_module +npm run validate +npm test +npm run build + +# Tag your release +git tag -a v1.0.0 -m "Release version 1.0.0" +git push origin v1.0.0 + +# Publish to Root Platform (Sandbox) +curl -X POST \ + -H "Authorization: Basic {{api_key}}" \ + "{{host}}/v1/apps/{{org_id}}/insurance/collection-modules/{{cm_key}}/publish?bumpSandbox=true" + +# Publish to Production +curl -X POST \ + -H "Authorization: Basic {{api_key}}" \ + "{{host}}/v1/apps/{{org_id}}/insurance/collection-modules/{{cm_key}}/publish?bumpSandbox=false" +``` + +See [docs/DEPLOYMENT.md](./stripe_collection_module/docs/DEPLOYMENT.md) for detailed deployment guide. + +### Environment-Specific Deployment + +**Sandbox:** +- Use sandbox/test credentials +- Deploy with `bumpSandbox=true` +- Test with provider sandbox + +**Production:** +- Use production credentials +- Deploy with `bumpSandbox=false` +- Configure via Root Platform dashboard +- Monitor via Root Platform logs + +--- + +## 🔍 Monitoring + +### Root Platform Logs + +Structured JSON logs accessible via Root Platform: + +- View logs in Root Platform dashboard +- Filter by severity, time, or search terms +- Monitor real-time activity +- Track webhook processing +- Debug lifecycle hook execution + +### Metrics + +Track via Root Platform dashboard: +- Request volume +- Error rates +- Processing latency +- Webhook delivery status +- API call success rates + +### Monitoring + +Set up monitoring for: +- High error rates +- Slow response times +- Failed webhook deliveries +- API connectivity issues + +--- + +## 🔐 Security + +### Best Practices + +- ✅ Store secrets securely (never commit env.ts) +- ✅ Verify all webhook signatures +- ✅ Validate all inputs +- ✅ Never log sensitive data (API keys, PII, card numbers) +- ✅ Use environment-specific credentials +- ✅ Rotate API keys quarterly +- ✅ Use Stripe test mode for development +- ✅ Monitor security events in Root Platform dashboard + +### Webhook Security + +All webhooks must: +1. Verify signature from provider +2. Validate event structure +3. Log processing attempts +4. Handle duplicate events (idempotency) + +See [docs/WEBHOOKS.md](./stripe_collection_module/docs/WEBHOOKS.md) for implementation details. + +--- + +## 🎯 Implementing Stripe Integration + +This template provides a complete structure for Stripe integration with stub implementations. + +### Implementation Steps + +1. **Implement Services** - Complete the stub methods in `code/services/stripe.service.ts` +2. **Add Stripe Clients** - Configure Stripe SDK client in `code/clients/stripe-client.ts` +3. **Create Event Processors** - Implement Stripe webhook handlers in `code/controllers/stripe-event-processors/` +4. **Wire Lifecycle Hooks** - Connect Root Platform lifecycle hooks to Stripe operations +5. **Add Validation** - Implement input validation with Joi +6. **Write Tests** - Add comprehensive test coverage + +See [docs/CUSTOMIZING.md](./stripe_collection_module/docs/CUSTOMIZING.md) for detailed implementation guide. + +### Included Patterns + +The template includes: +- Retry logic with exponential backoff +- Error handling and classification +- Webhook signature verification +- Idempotency handling +- Structured logging +- Dependency injection + +--- + +## 📦 Features + +### Logging System + +**Structured logging:** +- JSON logs to Root Platform +- Multiple log levels (DEBUG, INFO, WARN, ERROR) +- Rich metadata support +- Request tracking +- Easy debugging via dashboard + +### Dependency Injection + +**Clean, testable architecture:** +- Custom DI container (no heavy frameworks) +- Service lifetime management (singleton/transient) +- Easy mocking for tests +- Clear dependency graphs + +### Configuration Management + +**Type-safe configuration:** +- Injectable `ConfigurationService` +- Environment-specific configs +- Validation on startup +- Helpful error messages + +### Testing Infrastructure + +**Comprehensive test support:** +- Jest with TypeScript +- Test utilities and helpers +- Mock factories for common objects +- High test coverage (70%+) + +--- + +## 🛠️ Available Scripts + +```bash +# Validation +npm run validate # Validate configuration + +# Development +npm run lint # Check code quality +npm run lint:fix # Auto-fix linting issues +npm run prettier # Format code +npm test # Run tests +npm run test:watch # Watch mode + +# Build +npm run build # Compile TypeScript +npm run package:lambda # Create deployment package + +# Cleanup +npm run clean # Remove build artifacts +``` + +--- + +## 🐛 Troubleshooting + +### Common Issues + +| Issue | Solution | +|-------|----------| +| "ENVIRONMENT is not set" | Set `NODE_ENV` in `code/env.ts` | +| "Module not found" errors | Run `npm install` | +| Tests failing | Run `nvm use` then `npm install` | +| TypeScript errors | Check `tsconfig.json` includes all files | +| Placeholder warnings | Update `code/env.ts` with real values | + +See documentation for detailed troubleshooting guides. + +--- + +## 📝 Maintenance + +### Regular Tasks + +- **Weekly**: Review logs and metrics +- **Monthly**: Update dependencies +- **Quarterly**: Rotate API keys +- **As Needed**: Update documentation + +### Monitoring + +- Check Root Platform dashboard daily +- Review error rates and patterns +- Monitor webhook processing +- Track API success rates + +--- + +## 🤝 Contributing to Template + +When improving this template: + +1. Keep it Stripe-focused +2. Update documentation +3. Add tests for new features +4. Follow existing patterns +5. Update this README + +--- + +## 📄 License + +Review the LICENSE file for usage terms. + +--- + +## 🔗 Resources + +### Stripe Documentation + +- **Stripe API**: https://stripe.com/docs/api +- **Stripe Webhooks**: https://stripe.com/docs/webhooks +- **Stripe Testing**: https://stripe.com/docs/testing +- **Stripe CLI**: https://stripe.com/docs/stripe-cli +- **Stripe Node.js**: https://github.com/stripe/stripe-node + +### Root Platform + +- **Root Platform Documentation**: Contact your Root representative +- **API Reference**: Available in Root Platform dashboard +- **Collection Modules**: Platform-specific documentation + +--- + +## 📞 Support -## Configuration +### Documentation -### Root Config -Copy .root-config.sample.json and rename to .root-config.json. Add your root configuration here. +All documentation is in `stripe_collection_module/docs/`: +- Setup and configuration +- Customization guide +- Deployment instructions +- Testing guide +- Best practices +- Architecture overview -| Variable | Description | -| ------------------------ | -------------------------------------------------------------------------------------------- | -| `collectionModuleKey` | The unique key of the collection module. | -| `collectionModuleName` | The name of the collection module. | -| `organizationId` | The Root organization ID for the collection module. | -| `host` | The host URL | +### Getting Help +1. Check the [documentation](./stripe_collection_module/docs/) +2. Review [troubleshooting](#troubleshooting) section +3. Check Root Platform logs for errors +4. Review Stripe documentation -### Env.ts +--- -Copy env.sample and rename to env.ts. Add your environment variables here. +## ✨ What's Next? -| Variable | Description | -| ------------------------------------- | -------------------------------------------------------------------------------------------- | -| `STRIPE_WEBHOOK_SIGNING_SECRET_LIVE` | The Stripe webhook signing secret for the live environment. | -| `STRIPE_WEBHOOK_SIGNING_SECRET_TEST` | The Stripe webhook signing secret for the test environment. | -| `STRIPE_PRODUCT_ID_LIVE` | The Stripe product id for the live environment. | -| `STRIPE_PRODUCT_ID_TEST` | The Stripe product id for the test environment. | -| `STRIPE_PUBLISHABLE_KEY_LIVE` | The Stripe publishable key for the live environment. | -| `STRIPE_PUBLISHABLE_KEY_TEST` | The Stripe publishable key for the test environment. | -| `STRIPE_SECRET_KEY_LIVE` | The Stripe API secret key for the live environment. | -| `STRIPE_SECRET_KEY_TEST` | The Stripe API secret key for the test environment. | -| `ROOT_COLLECTION_MODULE_KEY` | The collection module unique key. | -| `ROOT_API_KEY_LIVE` | The Root API key for the production environment. | -| `ROOT_API_KEY_TEST` | The Root API key for the sandbox environment. | -| `ROOT_BASE_URL_LIVE` | The Root API base URL the production environment. | -| `ROOT_BASE_URL_TEST` | The Root API base URL the sandbox environment. | +After setup: +1. **Implement** - Complete Stripe service implementations ([docs/CUSTOMIZING.md](./stripe_collection_module/docs/CUSTOMIZING.md)) +2. **Test** - Write comprehensive tests ([docs/TESTING.md](./stripe_collection_module/docs/TESTING.md)) +3. **Deploy** - Publish to Root Platform ([docs/DEPLOYMENT.md](./stripe_collection_module/docs/DEPLOYMENT.md)) +4. **Monitor** - Review Root Platform logs and metrics +5. **Iterate** - Refine based on production usage -## How to deploy the template +--- -Once your code has been merged into main, go to the [github repository](https://github.com/RootBank/collection-module-template_stripe) -and select [releases](TODO). Create a new release add the new version number e.g. v2.0.0. -The collection module will be pushed and published. \ No newline at end of file +**Built with ❤️ for the Root Platform** diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..59da576 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,358 @@ +# Getting Started with the Stripe Collection Module Template + +This guide will help you set up and customize this template for your specific use case. + +## Prerequisites + +- Node.js 18+ (check with `node --version`) +- npm or yarn +- Root Platform account and API credentials +- Stripe account and API keys + +## Initial Setup + +### 1. Clone and Run Setup + +```bash +# Clone the repository (or use this template) +git clone +cd collection-module-template_stripe + +# Run the automated setup script +./setup.sh +``` + +The setup script will: +- ✓ Install all dependencies +- ✓ Configure Root Platform settings (`.root-config.json` and `.root-auth`) +- ✓ Create `code/env.ts` from the template +- ✓ Run validation checks +- ✓ Show you next steps + +### 2. Configure Root Platform + +The setup script will prompt you for: + +**`.root-config.json`** - Collection module metadata: +- Collection Module Key (e.g., `cm_stripe_yourcompany`) +- Collection Module Name (e.g., "Your Company Stripe Integration") +- Organization ID +- Root Platform Host URL + +**`.root-auth`** - Authentication: +- Root Platform API Key (stored securely, gitignored) + +For detailed information about these files, see [Root Configuration Guide](stripe_collection_module/docs/ROOT_CONFIGURATION.md). + +### 3. Configure Environment + +Edit `stripe_collection_module/code/env.ts` with your actual credentials: + +```typescript +export default { + // Root Platform Configuration + rootOrganisationId: 'your-org-id', + rootApiKey: 'your-api-key', + rootEnvironment: 'sandbox', // or 'production' + rootBaseUrl: 'https://api.root.co.za', + + // Stripe Configuration + stripeSecretKey: 'sk_test_...', + stripeWebhookSigningSecret: 'whsec_...', + stripePublishableKey: 'pk_test_...', + stripeProductId: 'prod_...', + + // Module Configuration + environment: 'development', +}; +``` + +**⚠️ Important:** Never commit sensitive files to version control: +- `code/env.ts` - Contains API keys and secrets +- `.root-auth` - Contains Root Platform API key + +Both files are already in `.gitignore`. + +## Project Structure + +``` +collection-module-template_stripe/ +├── setup.sh # Automated setup script +├── README.md # Main documentation +└── stripe_collection_module/ + ├── code/ + │ ├── controllers/ # Event handlers + │ │ ├── root-event-processors/ + │ │ └── stripe-event-processors/ + │ ├── services/ # Business logic + │ │ ├── config.service.ts + │ │ ├── log.service.ts + │ │ ├── root.service.ts + │ │ └── stripe.service.ts + │ ├── core/ # DI container setup + │ ├── clients/ # API clients + │ ├── utils/ # Utilities + │ ├── lifecycle-hooks/ # Root Platform hooks + │ ├── webhook-hooks.ts # Stripe webhook handler + │ └── main.ts # Entry point + ├── __tests__/ # Tests + ├── docs/ # Detailed documentation + │ ├── SETUP.md + │ ├── DEPLOYMENT.md + │ ├── CUSTOMIZING.md + │ └── BEST_PRACTICES.md + └── scripts/ # Deployment scripts +``` + +## Customization Steps + +### 1. Update Module Metadata + +Edit `stripe_collection_module/package.json`: + +```json +{ + "name": "your-collection-module-name", + "version": "1.0.0", + "description": "Your module description" +} +``` + +### 2. Implement Your Controllers + +The template includes example controllers. Customize them for your use case: + +**Stripe Event Processor Example:** + +```typescript +// code/controllers/stripe-event-processors/invoice-paid.controller.ts +export class InvoicePaidController { + async handle(invoice: Stripe.Invoice): Promise { + // Your logic here + } +} +``` + +**Root Event Processor Example:** + +```typescript +// code/controllers/root-event-processors/payment-creation.controller.ts +export class PaymentCreationController { + async handle(params: any): Promise { + // Your logic here + } +} +``` + +### 3. Add Your Services + +Extend or create new services in `code/services/`: + +```typescript +// code/services/your-service.ts +export class YourService { + constructor( + private readonly logService: LogService, + // ... other dependencies + ) {} + + async yourMethod(): Promise { + // Your logic + } +} +``` + +### 4. Register in DI Container + +Add your services and controllers to `code/core/container.setup.ts`: + +```typescript +container.register( + ServiceToken.YOUR_SERVICE, + (c) => { + const logService = c.resolve(ServiceToken.LOG_SERVICE); + return new YourService(logService); + }, + ServiceLifetime.SINGLETON +); +``` + +## Development Workflow + +### Running Locally + +```bash +cd stripe_collection_module + +# Lint your code +npm run lint + +# Run tests +npm run test + +# Build the module +npm run build +``` + +### Testing + +```bash +# Run all tests +npm run test + +# Run with coverage +npm run test:coverage + +# Run in watch mode +npm run test:watch +``` + +### Validation + +```bash +# Check configuration +npm run validate + +# Format code +npm run format + +# Pre-deployment checks +npm run predeploy +``` + +## Deployment + +### First Deployment + +1. Ensure all tests pass: `npm run test` +2. Build the module: `npm run build` +3. Deploy to sandbox first: `npm run deploy:sandbox` +4. Test thoroughly in sandbox environment +5. Deploy to production: `npm run deploy:production` + +### Automated Deployment + +The deployment script handles: +- Pre-deployment validation +- Building the module +- Git tagging (with semver) +- Publishing to Root Platform API +- Dry-run mode for testing + +```bash +# Deploy to sandbox +npm run deploy:sandbox + +# Deploy to production +npm run deploy:production + +# Test deployment without publishing +npm run deploy:dry-run +``` + +See [DEPLOYMENT.md](stripe_collection_module/docs/DEPLOYMENT.md) for details. + +## Configuration Management + +### Environment-Specific Config + +The template uses a single `env.ts` file. For multiple environments: + +**Option 1: Environment Variables** + +```typescript +// code/env.ts +export default { + rootApiKey: process.env.ROOT_API_KEY || 'fallback', + // ... +}; +``` + +**Option 2: Multiple Config Files** (not recommended for templates) + +```bash +code/env.development.ts +code/env.sandbox.ts +code/env.production.ts +``` + +### Secrets Management + +**Never commit secrets!** The template's `.gitignore` already excludes: +- `code/env.ts` - Stripe keys and module configuration +- `.root-auth` - Root Platform API key +- `*.env` - Any environment files +- `.env*` - Environment file variants + +## Next Steps + +1. **Review Documentation** + - [Customization Guide](stripe_collection_module/docs/CUSTOMIZING.md) + - [Best Practices](stripe_collection_module/docs/BEST_PRACTICES.md) + - [Deployment Guide](stripe_collection_module/docs/DEPLOYMENT.md) + +2. **Implement Your Logic** + - Start with one controller + - Add services as needed + - Write tests as you go + +3. **Test Thoroughly** + - Unit tests for services + - Integration tests for controllers + - Test in sandbox environment + +4. **Deploy** + - Follow the deployment guide + - Start with sandbox + - Monitor logs and metrics + +## Getting Help + +- Check the [README](stripe_collection_module/README.md) for overview +- Review [example controllers](stripe_collection_module/code/controllers/) +- See [Stripe Integration Guide](stripe_collection_module/docs/CUSTOMIZING.md) +- Root Platform docs: https://docs.root.co.za +- Stripe API docs: https://stripe.com/docs/api + +## Common Issues + +### "Module not found" errors + +```bash +rm -rf node_modules package-lock.json +npm install +``` + +### Build failures + +```bash +npm run clean +npm install +npm run build +``` + +### Type errors from SDKs + +Make sure you're using the correct SDK types: +- Stripe types: `Stripe.Customer`, `Stripe.Invoice`, etc. +- Root types: `root.Policy`, `root.PaymentMethod`, etc. + +### Deployment fails + +1. Check your API credentials in `env.ts` +2. Ensure tests pass: `npm run test` +3. Try dry-run first: `npm run deploy:dry-run` + +## Contributing to the Template + +If you find improvements that would benefit all users of this template: + +1. Keep changes generic and configurable +2. Update documentation +3. Add tests +4. Submit a pull request + +--- + +**Ready to build?** Start with `./setup.sh` and follow the wizard! 🚀 + diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..11777bf --- /dev/null +++ b/setup.sh @@ -0,0 +1,167 @@ +#!/bin/bash + +# Collection Module Template Setup Script +# This script helps you set up the template after cloning + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}╔════════════════════════════════════════════════════════╗${NC}" +echo -e "${BLUE}║ Stripe Collection Module Template - Setup Wizard ║${NC}" +echo -e "${BLUE}╚════════════════════════════════════════════════════════╝${NC}" +echo "" + +# Check if we're in the right directory +if [ ! -f "stripe_collection_module/package.json" ]; then + echo -e "${RED}❌ Error: This script must be run from the template root directory${NC}" + exit 1 +fi + +cd stripe_collection_module + +# Step 1: Install dependencies +echo -e "${YELLOW}📦 Step 1: Installing dependencies...${NC}" +if command -v npm &> /dev/null; then + npm install + echo -e "${GREEN}✓ Dependencies installed${NC}" +else + echo -e "${RED}❌ npm not found. Please install Node.js first.${NC}" + exit 1 +fi +echo "" + +# Step 2: Configure Root Platform settings +echo -e "${YELLOW}🔑 Step 2: Configuring Root Platform...${NC}" + +# Check if .root-config.json needs setup +if [ ! -f ".root-config.json" ]; then + echo -e "${RED}❌ .root-config.json not found${NC}" + exit 1 +fi + +# Check if this is the default template config +if grep -q "my_collection_module_cm_stripe" .root-config.json 2>/dev/null; then + echo -e "${BLUE}📝 Let's configure your Root Platform settings...${NC}" + echo "" + + # Prompt for configuration values + read -p "Collection Module Key (e.g., cm_stripe_yourcompany): " cm_key + read -p "Collection Module Name (e.g., Your Company Stripe Integration): " cm_name + read -p "Organization ID: " org_id + read -p "Root Platform Host (default: https://api.rootplatform.com): " host + host=${host:-https://api.rootplatform.com} + + # Update .root-config.json + cat > .root-config.json << EOF +{ + "collectionModuleKey": "${cm_key}", + "collectionModuleName": "${cm_name}", + "organizationId": "${org_id}", + "host": "${host}", + "settings": { + "legacyCodeExecution": false + }, + "manualTransactions": [] +} +EOF + echo -e "${GREEN}✓ Created .root-config.json${NC}" +else + echo -e "${GREEN}✓ .root-config.json already configured${NC}" +fi + +# Set up .root-auth +if [ ! -f ".root-auth" ]; then + if [ -f ".root-auth.sample" ]; then + echo "" + read -p "Root Platform API Key (will be stored in .root-auth): " api_key + echo "ROOT_API_KEY=${api_key}" > .root-auth + echo -e "${GREEN}✓ Created .root-auth${NC}" + echo -e "${BLUE}ℹ️ This file is gitignored for security${NC}" + else + echo -e "${YELLOW}⚠️ .root-auth.sample not found, skipping .root-auth creation${NC}" + fi +else + echo -e "${GREEN}✓ .root-auth already exists${NC}" +fi +echo "" + +# Step 3: Set up environment configuration +echo -e "${YELLOW}⚙️ Step 3: Setting up code environment configuration...${NC}" +if [ ! -f "code/env.ts" ]; then + cp code/env.sample.ts code/env.ts + echo -e "${GREEN}✓ Created code/env.ts from template${NC}" + echo -e "${BLUE}ℹ️ Please edit code/env.ts with your Stripe API keys and other configuration${NC}" +else + echo -e "${BLUE}ℹ️ code/env.ts already exists, skipping...${NC}" +fi +echo "" + +# Step 4: Set up .nvmrc (optional) +echo -e "${YELLOW}🔧 Step 4: Node.js version...${NC}" +if [ -f ".nvmrc" ]; then + echo -e "${GREEN}✓ .nvmrc already configured${NC}" + if command -v nvm &> /dev/null; then + echo -e "${BLUE}ℹ️ Run 'nvm use' to switch to the correct Node.js version${NC}" + fi +else + echo -e "${BLUE}ℹ️ No .nvmrc found (optional)${NC}" +fi +echo "" + +# Step 5: Run validation +echo -e "${YELLOW}✓ Step 5: Running validation checks...${NC}" +if npm run lint > /dev/null 2>&1; then + echo -e "${GREEN}✓ Linting passed${NC}" +else + echo -e "${YELLOW}⚠️ Linting has warnings (this is normal for a template)${NC}" +fi + +if npm run build > /dev/null 2>&1; then + echo -e "${GREEN}✓ Build successful${NC}" +else + echo -e "${RED}❌ Build failed - please check your setup${NC}" +fi +echo "" + +# Step 6: Next steps +echo -e "${BLUE}╔════════════════════════════════════════════════════════╗${NC}" +echo -e "${BLUE}║ Setup Complete! Next Steps: ║${NC}" +echo -e "${BLUE}╚════════════════════════════════════════════════════════╝${NC}" +echo "" +echo -e "${GREEN}1. Finish configuring your environment:${NC}" +echo -e " ${BLUE}→${NC} Edit ${YELLOW}stripe_collection_module/code/env.ts${NC}" +echo -e " ${BLUE}→${NC} Add your Stripe API keys and webhook secrets" +echo -e " ${BLUE}→${NC} Verify ${YELLOW}stripe_collection_module/.root-config.json${NC}" +echo -e " ${BLUE}→${NC} Verify ${YELLOW}stripe_collection_module/.root-auth${NC} has correct API key" +echo "" +echo -e "${GREEN}2. Customize for your use case:${NC}" +echo -e " ${BLUE}→${NC} Update ${YELLOW}README.md${NC} with your module details" +echo -e " ${BLUE}→${NC} Implement controllers in ${YELLOW}code/controllers/${NC}" +echo -e " ${BLUE}→${NC} Add your business logic to services" +echo "" +echo -e "${GREEN}3. Review documentation:${NC}" +echo -e " ${BLUE}→${NC} ${YELLOW}stripe_collection_module/docs/SETUP.md${NC}" +echo -e " ${BLUE}→${NC} ${YELLOW}stripe_collection_module/docs/DEPLOYMENT.md${NC}" +echo -e " ${BLUE}→${NC} ${YELLOW}stripe_collection_module/docs/CUSTOMIZING.md${NC}" +echo "" +echo -e "${GREEN}4. Development commands:${NC}" +echo -e " ${BLUE}→${NC} ${YELLOW}npm run lint${NC} - Check code quality" +echo -e " ${BLUE}→${NC} ${YELLOW}npm run test${NC} - Run tests" +echo -e " ${BLUE}→${NC} ${YELLOW}npm run build${NC} - Build the module" +echo "" +echo -e "${GREEN}5. Deploy when ready:${NC}" +echo -e " ${BLUE}→${NC} ${YELLOW}npm run deploy:sandbox${NC} - Deploy to sandbox" +echo -e " ${BLUE}→${NC} ${YELLOW}npm run deploy:production${NC} - Deploy to production" +echo "" +echo -e "${BLUE}📚 For detailed documentation, see:${NC}" +echo -e " ${YELLOW}stripe_collection_module/README.md${NC}" +echo "" +echo -e "${GREEN}✨ Happy coding!${NC}" +echo "" + diff --git a/stripe_collection_module/.eslintrc.js b/stripe_collection_module/.eslintrc.js index a276107..cc75e2c 100644 --- a/stripe_collection_module/.eslintrc.js +++ b/stripe_collection_module/.eslintrc.js @@ -188,6 +188,7 @@ module.exports = { es2020: true, node: true, mocha: true, + jest: true, }, extends: [ 'eslint:recommended', @@ -224,6 +225,12 @@ module.exports = { }, ignorePatterns: ['node_modules*/'], overrides: [ + { + files: ['__tests__/**/*', '**/*.test.ts', '**/*.spec.ts'], + env: { + jest: true, + }, + }, { files: ['./**/*.js'], rules: { diff --git a/stripe_collection_module/.nvmrc b/stripe_collection_module/.nvmrc new file mode 100644 index 0000000..19b3677 --- /dev/null +++ b/stripe_collection_module/.nvmrc @@ -0,0 +1,5 @@ +18 + + + + diff --git a/stripe_collection_module/.prettierrc b/stripe_collection_module/.prettierrc new file mode 100644 index 0000000..63d982b --- /dev/null +++ b/stripe_collection_module/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always", + "endOfLine": "lf" +} + diff --git a/stripe_collection_module/.root-config.json b/stripe_collection_module/.root-config.json index d79e97f..e20a43d 100644 --- a/stripe_collection_module/.root-config.json +++ b/stripe_collection_module/.root-config.json @@ -1,10 +1,10 @@ { - "collectionModuleKey": "my_collection_module_cm_stripe", - "collectionModuleName": "Stripe Collection Module", - "organizationId": "00000000-0000-0000-0000-000000000001", - "host": "http://localhost:4000", + "collectionModuleKey": "test_cm", + "collectionModuleName": "Test CM", + "organizationId": "12345", + "host": "https://api.rootplatform.com", "settings": { "legacyCodeExecution": false }, "manualTransactions": [] -} \ No newline at end of file +} diff --git a/stripe_collection_module/.root-config.json.sample b/stripe_collection_module/.root-config.json.sample new file mode 100644 index 0000000..6716e4d --- /dev/null +++ b/stripe_collection_module/.root-config.json.sample @@ -0,0 +1,12 @@ +{ + "collectionModuleKey": "my_collection_module_cm_stripe", + "collectionModuleName": "Stripe Collection Module", + "organizationId": "00000000-0000-0000-0000-000000000001", + "host": "http://localhost:4000", + "settings": { + "legacyCodeExecution": false + }, + "manualTransactions": [] +} + + diff --git a/stripe_collection_module/.vscode/launch.json b/stripe_collection_module/.vscode/launch.json new file mode 100644 index 0000000..b734406 --- /dev/null +++ b/stripe_collection_module/.vscode/launch.json @@ -0,0 +1,96 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Jest: Current File", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": [ + "${fileBasenameNoExtension}", + "--config", + "jest.config.js", + "--runInBand", + "--no-cache" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "windows": { + "program": "${workspaceFolder}/node_modules/jest/bin/jest" + } + }, + { + "type": "node", + "request": "launch", + "name": "Jest: All Tests", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": [ + "--config", + "jest.config.js", + "--runInBand", + "--no-cache" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "windows": { + "program": "${workspaceFolder}/node_modules/jest/bin/jest" + } + }, + { + "type": "node", + "request": "launch", + "name": "Jest: Watch Mode", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": [ + "--config", + "jest.config.js", + "--watch", + "--runInBand" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "windows": { + "program": "${workspaceFolder}/node_modules/jest/bin/jest" + } + }, + { + "type": "node", + "request": "launch", + "name": "Debug TypeScript File", + "program": "${workspaceFolder}/node_modules/.bin/ts-node", + "args": [ + "${relativeFile}" + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + }, + { + "type": "node", + "request": "attach", + "name": "Attach to Process", + "port": 9229, + "restart": true, + "skipFiles": [ + "/**" + ] + }, + { + "type": "node", + "request": "launch", + "name": "Debug Lambda Locally", + "program": "${workspaceFolder}/code/main.ts", + "preLaunchTask": "npm: build", + "outFiles": [ + "${workspaceFolder}/dist/**/*.js" + ], + "env": { + "NODE_ENV": "development", + "AWS_REGION": "us-east-1" + }, + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + ] +} + diff --git a/stripe_collection_module/.vscode/settings.json b/stripe_collection_module/.vscode/settings.json new file mode 100644 index 0000000..d4e0653 --- /dev/null +++ b/stripe_collection_module/.vscode/settings.json @@ -0,0 +1,87 @@ +{ + // Editor + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.tabSize": 2, + "editor.rulers": [80, 120], + + // TypeScript + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + "typescript.preferences.importModuleSpecifier": "relative", + "typescript.updateImportsOnFileMove.enabled": "always", + + // JavaScript/TypeScript specific + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + + // ESLint + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" + ], + "eslint.format.enable": false, + + // Prettier + "prettier.requireConfig": true, + + // Files + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/node_modules": true, + "**/dist": true, + "**/coverage": true + }, + "files.watcherExclude": { + "**/node_modules/**": true, + "**/dist/**": true, + "**/coverage/**": true + }, + "files.eol": "\n", + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + + // Search + "search.exclude": { + "**/node_modules": true, + "**/dist": true, + "**/coverage": true, + "**/*.log": true + }, + + // Git + "git.ignoreLimitWarning": true, + + // Jest + "jest.autoRun": "off", + "jest.showCoverageOnLoad": false, + + // Terminal + "terminal.integrated.env.osx": { + "NODE_ENV": "development" + }, + "terminal.integrated.env.linux": { + "NODE_ENV": "development" + }, + + // Problems + "problems.showCurrentInStatus": true +} + diff --git a/stripe_collection_module/.vscode/tasks.json b/stripe_collection_module/.vscode/tasks.json new file mode 100644 index 0000000..5787e38 --- /dev/null +++ b/stripe_collection_module/.vscode/tasks.json @@ -0,0 +1,57 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "npm: build", + "type": "npm", + "script": "build", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [ + "$tsc" + ] + }, + { + "label": "npm: test", + "type": "npm", + "script": "test", + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": [] + }, + { + "label": "npm: lint", + "type": "npm", + "script": "lint", + "problemMatcher": [ + "$eslint-stylish" + ] + }, + { + "label": "npm: lint:fix", + "type": "npm", + "script": "lint:fix", + "problemMatcher": [ + "$eslint-stylish" + ] + }, + { + "label": "npm: validate", + "type": "npm", + "script": "validate", + "problemMatcher": [] + }, + { + "label": "npm: test:watch", + "type": "npm", + "script": "test:watch", + "isBackground": true, + "problemMatcher": [] + } + ] +} + diff --git a/stripe_collection_module/__tests__/core/container.test.ts b/stripe_collection_module/__tests__/core/container.test.ts new file mode 100644 index 0000000..9b172b5 --- /dev/null +++ b/stripe_collection_module/__tests__/core/container.test.ts @@ -0,0 +1,318 @@ +/** + * Container Tests + */ + +import { + Container, + ServiceLifetime, + ServiceToken, +} from '../../code/core/container'; + +// Mock services for testing +class MockServiceA { + constructor(public name: string = 'ServiceA') {} +} + +class MockServiceB { + constructor( + public serviceA: MockServiceA, + public name: string = 'ServiceB', + ) {} +} + +describe('Container', () => { + let container: Container; + + beforeEach(() => { + container = new Container(); + }); + + describe('Service Registration', () => { + it('should register a service', () => { + container.register( + 'MockServiceA', + () => new MockServiceA(), + ServiceLifetime.SINGLETON, + ); + + expect(container.has('MockServiceA')).toBe(true); + }); + + it('should register multiple services', () => { + container.register( + 'ServiceA', + () => new MockServiceA(), + ServiceLifetime.SINGLETON, + ); + container.register( + 'ServiceB', + () => new MockServiceB(new MockServiceA()), + ServiceLifetime.SINGLETON, + ); + + expect(container.has('ServiceA')).toBe(true); + expect(container.has('ServiceB')).toBe(true); + }); + + it('should support symbol tokens', () => { + const token = Symbol('MockService'); + container.register( + token, + () => new MockServiceA(), + ServiceLifetime.SINGLETON, + ); + + expect(container.has(token)).toBe(true); + }); + }); + + describe('Service Resolution', () => { + it('should resolve a registered service', () => { + container.register( + 'MockServiceA', + () => new MockServiceA(), + ServiceLifetime.SINGLETON, + ); + + const service = container.resolve('MockServiceA'); + + expect(service).toBeInstanceOf(MockServiceA); + expect(service.name).toBe('ServiceA'); + }); + + it('should throw error when resolving unregistered service', () => { + expect(() => { + container.resolve('NonExistent'); + }).toThrow('Service not registered: NonExistent'); + }); + + it('should support dependency injection', () => { + container.register( + 'ServiceA', + () => new MockServiceA(), + ServiceLifetime.SINGLETON, + ); + + container.register( + 'ServiceB', + (c) => new MockServiceB(c.resolve('ServiceA')), + ServiceLifetime.SINGLETON, + ); + + const serviceB = container.resolve('ServiceB'); + + expect(serviceB).toBeInstanceOf(MockServiceB); + expect(serviceB.serviceA).toBeInstanceOf(MockServiceA); + expect(serviceB.name).toBe('ServiceB'); + }); + }); + + describe('Service Lifetimes', () => { + it('should return same instance for singleton services', () => { + container.register( + 'MockServiceA', + () => new MockServiceA(), + ServiceLifetime.SINGLETON, + ); + + const instance1 = container.resolve('MockServiceA'); + const instance2 = container.resolve('MockServiceA'); + + expect(instance1).toBe(instance2); + }); + + it('should return new instance for transient services', () => { + container.register( + 'MockServiceA', + () => new MockServiceA(), + ServiceLifetime.TRANSIENT, + ); + + const instance1 = container.resolve('MockServiceA'); + const instance2 = container.resolve('MockServiceA'); + + expect(instance1).not.toBe(instance2); + expect(instance1).toBeInstanceOf(MockServiceA); + expect(instance2).toBeInstanceOf(MockServiceA); + }); + }); + + describe('Service Replacement', () => { + it('should replace a registered service', () => { + container.register( + 'MockServiceA', + () => new MockServiceA('Original'), + ServiceLifetime.SINGLETON, + ); + + const original = container.resolve('MockServiceA'); + expect(original.name).toBe('Original'); + + container.replace( + 'MockServiceA', + () => new MockServiceA('Replaced'), + ServiceLifetime.SINGLETON, + ); + + const replaced = container.resolve('MockServiceA'); + expect(replaced.name).toBe('Replaced'); + expect(replaced).not.toBe(original); + }); + }); + + describe('Service Unregistration', () => { + it('should unregister a service', () => { + container.register( + 'MockServiceA', + () => new MockServiceA(), + ServiceLifetime.SINGLETON, + ); + + expect(container.has('MockServiceA')).toBe(true); + + container.unregister('MockServiceA'); + + expect(container.has('MockServiceA')).toBe(false); + }); + }); + + describe('Container Management', () => { + it('should clear all services', () => { + container.register( + 'ServiceA', + () => new MockServiceA(), + ServiceLifetime.SINGLETON, + ); + container.register( + 'ServiceB', + () => new MockServiceB(new MockServiceA()), + ServiceLifetime.SINGLETON, + ); + + expect(container.has('ServiceA')).toBe(true); + expect(container.has('ServiceB')).toBe(true); + + container.clear(); + + expect(container.has('ServiceA')).toBe(false); + expect(container.has('ServiceB')).toBe(false); + }); + + it('should return all registered tokens', () => { + container.register( + 'ServiceA', + () => new MockServiceA(), + ServiceLifetime.SINGLETON, + ); + container.register( + 'ServiceB', + () => new MockServiceB(new MockServiceA()), + ServiceLifetime.SINGLETON, + ); + + const tokens = container.getRegisteredTokens(); + + expect(tokens).toHaveLength(2); + expect(tokens).toContain('ServiceA'); + expect(tokens).toContain('ServiceB'); + }); + }); + + describe('Service Tokens', () => { + it('should have predefined service tokens', () => { + expect(ServiceToken.LOG_SERVICE).toBeDefined(); + expect(ServiceToken.CONFIG_SERVICE).toBeDefined(); + expect(ServiceToken.STRIPE_CLIENT).toBeDefined(); + expect(ServiceToken.ROOT_CLIENT).toBeDefined(); + expect(ServiceToken.ROOT_CLIENT).toBeDefined(); + expect(ServiceToken.ROOT_SERVICE).toBeDefined(); + expect(ServiceToken.STRIPE_SERVICE).toBeDefined(); + expect(ServiceToken.RENDER_SERVICE).toBeDefined(); + }); + + it('should use symbols for type-safe tokens', () => { + expect(typeof ServiceToken.LOG_SERVICE).toBe('symbol'); + expect(typeof ServiceToken.CONFIG_SERVICE).toBe('symbol'); + }); + }); + + describe('Testing Support', () => { + it('should support mocking services for tests', () => { + // Register real service + container.register( + 'ServiceA', + () => new MockServiceA('Real'), + ServiceLifetime.SINGLETON, + ); + + // Use service + const realService = container.resolve('ServiceA'); + expect(realService.name).toBe('Real'); + + // Replace with mock for testing + container.replace( + 'ServiceA', + () => new MockServiceA('Mock'), + ServiceLifetime.SINGLETON, + ); + + const mockService = container.resolve('ServiceA'); + expect(mockService.name).toBe('Mock'); + }); + + it('should allow creating isolated test containers', () => { + const testContainer = new Container(); + + testContainer.register( + 'MockServiceA', + () => new MockServiceA('Test'), + ServiceLifetime.SINGLETON, + ); + + const service = testContainer.resolve('MockServiceA'); + expect(service.name).toBe('Test'); + + // Original container is unaffected + expect(container.has('MockServiceA')).toBe(false); + }); + }); + + describe('Complex Dependency Graph', () => { + it('should resolve complex dependency chains', () => { + class ServiceC { + constructor( + public serviceA: MockServiceA, + public serviceB: MockServiceB, + ) {} + } + + container.register( + 'ServiceA', + () => new MockServiceA(), + ServiceLifetime.SINGLETON, + ); + + container.register( + 'ServiceB', + (c) => new MockServiceB(c.resolve('ServiceA')), + ServiceLifetime.SINGLETON, + ); + + container.register( + 'ServiceC', + (c) => + new ServiceC( + c.resolve('ServiceA'), + c.resolve('ServiceB'), + ), + ServiceLifetime.SINGLETON, + ); + + const serviceC = container.resolve('ServiceC'); + + expect(serviceC.serviceA).toBeInstanceOf(MockServiceA); + expect(serviceC.serviceB).toBeInstanceOf(MockServiceB); + expect(serviceC.serviceB.serviceA).toBe(serviceC.serviceA); // Same singleton instance + }); + }); +}); diff --git a/stripe_collection_module/__tests__/helpers/factories.ts b/stripe_collection_module/__tests__/helpers/factories.ts new file mode 100644 index 0000000..7270ade --- /dev/null +++ b/stripe_collection_module/__tests__/helpers/factories.ts @@ -0,0 +1,175 @@ +/** + * Test data factories + * Create mock objects for testing + */ + +import Stripe from 'stripe'; + +/** + * Create a mock Stripe customer + */ +export const createMockStripeCustomer = ( + overrides?: Partial, +): Stripe.Customer => { + return { + id: 'cus_test_123', + object: 'customer', + created: Date.now() / 1000, + email: 'test@example.com', + name: 'Test Customer', + description: 'Test customer for unit tests', + livemode: false, + metadata: {}, + ...overrides, + } as Stripe.Customer; +}; + +/** + * Create a mock Stripe payment method + */ +export const createMockStripePaymentMethod = ( + overrides?: Partial, +): Stripe.PaymentMethod => { + return { + id: 'pm_test_123', + object: 'payment_method', + created: Date.now() / 1000, + type: 'card', + livemode: false, + card: { + brand: 'visa', + last4: '4242', + exp_month: 12, + exp_year: 2025, + funding: 'credit', + checks: null, + country: 'US', + fingerprint: 'test_fingerprint', + generated_from: null, + networks: null, + three_d_secure_usage: null, + wallet: null, + }, + billing_details: { + address: null, + email: null, + name: null, + phone: null, + }, + metadata: {}, + ...overrides, + } as Stripe.PaymentMethod; +}; + +/** + * Create a mock Stripe subscription + */ +export const createMockStripeSubscription = ( + overrides?: Partial, +): Stripe.Subscription => { + return { + id: 'sub_test_123', + object: 'subscription', + created: Date.now() / 1000, + current_period_start: Date.now() / 1000, + current_period_end: Date.now() / 1000 + 30 * 24 * 60 * 60, + customer: 'cus_test_123', + status: 'active', + items: { + object: 'list', + data: [], + has_more: false, + url: '/v1/subscription_items', + }, + metadata: {}, + ...overrides, + } as Stripe.Subscription; +}; + +/** + * Create a mock Stripe invoice + */ +export const createMockStripeInvoice = ( + overrides?: Partial, +): Stripe.Invoice => { + return { + id: 'in_test_123', + object: 'invoice', + created: Date.now() / 1000, + customer: 'cus_test_123', + status: 'paid', + amount_due: 10000, + amount_paid: 10000, + amount_remaining: 0, + currency: 'zar', + lines: { + object: 'list', + data: [], + has_more: false, + url: '/v1/invoices/in_test_123/lines', + }, + metadata: {}, + ...overrides, + } as Stripe.Invoice; +}; + +/** + * Create a mock Root policy + */ +export const createMockRootPolicy = (overrides?: any): any => { + return { + policy_id: 'policy_test_123', + policy_number: 'TEST-12345', + policyholder_id: 'policyholder_test_123', + start_date: '2024-01-01T00:00:00Z', + end_date: '2025-01-01T00:00:00Z', + monthly_premium: 50000, // in cents + currency: 'ZAR', + billing_frequency: 'monthly', + billing_day: 1, + app_data: {}, + ...overrides, + }; +}; + +/** + * Create a mock Root payment method + */ +export const createMockRootPaymentMethod = (overrides?: any): any => { + return { + payment_method_id: 'pm_root_test_123', + collection_module_key: 'test_collection_module', + collection_module_definition_id: 'cmd_test_123', + module: { + id: 'si_test_123', + usage: 'off_session', + object: 'setup_intent', + status: 'succeeded', + livemode: false, + payment_method: 'pm_test_123', + }, + ...overrides, + }; +}; + +/** + * Create a mock Root payment + */ +export const createMockRootPayment = (overrides?: any): any => { + return { + payment_id: 'payment_test_123', + policy_id: 'policy_test_123', + amount: 50000, // in cents + currency: 'ZAR', + status: 'pending', + payment_type: 'premium', + payment_date: '2024-01-01T00:00:00Z', + description: 'Test payment', + external_reference: 'ext_ref_123', + ...overrides, + }; +}; + + + + diff --git a/stripe_collection_module/__tests__/helpers/test-utils.ts b/stripe_collection_module/__tests__/helpers/test-utils.ts new file mode 100644 index 0000000..1f2c42f --- /dev/null +++ b/stripe_collection_module/__tests__/helpers/test-utils.ts @@ -0,0 +1,63 @@ +/** + * Test utilities and helper functions + */ + +/** + * Wait for a specified amount of time + */ +export const wait = (ms: number): Promise => { + return new Promise((resolve) => setTimeout(resolve, ms)); +}; + +/** + * Mock timer utilities + */ +export const mockTimers = { + enable: () => { + jest.useFakeTimers(); + }, + disable: () => { + jest.useRealTimers(); + }, + advance: (ms: number) => { + jest.advanceTimersByTime(ms); + }, + runAll: () => { + jest.runAllTimers(); + }, +}; + +/** + * Create a mock function with type safety + */ +export const createMockFn = < + T extends (...args: any[]) => any, +>(): jest.MockedFunction => { + return jest.fn() as unknown as jest.MockedFunction; +}; + +/** + * Assert that a function throws a specific error + */ +export const expectToThrow = async ( + fn: () => any, + errorMessage?: string | RegExp +): Promise => { + await expect(fn()).rejects.toThrow(errorMessage); +}; + +/** + * Create a spy on console methods + */ +export const spyOnConsole = ( + method: 'log' | 'info' | 'warn' | 'error' | 'debug' +) => { + return jest.spyOn(console, method).mockImplementation(() => {}); +}; + +/** + * Restore all console spies + */ +export const restoreConsole = () => { + jest.restoreAllMocks(); +}; diff --git a/stripe_collection_module/__tests__/services/config.service.test.ts b/stripe_collection_module/__tests__/services/config.service.test.ts new file mode 100644 index 0000000..9483c97 --- /dev/null +++ b/stripe_collection_module/__tests__/services/config.service.test.ts @@ -0,0 +1,294 @@ +/** + * ConfigurationService Tests + */ + +import { + ConfigurationService, + EnvironmentConfig, +} from '../../code/services/config.service'; + +describe('ConfigurationService', () => { + let originalEnv: string | undefined; + + beforeEach(() => { + // Save original environment + originalEnv = process.env.ENVIRONMENT; + }); + + afterEach(() => { + // Restore original environment + if (originalEnv !== undefined) { + process.env.ENVIRONMENT = originalEnv; + } else { + delete process.env.ENVIRONMENT; + } + }); + + describe('Initialization', () => { + it('should initialize with default environment (development)', () => { + delete process.env.ENVIRONMENT; + + const config = new ConfigurationService({ skipValidation: true }); + + expect(config.getEnvironment()).toBe('development'); + expect(config.isDevelopment()).toBe(true); + expect(config.isProduction()).toBe(false); + }); + + it('should initialize with production environment', () => { + process.env.ENVIRONMENT = 'production'; + + const config = new ConfigurationService({ skipValidation: true }); + + expect(config.getEnvironment()).toBe('production'); + expect(config.isProduction()).toBe(true); + expect(config.isDevelopment()).toBe(false); + }); + + it('should initialize with environment from options', () => { + const config = new ConfigurationService({ + environment: 'production', + skipValidation: true, + }); + + expect(config.getEnvironment()).toBe('production'); + expect(config.isProduction()).toBe(true); + }); + + it('should prioritize options.environment over process.env.ENVIRONMENT', () => { + process.env.ENVIRONMENT = 'development'; + + const config = new ConfigurationService({ + environment: 'production', + skipValidation: true, + }); + + expect(config.getEnvironment()).toBe('production'); + }); + }); + + describe('Validation', () => { + it('should throw error for invalid environment', () => { + expect(() => { + new ConfigurationService({ environment: 'invalid' }); + }).toThrow('Invalid ENVIRONMENT: invalid'); + }); + + it('should validate environment on initialization by default', () => { + process.env.ENVIRONMENT = 'production'; + + // Should not throw with valid config + expect(() => { + new ConfigurationService(); + }).not.toThrow(); + }); + + it('should skip validation when skipValidation is true', () => { + // This would normally fail validation due to missing env vars + // but with skipValidation it should work + expect(() => { + new ConfigurationService({ + environment: 'production', + skipValidation: true, + }); + }).not.toThrow(); + }); + }); + + describe('Configuration Access', () => { + let config: ConfigurationService; + + beforeEach(() => { + config = new ConfigurationService({ skipValidation: true }); + }); + + it('should get configuration values by key', () => { + const environment = config.get('environment'); + expect(typeof environment).toBe('string'); + expect(environment).toBeDefined(); + }); + + it('should get stripe secret key', () => { + const secretKey = config.get('stripeSecretKey'); + expect(secretKey).toBeDefined(); + }); + + it('should get root API key', () => { + const apiKey = config.get('rootApiKey'); + expect(apiKey).toBeDefined(); + }); + + it('should get all configuration', () => { + const allConfig = config.getAll(); + + expect(allConfig).toHaveProperty('environment'); + expect(allConfig).toHaveProperty('stripeSecretKey'); + expect(allConfig).toHaveProperty('rootApiKey'); + }); + + it('should return a copy when getting all configuration', () => { + const config1 = config.getAll(); + const config2 = config.getAll(); + + expect(config1).not.toBe(config2); // Different objects + expect(config1).toEqual(config2); // Same values + }); + }); + + describe('Environment-Specific Configuration', () => { + it('should load production configuration', () => { + const config = new ConfigurationService({ + environment: 'production', + skipValidation: true, + }); + + expect(config.get('environment')).toBe('production'); + // Production should use LIVE keys + expect(config.get('stripePublishableKey')).toContain('pk_live'); + expect(config.get('stripeSecretKey')).toContain('sk_live'); + }); + + it('should load development configuration', () => { + const config = new ConfigurationService({ + environment: 'development', + skipValidation: true, + }); + + expect(config.get('environment')).toBe('development'); + // Development should use TEST keys + expect(config.get('stripePublishableKey')).toContain('pk_test'); + expect(config.get('stripeSecretKey')).toContain('sk_test'); + }); + + it('should have different API keys for production vs development', () => { + const prodConfig = new ConfigurationService({ + environment: 'production', + skipValidation: true, + }); + const devConfig = new ConfigurationService({ + environment: 'development', + skipValidation: true, + }); + + expect(prodConfig.get('stripeSecretKey')).not.toBe( + devConfig.get('stripeSecretKey'), + ); + expect(prodConfig.get('rootApiKey')).not.toBe( + devConfig.get('rootApiKey'), + ); + }); + }); + + describe('Helper Methods', () => { + let config: ConfigurationService; + + beforeEach(() => { + config = new ConfigurationService({ skipValidation: true }); + }); + + it('should parse time delay as number', () => { + const delayMs = config.getTimeDelayMs(); + + expect(typeof delayMs).toBe('number'); + expect(delayMs).toBeGreaterThanOrEqual(0); + }); + + it('should return correct environment name', () => { + const devConfig = new ConfigurationService({ + environment: 'development', + skipValidation: true, + }); + const prodConfig = new ConfigurationService({ + environment: 'production', + skipValidation: true, + }); + + expect(devConfig.getEnvironment()).toBe('development'); + expect(prodConfig.getEnvironment()).toBe('production'); + }); + + it('should correctly identify production environment', () => { + const prodConfig = new ConfigurationService({ + environment: 'production', + skipValidation: true, + }); + + expect(prodConfig.isProduction()).toBe(true); + expect(prodConfig.isDevelopment()).toBe(false); + }); + + it('should correctly identify development environment', () => { + const devConfig = new ConfigurationService({ + environment: 'development', + skipValidation: true, + }); + + expect(devConfig.isDevelopment()).toBe(true); + expect(devConfig.isProduction()).toBe(false); + }); + }); + + describe('Type Safety', () => { + it('should provide type-safe configuration access', () => { + const config = new ConfigurationService({ skipValidation: true }); + + // TypeScript should ensure these are the correct types + const environment: string = config.get('environment'); + const stripeKey: string = config.get('stripeSecretKey'); + + expect(typeof environment).toBe('string'); + expect(typeof stripeKey).toBe('string'); + }); + + it('should return complete EnvironmentConfig object', () => { + const config = new ConfigurationService({ skipValidation: true }); + const allConfig: EnvironmentConfig = config.getAll(); + + // Should have all required properties + expect(allConfig).toHaveProperty('environment'); + expect(allConfig).toHaveProperty('stripeSecretKey'); + expect(allConfig).toHaveProperty('stripePublishableKey'); + expect(allConfig).toHaveProperty('stripeProductId'); + expect(allConfig).toHaveProperty('stripeWebhookSigningSecret'); + expect(allConfig).toHaveProperty('rootApiKey'); + expect(allConfig).toHaveProperty('rootBaseUrl'); + expect(allConfig).toHaveProperty('rootCollectionModuleKey'); + expect(allConfig).toHaveProperty('timeDelayInMilliseconds'); + }); + }); + + describe('Integration with Container', () => { + it('should be instantiable without dependencies', () => { + // ConfigurationService should not depend on other services + expect(() => { + new ConfigurationService({ skipValidation: true }); + }).not.toThrow(); + }); + + it('should create new instance each time (not singleton by itself)', () => { + const config1 = new ConfigurationService({ skipValidation: true }); + const config2 = new ConfigurationService({ skipValidation: true }); + + expect(config1).not.toBe(config2); + expect(config1.getAll()).toEqual(config2.getAll()); + }); + }); + + describe('Error Handling', () => { + it('should provide clear error for invalid environment', () => { + expect(() => { + new ConfigurationService({ environment: 'staging' }); + }).toThrow(/Invalid ENVIRONMENT: staging/); + }); + + it('should list valid environments in error message', () => { + try { + new ConfigurationService({ environment: 'invalid' }); + fail('Should have thrown an error'); + } catch (error: any) { + expect(error.message).toContain('production'); + expect(error.message).toContain('development'); + } + }); + }); +}); diff --git a/stripe_collection_module/__tests__/services/log.service.test.ts b/stripe_collection_module/__tests__/services/log.service.test.ts new file mode 100644 index 0000000..64c8d67 --- /dev/null +++ b/stripe_collection_module/__tests__/services/log.service.test.ts @@ -0,0 +1,169 @@ +/** + * LogService Tests + */ + +import { LogService, LogLevel } from '../../code/services/log.service'; + +describe('LogService', () => { + let logService: LogService; + let consoleDebugSpy: jest.SpyInstance; + let consoleInfoSpy: jest.SpyInstance; + let consoleWarnSpy: jest.SpyInstance; + let consoleErrorSpy: jest.SpyInstance; + + beforeEach(() => { + logService = new LogService({ + environment: 'test', + }); + + // Spy on console methods + consoleDebugSpy = jest.spyOn(console, 'debug').mockImplementation(); + consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(); + consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('Basic Logging', () => { + it('should log debug messages', () => { + logService.debug('Test debug message', 'TestContext'); + + expect(consoleDebugSpy).toHaveBeenCalledTimes(1); + const logOutput = JSON.parse(consoleDebugSpy.mock.calls[0][0]); + expect(logOutput.level).toBe('DEBUG'); + expect(logOutput.message).toBe('Test debug message'); + expect(logOutput.context).toBe('TestContext'); + }); + + it('should log info messages', () => { + logService.info('Test info message'); + + expect(consoleInfoSpy).toHaveBeenCalledTimes(1); + const logOutput = JSON.parse(consoleInfoSpy.mock.calls[0][0]); + expect(logOutput.level).toBe('INFO'); + expect(logOutput.message).toBe('Test info message'); + }); + + it('should log warning messages', () => { + logService.warn('Test warning message'); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + const logOutput = JSON.parse(consoleWarnSpy.mock.calls[0][0]); + expect(logOutput.level).toBe('WARN'); + expect(logOutput.message).toBe('Test warning message'); + }); + + it('should log error messages', () => { + const testError = new Error('Test error'); + logService.error('Test error message', 'ErrorContext', {}, testError); + + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + const logOutput = JSON.parse(consoleErrorSpy.mock.calls[0][0]); + expect(logOutput.level).toBe('ERROR'); + expect(logOutput.message).toBe('Test error message'); + expect(logOutput.error.message).toBe('Test error'); + expect(logOutput.error.stack).toBeDefined(); + }); + + it('should include metadata in logs', () => { + const metadata = { userId: '123', action: 'test' }; + logService.info('Test with metadata', 'TestContext', metadata); + + const logOutput = JSON.parse(consoleInfoSpy.mock.calls[0][0]); + expect(logOutput.metadata).toEqual(metadata); + }); + + it('should include environment in logs', () => { + logService.info('Test environment'); + + const logOutput = JSON.parse(consoleInfoSpy.mock.calls[0][0]); + expect(logOutput.environment).toBe('test'); + }); + }); + + describe('Correlation IDs', () => { + it('should generate and set correlation ID', () => { + const correlationId = logService.generateCorrelationId(); + + expect(correlationId).toBeDefined(); + expect(typeof correlationId).toBe('string'); + expect(logService.getCorrelationId()).toBe(correlationId); + }); + + it('should include correlation ID in logs', () => { + const correlationId = logService.generateCorrelationId(); + logService.info('Test with correlation ID'); + + const logOutput = JSON.parse(consoleInfoSpy.mock.calls[0][0]); + expect(logOutput.correlationId).toBe(correlationId); + }); + + it('should allow setting custom correlation ID', () => { + logService.setCorrelationId('custom-correlation-id'); + + expect(logService.getCorrelationId()).toBe('custom-correlation-id'); + + logService.info('Test message'); + const logOutput = JSON.parse(consoleInfoSpy.mock.calls[0][0]); + expect(logOutput.correlationId).toBe('custom-correlation-id'); + }); + + it('should clear correlation ID', () => { + logService.setCorrelationId('test-id'); + logService.clearCorrelationId(); + + expect(logService.getCorrelationId()).toBeNull(); + }); + }); + + // Buffer management tests removed - LogService now only outputs to stdout + + describe('Log Level Filtering', () => { + it('should respect minimum log level', () => { + const infoOnlyService = new LogService({ + environment: 'test', + minLogLevel: LogLevel.INFO, + }); + + // Spy on console again for the new service + const debugSpy = jest.spyOn(console, 'debug').mockImplementation(); + const infoSpy = jest.spyOn(console, 'info').mockImplementation(); + + infoOnlyService.debug('Debug message'); + infoOnlyService.info('Info message'); + + expect(debugSpy).not.toHaveBeenCalled(); + expect(infoSpy).toHaveBeenCalledTimes(1); + + debugSpy.mockRestore(); + infoSpy.mockRestore(); + }); + }); + + describe('Structured Output', () => { + it('should output valid JSON to stdout', () => { + logService.info('Test message', 'TestContext', { key: 'value' }); + + const logOutput = JSON.parse(consoleInfoSpy.mock.calls[0][0]); + + expect(logOutput).toHaveProperty('timestamp'); + expect(logOutput).toHaveProperty('level'); + expect(logOutput).toHaveProperty('environment'); + expect(logOutput).toHaveProperty('message'); + expect(logOutput).toHaveProperty('context'); + expect(logOutput).toHaveProperty('metadata'); + }); + + it('should include timestamp in ISO format', () => { + logService.info('Test message'); + + const logOutput = JSON.parse(consoleInfoSpy.mock.calls[0][0]); + const timestamp = new Date(logOutput.timestamp); + + expect(timestamp.toISOString()).toBe(logOutput.timestamp); + }); + }); +}); diff --git a/stripe_collection_module/__tests__/services/render.service.test.ts b/stripe_collection_module/__tests__/services/render.service.test.ts new file mode 100644 index 0000000..975ec8d --- /dev/null +++ b/stripe_collection_module/__tests__/services/render.service.test.ts @@ -0,0 +1,255 @@ +/** + * RenderService Tests + */ + +import { RenderService } from '../../code/services/render.service'; + +describe('RenderService', () => { + let renderService: RenderService; + + beforeEach(() => { + renderService = new RenderService(); + }); + + describe('renderCreatePaymentMethod', () => { + it('should render payment method creation form', () => { + const html = renderService.renderCreatePaymentMethod({ + stripePublishableKey: 'pk_test_123', + setupIntentClientSecret: 'seti_123_secret_456', + }); + + expect(html).toContain(''); + expect(html).toContain('pk_test_123'); + expect(html).toContain('seti_123_secret_456'); + expect(html).toContain('stripe.com/v3/'); + expect(html).toContain('payment-element'); + }); + + it('should escape special characters in keys', () => { + const html = renderService.renderCreatePaymentMethod({ + stripePublishableKey: 'pk_test_', + last4: '4242', + exp_month: 12, + exp_year: 2025, + }, + }, + }); + + expect(html).toContain('<script>'); + expect(html).not.toContain('', + module: { + id: 'si_123', + payment_method: 'pm_123', + livemode: false, + status: 'succeeded', + usage: 'off_session', + }, + }, + policy: { + billing_day: 15, + }, + }); + + expect(html).toContain('<script>'); + expect(html).not.toContain(' - - - -
-
-
-
-
- - - - `; -}; - -export const renderViewPaymentMethodSummary = async (params: { - payment_method: any; -}) => { - const { payment_method } = params; - - const paymentMethodDetails = await stripeClient.stripeSDK.paymentMethods - .retrieve(payment_method?.module?.payment_method as string) - .catch((error) => { - throw new ModuleError( - `Error retrieving payment method: ${error.message}`, - ); - }); - - return ` - - - - - - - - -
-

Stripe payment method

-
- Card: ${ - paymentMethodDetails.card?.brand || 'Unknown' - } **** **** **** ${ - paymentMethodDetails.card?.last4 || 'Unknown' - }, Expires: ${paymentMethodDetails.card?.exp_month || 'Unknown'}/${ - paymentMethodDetails.card?.exp_year || 'Unknown' - } -
-
- - `; -}; - -export const renderViewPaymentMethod = async (params: any) => { - const { payment_method, policy } = params; - - return ` - - - - - Simple Table - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TypeCollection module
Key${payment_method.collection_module_key}
Id${payment_method.module.id}
Payment method${payment_method.module.payment_method}
Billing day${policy.billing_day}
Livemode${payment_method.module.livemode}
Status${payment_method.module.status}
Usage${payment_method.module.usage}
- - - `; -}; - -export const afterPolicyPaymentMethodAssigned = async ({ - policy, -}: { - policy: root.Policy; -}) => { - Logger.info(`start`, { - policy, - }); - - await new ProcessPolicyPaymentMethodAssignedEventController().process({ - policy, - }); - - Logger.info(`complete`, { - policy, - }); -}; - -export const afterPaymentCreated = async ({ - policy, - payment, -}: { - policy: root.Policy; - payment: any; -}) => { - Logger.info(`start`, { - policy, - }); - - await new ProcessCreatePaymentEventController().process({ - rootPaymentId: payment.payment_id, - rootPolicyId: policy.policy_id, - amount: payment.amount, - description: payment.description, - status: payment.status, - stripePaymentMethodId: payment.stripe_payment_method_id, - }); - - Logger.info(`complete`, { - policy, - }); -}; - -export function afterPaymentUpdated({ - policy, - payment, -}: { - policy: root.Policy; - payment: root.PaymentMethod; -}) { - Logger.info(`start`, { - policy, - payment, - }); - - // Not implemented - - Logger.info(`complete`, { - policy, - payment, - }); -} - -export const afterPaymentMethodRemoved = async ({ - policy, -}: { - policy: root.Policy; -}) => { - Logger.info(`start`, { - policy, - }); - - await new ProcessPolicyCancellationEventsController().process( - policy.policy_id, - RootSupportedEvent.PaymentMethodRemoved, - ); - - Logger.info(`complete`, { - policy, - }); -}; - -export const afterPolicyCancelled = async ({ - policy, -}: { - policy: root.Policy; -}) => { - Logger.info(`start`, { - policy, - }); - - await new ProcessPolicyCancellationEventsController().process( - policy.policy_id, - RootSupportedEvent.PolicyCancelled, - ); - - Logger.info(`complete`, { - policy, - }); -}; - -export const afterPolicyExpired = async ({ - policy, -}: { - policy: root.Policy; -}) => { - Logger.info(`start`, { - policy, - }); - - await new ProcessPolicyCancellationEventsController().process( - policy.policy_id, - RootSupportedEvent.PolicyExpired, - ); - - Logger.info(`complete`, { - policy, - }); -}; - -export const afterPolicyLapsed = async ({ - policy, -}: { - policy: root.Policy; -}) => { - Logger.info(`start`, { - policy, - }); - - await new ProcessPolicyCancellationEventsController().process( - policy.policy_id, - RootSupportedEvent.PolicyLapsed, - ); - - Logger.info(`complete`, { - policy, - }); -}; - -export const afterPolicyUpdated = async ({ - policy, - updates, -}: { - policy: root.Policy; - updates: any; -}) => { - Logger.info('start', { - policy, - updates, - }); - - await new ProcessPolicyUpdatedEventController().process({ - rootPolicyId: policy.policy_id, - updates, - }); - - Logger.info('complete', { - policy, - updates, - }); -}; - -export const afterAlterationPackageApplied = async ({ - policy, - alteration_package, - alteration_hook_key, -}: { - policy: root.Policy; - alteration_package: any; - alteration_hook_key: string; -}) => { - Logger.info(`start`, { - policy, - alteration_package, - alteration_hook_key, - }); - - await new ProcessPolicyAlterationEventsController().process({ - rootPolicyId: policy.policy_id, - updatedMonthlyPremiumAmount: policy.monthly_premium, - currency: policy.currency, - billingFrequency: policy.billing_frequency as - | 'monthly' - | 'yearly' - | 'once_off', - rootPolicyStartDate: policy.start_date, - rootPolicyEndDate: policy.end_date, - billingDay: policy.billing_day, - policyAppData: policy.app_data, - alterationHookKey: alteration_hook_key, - alterationPackage: alteration_package, - }); - - Logger.info(`complete`, { - policy, - alteration_package, - alteration_hook_key, - }); -}; diff --git a/stripe_collection_module/code/lifecycle-hooks/README.md b/stripe_collection_module/code/lifecycle-hooks/README.md new file mode 100644 index 0000000..32da5a7 --- /dev/null +++ b/stripe_collection_module/code/lifecycle-hooks/README.md @@ -0,0 +1,515 @@ +# Lifecycle Hooks + +This directory contains lifecycle hook functions that are called by the Root Platform at various points in the policy and payment lifecycle. + +Lifecycle hooks are callback functions invoked by Root Platform when events occur: +- Policy issued +- Payment method assigned +- Payment created +- Policy updated +- Policy cancelled +- And more... + +## Architecture + +``` +Root Platform Event → Lifecycle Hook → Your Logic → External Systems + (trigger) (callback) (implement) (Stripe, etc.) +``` + +## Available Hooks + +### Payment Method Lifecycle + +#### renderCreatePaymentMethod() + +Renders HTML form for creating a payment method. + +**Trigger**: User clicks "Add Payment Method" in Root dashboard + +**Returns**: HTML string with Stripe Elements form + +```typescript +export async function renderCreatePaymentMethod(): Promise { + // 1. Create Stripe setup intent + const setupIntent = await stripeClient.stripeSDK.setupIntents.create({}); + + // 2. Render form with Stripe Elements + return renderService.renderCreatePaymentMethod({ + stripePublishableKey: config.get('stripePublishableKey'), + setupIntentClientSecret: setupIntent.client_secret, + }); +} +``` + +#### createPaymentMethod() + +Creates payment method data structure after form submission. + +**Trigger**: After user submits payment method form + +**Parameters**: `{ data: { setupIntent } }` + +**Returns**: `{ module: PaymentMethodData }` + +```typescript +export function createPaymentMethod({ data }): { module: any } { + return { + module: { + id: data.setupIntent.id, + usage: data.setupIntent.usage, + payment_method: data.setupIntent.payment_method, + status: data.setupIntent.status, + }, + }; +} +``` + +#### renderViewPaymentMethod() + +Renders detailed view of payment method. + +**Trigger**: Viewing payment method details in dashboard + +```typescript +export function renderViewPaymentMethod(params): string { + return renderService.renderViewPaymentMethod({ + payment_method: params.payment_method, + policy: params.policy, + }); +} +``` + +#### renderViewPaymentMethodSummary() + +Renders compact payment method summary. + +**Trigger**: Listing payment methods + +```typescript +export async function renderViewPaymentMethodSummary(params): Promise { + // Fetch payment method details from Stripe + const paymentMethodDetails = await stripeClient.stripeSDK.paymentMethods.retrieve( + params.payment_method.module.payment_method + ); + + return renderService.renderViewPaymentMethodSummary({ + payment_method: params.payment_method, + paymentMethodDetails: { card: paymentMethodDetails.card }, + }); +} +``` + +### Policy Lifecycle + +#### afterPolicyIssued() + +Called after a policy is issued. + +**Trigger**: New policy created + +**Parameters**: `{ policy }` + +**Implementation Example**: + +```typescript +export async function afterPolicyIssued({ policy }): Promise { + const logService = getLogService(); + logService.info('Policy issued', 'afterPolicyIssued', { + policyId: policy.policy_id, + }); + + // Example: Create Stripe customer for the policy + const customer = await stripeClient.stripeSDK.customers.create({ + email: policy.policyholder.email, + name: `${policy.policyholder.first_name} ${policy.policyholder.last_name}`, + metadata: { + root_policy_id: policy.policy_id, + }, + }); + + // Store customer ID in policy + await rootClient.SDK.updatePolicy({ + policyId: policy.policy_id, + body: { + app_data: { + stripe_customer_id: customer.id, + }, + }, + }); +} +``` + +#### afterPolicyPaymentMethodAssigned() + +Called when a payment method is assigned to a policy. + +**Trigger**: Payment method linked to policy + +**Parameters**: `{ policy }` + +**Implementation Example**: + +```typescript +export async function afterPolicyPaymentMethodAssigned({ policy }): Promise { + const logService = getLogService(); + logService.info('Payment method assigned', 'afterPolicyPaymentMethodAssigned', { + policyId: policy.policy_id, + }); + + // 1. Get payment method from Root + const paymentMethod = await rootClient.SDK.getPolicyPaymentMethod({ + policyId: policy.policy_id, + }); + const stripePaymentMethodId = paymentMethod.module.payment_method; + + // 2. Get or create Stripe customer + let customerId = policy.app_data?.stripe_customer_id; + if (!customerId) { + const customer = await stripeClient.stripeSDK.customers.create({ + email: policy.policyholder.email, + metadata: { root_policy_id: policy.policy_id }, + }); + customerId = customer.id; + } + + // 3. Attach payment method to customer + await stripeClient.stripeSDK.paymentMethods.attach(stripePaymentMethodId, { + customer: customerId, + }); + + // 4. Set as default payment method + await stripeClient.stripeSDK.customers.update(customerId, { + invoice_settings: { + default_payment_method: stripePaymentMethodId, + }, + }); + + // 5. Update policy with Stripe data + await rootClient.SDK.updatePolicy({ + policyId: policy.policy_id, + body: { + app_data: { + stripe_customer_id: customerId, + stripe_payment_method_id: stripePaymentMethodId, + }, + }, + }); +} +``` + +#### afterPaymentMethodRemoved() + +Called when payment method is removed from policy. + +**Trigger**: Payment method unlinked + +**Parameters**: `{ policy }` + +#### afterPolicyUpdated() + +Called when policy is updated. + +**Trigger**: Policy fields modified + +**Parameters**: `{ policy, updates }` + +**Implementation Example**: + +```typescript +export async function afterPolicyUpdated({ policy, updates }): Promise { + // Check if billing amount changed + if (updates.billing_amount) { + const customerId = policy.app_data?.stripe_customer_id; + const subscriptionId = policy.app_data?.stripe_subscription_id; + + if (subscriptionId) { + // Update subscription price + await updateStripeSubscriptionPrice( + subscriptionId, + policy.billing_amount + ); + } + } +} +``` + +#### afterPolicyCancelled() + +Called when policy is cancelled. + +**Trigger**: Policy cancellation + +**Parameters**: `{ policy }` + +**Implementation Example**: + +```typescript +export async function afterPolicyCancelled({ policy }): Promise { + const subscriptionId = policy.app_data?.stripe_subscription_id; + + if (subscriptionId) { + // Cancel Stripe subscription + await stripeClient.stripeSDK.subscriptions.cancel(subscriptionId); + } +} +``` + +#### afterPolicyExpired() + +Called when policy expires. + +**Trigger**: Policy expiration date passes + +**Parameters**: `{ policy }` + +#### afterPolicyLapsed() + +Called when policy lapses. + +**Trigger**: Policy goes into lapse + +**Parameters**: `{ policy }` + +### Payment Lifecycle + +#### afterPaymentCreated() + +Called when a payment is created. + +**Trigger**: New payment created on Root + +**Parameters**: `{ policy, payment }` + +**Implementation Example**: + +```typescript +export async function afterPaymentCreated({ policy, payment }): Promise { + const logService = getLogService(); + logService.info('Payment created', 'afterPaymentCreated', { + policyId: policy.policy_id, + paymentId: payment.payment_id, + }); + + // Get customer ID + const customerId = policy.app_data?.stripe_customer_id; + if (!customerId) { + throw new Error('Policy missing Stripe customer ID'); + } + + // Create payment intent on Stripe + const paymentIntent = await stripeClient.stripeSDK.paymentIntents.create({ + amount: payment.amount, + currency: policy.currency, + customer: customerId, + description: payment.description, + metadata: { + root_payment_id: payment.payment_id, + root_policy_id: policy.policy_id, + }, + confirm: true, + off_session: true, + }); + + logService.info('Payment intent created', 'afterPaymentCreated', { + paymentIntentId: paymentIntent.id, + }); +} +``` + +#### afterPaymentUpdated() + +Called when payment is updated. + +**Trigger**: Payment fields modified + +**Parameters**: `{ policy, payment }` + +### Alteration Lifecycle + +#### afterAlterationPackageApplied() + +Called after alteration package is applied. + +**Trigger**: Policy alteration processed + +**Parameters**: `{ policy, alteration_package, alteration_hook_key }` + +**Implementation Example**: + +```typescript +export async function afterAlterationPackageApplied({ + policy, + alteration_package, + alteration_hook_key, +}): Promise { + // Check if billing amount changed + if (alteration_package.changes.billing_amount) { + // Update Stripe subscription + await updateStripeSubscription(policy, alteration_package.changes); + } +} +``` + +## Hook Implementation Pattern + +### Basic Structure + +```typescript +export async function hookName(params: HookParams): Promise { + const logService = getLogService(); + + try { + // 1. Log entry + logService.info('Hook started', 'HookName', params); + + // 2. Validate inputs + if (!params.policy) { + throw new Error('Policy is required'); + } + + // 3. Perform operations + const result = await performOperation(params); + + // 4. Log success + logService.info('Hook completed', 'HookName', { result }); + + return result; + } catch (error: any) { + // 5. Log error + logService.error('Hook failed', 'HookName', params, error); + throw error; + } +} +``` + +### Using Services in Hooks + +```typescript +import { getLogService } from '../services/log-instance'; +import { RootService } from '../services/root.service'; +import StripeClient from '../clients/stripe-client'; + +export async function afterPolicyPaymentMethodAssigned({ policy }) { + const logService = getLogService(); + const rootService = new RootService(logService); + const stripeClient = new StripeClient(); + + // Use services for business logic + const paymentMethod = await rootClient.SDK.getPolicyPaymentMethod({ + policyId: policy.policy_id, + }); + const customer = await stripeClient.stripeSDK.customers.create({ ... }); + + await rootClient.SDK.updatePolicy({ + policyId: policy.policy_id, + body: { + app_data: { + stripe_customer_id: customer.id, + }, + }, + }); +} +``` + +## Testing Lifecycle Hooks + +Lifecycle hooks can be tested like any other function: + +```typescript +import { afterPolicyPaymentMethodAssigned } from '../lifecycle-hooks'; +import { getLogService } from '../services/log-instance'; + +jest.mock('../services/log-instance'); +jest.mock('../clients/stripe-client'); +jest.mock('../services/root.service'); + +describe('afterPolicyPaymentMethodAssigned', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should assign payment method to Stripe customer', async () => { + const policy = { + policy_id: 'policy_123', + app_data: { stripe_customer_id: 'cus_123' }, + }; + + await afterPolicyPaymentMethodAssigned({ policy }); + + expect(mockStripeClient.paymentMethods.attach).toHaveBeenCalledWith( + 'pm_123', + { customer: 'cus_123' } + ); + }); +}); +``` + +## Best Practices + +### Do ✅ + +- Log hook entry and exit +- Validate input parameters +- Handle errors gracefully +- Use services for business logic +- Keep hooks thin (orchestration only) +- Add error context +- Test with mocked dependencies + +### Don't ❌ + +- Put complex business logic in hooks +- Ignore errors +- Skip logging +- Make hooks stateful +- Hard-code configuration +- Skip input validation +- Create direct SDK calls (use clients) + +## Common Patterns + +### Idempotency + +```typescript +export async function afterPaymentCreated({ policy, payment }) { + // Check if already processed + const existingPaymentIntent = payment.metadata?.stripe_payment_intent_id; + if (existingPaymentIntent) { + logService.info('Payment already processed', 'afterPaymentCreated'); + return; + } + + // Process payment + const paymentIntent = await createPaymentIntent(payment); + + // Store reference to prevent re-processing + await updatePaymentMetadata(payment.payment_id, { + stripe_payment_intent_id: paymentIntent.id, + }); +} +``` + +### Error Recovery + +```typescript +export async function afterPolicyPaymentMethodAssigned({ policy }) { + try { + await assignPaymentMethod(policy); + } catch (error: any) { + logService.error('Failed to assign payment method', 'Hook', {}, error); + + // Don't throw - allow policy to continue + // Root Platform will retry later + return; + } +} +``` + +## Related Documentation + +- [Controllers Documentation](../controllers/README.md) - Similar patterns +- [Services Documentation](../services/README.md) - Business logic layer +- [Root Platform Docs](https://docs.root.co.za) - Official lifecycle hooks documentation +- [Webhooks](../../docs/WEBHOOKS.md) - Webhook vs lifecycle hooks + diff --git a/stripe_collection_module/code/lifecycle-hooks/index.ts b/stripe_collection_module/code/lifecycle-hooks/index.ts new file mode 100644 index 0000000..603b0d1 --- /dev/null +++ b/stripe_collection_module/code/lifecycle-hooks/index.ts @@ -0,0 +1,340 @@ +/** + * Lifecycle Hooks + * + * These are called by the Root platform at various points in the policy lifecycle. + * This is a simplified stub implementation with clear extension points. + * + * Full implementation will be added when the Stripe/Root integration is built. + */ + +import { RenderService } from '../services/render.service'; +import { getLogService } from '../services/log-instance'; +import { getConfigService } from '../services/config-instance'; +import StripeClient from '../clients/stripe-client'; + +const renderService = new RenderService(); +const stripeClient = new StripeClient(); + +/** + * Called after a policy is issued + * + * TODO: Implement policy issued logic + */ +export function afterPolicyIssued(): void { + // Stub implementation +} + +/** + * Create payment method - returns module data structure + * + * This is called when a setup intent is completed. + */ +export function createPaymentMethod({ + data, +}: { + data?: { setupIntent?: any }; +}): { module: any } { + const logService = getLogService(); + logService.info('Creating payment method', 'createPaymentMethod', data); + + if (data?.setupIntent) { + return { + module: { + id: data.setupIntent.id, + usage: data.setupIntent.usage, + object: data.setupIntent.object, + status: data.setupIntent.status, + livemode: data.setupIntent.livemode, + payment_method: data.setupIntent.payment_method, + }, + }; + } + + return { + module: data, + }; +} + +/** + * Render payment method creation form + * + * Returns HTML form with Stripe Elements for capturing payment details. + */ +export async function renderCreatePaymentMethod(): Promise { + const logService = getLogService(); + logService.info( + 'Rendering payment method creation form', + 'renderCreatePaymentMethod' + ); + + try { + // Create Stripe setup intent + const setupIntent = await stripeClient.stripeSDK.setupIntents.create({}); + + if (!setupIntent.client_secret) { + throw new Error('Setup intent client secret is missing'); + } + + // Render form using RenderService + const config = getConfigService(); + return renderService.renderCreatePaymentMethod({ + stripePublishableKey: config.get('stripePublishableKey'), + setupIntentClientSecret: setupIntent.client_secret, + }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + logService.error( + 'Error rendering payment method form', + 'renderCreatePaymentMethod', + {}, + error as Error + ); + throw new Error(`Error creating setup intent: ${errorMessage}`); + } +} + +/** + * Render payment method summary view (compact card) + * + * Returns HTML showing a brief summary of the payment method. + */ +export async function renderViewPaymentMethodSummary(params: { + payment_method: any; +}): Promise { + const logService = getLogService(); + logService.info( + 'Rendering payment method summary', + 'renderViewPaymentMethodSummary' + ); + + try { + const { payment_method: paymentMethod } = params; + + // Get payment method details from Stripe + const paymentMethodDetails = + await stripeClient.stripeSDK.paymentMethods.retrieve( + paymentMethod?.module?.payment_method as string + ); + + return renderService.renderViewPaymentMethodSummary({ + payment_method: paymentMethod, + paymentMethodDetails: { + card: paymentMethodDetails.card, + }, + }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + logService.error( + 'Error rendering payment method summary', + 'renderViewPaymentMethodSummary', + {}, + error as Error + ); + throw new Error(`Error retrieving payment method: ${errorMessage}`); + } +} + +/** + * Render full payment method details view + * + * Returns HTML showing comprehensive payment method information. + */ +export function renderViewPaymentMethod(params: any): string { + const logService = getLogService(); + logService.info( + 'Rendering payment method details', + 'renderViewPaymentMethod' + ); + + const { payment_method: paymentMethod, policy } = params; + + return renderService.renderViewPaymentMethod({ + payment_method: paymentMethod, + policy, + }); +} + +/** + * Called after payment method is assigned to a policy + * + * TODO: Implement full payment method assignment workflow: + * - Get or create Stripe customer + * - Attach payment method to customer + * - Create or update subscription + */ +export function afterPolicyPaymentMethodAssigned({ + policy, +}: { + policy: any; +}): void { + const logService = getLogService(); + logService.info( + 'Payment method assigned to policy', + 'afterPolicyPaymentMethodAssigned', + { + policyId: policy.policy_id, + } + ); + + // TODO: Implement payment method assignment logic + // This will use PaymentMethodService to orchestrate the workflow +} + +/** + * Called after a payment is created + * + * TODO: Implement payment creation handling: + * - Create Stripe payment intent + * - Link to Root payment + */ +export function afterPaymentCreated({ + policy, + payment, +}: { + policy: any; + payment: any; +}): void { + const logService = getLogService(); + logService.info('Payment created', 'afterPaymentCreated', { + policyId: policy.policy_id, + paymentId: payment.payment_id, + }); + + // TODO: Implement payment creation logic +} + +/** + * Called after a payment is updated + * + * TODO: Implement payment update handling + */ +export function afterPaymentUpdated({ + policy, + payment, +}: { + policy: any; + payment: any; +}): void { + const logService = getLogService(); + logService.info('Payment updated', 'afterPaymentUpdated', { + policyId: policy.policy_id, + paymentId: payment.payment_id, + }); + + // TODO: Implement payment update logic +} + +/** + * Called after payment method is removed from policy + * + * TODO: Implement payment method removal: + * - Cancel subscriptions + * - Clean up Stripe resources + */ +export function afterPaymentMethodRemoved({ policy }: { policy: any }): void { + const logService = getLogService(); + logService.info('Payment method removed', 'afterPaymentMethodRemoved', { + policyId: policy.policy_id, + }); + + // TODO: Implement payment method removal logic +} + +/** + * Called after policy is cancelled + * + * TODO: Implement policy cancellation: + * - Cancel Stripe subscriptions + * - Handle refunds if needed + */ +export function afterPolicyCancelled({ policy }: { policy: any }): void { + const logService = getLogService(); + logService.info('Policy cancelled', 'afterPolicyCancelled', { + policyId: policy.policy_id, + }); + + // TODO: Implement policy cancellation logic +} + +/** + * Called after policy expires + * + * TODO: Implement policy expiration handling + */ +export function afterPolicyExpired({ policy }: { policy: any }): void { + const logService = getLogService(); + logService.info('Policy expired', 'afterPolicyExpired', { + policyId: policy.policy_id, + }); + + // TODO: Implement policy expiration logic +} + +/** + * Called after policy lapses + * + * TODO: Implement policy lapse handling + */ +export function afterPolicyLapsed({ policy }: { policy: any }): void { + const logService = getLogService(); + logService.info('Policy lapsed', 'afterPolicyLapsed', { + policyId: policy.policy_id, + }); + + // TODO: Implement policy lapse logic +} + +/** + * Called after policy is updated + * + * TODO: Implement policy update handling: + * - Update subscription amounts + * - Handle proration + */ +export function afterPolicyUpdated({ + policy, + updates, +}: { + policy: any; + updates: any; +}): void { + const logService = getLogService(); + logService.info('Policy updated', 'afterPolicyUpdated', { + policyId: policy.policy_id, + updates, + }); + + // TODO: Implement policy update logic +} + +/** + * Called after alteration package is applied + * + * TODO: Implement alteration handling: + * - Update subscription pricing + * - Handle billing frequency changes + */ +export function afterAlterationPackageApplied({ + policy, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + alteration_package, + alteration_hook_key, +}: { + policy: any; + alteration_package: any; + alteration_hook_key: string; +}): void { + const logService = getLogService(); + logService.info( + 'Alteration package applied', + 'afterAlterationPackageApplied', + { + policyId: policy.policy_id, + alterationHookKey: alteration_hook_key, + } + ); + + // TODO: Implement alteration logic +} diff --git a/stripe_collection_module/code/main.ts b/stripe_collection_module/code/main.ts index bd323f9..0ad666e 100644 --- a/stripe_collection_module/code/main.ts +++ b/stripe_collection_module/code/main.ts @@ -1,2 +1,16 @@ -export * from './lifecycle-hooks'; +/** + * Collection Module Main Entry Point + * + * This is the main entry point for the collection module. + * It initializes the DI container and exports lifecycle hooks. + */ + +import { getContainer } from './core/container.setup'; + +// Initialize the DI container on module load +// This sets up all services including ConfigurationService and LogService +getContainer(); + +// Export lifecycle hooks and webhook handlers +export * from './lifecycle-hooks/'; export * from './webhook-hooks'; diff --git a/stripe_collection_module/code/sample.env.ts b/stripe_collection_module/code/sample.env.ts deleted file mode 100644 index 73f532e..0000000 --- a/stripe_collection_module/code/sample.env.ts +++ /dev/null @@ -1,24 +0,0 @@ -export const NODE_ENV = 'development'; - -export const STRIPE_WEBHOOK_SIGNING_SECRET_LIVE = 'whsec_.....'; -export const STRIPE_WEBHOOK_SIGNING_SECRET_TEST = 'whsec_.....'; - -export const STRIPE_PRODUCT_ID_LIVE = 'prod_.....'; -export const STRIPE_PRODUCT_ID_TEST = 'prod_.....'; - -export const TIME_DELAY_IN_MILLISECONDS = '10000'; - -export const STRIPE_PUBLISHABLE_KEY_LIVE = 'pk_live_.....'; -export const STRIPE_PUBLISHABLE_KEY_TEST = 'pk_test_.....'; - -export const STRIPE_API_KEY_LIVE = 'sk_live_.....'; -export const STRIPE_API_KEY_TEST = 'sk_test_.....'; - -export const ROOT_COLLECTION_MODULE_KEY = 'cm_stripe_admiral_business'; - -export const ROOT_API_KEY_LIVE = 'production_.....'; -export const ROOT_API_KEY_SANDBOX = 'sandbox_.....'; -export const ROOT_BASE_URL_LIVE = - 'https://api.uk.rootplatform.com/v1/insurance'; -export const ROOT_BASE_URL_SANDBOX = - 'https://sandbox.uk.rootplatform.com/v1/insurance'; diff --git a/stripe_collection_module/code/services/README.md b/stripe_collection_module/code/services/README.md new file mode 100644 index 0000000..26193cd --- /dev/null +++ b/stripe_collection_module/code/services/README.md @@ -0,0 +1,516 @@ +# Services + +This directory contains business logic services that orchestrate operations across clients, adapters, and utilities. Services form the core business layer of the application. + +## Architecture + +``` +Controllers → Services → Clients +``` + +Services encapsulate business logic and: +- Coordinate multiple operations +- Transform data between systems +- Implement business rules +- Handle complex workflows +- Testable via dependency injection + +## Available Services + +### LogService + +Structured JSON logging to stdout for log aggregation systems. + +**Location**: `log.service.ts` + +**Key Features**: +- Multiple log levels (DEBUG, INFO, WARN, ERROR) +- Correlation IDs for request tracking +- Structured JSON output +- Environment context + +**Usage**: + +```typescript +import { getLogService } from '../services/log-instance'; + +const logService = getLogService(); + +// Basic logging +logService.info('Operation completed', 'MyService'); +logService.error('Operation failed', 'MyService', {}, error); + +// With metadata +logService.info('Payment created', 'PaymentService', { + paymentId: 'payment_123', + amount: 10000, +}); + +// With correlation ID +const correlationId = logService.generateCorrelationId(); +logService.info('Processing request', 'WebhookHandler'); +// ... all subsequent logs will include this correlation ID +logService.clearCorrelationId(); +``` + +**Configuration**: + +```typescript +new LogService({ + environment: 'production', + minLogLevel: LogLevel.INFO, // Filter out DEBUG logs +}); +``` + +### ConfigurationService + +Type-safe, validated configuration management with environment-specific settings. + +**Location**: `config.service.ts` + +**Key Features**: +- Environment-specific configs (production/development) +- Validation on startup +- Type-safe access +- Injectable for testing + +**Usage**: + +```typescript +import { getConfigService } from '../services/config-instance'; + +const config = getConfigService(); + +// Get individual config values +const apiKey = config.get('stripeSecretKey'); +const baseUrl = config.get('rootBaseUrl'); + +// Check environment +if (config.isProduction()) { + // Production-specific logic +} + +// Get all config +const allConfig = config.getAll(); +``` + +**Available Configuration**: +- `stripeSecretKey` +- `stripePublishableKey` +- `stripeWebhookSigningSecret` +- `stripeProductId` +- `rootApiKey` +- `rootBaseUrl` +- `rootCollectionModuleKey` +- `environment` + +### RenderService + +HTML generation for Root Platform dashboard views. + +**Location**: `render.service.ts` + +**Key Features**: +- Payment method forms with Stripe Elements +- Payment method summary views +- Payment method detail views +- XSS protection via HTML escaping + +**Usage**: + +```typescript +import { RenderService } from '../services/render.service'; + +const renderService = new RenderService(); + +// Render payment method creation form +const html = renderService.renderCreatePaymentMethod({ + stripePublishableKey: 'pk_test_...', + setupIntentClientSecret: 'seti_...', +}); + +// Render payment method summary +const summaryHtml = renderService.renderViewPaymentMethodSummary({ + payment_method: paymentMethod, + paymentMethodDetails: { + card: { + brand: 'visa', + last4: '4242', + exp_month: 12, + exp_year: 2025, + }, + }, +}); + +// Render full payment method view +const detailHtml = renderService.renderViewPaymentMethod({ + payment_method: paymentMethod, + policy: policy, +}); +``` + +### RootService + +Business logic for Root Platform operations. + +**Location**: `root.service.ts` + +**Key Features**: +- Policy management +- Payment operations +- Type-safe wrappers around Root SDK +- Error handling and logging + +**Usage**: + +```typescript +import { RootService } from '../services/root.service'; +import { PaymentStatus } from '@rootplatform/node-sdk'; + +const rootService = new RootService(logService); + +// Get policy +const policy = await rootService.getPolicy('policy_123'); + +// Update payment status +await rootService.updatePaymentStatus({ + paymentId: 'payment_456', + status: PaymentStatus.Successful, +}); +``` + +### StripeService + +Business logic for Stripe operations. + +**Location**: `stripe.service.ts` + +**Key Features**: +- Customer management (create, get, update) +- Payment intent creation +- Payment method operations (get, attach) +- Subscription management +- Type-safe wrappers around Stripe SDK +- Error handling and logging + +**Usage**: + +```typescript +import { StripeService } from '../services/stripe.service'; + +const stripeService = new StripeService(logService); + +// Create customer +const customer = await stripeService.createCustomer({ + email: 'customer@example.com', + name: 'John Doe', + metadata: { root_policy_id: 'policy_123' }, +}); + +// Create payment intent +const paymentIntent = await stripeService.createPaymentIntent({ + amount: 10000, + currency: 'zar', + customerId: customer.id, + description: 'Premium payment', + metadata: { root_payment_id: 'payment_456' }, + confirm: true, + offSession: true, +}); + +// Attach payment method to customer +const paymentMethod = await stripeService.attachPaymentMethod({ + paymentMethodId: 'pm_123', + customerId: customer.id, +}); + +// Update customer +await stripeService.updateCustomer(customer.id, { + invoice_settings: { + default_payment_method: 'pm_123', + }, +}); + +// Cancel subscription +await stripeService.cancelSubscription('sub_123'); +``` + +## Service Helper Instances + +For convenience, some services provide instance helper functions: + +### log-instance.ts + +```typescript +import { getLogService } from '../services/log-instance'; + +// Get singleton instance +const logService = getLogService(); + +// Check if initialized +if (isLogServiceInitialized()) { + // Use log service +} +``` + +### config-instance.ts + +```typescript +import { getConfigService } from '../services/config-instance'; + +// Get singleton instance +const config = getConfigService(); + +// Check if initialized +if (isConfigServiceInitialized()) { + // Use config service +} +``` + +## Creating a New Service + +When you need to add new business logic: + +### 1. Create Service File + +```typescript +// services/my-feature.service.ts +import { LogService } from './log.service'; +import StripeClient from '../clients/stripe-client'; +import { RootService } from './root.service'; + +export class MyFeatureService { + constructor( + private readonly logService: LogService, + private readonly stripeClient: StripeClient, + private readonly rootService: RootService + ) {} + + async performOperation(params: OperationParams): Promise { + this.logService.info('Starting operation', 'MyFeatureService', params); + + try { + // 1. Fetch data from Stripe + const stripeData = await this.stripeClient.stripeSDK.someMethod(); + + // 2. Transform data + const transformed = this.transformData(stripeData); + + // 3. Update Root + await this.rootService.updateSomething(transformed); + + this.logService.info('Operation completed', 'MyFeatureService'); + return result; + } catch (error: any) { + this.logService.error( + 'Operation failed', + 'MyFeatureService', + { params }, + error + ); + throw error; + } + } + + private transformData(data: any) { + // Business logic here + return transformed; + } +} +``` + +### 2. Register in DI Container + +```typescript +// core/container.setup.ts +container.register( + ServiceToken.MY_FEATURE_SERVICE, + (c) => { + const logService = c.resolve(ServiceToken.LOG_SERVICE); + const rootService = c.resolve(ServiceToken.ROOT_SERVICE); + const stripeClient = new StripeClient(); + + const { MyFeatureService } = require('../services/my-feature.service'); + return new MyFeatureService(logService, stripeClient, rootService); + }, + ServiceLifetime.SINGLETON +); +``` + +### 3. Use in Controllers + +```typescript +import { getContainer } from '../core/container.setup'; +import { ServiceToken } from '../core/container'; + +const container = getContainer(); +const myFeatureService = container.resolve(ServiceToken.MY_FEATURE_SERVICE); + +await myFeatureService.performOperation(params); +``` + +## Testing Services + +Services are designed for easy testing with dependency injection: + +```typescript +import { MyFeatureService } from '../services/my-feature.service'; +import { LogService } from '../services/log.service'; + +describe('MyFeatureService', () => { + let service: MyFeatureService; + let mockLogService: jest.Mocked; + let mockStripeClient: jest.Mocked; + let mockRootService: jest.Mocked; + + beforeEach(() => { + // Create mocks + mockLogService = { + info: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + } as any; + + mockStripeClient = { + stripeSDK: { + customers: { + retrieve: jest.fn(), + }, + }, + } as any; + + mockRootService = { + updatePaymentStatus: jest.fn(), + } as any; + + // Inject mocks + service = new MyFeatureService( + mockLogService, + mockStripeClient, + mockRootService + ); + }); + + it('should perform operation successfully', async () => { + // Arrange + mockStripeClient.stripeSDK.customers.retrieve.mockResolvedValue({ + id: 'cus_123', + }); + + // Act + const result = await service.performOperation({ id: '123' }); + + // Assert + expect(result).toBeDefined(); + expect(mockLogService.info).toHaveBeenCalledWith( + 'Starting operation', + 'MyFeatureService', + { id: '123' } + ); + }); + + it('should handle errors gracefully', async () => { + // Arrange + const error = new Error('API failed'); + mockStripeClient.stripeSDK.customers.retrieve.mockRejectedValue(error); + + // Act & Assert + await expect(service.performOperation({ id: '123' })).rejects.toThrow(); + expect(mockLogService.error).toHaveBeenCalled(); + }); +}); +``` + +## Best Practices + +### Do ✅ + +- Inject all dependencies via constructor +- Use LogService for all logging +- Return domain objects, not raw API responses +- Handle errors with context +- Keep services focused (single responsibility) +- Write unit tests with mocked dependencies +- Document public methods with JSDoc + +### Don't ❌ + +- Create dependencies with `new` inside service (except clients) +- Mix infrastructure concerns with business logic +- Catch errors without logging +- Skip input validation +- Create global state/singletons (use DI container) +- Expose internal implementation details +- Skip error handling + +## Service Patterns + +### Orchestration Pattern + +```typescript +// Service coordinates multiple operations +async createCustomerAndSubscription(policy: Policy) { + // 1. Create customer + const customer = await this.stripeClient.stripeSDK.customers.create({ + email: policy.email, + }); + + // 2. Attach payment method + await this.stripeClient.stripeSDK.paymentMethods.attach( + paymentMethodId, + { customer: customer.id } + ); + + // 3. Create subscription + const subscription = await this.stripeClient.stripeSDK.subscriptions.create({ + customer: customer.id, + items: [{ price: priceId }], + }); + + // 4. Update Root + await this.rootService.updatePolicyAppData(policy.policy_id, { + stripe_customer_id: customer.id, + stripe_subscription_id: subscription.id, + }); + + return { customer, subscription }; +} +``` + +### Transformation Pattern + +```typescript +// Service transforms data between systems +async syncPaymentStatus(stripeInvoice: Stripe.Invoice, rootPaymentId: string) { + // Transform Stripe data to Root format + const status = this.mapStripeStatusToRoot(stripeInvoice.status); + const failureReason = stripeInvoice.last_finalization_error?.message; + + // Update Root + await this.rootService.updatePaymentStatus({ + paymentId: rootPaymentId, + status, + failureReason, + }); +} + +private mapStripeStatusToRoot(stripeStatus: string): PaymentStatus { + const mapping = { + paid: PaymentStatus.Successful, + open: PaymentStatus.Pending, + void: PaymentStatus.Cancelled, + uncollectible: PaymentStatus.Failed, + }; + return mapping[stripeStatus] || PaymentStatus.Failed; +} +``` + +## Related Documentation + +- [Clients Documentation](../clients/README.md) - Using API clients +- [Controllers Documentation](../controllers/README.md) - Calling services from controllers +- [Core/DI Container](../core/README.md) - Dependency injection patterns +- [Testing Guide](../../docs/TESTING.md) - Testing services +- [Best Practices](../../docs/BEST_PRACTICES.md) - Service design patterns + diff --git a/stripe_collection_module/code/services/config-instance.ts b/stripe_collection_module/code/services/config-instance.ts new file mode 100644 index 0000000..72b30e5 --- /dev/null +++ b/stripe_collection_module/code/services/config-instance.ts @@ -0,0 +1,43 @@ +/** + * Configuration Instance Helper + * + * Provides easy access to the ConfigurationService from the DI container. + * This is a convenience wrapper similar to log-instance.ts + */ + +import { getContainer } from '../core/container.setup'; +import { ServiceToken } from '../core/container'; +import { ConfigurationService } from './config.service'; + +/** + * Get the ConfigurationService instance from the container + * + * @returns ConfigurationService instance + * @throws Error if container is not initialized + * + * @example + * ```typescript + * import { getConfigService } from './services/config-instance'; + * + * const config = getConfigService(); + * const apiKey = config.get('stripeSecretKey'); + * ``` + */ +export function getConfigService(): ConfigurationService { + const container = getContainer(); + return container.resolve(ServiceToken.CONFIG_SERVICE); +} + +/** + * Check if ConfigurationService is initialized in the container + * + * @returns true if ConfigurationService is available + */ +export function isConfigServiceInitialized(): boolean { + try { + const container = getContainer(); + return container.has(ServiceToken.CONFIG_SERVICE); + } catch { + return false; + } +} diff --git a/stripe_collection_module/code/services/config.service.ts b/stripe_collection_module/code/services/config.service.ts new file mode 100644 index 0000000..1048172 --- /dev/null +++ b/stripe_collection_module/code/services/config.service.ts @@ -0,0 +1,213 @@ +/** + * ConfigurationService - Injectable configuration management + * + * This service provides type-safe access to environment-specific configuration. + * It validates configuration on initialization and throws errors for missing values. + */ + +import * as env from '../env'; + +export interface EnvironmentConfig { + timeDelayInMilliseconds: string; + environment: Environment; + stripeWebhookSigningSecret: string; + stripeProductId: string; + rootCollectionModuleKey: string; + stripePublishableKey: string; + stripeSecretKey: string; + rootApiKey: string; + rootBaseUrl: string; +} + +export interface ConfigMap { + production: EnvironmentConfig; + development: EnvironmentConfig; +} + +export interface ConfigurationServiceOptions { + environment?: string; + skipValidation?: boolean; // For testing +} + +export enum Environment { + PRODUCTION = 'production', + DEVELOPMENT = 'development', +} + +/** + * ConfigurationService - Manages environment-specific configuration + * + * @example + * ```typescript + * const config = new ConfigurationService(); + * const apiKey = config.get('stripeSecretKey'); + * const isProduction = config.isProduction(); + * ``` + */ +export class ConfigurationService { + private readonly configs: ConfigMap; + private readonly environment: string; + private readonly currentConfig: EnvironmentConfig; + + constructor(options: ConfigurationServiceOptions = {}) { + this.environment = + options.environment || process.env.ENVIRONMENT || Environment.DEVELOPMENT; + this.configs = this.buildConfigMap(); + + if (!options.skipValidation) { + this.validateEnvironment(); + } + + this.currentConfig = this.configs[this.environment as keyof ConfigMap]; + } + + /** + * Build configuration map from environment variables + */ + private buildConfigMap(): ConfigMap { + const baseConfig = { + timeDelayInMilliseconds: env.TIME_DELAY_IN_MILLISECONDS, + rootCollectionModuleKey: env.ROOT_COLLECTION_MODULE_KEY, + environment: this.environment, + }; + + const production: EnvironmentConfig = { + ...baseConfig, + environment: Environment.PRODUCTION, + stripeWebhookSigningSecret: env.STRIPE_WEBHOOK_SIGNING_SECRET_LIVE, + stripePublishableKey: env.STRIPE_PUBLISHABLE_KEY_LIVE, + stripeSecretKey: env.STRIPE_SECRET_KEY_LIVE, + stripeProductId: env.STRIPE_PRODUCT_ID_LIVE, + rootApiKey: env.ROOT_API_KEY_LIVE, + rootBaseUrl: env.ROOT_BASE_URL_LIVE, + }; + + const development: EnvironmentConfig = { + ...baseConfig, + environment: Environment.DEVELOPMENT, + stripeWebhookSigningSecret: env.STRIPE_WEBHOOK_SIGNING_SECRET_TEST, + stripePublishableKey: env.STRIPE_PUBLISHABLE_KEY_TEST, + stripeSecretKey: env.STRIPE_SECRET_KEY_TEST, + stripeProductId: env.STRIPE_PRODUCT_ID_TEST, + rootApiKey: env.ROOT_API_KEY_SANDBOX, + rootBaseUrl: env.ROOT_BASE_URL_SANDBOX, + }; + + return { + production, + development, + }; + } + + /** + * Validate that environment is set correctly and all config values are present + */ + private validateEnvironment(): void { + if (!this.environment) { + throw new Error( + 'ENVIRONMENT is not set. Set ENVIRONMENT=production or ENVIRONMENT=development' + ); + } + + const validEnvironments = Object.keys(this.configs); + + if (!validEnvironments.includes(this.environment)) { + throw new Error( + `Invalid ENVIRONMENT: ${ + this.environment + }. Valid values: ${validEnvironments.join(', ')}` + ); + } + + const config = this.configs[this.environment as keyof ConfigMap]; + const missingKeys: string[] = []; + const invalidUrls: string[] = []; + + // Check for missing values + for (const [key, value] of Object.entries(config)) { + if (value === '' || value === null || value === undefined) { + missingKeys.push(key); + } + } + + // Validate URL formats + if (config.rootBaseUrl && !this.isValidUrl(config.rootBaseUrl)) { + invalidUrls.push(`rootBaseUrl: "${config.rootBaseUrl}"`); + } + + // Combine validation errors + const errors: string[] = []; + + if (missingKeys.length > 0) { + errors.push( + `Missing required configuration values for ${ + this.environment + }: ${missingKeys.join(', ')}`, + 'Hint: Check that code/env.ts is properly configured with all required values.', + 'For AWS Lambda: Ensure all environment variables are set in Lambda configuration.' + ); + } + + if (invalidUrls.length > 0) { + errors.push(`Invalid URL format(s): ${invalidUrls.join(', ')}`); + } + + if (errors.length > 0) { + throw new Error(`Configuration validation failed:\n${errors.join('\n')}`); + } + } + + /** + * Validate URL format + */ + private isValidUrl(url: string): boolean { + try { + const parsed = new URL(url); + return ['http:', 'https:'].includes(parsed.protocol); + } catch { + return false; + } + } + + /** + * Get a configuration value by key + */ + public get(key: K): EnvironmentConfig[K] { + return this.currentConfig[key]; + } + + /** + * Get the entire current configuration object + */ + public getAll(): EnvironmentConfig { + return { ...this.currentConfig }; + } + + /** + * Get the current environment name + */ + public getEnvironment(): string { + return this.environment; + } + + /** + * Check if running in production + */ + public isProduction(): boolean { + return this.environment === Environment.PRODUCTION.toString(); + } + + /** + * Check if running in development + */ + public isDevelopment(): boolean { + return this.environment === Environment.DEVELOPMENT.toString(); + } + + /** + * Get time delay in milliseconds as a number + */ + public getTimeDelayMs(): number { + return parseInt(this.currentConfig.timeDelayInMilliseconds, 10); + } +} diff --git a/stripe_collection_module/code/services/log-instance.ts b/stripe_collection_module/code/services/log-instance.ts new file mode 100644 index 0000000..ca3cec4 --- /dev/null +++ b/stripe_collection_module/code/services/log-instance.ts @@ -0,0 +1,43 @@ +/** + * LogService Instance Helper + * + * Provides easy access to the LogService from the DI container. + * The LogService is automatically initialized when the container is created. + */ + +import { getContainer } from '../core/container.setup'; +import { ServiceToken } from '../core/container'; +import { LogService } from './log.service'; + +/** + * Get the LogService instance from the container + * + * @returns LogService instance + * @throws Error if container is not initialized + * + * @example + * ```typescript + * import { getLogService } from './services/log-instance'; + * + * const logService = getLogService(); + * logService.info('Hello, world!'); + * ``` + */ +export function getLogService(): LogService { + const container = getContainer(); + return container.resolve(ServiceToken.LOG_SERVICE); +} + +/** + * Check if LogService is initialized in the container + * + * @returns true if LogService is available + */ +export function isLogServiceInitialized(): boolean { + try { + const container = getContainer(); + return container.has(ServiceToken.LOG_SERVICE); + } catch { + return false; + } +} diff --git a/stripe_collection_module/code/services/log.service.ts b/stripe_collection_module/code/services/log.service.ts new file mode 100644 index 0000000..09310a8 --- /dev/null +++ b/stripe_collection_module/code/services/log.service.ts @@ -0,0 +1,212 @@ +/** + * LogService - Structured logging system + * + * Features: + * - Structured JSON output to stdout (for DataDog/CloudWatch) + * - Correlation IDs for request tracking + * - Multiple log levels + */ + +import { randomUUID } from 'crypto'; + +export enum LogLevel { + DEBUG = 'DEBUG', + INFO = 'INFO', + WARN = 'WARN', + ERROR = 'ERROR', +} + +export interface LogEntry { + timestamp: string; + level: LogLevel; + message: string; + context?: string; + correlationId?: string; + metadata?: Record; + error?: { + message: string; + stack?: string; + name?: string; + }; +} + +export interface LogServiceConfig { + environment: string; + minLogLevel?: LogLevel; // Minimum log level to output +} + +export class LogService { + private readonly minLogLevel: LogLevel; + private readonly environment: string; + private currentCorrelationId: string | null = null; + + // Map log levels to numeric priorities for comparison + private static readonly LOG_LEVEL_PRIORITY: Record = { + [LogLevel.DEBUG]: 0, + [LogLevel.INFO]: 1, + [LogLevel.WARN]: 2, + [LogLevel.ERROR]: 3, + }; + + constructor(config: LogServiceConfig) { + this.environment = config.environment; + this.minLogLevel = config.minLogLevel || LogLevel.DEBUG; + } + + /** + * Set correlation ID for the current request/operation + */ + public setCorrelationId(correlationId: string): void { + this.currentCorrelationId = correlationId; + } + + /** + * Generate and set a new correlation ID + */ + public generateCorrelationId(): string { + const correlationId = randomUUID(); + this.currentCorrelationId = correlationId; + return correlationId; + } + + /** + * Clear the current correlation ID + */ + public clearCorrelationId(): void { + this.currentCorrelationId = null; + } + + /** + * Get the current correlation ID + */ + public getCorrelationId(): string | null { + return this.currentCorrelationId; + } + + /** + * Log a debug message + */ + public debug( + message: string, + context?: string, + metadata?: Record + ): void { + this.log(LogLevel.DEBUG, message, context, metadata); + } + + /** + * Log an info message + */ + public info( + message: string, + context?: string, + metadata?: Record + ): void { + this.log(LogLevel.INFO, message, context, metadata); + } + + /** + * Log a warning message + */ + public warn( + message: string, + context?: string, + metadata?: Record + ): void { + this.log(LogLevel.WARN, message, context, metadata); + } + + /** + * Log an error message + */ + public error( + message: string, + context?: string, + metadata?: Record, + error?: Error + ): void { + const errorData = error + ? { + message: error.message, + stack: error.stack, + name: error.name, + } + : undefined; + + this.log(LogLevel.ERROR, message, context, metadata, errorData); + } + + /** + * Core logging method + */ + private log( + level: LogLevel, + message: string, + context?: string, + metadata?: Record, + error?: { message: string; stack?: string; name?: string } + ): void { + // Check if this log level should be output + if (!this.shouldLog(level)) { + return; + } + + const entry: LogEntry = { + timestamp: new Date().toISOString(), + level, + message, + context, + correlationId: this.currentCorrelationId || undefined, + metadata, + error, + }; + + // Output to stdout as JSON (for DataDog/CloudWatch) + this.outputToStdout(entry); + } + + /** + * Check if a log level should be output + */ + private shouldLog(level: LogLevel): boolean { + return ( + LogService.LOG_LEVEL_PRIORITY[level] >= + LogService.LOG_LEVEL_PRIORITY[this.minLogLevel] + ); + } + + /** + * Output log entry to stdout as structured JSON + */ + private outputToStdout(entry: LogEntry): void { + const output = { + timestamp: entry.timestamp, + level: entry.level, + environment: this.environment, + message: entry.message, + ...(entry.context && { context: entry.context }), + ...(entry.correlationId && { correlationId: entry.correlationId }), + ...(entry.metadata && { metadata: entry.metadata }), + ...(entry.error && { error: entry.error }), + }; + + // Use console methods based on log level + switch (entry.level) { + case LogLevel.DEBUG: + console.debug(JSON.stringify(output)); + break; + case LogLevel.INFO: + console.info(JSON.stringify(output)); + break; + case LogLevel.WARN: + console.warn(JSON.stringify(output)); + break; + case LogLevel.ERROR: + console.error(JSON.stringify(output)); + break; + default: + console.log(JSON.stringify(output)); + break; + } + } +} diff --git a/stripe_collection_module/code/services/render.service.ts b/stripe_collection_module/code/services/render.service.ts new file mode 100644 index 0000000..acf016e --- /dev/null +++ b/stripe_collection_module/code/services/render.service.ts @@ -0,0 +1,353 @@ +/** + * RenderService - Handles HTML rendering for dashboard views + * + * This service consolidates all HTML rendering logic used by lifecycle hooks + * to display UI elements on the Root dashboard. + */ + +export interface RenderPaymentMethodParams { + stripePublishableKey: string; + setupIntentClientSecret: string; +} + +export interface ViewPaymentMethodParams { + payment_method: { + collection_module_key: string; + module: { + id: string; + payment_method: string; + livemode: boolean; + status: string; + usage: string; + }; + }; + policy: { + billing_day: number; + }; +} + +export interface ViewPaymentMethodSummaryParams { + payment_method: { + module: { + payment_method: string; + }; + }; + paymentMethodDetails?: { + card?: { + brand: string; + last4: string; + exp_month: number; + exp_year: number; + }; + }; +} + +export class RenderService { + /** + * Common CSS styles used across all renders + */ + private readonly commonStyles = ` + .api-attributes { + font-size: 9px; + } + .api-attributes-wrapper { + display: none; + } + `; + + /** + * Render the payment method creation form with Stripe Elements + * + * This creates a form that allows users to enter their payment details. + * Uses Stripe Elements for PCI compliance. + */ + renderCreatePaymentMethod(params: RenderPaymentMethodParams): string { + const { stripePublishableKey, setupIntentClientSecret } = params; + + return ` + + + + + + + + +
+
+
+
+
+ + +`; + } + + /** + * Render payment method summary view (compact card view) + * + * Displays a brief summary of the payment method, typically used in lists. + */ + renderViewPaymentMethodSummary( + params: ViewPaymentMethodSummaryParams + ): string { + const { paymentMethodDetails } = params; + + const brand = paymentMethodDetails?.card?.brand || 'Unknown'; + const last4 = paymentMethodDetails?.card?.last4 || 'Unknown'; + const expMonth = paymentMethodDetails?.card?.exp_month || 'Unknown'; + const expYear = paymentMethodDetails?.card?.exp_year || 'Unknown'; + + return ` + + + + + + + + +
+

Stripe payment method

+
+ Card: ${this.escapeHtml(brand)} **** **** **** ${this.escapeHtml( + last4 + )}, + Expires: ${this.escapeHtml(String(expMonth))}/${this.escapeHtml( + String(expYear) + )} +
+
+ +`; + } + + /** + * Render full payment method details view + * + * Displays comprehensive information about the payment method. + */ + renderViewPaymentMethod(params: ViewPaymentMethodParams): string { + const { payment_method: paymentMethod, policy } = params; + + return ` + + + + + Payment Method Details + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeCollection module
Key${this.escapeHtml( + paymentMethod.collection_module_key + )}
Id${this.escapeHtml(paymentMethod.module.id)}
Payment method${this.escapeHtml(paymentMethod.module.payment_method)}
Billing day${this.escapeHtml(String(policy.billing_day))}
Livemode${this.escapeHtml(String(paymentMethod.module.livemode))}
Status${this.escapeHtml(paymentMethod.module.status)}
Usage${this.escapeHtml(paymentMethod.module.usage)}
+ + +`; + } + + /** + * Escape HTML to prevent XSS + */ + private escapeHtml(text: string): string { + const map: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }; + return text.replaceAll(/[&<>"']/g, (m) => map[m]); + } + + /** + * Escape JavaScript strings for safe embedding in HTML + */ + private escapeJs(text: string): string { + return text + .replaceAll('\\', '\\\\') + .replaceAll("'", String.raw`\'`) + .replaceAll('"', String.raw`\"`) + .replaceAll('\n', String.raw`\n`) + .replaceAll('\r', String.raw`\r`) + .replaceAll('\t', String.raw`\t`); + } +} diff --git a/stripe_collection_module/code/services/root.service.ts b/stripe_collection_module/code/services/root.service.ts new file mode 100644 index 0000000..c34f2ed --- /dev/null +++ b/stripe_collection_module/code/services/root.service.ts @@ -0,0 +1,79 @@ +/** + * RootService - Business logic for Root Platform operations + * + * This service provides a high-level interface for Root Platform operations. + * It wraps the Root client and provides domain-specific methods. + */ + +import * as root from '@rootplatform/node-sdk'; +import { LogService } from './log.service'; +import RootClient from '../clients/root-client'; + +export interface UpdatePaymentStatusParams { + paymentId: string; + status: root.PaymentStatus; + failureReason?: string; + failureAction?: root.FailureAction; +} + +export class RootService { + constructor( + private readonly logService: LogService, + private readonly rootClient: typeof RootClient + ) {} + + /** + * Get a policy by ID + */ + async getPolicy(policyId: string): Promise { + this.logService.debug(`Getting policy: ${policyId}`, 'RootService'); + + try { + const result = await this.rootClient.SDK.getPolicyById({ policyId }); + return result; + } catch (error: any) { + this.logService.error( + `Failed to get policy: ${error.message}`, + 'RootService', + { policyId, error } + ); + throw error; + } + } + + /** + * Update payment status on Root Platform + */ + async updatePaymentStatus(params: UpdatePaymentStatusParams): Promise { + this.logService.info('Updating payment status', 'RootService', params); + + try { + await this.rootClient.SDK.updatePaymentsAsync({ + paymentUpdates: [ + { + payment_id: params.paymentId, + status: params.status, + failure_reason: params.failureReason, + failure_action: params.failureAction, + }, + ], + }); + + this.logService.info( + 'Payment status updated successfully', + 'RootService', + { + paymentId: params.paymentId, + status: params.status, + } + ); + } catch (error: any) { + this.logService.error( + `Failed to update payment status: ${error.message}`, + 'RootService', + { params, error } + ); + throw error; + } + } +} diff --git a/stripe_collection_module/code/services/stripe.service.ts b/stripe_collection_module/code/services/stripe.service.ts new file mode 100644 index 0000000..335aeb8 --- /dev/null +++ b/stripe_collection_module/code/services/stripe.service.ts @@ -0,0 +1,278 @@ +/** + * StripeService - Business logic for Stripe operations + * + * This service provides a high-level interface for Stripe operations. + * It wraps the Stripe client and provides domain-specific methods. + */ + +import Stripe from 'stripe'; +import { LogService } from './log.service'; +import StripeClient from '../clients/stripe-client'; + +export interface CreateCustomerParams { + email: string; + name?: string; + metadata?: Record; +} + +export interface CreatePaymentIntentParams { + amount: number; + currency: string; + customerId: string; + description?: string; + metadata?: Record; + confirm?: boolean; + offSession?: boolean; +} + +export interface AttachPaymentMethodParams { + paymentMethodId: string; + customerId: string; +} + +export class StripeService { + constructor( + private readonly logService: LogService, + private readonly stripeClient: StripeClient + ) {} + + /** + * Create a Stripe customer + */ + async createCustomer(params: CreateCustomerParams): Promise { + this.logService.info('Creating Stripe customer', 'StripeService', { + email: params.email, + }); + + try { + const customer = await this.stripeClient.stripeSDK.customers.create({ + email: params.email, + name: params.name, + metadata: params.metadata, + }); + + this.logService.info( + 'Stripe customer created successfully', + 'StripeService', + { + customerId: customer.id, + email: customer.email, + } + ); + + return customer; + } catch (error: any) { + this.logService.error( + `Failed to create Stripe customer: ${error.message}`, + 'StripeService', + { params, error } + ); + throw error; + } + } + + /** + * Get a Stripe customer by ID + */ + async getCustomer(customerId: string): Promise { + this.logService.debug( + `Getting Stripe customer: ${customerId}`, + 'StripeService' + ); + + try { + const customer = + await this.stripeClient.stripeSDK.customers.retrieve(customerId); + + if (customer.deleted) { + throw new Error(`Customer ${customerId} has been deleted`); + } + + return customer as Stripe.Customer; + } catch (error: any) { + this.logService.error( + `Failed to get Stripe customer: ${error.message}`, + 'StripeService', + { customerId, error } + ); + throw error; + } + } + + /** + * Update a Stripe customer + */ + async updateCustomer( + customerId: string, + params: Stripe.CustomerUpdateParams + ): Promise { + this.logService.info('Updating Stripe customer', 'StripeService', { + customerId, + }); + + try { + const customer = await this.stripeClient.stripeSDK.customers.update( + customerId, + params + ); + + this.logService.info( + 'Stripe customer updated successfully', + 'StripeService', + { + customerId: customer.id, + } + ); + + return customer; + } catch (error: any) { + this.logService.error( + `Failed to update Stripe customer: ${error.message}`, + 'StripeService', + { customerId, params, error } + ); + throw error; + } + } + + /** + * Create a payment intent + */ + async createPaymentIntent( + params: CreatePaymentIntentParams + ): Promise { + this.logService.info('Creating Stripe payment intent', 'StripeService', { + amount: params.amount, + currency: params.currency, + customerId: params.customerId, + }); + + try { + const paymentIntent = + await this.stripeClient.stripeSDK.paymentIntents.create({ + amount: params.amount, + currency: params.currency, + customer: params.customerId, + description: params.description, + metadata: params.metadata, + payment_method_types: ['card'], + confirm: params.confirm, + off_session: params.offSession, + }); + + this.logService.info( + 'Payment intent created successfully', + 'StripeService', + { + paymentIntentId: paymentIntent.id, + status: paymentIntent.status, + } + ); + + return paymentIntent; + } catch (error: any) { + this.logService.error( + `Failed to create payment intent: ${error.message}`, + 'StripeService', + { params, error } + ); + throw error; + } + } + + /** + * Get a payment method by ID + */ + async getPaymentMethod( + paymentMethodId: string + ): Promise { + this.logService.debug( + `Getting payment method: ${paymentMethodId}`, + 'StripeService' + ); + + try { + return await this.stripeClient.stripeSDK.paymentMethods.retrieve( + paymentMethodId + ); + } catch (error: any) { + this.logService.error( + `Failed to get payment method: ${error.message}`, + 'StripeService', + { paymentMethodId, error } + ); + throw error; + } + } + + /** + * Attach a payment method to a customer + */ + async attachPaymentMethod( + params: AttachPaymentMethodParams + ): Promise { + this.logService.info('Attaching payment method', 'StripeService', params); + + try { + const paymentMethod = + await this.stripeClient.stripeSDK.paymentMethods.attach( + params.paymentMethodId, + { + customer: params.customerId, + } + ); + + this.logService.info( + 'Payment method attached successfully', + 'StripeService', + { + paymentMethodId: params.paymentMethodId, + customerId: params.customerId, + } + ); + + return paymentMethod; + } catch (error: any) { + this.logService.error( + `Failed to attach payment method: ${error.message}`, + 'StripeService', + { params, error } + ); + throw error; + } + } + + /** + * Cancel a subscription + */ + async cancelSubscription( + subscriptionId: string + ): Promise { + this.logService.info('Canceling Stripe subscription', 'StripeService', { + subscriptionId, + }); + + try { + const subscription = + await this.stripeClient.stripeSDK.subscriptions.cancel(subscriptionId); + + this.logService.info( + 'Subscription canceled successfully', + 'StripeService', + { + subscriptionId, + status: subscription.status, + } + ); + + return subscription; + } catch (error: any) { + this.logService.error( + `Failed to cancel subscription: ${error.message}`, + 'StripeService', + { subscriptionId, error } + ); + throw error; + } + } +} diff --git a/stripe_collection_module/code/utils/README.md b/stripe_collection_module/code/utils/README.md new file mode 100644 index 0000000..bf1ffe4 --- /dev/null +++ b/stripe_collection_module/code/utils/README.md @@ -0,0 +1,179 @@ +# Utility Functions + +This directory contains reusable utility functions and helpers used throughout the collection module. + +## Files + +### `error.ts` +Simple error class for collection module errors. + +**Usage:** +```typescript +import ModuleError from './utils/error'; + +throw new ModuleError('Payment failed', { policyId: '123' }); +``` + +### `error-types.ts` +Enhanced error types with categorization and retry logic. + +**Classes:** +- `EnhancedModuleError` - Base error with request tracking +- `ValidationError` - Input validation errors +- `NotFoundError` - Resource not found errors +- `NetworkError` - Network/connectivity errors (retryable) +- `TimeoutError` - Timeout errors (retryable) +- `RateLimitError` - Rate limit errors (retryable) +- `ServerError` - Server-side errors (retryable) + +**Functions:** +- `categorizeError()` - Categorize any error +- `isRetryableError()` - Check if error should be retried +- `formatErrorForLogging()` - Format error for logs + +**Usage:** +```typescript +import { ValidationError, NetworkError } from './utils/error-types'; + +// Non-retryable validation error +throw new ValidationError('Invalid email', { email: user.email }); + +// Retryable network error +throw new NetworkError('Connection failed', { url: apiUrl }); +``` + +### `retry.ts` +Retry logic with exponential backoff and jitter. + +**Usage:** +```typescript +import { retryWithBackoff } from './utils/retry'; + +const result = await retryWithBackoff( + () => stripeClient.getCustomer(customerId), + { + maxRetries: 3, + initialDelay: 1000, + maxDelay: 10000, + shouldRetry: (error) => error.statusCode >= 500, + } +); +``` + +### `timeout.ts` +Add timeout to async operations. + +**Usage:** +```typescript +import { withTimeout } from './utils/timeout'; + +const result = await withTimeout( + slowOperation(), + 5000, // 5 second timeout + 'Operation timed out' +); +``` + +### `logger.ts` +Legacy logger (kept for backwards compatibility). + +**Note:** New code should use `LogService` from the DI container instead. + +```typescript +// ❌ Old approach +import Logger from './utils/logger'; +Logger.info('message'); + +// ✅ New approach +const logService = container.resolve(ServiceToken.LOG_SERVICE); +logService.info('message', 'ComponentName', { context }); +``` + +### `index.ts` +Re-exports commonly used utilities for convenience. + +**Usage:** +```typescript +// Import from utils directly +import { + ModuleError, + retryWithBackoff, + withTimeout +} from './utils'; +``` + +## Best Practices + +### 1. Use Enhanced Errors for Better Error Handling + +```typescript +// ✅ Good - specific error type +throw new ValidationError('Invalid amount', { amount }); + +// ❌ Bad - generic error +throw new Error('Invalid amount'); +``` + +### 2. Add Retry Logic for External APIs + +```typescript +// ✅ Good - retry on transient failures +const customer = await retryWithBackoff( + () => stripeClient.customers.retrieve(customerId), + { maxRetries: 3 } +); + +// ❌ Bad - no retry, fails on transient errors +const customer = await stripeClient.customers.retrieve(customerId); +``` + +### 3. Add Timeouts for Long Operations + +```typescript +// ✅ Good - timeout prevents hanging +const result = await withTimeout( + externalApiCall(), + 30000 // 30 second timeout +); + +// ❌ Bad - could hang indefinitely +const result = await externalApiCall(); +``` + +## Examples + +### Error Handling with Retry + +```typescript +import { retryWithBackoff, NetworkError, isRetryableError } from './utils'; + +try { + const result = await retryWithBackoff( + async () => { + const response = await fetch(apiUrl); + if (!response.ok) { + throw new NetworkError('API call failed', { + status: response.status + }); + } + return response.json(); + }, + { + maxRetries: 3, + shouldRetry: (error) => isRetryableError(error), + } + ); +} catch (error) { + logService.error('Failed after retries', 'ServiceName', { + error: formatErrorForLogging(error), + }); + throw error; +} +``` + +## Related Documentation + +- [Error Handling Best Practices](../../docs/BEST_PRACTICES.md) +- [Testing Guide](../../docs/TESTING.md) +- [Architecture](../../docs/ARCHITECTURE.md) + diff --git a/stripe_collection_module/code/utils/error-types.ts b/stripe_collection_module/code/utils/error-types.ts new file mode 100644 index 0000000..d36f0f6 --- /dev/null +++ b/stripe_collection_module/code/utils/error-types.ts @@ -0,0 +1,205 @@ +/** + * Enhanced Error Types with Request Tracking + * + * Provides structured error types with categorization, retry logic, and request tracking. + */ + +/** + * Error Categories + */ +export enum ErrorCategory { + VALIDATION = 'validation', + NOT_FOUND = 'not_found', + AUTHENTICATION = 'authentication', + AUTHORIZATION = 'authorization', + NETWORK = 'network', + SERVER_ERROR = 'server_error', + TIMEOUT = 'timeout', + RATE_LIMIT = 'rate_limit', + UNKNOWN = 'unknown', +} + +/** + * Enhanced module error with request tracking + */ +export class EnhancedModuleError extends Error { + public readonly timestamp: Date; + public requestId?: string; + public correlationId?: string; + + constructor( + message: string, + public readonly category: ErrorCategory = ErrorCategory.UNKNOWN, + public readonly retryable: boolean = false, + public readonly statusCode?: number, + public readonly context?: Record + ) { + super(message); + this.name = 'EnhancedModuleError'; + this.timestamp = new Date(); + Error.captureStackTrace(this, EnhancedModuleError); + } + + /** + * Add request tracking information + */ + withRequestId(requestId: string, correlationId?: string): this { + this.requestId = requestId; + this.correlationId = correlationId || requestId; + return this; + } + + /** + * Convert error to JSON for logging + */ + toJSON(): Record { + return { + name: this.name, + message: this.message, + category: this.category, + retryable: this.retryable, + statusCode: this.statusCode, + timestamp: this.timestamp.toISOString(), + requestId: this.requestId, + correlationId: this.correlationId, + context: this.context, + stack: this.stack, + }; + } +} + +/** + * Validation error + */ +export class ValidationError extends EnhancedModuleError { + constructor(message: string, context?: Record) { + super(message, ErrorCategory.VALIDATION, false, 400, context); + this.name = 'ValidationError'; + } +} + +/** + * Not found error + */ +export class NotFoundError extends EnhancedModuleError { + constructor(message: string, context?: Record) { + super(message, ErrorCategory.NOT_FOUND, false, 404, context); + this.name = 'NotFoundError'; + } +} + +/** + * Network error + */ +export class NetworkError extends EnhancedModuleError { + constructor(message: string, context?: Record) { + super(message, ErrorCategory.NETWORK, true, 503, context); + this.name = 'NetworkError'; + } +} + +/** + * Timeout error + */ +export class TimeoutError extends EnhancedModuleError { + constructor(message: string, context?: Record) { + super(message, ErrorCategory.TIMEOUT, true, 504, context); + this.name = 'TimeoutError'; + } +} + +/** + * Rate limit error + */ +export class RateLimitError extends EnhancedModuleError { + constructor(message: string, context?: Record) { + super(message, ErrorCategory.RATE_LIMIT, true, 429, context); + this.name = 'RateLimitError'; + } +} + +/** + * Server error + */ +export class ServerError extends EnhancedModuleError { + constructor(message: string, context?: Record) { + super(message, ErrorCategory.SERVER_ERROR, true, 500, context); + this.name = 'ServerError'; + } +} + +/** + * Categorize an unknown error + */ +export function categorizeError(error: any): ErrorCategory { + // Already categorized + if (error instanceof EnhancedModuleError) { + return error.category; + } + + // HTTP status codes + if (error.statusCode) { + if (error.statusCode === 404) return ErrorCategory.NOT_FOUND; + if (error.statusCode === 401 || error.statusCode === 403) + return ErrorCategory.AUTHENTICATION; + if (error.statusCode === 429) return ErrorCategory.RATE_LIMIT; + if (error.statusCode >= 500) return ErrorCategory.SERVER_ERROR; + if (error.statusCode >= 400) return ErrorCategory.VALIDATION; + } + + // Network errors + if ( + error.code === 'ECONNREFUSED' || + error.code === 'ETIMEDOUT' || + error.code === 'ENOTFOUND' || + error.code === 'NETWORK_ERROR' + ) { + return ErrorCategory.NETWORK; + } + + // Timeout errors + if (error.name === 'TimeoutError' || error.message?.includes('timeout')) { + return ErrorCategory.TIMEOUT; + } + + return ErrorCategory.UNKNOWN; +} + +/** + * Check if error should be retried + */ +export function isRetryableError(error: any): boolean { + // Module errors have explicit retryable flag + if (error instanceof EnhancedModuleError) { + return error.retryable; + } + + // Categorize and check + const category = categorizeError(error); + + return [ + ErrorCategory.NETWORK, + ErrorCategory.TIMEOUT, + ErrorCategory.RATE_LIMIT, + ErrorCategory.SERVER_ERROR, + ].includes(category); +} + +/** + * Format error for logging + */ +export function formatErrorForLogging(error: any): Record { + if (error instanceof EnhancedModuleError) { + return error.toJSON(); + } + + return { + name: error.name || 'Error', + message: error.message || 'Unknown error', + category: categorizeError(error), + retryable: isRetryableError(error), + statusCode: error.statusCode, + code: error.code, + stack: error.stack, + }; +} diff --git a/stripe_collection_module/code/utils/error.ts b/stripe_collection_module/code/utils/error.ts index c3ba140..48d95ad 100644 --- a/stripe_collection_module/code/utils/error.ts +++ b/stripe_collection_module/code/utils/error.ts @@ -1,12 +1,13 @@ -import Config from '../config'; +import { getConfigService } from '../services/config-instance'; export default class ModuleError extends Error { constructor(message: string, metadata?: Record) { const metadataString = JSON.stringify(metadata); const stackTrace = new Error('Error').stack; const caller = stackTrace?.split('\n')[2].trim().split(' ')[1]; + const config = getConfigService(); super( - `[${Config.env.environment} | ${caller}] ${message} ${metadataString}`, + `[${config.get('environment')} | ${caller}] ${message} ${metadataString}` ); } } diff --git a/stripe_collection_module/code/utils/index.ts b/stripe_collection_module/code/utils/index.ts index e6473e2..95223ac 100644 --- a/stripe_collection_module/code/utils/index.ts +++ b/stripe_collection_module/code/utils/index.ts @@ -1,123 +1,26 @@ -import { StripeEvents } from '../interfaces'; -import StripeClient from '../clients/stripe-client'; -import moment from 'moment-timezone'; -import Config from '../config'; -import rootClient from '../clients/root-client'; -import Logger from './logger'; -import ModuleError from './error'; - -export function getStripeProductId() { - return Config.env.stripeProductId; -} - -export function convertStripeTimestampToSAST(timestamp: number) { - // Stripe sends the timestamp in seconds - // We have to convert to milliseconds to get the right conversion - // The true parameter in .toISOString keeps the conversion at UTC+2 - const utcTime = moment(timestamp * 1000); - return utcTime.clone().tz('Africa/Johannesburg').toISOString(true); -} - -/** - * Checks if a policy payment method has a collection module definition associated with it. - */ -export async function isPolicyPaymentMethodLinkedToCollectionModule( - policyId: string, -) { - const paymentMethod = await rootClient.SDK.getPolicyPaymentMethod({ - policyId, - }); - return !!paymentMethod.collection_module_definition_id; -} - -export async function getPolicyIdFromStripeEvent(event: any) { - const { type } = event; - - Logger.info(`Check for policyId in event type ${type}`, { - event, - }); - - const dataObject = event.data.object; - - switch (type) { - case StripeEvents.InvoiceCreated: { - const rootPolicyId = - dataObject?.metadata?.rootPolicyId || - dataObject?.subscription_details?.metadata?.rootPolicyId; - return rootPolicyId; - } - case StripeEvents.InvoicePaid: - case StripeEvents.InvoicePaymentFailed: - case StripeEvents.InvoiceVoided: - case StripeEvents.InvoiceMarkedUncollectible: { - return getPolicyIdFromInvoice(dataObject.id as string); - } - case StripeEvents.ChargeRefunded: { - return getPolicyIdFromInvoice(dataObject.invoice as string); - } - case StripeEvents.ChargeDisputeFundsWithdrawn: { - const charge = await getChargeDetails(dataObject.charge as string); - return getPolicyIdFromInvoice(charge.invoice as string); - } - case StripeEvents.SubscriptionScheduleUpdated: { - return dataObject?.metadata?.rootPolicyId; - } - case StripeEvents.PaymentIntentSucceeded: - case StripeEvents.PaymentIntentFailed: { - return dataObject?.metadata?.rootPolicyId; - } - default: - return undefined; - } -} - /** - * Get's the policyId from a Stripe invoice + * Utility Functions + * + * This file re-exports commonly used utilities for convenience. + * Actual implementations are in their respective modules. */ -async function getPolicyIdFromInvoice(invoiceId: string) { - const stripeAPIClient = new StripeClient(); - const invoice = await stripeAPIClient.stripeSDK.invoices.retrieve(invoiceId); - return invoice.subscription_details?.metadata?.rootPolicyId; -} - -/** - * Get's the charge details using a chargeId - */ -async function getChargeDetails(chargeId: string) { - const stripeAPIClient = new StripeClient(); - const charge = await stripeAPIClient.stripeSDK.charges.retrieve(chargeId); - return charge; -} - -/** - * Gets the next occurrence of a target day on or after a reference date - * @param referenceDate - * @param targetDay - * @returns - */ -export const getNextOccurrence = ( - referenceDate: moment.Moment, - targetDay: number, -) => { - if (targetDay < 1) { - throw new ModuleError( - `Target Day needs to be >= 1 to be valid. TargetDay=${targetDay}`, - ); - } - - // Find the next occurrence of the target day on or after the reference date - let nextOccurrence = moment(referenceDate).date(targetDay); - - if (nextOccurrence.isBefore(referenceDate)) { - nextOccurrence = nextOccurrence.add(1, 'months'); - } - - if (nextOccurrence < referenceDate) { - throw new ModuleError(`NextOccurrence date needs to be >= ReferenceDate.`, { - nextOccurrence: nextOccurrence.toISOString(), - referenceDate: referenceDate.toISOString(), - }); - } - return nextOccurrence; -}; +// Error utilities +export { default as ModuleError } from './error'; +export { + EnhancedModuleError, + ValidationError, + NotFoundError, + NetworkError, + TimeoutError, + RateLimitError, + ServerError, + ErrorCategory, + categorizeError, + isRetryableError, + formatErrorForLogging, +} from './error-types'; + +// Retry and timeout utilities +export { retryWithBackoff } from './retry'; +export { withTimeout } from './timeout'; diff --git a/stripe_collection_module/code/utils/logger.ts b/stripe_collection_module/code/utils/logger.ts index 3e13b3e..95af131 100644 --- a/stripe_collection_module/code/utils/logger.ts +++ b/stripe_collection_module/code/utils/logger.ts @@ -1,4 +1,4 @@ -import Config from '../config'; +import { getConfigService } from '../services/config-instance'; enum LogLevel { DEBUG = 'debug', @@ -11,7 +11,7 @@ export default class Logger { private static logMessage( logLevel: LogLevel, message: string, - metadata?: Record, + metadata?: Record ): void { const metadataString = JSON.stringify(metadata); // We're just using this to get the caller function name. No other way to do it in TS due to strict mode. @@ -19,8 +19,11 @@ export default class Logger { ?.split('\n')[2] .trim() .split(' ')[1]; + const config = getConfigService(); console[logLevel]( - `[${Config.env.environment.toUpperCase()} | ${caller}] ${message} ${metadataString}`, + `[${config + .get('environment') + .toUpperCase()} | ${caller}] ${message} ${metadataString}` ); } diff --git a/stripe_collection_module/code/utils/retry.ts b/stripe_collection_module/code/utils/retry.ts new file mode 100644 index 0000000..5da57e1 --- /dev/null +++ b/stripe_collection_module/code/utils/retry.ts @@ -0,0 +1,176 @@ +/** + * Retry Utilities + * + * Provides retry logic with exponential backoff and configurable strategies. + */ + +export interface RetryOptions { + /** Maximum number of retry attempts */ + maxRetries?: number; + /** Initial delay in milliseconds */ + initialDelay?: number; + /** Maximum delay in milliseconds */ + maxDelay?: number; + /** Backoff multiplier (2 = exponential) */ + backoffMultiplier?: number; + /** Function to determine if error should trigger retry */ + shouldRetry?: (error: any) => boolean; + /** Callback before each retry */ + onRetry?: (attempt: number, error: any) => void | Promise; +} + +/** + * Retry an async operation with exponential backoff + * + * @example + * ```typescript + * const result = await retryWithBackoff( + * () => apiClient.call(), + * { + * maxRetries: 3, + * initialDelay: 1000, + * shouldRetry: (error) => error.statusCode >= 500 + * } + * ); + * ``` + */ +export async function retryWithBackoff( + operation: () => Promise, + options: RetryOptions = {} +): Promise { + const { + maxRetries = 3, + initialDelay = 1000, + maxDelay = 30000, + backoffMultiplier = 2, + shouldRetry = () => true, + onRetry, + } = options; + + let lastError: Error; + let delay = initialDelay; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await operation(); + } catch (error) { + lastError = error as Error; + + // Check if we should retry + if (attempt === maxRetries || !shouldRetry(error)) { + throw error; + } + + // Call onRetry callback if provided + if (onRetry) { + await onRetry(attempt + 1, error); + } + + // Wait before retrying + await sleep(delay); + + // Calculate next delay with exponential backoff + delay = Math.min(delay * backoffMultiplier, maxDelay); + } + } + + throw lastError!; +} + +/** + * Sleep for specified milliseconds + */ +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Retry with jitter to prevent thundering herd + * + * @example + * ```typescript + * const result = await retryWithJitter( + * () => apiClient.call(), + * { maxRetries: 3 } + * ); + * ``` + */ +export async function retryWithJitter( + operation: () => Promise, + options: RetryOptions = {} +): Promise { + return retryWithBackoff(operation, { + ...options, + onRetry: async (attempt, error) => { + // Add random jitter (0-50% of delay) + const jitter = Math.random() * 0.5; + await sleep((options.initialDelay || 1000) * jitter); + + if (options.onRetry) { + await options.onRetry(attempt, error); + } + }, + }); +} + +/** + * Retry only for specific error types + * + * @example + * ```typescript + * const result = await retryForErrors( + * () => apiClient.call(), + * ['NETWORK_ERROR', 'TIMEOUT'], + * { maxRetries: 3 } + * ); + * ``` + */ +export async function retryForErrors( + operation: () => Promise, + retryableErrorCodes: string[], + options: RetryOptions = {} +): Promise { + return retryWithBackoff(operation, { + ...options, + shouldRetry: (error) => { + const errorCode: string = + error.code || error.statusCode?.toString() || ''; + return retryableErrorCodes.includes(errorCode); + }, + }); +} + +/** + * Retry for network and server errors only + * + * @example + * ```typescript + * const result = await retryForNetworkErrors( + * () => apiClient.call() + * ); + * ``` + */ +export async function retryForNetworkErrors( + operation: () => Promise, + options: RetryOptions = {} +): Promise { + return retryWithBackoff(operation, { + ...options, + shouldRetry: (error) => { + // Retry on network errors + if ( + error.code === 'ECONNREFUSED' || + error.code === 'ETIMEDOUT' || + error.code === 'ENOTFOUND' + ) { + return true; + } + + // Retry on 5xx errors or 429 (rate limit) + return ( + (error.statusCode && error.statusCode >= 500) || + error.statusCode === 429 + ); + }, + }); +} diff --git a/stripe_collection_module/code/utils/stripe-utils.ts b/stripe_collection_module/code/utils/stripe-utils.ts deleted file mode 100644 index 039cc93..0000000 --- a/stripe_collection_module/code/utils/stripe-utils.ts +++ /dev/null @@ -1,198 +0,0 @@ -import 'moment-timezone'; -import Stripe from 'stripe'; - -import { StripeEvents } from '../interfaces/stripe-events'; -import moment from 'moment-timezone'; -import Config from '../config'; -import StripeClient from '../clients/stripe-client'; -import rootClient from '../clients/root-client'; -import Logger from './logger'; -import ModuleError from './error'; - -class StripeUtils { - private stripeClient: StripeClient; - - constructor() { - this.stripeClient = new StripeClient(); - } - - async getSuccessfulInvoiceCharges(params: { stripeCustomerId: string }) { - const { stripeCustomerId } = params; - const charges = await this.stripeClient.stripeSDK.charges.list({ - customer: stripeCustomerId, - limit: 3, - }); - const chargesToRefund = charges.data.filter( - (charge) => - ['succeeded'].includes(charge.status) && - !charge.refunded && - !!charge.invoice, - ); - return chargesToRefund; - } - - async refundCharges(params: { charges: any[] }) { - const { charges } = params; - for (const charge of charges) { - await this.stripeClient.stripeSDK.refunds.create({ - charge: charge.id.toString(), - reason: 'requested_by_customer', - }); - - Logger.debug(`Charge refunded`, { - charge, - }); - } - } - - convertStripeTimestampToSAST = (timestamp: number) => { - const utcTime = moment(timestamp * 1000); - return utcTime.clone().tz('Africa/Johannesburg').toISOString(true); - }; - - async cancelStripeScheduleAndSubscription(params: { - rootPolicyId: string; - policyAppData: any; - prorate: boolean; - invoice_now: boolean; - }) { - const { rootPolicyId, policyAppData, prorate, invoice_now } = params; - - Logger.info( - `Start cancelling Stripe schedule & subscription for policy ${rootPolicyId}`, - ); - - try { - const stripeSubscriptionScheduleId = - policyAppData.stripe_subscription_schedule_id; - const stripeSubscriptionId = policyAppData.stripe_subscription_id; - - if (stripeSubscriptionScheduleId) { - const subscriptionSchedule = - await this.stripeClient.stripeSDK.subscriptionSchedules.retrieve( - stripeSubscriptionScheduleId as string, - ); - - const subscriptionId = - stripeSubscriptionId || subscriptionSchedule.subscription; - - if (['not_started', 'active'].includes(subscriptionSchedule.status)) { - await this.stripeClient.stripeSDK.subscriptionSchedules.cancel( - stripeSubscriptionScheduleId as string, - { - invoice_now, - prorate, - }, - ); - } - - if (subscriptionId) { - const subscription = - await this.stripeClient.stripeSDK.subscriptions.retrieve( - subscriptionId as string, - ); - - if (subscription.status !== 'canceled') { - await this.stripeClient.stripeSDK.subscriptions.cancel( - subscriptionId as string, - { - prorate, - invoice_now, - }, - ); - } - } - } - } catch (error: any) { - throw new ModuleError( - `There was an error cancelling the Stripe schedule or subscription for policy ${rootPolicyId}: ${error.message}`, - { - rootPolicyId, - error, - }, - ); - } - } - - getStripeProductId() { - return Config.env.stripeProductId; - } - - async getPolicyIdFromStripeEvent(event: any) { - const { type } = event; - - Logger.info(`Check for policyId in event type ${type}`, { - event, - }); - - const dataObject = event.data.object; - - switch (type) { - case StripeEvents.InvoiceCreated: { - const rootPolicyId = - dataObject?.metadata?.rootPolicyId || - dataObject?.subscription_details?.metadata?.rootPolicyId; - return rootPolicyId; - } - case StripeEvents.InvoicePaid: - case StripeEvents.InvoicePaymentFailed: - case StripeEvents.InvoiceVoided: - case StripeEvents.InvoiceMarkedUncollectible: { - return this.getPolicyIdFromInvoice(dataObject.id as string); - } - case StripeEvents.ChargeRefunded: { - return this.getPolicyIdFromInvoice(dataObject.invoice as string); - } - case StripeEvents.ChargeDisputeFundsWithdrawn: { - const charge: Stripe.Charge = await this.getChargeDetails( - dataObject.charge as string, - ); - if (typeof charge.invoice === 'string') { - return this.getPolicyIdFromInvoice(charge.invoice); - } - } - case StripeEvents.SubscriptionScheduleUpdated: { - return dataObject?.metadata?.rootPolicyId; - } - case StripeEvents.PaymentIntentSucceeded: - case StripeEvents.PaymentIntentFailed: { - return dataObject?.metadata?.rootPolicyId; - } - default: - return undefined; - } - } - - async isPolicyPaymentMethodLinkedToCollectionModule(policyId: string) { - const paymentMethod = await rootClient.SDK.getPolicyPaymentMethod({ - policyId, - }); - return !!paymentMethod.collection_module_definition_id; - } - - async getPolicyIdFromInvoice(invoiceId: string) { - const invoice = await this.stripeClient.stripeSDK.invoices.retrieve( - invoiceId, - ); - return invoice.subscription_details?.metadata?.rootPolicyId; - } - - async getPolicyIdFromPaymentIntent({ - paymentIntentId, - }: { - paymentIntentId: string; - }) { - const paymentIntent = - await this.stripeClient.stripeSDK.paymentIntents.retrieve( - paymentIntentId, - ); - return paymentIntent.metadata.rootPolicyId; - } - - async getChargeDetails(chargeId: string) { - const charge = await this.stripeClient.stripeSDK.charges.retrieve(chargeId); - return charge; - } -} - -export { StripeUtils }; diff --git a/stripe_collection_module/code/utils/timeout.ts b/stripe_collection_module/code/utils/timeout.ts new file mode 100644 index 0000000..8b7c029 --- /dev/null +++ b/stripe_collection_module/code/utils/timeout.ts @@ -0,0 +1,119 @@ +/** + * Timeout Utilities + * + * Provides timeout handling for async operations. + */ + +const TIMEOUT_MESSAGE = 'Operation timeout'; + +/** + * Execute an operation with a timeout + * + * @param operation - The async operation to execute + * @param timeoutMs - Timeout in milliseconds + * @returns Promise that resolves with operation result or rejects on timeout + * + * @example + * ```typescript + * try { + * const result = await withTimeout(apiCall(), 5000); + * } catch (error) { + * if (error.message === 'Operation timeout') { + * console.error('Operation timed out after 5 seconds'); + * } + * } + * ``` + */ +export async function withTimeout( + operation: Promise, + timeoutMs: number +): Promise { + return Promise.race([ + operation, + new Promise((_, reject) => + setTimeout(() => reject(new Error(TIMEOUT_MESSAGE)), timeoutMs) + ), + ]); +} + +/** + * Execute an operation with a timeout and fallback value + * + * @param operation - The async operation to execute + * @param timeoutMs - Timeout in milliseconds + * @param fallbackValue - Value to return on timeout + * @returns Promise that resolves with operation result or fallback value + * + * @example + * ```typescript + * const result = await withTimeoutFallback( + * apiCall(), + * 5000, + * { default: true } + * ); + * ``` + */ +export async function withTimeoutFallback( + operation: Promise, + timeoutMs: number, + fallbackValue: T +): Promise { + try { + return await withTimeout(operation, timeoutMs); + } catch (error) { + if (error instanceof Error && error.message === TIMEOUT_MESSAGE) { + return fallbackValue; + } + throw error; + } +} + +/** + * Create a timeout error with context + */ +export class TimeoutError extends Error { + constructor( + message: string, + public readonly timeoutMs: number, + public readonly operation?: string + ) { + super(message); + this.name = 'TimeoutError'; + } +} + +/** + * Execute operation with timeout and custom error + * + * @example + * ```typescript + * try { + * await withTimeoutError( + * apiCall(), + * 5000, + * 'API call timed out', + * 'fetchUserData' + * ); + * } catch (error) { + * if (error instanceof TimeoutError) { + * console.error(`${error.operation} timed out after ${error.timeoutMs}ms`); + * } + * } + * ``` + */ +export async function withTimeoutError( + operation: Promise, + timeoutMs: number, + errorMessage: string = TIMEOUT_MESSAGE, + operationName?: string +): Promise { + return Promise.race([ + operation, + new Promise((_, reject) => + setTimeout( + () => reject(new TimeoutError(errorMessage, timeoutMs, operationName)), + timeoutMs + ) + ), + ]); +} diff --git a/stripe_collection_module/code/webhook-hooks.ts b/stripe_collection_module/code/webhook-hooks.ts index 1e95a3d..5e57445 100644 --- a/stripe_collection_module/code/webhook-hooks.ts +++ b/stripe_collection_module/code/webhook-hooks.ts @@ -1,214 +1,264 @@ -import ProcessInvoiceCreatedEventController from './controllers/stripe-event-processors/processInvoiceCreatedEventController'; -import ProcessInvoicePaidEventController from './controllers/stripe-event-processors/processInvoicePaidEventController'; +/** + * Webhook Handler for Stripe Events + * + * This module handles incoming webhook requests from Stripe. + * It verifies signatures, validates events, and routes them to appropriate controllers. + * + * Architecture: + * - Uses dependency injection for controller resolution + * - Delegates to controllers for event processing + * - Focuses on routing and authentication only + */ + import * as crypto from 'crypto'; -import { StripeEvents } from './interfaces'; - -import { - getPolicyIdFromStripeEvent, - isPolicyPaymentMethodLinkedToCollectionModule, -} from './utils'; -import ProcessInvoicePaymentFailedEventController from './controllers/stripe-event-processors/processInvoicePaymentFailedEventController'; -import ProcessSubscriptionScheduleUpdatedEventController from './controllers/stripe-event-processors/processSubscriptionScheduleUpdatedEventController'; -import ProcessInvoiceMarkedUncollectableEventController from './controllers/stripe-event-processors/processInvoiceMarkedUncollectableEventController'; -import ProcessPaymentIntentSucceededEventController from './controllers/stripe-event-processors/processPaymentIntentSucceededEventController'; -import ProcessInvoiceChargeRefundedEventController from './controllers/stripe-event-processors/processInvoiceChargeRefundedEventController'; import Stripe from 'stripe'; -import Config from './config'; -import Logger from './utils/logger'; +import { getContainer } from './core/container.setup'; +import { ServiceToken } from './core/container'; +import { LogService } from './services/log.service'; +import { StripeEvents } from './interfaces/stripe-events'; +import { InvoicePaidController } from './controllers/stripe-event-processors/invoice-paid.controller'; +import { getConfigService } from './services/config-instance'; import ModuleError from './utils/error'; +import rootClient from './clients/root-client'; -const authWebhookRequest = async (request: any) => { - // https://stripe.com/docs/webhooks/signatures#verify-manually +/** + * Verify Stripe webhook signature + * + * @param request - Incoming webhook request + * @returns Response object if verification fails, undefined if successful + */ +const verifyWebhookSignature = (request: any) => { const { headers } = request.request; - const signature: any = { t: undefined, v1: undefined }; - headers['stripe-signature'].split(',').map((rawElement: any) => { + const stripeSignature: string = headers['stripe-signature']; + + if (!stripeSignature) { + return { + response: { + status: 403, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ error: 'Missing stripe-signature header' }), + }, + }; + } + + // Parse Stripe signature header + // Format: t=timestamp,v1=signature + const signature: { t?: string; v1?: string } = { + t: undefined, + v1: undefined, + }; + const elements = stripeSignature.split(','); + for (const rawElement of elements) { const [prefix, value] = rawElement.split('='); - if (['t', 'v1'].includes(prefix)) { + if (prefix === 't' || prefix === 'v1') { signature[prefix] = value; } - }); + } - const { body } = request.request; + if (!signature.t || !signature.v1) { + return { + response: { + status: 403, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ error: 'Invalid stripe-signature format' }), + }, + }; + } + const { body } = request.request; const signedPayload = `${signature.t}.${body.toString('utf8')}`; + // Get webhook secret from configuration + const config = getConfigService(); + const webhookSecret = config.get('stripeWebhookSigningSecret'); + + // Compute expected signature const expectedSignature = crypto - .createHmac('sha256', Config.env.stripeWebhookSigningSecret) + .createHmac('sha256', webhookSecret) .update(signedPayload) .digest('hex'); - // Compare the expected signature with the received signature - const signatureVerified = crypto.timingSafeEqual( - Buffer.from(signature.v1, 'hex'), - Buffer.from(expectedSignature, 'hex'), - ); + // Verify signature using timing-safe comparison + try { + const signatureVerified = crypto.timingSafeEqual( + Buffer.from(signature.v1, 'hex') as unknown as Uint8Array, + Buffer.from(expectedSignature, 'hex') as unknown as Uint8Array + ); - if (!signatureVerified) { + if (!signatureVerified) { + return { + response: { + status: 403, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ error: 'Invalid signature' }), + }, + }; + } + } catch (error) { + // Buffer length mismatch or other error return { response: { status: 403, headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({}), + body: JSON.stringify({ error: 'Signature verification failed' }), }, }; } -}; -/** - * @typedef {Object} Request - * @property {string | null} body - The incoming request's body - * @property {Record} headers - An object containing the incoming request's body - * @property {string} method - The HTTP method used to make to incoming request (e.g. "POST") - */ - -/** - * @typedef {Object} Response - * @property {number} status - The response status code (valid range is 200 to 599) - * @property {string} body - The response body - */ + // Signature verified successfully + return undefined; +}; /** - * @typedef {Object} ProcessWebhookRequestResult - * @property {Response} response - The response object + * Create success response */ +const successResponse = () => ({ + response: { + status: 200, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ received: true }), + }, +}); /** - * Process incoming webhook request. + * Process incoming Stripe webhook request * - * @param {Request} request - * @returns {ProcessWebhookRequestResult} + * @param request - Incoming webhook request from Stripe + * @returns Response object */ export const processWebhookRequest = async (request: any) => { - const authResult = await authWebhookRequest(request); - if (authResult) { - return authResult; - } + const container = getContainer(); + const logService = container.resolve(ServiceToken.LOG_SERVICE); + + try { + // Step 1: Verify webhook signature + logService.debug('Verifying webhook signature', 'WebhookHandler'); + const authResult = verifyWebhookSignature(request); + if (authResult) { + logService.warn( + 'Webhook signature verification failed', + 'WebhookHandler' + ); + return authResult; + } - const parsedBody = JSON.parse(request.request.body); - // When handling a new Stripe event, please check getPolicyIdFromStripeEvent - // So the event data can be handled in that function too - const policyId = await getPolicyIdFromStripeEvent(parsedBody); - Logger.info(`policyId from stripe event: ${policyId}`); + // Step 2: Parse webhook body + const parsedBody = JSON.parse(request.request.body as string); + const eventType: string = parsedBody.type; - if (!policyId) { - Logger.info('No policyId found in the event', { - event: parsedBody, + logService.info('Received Stripe webhook', 'WebhookHandler', { + eventType, + eventId: parsedBody.id, }); - return { - response: { - status: 200, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({}), - }, - }; - } + // Step 3: Extract policy ID from event + // Different event types store policy ID in different locations + const dataObject = parsedBody.data.object; + let policyId: string | undefined; - Logger.info(`Processing stripe event of type: ${parsedBody.type}`, { - policyId, - event: parsedBody, - }); + switch (eventType) { + case StripeEvents.InvoiceCreated: + case StripeEvents.InvoicePaid: + policyId = dataObject.metadata?.rootPolicyId; + break; + case StripeEvents.PaymentIntentSucceeded: + case StripeEvents.PaymentIntentFailed: + policyId = dataObject.metadata?.rootPolicyId; + break; + // TODO: Add more event types as needed for your implementation + default: + break; + } - const assignedToCollectionModule = - await isPolicyPaymentMethodLinkedToCollectionModule(policyId); + if (!policyId) { + logService.info( + 'No policy ID found in event, skipping', + 'WebhookHandler', + { eventType } + ); + return successResponse(); + } - if (!assignedToCollectionModule) { - Logger.debug( - `Ignoring this request as this policy payment method has not been assigned a collection module - policyId: ${policyId}`, - ); + logService.debug('Found policy ID in event', 'WebhookHandler', { + policyId, + eventType, + }); - return { - response: { - status: 200, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({}), - }, - }; - } + // Step 4: Verify policy is assigned to this collection module + let isAssigned = false; + try { + const paymentMethod = await rootClient.SDK.getPolicyPaymentMethod({ + policyId, + }); + isAssigned = !!paymentMethod.collection_module_definition_id; + } catch (error: any) { + logService.warn( + 'Failed to check payment method for policy', + 'WebhookHandler', + { policyId, error: error.message } + ); + } - Logger.debug(`Processing stripe event of type: ${parsedBody.type}`, { - policyId, - event: parsedBody, - }); + if (!isAssigned) { + logService.info( + 'Policy not assigned to this collection module, skipping', + 'WebhookHandler', + { policyId, eventType } + ); + return successResponse(); + } - try { + // Step 5: Route event to appropriate controller const payload = parsedBody.data.object; - /** - * Add / remove the Stripe event types that you want to handle in the switch statement below. - */ - switch (parsedBody.type) { - case StripeEvents.InvoiceCreated: { - await new ProcessInvoiceCreatedEventController().process( - payload as Stripe.Invoice, - ); - break; - } + logService.info('Processing Stripe event', 'WebhookHandler', { + eventType, + policyId, + }); + switch (eventType) { case StripeEvents.InvoicePaid: { - await new ProcessInvoicePaidEventController().process( - payload as Stripe.Invoice, + const controller = container.resolve( + ServiceToken.INVOICE_PAID_CONTROLLER ); + await controller.handle(payload as Stripe.Invoice); break; } - case StripeEvents.InvoicePaymentFailed: { - await new ProcessInvoicePaymentFailedEventController().process( - payload as Stripe.Invoice, - ); - break; - } - - case StripeEvents.SubscriptionScheduleUpdated: { - await new ProcessSubscriptionScheduleUpdatedEventController().process( - payload as Stripe.SubscriptionSchedule, - ); - break; - } - - case StripeEvents.InvoiceVoided: - case StripeEvents.InvoiceMarkedUncollectible: { - await new ProcessInvoiceMarkedUncollectableEventController().process( - payload as Stripe.Invoice, - ); - break; - } - - case StripeEvents.ChargeRefunded: { - await new ProcessInvoiceChargeRefundedEventController().process( - payload as Stripe.Charge, - ); - break; - } - - case StripeEvents.PaymentIntentSucceeded: { - await new ProcessPaymentIntentSucceededEventController().process( - payload as Stripe.PaymentIntent, - ); - break; - } + // TODO: Add more event handlers as you implement them + // Example: + // case StripeEvents.InvoicePaymentFailed: { + // const controller = container.resolve( + // ServiceToken.INVOICE_PAYMENT_FAILED_CONTROLLER, + // ); + // await controller.handle(payload as Stripe.Invoice); + // break; + // } default: - // Unexpected event type - throw new ModuleError( - `Collection module does not handle event type '${parsedBody.type}'.`, - ); + logService.warn('Unhandled Stripe event type', 'WebhookHandler', { + eventType, + }); + // Return success even for unhandled events to prevent retries + return successResponse(); } - return { - response: { - status: 200, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({}), - }, - }; - } catch (error) { - throw new ModuleError( - `Error processing stripe event of type: ${parsedBody.type}`, - { - error, - event: parsedBody, - }, - ); + logService.info('Successfully processed Stripe event', 'WebhookHandler', { + eventType, + policyId, + }); + + return successResponse(); + } catch (error: any) { + logService.error('Error processing webhook', 'WebhookHandler', { + error: error.message, + stack: error.stack, + }); + + // Re-throw as ModuleError for consistent error handling + throw new ModuleError('Webhook processing failed', { + error: error.message, + stack: error.stack, + }); } }; diff --git a/stripe_collection_module/docs/ARCHITECTURE.md b/stripe_collection_module/docs/ARCHITECTURE.md new file mode 100644 index 0000000..8c46500 --- /dev/null +++ b/stripe_collection_module/docs/ARCHITECTURE.md @@ -0,0 +1,227 @@ +# Collection Module Architecture + +This document describes the architecture of the Stripe Collection Module template. This template has been rebuilt from the ground up with a focus on testability, maintainability, and clear separation of concerns. + +## Overview + +The collection module follows a simple service-oriented architecture with dependency injection. It's designed to be easy to understand and extend while providing solid patterns for building integrations. + +## Directory Structure + +``` +stripe_collection_module/ +├── code/ +│ ├── core/ # Core domain models and DI container +│ │ ├── models/ # Domain model interfaces +│ │ │ ├── customer.model.ts +│ │ │ ├── payment.model.ts +│ │ │ ├── subscription.model.ts +│ │ │ ├── policy.model.ts +│ │ │ └── payment-method.model.ts +│ │ ├── container.ts # Dependency injection container +│ │ └── container.setup.ts # Container configuration +│ │ +│ ├── services/ # Business logic services +│ │ ├── log.service.ts # Structured JSON logging +│ │ ├── config.service.ts # Configuration management +│ │ ├── render.service.ts # Payment method rendering +│ │ └── root.service.ts # Root platform operations +│ │ +│ ├── clients/ # API client wrappers +│ │ ├── stripe-client.ts +│ │ └── root-client.ts +│ │ +│ ├── adapters/ # Data transformation +│ │ └── stripe-to-root-adapter.ts +│ │ +│ ├── lifecycle-hooks/ # Root platform hooks +│ │ └── index.ts # Main lifecycle hooks +│ │ +│ ├── utils/ # Utilities +│ │ ├── error.ts +│ │ └── logger.ts (legacy) +│ │ +│ ├── lifecycle-hooks.ts # Exports lifecycle hooks +│ ├── webhook-hooks.ts # Webhook processing +│ ├── config.ts # Configuration +│ └── main.ts # Entry point +│ +├── __tests__/ # Test files +│ ├── core/ +│ ├── services/ +│ ├── lifecycle-hooks/ +│ └── helpers/ +│ +└── docs/ # Documentation + ├── ARCHITECTURE.md + ├── TESTING.md + └── LOG_VIEWING.md +``` + +## Key Architectural Decisions + +### 1. Service-Oriented Architecture + +All business logic is encapsulated in services. Services are: +- **Injectable** - Accept dependencies via constructor +- **Testable** - Can be easily mocked and tested in isolation +- **Single Responsibility** - Each service has a clear, focused purpose + +### 2. Dependency Injection + +We use a simple, custom DI container that: +- Manages service lifecycles (singleton vs transient) +- Resolves dependencies automatically +- Makes testing easy through service replacement +- Avoids the complexity of heavy DI frameworks + +Example: +```typescript +// Register a service +container.register( + ServiceToken.LOG_SERVICE, + () => new LogService({ environment: 'production' }), + ServiceLifetime.SINGLETON +); + +// Resolve a service +const logService = container.resolve(ServiceToken.LOG_SERVICE); +``` + +### 3. Domain Models + +Domain models define clear contracts: +- Provider-agnostic where possible +- Specific implementations for Stripe/Root +- Located in `core/models/` +- No business logic, just data structures + +### 4. Structured Logging + +The `LogService` provides structured JSON logging to stdout for log aggregation systems (DataDog, CloudWatch, etc.) + +Features: +- Correlation IDs for request tracking +- Multiple log levels (DEBUG, INFO, WARN, ERROR) +- Metadata support +- JSON-formatted output for easy parsing + +### 5. HTML Rendering + +The `RenderService` centralizes all HTML generation: +- Payment method creation forms (Stripe Elements) +- Payment method summary views +- Payment method detail views +- XSS protection via HTML escaping + +## Data Flow + +### Payment Method Assignment + +``` +Root Platform + ↓ (afterPolicyPaymentMethodAssigned event) +Lifecycle Hook + ↓ +Implementation TODO + ├→ Create Stripe customer + ├→ Attach payment method + └→ Update Root policy with Stripe IDs +``` + +### Webhook Processing + +``` +Stripe Webhook + ↓ +webhook-hooks.ts (signature verification) + ↓ +Event Router (switch on event type) + ↓ +Event-Specific Controller + ├→ Stripe Client (get details) + ├→ RootService (update payments) + └→ LogService (record activities) +``` + +## Service Descriptions + +### LogService +- **Purpose**: Structured JSON logging to stdout +- **Key Methods**: `debug()`, `info()`, `warn()`, `error()` +- **Features**: Correlation IDs, JSON formatting, multiple log levels + +### ConfigurationService +- **Purpose**: Type-safe, validated configuration management +- **Key Methods**: `get()`, `getAll()`, `isProduction()` +- **Features**: Environment-specific configs, validation on startup + +### RenderService +- **Purpose**: Generate HTML for dashboard views +- **Key Methods**: `renderCreatePaymentMethod()`, `renderViewPaymentMethod()` +- **Features**: XSS protection, consistent styling + +### RootService +- **Purpose**: High-level Root platform operations +- **Key Methods**: `getPolicy()`, `updatePaymentStatus()`, `createPayment()` +- **Features**: Business logic for Root API interactions + +## Extension Points + +When implementing the full Stripe/Root integration: + +1. **Add Event Controllers**: Create controllers for additional Stripe webhook events +2. **Implement Lifecycle Hooks**: Add logic for payment method assignment, policy updates, etc. +3. **Add Business Services**: Create service layers for complex orchestration logic +4. **Add Validation**: Create input validation for webhook events +5. **Write Tests**: Add comprehensive test coverage for new features + +## Testing Strategy + +See [TESTING.md](./TESTING.md) for detailed testing guidelines. + +Key principles: +- Unit test all services with mocked dependencies +- Test orchestration logic in controllers +- Mock external APIs (Stripe, Root) +- Use the DI container for easy mocking + +## Configuration + +Configuration is managed via environment variables: +- Loaded in `config.ts` +- Validated on startup +- Injectable for testing + +## Error Handling + +- Custom `ModuleError` class with structured logging +- Errors include context and metadata +- Stack traces captured and logged +- User-friendly error messages for dashboard + +## Security + +- Webhook signature verification +- HTML escaping for XSS prevention +- Sensitive data not logged +- Secure environment variable handling + +## Next Steps + +1. Implement additional Stripe event controllers as needed +2. Add lifecycle hook implementations for your specific use case +3. Create integration tests for critical workflows +4. Set up monitoring and alerting via DataDog/CloudWatch +5. Performance optimization and error handling improvements + +## Questions? + +For questions or clarifications, consult: +- [TESTING.md](./TESTING.md) - Testing guide +- [CUSTOMIZING.md](./CUSTOMIZING.md) - Implementation guide +- Source code comments and JSDoc + + + + diff --git a/stripe_collection_module/docs/BEST_PRACTICES.md b/stripe_collection_module/docs/BEST_PRACTICES.md new file mode 100644 index 0000000..9f53941 --- /dev/null +++ b/stripe_collection_module/docs/BEST_PRACTICES.md @@ -0,0 +1,637 @@ +# Best Practices for Collection Modules + +This guide covers production best practices for building and maintaining collection modules. + +## Table of Contents + +- [Architecture](#architecture) +- [Error Handling](#error-handling) +- [Performance](#performance) +- [Security](#security) +- [Testing](#testing) +- [Logging & Monitoring](#logging--monitoring) +- [Cost Optimization](#cost-optimization) +- [Maintenance](#maintenance) + +--- + +## Architecture + +### Use Dependency Injection + +✅ **Do:** +```typescript +class PaymentService { + constructor( + private readonly client: PaymentClient, + private readonly logger: LogService + ) {} +} +``` + +❌ **Don't:** +```typescript +class PaymentService { + private client = new PaymentClient(); // Hard dependency + private logger = console; // Global dependency +} +``` + +### Keep Services Stateless + +✅ **Do:** +```typescript +class PaymentService { + async processPayment(request: PaymentRequest) { + // No instance state, idempotent operation + return await this.client.create(request); + } +} +``` + +❌ **Don't:** +```typescript +class PaymentService { + private processedIds = new Set(); // State won't persist across Lambda invocations +} +``` + +### Separate Concerns + +✅ **Do:** +- **Clients**: API communication only +- **Services**: Business logic only +- **Controllers**: Event routing only +- **Models**: Data structures only + +❌ **Don't:** +- Mix API calls with business logic +- Put business logic in controllers +- Use models for data transformation + +--- + +## Error Handling + +### Use Structured Errors + +✅ **Do:** +```typescript +class PaymentError extends Error { + constructor( + message: string, + public readonly code: string, + public readonly retryable: boolean, + public readonly context?: Record + ) { + super(message); + this.name = 'PaymentError'; + } +} + +throw new PaymentError( + 'Payment failed', + 'PAYMENT_DECLINED', + false, // Don't retry + { orderId: '123', reason: 'insufficient_funds' } +); +``` + +❌ **Don't:** +```typescript +throw new Error('Payment failed'); // No context, unclear if retryable +``` + +### Implement Retry Logic + +✅ **Do:** +```typescript +async function withRetry( + operation: () => Promise, + maxRetries: number = 3 +): Promise { + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + return await operation(); + } catch (error) { + if (!error.retryable || attempt === maxRetries - 1) { + throw error; + } + await sleep(Math.pow(2, attempt) * 1000); // Exponential backoff + } + } +} +``` + +### Handle Timeouts + +✅ **Do:** +```typescript +async function withTimeout( + operation: Promise, + timeoutMs: number +): Promise { + return Promise.race([ + operation, + new Promise((_, reject) => + setTimeout(() => reject(new Error('Operation timeout')), timeoutMs) + ) + ]); +} +``` + +--- + +## Performance + +### Optimize Cold Starts + +✅ **Do:** +- Minimize dependencies +- Lazy-load heavy modules +- Use ARM architecture (Graviton2) +- Increase memory allocation (faster CPU) + +```typescript +// Lazy load heavy dependencies +let stripeSDK: any; +async function getStripeSDK() { + if (!stripeSDK) { + stripeSDK = await import('stripe'); + } + return stripeSDK; +} +``` + +### Use Connection Pooling + +✅ **Do:** +```typescript +// Reuse client across invocations +let cachedClient: StripeClient; + +export function getStripeClient(): StripeClient { + if (!cachedClient) { + cachedClient = new StripeClient(); + } + return cachedClient; +} +``` + +❌ **Don't:** +```typescript +// Creates new client every time +export function processPayment() { + const client = new StripeClient(); // Slow! + // ... +} +``` + +### Batch Operations + +✅ **Do:** +```typescript +// Process multiple items in one call +const payments = await Promise.all( + policyIds.map(id => rootClient.getPayment(id)) +); +``` + +❌ **Don't:** +```typescript +// Sequential calls - slow! +for (const id of policyIds) { + const payment = await rootClient.getPayment(id); +} +``` + +### Rate Limiting + +✅ **Do:** +```typescript +class RateLimiter { + private queue: Array<() => void> = []; + private processing = 0; + + constructor(private maxConcurrent: number = 10) {} + + async execute(operation: () => Promise): Promise { + while (this.processing >= this.maxConcurrent) { + await new Promise(resolve => this.queue.push(resolve)); + } + + this.processing++; + try { + return await operation(); + } finally { + this.processing--; + this.queue.shift()?.(); + } + } +} +``` + +--- + +## Security + +### Never Log Sensitive Data + +✅ **Do:** +```typescript +logger.info('Payment processed', 'PaymentService', { + paymentId: payment.id, + amount: payment.amount, + // Don't log: card numbers, API keys, PII +}); +``` + +❌ **Don't:** +```typescript +logger.info('Payment processed', payment); // May contain sensitive data! +``` + +### Validate All Inputs + +✅ **Do:** +```typescript +import Joi from 'joi'; + +const paymentSchema = Joi.object({ + amount: Joi.number().positive().required(), + currency: Joi.string().length(3).required(), + policyId: Joi.string().uuid().required() +}); + +export async function createPayment(data: unknown) { + const validated = await paymentSchema.validateAsync(data); + // Now safely use validated data +} +``` + +### Verify Webhook Signatures + +✅ **Do:** +```typescript +export async function handleWebhook(event: any) { + // ALWAYS verify signature first + const isValid = stripe.webhooks.constructEvent( + event.body, + event.headers['stripe-signature'], + webhookSecret + ); + + if (!isValid) { + return { statusCode: 400, body: 'Invalid signature' }; + } + + // Process webhook +} +``` + +❌ **Don't:** +```typescript +export async function handleWebhook(event: any) { + // Process webhook WITHOUT verification - dangerous! + await processPayment(event.data); +} +``` + +### Use Environment-Specific Keys + +✅ **Do:** +```typescript +const config = { + development: { + stripeKey: 'sk_test_xxxxx', // Test key + rootApiKey: 'sandbox_xxxxx' // Sandbox key + }, + production: { + stripeKey: process.env.STRIPE_KEY_LIVE, // From Secrets Manager + rootApiKey: process.env.ROOT_KEY_LIVE + } +}; +``` + +### Implement API Key Rotation + +✅ **Do:** +- Store keys in AWS Secrets Manager +- Support multiple valid keys during rotation +- Audit key usage in CloudWatch +- Rotate keys every 90 days + +--- + +## Testing + +### Test Coverage Goals + +- **Critical paths**: 90%+ coverage +- **Services**: 80%+ coverage +- **Overall**: 70%+ coverage + +### Test Pyramid + +``` + E2E Tests (10%) + Integration Tests (30%) + Unit Tests (60%) +``` + +### Write Meaningful Tests + +✅ **Do:** +```typescript +it('should create payment and update Root when invoice is paid', async () => { + // Arrange + const invoice = createMockInvoice({ amount: 1000 }); + mockStripeClient.getInvoice.mockResolvedValue(invoice); + mockRootClient.createPayment.mockResolvedValue({ id: 'pay_123' }); + + // Act + await processInvoicePaid({ invoiceId: invoice.id }); + + // Assert + expect(mockRootClient.createPayment).toHaveBeenCalledWith({ + policyId: invoice.metadata.policyId, + amount: 10.00, + status: 'succeeded' + }); +}); +``` + +❌ **Don't:** +```typescript +it('works', async () => { + const result = await service.doSomething(); + expect(result).toBeDefined(); // Too vague +}); +``` + +### Use Test Factories + +✅ **Do:** +```typescript +// __tests__/helpers/factories.ts +export function createMockPayment(overrides?: Partial): Payment { + return { + id: 'pay_123', + amount: 100, + status: 'succeeded', + ...overrides + }; +} + +// In tests +const payment = createMockPayment({ amount: 500 }); +``` + +--- + +## Logging & Monitoring + +### Use Structured Logging + +✅ **Do:** +```typescript +logger.info('Payment processed', 'PaymentService', { + paymentId: 'pay_123', + amount: 100, + currency: 'USD', + policyId: 'pol_456', + duration: 234 // ms +}); +``` + +❌ **Don't:** +```typescript +console.log('Payment pay_123 processed for $100 USD'); // Unstructured +``` + +### Log Appropriate Levels + +- **ERROR**: Something failed, needs attention +- **WARN**: Something unexpected, but handled +- **INFO**: Important business events +- **DEBUG**: Detailed information (dev only) + +✅ **Do:** +```typescript +logger.info('Payment created', 'PaymentService', { paymentId }); +logger.warn('Retrying failed API call', 'StripeClient', { attempt: 2 }); +logger.error('Payment failed', 'PaymentService', { error, paymentId }); +``` + +### Include Correlation IDs + +✅ **Do:** +```typescript +const correlationId = event.requestContext?.requestId || generateId(); + +logger.info('Processing request', 'Handler', { correlationId }); +// Pass correlationId through all operations +``` + +### Set Up Alarms + +Create CloudWatch Alarms for: +- **Error rate** > 5% over 5 minutes +- **Timeout rate** > 1% over 5 minutes +- **API latency** > 3 seconds (p95) +- **Memory usage** > 80% + +### Create Dashboards + +Monitor: +- Request volume +- Error rates by type +- Latency percentiles (p50, p95, p99) +- Cold start frequency +- Memory utilization +- Cost per invocation + +--- + +## Cost Optimization + +### Right-Size Memory + +✅ **Do:** +- Start with 512 MB +- Monitor actual usage in CloudWatch +- Adjust based on metrics +- Test different configurations + +### Use ARM Architecture + +✅ **Do:** +- Use ARM64 (Graviton2) for 20% cost savings +- Same or better performance +- Ensure dependencies support ARM + +### Optimize Timeouts + +✅ **Do:** +- Set realistic timeouts +- Don't over-provision (default 3s often sufficient) +- Monitor actual duration +- Fail fast on errors + +### Manage Log Retention + +✅ **Do:** +```bash +# Set appropriate retention +aws logs put-retention-policy \ + --log-group-name /aws/lambda/your-function \ + --retention-in-days 30 # or 7 for dev +``` + +### Use Reserved Concurrency Wisely + +❌ **Don't:** +- Reserve concurrency unless you have a specific need +- It costs money even when not used + +✅ **Do:** +- Use on-demand scaling +- Set account-level concurrency limits if needed + +--- + +## Maintenance + +### Version Control + +✅ **Do:** +- Use semantic versioning +- Tag releases +- Maintain changelog +- Document breaking changes + +### Code Reviews + +✅ **Do:** +- Review all code changes +- Check test coverage +- Verify error handling +- Review security implications + +### Documentation + +✅ **Do:** +- Keep README up to date +- Document configuration changes +- Maintain runbooks +- Document incident responses + +### Monitoring + +✅ **Do:** +- Review logs weekly +- Check dashboards daily +- Investigate anomalies +- Track key metrics + +### Regular Updates + +✅ **Do:** +- Update dependencies monthly +- Review security advisories +- Test after updates +- Rotate API keys quarterly + +--- + +## Anti-Patterns + +### Don't Use Global State + +❌ **Don't:** +```typescript +let processedEvents = []; // Won't work across Lambda invocations + +export async function processEvent(event: any) { + if (processedEvents.includes(event.id)) return; + processedEvents.push(event.id); +} +``` + +### Don't Ignore Errors + +❌ **Don't:** +```typescript +try { + await updatePayment(); +} catch (error) { + // Silent failure - dangerous! +} +``` + +### Don't Block Event Loop + +❌ **Don't:** +```typescript +// Synchronous crypto operations block event loop +const hash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512'); +``` + +✅ **Do:** +```typescript +// Use async version +const hash = await crypto.pbkdf2(password, salt, 10000, 64, 'sha512'); +``` + +### Don't Trust User Input + +❌ **Don't:** +```typescript +const sql = `SELECT * FROM payments WHERE id = ${req.body.id}`; +``` + +✅ **Do:** +```typescript +// Validate and sanitize +const paymentId = paymentIdSchema.validateSync(req.body.id); +const payment = await db.getPayment(paymentId); +``` + +--- + +## Quick Reference Checklist + +### Before Deployment +- [ ] All tests passing (>70% coverage) +- [ ] No sensitive data in logs +- [ ] Error handling implemented +- [ ] Webhook signature verification +- [ ] Configuration validated +- [ ] Dependencies updated +- [ ] Security review completed +- [ ] Documentation updated + +### Production Monitoring +- [ ] CloudWatch Alarms configured +- [ ] Dashboard created +- [ ] Log retention set +- [ ] Metrics tracked +- [ ] On-call rotation defined +- [ ] Runbook documented + +### Regular Maintenance +- [ ] Review logs weekly +- [ ] Update dependencies monthly +- [ ] Rotate keys quarterly +- [ ] Review metrics monthly +- [ ] Update documentation as needed + +--- + +## Resources + +- [AWS Lambda Best Practices](https://docs.aws.amazon.com/lambda/latest/dg/best-practices.html) +- [Node.js Best Practices](https://github.com/goldbergyoni/nodebestpractices) +- [TypeScript Best Practices](https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html) +- [Logging Best Practices](https://aws.amazon.com/blogs/mt/best-practices-cloudwatch-logs/) + diff --git a/stripe_collection_module/docs/CODE_STRUCTURE.md b/stripe_collection_module/docs/CODE_STRUCTURE.md new file mode 100644 index 0000000..0281fbe --- /dev/null +++ b/stripe_collection_module/docs/CODE_STRUCTURE.md @@ -0,0 +1,395 @@ +# Code Structure Guide + +This document provides an overview of the codebase organization and links to detailed documentation for each area. + +## Directory Overview + +``` +stripe_collection_module/ +├── code/ # Source code +│ ├── adapters/ # Data transformation layer +│ ├── clients/ # External API wrappers +│ ├── controllers/ # Event processors & orchestration +│ ├── core/ # DI container & infrastructure +│ ├── interfaces/ # TypeScript interfaces +│ ├── lifecycle-hooks/ # Root Platform callbacks +│ ├── services/ # Business logic layer +│ ├── utils/ # Utility functions +│ ├── main.ts # Entry point +│ └── webhook-hooks.ts # Stripe webhook handler +│ +├── __tests__/ # Test files +│ ├── core/ # Core tests +│ ├── helpers/ # Test utilities +│ ├── lifecycle-hooks/ # Hook tests +│ └── services/ # Service tests +│ +├── docs/ # Documentation +│ ├── ARCHITECTURE.md # System architecture +│ ├── BEST_PRACTICES.md # Coding guidelines +│ ├── CODE_STRUCTURE.md # This file +│ ├── CUSTOMIZING.md # Implementation guide +│ ├── DEPLOYMENT.md # Deployment guide +│ ├── LOG_VIEWING.md # Logging guide +│ ├── ROOT_CONFIGURATION.md # Root Platform config +│ ├── SETUP.md # Setup instructions +│ ├── TESTING.md # Testing guide +│ └── WEBHOOKS.md # Webhook guide +│ +└── scripts/ # Deployment scripts + ├── deploy.sh # Deployment automation + └── validate-config.sh # Config validation +``` + +## Code Organization + +### Layer Architecture + +The codebase follows a clean layered architecture: + +``` +┌─────────────────────────────────────────┐ +│ Entry Points (main.ts, webhook-hooks) │ +├─────────────────────────────────────────┤ +│ Lifecycle Hooks & Controllers │ ← Orchestration layer +├─────────────────────────────────────────┤ +│ Services │ ← Business logic layer +├─────────────────────────────────────────┤ +│ Adapters & Clients │ ← Infrastructure layer +├─────────────────────────────────────────┤ +│ External APIs (Stripe, Root) │ +└─────────────────────────────────────────┘ +``` + +Each layer has a specific responsibility and only depends on layers below it. + +## Detailed Documentation + +### 📁 [Adapters](../code/adapters/README.md) + +Data transformation between Stripe and Root formats. + +**Key Topics**: +- What are adapters and when to use them +- StripeToRootAdapter usage examples +- Creating custom adapters +- Testing adapters + +**Quick Example**: +```typescript +import StripeToRootAdapter from './adapters/stripe-to-root-adapter'; + +const adapter = new StripeToRootAdapter(); +const rootPayment = adapter.convertInvoiceToRootPayment(invoice, { + status: PaymentStatus.Successful, +}); +``` + +--- + +### 📁 [Clients](../code/clients/README.md) + +Thin wrappers around external API SDKs. + +**Key Topics**: +- StripeClient configuration and usage +- RootClient singleton pattern +- Error handling in clients +- Testing with mocked clients +- Creating new API clients + +**Quick Example**: +```typescript +import StripeClient from './clients/stripe-client'; +import rootClient from './clients/root-client'; + +const stripeClient = new StripeClient(); +const customer = await stripeClient.stripeSDK.customers.create({...}); + +const policy = await rootClient.SDK.getPolicyById({ policyId: 'pol_123' }); +``` + +--- + +### 📁 [Controllers](../code/controllers/README.md) + +Event processors that orchestrate service calls. + +**Key Topics**: +- Controller architecture and patterns +- Stripe webhook event processors +- Root lifecycle event processors +- Dependency injection in controllers +- Creating new controllers +- Testing controllers + +**Quick Example**: +```typescript +export class InvoicePaidController { + constructor( + private readonly logService: LogService, + private readonly rootService: RootService + ) {} + + async handle(invoice: Stripe.Invoice): Promise { + // Orchestrate the workflow + } +} +``` + +--- + +### 📁 [Core](../code/core/README.md) + +Dependency injection container and infrastructure. + +**Key Topics**: +- DI container usage +- Service registration patterns +- Service tokens +- Singleton vs Transient lifetimes +- Testing with DI container + +**Quick Example**: +```typescript +import { getContainer } from './core/container.setup'; +import { ServiceToken } from './core/container'; + +const container = getContainer(); +const logService = container.resolve(ServiceToken.LOG_SERVICE); +``` + +--- + +### 📁 [Lifecycle Hooks](../code/lifecycle-hooks/README.md) + +Root Platform callback functions. + +**Key Topics**: +- What are lifecycle hooks +- Available hook functions +- Payment method hooks +- Policy lifecycle hooks +- Payment lifecycle hooks +- Implementation patterns +- Testing hooks + +**Quick Example**: +```typescript +export async function afterPolicyPaymentMethodAssigned({ policy }) { + // React to payment method assignment + await assignPaymentMethodToStripe(policy); +} +``` + +--- + +### 📁 [Services](../code/services/README.md) + +Business logic and domain operations. + +**Key Topics**: +- Service layer overview +- LogService - structured logging +- ConfigurationService - type-safe config +- RenderService - HTML generation +- RootService - Root Platform operations +- Creating custom services +- Testing services + +**Quick Example**: +```typescript +export class RootService { + constructor(private readonly logService: LogService) {} + + async updatePaymentStatus(params: UpdateParams): Promise { + // Business logic here + } +} +``` + +--- + +### 📁 [Utils](../code/utils/README.md) + +Reusable utility functions and helpers. + +**Key Topics**: +- Error types and handling +- Retry logic with exponential backoff +- Timeout utilities +- Error categorization +- Best practices + +**Quick Example**: +```typescript +import { retryWithBackoff, withTimeout } from './utils'; + +const result = await retryWithBackoff( + () => externalApiCall(), + { maxRetries: 3 } +); +``` + +--- + +## Cross-Cutting Concerns + +### Configuration + +Configuration is managed centrally: +- **Source**: `code/env.ts` - Environment variables +- **Service**: `code/services/config.service.ts` - Type-safe access +- **Documentation**: [ROOT_CONFIGURATION.md](./ROOT_CONFIGURATION.md) + +### Logging + +Structured logging throughout: +- **Service**: `code/services/log.service.ts` - LogService +- **Output**: JSON to stdout (CloudWatch/DataDog) +- **Documentation**: [LOG_VIEWING.md](./LOG_VIEWING.md) + +### Error Handling + +Consistent error handling: +- **Errors**: `code/utils/error.ts` and `error-types.ts` +- **Pattern**: Catch, log, re-throw with context +- **Documentation**: [BEST_PRACTICES.md](./BEST_PRACTICES.md#error-handling) + +### Testing + +Comprehensive test coverage: +- **Location**: `__tests__/` directory +- **Pattern**: Unit tests with mocked dependencies +- **Documentation**: [TESTING.md](./TESTING.md) + +## Common Workflows + +### Adding a New Feature + +1. **Create Service** (if needed) + - Add business logic in `code/services/` + - See: [Services README](../code/services/README.md) + +2. **Create Controller** (if handling events) + - Add event processor in `code/controllers/` + - See: [Controllers README](../code/controllers/README.md) + +3. **Register in DI Container** + - Update `code/core/container.setup.ts` + - See: [Core README](../code/core/README.md) + +4. **Wire in Entry Point** + - Update `code/webhook-hooks.ts` or `code/lifecycle-hooks/` + - See: [Lifecycle Hooks README](../code/lifecycle-hooks/README.md) + +5. **Add Tests** + - Create test file in `__tests__/` + - See: [TESTING.md](./TESTING.md) + +### Handling a Webhook + +``` +Stripe Webhook + ↓ +webhook-hooks.ts (signature verification, routing) + ↓ +Controller (orchestration) + ↓ +Services (business logic) + ↓ +Clients/Adapters (external APIs) +``` + +See: [WEBHOOKS.md](./WEBHOOKS.md) + +### Processing a Lifecycle Hook + +``` +Root Platform Event + ↓ +lifecycle-hooks/index.ts (hook function) + ↓ +Services (business logic) + ↓ +Clients (external APIs) +``` + +See: [Lifecycle Hooks README](../code/lifecycle-hooks/README.md) + +## Key Principles + +### 1. Separation of Concerns + +- **Hooks/Controllers**: Orchestration +- **Services**: Business logic +- **Clients**: API calls +- **Adapters**: Data transformation +- **Utils**: Reusable helpers + +### 2. Dependency Injection + +All dependencies injected via constructor. + +### 3. Type Safety + +TypeScript with interfaces and type-safe configuration. + +### 4. Testability + +Unit tests with mocked dependencies, integration tests with test containers. + +### 5. Logging & Observability + +Structured JSON logs with correlation IDs and error context. + +## Getting Started + +### For New Developers + +1. Read [SETUP.md](./SETUP.md) - Set up your environment +2. Read [ARCHITECTURE.md](./ARCHITECTURE.md) - Understand the system +3. Read this document - Navigate the codebase +4. Start with a simple task - Add a log statement or test + +### For Implementation + +1. Read [CUSTOMIZING.md](./CUSTOMIZING.md) - Implementation guide +2. Read component READMEs - Detailed documentation +3. Look at existing code - Examples and patterns +4. Write tests first - TDD approach + +### For Deployment + +1. Read [ROOT_CONFIGURATION.md](./ROOT_CONFIGURATION.md) - Configuration +2. Read [DEPLOYMENT.md](./DEPLOYMENT.md) - Deployment process +3. Test in sandbox - Verify before production + +## Need Help? + +### Finding Code + +Use the directory structure above and component READMEs to navigate. + +### Understanding Patterns + +Each README includes: +- Architecture diagrams +- Usage examples +- Best practices +- Anti-patterns + +### Troubleshooting + +- Check relevant README for your component +- Review [BEST_PRACTICES.md](./BEST_PRACTICES.md) +- Look at test files for examples +- Check logs with [LOG_VIEWING.md](./LOG_VIEWING.md) + +## Related Documentation + +- **[ARCHITECTURE.md](./ARCHITECTURE.md)** - High-level system design +- **[BEST_PRACTICES.md](./BEST_PRACTICES.md)** - Coding standards +- **[TESTING.md](./TESTING.md)** - Testing strategy +- **[CUSTOMIZING.md](./CUSTOMIZING.md)** - Implementation guide + diff --git a/stripe_collection_module/docs/CUSTOMIZING.md b/stripe_collection_module/docs/CUSTOMIZING.md new file mode 100644 index 0000000..e2651dd --- /dev/null +++ b/stripe_collection_module/docs/CUSTOMIZING.md @@ -0,0 +1,635 @@ +# Stripe Implementation Guide + +This guide explains how to complete the Stripe integration in this collection module template. + +## Overview + +The template provides a complete structure with stub implementations. You'll need to implement the actual Stripe integration by completing the service methods, configuring the Stripe client, and implementing webhook handlers. + +--- + +## Implementation Strategy + +### 1. Understand the Template Structure + +The template follows a clean architecture: + +``` +Stripe SDK Client → Stripe Service → Controllers → Root Platform Integration + ↓ + Root API +``` + +### What's Already Done + +✅ **Architecture**: +- Dependency injection container +- Configuration management +- Logging infrastructure +- Error handling patterns + +✅ **Structure**: +- Service stubs with method signatures +- Event processor templates +- Test infrastructure +- Documentation + +### What You Need to Implement + +❌ **Stripe Integration**: +- Complete service stub methods +- Configure Stripe SDK client +- Implement webhook handlers +- Add data transformation logic + +--- + +## Step-by-Step Implementation + +### Step 1: Configure Stripe Client + +**File**: `code/clients/stripe-client.ts` + +Complete the Stripe client initialization: + +```typescript +import Stripe from 'stripe'; +import { getConfigService } from '../services/config-instance'; + +export class StripeClient { + private stripe: Stripe; + + constructor(apiKey?: string) { + const config = getConfigService(); + const key = apiKey || config.get('stripeSecretKey'); + + // Initialize Stripe SDK + this.stripe = new Stripe(key, { + apiVersion: '2023-10-16', // Use latest stable version + typescript: true, + }); + } + + /** + * Get the Stripe SDK instance + */ + public getClient(): Stripe { + return this.stripe; + } + + /** + * Verify webhook signature + */ + public constructWebhookEvent( + payload: string | Buffer, + signature: string, + secret: string + ): Stripe.Event { + return this.stripe.webhooks.constructEvent(payload, signature, secret); + } +} +``` + +### Step 2: Implement Stripe Service Methods + +**File**: `code/services/stripe.service.ts` + +Complete the stub methods with actual Stripe API calls: + +```typescript +/** + * Create a Stripe customer + */ +async createCustomer(params: { + email?: string; + name?: string; + phone?: string; + paymentMethod?: string; + metadata?: Record; +}): Promise { + this.logService.info('Creating Stripe customer', 'CustomerService', params); + + try { + const customer = await this.stripeClient.getClient().customers.create({ + email: params.email, + name: params.name, + phone: params.phone, + payment_method: params.paymentMethod, + metadata: params.metadata, + }); + + this.logService.info('Customer created successfully', 'CustomerService', { + customerId: customer.id, + }); + + return this.mapStripeCustomer(customer); + } catch (error) { + this.logService.error( + `Failed to create customer: ${error.message}`, + 'CustomerService', + { error } + ); + throw error; + } +} + +/** + * Map Stripe customer to domain model + */ +private mapStripeCustomer(stripeCustomer: Stripe.Customer): StripeCustomer { + return { + id: stripeCustomer.id, + email: stripeCustomer.email, + name: stripeCustomer.name, + phone: stripeCustomer.phone, + defaultPaymentMethodId: stripeCustomer.invoice_settings?.default_payment_method as string, + created: new Date(stripeCustomer.created * 1000), + metadata: stripeCustomer.metadata, + }; +} +``` + +**Complete these methods**: +- `createCustomer()` - Create Stripe customer +- `getCustomer()` - Retrieve customer by ID +- `updateCustomer()` - Update customer details +- `createSubscription()` - Create recurring subscription +- `getSubscription()` - Get subscription details +- `updateSubscription()` - Update subscription +- `cancelSubscription()` - Cancel subscription +- `createPayment()` - Create one-time payment +- `refundPayment()` - Process refund +- `attachPaymentMethod()` - Attach payment method to customer + +### Step 3: Implement Webhook Event Processors + +**File**: `code/controllers/stripe-event-processors/processInvoicePaidEventController.ts` + +Implement handlers for each Stripe webhook event: + +```typescript +import { getLogService } from '../../services/log-instance'; +import { RootService } from '../../services/root.service'; +import Stripe from 'stripe'; + +export async function processInvoicePaid(event: Stripe.Event): Promise { + const logService = getLogService(); + const invoice = event.data.object as Stripe.Invoice; + + logService.info('Processing invoice.paid event', 'InvoicePaidController', { + invoiceId: invoice.id, + customerId: invoice.customer, + amount: invoice.amount_paid, + }); + + try { + // 1. Get Root policy ID from invoice metadata + const rootPolicyId = invoice.metadata?.rootPolicyId; + + if (!rootPolicyId) { + logService.warn('No Root policy ID in invoice metadata', 'InvoicePaidController'); + return; + } + + // 2. Create payment record in Root + const rootService = new RootService(/* inject dependencies */); + await rootService.createPayment({ + policyId: rootPolicyId, + amount: invoice.amount_paid / 100, // Convert cents to dollars + currency: invoice.currency, + status: 'succeeded', + paymentDate: new Date(invoice.status_transitions.paid_at! * 1000), + externalId: invoice.payment_intent as string, + metadata: { + invoiceId: invoice.id, + provider: 'stripe', + }, + }); + + logService.info('Invoice payment processed successfully', 'InvoicePaidController'); + } catch (error) { + logService.error( + `Failed to process invoice payment: ${error.message}`, + 'InvoicePaidController', + { invoiceId: invoice.id, error } + ); + throw error; + } +} +``` + +**Implement these event processors**: +- `processInvoicePaidEventController.ts` - Payment succeeded +- `processInvoicePaymentFailedEventController.ts` - Payment failed +- `processInvoiceCreatedEventController.ts` - Invoice created +- `processPaymentIntentSucceededEventController.ts` - Payment intent succeeded +- `processPaymentIntentFailedEventController.ts` - Payment intent failed +- `processChargeDisputedEventController.ts` - Dispute created +- `processInvoiceChargeRefundedEventController.ts` - Refund processed +- `processSubscriptionScheduleUpdatedEventController.ts` - Subscription changed + +### Step 4: Wire Webhook Handler + +**File**: `code/webhook-hooks.ts` + +Connect webhook events to processors: + +```typescript +import { getLogService } from './services/log-instance'; +import { getConfigService } from './services/config-instance'; +import { StripeClient } from './clients/stripe-client'; +import Stripe from 'stripe'; + +// Import event processors +import { processInvoicePaid } from './controllers/stripe-event-processors/processInvoicePaidEventController'; +import { processInvoicePaymentFailed } from './controllers/stripe-event-processors/processInvoicePaymentFailedEventController'; +// ... import other processors + +export async function processStripeWebhook(event: any): Promise { + const logService = getLogService(); + const config = getConfigService(); + + try { + // 1. Verify webhook signature + const stripeClient = new StripeClient(); + const signature = event.headers['stripe-signature']; + const webhookSecret = config.get('stripeWebhookSigningSecret'); + + const stripeEvent = stripeClient.constructWebhookEvent( + event.body, + signature, + webhookSecret + ); + + logService.info(`Processing Stripe webhook: ${stripeEvent.type}`, 'WebhookHandler', { + eventId: stripeEvent.id, + }); + + // 2. Route to appropriate processor + switch (stripeEvent.type) { + case 'invoice.paid': + await processInvoicePaid(stripeEvent); + break; + + case 'invoice.payment_failed': + await processInvoicePaymentFailed(stripeEvent); + break; + + case 'payment_intent.succeeded': + await processPaymentIntentSucceeded(stripeEvent); + break; + + case 'payment_intent.payment_failed': + await processPaymentIntentFailed(stripeEvent); + break; + + case 'charge.dispute.created': + await processChargeDisputed(stripeEvent); + break; + + default: + logService.info(`Unhandled event type: ${stripeEvent.type}`, 'WebhookHandler'); + } + + return { + statusCode: 200, + body: JSON.stringify({ received: true }), + }; + } catch (error) { + logService.error(`Webhook processing failed: ${error.message}`, 'WebhookHandler'); + return { + statusCode: 400, + body: JSON.stringify({ error: error.message }), + }; + } +} +``` + +### Step 5: Implement Lifecycle Hooks + +**File**: `code/lifecycle-hooks/index.ts` + +Connect Root Platform lifecycle hooks to Stripe operations: + +```typescript +import { getLogService } from '../services/log-instance'; +import { StripeService } from '../services/stripe.service'; +import { RootService } from '../services/root.service'; + +/** + * Called when a payment method is assigned to a policy + */ +export async function afterPolicyPaymentMethodAssigned(event: any): Promise { + const logService = getLogService(); + + logService.info('Processing payment method assignment', 'LifecycleHooks', { + policyId: event.policyId, + paymentMethodId: event.paymentMethodId, + }); + + try { + // 1. Get policy details from Root + const rootService = new RootService(/* dependencies */); + const policy = await rootService.getPolicy(event.policyId); + const policyholder = await rootService.getPolicyholder(policy.policyholderId); + + // 2. Create or get Stripe customer + const stripeService = new StripeService(/* dependencies */); + let customerId = policy.metadata?.stripeCustomerId; + + if (!customerId) { + const customer = await stripeService.createCustomer({ + email: policyholder.email, + name: `${policyholder.firstName} ${policyholder.lastName}`, + phone: policyholder.phone, + metadata: { + rootPolicyId: policy.id, + rootPolicyholderId: policyholder.id, + }, + }); + customerId = customer.id; + + // Update policy with Stripe customer ID + await rootService.updatePolicy(policy.id, { + metadata: { + ...policy.metadata, + stripeCustomerId: customerId, + }, + }); + } + + // 3. Create Stripe subscription + const subscription = await stripeService.createSubscription({ + customerId, + priceId: policy.metadata?.stripePriceId, + amount: policy.monthlyPremium, + currency: policy.currency, + metadata: { + rootPolicyId: policy.id, + }, + }); + + // 4. Update policy with subscription ID + await rootService.updatePolicy(policy.id, { + metadata: { + ...policy.metadata, + stripeSubscriptionId: subscription.id, + }, + }); + + logService.info('Payment method assignment completed', 'LifecycleHooks'); + } catch (error) { + logService.error(`Failed to process payment method assignment: ${error.message}`, 'LifecycleHooks'); + throw error; + } +} +``` + +### Step 6: Add Data Transformation + +**File**: `code/adapters/stripe-to-root-adapter.ts` + +Implement adapters to transform Stripe data to Root format: + +```typescript +import Stripe from 'stripe'; +import { RootPayment, RootPaymentStatus } from '../interfaces/root-payment'; + +/** + * Map Stripe payment intent to Root payment + */ +export function mapStripePaymentIntentToRootPayment( + paymentIntent: Stripe.PaymentIntent +): Partial { + return { + externalId: paymentIntent.id, + amount: paymentIntent.amount / 100, // Convert cents to dollars + currency: paymentIntent.currency.toUpperCase(), + status: mapStripePaymentStatus(paymentIntent.status), + paymentDate: new Date(paymentIntent.created * 1000), + metadata: { + provider: 'stripe', + paymentMethod: paymentIntent.payment_method, + receiptUrl: paymentIntent.charges?.data[0]?.receipt_url, + }, + }; +} + +/** + * Map Stripe payment status to Root status + */ +function mapStripePaymentStatus(stripeStatus: string): RootPaymentStatus { + const statusMap: Record = { + 'succeeded': 'succeeded', + 'processing': 'pending', + 'requires_payment_method': 'pending', + 'requires_confirmation': 'pending', + 'requires_action': 'pending', + 'canceled': 'canceled', + 'failed': 'failed', + }; + + return statusMap[stripeStatus] || 'pending'; +} +``` + +### Step 7: Add Input Validation + +**File**: `code/validation/stripe-schemas.ts` + +Add Joi validation schemas for Stripe data: + +```typescript +import Joi from 'joi'; + +export const createCustomerSchema = Joi.object({ + email: Joi.string().email().required(), + name: Joi.string().min(1).max(200).required(), + phone: Joi.string().optional(), + metadata: Joi.object().optional(), +}); + +export const createSubscriptionSchema = Joi.object({ + customerId: Joi.string().required(), + priceId: Joi.string().optional(), + amount: Joi.number().positive().required(), + currency: Joi.string().length(3).required(), + interval: Joi.string().valid('day', 'week', 'month', 'year').required(), + metadata: Joi.object().optional(), +}); + +/** + * Validate and return typed data + */ +export async function validateCreateCustomer(data: unknown) { + return await createCustomerSchema.validateAsync(data); +} +``` + +--- + +## Testing Your Implementation + +### Unit Tests + +Test each service method in isolation: + +```typescript +describe('CustomerService', () => { + let customerService: CustomerService; + let mockStripeClient: jest.Mocked; + let mockLogService: jest.Mocked; + + beforeEach(() => { + mockStripeClient = createMockStripeClient(); + mockLogService = createMockLogService(); + customerService = new CustomerService(mockLogService, mockStripeClient); + }); + + describe('createCustomer', () => { + it('should create a Stripe customer', async () => { + const params = { + email: 'test@example.com', + name: 'Test User', + }; + + mockStripeClient.getClient().customers.create.mockResolvedValue({ + id: 'cus_123', + email: params.email, + name: params.name, + } as any); + + const customer = await stripeService.createCustomer(params); + + expect(customer.id).toBe('cus_123'); + expect(mockStripeClient.getClient().customers.create).toHaveBeenCalledWith({ + email: params.email, + name: params.name, + }); + }); + }); +}); +``` + +### Integration Tests + +Test complete workflows: + +```typescript +describe('Stripe Integration', () => { + it('should create customer and subscription', async () => { + // 1. Create customer + const customer = await stripeService.createCustomer({ + email: 'test@example.com', + name: 'Test User', + }); + + expect(customer.id).toBeDefined(); + + // 2. Create subscription + const subscription = await stripeService.createSubscription({ + customerId: customer.id, + amount: 1000, + currency: 'usd', + interval: 'month', + }); + + expect(subscription.id).toBeDefined(); + expect(subscription.customerId).toBe(customer.id); + }); +}); +``` + +--- + +## Common Patterns + +### Retry Logic for API Calls + +```typescript +import { retryWithBackoff } from '../utils/retry'; + +async createCustomer(params: any) { + return retryWithBackoff( + () => this.stripeClient.getClient().customers.create(params), + { + maxRetries: 3, + initialDelay: 1000, + shouldRetry: (error) => error.statusCode >= 500 || error.code === 'ETIMEDOUT', + } + ); +} +``` + +### Idempotency for Webhooks + +```typescript +const processedEvents = new Set(); + +export async function processInvoicePaid(event: Stripe.Event) { + // Check if already processed + if (processedEvents.has(event.id)) { + logService.info('Event already processed', 'InvoicePaidController'); + return; + } + + try { + await doActualProcessing(event); + processedEvents.add(event.id); + } catch (error) { + // Don't mark as processed - allow retry + throw error; + } +} +``` + +### Error Handling + +```typescript +try { + await stripeService.createCustomer(params); +} catch (error) { + if (error.type === 'StripeCardError') { + // Customer-facing error - don't retry + throw new ValidationError('Card declined', { code: error.code }); + } else if (error.statusCode >= 500) { + // Server error - retry + throw new ServerError('Stripe API error', { error: error.message }); + } else { + // Unknown error + throw error; + } +} +``` + +--- + +## Implementation Checklist + +- [ ] Configure Stripe client with API key +- [ ] Create service layers as needed for your business logic +- [ ] Create webhook event controllers for all events you need to handle +- [ ] Wire webhook handler with event routing +- [ ] Implement lifecycle hooks for your specific use case +- [ ] Add data transformation adapters +- [ ] Add input validation schemas +- [ ] Write unit tests for all services +- [ ] Write integration tests for workflows +- [ ] Test with Stripe test mode +- [ ] Configure webhook URL in Stripe Dashboard +- [ ] Test webhook delivery +- [ ] Deploy and monitor + +--- + +## Resources + +- **Stripe API Documentation**: https://stripe.com/docs/api +- **Stripe Webhooks Guide**: https://stripe.com/docs/webhooks +- **Stripe Testing**: https://stripe.com/docs/testing +- **Stripe CLI**: https://stripe.com/docs/stripe-cli +- **Stripe Node.js Library**: https://github.com/stripe/stripe-node + +--- + +**Ready to build! 🚀** diff --git a/stripe_collection_module/docs/DEPLOYMENT.md b/stripe_collection_module/docs/DEPLOYMENT.md new file mode 100644 index 0000000..0385699 --- /dev/null +++ b/stripe_collection_module/docs/DEPLOYMENT.md @@ -0,0 +1,565 @@ +# Collection Module Deployment Guide + +This guide covers deploying your collection module to the Root Platform. + +## Overview + +Collection modules are deployed through the Root Platform API. Root Platform handles all infrastructure, hosting, and execution automatically. You simply: + +1. Develop and test your collection module locally +2. Commit your changes to version control +3. Create a release/tag +4. Publish the version via the Root Platform API + +Root Platform takes care of: +- Infrastructure provisioning +- Code deployment +- Environment configuration +- Scaling and availability +- Monitoring and logging + +--- + +## Prerequisites + +### Required + +- Completed and tested collection module +- Root Platform account with organization access +- Root Platform API key +- Git repository (recommended) + +### You'll Need + +| Item | Description | Where to Find | +|------|-------------|---------------| +| API Key | Root Platform API key | Root Platform → Settings → API Keys | +| Organization ID | Your Root organization ID | Root Platform → Organization Settings | +| Collection Module Key | Your module's unique key | Defined in `.root-config.json` | +| Host URL | Root Platform API URL | `https://api.rootplatform.com` or regional variant | + +--- + +## Deployment Process + +### Step 1: Prepare Your Code + +Ensure your collection module is ready for deployment: + +```bash +cd stripe_collection_module + +# Run validation +npm run validate + +# Run all tests +npm test + +# Check code quality +npm run lint + +# Build (optional - Root Platform may build for you) +npm run build +``` + +**Checklist:** +- [ ] All tests passing +- [ ] No linting errors +- [ ] Configuration validated +- [ ] Code committed to git +- [ ] Sensitive data not committed (env.ts is gitignored) + +### Step 2: Create a Release + +Create a version tag in your git repository: + +```bash +# Tag your release +git tag -a v1.0.0 -m "Release version 1.0.0" +git push origin v1.0.0 + +# Or create a GitHub/GitLab release +``` + +**Version Naming:** +- Use semantic versioning: `v1.0.0`, `v1.1.0`, `v2.0.0` +- Include release notes describing changes +- Tag stable, tested versions only + +### Step 3: Publish to Root Platform + +Publish your collection module version via the Root Platform API: + +```bash +curl -X POST \ + -H "Authorization: Basic {{api_key}}" \ + "{{host}}/v1/apps/{{org_id}}/insurance/collection-modules/{{cm_key}}/publish?bumpSandbox=true" +``` + +**Parameters:** + +| Parameter | Description | Example | +|-----------|-------------|---------| +| `{{api_key}}` | Your Root Platform API key (as username in Basic Auth) | `sandbox_abc123...` | +| `{{host}}` | Root Platform API URL | `https://api.rootplatform.com` | +| `{{org_id}}` | Your organization ID | `00000000-0000-0000-0000-000000000001` | +| `{{cm_key}}` | Collection module key | `cm_stripe` | +| `bumpSandbox` | Publish to sandbox (true) or production (false) | `true` or `false` | + +**Example:** + +```bash +curl -X POST \ + -H "Authorization: Basic sandbox_sk_abc123xyz..." \ + "https://api.rootplatform.com/v1/apps/00000000-0000-0000-0000-000000000001/insurance/collection-modules/cm_stripe/publish?bumpSandbox=true" +``` + +**Response:** + +```json +{ + "success": true, + "version": "1.0.0", + "status": "published", + "environment": "sandbox" +} +``` + +### Step 4: Verify Deployment + +After publishing, verify your deployment: + +1. **Check Root Platform Dashboard** + - Navigate to Collection Modules + - Verify version is shown as deployed + - Check deployment status + +2. **Test Functionality** + - Create a test policy + - Assign a payment method + - Verify webhooks are received + - Check logs for any errors + +3. **Monitor Logs** + - Access logs via Root Platform dashboard + - Look for any deployment errors + - Verify lifecycle hooks are executing + +--- + +## Environment-Specific Deployment + +### Sandbox Deployment + +Deploy to sandbox for testing: + +```bash +curl -X POST \ + -H "Authorization: Basic {{sandbox_api_key}}" \ + "{{host}}/v1/apps/{{org_id}}/insurance/collection-modules/{{cm_key}}/publish?bumpSandbox=true" +``` + +**Use sandbox for:** +- Testing new features +- Integration testing +- QA validation +- Demo environments + +### Production Deployment + +Deploy to production after thorough testing: + +```bash +curl -X POST \ + -H "Authorization: Basic {{production_api_key}}" \ + "{{host}}/v1/apps/{{org_id}}/insurance/collection-modules/{{cm_key}}/publish?bumpSandbox=false" +``` + +**Production checklist:** +- [ ] Tested in sandbox +- [ ] Code reviewed +- [ ] Documentation updated +- [ ] Stakeholders notified +- [ ] Rollback plan prepared +- [ ] Monitoring configured + +--- + +## Configuration Management + +### Environment Variables + +Collection modules get configuration from: + +1. **Root Platform Settings** + - Configured in the Root Platform dashboard + - Managed per environment (sandbox/production) + - Secure storage of API keys and secrets + +2. **Collection Module Metadata** + - Defined in your code + - Available via Root Platform context + +### Setting Environment Variables + +Configure in Root Platform dashboard: +1. Navigate to Collection Modules → Your Module +2. Go to Settings/Configuration +3. Add environment variables: + - Provider API keys + - Webhook secrets + - Feature flags + - Timeouts + +**Security Note:** Never commit sensitive values. Use Root Platform's secure configuration. + +--- + +## Versioning Strategy + +### Semantic Versioning + +Follow semantic versioning (semver): + +**Format:** `MAJOR.MINOR.PATCH` + +- **MAJOR** - Breaking changes (e.g., `1.0.0` → `2.0.0`) +- **MINOR** - New features, backward compatible (e.g., `1.0.0` → `1.1.0`) +- **PATCH** - Bug fixes, backward compatible (e.g., `1.0.0` → `1.0.1`) + +**Examples:** + +```bash +# Patch release (bug fix) +git tag -a v1.0.1 -m "Fix payment processing bug" + +# Minor release (new feature) +git tag -a v1.1.0 -m "Add refund support" + +# Major release (breaking change) +git tag -a v2.0.0 -m "New API structure" +``` + +### Version History + +Maintain clear version history: + +**CHANGELOG.md:** +```markdown +# Changelog + +## [1.1.0] - 2025-11-06 +### Added +- Refund processing support +- Enhanced error messages + +### Fixed +- Payment timeout handling + +## [1.0.0] - 2025-11-01 +### Added +- Initial release +- Payment processing +- Webhook handling +``` + +--- + +## Continuous Deployment + +### GitHub Actions Example + +Automate deployment with CI/CD: + +**.github/workflows/deploy.yml:** + +```yaml +name: Deploy Collection Module + +on: + push: + tags: + - 'v*' + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + + - name: Install dependencies + working-directory: ./stripe_collection_module + run: npm ci + + - name: Run tests + working-directory: ./stripe_collection_module + run: npm test + + - name: Validate configuration + working-directory: ./stripe_collection_module + run: npm run validate + + - name: Publish to Root Platform (Sandbox) + if: contains(github.ref, '-beta') + run: | + curl -X POST \ + -H "Authorization: Basic ${{ secrets.ROOT_SANDBOX_API_KEY }}" \ + "${{ secrets.ROOT_HOST }}/v1/apps/${{ secrets.ROOT_ORG_ID }}/insurance/collection-modules/${{ secrets.CM_KEY }}/publish?bumpSandbox=true" + + - name: Publish to Root Platform (Production) + if: "!contains(github.ref, '-beta')" + run: | + curl -X POST \ + -H "Authorization: Basic ${{ secrets.ROOT_PRODUCTION_API_KEY }}" \ + "${{ secrets.ROOT_HOST }}/v1/apps/${{ secrets.ROOT_ORG_ID }}/insurance/collection-modules/${{ secrets.CM_KEY }}/publish?bumpSandbox=false" +``` + +**GitHub Secrets Required:** +- `ROOT_SANDBOX_API_KEY` +- `ROOT_PRODUCTION_API_KEY` +- `ROOT_HOST` +- `ROOT_ORG_ID` +- `CM_KEY` + +--- + +## Rollback Strategy + +### Rolling Back a Deployment + +If issues occur, rollback to previous version: + +1. **Identify Previous Version** + ```bash + git tag -l + # v1.0.0 + # v1.0.1 + # v1.1.0 (current, broken) + ``` + +2. **Publish Previous Version** + ```bash + git checkout v1.0.1 + + curl -X POST \ + -H "Authorization: Basic {{api_key}}" \ + "{{host}}/v1/apps/{{org_id}}/insurance/collection-modules/{{cm_key}}/publish?bumpSandbox=false" + ``` + +3. **Verify Rollback** + - Check Root Platform shows correct version + - Test functionality + - Monitor logs + +### Rollback Checklist + +- [ ] Identify stable version to rollback to +- [ ] Notify stakeholders +- [ ] Execute rollback +- [ ] Verify functionality +- [ ] Document incident +- [ ] Fix issue in new version + +--- + +## Monitoring After Deployment + +### CloudWatch Logs + +Root Platform automatically logs to CloudWatch: + +**Access Logs:** +1. Root Platform Dashboard → Collection Modules → Your Module +2. Click "Logs" or "Monitoring" +3. View real-time logs +4. Filter by severity, time range, or search terms + +**What to Monitor:** +- Error rates +- Response times +- Webhook processing +- API call success rates + +### Alerting + +Set up alerts for: +- High error rates +- Slow response times +- Failed webhook deliveries +- Missing configuration + +### Health Checks + +Monitor collection module health: +- Webhook delivery status +- API connectivity +- Processing latency +- Error patterns + +--- + +## Troubleshooting + +### Deployment Failed + +**Symptoms:** +- API returns error +- Version not shown in dashboard +- Old version still running + +**Solutions:** +1. Check API response for error details +2. Verify API key has correct permissions +3. Ensure organization ID and module key are correct +4. Check collection module code for errors +5. Review Root Platform status page + +### Module Not Executing + +**Symptoms:** +- Lifecycle hooks not called +- Webhooks not received +- No logs generated + +**Solutions:** +1. Verify module is published and active +2. Check webhook URL is correct +3. Verify payment provider webhook configuration +4. Review logs for initialization errors +5. Test with sample events + +### Configuration Issues + +**Symptoms:** +- Missing environment variables +- API keys not working +- Wrong configuration values + +**Solutions:** +1. Check Root Platform dashboard configuration +2. Verify environment variables are set correctly +3. Check for typos in API keys +4. Ensure correct environment (sandbox vs production) + +--- + +## Best Practices + +### Pre-Deployment + +- ✅ Run full test suite (`npm test`) +- ✅ Validate configuration (`npm run validate`) +- ✅ Check code quality (`npm run lint`) +- ✅ Review code changes +- ✅ Update version number +- ✅ Update CHANGELOG.md +- ✅ Tag release in git + +### During Deployment + +- ✅ Deploy to sandbox first +- ✅ Test thoroughly in sandbox +- ✅ Monitor logs during deployment +- ✅ Verify webhook connectivity +- ✅ Have rollback plan ready + +### Post-Deployment + +- ✅ Verify version in dashboard +- ✅ Test critical workflows +- ✅ Monitor error rates +- ✅ Check log outputs +- ✅ Update documentation +- ✅ Notify team + +### Security + +- ✅ Never commit API keys or secrets +- ✅ Use Root Platform configuration for sensitive values +- ✅ Rotate API keys regularly +- ✅ Use different keys for sandbox/production +- ✅ Review permissions before deployment + +--- + +## Deployment Checklist + +### Pre-Deployment +- [ ] All tests passing +- [ ] Code reviewed +- [ ] Configuration validated +- [ ] Documentation updated +- [ ] Version tagged in git +- [ ] CHANGELOG updated + +### Sandbox Deployment +- [ ] Published to sandbox +- [ ] Tested lifecycle hooks +- [ ] Verified webhook processing +- [ ] Checked logs for errors +- [ ] Tested edge cases + +### Production Deployment +- [ ] Sandbox testing complete +- [ ] Stakeholders notified +- [ ] Published to production +- [ ] Verified in dashboard +- [ ] Monitored initial traffic +- [ ] Documented deployment + +### Post-Deployment +- [ ] Functionality verified +- [ ] Logs reviewed +- [ ] Metrics monitored +- [ ] Team notified +- [ ] Documentation updated + +--- + +## Support + +### Documentation + +- **Setup**: [SETUP.md](./SETUP.md) +- **Customization**: [CUSTOMIZING.md](./CUSTOMIZING.md) +- **Webhooks**: [WEBHOOKS.md](./WEBHOOKS.md) +- **Architecture**: [ARCHITECTURE.md](./ARCHITECTURE.md) +- **Testing**: [TESTING.md](./TESTING.md) + +### Getting Help + +1. Check Root Platform documentation +2. Review CloudWatch Logs +3. Check Root Platform status page +4. Contact Root Platform support + +### Common Issues + +| Issue | Solution | +|-------|----------| +| API key invalid | Verify key is correct for environment | +| Module not found | Check organization ID and module key | +| Permission denied | Verify API key has deployment permissions | +| Version conflict | Ensure version number is incremented | + +--- + +## Next Steps + +After deployment: + +1. **Monitor Performance** - Watch logs and metrics +2. **Test End-to-End** - Process real transactions +3. **Document Changes** - Update runbooks and documentation +4. **Plan Next Release** - Gather feedback and plan improvements +5. **Set Up Webhooks** - See [WEBHOOKS.md](./WEBHOOKS.md) + +--- + +**Ready to deploy! 🚀** diff --git a/stripe_collection_module/docs/LOG_VIEWING.md b/stripe_collection_module/docs/LOG_VIEWING.md new file mode 100644 index 0000000..0255de3 --- /dev/null +++ b/stripe_collection_module/docs/LOG_VIEWING.md @@ -0,0 +1,321 @@ +# Log Viewing Guide + +This guide explains how to view and use the collection module logs. + +## Overview + +The collection module implements **structured JSON logging** to stdout. All logs are automatically captured by your log aggregation system (CloudWatch Logs, DataDog, etc.) where they are: + +- Persistent across Lambda invocations +- Searchable and filterable +- Available for long-term retention +- Accessible via your monitoring platform's UI and API + +The collection module outputs structured JSON to stdout - your logging infrastructure handles collection, storage, and visualization. + +## Accessing Logs + +### Via CloudWatch Logs (AWS) + +1. Navigate to AWS CloudWatch Logs +2. Find your Lambda function's log group +3. View log streams for recent invocations +4. Use CloudWatch Insights for advanced queries + +### Via DataDog (or other platforms) + +Configure your log aggregation platform to collect Lambda stdout logs. The structured JSON format makes it easy to parse and filter. + +## Log Structure + +Each log entry is a JSON object with the following fields: + +| Field | Description | +|-------|-------------| +| `timestamp` | ISO 8601 timestamp | +| `level` | DEBUG, INFO, WARN, or ERROR | +| `environment` | production or development | +| `message` | The log message | +| `context` | Which part of the code logged this (optional) | +| `correlationId` | Request tracking ID (optional) | +| `metadata` | Additional structured data (optional) | +| `error` | Error details including stack trace (optional) | + +### Log Levels + +- 🔵 **DEBUG** - Detailed diagnostic information +- 🟢 **INFO** - General informational messages +- 🟡 **WARN** - Warning messages (potential issues) +- 🔴 **ERROR** - Error messages (something went wrong) + +## Understanding Logs + +### Common Contexts + +| Context | What It Means | +|---------|---------------| +| `renderCreatePaymentMethod` | Creating payment method form | +| `afterPolicyPaymentMethodAssigned` | Processing payment method assignment | +| `afterPaymentCreated` | Processing payment creation | +| `WebhookHandler` | Processing Stripe webhook | +| `InvoicePaidController` | Processing invoice paid event | +| `PaymentCreationController` | Processing payment creation | +| `RootService` | Root API operations | + +### Correlation IDs + +Correlation IDs link related log entries together. For example, when a webhook is received, all logs for processing that webhook will share the same correlation ID. + +**Use case**: Filter by correlation ID to see the complete flow of a single request. + +### Metadata + +Metadata provides additional context in structured format: + +``` +Message: "Payment method assigned to policy" +Context: "afterPolicyPaymentMethodAssigned" +Metadata: { + "policyId": "policy_abc123", + "stripeCustomerId": "cus_xyz789" +} +``` + +## Troubleshooting with Logs + +### Example: Payment Failed + +1. Open CloudWatch Logs or DataDog +2. Filter by level: ERROR +3. Look for recent error entries +4. Check error message and stack trace +5. Note the correlation ID +6. Filter by that correlation ID to see full request flow + +### Example: Payment Method Issues + +1. Filter by context: `afterPolicyPaymentMethodAssigned` +2. Look for your policy ID in metadata +3. Follow the log sequence to see where it stopped +4. Check for warnings or errors + +### Example: Debugging Webhook Issues + +1. Filter by context: `WebhookHandler` +2. Find the webhook event by timestamp +3. Get its correlation ID +4. Filter by correlation ID to see complete processing + +## Log Retention + +### CloudWatch Logs + +- **Retention**: Configurable (default: 7-30 days based on log group settings) +- **Automatic Persistence**: All logs automatically sent to CloudWatch +- **Query Capabilities**: CloudWatch Insights for advanced querying +- **Access**: Available via CloudWatch console and API + +### DataDog (Optional) + +CloudWatch logs can optionally be forwarded to DataDog for: + +- Advanced analytics and visualization +- Alerting and monitoring +- Performance tracking +- Cross-service correlation + +## For Developers + +### Adding Logs + +Use the `LogService` throughout your code: + +```typescript +import { getLogService } from '../services/log-instance'; + +const logService = getLogService(); + +// Log with context +logService.info('Processing started', 'MyContext'); + +// Log with metadata +logService.info('Payment created', 'PaymentProcessing', { + paymentId: 'payment_123', + amount: 10000, +}); + +// Log errors +try { + await dangerousOperation(); +} catch (error) { + logService.error( + 'Operation failed', + 'MyContext', + { operation: 'dangerousOperation' }, + error + ); +} +``` + +### Using Correlation IDs + +For request flows: + +```typescript +// Generate at start of request +const correlationId = logService.generateCorrelationId(); + +logService.info('Request started', 'WebhookHandler'); + +// ... process request ... + +// All logs will include this correlation ID + +// Clear at end +logService.clearCorrelationId(); +``` + +### Best Practices + +1. **Be Descriptive** - Write clear log messages + ```typescript + // Bad + logService.info('Done'); + + // Good + logService.info('Payment method successfully attached to customer', 'RootService'); + ``` + +2. **Include Context** - Always provide context (service/controller name) + ```typescript + logService.info('Processing payment', 'PaymentCreationController'); + ``` + +3. **Add Metadata** - Include relevant IDs and data + ```typescript + logService.info('Creating payment intent', 'PaymentCreationController', { + customerId: 'cus_123', + policyId: 'pol_456', + }); + ``` + +4. **Use Appropriate Levels** + - DEBUG: Detailed diagnostic info (not shown to clients by default) + - INFO: Normal operations + - WARN: Something unusual but handled + - ERROR: Something went wrong + +5. **Don't Log Sensitive Data** + ```typescript + // Bad + logService.info('Card details', 'PaymentService', { + cardNumber: '4242424242424242', // Never do this! + }); + + // Good + logService.info('Card details received', 'PaymentService', { + cardLast4: '4242', + cardBrand: 'visa', + }); + ``` + +## Log Format + +### JSON Format + +Logs are output as structured JSON to stdout: + +```json +{ + "timestamp": "2024-01-15T10:30:45.123Z", + "level": "INFO", + "environment": "production", + "message": "Payment method assigned", + "context": "afterPolicyPaymentMethodAssigned", + "correlationId": "abc-123-def", + "metadata": { + "policyId": "policy_123", + "stripeCustomerId": "cus_456" + } +} +``` + +This format is automatically parsed by CloudWatch Logs, DataDog, and other log aggregation systems. + +## FAQ + +### Q: How long are logs kept? +**A**: In CloudWatch: Based on log group retention settings (typically 7-30 days). The retention period is configurable in CloudWatch. + +### Q: Where can I view logs? +**A**: Logs are available in CloudWatch Logs (AWS Console) or your configured log aggregation platform (DataDog, etc.). + +### Q: What if I need older logs? +**A**: All logs are retained in CloudWatch according to the retention policy. Access them via CloudWatch Logs console or the dashboard endpoint. + +### Q: Can I filter by date range? +**A**: Yes, CloudWatch Insights supports powerful filtering by time range, log level, context, correlation ID, and more. + +### Q: Why don't I see recent logs? +**A**: Logs should appear in CloudWatch within seconds. If logs are missing, check: + 1. CloudWatch log group exists for the Lambda function + 2. Lambda has IAM permissions to write to CloudWatch + 3. Logs are being output to stdout (check Lambda execution logs) + +### Q: Does Lambda cold start affect logging? +**A**: No. Logs are sent to CloudWatch regardless of cold/warm starts. Every Lambda invocation outputs logs to stdout, which CloudWatch captures automatically. + +### Q: Can I export logs? +**A**: The dashboard view is HTML. You can copy/paste or screenshot. For bulk export, contact support for DataDog access. + +### Q: What does the correlation ID mean? +**A**: It's a unique ID for a single request flow. All logs from processing one webhook or event share the same correlation ID. + +## Lambda-Specific Considerations + +### CloudWatch Logs + +The collection module automatically logs to CloudWatch: +1. **Log Group**: Auto-created by Lambda (e.g., `/aws/lambda/collection-module-function`) +2. **Log Streams**: One per Lambda execution +3. **Retention**: Configurable in CloudWatch (default: Never expire) +4. **IAM Permissions**: Lambda execution role automatically has CloudWatch Logs permissions + +### Performance + +- **Write Latency**: Negligible (~1-2ms to stdout) +- **CloudWatch Ingestion**: Near real-time (1-2 second delay) +- **Cost**: CloudWatch Logs pricing (~$0.50/GB ingested, $0.03/GB stored) + +### Monitoring + +Monitor these CloudWatch metrics: +- **Log Events**: Number of log entries +- **Ingestion**: Bytes ingested +- **Storage**: Total storage used + +### Querying Logs + +Use CloudWatch Logs Insights to query logs: + +``` +fields @timestamp, level, message, context, correlationId +| filter level = "ERROR" +| sort @timestamp desc +| limit 100 +``` + +## Support + +For issues with log viewing: +1. Check if logs are appearing in CloudWatch Logs console +2. Verify Lambda has CloudWatch Logs permissions (usually automatic) +3. Check the log group name matches your Lambda function +4. Contact support with the policy ID and timestamp +5. Include any error messages shown + +## Related Documentation + +- [ARCHITECTURE.md](./ARCHITECTURE.md) - System architecture overview +- [TESTING.md](./TESTING.md) - Testing guide including LogService tests + diff --git a/stripe_collection_module/docs/ROOT_CONFIGURATION.md b/stripe_collection_module/docs/ROOT_CONFIGURATION.md new file mode 100644 index 0000000..49bf8fc --- /dev/null +++ b/stripe_collection_module/docs/ROOT_CONFIGURATION.md @@ -0,0 +1,327 @@ +# Root Platform Configuration + +This guide explains how to configure your collection module for the Root Platform. + +## Configuration Files + +### `.root-config.json` + +This file contains the collection module metadata and settings used by the Root Platform. + +**Location:** `stripe_collection_module/.root-config.json` + +**Format:** + +```json +{ + "collectionModuleKey": "cm_stripe_yourcompany", + "collectionModuleName": "Your Company Stripe Integration", + "organizationId": "00000000-0000-0000-0000-000000000001", + "host": "https://api.rootplatform.com", + "settings": { + "legacyCodeExecution": false + }, + "manualTransactions": [] +} +``` + +**Fields:** + +| Field | Required | Description | Example | +|-------|----------|-------------|---------| +| `collectionModuleKey` | ✓ | Unique identifier for your collection module. Must start with `cm_` | `cm_stripe_acme` | +| `collectionModuleName` | ✓ | Human-readable name displayed in Root Platform dashboard | `"ACME Stripe Integration"` | +| `organizationId` | ✓ | Your Root Platform organization ID (UUID format) | `"12345678-1234-..."` | +| `host` | ✓ | Root Platform API URL | `"https://api.rootplatform.com"` | +| `settings.legacyCodeExecution` | ✓ | Whether to use legacy execution mode (should be `false` for new modules) | `false` | +| `manualTransactions` | ✓ | Array of manual transaction configurations (usually empty) | `[]` | + +**Naming Conventions:** + +- **Collection Module Key**: Use lowercase with underscores, format: `cm_{provider}_{company}` + - Good: `cm_stripe_acme`, `cm_stripe_widgets_inc` + - Bad: `stripeModule`, `cm-stripe-acme`, `STRIPE_CM` + +- **Collection Module Name**: Use Title Case, be descriptive + - Good: `"ACME Stripe Integration"`, `"Widgets Inc Payment Collection"` + - Bad: `"stripe"`, `"collection_module"`, `"CM"` + +**Environment-Specific Configuration:** + +For different environments (sandbox/production), you can: + +1. **Option 1: Single config with environment variable** + ```json + { + "host": "https://api.rootplatform.com", + ... + } + ``` + Use different API keys in `.root-auth` for sandbox vs production. + +2. **Option 2: Multiple config files** (not recommended for this template) + ``` + .root-config.sandbox.json + .root-config.production.json + ``` + +**⚠️ Important Notes:** + +- This file is **NOT** gitignored - it contains configuration metadata, not secrets +- The `.root-config.json` should be committed to version control +- Secrets go in `.root-auth` (which IS gitignored) + +--- + +### `.root-auth` + +This file contains authentication credentials for the Root Platform API. + +**Location:** `stripe_collection_module/.root-auth` + +**Format:** + +```bash +ROOT_API_KEY=sandbox_sk_abc123... +``` + +**Fields:** + +| Variable | Description | Example | +|----------|-------------|---------| +| `ROOT_API_KEY` | Your Root Platform API key | `sandbox_sk_abc123...` or `production_sk_xyz789...` | + +**Getting Your API Key:** + +1. Log in to the Root Platform dashboard +2. Navigate to **Settings → API Keys** +3. Generate a new API key with appropriate permissions: + - **Sandbox keys**: Start with `sandbox_sk_` + - **Production keys**: Start with `production_sk_` + +**Environment-Specific Keys:** + +For local development and deployment to different environments: + +**Sandbox/Development:** +```bash +ROOT_API_KEY=sandbox_sk_abc123... +``` + +**Production:** +```bash +ROOT_API_KEY=production_sk_xyz789... +``` + +**⚠️ Security:** + +- **NEVER commit `.root-auth` to version control** (already in `.gitignore`) +- Store production keys securely (1Password, AWS Secrets Manager, etc.) +- Rotate API keys quarterly +- Use sandbox keys for all development and testing +- Use different API keys for CI/CD vs local development + +--- + +## Setup Process + +### Automated Setup (Recommended) + +Run the setup script from the repository root: + +```bash +./setup.sh +``` + +The script will prompt you for: +1. Collection Module Key +2. Collection Module Name +3. Organization ID +4. Root Platform Host +5. Root Platform API Key + +All values will be saved to the appropriate files. + +### Manual Setup + +1. **Configure `.root-config.json`:** + ```bash + cd stripe_collection_module + # Edit the file with your values + nano .root-config.json + ``` + +2. **Create `.root-auth`:** + ```bash + cd stripe_collection_module + cp .root-auth.sample .root-auth + # Edit with your actual API key + nano .root-auth + ``` + +--- + +## Validation + +### Verify Configuration + +Check that your configuration is valid: + +```bash +cd stripe_collection_module + +# Check .root-config.json format +cat .root-config.json | jq '.' + +# Verify .root-auth exists and has correct format +if [ -f .root-auth ]; then + echo "✓ .root-auth exists" + grep -q "ROOT_API_KEY=" .root-auth && echo "✓ ROOT_API_KEY is set" +else + echo "❌ .root-auth not found" +fi +``` + +### Test API Connection + +You can test your Root Platform connection using the Root Platform CLI or a simple curl command: + +```bash +# Using curl +curl -H "Authorization: Basic $(cat .root-auth | grep ROOT_API_KEY | cut -d= -f2)" \ + "https://api.rootplatform.com/v1/insurance/organizations/$(jq -r .organizationId .root-config.json)" + +# Expected: JSON response with organization details +``` + +--- + +## Troubleshooting + +### "collectionModuleKey is invalid" + +**Problem:** The collection module key doesn't match Root Platform requirements. + +**Solution:** +- Must start with `cm_` +- Use only lowercase letters, numbers, and underscores +- Should be unique within your organization +- Format: `cm_{provider}_{company}` + +### "Unauthorized" or "Invalid API key" + +**Problem:** The API key in `.root-auth` is invalid or expired. + +**Solution:** +1. Check the API key is correct (no extra spaces/newlines) +2. Verify key has not been revoked in Root Platform dashboard +3. Ensure you're using the right environment key (sandbox vs production) +4. Generate a new API key if needed + +### "Organization not found" + +**Problem:** The `organizationId` in `.root-config.json` is incorrect. + +**Solution:** +1. Log in to Root Platform dashboard +2. Check your organization ID in **Settings → Organization** +3. Update `.root-config.json` with correct UUID + +### ".root-auth not found" during deployment + +**Problem:** The `.root-auth` file is missing. + +**Solution:** +```bash +cd stripe_collection_module +cp .root-auth.sample .root-auth +# Edit with your API key +nano .root-auth +``` + +### "Cannot read property 'host' of undefined" + +**Problem:** The `.root-config.json` file is malformed or missing. + +**Solution:** +```bash +# Validate JSON syntax +cat .root-config.json | jq '.' + +# If invalid, restore from sample +cp .root-config.json.sample .root-config.json +# Then edit with your values +``` + +--- + +## Environment Variables + +Some deployment scenarios may require environment variables instead of or in addition to config files: + +```bash +# For deployment scripts +export ROOT_API_KEY="sandbox_sk_abc123..." +export ROOT_ORG_ID="00000000-0000-0000-0000-000000000001" +export ROOT_HOST="https://api.rootplatform.com" +export CM_KEY="cm_stripe_yourcompany" +``` + +The deployment script (`scripts/deploy.sh`) can read from either: +1. `.root-auth` and `.root-config.json` files (preferred) +2. Environment variables (for CI/CD) + +--- + +## Best Practices + +### Local Development + +- ✅ Use sandbox API keys +- ✅ Keep `.root-auth` in your local directory only +- ✅ Never commit `.root-auth` to git +- ✅ Test with sandbox environment before production + +### CI/CD Deployment + +- ✅ Store API keys in CI/CD secrets (GitHub Secrets, GitLab CI Variables, etc.) +- ✅ Use environment variables in deployment scripts +- ✅ Rotate API keys regularly +- ✅ Use different keys for sandbox and production pipelines + +### Security + +- ✅ Treat `.root-auth` like a password file +- ✅ Never share API keys via email or chat +- ✅ Use 1Password or similar for team sharing +- ✅ Audit API key usage regularly in Root Platform dashboard +- ✅ Revoke unused or compromised keys immediately + +### Version Control + +- ✅ Commit `.root-config.json` (no secrets) +- ✅ Commit `.root-config.json.sample` (template) +- ✅ Commit `.root-auth.sample` (template) +- ❌ Never commit `.root-auth` (contains secrets) +- ❌ Never commit backup files like `.root-auth.backup` + +--- + +## Related Documentation + +- [Setup Guide](SETUP.md) - Complete setup walkthrough +- [Deployment Guide](DEPLOYMENT.md) - Deploying to Root Platform +- [Environment Configuration](../code/env.sample.ts) - Stripe and module configuration + +--- + +## Support + +If you encounter issues with Root Platform configuration: + +1. Check this documentation +2. Verify your API key in Root Platform dashboard +3. Review Root Platform logs for errors +4. Contact your Root Platform representative + + diff --git a/stripe_collection_module/docs/SETUP.md b/stripe_collection_module/docs/SETUP.md new file mode 100644 index 0000000..cdf038f --- /dev/null +++ b/stripe_collection_module/docs/SETUP.md @@ -0,0 +1,465 @@ +# Collection Module Setup Guide + +This guide will walk you through setting up a collection module from this template. + +## Quick Start (5 Minutes) + +### 1. Run Setup Script + +```bash +# From the repository root +chmod +x setup-template.sh +./setup-template.sh +``` + +The script will guide you through: +- Provider name and configuration +- Collection module key setup +- Environment file creation +- Dependency installation + +### 2. Update Environment Variables + +```bash +cd stripe_collection_module +# Edit code/env.ts with your actual API keys +``` + +### 3. Validate Configuration + +```bash +npm run validate +``` + +### 4. Run Tests + +```bash +nvm use # Use Node 18+ +npm test +``` + +You're ready to customize! See [CUSTOMIZING.md](./CUSTOMIZING.md) for next steps. + +--- + +## Detailed Setup Instructions + +### Prerequisites + +**Required:** +- Node.js 18+ (use `nvm` for version management) +- npm 8+ +- Git + +**Accounts Needed:** +- Root Platform account with organization access +- Payment provider account (e.g., Stripe, PayPal) +- AWS account (for Lambda deployment) + +### Step 1: Clone or Copy Template + +```bash +# Clone the repository +git clone +cd collection-module-template_stripe + +# Or copy the template directory +cp -r collection-module-template_stripe my-provider-collection-module +cd my-provider-collection-module +``` + +### Step 2: Configure Your Provider + +#### Option A: Automated Setup + +```bash +./setup-template.sh +``` + +#### Option B: Manual Setup + +1. **Update module configuration** + +Edit `stripe_collection_module/.root-config.json`: + +```json +{ + "collectionModuleKey": "cm_your_provider", + "collectionModuleName": "Your Provider Collection Module", + "organizationId": "your-root-organization-id", + "host": "http://localhost:4000", + "settings": { + "legacyCodeExecution": false + } +} +``` + +2. **Create environment file** + +```bash +cd stripe_collection_module +cp code/env.sample.ts code/env.ts +``` + +3. **Update environment variables** + +Edit `code/env.ts` with your actual credentials: + +```typescript +// Environment +export const NODE_ENV = 'development'; + +// Your Provider API Keys +export const STRIPE_SECRET_KEY_LIVE = 'sk_live_YOUR_KEY'; +export const STRIPE_SECRET_KEY_TEST = 'sk_test_YOUR_KEY'; +// ... other keys + +// Root Platform +export const ROOT_API_KEY_LIVE = 'production_YOUR_KEY'; +export const ROOT_API_KEY_SANDBOX = 'sandbox_YOUR_KEY'; +export const ROOT_BASE_URL_LIVE = 'https://api.rootplatform.com/v1/insurance'; +export const ROOT_COLLECTION_MODULE_KEY = 'cm_your_provider'; +``` + +### Step 3: Install Dependencies + +```bash +cd stripe_collection_module + +# Use correct Node version +nvm use + +# Install packages +npm install +``` + +### Step 4: Validate Setup + +Run the configuration validator: + +```bash +npm run validate +``` + +This checks: +- All required configuration files exist +- No placeholder values remain +- TypeScript compiles successfully +- Tests pass +- Correct Node version + +### Step 5: Run Tests + +```bash +# All tests +npm test + +# With coverage +npm run test:coverage + +# Watch mode for development +npm run test:watch +``` + +### Step 6: Build + +```bash +npm run build +``` + +This compiles TypeScript and creates the `dist/` folder. + +--- + +## Configuration Details + +### Required Environment Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `NODE_ENV` | Environment name | `development` or `production` | +| `STRIPE_SECRET_KEY_LIVE` | Provider API secret (production) | `sk_live_...` | +| `STRIPE_SECRET_KEY_TEST` | Provider API secret (test) | `sk_test_...` | +| `STRIPE_PUBLISHABLE_KEY_LIVE` | Provider publishable key (prod) | `pk_live_...` | +| `STRIPE_PUBLISHABLE_KEY_TEST` | Provider publishable key (test) | `pk_test_...` | +| `STRIPE_WEBHOOK_SIGNING_SECRET_LIVE` | Webhook secret (production) | `whsec_...` | +| `STRIPE_WEBHOOK_SIGNING_SECRET_TEST` | Webhook secret (test) | `whsec_...` | +| `ROOT_API_KEY_LIVE` | Root API key (production) | `production_...` | +| `ROOT_API_KEY_SANDBOX` | Root API key (sandbox) | `sandbox_...` | +| `ROOT_BASE_URL_LIVE` | Root API base URL (prod) | `https://api.rootplatform.com/v1/insurance` | +| `ROOT_BASE_URL_SANDBOX` | Root API base URL (sandbox) | `https://sandbox.rootplatform.com/v1/insurance` | +| `ROOT_COLLECTION_MODULE_KEY` | Your module key | `cm_your_provider` | + +### Optional Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `TIME_DELAY_IN_MILLISECONDS` | Processing delay | `10000` | +| `STRIPE_PRODUCT_ID_LIVE` | Provider product ID (prod) | (optional) | +| `STRIPE_PRODUCT_ID_TEST` | Provider product ID (test) | (optional) | + +### Getting API Keys + +**Stripe:** +1. Go to [Stripe Dashboard](https://dashboard.stripe.com) +2. Navigate to Developers → API keys +3. Copy your keys (use test keys for development) + +**Root Platform:** +1. Log in to Root Platform +2. Go to Settings → API Keys +3. Create or copy existing API keys +4. Use sandbox keys for development + +### Validating Configuration + +The `ConfigurationService` validates your setup automatically: +- Checks all required variables are set +- Validates URL formats +- Ensures environment is valid (`production` or `development`) + +If configuration is invalid, you'll see helpful error messages: + +``` +Configuration validation failed: +Missing required configuration values for production: stripeSecretKey, rootApiKey +Hint: Check that code/env.ts is properly configured with all required values. +For AWS Lambda: Ensure all environment variables are set in Lambda configuration. +``` + +--- + +## Development Workflow + +### Local Development Setup + +1. **Use test/sandbox credentials** + +```typescript +// code/env.ts +export const NODE_ENV = 'development'; +export const STRIPE_SECRET_KEY_LIVE = 'sk_test_...'; // Use test key +export const ROOT_API_KEY_LIVE = 'sandbox_...'; // Use sandbox key +``` + +2. **Run in watch mode** + +```bash +npm run test:watch +``` + +3. **Make code changes** + +Edit files in `code/` directory. Tests will re-run automatically. + +4. **Check linting** + +```bash +npm run lint +npm run lint:fix # Auto-fix some issues +``` + +### Project Structure + +``` +stripe_collection_module/ +├── code/ # Source code +│ ├── core/ # Domain models & DI container +│ │ ├── models/ # Data models +│ │ ├── container.ts # DI container +│ │ └── container.setup.ts # Service registration +│ ├── services/ # Business logic +│ │ ├── config.service.ts # Configuration +│ │ ├── log.service.ts # Logging +│ │ ├── stripe.service.ts # Provider operations +│ │ └── root.service.ts # Root operations +│ ├── clients/ # API wrappers +│ │ ├── stripe-client.ts # Provider SDK wrapper +│ │ └── root-client.ts # Root SDK wrapper +│ ├── controllers/ # Event processors +│ │ ├── stripe-event-processors/ +│ │ └── root-event-processors/ +│ ├── lifecycle-hooks/ # Root platform hooks +│ │ └── index.ts +│ ├── utils/ # Utilities +│ └── env.ts # Your environment config +├── __tests__/ # Test files +│ ├── services/ +│ ├── core/ +│ └── helpers/ +├── docs/ # Documentation +├── scripts/ # Build/deploy scripts +└── package.json +``` + +--- + +## Troubleshooting + +### Error: "ENVIRONMENT is not set" + +**Solution:** Set `NODE_ENV` in `code/env.ts`: + +```typescript +export const NODE_ENV = 'development'; +``` + +### Error: "Missing required configuration values" + +**Solution:** Check `code/env.ts` has all required variables. Look for placeholder values like `xxxxx`. + +### Error: "Module not found" + +**Solution:** Install dependencies: + +```bash +npm install +``` + +### Error: Tests failing + +**Solutions:** +1. Use correct Node version: `nvm use` +2. Clear Jest cache: `npx jest --clearCache` +3. Reinstall: `rm -rf node_modules && npm install` + +### Error: TypeScript compilation errors + +**Solution:** Check `tsconfig.json` includes all source files and dependencies are installed. + +### Validation Script Warnings + +If `npm run validate` shows warnings: +- **Placeholder values**: Update `code/env.ts` and `.root-config.json` with real values +- **Linting issues**: Run `npm run lint:fix` to auto-fix +- **Node version mismatch**: Run `nvm use` + +--- + +## Next Steps + +After completing setup: + +1. **Read customization guide**: [CUSTOMIZING.md](./CUSTOMIZING.md) + - Adapt template for your payment provider + - Implement service methods + - Add provider-specific logic + +2. **Implement webhooks**: [WEBHOOKS.md](./WEBHOOKS.md) + - Set up webhook endpoints + - Handle provider events + - Test webhook delivery + +3. **Deploy to Lambda**: [DEPLOYMENT.md](./DEPLOYMENT.md) + - Configure AWS resources + - Set up CI/CD pipeline + - Deploy and test + +4. **Review architecture**: [ARCHITECTURE.md](./ARCHITECTURE.md) + - Understand system design + - Learn service patterns + - Explore DI container + +5. **Write tests**: [TESTING.md](./TESTING.md) + - Add unit tests for your services + - Create integration tests + - Achieve good coverage + +--- + +## Getting Help + +### Documentation + +- **Setup**: This file +- **Customization**: [CUSTOMIZING.md](./CUSTOMIZING.md) +- **Deployment**: [DEPLOYMENT.md](./DEPLOYMENT.md) +- **Webhooks**: [WEBHOOKS.md](./WEBHOOKS.md) +- **Architecture**: [ARCHITECTURE.md](./ARCHITECTURE.md) +- **Testing**: [TESTING.md](./TESTING.md) +- **Template Setup**: `../TEMPLATE_SETUP.md` (root) + +### Common Tasks + +```bash +# Validate configuration +npm run validate + +# Run tests +npm test + +# Run linter +npm run lint + +# Fix linting issues +npm run lint:fix + +# Build for production +npm run build + +# Run specific test file +npm test -- services/log.service.test + +# Watch tests +npm run test:watch + +# Check coverage +npm run test:coverage +``` + +### Support + +If you're stuck: +1. Check troubleshooting section above +2. Review relevant documentation +3. Check CloudWatch Logs (after deployment) +4. Review test output for hints + +--- + +## Best Practices + +### Security + +- ✅ Never commit `code/env.ts` to git +- ✅ Use separate keys for test/production +- ✅ Rotate API keys regularly +- ✅ Use AWS Secrets Manager for production +- ✅ Enable CloudWatch Logs monitoring + +### Development + +- ✅ Use test/sandbox credentials locally +- ✅ Run tests before committing +- ✅ Keep test coverage above 70% +- ✅ Follow TypeScript best practices +- ✅ Use the DI container for dependencies + +### Configuration + +- ✅ Validate before deploying +- ✅ Document all env variables +- ✅ Use meaningful module keys +- ✅ Set appropriate time delays +- ✅ Configure proper log levels + +--- + +## Checklist + +Use this to track your setup progress: + +- [ ] Cloned/copied template +- [ ] Ran setup script or manual configuration +- [ ] Created `code/env.ts` with real values +- [ ] Updated `.root-config.json` +- [ ] Installed dependencies (`npm install`) +- [ ] Ran validation (`npm run validate`) +- [ ] All tests passing (`npm test`) +- [ ] TypeScript compiling (`npm run build`) +- [ ] No linting errors (`npm run lint`) +- [ ] Reviewed customization guide +- [ ] Ready to implement provider logic + +Once complete, move to [CUSTOMIZING.md](./CUSTOMIZING.md) to adapt the template for your provider. + diff --git a/stripe_collection_module/docs/TESTING.md b/stripe_collection_module/docs/TESTING.md new file mode 100644 index 0000000..b82d3d0 --- /dev/null +++ b/stripe_collection_module/docs/TESTING.md @@ -0,0 +1,474 @@ +# Testing Guide + +This guide explains how to test the collection module effectively. + +## Overview + +We use **Jest** with **TypeScript** for unit testing. The testing infrastructure is designed to make tests easy to write, read, and maintain. + +## Running Tests + +```bash +# Run all tests +npm test + +# Run tests in watch mode (auto-rerun on changes) +npm test:watch + +# Run tests with coverage report +npm test:coverage + +# Run specific test file +npm test -- log.service.test.ts + +# Run tests matching a pattern +npm test -- --testNamePattern="LogService" +``` + +**Important**: Always run `nvm use` first to switch to Node 18+, as specified in `.nvmrc`. + +## Test Structure + +### Directory Organization + +``` +__tests__/ +├── core/ # Core functionality tests +│ └── container.test.ts +├── services/ # Service tests +│ ├── log.service.test.ts +│ └── render.service.test.ts +├── lifecycle-hooks/ # Lifecycle hook tests +│ └── render-logs.test.ts +├── helpers/ # Test utilities +│ ├── test-utils.ts # Helper functions +│ └── factories.ts # Mock data factories +└── setup.ts # Global test setup +``` + +### Test File Naming + +- Test files: `*.test.ts` +- Place tests adjacent to code: `__tests__/services/my-service.test.ts` +- Match source file names: `log.service.ts` → `log.service.test.ts` + +## Writing Tests + +### Basic Test Structure + +```typescript +import { MyService } from '../../code/services/my-service'; + +describe('MyService', () => { + let service: MyService; + + beforeEach(() => { + // Setup - runs before each test + service = new MyService(); + }); + + afterEach(() => { + // Cleanup - runs after each test + jest.restoreAllMocks(); + }); + + describe('methodName', () => { + it('should do something specific', () => { + // Arrange + const input = 'test'; + + // Act + const result = service.methodName(input); + + // Assert + expect(result).toBe('expected output'); + }); + }); +}); +``` + +### Testing with Dependencies + +Use the DI container for easy mocking: + +```typescript +import { Container, ServiceToken } from '../../code/core/container'; +import { MyService } from '../../code/services/my-service'; +import { LogService } from '../../code/services/log.service'; + +describe('MyService with Dependencies', () => { + let container: Container; + let mockLogService: jest.Mocked; + let myService: MyService; + + beforeEach(() => { + // Create isolated container for testing + container = new Container(); + + // Create mock log service + mockLogService = { + info: jest.fn(), + error: jest.fn(), + // ... other methods + } as any; + + // Register mock + container.register( + ServiceToken.LOG_SERVICE, + () => mockLogService, + ServiceLifetime.SINGLETON + ); + + // Get service under test + myService = new MyService( + container.resolve(ServiceToken.LOG_SERVICE) + ); + }); + + it('should log when processing', () => { + myService.process(); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Processing started', + 'MyService' + ); + }); +}); +``` + +### Using Test Factories + +We provide factory functions for creating mock data: + +```typescript +import { + createMockStripeCustomer, + createMockRootPolicy, + createMockRootPayment +} from '../helpers/factories'; + +it('should process payment', () => { + // Create mock data easily + const customer = createMockStripeCustomer({ + email: 'test@example.com' + }); + + const policy = createMockRootPolicy({ + monthly_premium: 10000 + }); + + // Test with mock data + const result = service.processPayment(customer, policy); + expect(result).toBeDefined(); +}); +``` + +### Testing Async Code + +```typescript +it('should handle async operations', async () => { + const promise = service.asyncMethod(); + + await expect(promise).resolves.toBe('success'); +}); + +it('should handle errors', async () => { + const promise = service.failingMethod(); + + await expect(promise).rejects.toThrow('Expected error'); +}); +``` + +### Mocking External APIs + +```typescript +import StripeClient from '../../code/clients/stripe-client'; + +jest.mock('../../code/clients/stripe-client'); + +describe('Service with Stripe', () => { + let mockStripeClient: jest.Mocked; + + beforeEach(() => { + mockStripeClient = { + stripeSDK: { + customers: { + create: jest.fn().mockResolvedValue({ + id: 'cus_123', + email: 'test@example.com' + }) + } + } + } as any; + }); + + it('should create customer', async () => { + const result = await service.createCustomer('test@example.com'); + + expect(mockStripeClient.stripeSDK.customers.create).toHaveBeenCalled(); + expect(result.id).toBe('cus_123'); + }); +}); +``` + +### Testing Console Output + +The LogService outputs to console, which you may want to verify: + +```typescript +it('should log to console', () => { + const consoleSpy = jest.spyOn(console, 'info').mockImplementation(); + + logService.info('Test message'); + + expect(consoleSpy).toHaveBeenCalled(); + const logOutput = JSON.parse(consoleSpy.mock.calls[0][0]); + expect(logOutput.message).toBe('Test message'); + + consoleSpy.mockRestore(); +}); +``` + +## Test Utilities + +### Available Helpers + +Located in `__tests__/helpers/test-utils.ts`: + +```typescript +import { wait, mockTimers, createMockFn } from '../helpers/test-utils'; + +// Wait for a specific time +await wait(1000); + +// Mock timers +mockTimers.enable(); +mockTimers.advance(1000); +mockTimers.runAll(); +mockTimers.disable(); + +// Create type-safe mock functions +const mockFn = createMockFn<(x: number) => string>(); +``` + +### Factory Functions + +Located in `__tests__/helpers/factories.ts`: + +- `createMockStripeCustomer()` +- `createMockStripePaymentMethod()` +- `createMockStripeSubscription()` +- `createMockStripeInvoice()` +- `createMockRootPolicy()` +- `createMockRootPaymentMethod()` +- `createMockRootPayment()` + +## Testing Best Practices + +### 1. Test Behavior, Not Implementation + +❌ **Bad**: Testing implementation details +```typescript +it('should call private method', () => { + const spy = jest.spyOn(service as any, '_privateMethod'); + service.publicMethod(); + expect(spy).toHaveBeenCalled(); +}); +``` + +✅ **Good**: Testing observable behavior +```typescript +it('should return processed result', () => { + const result = service.publicMethod(); + expect(result).toEqual({ processed: true }); +}); +``` + +### 2. Use Descriptive Test Names + +❌ **Bad**: Vague test names +```typescript +it('works', () => { ... }); +it('test 1', () => { ... }); +``` + +✅ **Good**: Descriptive test names +```typescript +it('should create customer when email is provided', () => { ... }); +it('should throw error when policy ID is invalid', () => { ... }); +``` + +### 3. Follow AAA Pattern + +Always structure tests with Arrange, Act, Assert: + +```typescript +it('should calculate total', () => { + // Arrange - Set up test data + const items = [{ price: 100 }, { price: 200 }]; + + // Act - Execute the code under test + const total = service.calculateTotal(items); + + // Assert - Verify the result + expect(total).toBe(300); +}); +``` + +### 4. Test One Thing at a Time + +❌ **Bad**: Testing multiple things +```typescript +it('should create and update customer', () => { + const customer = service.create(); + expect(customer).toBeDefined(); + + const updated = service.update(customer.id, { name: 'New' }); + expect(updated.name).toBe('New'); +}); +``` + +✅ **Good**: Separate tests +```typescript +it('should create customer', () => { + const customer = service.create(); + expect(customer).toBeDefined(); +}); + +it('should update customer name', () => { + const customer = service.create(); + const updated = service.update(customer.id, { name: 'New' }); + expect(updated.name).toBe('New'); +}); +``` + +### 5. Don't Test External Libraries + +Don't test Stripe SDK, Root SDK, or other external libraries. Test YOUR code: + +❌ **Bad**: Testing Stripe SDK +```typescript +it('should call Stripe API', () => { + expect(stripe.customers.create).toBeDefined(); +}); +``` + +✅ **Good**: Testing your service +```typescript +it('should create customer using Stripe', async () => { + mockStripe.customers.create.mockResolvedValue({ id: 'cus_123' }); + + const customer = await service.createCustomer('test@example.com'); + + expect(customer.id).toBe('cus_123'); +}); +``` + +### 6. Isolate Tests + +Each test should be independent: + +```typescript +// Good - each test creates its own data +beforeEach(() => { + service = new MyService(); +}); + +it('test 1', () => { + const data = createTestData(); + // use data +}); + +it('test 2', () => { + const data = createTestData(); + // use data +}); +``` + +## Coverage Goals + +Aim for: +- **70%+ overall coverage** +- **80%+ for services** +- **90%+ for critical paths** (payment processing, webhook handling) + +Check coverage: +```bash +npm test:coverage +``` + +View HTML report: +```bash +open coverage/lcov-report/index.html +``` + +## Debugging Tests + +### Run Single Test + +```bash +# Run specific test file +npm test -- log.service.test.ts + +# Run tests matching pattern +npm test -- --testNamePattern="should log debug" +``` + +### Debug in VS Code + +Add to `.vscode/launch.json`: +```json +{ + "type": "node", + "request": "launch", + "name": "Jest Debug", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["--runInBand", "--no-cache"], + "console": "integratedTerminal" +} +``` + +## Common Issues + +### Issue: Tests timing out + +**Solution**: Increase timeout +```typescript +jest.setTimeout(10000); // 10 seconds +``` + +### Issue: Async tests not waiting + +**Solution**: Always use `async/await` or return promises +```typescript +it('should work', async () => { + await service.asyncMethod(); // Don't forget await! +}); +``` + +### Issue: Mock not working + +**Solution**: Ensure mock is set up before test runs +```typescript +beforeEach(() => { + jest.clearAllMocks(); // Clear previous mock calls + mockService.method.mockResolvedValue('result'); +}); +``` + +## Next Steps + +1. Write tests for new features as you implement them +2. Maintain >70% coverage +3. Run tests before committing +4. Review test failures carefully - they often catch real bugs! + +## Questions? + +- Check existing tests for examples +- See Jest documentation: https://jestjs.io/ +- Ask in team chat + + + + diff --git a/stripe_collection_module/docs/WEBHOOKS.md b/stripe_collection_module/docs/WEBHOOKS.md new file mode 100644 index 0000000..f38e6c2 --- /dev/null +++ b/stripe_collection_module/docs/WEBHOOKS.md @@ -0,0 +1,678 @@ +# Webhook Setup Guide + +This guide covers setting up and handling webhooks from your payment provider. + +## Overview + +Webhooks allow payment providers to notify your collection module about events like: +- Payment succeeded/failed +- Subscription updated/cancelled +- Refund processed +- Dispute created +- Invoice finalized + +Your collection module receives these webhooks, processes them, and updates Root Platform accordingly. + +--- + +## Architecture + +``` +Payment Provider (Stripe) + ↓ HTTP POST +Your Lambda Function (API Gateway endpoint) + ↓ Verify signature +Webhook Handler + ↓ Route by event type +Event-Specific Controller + ↓ Process and update +Root Platform API +``` + +--- + +## Setting Up Webhooks + +### Step 1: Deploy Your Lambda Function + +First, deploy your collection module to AWS Lambda (see [DEPLOYMENT.md](./DEPLOYMENT.md)). + +You'll need the API Gateway endpoint URL: +``` +https://xxxxx.execute-api.region.amazonaws.com/default/webhook +``` + +### Step 2: Register Webhook with Provider + +#### Stripe Example + +1. **Go to Stripe Dashboard** + - Navigate to [Developers → Webhooks](https://dashboard.stripe.com/webhooks) + - Click "Add endpoint" + +2. **Configure Endpoint** + - Endpoint URL: Your API Gateway URL + - Description: "Collection Module Webhooks" + - API Version: Latest + +3. **Select Events** + +Select the events you need to handle: + +**Payment Events:** +- `payment_intent.succeeded` +- `payment_intent.payment_failed` +- `charge.refunded` +- `charge.dispute.created` + +**Invoice Events:** +- `invoice.created` +- `invoice.paid` +- `invoice.payment_failed` +- `invoice.voided` +- `invoice.marked_uncollectible` + +**Subscription Events:** +- `subscription_schedule.updated` +- `customer.subscription.updated` +- `customer.subscription.deleted` + +4. **Get Signing Secret** + - After creating the endpoint, copy the "Signing secret" + - Format: `whsec_xxxxxxxxxxxxx` + - Save this securely + +5. **Update Your Configuration** + +Add the signing secret to your Lambda environment variables: + +```bash +STRIPE_WEBHOOK_SIGNING_SECRET_LIVE=whsec_xxxxx +``` + +Or in `code/env.ts` for local testing: +```typescript +export const STRIPE_WEBHOOK_SIGNING_SECRET_LIVE = 'whsec_xxxxx'; +``` + +### Step 3: Test Webhook Delivery + +1. **Send Test Event from Provider Dashboard** + - In Stripe Dashboard → Webhooks → Your endpoint + - Click "Send test webhook" + - Select an event type + - Click "Send test event" + +2. **Check Logs** + +```bash +# AWS CloudWatch +aws logs tail /aws/lambda/your-function-name --follow + +# Or in AWS Console +CloudWatch → Log Groups → /aws/lambda/your-function-name +``` + +3. **Verify Response** + - HTTP 200 = Success + - HTTP 400/500 = Error (check logs) + +--- + +## Webhook Security + +### Signature Verification + +**Critical:** Always verify webhook signatures to prevent spoofing. + +#### How It Works + +1. Provider sends webhook with signature header +2. Your code computes expected signature +3. Compare signatures - if they match, webhook is authentic + +#### Implementation + +```typescript +// In webhook-hooks.ts + +import Stripe from 'stripe'; + +export async function processWebhook(event: any) { + const signature = event.headers['stripe-signature']; + const payload = event.body; + + // Verify signature + try { + const webhookEvent = stripe.webhooks.constructEvent( + payload, + signature, + process.env.STRIPE_WEBHOOK_SIGNING_SECRET_LIVE + ); + + // Process the verified event + await handleWebhookEvent(webhookEvent); + + return { + statusCode: 200, + body: JSON.stringify({ received: true }) + }; + } catch (err) { + console.error('Webhook signature verification failed:', err.message); + return { + statusCode: 400, + body: JSON.stringify({ error: 'Invalid signature' }) + }; + } +} +``` + +#### Provider-Specific Verification + +**Stripe:** +```typescript +const event = stripe.webhooks.constructEvent( + payload, + signature, + webhookSecret +); +``` + +**PayPal:** +```typescript +const isValid = paypal.notification.webhookEvent.verify( + headers, + payload, + webhookId +); +``` + +**Square:** +```typescript +const isValid = signature === + crypto.createHmac('sha256', webhookSecret) + .update(payload) + .digest('base64'); +``` + +### Security Best Practices + +- ✅ **Always** verify signatures +- ✅ Use HTTPS endpoints only +- ✅ Validate event structure +- ✅ Log all webhook attempts +- ✅ Rate limit webhook endpoints +- ✅ Use API keys for API Gateway +- ✅ Monitor for suspicious patterns + +--- + +## Handling Webhook Events + +### Event Router Pattern + +```typescript +// In webhook-hooks.ts + +export async function handleWebhookEvent(event: StripeEvent) { + logService.info(`Processing webhook: ${event.type}`, 'WebhookHandler', { + eventId: event.id, + type: event.type + }); + + try { + switch (event.type) { + case 'payment_intent.succeeded': + await processPaymentIntentSucceeded(event); + break; + + case 'payment_intent.payment_failed': + await processPaymentIntentFailed(event); + break; + + case 'invoice.paid': + await processInvoicePaid(event); + break; + + case 'invoice.payment_failed': + await processInvoicePaymentFailed(event); + break; + + case 'charge.refunded': + await processChargeRefunded(event); + break; + + default: + logService.info(`Unhandled event type: ${event.type}`, 'WebhookHandler'); + } + } catch (error) { + logService.error( + `Error processing webhook: ${error.message}`, + 'WebhookHandler', + { eventId: event.id, error } + ); + throw error; // Trigger retry + } +} +``` + +### Event-Specific Controllers + +Create controllers for each event type in `code/controllers/stripe-event-processors/`: + +```typescript +// processInvoicePaidEventController.ts + +export async function processInvoicePaid(event: StripeEvent) { + const invoice = event.data.object as Stripe.Invoice; + + logService.info('Processing invoice.paid event', 'InvoicePaidController', { + invoiceId: invoice.id, + customerId: invoice.customer, + amount: invoice.amount_paid + }); + + try { + // 1. Get policy from Root using metadata + const policyId = invoice.metadata.rootPolicyId; + const policy = await rootService.getPolicy(policyId); + + // 2. Create payment record in Root + await rootService.createPayment({ + policyId: policy.id, + amount: invoice.amount_paid / 100, // Convert cents to dollars + currency: invoice.currency, + status: 'succeeded', + paymentDate: new Date(invoice.status_transitions.paid_at * 1000), + externalId: invoice.payment_intent as string, + metadata: { + invoiceId: invoice.id, + provider: 'stripe' + } + }); + + logService.info('Invoice payment processed successfully', 'InvoicePaidController'); + } catch (error) { + logService.error( + `Failed to process invoice payment: ${error.message}`, + 'InvoicePaidController', + { invoiceId: invoice.id, error } + ); + throw error; + } +} +``` + +--- + +## Local Testing + +### Using Webhook Testing Tools + +#### Option 1: Stripe CLI (Recommended for Stripe) + +```bash +# Install Stripe CLI +brew install stripe/stripe-cli/stripe + +# Login +stripe login + +# Forward webhooks to local endpoint +stripe listen --forward-to http://localhost:3000/webhook + +# Trigger test events +stripe trigger payment_intent.succeeded +stripe trigger invoice.paid +``` + +#### Option 2: ngrok (Works with any provider) + +```bash +# Install ngrok +brew install ngrok + +# Start local server +npm run dev + +# Expose to internet +ngrok http 3000 + +# Use the ngrok URL as webhook endpoint +https://xxxxx.ngrok.io/webhook +``` + +#### Option 3: Mock Webhooks + +Create test files in `__tests__/fixtures/`: + +```typescript +// __tests__/fixtures/stripe-webhooks.ts + +export const mockInvoicePaidEvent = { + id: 'evt_test_123', + type: 'invoice.paid', + data: { + object: { + id: 'in_test_123', + customer: 'cus_test_123', + amount_paid: 5000, + currency: 'usd', + status: 'paid', + metadata: { + rootPolicyId: 'policy_123' + } + } + } +}; +``` + +Then test in your code: + +```typescript +// __tests__/integration/webhook.integration.test.ts + +import { processWebhook } from '../../code/webhook-hooks'; +import { mockInvoicePaidEvent } from '../fixtures/stripe-webhooks'; + +describe('Webhook Processing', () => { + it('should process invoice.paid event', async () => { + const result = await processWebhook({ + body: JSON.stringify(mockInvoicePaidEvent), + headers: { + 'stripe-signature': 'test-signature' + } + }); + + expect(result.statusCode).toBe(200); + }); +}); +``` + +--- + +## Error Handling and Retries + +### Retry Strategy + +Most providers automatically retry failed webhooks: + +**Stripe:** +- Retries for 3 days +- Exponential backoff +- Stops after successful response (2xx) + +**Best Practices:** +- Return 200 immediately after processing +- Use idempotency keys to prevent duplicates +- Log all processing attempts +- Handle duplicate events gracefully + +### Implementing Idempotency + +```typescript +// In your event controller + +const processedEvents = new Set(); + +export async function processInvoicePaid(event: StripeEvent) { + // Check if already processed + if (processedEvents.has(event.id)) { + logService.info('Event already processed, skipping', 'InvoicePaidController', { + eventId: event.id + }); + return; + } + + try { + // Process event + await doActualProcessing(event); + + // Mark as processed + processedEvents.add(event.id); + } catch (error) { + // Don't mark as processed on error - allow retry + throw error; + } +} +``` + +For production, use a persistent store (DynamoDB, Redis) instead of in-memory Set. + +### Error Response Codes + +Return appropriate status codes: + +- **200-299**: Success, don't retry +- **400-499**: Client error, don't retry (except 429) +- **500-599**: Server error, retry + +```typescript +export async function webhookHandler(event: any) { + try { + await processWebhook(event); + return { statusCode: 200, body: 'OK' }; + } catch (error) { + if (error.type === 'validation_error') { + // Don't retry validation errors + return { statusCode: 400, body: error.message }; + } + + // Retry server errors + return { statusCode: 500, body: 'Internal Server Error' }; + } +} +``` + +--- + +## Monitoring Webhooks + +### CloudWatch Metrics + +Track webhook metrics: + +```typescript +// In webhook handler + +import { CloudWatch } from 'aws-sdk'; +const cloudwatch = new CloudWatch(); + +async function recordWebhookMetric(eventType: string, success: boolean) { + await cloudwatch.putMetricData({ + Namespace: 'CollectionModule', + MetricData: [{ + MetricName: 'WebhookProcessed', + Value: success ? 1 : 0, + Unit: 'Count', + Dimensions: [{ + Name: 'EventType', + Value: eventType + }] + }] + }).promise(); +} +``` + +### CloudWatch Alarms + +Create alarms for: +- High error rate +- Slow processing time +- Missing webhooks + +```bash +aws cloudwatch put-metric-alarm \ + --alarm-name webhook-error-rate \ + --alarm-description "Webhook error rate too high" \ + --metric-name WebhookProcessed \ + --namespace CollectionModule \ + --statistic Average \ + --period 300 \ + --threshold 0.9 \ + --comparison-operator LessThanThreshold +``` + +### Webhook Dashboard + +Track webhook health: +- Total webhooks received +- Success/failure rate +- Processing time +- Events by type +- Retry attempts + +--- + +## Debugging Webhooks + +### Common Issues + +#### 1. Signature Verification Failed + +**Symptoms:** +- 400 responses +- "Invalid signature" errors + +**Solutions:** +- Verify webhook secret is correct +- Check you're using raw request body (not parsed JSON) +- Ensure correct header name (`stripe-signature`, `x-square-signature`, etc.) +- Test with provider's CLI tool + +#### 2. Webhooks Timing Out + +**Symptoms:** +- 504 Gateway Timeout +- Provider shows "failed" status + +**Solutions:** +- Increase Lambda timeout +- Optimize slow operations +- Process asynchronously (queue-based) +- Return 200 immediately, process later + +#### 3. Duplicate Events + +**Symptoms:** +- Same event processed multiple times +- Duplicate payments/updates in Root + +**Solutions:** +- Implement idempotency checks +- Use event ID to track processed events +- Check for duplicate external IDs before creating + +#### 4. Missing Webhooks + +**Symptoms:** +- Events happen but webhooks don't arrive +- Provider shows "not delivered" + +**Solutions:** +- Verify endpoint URL is correct and accessible +- Check API Gateway configuration +- Verify Lambda permissions +- Check provider webhook logs + +### Debugging Tips + +1. **Enable Verbose Logging** + +```typescript +logService.debug('Webhook received', 'WebhookHandler', { + headers: event.headers, + body: event.body, + eventType: webhookEvent.type +}); +``` + +2. **Test with Provider Dashboard** + - Send test events + - Check delivery logs + - Review response codes + +3. **Check CloudWatch Logs** + +```bash +aws logs tail /aws/lambda/your-function --follow --filter-pattern "ERROR" +``` + +4. **Use Provider CLI Tools** + +```bash +# Stripe +stripe listen --forward-to http://localhost:3000 +stripe logs tail +``` + +--- + +## Webhook Event Reference + +### Common Events to Handle + +| Event | When to Handle | Action | +|-------|---------------|--------| +| `payment_intent.succeeded` | Payment completed | Create/update payment in Root | +| `payment_intent.payment_failed` | Payment failed | Mark payment as failed in Root | +| `invoice.paid` | Invoice paid | Record payment | +| `invoice.payment_failed` | Invoice payment failed | Update payment status | +| `charge.refunded` | Refund processed | Create refund record | +| `subscription.updated` | Subscription changed | Update policy in Root | +| `subscription.deleted` | Subscription cancelled | Cancel policy in Root | +| `charge.dispute.created` | Dispute opened | Flag payment, notify team | + +### Event Priority + +**High Priority** (process immediately): +- Payment succeeded/failed +- Refunds +- Disputes + +**Medium Priority** (can be delayed): +- Subscription updates +- Invoice created + +**Low Priority** (informational): +- Customer updated +- Payment method updated + +--- + +## Testing Checklist + +- [ ] Webhook endpoint deployed and accessible +- [ ] Signature verification implemented +- [ ] All required events registered with provider +- [ ] Test events sent and processed successfully +- [ ] Error handling tested +- [ ] Idempotency implemented +- [ ] Logging configured +- [ ] CloudWatch alarms set up +- [ ] Retry logic validated +- [ ] Integration with Root Platform tested +- [ ] Documentation updated + +--- + +## Next Steps + +After webhook setup: + +1. **Test End-to-End**: Process real transactions +2. **Monitor Performance**: Watch CloudWatch metrics +3. **Handle Edge Cases**: Test failure scenarios +4. **Document Runbook**: Create incident response guide +5. **Set Up Alerts**: Configure notifications + +--- + +## Resources + +- **Stripe Webhooks**: https://stripe.com/docs/webhooks +- **Webhook Security**: https://stripe.com/docs/webhooks/signatures +- **Stripe CLI**: https://stripe.com/docs/stripe-cli +- **API Gateway**: https://docs.aws.amazon.com/apigateway/ +- **Lambda**: https://docs.aws.amazon.com/lambda/ + diff --git a/stripe_collection_module/jest.config.js b/stripe_collection_module/jest.config.js new file mode 100644 index 0000000..be3d975 --- /dev/null +++ b/stripe_collection_module/jest.config.js @@ -0,0 +1,37 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/code', '/__tests__'], + testMatch: ['**/__tests__/**/*.test.ts', '**/?(*.)+(spec|test).ts'], + transform: { + '^.+\\.ts$': 'ts-jest', + }, + collectCoverageFrom: [ + 'code/**/*.ts', + '!code/**/*.d.ts', + '!code/env.ts', + '!code/env.sample.ts', + '!code/sample.env.ts', + '!code/main.ts', + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + coverageThreshold: { + global: { + branches: 70, + functions: 70, + lines: 70, + statements: 70, + }, + }, + moduleFileExtensions: ['ts', 'js', 'json'], + verbose: true, + setupFilesAfterEnv: ['/__tests__/setup.ts'], + testTimeout: 10000, + clearMocks: true, + resetMocks: true, + restoreMocks: true, +}; + + + diff --git a/stripe_collection_module/package-lock.json b/stripe_collection_module/package-lock.json index ca585ff..20d519a 100644 --- a/stripe_collection_module/package-lock.json +++ b/stripe_collection_module/package-lock.json @@ -10,21 +10,27 @@ "moment-timezone": "0.5.40", "node-fetch": "2.6.7", "phone": "2.4.22", - "stripe": "14.5.0" + "stripe": "^19.2.1" }, "devDependencies": { + "@jest/globals": "^30.2.0", + "@types/jest": "^30.0.0", "@types/node": "^20.11.20", "@types/node-fetch": "^2.6.11", - "eslint": "^8.51.0", - "eslint-config-airbnb-typescript": "^17.0.0", - "eslint-config-prettier": "^8.7.0", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "eslint": "^8.57.0", + "eslint-config-airbnb-typescript": "^18.0.0", + "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-webpack": "^0.13.2", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-mocha": "^10.1.0", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-sonarjs": "^0.18.0", - "eslint-plugin-unicorn": "^48.0.1", - "prettier": "^2.8.3" + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-mocha": "^10.5.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-sonarjs": "^0.25.1", + "eslint-plugin-unicorn": "^55.0.0", + "jest": "^30.2.0", + "prettier": "^3.3.3", + "ts-jest": "^29.4.5" }, "engines": { "node": ">=18.0.0", @@ -125,111 +131,525 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/@babel/code-frame": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "dependencies": { - "@babel/highlight": "^7.24.2", - "picocolors": "^1.0.0" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@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" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", - "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.24.5", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "dependencies": { - "color-convert": "^1.9.0" + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" }, "engines": { - "node": ">=4" + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", "dev": true, "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", "dev": true, "dependencies": { - "color-name": "1.1.3" + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, "engines": { - "node": ">=0.8.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "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", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, "engines": { - "node": ">=4" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, "dependencies": { - "has-flag": "^3.0.0" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@emnapi/core": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.0.tgz", + "integrity": "sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==", + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz", + "integrity": "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@eslint-community/eslint-utils": { @@ -370,1287 +790,2528 @@ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, - "peer": true, "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" }, "engines": { - "node": ">=6.0.0" + "node": ">=12" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, - "peer": true, "engines": { - "node": ">=6.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "peer": true, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dev": true, - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true, - "peer": true + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, - "peer": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">= 8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, "engines": { - "node": ">= 8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" }, "engines": { - "node": ">= 8" - } - }, - "node_modules/@rootplatform/node-sdk": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@rootplatform/node-sdk/-/node-sdk-0.0.7.tgz", - "integrity": "sha512-jYPcPQfz7lu2Guea44ZAVNh45elFdLexS4q07LUz8Qv+wUOr7fHbqMo0LI22+fJ61eWMHJ+vYH4dc1XdsCoXQg==", - "dependencies": { - "oazapfts": "^5.1.7" + "node": ">=8" } }, - "node_modules/@types/eslint": { - "version": "8.56.10", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", - "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, - "peer": true, "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" + "sprintf-js": "~1.0.2" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, - "peer": true, "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true, - "peer": true - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, - "peer": true - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true - }, - "node_modules/@types/node": { - "version": "20.11.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", - "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", "dependencies": { - "undici-types": "~5.26.4" + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@types/node-fetch": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", - "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "dependencies": { - "@types/node": "*", - "form-data": "^4.0.0" + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true - }, - "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", - "dev": true, - "peer": true - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, - "peer": true, "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "p-try": "^2.0.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": ">=6" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, - "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4" + "p-limit": "^2.2.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=8" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", + "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", "dev": true, - "peer": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "node_modules/@jest/core": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", + "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", "dev": true, - "peer": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "@jest/console": "30.2.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.2.0", + "jest-config": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-resolve-dependencies": "30.2.0", + "jest-runner": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "jest-watcher": "30.2.0", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "peerDependenciesMeta": { - "typescript": { + "node-notifier": { "optional": true } } }, - "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", "dev": true, - "peer": true, "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "node_modules/@jest/environment": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", "dev": true, - "peer": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "node_modules/@jest/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", "dev": true, - "peer": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" + "expect": "30.2.0", + "jest-snapshot": "30.2.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", "dev": true, - "peer": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" + "@jest/get-type": "30.1.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "node_modules/@jest/fake-timers": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", "dev": true, - "peer": true, "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", "dev": true, - "peer": true + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "node_modules/@jest/globals": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", + "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", "dev": true, - "peer": true + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/types": "30.2.0", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", "dev": true, - "peer": true + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "node_modules/@jest/reporters": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", + "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", "dev": true, - "peer": true, "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@xtuc/long": "4.2.2" + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "node_modules/@jest/reporters/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, - "peer": true + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "node_modules/@jest/reporters/node_modules/jest-worker": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", "dev": true, - "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "peer": true, "dependencies": { - "@xtuc/ieee754": "^1.2.0" + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, - "peer": true, "dependencies": { - "@xtuc/long": "4.2.2" + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "node_modules/@jest/snapshot-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", + "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", "dev": true, - "peer": true + "dependencies": { + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", "dev": true, - "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "node_modules/@jest/test-result": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", + "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", "dev": true, - "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@jest/console": "30.2.0", + "@jest/types": "30.2.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "node_modules/@jest/test-sequencer": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", + "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", "dev": true, - "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" + "@jest/test-result": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "node_modules/@jest/transform": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", + "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", "dev": true, - "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@babel/core": "^7.27.4", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", "dev": true, - "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@xtuc/long": "4.2.2" + "@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" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, - "peer": true + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, - "peer": true + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, - "bin": { - "acorn": "bin/acorn" - }, "engines": { - "node": ">=0.4.0" + "node": ">=6.0.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, "peer": true, - "peerDependencies": { - "acorn": "^8" + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peer": true, - "peerDependencies": { - "ajv": "^6.9.1" - } + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "dependencies": { - "color-convert": "^2.0.1" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">= 8" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 8" } }, - "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 8" } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, - "peer": true, + "optional": true, "engines": { - "node": ">=8" + "node": ">=14" } }, - "node_modules/array.prototype.find": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.2.3.tgz", - "integrity": "sha512-fO/ORdOELvjbbeIfZfzrXFMhYHGofRGqd+am9zm3tZ4GlJINj/pA2eITyfd65Vg6+ZbHd/Cys7stpoRSWtQFdA==", + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, "engines": { - "node": ">= 0.4" + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/pkgr" } }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", - "dev": true, + "node_modules/@rootplatform/node-sdk": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@rootplatform/node-sdk/-/node-sdk-0.0.7.tgz", + "integrity": "sha512-jYPcPQfz7lu2Guea44ZAVNh45elFdLexS4q07LUz8Qv+wUOr7fHbqMo0LI22+fJ61eWMHJ+vYH4dc1XdsCoXQg==", "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "oazapfts": "^5.1.7" } }, - "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "type-detect": "4.0.8" } }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "@sinonjs/commons": "^3.0.1" } }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "dev": true, + "optional": true, "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "tslib": "^2.4.0" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dev": true, "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "@babel/types": "^7.0.0" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, - "peer": true, "dependencies": { - "balanced-match": "^1.0.0" + "@babel/types": "^7.28.2" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/@types/eslint": { + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", "dev": true, "peer": true, "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" + "@types/estree": "*", + "@types/json-schema": "*" } }, - "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "peer": true, "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "@types/eslint": "*", + "@types/estree": "*" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true, "peer": true }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-me-maybe": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", - "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==" - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001620", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001620.tgz", - "integrity": "sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "peer": true + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6.0" + "@types/istanbul-lib-coverage": "*" } }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" + "dependencies": { + "@types/istanbul-lib-report": "*" } }, - "node_modules/clean-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", - "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", "dev": true, "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=4" + "expect": "^30.0.0", + "pretty-format": "^30.0.0" } }, - "node_modules/clean-regexp/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, - "engines": { - "node": ">=0.8.0" - } + "peer": true }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@types/node": { + "version": "20.11.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", + "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", + "devOptional": true, "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "undici-types": "~5.26.4" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", "dev": true, "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" + "@types/node": "*", + "form-data": "^4.0.0" } }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "peer": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, - "node_modules/confusing-browser-globals": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", - "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "node_modules/@types/yargs": { + "version": "17.0.34", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.34.tgz", + "integrity": "sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==", "dev": true, "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" + "@types/yargs-parser": "*" } }, - "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", "dev": true, "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || >=20.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || >=20.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", "dev": true, "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || >=20.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", "dev": true, "dependencies": { - "ms": "2.1.2" + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": ">=6.0" + "node": "^18.18.0 || >=20.0.0" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true } } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || >=20.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", "dev": true, "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || >=20.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": ">=0.4.0" + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], "dev": true, - "peer": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "optional": true, "dependencies": { - "path-type": "^4.0.0" + "@napi-rs/wasm-runtime": "^0.2.11" }, "engines": { - "node": ">=8" + "node": ">=14.0.0" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", "dev": true, + "peer": true, "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" } }, - "node_modules/electron-to-chromium": { - "version": "1.4.776", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.776.tgz", - "integrity": "sha512-s694bi3+gUzlliqxjPHpa9NRTlhzTgB34aan+pVKZmOTGy2xoZXl+8E1B8i5p5rtev3PKMK/H4asgNejC+YHNg==", + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", "dev": true, "peer": true }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true, + "peer": true }, - "node_modules/enhanced-resolve": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz", - "integrity": "sha512-kxpoMgrdtkXZ5h0SeraBS1iRntpTpQ3R8ussdb38+UAFnMGX5DDyJXePm+OCHOcoXvHDw7mc2erbJBpDnl7TPw==", + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", "dev": true, + "peer": true, "dependencies": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.2.0", - "tapable": "^0.1.8" - }, - "engines": { - "node": ">=0.6" + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" } }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", "dev": true, + "peer": true, "dependencies": { - "is-arrayish": "^0.2.1" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.12.1" } }, - "node_modules/es-abstract": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", - "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", "dev": true, + "peer": true, "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "peer": true + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "peer": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peer": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.find": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.2.3.tgz", + "integrity": "sha512-fO/ORdOELvjbbeIfZfzrXFMhYHGofRGqd+am9zm3tZ4GlJINj/pA2eITyfd65Vg6+ZbHd/Cys7stpoRSWtQFdA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", + "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", + "dev": true, + "dependencies": { + "@jest/transform": "30.2.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", + "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", + "dev": true, + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", + "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.24", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.24.tgz", + "integrity": "sha512-uUhTRDPXamakPyghwrUcjaGvvBqGrWvBHReoiULMIpOJVM9IYzQh83Xk2Onx5HlGI2o10NNCzcs9TG/S3TkwrQ==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001753", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz", + "integrity": "sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", + "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", + "dev": true + }, + "node_modules/clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clean-regexp/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "peer": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/core-js-compat": { + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.46.0.tgz", + "integrity": "sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==", + "dev": true, + "dependencies": { + "browserslist": "^4.26.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.245", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.245.tgz", + "integrity": "sha512-rdmGfW47ZhL/oWEJAY4qxRtdly2B98ooTJ0pdEI4jhVLZ6tNf8fPtov2wS1IRKwFJT92le3x4Knxiwzl7cPPpQ==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/enhanced-resolve": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz", + "integrity": "sha512-kxpoMgrdtkXZ5h0SeraBS1iRntpTpQ3R8ussdb38+UAFnMGX5DDyJXePm+OCHOcoXvHDw7mc2erbJBpDnl7TPw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.2.0", + "tapable": "^0.1.8" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2", "has-proto": "^1.0.3", @@ -1662,574 +3323,1105 @@ "is-data-view": "^1.0.1", "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", - "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.3.tgz", + "integrity": "sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==", + "dev": true, + "peer": true + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-airbnb-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", + "dev": true, + "dependencies": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.2" + } + }, + "node_modules/eslint-config-airbnb-base/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-config-airbnb-typescript": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-18.0.0.tgz", + "integrity": "sha512-oc+Lxzgzsu8FQyFVa4QFaVKiitTYiiW3frB9KYW5OWdPrqFc7FzxgB20hP4cHMlr+MBzGcLl3jnCOVOydL9mIg==", + "dev": true, + "dependencies": { + "eslint-config-airbnb-base": "^15.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-import-resolver-webpack": { + "version": "0.13.8", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.13.8.tgz", + "integrity": "sha512-Y7WIaXWV+Q21Rz/PJgUxiW/FTBOWmU8NTLdz+nz9mMoiz5vAev/fOaQxwD7qRzTfE3HSm1qsxZ5uRd7eX+VEtA==", + "dev": true, + "dependencies": { + "array.prototype.find": "^2.2.2", + "debug": "^3.2.7", + "enhanced-resolve": "^0.9.1", + "find-root": "^1.1.0", + "hasown": "^2.0.0", + "interpret": "^1.4.0", + "is-core-module": "^2.13.1", + "is-regex": "^1.1.4", + "lodash": "^4.17.21", + "resolve": "^2.0.0-next.5", + "semver": "^5.7.2" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "eslint-plugin-import": ">=1.4.0", + "webpack": ">=1.11.0" + } + }, + "node_modules/eslint-import-resolver-webpack/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-webpack/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", + "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-mocha": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.5.0.tgz", + "integrity": "sha512-F2ALmQVPT1GoP27O1JTZGrV9Pqg8k79OeIuvw63UxMtQKREZtmkK1NFgkZQ2TW7L2JSSFKHFPTtHu5z8R9QNRw==", + "dev": true, + "dependencies": { + "eslint-utils": "^3.0.0", + "globals": "^13.24.0", + "rambda": "^7.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-sonarjs": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.25.1.tgz", + "integrity": "sha512-5IOKvj/GMBNqjxBdItfotfRHo7w48496GOu1hxdeXuD0mB1JBlDCViiLHETDTfA8pDAVSBimBEQoetRXYceQEw==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-unicorn": { + "version": "55.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz", + "integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.5", + "@eslint-community/eslint-utils": "^4.4.0", + "ci-info": "^4.0.0", + "clean-regexp": "^1.0.0", + "core-js-compat": "^3.37.0", + "esquery": "^1.5.0", + "globals": "^15.7.0", + "indent-string": "^4.0.0", + "is-builtin-module": "^3.2.1", + "jsesc": "^3.0.2", + "pluralize": "^8.0.0", + "read-pkg-up": "^7.0.1", + "regexp-tree": "^0.1.27", + "regjsparser": "^0.10.0", + "semver": "^7.6.1", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=18.18" + }, + "funding": { + "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" + }, + "peerDependencies": { + "eslint": ">=8.56.0" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "*" } }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, "dependencies": { - "get-intrinsic": "^1.2.4" + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": ">= 0.4" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, "engines": { - "node": ">= 0.4" + "node": ">=4" } }, - "node_modules/es-module-lexer": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.3.tgz", - "integrity": "sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==", - "dev": true, - "peer": true - }, - "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "dependencies": { - "es-errors": "^1.3.0" + "estraverse": "^5.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10" } }, - "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.4", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "estraverse": "^5.2.0" }, "engines": { - "node": ">= 0.4" + "node": ">=4.0" } }, - "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "dependencies": { - "hasown": "^2.0.0" + "engines": { + "node": ">=4.0" } }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==" - }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "peer": true, "engines": { - "node": ">=6" + "node": ">=0.8.x" } }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" + "@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" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/eslint-config-airbnb-base": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", - "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "dependencies": { - "confusing-browser-globals": "^1.0.10", - "object.assign": "^4.1.2", - "object.entries": "^1.1.5", - "semver": "^6.3.0" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" }, "engines": { - "node": "^10.12.0 || >=12.0.0" - }, - "peerDependencies": { - "eslint": "^7.32.0 || ^8.2.0", - "eslint-plugin-import": "^2.25.2" + "node": ">=8.6.0" } }, - "node_modules/eslint-config-airbnb-base/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "bin": { - "semver": "bin/semver.js" + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/eslint-config-airbnb-typescript": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-17.1.0.tgz", - "integrity": "sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==", + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, "dependencies": { - "eslint-config-airbnb-base": "^15.0.0" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^5.13.0 || ^6.0.0", - "@typescript-eslint/parser": "^5.0.0 || ^6.0.0", - "eslint": "^7.32.0 || ^8.2.0", - "eslint-plugin-import": "^2.25.3" + "reusify": "^1.0.4" } }, - "node_modules/eslint-config-prettier": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", - "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", "dev": true, - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" + "dependencies": { + "bser": "2.1.1" } }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { - "ms": "^2.1.1" + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/eslint-import-resolver-node/node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, - "bin": { - "resolve": "bin/resolve" + "engines": { + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-import-resolver-webpack": { - "version": "0.13.8", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.13.8.tgz", - "integrity": "sha512-Y7WIaXWV+Q21Rz/PJgUxiW/FTBOWmU8NTLdz+nz9mMoiz5vAev/fOaQxwD7qRzTfE3HSm1qsxZ5uRd7eX+VEtA==", + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "dependencies": { - "array.prototype.find": "^2.2.2", - "debug": "^3.2.7", - "enhanced-resolve": "^0.9.1", - "find-root": "^1.1.0", - "hasown": "^2.0.0", - "interpret": "^1.4.0", - "is-core-module": "^2.13.1", - "is-regex": "^1.1.4", - "lodash": "^4.17.21", - "resolve": "^2.0.0-next.5", - "semver": "^5.7.2" + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" }, "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "eslint-plugin-import": ">=1.4.0", - "webpack": ">=1.11.0" + "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/eslint-import-resolver-webpack/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true }, - "node_modules/eslint-import-resolver-webpack/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", "dev": true, - "bin": { - "semver": "bin/semver" + "dependencies": { + "is-callable": "^1.1.3" } }, - "node_modules/eslint-module-utils": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", - "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, "dependencies": { - "debug": "^3.2.7" + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" }, "engines": { - "node": ">=4" + "node": ">=14" }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "dependencies": { - "ms": "^2.1.1" + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "dev": true, "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", - "semver": "^6.3.1", - "tsconfig-paths": "^3.15.0" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" }, "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "node": ">= 6" } }, - "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "dev": true, "dependencies": { - "esutils": "^2.0.2" + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, - "bin": { - "semver": "bin/semver.js" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/eslint-plugin-mocha": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.4.3.tgz", - "integrity": "sha512-emc4TVjq5Ht0/upR+psftuz6IBG5q279p+1dSRDeHf+NS9aaerBi3lXKo1SEzwC29hFIW21gO89CEWSvRsi8IQ==", - "dev": true, - "dependencies": { - "eslint-utils": "^3.0.0", - "globals": "^13.24.0", - "rambda": "^7.4.0" - }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "eslint": ">=7.0.0" + "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/eslint-plugin-prettier": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", - "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", - "dev": true, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { - "prettier-linter-helpers": "^1.0.0" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" }, "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "eslint": ">=7.28.0", - "prettier": ">=2.0.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "eslint-config-prettier": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-plugin-sonarjs": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.18.0.tgz", - "integrity": "sha512-DJ3osLnt6KFdT5e9ZuIDOjT5A6wUGSLeiJJT03lPgpdD+7CVWlYAw9Goe3bt7SmbFO3Xh89NOCZAuB9XA7bAUQ==", + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, "engines": { - "node": ">=14" - }, - "peerDependencies": { - "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + "node": ">=8.0.0" } }, - "node_modules/eslint-plugin-unicorn": { - "version": "48.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-48.0.1.tgz", - "integrity": "sha512-FW+4r20myG/DqFcCSzoumaddKBicIPeFnTrifon2mWIzlfyvzwyqZjqVP7m4Cqr/ZYisS2aiLghkUWaPg6vtCw==", + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", - "@eslint-community/eslint-utils": "^4.4.0", - "ci-info": "^3.8.0", - "clean-regexp": "^1.0.0", - "esquery": "^1.5.0", - "indent-string": "^4.0.0", - "is-builtin-module": "^3.2.1", - "jsesc": "^3.0.2", - "lodash": "^4.17.21", - "pluralize": "^8.0.0", - "read-pkg-up": "^7.0.1", - "regexp-tree": "^0.1.27", - "regjsparser": "^0.10.0", - "semver": "^7.5.4", - "strip-indent": "^3.0.0" - }, "engines": { - "node": ">=16" + "node": ">=10" }, "funding": { - "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" - }, - "peerDependencies": { - "eslint": ">=8.44.0" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dev": true, "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">= 0.4" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "dependencies": { - "eslint-visitor-keys": "^2.0.0" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + "node": "*" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, "engines": { - "node": ">=10" + "node": ">=10.13.0" } }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } + "peer": true }, - "node_modules/eslint/node_modules/brace-expansion": { + "node_modules/glob/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", @@ -2239,7 +4431,7 @@ "concat-map": "0.0.1" } }, - "node_modules/eslint/node_modules/minimatch": { + "node_modules/glob/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", @@ -2251,268 +4443,356 @@ "node": "*" } }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "type-fest": "^0.20.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=8" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, "dependencies": { - "estraverse": "^5.1.0" + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" }, "engines": { - "node": ">=0.10" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", "dev": true, "dependencies": { - "estraverse": "^5.2.0" + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" }, "engines": { - "node": ">=4.0" + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" } }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", "dev": true, - "engines": { - "node": ">=4.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "peer": true, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "engines": { - "node": ">=0.8.x" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, - "peer": true, "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "has-symbols": "^1.0.3" }, "engines": { - "node": ">=8.6.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "peer": true, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { - "is-glob": "^4.0.1" + "function-bind": "^1.1.2" }, "engines": { - "node": ">= 6" + "node": ">= 0.4" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "node_modules/hoek": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.3.1.tgz", + "integrity": "sha512-v7E+yIjcHECn973i0xHm4kJkEpv3C8sbYS4344WXbzYqRyiDD7rjnnKo4hsJkejQBAFdRMUGNHySeSPKSH9Rqw==", + "deprecated": "This module has moved and is now available at @hapi/hoek. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + "node_modules/http2-client": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", + "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==" }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, - "dependencies": { - "reusify": "^1.0.4" + "engines": { + "node": ">=10.17.0" } }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">= 4" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, - "peer": true, "dependencies": { - "to-regex-range": "^5.0.1" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "dev": true - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=0.8.19" } }, - "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dev": true, "dependencies": { - "is-callable": "^1.1.3" + "once": "^1.3.0", + "wrappy": "1" } }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dev": true, "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" }, "engines": { - "node": ">= 6" + "node": ">= 0.4" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" + "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -2521,50 +4801,52 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, - "engines": { - "node": ">= 0.4" - }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -2573,90 +4855,87 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "has-tostringtag": "^1.0.0" }, "engines": { - "node": "*" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, "engines": { - "node": ">=10.13.0" + "node": ">=0.10.0" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "peer": true + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "engines": { + "node": ">=6" } }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "is-extglob": "^2.1.1" }, "engines": { - "node": "*" + "node": ">=0.10.0" } }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { - "node": ">=8" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", "dev": true, "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" + "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -2665,83 +4944,66 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, - "peer": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, "dependencies": { - "get-intrinsic": "^1.1.3" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, "engines": { "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dependencies": { - "es-define-property": "^1.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -2749,10 +5011,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, "engines": { "node": ">= 0.4" }, @@ -2760,13 +5026,13 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dev": true, "dependencies": { - "has-symbols": "^1.0.3" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -2775,424 +5041,765 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, "dependencies": { - "function-bind": "^1.1.2" + "call-bind": "^1.0.2" }, - "engines": { - "node": ">= 0.4" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hoek": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.3.1.tgz", - "integrity": "sha512-v7E+yIjcHECn973i0xHm4kJkEpv3C8sbYS4344WXbzYqRyiDD7rjnnKo4hsJkejQBAFdRMUGNHySeSPKSH9Rqw==", - "deprecated": "This module has moved and is now available at @hapi/hoek. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.", + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isemail": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz", + "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==", + "dependencies": { + "punycode": "2.x.x" + }, "engines": { - "node": ">=6.0.0" + "node": ">=4.0.0" } }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "node_modules/http2-client": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", - "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==" - }, - "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "engines": { - "node": ">= 4" + "node": ">=8" } }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" }, "engines": { - "node": ">=6" + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=10" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, "engines": { - "node": ">=0.8.19" + "node": ">=10" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, "engines": { "node": ">=8" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "node_modules/jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", + "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", + "dev": true, + "dependencies": { + "@jest/core": "30.2.0", + "@jest/types": "30.2.0", + "import-local": "^3.2.0", + "jest-cli": "30.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } }, - "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "node_modules/jest-changed-files": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", + "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", "dev": true, "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" + "execa": "^5.1.1", + "jest-util": "30.2.0", + "p-limit": "^3.1.0" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "node_modules/jest-circus": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", + "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", "dev": true, + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "p-limit": "^3.1.0", + "pretty-format": "30.2.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", + "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", + "dev": true, + "dependencies": { + "@jest/core": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, "engines": { - "node": ">= 0.10" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "node_modules/jest-config": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", + "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", "dev": true, "dependencies": { - "has-bigints": "^1.0.1" + "detect-newline": "^3.1.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "node_modules/jest-each": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", + "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "jest-util": "30.2.0", + "pretty-format": "30.2.0" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", + "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", + "dev": true, + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-builtin-module": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", - "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "node_modules/jest-haste-map": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", + "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", "dev": true, "dependencies": { - "builtin-modules": "^3.3.0" + "@jest/types": "30.2.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "micromatch": "^4.0.8", + "walker": "^1.0.8" }, "engines": { - "node": ">=6" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "fsevents": "^2.3.3" } }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "node_modules/jest-haste-map/node_modules/jest-worker": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "node_modules/jest-leak-detector": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", + "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", "dev": true, "dependencies": { - "hasown": "^2.0.0" + "@jest/get-type": "30.1.0", + "pretty-format": "30.2.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", "dev": true, "dependencies": { - "is-typed-array": "^1.1.13" + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "@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" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/jest-resolve": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", + "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", "dev": true, "dependencies": { - "is-extglob": "^2.1.1" + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" }, "engines": { - "node": ">=0.10.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "node_modules/jest-resolve-dependencies": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", + "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", "dev": true, - "engines": { - "node": ">= 0.4" + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.2.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/jest-runner": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", + "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", "dev": true, - "peer": true, + "dependencies": { + "@jest/console": "30.2.0", + "@jest/environment": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-leak-detector": "30.2.0", + "jest-message-util": "30.2.0", + "jest-resolve": "30.2.0", + "jest-runtime": "30.2.0", + "jest-util": "30.2.0", + "jest-watcher": "30.2.0", + "jest-worker": "30.2.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, "engines": { - "node": ">=0.12.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "node_modules/jest-runner/node_modules/jest-worker": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "dev": true, - "engines": { - "node": ">=8" + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "node_modules/jest-runner/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "node_modules/jest-runtime": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", + "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", "dev": true, "dependencies": { - "call-bind": "^1.0.7" + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/globals": "30.2.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "node_modules/jest-runtime/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": ">= 0.4" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "node_modules/jest-runtime/node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, "engines": { - "node": ">= 0.4" + "node": ">=8" + } + }, + "node_modules/jest-snapshot": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", + "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "pretty-format": "30.2.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", "dev": true, "dependencies": { - "which-typed-array": "^1.1.14" + "@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" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "node_modules/jest-validate": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", + "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2" + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.2.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/isemail": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz", - "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==", + "node_modules/jest-watcher": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", + "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", + "dev": true, "dependencies": { - "punycode": "2.x.x" + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.2.0", + "string-length": "^4.0.2" }, "engines": { - "node": ">=4.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -3313,6 +5920,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3362,12 +5978,57 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/memory-fs": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz", @@ -3378,33 +6039,42 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "peer": true + "dev": true }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, - "peer": true, "engines": { "node": ">= 8" } }, "node_modules/micromatch": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.6.tgz", - "integrity": "sha512-Y4Ypn3oujJYxJcMacVgcs92wofTHxp9FzfDpQON4msDefoC0lb3ETvQLOdLcbhSwU1bz8HrL/1sygfBIHudrkQ==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, - "peer": true, "dependencies": { "braces": "^3.0.3", - "picomatch": "^4.0.2" + "picomatch": "^2.3.1" }, "engines": { "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -3426,6 +6096,15 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -3436,11 +6115,10 @@ } }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, - "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -3459,6 +6137,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -3484,6 +6171,21 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -3494,8 +6196,7 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/node-fetch": { "version": "2.6.7", @@ -3527,6 +6228,12 @@ "node": "4.x || >=6.0.0" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, "node_modules/node-readfiles": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", @@ -3536,11 +6243,10 @@ } }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true, - "peer": true + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true }, "node_modules/normalize-package-data": { "version": "2.5.0", @@ -3580,6 +6286,27 @@ "semver": "bin/semver" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/oas-kit-common": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", @@ -3767,6 +6494,21 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/openapi-types": { "version": "12.1.3", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", @@ -3829,6 +6571,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3892,12 +6640,33 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, - "peer": true, "engines": { "node": ">=8" } @@ -3911,9 +6680,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "node_modules/picomatch": { @@ -3921,7 +6690,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, @@ -3929,6 +6697,79 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -3957,15 +6798,15 @@ } }, "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" @@ -3983,6 +6824,32 @@ "node": ">=6.0.0" } }, + "node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3991,6 +6858,22 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, "node_modules/qs": { "version": "6.11.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", @@ -4041,6 +6924,12 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -4232,6 +7121,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -4365,9 +7275,9 @@ } }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -4503,12 +7413,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, - "peer": true, "engines": { "node": ">=8" } @@ -4518,7 +7433,6 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -4571,6 +7485,40 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -4584,6 +7532,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -4644,6 +7607,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -4653,6 +7629,15 @@ "node": ">=4" } }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -4678,15 +7663,22 @@ } }, "node_modules/stripe": { - "version": "14.5.0", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-14.5.0.tgz", - "integrity": "sha512-MTt0P0VYDAj2VZyZMW41AxcXVs9s06EDFIHYat4UUkluJgnBHo4T4E2byPhnxoJCPvdzWBwBLnhWbSOg//VTpA==", + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-19.2.1.tgz", + "integrity": "sha512-eRc2T413316D7fAjSSEgPbJJ+4a8KY9rOTOb27aXd7bkw9ADO/3OxmIk7YWDhWvHgvxnEZ/29YjcmBBOu4mhrw==", "dependencies": { - "@types/node": ">=8.1.0", "qs": "^6.11.0" }, "engines": { - "node": ">=12.*" + "node": ">=16" + }, + "peerDependencies": { + "@types/node": ">=16" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/supports-color": { @@ -4739,6 +7731,21 @@ "url": "https://github.com/Mermade/oas-kit?sponsor=1" } }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/tapable": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz", @@ -4802,18 +7809,59 @@ } } }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "peer": true, "dependencies": { "is-number": "^7.0.0" }, @@ -4843,7 +7891,6 @@ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, - "peer": true, "engines": { "node": ">=16" }, @@ -4851,6 +7898,82 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-jest": { + "version": "29.4.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz", + "integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==", + "dev": true, + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -4863,6 +7986,13 @@ "strip-bom": "^3.0.0" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "optional": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4875,6 +8005,15 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -4972,6 +8111,19 @@ "node": ">=14.17" } }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -4990,12 +8142,47 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "devOptional": true + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } }, "node_modules/update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", "dev": true, "funding": [ { @@ -5011,10 +8198,9 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -5031,6 +8217,20 @@ "punycode": "^2.1.0" } }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -5041,6 +8241,15 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/watchpack": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", @@ -5234,6 +8443,12 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -5250,12 +8465,55 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -5264,6 +8522,12 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, "node_modules/yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", @@ -5388,90 +8652,388 @@ } } }, - "@babel/code-frame": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + } + }, + "@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true + }, + "@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@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" + }, + "dependencies": { + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "requires": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true + }, + "@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "requires": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + } + }, + "@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true + }, + "@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true + }, + "@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "requires": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + } + }, + "@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "requires": { + "@babel/types": "^7.28.5" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "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", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + } + }, + "@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "dev": true, "requires": { - "@babel/highlight": "^7.24.2", - "picocolors": "^1.0.0" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" } }, - "@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "@babel/highlight": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", - "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "@emnapi/core": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.0.tgz", + "integrity": "sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==", "dev": true, + "optional": true, "requires": { - "@babel/helper-validator-identifier": "^7.24.5", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "@emnapi/runtime": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz", + "integrity": "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.4.0" + } + }, + "@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.4.0" } }, "@eslint-community/eslint-utils": { @@ -5582,15 +9144,463 @@ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/console": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", + "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", + "dev": true, + "requires": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", + "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", + "dev": true, + "requires": { + "@jest/console": "30.2.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.2.0", + "jest-config": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-resolve-dependencies": "30.2.0", + "jest-runner": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "jest-watcher": "30.2.0", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0" + } + }, + "@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true + }, + "@jest/environment": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "dev": true, + "requires": { + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" + } + }, + "@jest/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", + "dev": true, + "requires": { + "expect": "30.2.0", + "jest-snapshot": "30.2.0" + } + }, + "@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "dev": true, + "requires": { + "@jest/get-type": "30.1.0" + } + }, + "@jest/fake-timers": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "dev": true, + "requires": { + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + } + }, + "@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true + }, + "@jest/globals": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", + "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", + "dev": true, + "requires": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/types": "30.2.0", + "jest-mock": "30.2.0" + } + }, + "@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + } + }, + "@jest/reporters": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", + "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "dependencies": { + "glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + }, + "jest-worker": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "dev": true, + "requires": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/snapshot-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", + "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", + "dev": true, + "requires": { + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + } + }, + "@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + } + }, + "@jest/test-result": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", + "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", + "dev": true, + "requires": { + "@jest/console": "30.2.0", + "@jest/types": "30.2.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + } + }, + "@jest/test-sequencer": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", + "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", + "dev": true, + "requires": { + "@jest/test-result": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "slash": "^3.0.0" + } + }, + "@jest/transform": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", + "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", + "dev": true, + "requires": { + "@babel/core": "^7.27.4", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + } + }, + "@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "requires": { + "@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" + } + }, "@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, - "peer": true, "requires": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, @@ -5598,15 +9608,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "peer": true - }, - "@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "peer": true + "dev": true }, "@jridgewell/source-map": { "version": "0.3.6", @@ -5620,18 +9622,16 @@ } }, "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true, - "peer": true + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, - "peer": true, "requires": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -5642,6 +9642,18 @@ "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" }, + "@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "optional": true, + "requires": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5668,6 +9680,19 @@ "fastq": "^1.6.0" } }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, + "@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true + }, "@rootplatform/node-sdk": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/@rootplatform/node-sdk/-/node-sdk-0.0.7.tgz", @@ -5676,6 +9701,81 @@ "oazapfts": "^5.1.7" } }, + "@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true + }, + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1" + } + }, + "@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.4.0" + } + }, + "@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "requires": { + "@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", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "requires": { + "@babel/types": "^7.28.2" + } + }, "@types/eslint": { "version": "8.56.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", @@ -5705,6 +9805,40 @@ "dev": true, "peer": true }, + "@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "requires": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, "@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -5722,6 +9856,7 @@ "version": "20.11.20", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", + "devOptional": true, "requires": { "undici-types": "~5.26.4" } @@ -5742,128 +9877,265 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, - "@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "@types/yargs": { + "version": "17.0.34", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.34.tgz", + "integrity": "sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==", "dev": true, - "peer": true + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", "dev": true, - "peer": true, "requires": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "graphemer": "^1.4.0", - "ignore": "^5.2.4", + "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" } }, "@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, - "peer": true, "requires": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", "dev": true, - "peer": true, "requires": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" } }, "@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", "dev": true, - "peer": true, "requires": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" } }, "@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true, - "peer": true + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", "dev": true, - "peer": true, "requires": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" } }, "@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", "dev": true, - "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" } }, "@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", "dev": true, - "peer": true, "requires": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" } }, "@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true }, + "@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "dev": true, + "optional": true, + "requires": { + "@napi-rs/wasm-runtime": "^0.2.11" + } + }, + "@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "dev": true, + "optional": true + }, "@webassemblyjs/ast": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", @@ -6080,6 +10352,23 @@ "peer": true, "requires": {} }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + }, + "dependencies": { + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + } + } + }, "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -6093,6 +10382,24 @@ "color-convert": "^2.0.1" } }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "dependencies": { + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + } + } + }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -6127,8 +10434,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "peer": true + "dev": true }, "array.prototype.find": { "version": "2.2.3", @@ -6212,18 +10518,93 @@ "possible-typed-array-names": "^1.0.0" } }, + "babel-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", + "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", + "dev": true, + "requires": { + "@jest/transform": "30.2.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", + "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", + "dev": true, + "requires": { + "@types/babel__core": "^7.20.5" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + } + }, + "babel-preset-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", + "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "baseline-browser-mapping": { + "version": "2.8.24", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.24.tgz", + "integrity": "sha512-uUhTRDPXamakPyghwrUcjaGvvBqGrWvBHReoiULMIpOJVM9IYzQh83Xk2Onx5HlGI2o10NNCzcs9TG/S3TkwrQ==", + "dev": true + }, "brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, - "peer": true, "requires": { "balanced-match": "^1.0.0" } @@ -6233,30 +10614,46 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, - "peer": true, "requires": { "fill-range": "^7.1.1" } }, "browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "dev": true, + "requires": { + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" + } + }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "dev": true, - "peer": true, "requires": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "node-int64": "^0.4.0" } }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "peer": true + "dev": true }, "builtin-modules": { "version": "3.3.0", @@ -6287,12 +10684,17 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, "caniuse-lite": { - "version": "1.0.30001620", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001620.tgz", - "integrity": "sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==", - "dev": true, - "peer": true + "version": "1.0.30001753", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz", + "integrity": "sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==", + "dev": true }, "chalk": { "version": "4.1.2", @@ -6304,6 +10706,12 @@ "supports-color": "^7.1.0" } }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, "chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -6312,9 +10720,15 @@ "peer": true }, "ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true + }, + "cjs-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", + "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", "dev": true }, "clean-regexp": { @@ -6344,6 +10758,18 @@ "wrap-ansi": "^7.0.0" } }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -6385,10 +10811,25 @@ "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", "dev": true }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "core-js-compat": { + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.46.0.tgz", + "integrity": "sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==", + "dev": true, + "requires": { + "browserslist": "^4.26.3" + } + }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -6438,12 +10879,25 @@ "ms": "2.1.2" } }, + "dedent": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "dev": true, + "requires": {} + }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true + }, "define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -6471,12 +10925,17 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, - "peer": true, "requires": { "path-type": "^4.0.0" } @@ -6490,12 +10949,23 @@ "esutils": "^2.0.2" } }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "electron-to-chromium": { - "version": "1.4.776", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.776.tgz", - "integrity": "sha512-s694bi3+gUzlliqxjPHpa9NRTlhzTgB34aan+pVKZmOTGy2xoZXl+8E1B8i5p5rtev3PKMK/H4asgNejC+YHNg==", - "dev": true, - "peer": true + "version": "1.5.245", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.245.tgz", + "integrity": "sha512-rdmGfW47ZhL/oWEJAY4qxRtdly2B98ooTJ0pdEI4jhVLZ6tNf8fPtov2wS1IRKwFJT92le3x4Knxiwzl7cPPpQ==", + "dev": true + }, + "emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true }, "emoji-regex": { "version": "8.0.0", @@ -6642,9 +11112,9 @@ "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==" }, "escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==" + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" }, "escape-string-regexp": { "version": "4.0.0", @@ -6740,18 +11210,18 @@ } }, "eslint-config-airbnb-typescript": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-17.1.0.tgz", - "integrity": "sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==", + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-18.0.0.tgz", + "integrity": "sha512-oc+Lxzgzsu8FQyFVa4QFaVKiitTYiiW3frB9KYW5OWdPrqFc7FzxgB20hP4cHMlr+MBzGcLl3jnCOVOydL9mIg==", "dev": true, "requires": { "eslint-config-airbnb-base": "^15.0.0" } }, "eslint-config-prettier": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", - "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, "requires": {} }, @@ -6915,9 +11385,9 @@ } }, "eslint-plugin-mocha": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.4.3.tgz", - "integrity": "sha512-emc4TVjq5Ht0/upR+psftuz6IBG5q279p+1dSRDeHf+NS9aaerBi3lXKo1SEzwC29hFIW21gO89CEWSvRsi8IQ==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.5.0.tgz", + "integrity": "sha512-F2ALmQVPT1GoP27O1JTZGrV9Pqg8k79OeIuvw63UxMtQKREZtmkK1NFgkZQ2TW7L2JSSFKHFPTtHu5z8R9QNRw==", "dev": true, "requires": { "eslint-utils": "^3.0.0", @@ -6926,42 +11396,52 @@ } }, "eslint-plugin-prettier": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", - "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", "dev": true, "requires": { - "prettier-linter-helpers": "^1.0.0" + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" } }, "eslint-plugin-sonarjs": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.18.0.tgz", - "integrity": "sha512-DJ3osLnt6KFdT5e9ZuIDOjT5A6wUGSLeiJJT03lPgpdD+7CVWlYAw9Goe3bt7SmbFO3Xh89NOCZAuB9XA7bAUQ==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.25.1.tgz", + "integrity": "sha512-5IOKvj/GMBNqjxBdItfotfRHo7w48496GOu1hxdeXuD0mB1JBlDCViiLHETDTfA8pDAVSBimBEQoetRXYceQEw==", "dev": true, "requires": {} }, "eslint-plugin-unicorn": { - "version": "48.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-48.0.1.tgz", - "integrity": "sha512-FW+4r20myG/DqFcCSzoumaddKBicIPeFnTrifon2mWIzlfyvzwyqZjqVP7m4Cqr/ZYisS2aiLghkUWaPg6vtCw==", + "version": "55.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz", + "integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.24.5", "@eslint-community/eslint-utils": "^4.4.0", - "ci-info": "^3.8.0", + "ci-info": "^4.0.0", "clean-regexp": "^1.0.0", + "core-js-compat": "^3.37.0", "esquery": "^1.5.0", + "globals": "^15.7.0", "indent-string": "^4.0.0", "is-builtin-module": "^3.2.1", "jsesc": "^3.0.2", - "lodash": "^4.17.21", "pluralize": "^8.0.0", "read-pkg-up": "^7.0.1", "regexp-tree": "^0.1.27", "regjsparser": "^0.10.0", - "semver": "^7.5.4", + "semver": "^7.6.1", "strip-indent": "^3.0.0" + }, + "dependencies": { + "globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true + } } }, "eslint-scope": { @@ -7050,6 +11530,43 @@ "dev": true, "peer": true }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true + }, + "expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "dev": true, + "requires": { + "@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" + } + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7062,17 +11579,16 @@ "dev": true }, "fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, - "peer": true, "requires": { "@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" + "micromatch": "^4.0.8" }, "dependencies": { "glob-parent": { @@ -7080,7 +11596,6 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "peer": true, "requires": { "is-glob": "^4.0.1" } @@ -7110,7 +11625,16 @@ "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, "requires": { - "reusify": "^1.0.4" + "reusify": "^1.0.4" + } + }, + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "requires": { + "bser": "2.1.1" } }, "file-entry-cache": { @@ -7127,7 +11651,6 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, - "peer": true, "requires": { "to-regex-range": "^5.0.1" } @@ -7174,6 +11697,24 @@ "is-callable": "^1.1.3" } }, + "foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + } + } + }, "form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -7191,6 +11732,13 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, "function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -7214,6 +11762,12 @@ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -7231,6 +11785,18 @@ "hasown": "^2.0.0" } }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, "get-symbol-description": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", @@ -7317,7 +11883,6 @@ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, - "peer": true, "requires": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -7347,6 +11912,19 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, "has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -7405,11 +11983,23 @@ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "http2-client": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==" }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, "ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -7426,6 +12016,16 @@ "resolve-from": "^4.0.0" } }, + "import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -7559,6 +12159,12 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, "is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -7578,8 +12184,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "peer": true + "dev": true }, "is-number-object": { "version": "1.0.7", @@ -7615,6 +12220,12 @@ "call-bind": "^1.0.7" } }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, "is-string": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", @@ -7671,6 +12282,562 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "requires": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + } + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + } + }, + "istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", + "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", + "dev": true, + "requires": { + "@jest/core": "30.2.0", + "@jest/types": "30.2.0", + "import-local": "^3.2.0", + "jest-cli": "30.2.0" + } + }, + "jest-changed-files": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", + "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", + "dev": true, + "requires": { + "execa": "^5.1.1", + "jest-util": "30.2.0", + "p-limit": "^3.1.0" + } + }, + "jest-circus": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", + "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", + "dev": true, + "requires": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "p-limit": "^3.1.0", + "pretty-format": "30.2.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + } + }, + "jest-cli": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", + "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", + "dev": true, + "requires": { + "@jest/core": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "yargs": "^17.7.2" + } + }, + "jest-config": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", + "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", + "dev": true, + "requires": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + } + } + }, + "jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "requires": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + } + }, + "jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "requires": { + "detect-newline": "^3.1.0" + } + }, + "jest-each": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", + "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", + "dev": true, + "requires": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "jest-util": "30.2.0", + "pretty-format": "30.2.0" + } + }, + "jest-environment-node": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", + "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", + "dev": true, + "requires": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0" + } + }, + "jest-haste-map": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", + "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", + "dev": true, + "requires": { + "@jest/types": "30.2.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "fsevents": "^2.3.3", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "dependencies": { + "jest-worker": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "dev": true, + "requires": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-leak-detector": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", + "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", + "dev": true, + "requires": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.2.0" + } + }, + "jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "dev": true, + "requires": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + } + }, + "jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "requires": { + "@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-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "requires": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "requires": {} + }, + "jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true + }, + "jest-resolve": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", + "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", + "dev": true, + "requires": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + } + }, + "jest-resolve-dependencies": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", + "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", + "dev": true, + "requires": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.2.0" + } + }, + "jest-runner": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", + "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", + "dev": true, + "requires": { + "@jest/console": "30.2.0", + "@jest/environment": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-leak-detector": "30.2.0", + "jest-message-util": "30.2.0", + "jest-resolve": "30.2.0", + "jest-runtime": "30.2.0", + "jest-util": "30.2.0", + "jest-watcher": "30.2.0", + "jest-worker": "30.2.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "dependencies": { + "jest-worker": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "dev": true, + "requires": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + } + }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-runtime": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", + "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", + "dev": true, + "requires": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/globals": "30.2.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "dependencies": { + "glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + } + } + }, + "jest-snapshot": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", + "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", + "dev": true, + "requires": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "pretty-format": "30.2.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + } + }, + "jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "requires": { + "@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-validate": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", + "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", + "dev": true, + "requires": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.2.0" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + } + } + }, + "jest-watcher": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", + "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", + "dev": true, + "requires": { + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.2.0", + "string-length": "^4.0.2" + } + }, "jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -7768,6 +12935,12 @@ "json-buffer": "3.0.1" } }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -7805,12 +12978,51 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "requires": { + "tmpl": "1.0.5" + } + }, "memory-fs": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz", @@ -7821,25 +13033,30 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "peer": true + "dev": true }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "peer": true + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true }, "micromatch": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.6.tgz", - "integrity": "sha512-Y4Ypn3oujJYxJcMacVgcs92wofTHxp9FzfDpQON4msDefoC0lb3ETvQLOdLcbhSwU1bz8HrL/1sygfBIHudrkQ==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, - "peer": true, "requires": { "braces": "^3.0.3", - "picomatch": "^4.0.2" + "picomatch": "^2.3.1" + }, + "dependencies": { + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + } } }, "mime-db": { @@ -7857,6 +13074,12 @@ "mime-db": "1.52.0" } }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, "min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -7864,11 +13087,10 @@ "dev": true }, "minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, - "peer": true, "requires": { "brace-expansion": "^2.0.1" } @@ -7878,6 +13100,12 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true + }, "moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -7897,6 +13125,12 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -7907,8 +13141,7 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "peer": true + "dev": true }, "node-fetch": { "version": "2.6.7", @@ -7926,6 +13159,12 @@ "http2-client": "^1.2.5" } }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, "node-readfiles": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", @@ -7935,11 +13174,10 @@ } }, "node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true, - "peer": true + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true }, "normalize-package-data": { "version": "2.5.0", @@ -7972,6 +13210,21 @@ } } }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, "oas-kit-common": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", @@ -8111,6 +13364,15 @@ "wrappy": "1" } }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, "openapi-types": { "version": "12.1.3", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", @@ -8155,6 +13417,12 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8200,12 +13468,29 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + } + } + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "peer": true + "dev": true }, "phone": { "version": "2.4.22", @@ -8213,17 +13498,70 @@ "integrity": "sha512-k2f9qkIgcgbbeyFFMHDcCaYdPxq7u71EjmMvD998PEquwDvIT7zmUFe00S4hH9WPjk+IQlw9W/FlHOu1O17Tbw==" }, "picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "picomatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true + }, + "pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, - "peer": true + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + } + } }, "pluralize": { "version": "8.0.0", @@ -8244,9 +13582,9 @@ "dev": true }, "prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true }, "prettier-linter-helpers": { @@ -8258,11 +13596,36 @@ "fast-diff": "^1.1.2" } }, + "pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "requires": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, "punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" }, + "pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true + }, "qs": { "version": "6.11.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", @@ -8293,6 +13656,12 @@ "safe-buffer": "^5.1.0" } }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, "read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -8430,6 +13799,23 @@ "supports-preserve-symlinks-flag": "^1.0.0" } }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -8503,9 +13889,9 @@ } }, "semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true }, "serialize-javascript": { @@ -8617,19 +14003,23 @@ "object-inspect": "^1.13.1" } }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "peer": true + "dev": true }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "peer": true + "dev": true }, "source-map-support": { "version": "0.5.21", @@ -8679,6 +14069,33 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } + } + }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -8689,6 +14106,17 @@ "strip-ansi": "^6.0.1" } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, "string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -8731,12 +14159,27 @@ "ansi-regex": "^5.0.1" } }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, "strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -8753,11 +14196,10 @@ "dev": true }, "stripe": { - "version": "14.5.0", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-14.5.0.tgz", - "integrity": "sha512-MTt0P0VYDAj2VZyZMW41AxcXVs9s06EDFIHYat4UUkluJgnBHo4T4E2byPhnxoJCPvdzWBwBLnhWbSOg//VTpA==", + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-19.2.1.tgz", + "integrity": "sha512-eRc2T413316D7fAjSSEgPbJJ+4a8KY9rOTOb27aXd7bkw9ADO/3OxmIk7YWDhWvHgvxnEZ/29YjcmBBOu4mhrw==", "requires": { - "@types/node": ">=8.1.0", "qs": "^6.11.0" } }, @@ -8794,6 +14236,15 @@ "yargs": "^17.0.1" } }, + "synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "requires": { + "@pkgr/core": "^0.2.9" + } + }, "tapable": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz", @@ -8827,18 +14278,55 @@ "terser": "^5.26.0" } }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "peer": true, "requires": { "is-number": "^7.0.0" } @@ -8861,9 +14349,39 @@ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, - "peer": true, "requires": {} }, + "ts-jest": { + "version": "29.4.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz", + "integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==", + "dev": true, + "requires": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "dependencies": { + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true + } + } + }, "tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -8876,6 +14394,13 @@ "strip-bom": "^3.0.0" } }, + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "optional": true + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -8885,6 +14410,12 @@ "prelude-ls": "^1.2.1" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -8948,6 +14479,13 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==" }, + "uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "optional": true + }, "unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -8963,17 +14501,45 @@ "undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "devOptional": true + }, + "unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "requires": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1", + "napi-postinstall": "^0.3.0" + } }, "update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", "dev": true, - "peer": true, "requires": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" } }, "uri-js": { @@ -8984,6 +14550,17 @@ "punycode": "^2.1.0" } }, + "v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + } + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -8994,6 +14571,15 @@ "spdx-expression-parse": "^3.0.0" } }, + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "requires": { + "makeerror": "1.0.12" + } + }, "watchpack": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", @@ -9138,6 +14724,12 @@ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -9148,17 +14740,52 @@ "strip-ansi": "^6.0.0" } }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + } + } + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, "yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", diff --git a/stripe_collection_module/package.json b/stripe_collection_module/package.json index e832303..2d1030b 100644 --- a/stripe_collection_module/package.json +++ b/stripe_collection_module/package.json @@ -5,28 +5,50 @@ "moment-timezone": "0.5.40", "node-fetch": "2.6.7", "phone": "2.4.22", - "stripe": "14.5.0" + "stripe": "^19.2.1" }, "devDependencies": { + "@jest/globals": "^30.2.0", + "@types/jest": "^30.0.0", "@types/node": "^20.11.20", "@types/node-fetch": "^2.6.11", - "eslint": "^8.51.0", - "eslint-config-airbnb-typescript": "^17.0.0", - "eslint-config-prettier": "^8.7.0", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "eslint": "^8.57.0", + "eslint-config-airbnb-typescript": "^18.0.0", + "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-webpack": "^0.13.2", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-mocha": "^10.1.0", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-sonarjs": "^0.18.0", - "eslint-plugin-unicorn": "^48.0.1", - "prettier": "^2.8.3" + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-mocha": "^10.5.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-sonarjs": "^0.25.1", + "eslint-plugin-unicorn": "^55.0.0", + "jest": "^30.2.0", + "prettier": "^3.3.3", + "ts-jest": "^29.4.5" }, "scripts": { - "lint": "eslint code --ext .js,.jsx,.ts,.tsx --no-error-on-unmatched-pattern", - "lint:fix": "eslint --fix code --ext .js,.jsx,.ts,.tsx --no-error-on-unmatched-pattern --quiet", + "validate": "bash scripts/validate-config.sh", + "lint": "eslint code --ext .js,.jsx,.ts,.tsx", + "lint:fix": "eslint --fix code --ext .js,.jsx,.ts,.tsx", "prettier-check": "npx prettier --check code/", "prettier-write": "prettier --write code/", - "build": "rm -rf ./dist && tsc" + "format": "npm run prettier-write && npm run lint:fix", + "build": "rm -rf ./dist && tsc --project tsconfig.build.json", + "build:watch": "tsc --project tsconfig.build.json --watch", + "clean": "rm -rf ./dist ./coverage", + "test": "jest", + "test:unit": "jest --testPathPattern='__tests__/(services|core|utils)'", + "test:integration": "jest --testPathPattern='__tests__/integration'", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "test:ci": "jest --ci --coverage --maxWorkers=2", + "precommit": "npm run lint && npm run test", + "prepush": "npm run lint && npm run test && npm run build", + "predeploy": "npm run validate && npm run test && npm run build", + "deploy:sandbox": "bash scripts/deploy.sh sandbox", + "deploy:production": "bash scripts/deploy.sh production", + "deploy:dry-run": "bash scripts/deploy.sh --dry-run" }, "engines": { "npm": ">=8.0.0", diff --git a/stripe_collection_module/scripts/README.md b/stripe_collection_module/scripts/README.md new file mode 100644 index 0000000..511e1d0 --- /dev/null +++ b/stripe_collection_module/scripts/README.md @@ -0,0 +1,310 @@ +# Deployment Scripts + +This directory contains scripts for deploying the Stripe Collection Module to the Root Platform. + +## Quick Start + +### Using npm scripts (recommended) + +```bash +# Deploy to sandbox +npm run deploy:sandbox + +# Deploy to production (requires version tag) +npm run deploy:production v1.0.0 + +# Dry run (show what would happen without executing) +npm run deploy:dry-run production v1.0.0 +``` + +### Using the deploy script directly + +```bash +# Deploy to sandbox +./scripts/deploy.sh sandbox + +# Deploy to sandbox with version tag +./scripts/deploy.sh sandbox v1.0.0 + +# Deploy to production +./scripts/deploy.sh production v1.0.0 + +# Dry run +./scripts/deploy.sh --dry-run production v1.0.0 +``` + +## Prerequisites + +Set these environment variables or pass them as flags: + +```bash +export ROOT_API_KEY="your_api_key_here" +export ROOT_ORG_ID="your_org_id_here" +export ROOT_HOST="https://api.rootplatform.com" # Optional +export CM_KEY="cm_stripe" # Optional +``` + +Or use command-line flags: + +```bash +./scripts/deploy.sh \ + -k "your_api_key" \ + -o "your_org_id" \ + -h "https://api.rootplatform.com" \ + -c "cm_stripe" \ + production v1.0.0 +``` + +## What the Deployment Script Does + +1. **Validates Configuration** - Runs `npm run validate` +2. **Runs Tests** - Executes `npm test` (skip with `--skip-tests`) +3. **Lints Code** - Runs `npm run lint` +4. **Builds** - Compiles TypeScript (skip with `--skip-build`) +5. **Creates Git Tag** - Tags the release (skip with `--skip-tag`) +6. **Publishes to Root Platform** - Calls the Root Platform API + +## Options + +``` +Usage: deploy.sh [OPTIONS] [version] + +Arguments: + environment Target environment: 'sandbox' or 'production' + version Git tag version (e.g., v1.0.0) - optional for sandbox + +Options: + -k, --api-key KEY Root Platform API key + -o, --org-id ID Root organization ID + -h, --host URL Root Platform host + -c, --cm-key KEY Collection module key + --skip-tests Skip running tests + --skip-build Skip build step + --skip-tag Skip creating git tag + --dry-run Show what would be done without executing + --help Show help message +``` + +## Examples + +### Deploy to Sandbox + +```bash +# Simple deployment to sandbox +./scripts/deploy.sh sandbox + +# With version tag +./scripts/deploy.sh sandbox v1.0.0 + +# Skip tests (faster, for quick iterations) +./scripts/deploy.sh --skip-tests sandbox +``` + +### Deploy to Production + +```bash +# Standard production deployment +./scripts/deploy.sh production v1.1.0 + +# With explicit credentials +./scripts/deploy.sh \ + -k "prod_api_key" \ + -o "org_id_123" \ + production v1.1.0 + +# Dry run first (recommended) +./scripts/deploy.sh --dry-run production v1.1.0 +# Review output, then run for real +./scripts/deploy.sh production v1.1.0 +``` + +### Testing and Development + +```bash +# Dry run - see what would happen +./scripts/deploy.sh --dry-run sandbox v1.0.0-beta + +# Skip tests and build for faster iteration +./scripts/deploy.sh --skip-tests --skip-build sandbox + +# Skip git tagging (if managing tags manually) +./scripts/deploy.sh --skip-tag production v1.2.0 +``` + +## Workflow + +### Standard Development Workflow + +1. **Make changes** to your collection module +2. **Test locally** with `npm test` +3. **Deploy to sandbox** for integration testing + ```bash + npm run deploy:sandbox + ``` +4. **Test in sandbox** - verify everything works +5. **Create version tag** and **deploy to production** + ```bash + ./scripts/deploy.sh production v1.0.0 + ``` +6. **Monitor** the deployment in Root Platform dashboard + +### Hotfix Workflow + +1. **Create hotfix branch** + ```bash + git checkout -b hotfix/fix-payment-issue + ``` +2. **Make fix** and test +3. **Deploy directly to sandbox** + ```bash + ./scripts/deploy.sh --skip-tag sandbox + ``` +4. **Verify fix** in sandbox +5. **Merge and deploy to production** + ```bash + git checkout main + git merge hotfix/fix-payment-issue + ./scripts/deploy.sh production v1.0.1 + ``` + +## Environment Variables + +### Required + +- `ROOT_API_KEY` - Your Root Platform API key +- `ROOT_ORG_ID` - Your Root organization ID + +### Optional + +- `ROOT_HOST` - Root Platform API URL (default: https://api.rootplatform.com) +- `CM_KEY` - Collection module key (default: cm_stripe) + +### Setting Up Environment Variables + +Create a `.env` file (gitignored) in the project root: + +```bash +# .env +ROOT_API_KEY=sandbox_sk_abc123... +ROOT_ORG_ID=00000000-0000-0000-0000-000000000001 +ROOT_HOST=https://api.rootplatform.com +CM_KEY=cm_stripe +``` + +Then source it before deploying: + +```bash +source .env +./scripts/deploy.sh sandbox +``` + +Or use a tool like `direnv` for automatic loading. + +## Troubleshooting + +### "ROOT_API_KEY is not set" + +**Solution:** Set the environment variable or use the `-k` flag: +```bash +export ROOT_API_KEY="your_key" +# or +./scripts/deploy.sh -k "your_key" sandbox +``` + +### "Tests failed" + +**Solution:** Fix the failing tests or skip them (not recommended for production): +```bash +./scripts/deploy.sh --skip-tests sandbox +``` + +### "Configuration validation failed" + +**Solution:** Check your `code/env.ts` file has all required values: +```bash +npm run validate +``` + +### "Not a git repository" + +**Solution:** Initialize git or use `--skip-tag`: +```bash +git init +# or +./scripts/deploy.sh --skip-tag sandbox +``` + +### API Call Failed + +**Solution:** +- Verify API key has deployment permissions +- Check organization ID is correct +- Verify collection module key matches Root Platform +- Check Root Platform status page + +## Security Notes + +- **Never commit API keys** to version control +- Use different API keys for sandbox and production +- Store production keys securely (1Password, Vault, etc.) +- Rotate API keys quarterly +- Use read-only keys for monitoring, write keys only for deployment + +## CI/CD Integration + +### GitHub Actions Example + +```yaml +name: Deploy +on: + push: + tags: + - 'v*' + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: '18' + + - name: Install dependencies + working-directory: ./stripe_collection_module + run: npm ci + + - name: Deploy to Sandbox + if: contains(github.ref, '-beta') + working-directory: ./stripe_collection_module + env: + ROOT_API_KEY: ${{ secrets.ROOT_SANDBOX_API_KEY }} + ROOT_ORG_ID: ${{ secrets.ROOT_ORG_ID }} + run: npm run deploy:sandbox + + - name: Deploy to Production + if: "!contains(github.ref, '-beta')" + working-directory: ./stripe_collection_module + env: + ROOT_API_KEY: ${{ secrets.ROOT_PRODUCTION_API_KEY }} + ROOT_ORG_ID: ${{ secrets.ROOT_ORG_ID }} + run: ./scripts/deploy.sh production ${{ github.ref_name }} +``` + +## Related Documentation + +- [Deployment Guide](../docs/DEPLOYMENT.md) - Detailed deployment documentation +- [Setup Guide](../docs/SETUP.md) - Initial setup instructions +- [Testing Guide](../docs/TESTING.md) - Testing strategies + +## Support + +For issues with deployment: +1. Check the [Troubleshooting](#troubleshooting) section above +2. Review logs with `--dry-run` flag +3. Verify credentials and configuration +4. Check Root Platform status page +5. Contact Root Platform support + diff --git a/stripe_collection_module/scripts/deploy.sh b/stripe_collection_module/scripts/deploy.sh new file mode 100755 index 0000000..ad64cce --- /dev/null +++ b/stripe_collection_module/scripts/deploy.sh @@ -0,0 +1,475 @@ +#!/bin/bash + +############################################################################### +# Stripe Collection Module Deployment Script +# +# This script automates the deployment process to the Root Platform. +# +# Usage: +# ./scripts/deploy.sh [environment] [version] +# +# Examples: +# ./scripts/deploy.sh sandbox # Deploy to sandbox (no version tag) +# ./scripts/deploy.sh sandbox v1.0.0 # Deploy to sandbox with tag +# ./scripts/deploy.sh production v1.0.0 # Deploy to production with tag +# +# Prerequisites: +# - ROOT_API_KEY environment variable (or use -k flag) +# - ROOT_ORG_ID environment variable (or use -o flag) +# - ROOT_HOST environment variable (or use -h flag) +# - CM_KEY environment variable (or use -c flag) +############################################################################### + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +# Default values +ENVIRONMENT="" +VERSION="" +SKIP_TESTS=false +SKIP_BUILD=false +SKIP_TAG=false +DRY_RUN=false + +# Root Platform configuration +ROOT_API_KEY="${ROOT_API_KEY:-}" +ROOT_ORG_ID="${ROOT_ORG_ID:-}" +ROOT_HOST="${ROOT_HOST:-https://api.rootplatform.com}" +CM_KEY="${CM_KEY:-cm_stripe}" + +############################################################################### +# Helper Functions +############################################################################### + +print_header() { + echo "" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${BLUE} $1${NC}" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo "" +} + +print_success() { + echo -e "${GREEN}✓${NC} $1" +} + +print_error() { + echo -e "${RED}✗${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}⚠${NC} $1" +} + +print_info() { + echo -e "${BLUE}ℹ${NC} $1" +} + +print_usage() { + cat << EOF +Usage: $0 [OPTIONS] [version] + +Deployment script for Stripe Collection Module to Root Platform. + +Arguments: + environment Target environment: 'sandbox' or 'production' + version Git tag version (e.g., v1.0.0) - optional for sandbox + +Options: + -k, --api-key KEY Root Platform API key (or set ROOT_API_KEY env var) + -o, --org-id ID Root organization ID (or set ROOT_ORG_ID env var) + -h, --host URL Root Platform host (default: https://api.rootplatform.com) + -c, --cm-key KEY Collection module key (default: cm_stripe) + --skip-tests Skip running tests + --skip-build Skip build step + --skip-tag Skip creating git tag + --dry-run Show what would be done without executing + --help Show this help message + +Examples: + # Deploy to sandbox + $0 sandbox + + # Deploy to sandbox with version tag + $0 sandbox v1.0.0 + + # Deploy to production with explicit credentials + $0 -k "prod_key_123" -o "org_id" production v1.1.0 + + # Dry run for production + $0 --dry-run production v1.0.0 + +Environment Variables: + ROOT_API_KEY Root Platform API key + ROOT_ORG_ID Root organization ID + ROOT_HOST Root Platform API URL + CM_KEY Collection module key + +EOF +} + +############################################################################### +# Parse Arguments +############################################################################### + +while [[ $# -gt 0 ]]; do + case $1 in + -k|--api-key) + ROOT_API_KEY="$2" + shift 2 + ;; + -o|--org-id) + ROOT_ORG_ID="$2" + shift 2 + ;; + -h|--host) + ROOT_HOST="$2" + shift 2 + ;; + -c|--cm-key) + CM_KEY="$2" + shift 2 + ;; + --skip-tests) + SKIP_TESTS=true + shift + ;; + --skip-build) + SKIP_BUILD=true + shift + ;; + --skip-tag) + SKIP_TAG=true + shift + ;; + --dry-run) + DRY_RUN=true + shift + ;; + --help) + print_usage + exit 0 + ;; + sandbox|production) + ENVIRONMENT="$1" + shift + ;; + v*.*.*) + VERSION="$1" + shift + ;; + *) + print_error "Unknown option: $1" + print_usage + exit 1 + ;; + esac +done + +############################################################################### +# Validate Arguments +############################################################################### + +if [ -z "$ENVIRONMENT" ]; then + print_error "Environment argument is required (sandbox or production)" + print_usage + exit 1 +fi + +if [ "$ENVIRONMENT" != "sandbox" ] && [ "$ENVIRONMENT" != "production" ]; then + print_error "Invalid environment: $ENVIRONMENT (must be 'sandbox' or 'production')" + exit 1 +fi + +if [ "$ENVIRONMENT" = "production" ] && [ -z "$VERSION" ]; then + print_error "Version is required for production deployment" + print_info "Usage: $0 production v1.0.0" + exit 1 +fi + +if [ -z "$ROOT_API_KEY" ]; then + print_error "ROOT_API_KEY is not set" + print_info "Set via environment variable or use -k flag" + exit 1 +fi + +if [ -z "$ROOT_ORG_ID" ]; then + print_error "ROOT_ORG_ID is not set" + print_info "Set via environment variable or use -o flag" + exit 1 +fi + +############################################################################### +# Display Configuration +############################################################################### + +print_header "Deployment Configuration" + +echo "Environment: $ENVIRONMENT" +echo "Version: ${VERSION:-}" +echo "Root Host: $ROOT_HOST" +echo "Organization ID: $ROOT_ORG_ID" +echo "Module Key: $CM_KEY" +echo "API Key: ${ROOT_API_KEY:0:10}..." +echo "" +echo "Skip Tests: $SKIP_TESTS" +echo "Skip Build: $SKIP_BUILD" +echo "Skip Tag: $SKIP_TAG" +echo "Dry Run: $DRY_RUN" + +if [ "$DRY_RUN" = true ]; then + print_warning "DRY RUN MODE - No changes will be made" +fi + +############################################################################### +# Confirmation +############################################################################### + +if [ "$ENVIRONMENT" = "production" ] && [ "$DRY_RUN" = false ]; then + echo "" + print_warning "You are about to deploy to PRODUCTION!" + read -p "Are you sure you want to continue? (yes/no): " -r + echo "" + if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then + print_info "Deployment cancelled" + exit 0 + fi +fi + +############################################################################### +# Pre-deployment Checks +############################################################################### + +print_header "Pre-deployment Checks" + +# Check if we're in the right directory +cd "$PROJECT_DIR" +print_success "Changed to project directory: $PROJECT_DIR" + +# Check if node_modules exists +if [ ! -d "node_modules" ]; then + print_error "node_modules not found. Run 'npm install' first." + exit 1 +fi +print_success "Dependencies installed" + +# Check if git repository (if version tagging is needed) +if [ -n "$VERSION" ] && [ "$SKIP_TAG" = false ]; then + if ! git rev-parse --git-dir > /dev/null 2>&1; then + print_error "Not a git repository. Cannot create version tag." + exit 1 + fi + print_success "Git repository detected" + + # Check for uncommitted changes + if ! git diff-index --quiet HEAD --; then + print_warning "You have uncommitted changes" + read -p "Continue anyway? (yes/no): " -r + if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then + exit 0 + fi + fi +fi + +############################################################################### +# Validation +############################################################################### + +print_header "Step 1: Validate Configuration" + +if [ "$DRY_RUN" = false ]; then + if npm run validate; then + print_success "Configuration validated" + else + print_error "Configuration validation failed" + exit 1 + fi +else + print_info "Would run: npm run validate" +fi + +############################################################################### +# Run Tests +############################################################################### + +if [ "$SKIP_TESTS" = false ]; then + print_header "Step 2: Run Tests" + + if [ "$DRY_RUN" = false ]; then + if npm test; then + print_success "All tests passed" + else + print_error "Tests failed" + exit 1 + fi + else + print_info "Would run: npm test" + fi +else + print_header "Step 2: Run Tests" + print_warning "Skipping tests (--skip-tests flag)" +fi + +############################################################################### +# Lint Code +############################################################################### + +print_header "Step 3: Lint Code" + +if [ "$DRY_RUN" = false ]; then + if npm run lint; then + print_success "Code linting passed" + else + print_error "Linting failed" + print_info "Run 'npm run lint:fix' to auto-fix issues" + exit 1 + fi +else + print_info "Would run: npm run lint" +fi + +############################################################################### +# Build +############################################################################### + +if [ "$SKIP_BUILD" = false ]; then + print_header "Step 4: Build" + + if [ "$DRY_RUN" = false ]; then + if npm run build; then + print_success "Build completed successfully" + else + print_error "Build failed" + exit 1 + fi + else + print_info "Would run: npm run build" + fi +else + print_header "Step 4: Build" + print_warning "Skipping build (--skip-build flag)" +fi + +############################################################################### +# Create Git Tag +############################################################################### + +if [ -n "$VERSION" ] && [ "$SKIP_TAG" = false ]; then + print_header "Step 5: Create Git Tag" + + # Check if tag already exists + if git rev-parse "$VERSION" >/dev/null 2>&1; then + print_warning "Tag $VERSION already exists" + read -p "Continue with existing tag? (yes/no): " -r + if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then + exit 0 + fi + else + if [ "$DRY_RUN" = false ]; then + git tag -a "$VERSION" -m "Release $VERSION" + print_success "Created tag: $VERSION" + + read -p "Push tag to remote? (yes/no): " -r + if [[ $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then + git push origin "$VERSION" + print_success "Pushed tag to remote" + fi + else + print_info "Would create tag: $VERSION" + print_info "Would push tag to remote (if confirmed)" + fi + fi +elif [ -n "$VERSION" ]; then + print_header "Step 5: Create Git Tag" + print_warning "Skipping git tag (--skip-tag flag)" +fi + +############################################################################### +# Publish to Root Platform +############################################################################### + +print_header "Step 6: Publish to Root Platform" + +# Determine if sandbox or production +if [ "$ENVIRONMENT" = "sandbox" ]; then + BUMP_SANDBOX="true" +else + BUMP_SANDBOX="false" +fi + +# Build the API URL +API_URL="${ROOT_HOST}/v1/apps/${ROOT_ORG_ID}/insurance/collection-modules/${CM_KEY}/publish?bumpSandbox=${BUMP_SANDBOX}" + +print_info "Publishing to: $ENVIRONMENT" +print_info "API URL: $API_URL" + +if [ "$DRY_RUN" = false ]; then + # Make the API call + RESPONSE=$(curl -X POST \ + -H "Authorization: Basic ${ROOT_API_KEY}" \ + -w "\nHTTP_STATUS:%{http_code}" \ + -s \ + "$API_URL") + + # Extract HTTP status code + HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS:" | cut -d: -f2) + RESPONSE_BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS:/d') + + # Check response + if [ "$HTTP_STATUS" -ge 200 ] && [ "$HTTP_STATUS" -lt 300 ]; then + print_success "Successfully published to $ENVIRONMENT!" + echo "" + echo "Response:" + echo "$RESPONSE_BODY" | jq . 2>/dev/null || echo "$RESPONSE_BODY" + else + print_error "Deployment failed with status: $HTTP_STATUS" + echo "" + echo "Response:" + echo "$RESPONSE_BODY" + exit 1 + fi +else + print_info "Would execute: curl -X POST -H 'Authorization: Basic ***' '$API_URL'" +fi + +############################################################################### +# Post-deployment Steps +############################################################################### + +print_header "Post-deployment" + +echo "" +print_success "Deployment to $ENVIRONMENT completed successfully!" +echo "" + +if [ "$DRY_RUN" = false ]; then + print_info "Next steps:" + echo " 1. Verify deployment in Root Platform dashboard" + echo " 2. Check logs for any errors" + echo " 3. Test critical workflows" + echo " 4. Monitor error rates and performance" + + if [ "$ENVIRONMENT" = "sandbox" ]; then + echo "" + print_info "After testing in sandbox, deploy to production with:" + echo " ./scripts/deploy.sh production $VERSION" + fi +else + print_info "This was a dry run. No changes were made." + print_info "Remove --dry-run flag to execute deployment." +fi + +echo "" +print_success "Done! 🚀" +echo "" + diff --git a/stripe_collection_module/scripts/validate-config.sh b/stripe_collection_module/scripts/validate-config.sh new file mode 100755 index 0000000..dd903c0 --- /dev/null +++ b/stripe_collection_module/scripts/validate-config.sh @@ -0,0 +1,157 @@ +#!/bin/bash + +# Configuration Validation Script +# Checks that all required environment variables are set + +set -e + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +info() { echo -e "${BLUE}ℹ${NC} $1"; } +success() { echo -e "${GREEN}✓${NC} $1"; } +warning() { echo -e "${YELLOW}⚠${NC} $1"; } +error() { echo -e "${RED}✗${NC} $1"; } + +echo "" +echo "╔═══════════════════════════════════════════════════════════╗" +echo "║ Configuration Validation ║" +echo "╚═══════════════════════════════════════════════════════════╝" +echo "" + +ERRORS=0 +WARNINGS=0 + +# Check if env.ts exists +info "Checking environment configuration..." + +if [ ! -f "code/env.ts" ]; then + error "code/env.ts not found!" + echo " Copy code/env.sample.ts to code/env.ts and fill in your values." + ERRORS=$((ERRORS + 1)) +else + success "code/env.ts found" +fi + +# Check if .root-config.json exists +if [ ! -f ".root-config.json" ]; then + error ".root-config.json not found!" + echo " Copy .root-config.sample.json to .root-config.json and configure." + ERRORS=$((ERRORS + 1)) +else + success ".root-config.json found" + + # Validate .root-config.json content + if grep -q "my_collection_module_cm_stripe" .root-config.json; then + warning ".root-config.json contains template placeholder values" + echo " Update collectionModuleKey with your actual module key." + WARNINGS=$((WARNINGS + 1)) + fi + + if grep -q "00000000-0000-0000-0000-000000000001" .root-config.json; then + warning ".root-config.json contains placeholder organization ID" + echo " Update organizationId with your actual Root organization ID." + WARNINGS=$((WARNINGS + 1)) + fi +fi + +# Check for placeholder values in env.ts +if [ -f "code/env.ts" ]; then + info "Checking for placeholder values..." + + PLACEHOLDERS=( + "xxxxx" + "my_collection_module_cm_stripe" + ) + + for PLACEHOLDER in "${PLACEHOLDERS[@]}"; do + if grep -q "$PLACEHOLDER" code/env.ts; then + warning "Found placeholder value in code/env.ts: $PLACEHOLDER" + echo " Replace with actual API keys and configuration values." + WARNINGS=$((WARNINGS + 1)) + fi + done +fi + +# Check TypeScript compilation +info "Checking TypeScript compilation..." +if npm run build > /dev/null 2>&1; then + success "TypeScript compilation successful" +else + error "TypeScript compilation failed" + echo " Run 'npm run build' to see detailed errors." + ERRORS=$((ERRORS + 1)) +fi + +# Check linting +info "Checking linting..." +if npm run lint > /dev/null 2>&1; then + success "Linting passed" +else + warning "Linting failed" + echo " Run 'npm run lint' to see detailed errors." + echo " Run 'npm run lint:fix' to auto-fix some issues." + WARNINGS=$((WARNINGS + 1)) +fi + +# Run tests +info "Running tests..." +if npm test > /dev/null 2>&1; then + success "All tests passed" +else + error "Tests failed" + echo " Run 'npm test' to see detailed errors." + ERRORS=$((ERRORS + 1)) +fi + +# Check Node version +info "Checking Node version..." +if [ -f ".nvmrc" ]; then + REQUIRED_VERSION=$(cat .nvmrc) + CURRENT_VERSION=$(node -v | sed 's/v//') + + if [[ "$CURRENT_VERSION" == "$REQUIRED_VERSION"* ]]; then + success "Node version $CURRENT_VERSION matches requirement" + else + warning "Node version mismatch" + echo " Required: $REQUIRED_VERSION" + echo " Current: $CURRENT_VERSION" + echo " Run 'nvm use' to switch to the correct version." + WARNINGS=$((WARNINGS + 1)) + fi +fi + +# Summary +echo "" +echo "─────────────────────────────────────────────────────────────" +echo " Validation Summary" +echo "─────────────────────────────────────────────────────────────" +echo "" + +if [ $ERRORS -eq 0 ] && [ $WARNINGS -eq 0 ]; then + success "Configuration is valid! No errors or warnings." + echo "" + echo "You're ready to:" + echo " - Run locally for testing" + echo " - Deploy to AWS Lambda" + echo " - Register webhooks with your provider" + echo "" + exit 0 +elif [ $ERRORS -eq 0 ]; then + warning "Configuration is mostly valid with $WARNINGS warning(s)." + echo "" + echo "Review warnings above and fix before deploying to production." + echo "" + exit 0 +else + error "Configuration validation failed with $ERRORS error(s) and $WARNINGS warning(s)." + echo "" + echo "Fix the errors above before deploying." + echo "" + exit 1 +fi + diff --git a/stripe_collection_module/tsconfig.build.json b/stripe_collection_module/tsconfig.build.json index 8c174d9..951ff9f 100644 --- a/stripe_collection_module/tsconfig.build.json +++ b/stripe_collection_module/tsconfig.build.json @@ -1,5 +1,7 @@ { "extends": "./tsconfig.json", + "include": ["./code/**/*"], + "exclude": ["./__tests__/**/*"], "compilerOptions": { "skipLibCheck": false } diff --git a/stripe_collection_module/tsconfig.eslint.json b/stripe_collection_module/tsconfig.eslint.json index ab3c206..f795117 100644 --- a/stripe_collection_module/tsconfig.eslint.json +++ b/stripe_collection_module/tsconfig.eslint.json @@ -1,4 +1,7 @@ { "extends": "./tsconfig.json", - "include": ["**/*.ts", "**/*.js"] + "include": ["**/*.ts", "**/*.js", "__tests__/**/*"], + "compilerOptions": { + "types": ["jest"] + } } diff --git a/stripe_collection_module/tsconfig.json b/stripe_collection_module/tsconfig.json index d0760a6..fce63d0 100644 --- a/stripe_collection_module/tsconfig.json +++ b/stripe_collection_module/tsconfig.json @@ -1,16 +1,21 @@ { "include": [ - "./code/**/*" + "./code/**/*", + "./__tests__/**/*" ], "compilerOptions": { "lib": [ - "es2022" + "es2022", + "dom" ], "module": "es2022", + "target": "es2022", "skipLibCheck": true, "allowJs": true, "strict": true, "moduleResolution": "Bundler", - "noEmit": true + "downlevelIteration": true, + "noEmit": true, + "types": ["jest"] } } From ba32288e85d1a2642edf05fa107da950d615344f Mon Sep 17 00:00:00 2001 From: Marcel Smuts Date: Sat, 8 Nov 2025 13:28:47 +0200 Subject: [PATCH 03/16] =?UTF-8?q?=F0=9F=93=9D=20docs:=20removed=20some=20u?= =?UTF-8?q?nnecessary=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- stripe_collection_module/docs/ARCHITECTURE.md | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/stripe_collection_module/docs/ARCHITECTURE.md b/stripe_collection_module/docs/ARCHITECTURE.md index 8c46500..011b04e 100644 --- a/stripe_collection_module/docs/ARCHITECTURE.md +++ b/stripe_collection_module/docs/ARCHITECTURE.md @@ -116,19 +116,6 @@ The `RenderService` centralizes all HTML generation: ## Data Flow -### Payment Method Assignment - -``` -Root Platform - ↓ (afterPolicyPaymentMethodAssigned event) -Lifecycle Hook - ↓ -Implementation TODO - ├→ Create Stripe customer - ├→ Attach payment method - └→ Update Root policy with Stripe IDs -``` - ### Webhook Processing ``` @@ -148,22 +135,22 @@ Event-Specific Controller ### LogService - **Purpose**: Structured JSON logging to stdout -- **Key Methods**: `debug()`, `info()`, `warn()`, `error()` - **Features**: Correlation IDs, JSON formatting, multiple log levels ### ConfigurationService - **Purpose**: Type-safe, validated configuration management -- **Key Methods**: `get()`, `getAll()`, `isProduction()` - **Features**: Environment-specific configs, validation on startup ### RenderService - **Purpose**: Generate HTML for dashboard views -- **Key Methods**: `renderCreatePaymentMethod()`, `renderViewPaymentMethod()` - **Features**: XSS protection, consistent styling ### RootService -- **Purpose**: High-level Root platform operations -- **Key Methods**: `getPolicy()`, `updatePaymentStatus()`, `createPayment()` +- **Purpose**: Root platform operations +- **Features**: Business logic for Root API interactions + +### StripeService +- **Purpose**: Stripe operations - **Features**: Business logic for Root API interactions ## Extension Points @@ -222,6 +209,3 @@ For questions or clarifications, consult: - [CUSTOMIZING.md](./CUSTOMIZING.md) - Implementation guide - Source code comments and JSDoc - - - From 471b14a8d71c574747ad9677a89260c8d7fb8a28 Mon Sep 17 00:00:00 2001 From: Marcel Smuts Date: Sat, 8 Nov 2025 14:50:11 +0200 Subject: [PATCH 04/16] =?UTF-8?q?=E2=9C=85=20test:=20added=20tests.=20Cove?= =?UTF-8?q?rage=20is=20now=20at=2093%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapters/stripe-to-root-adapter.test.ts | 174 ++++++ .../__tests__/clients/root-client.test.ts | 31 ++ .../__tests__/clients/stripe-client.test.ts | 35 ++ .../invoice-paid.controller.test.ts | 336 ++++++++++++ .../payment-creation.controller.test.ts | 298 +++++++++++ .../__tests__/core/container.setup.test.ts | 104 ++++ .../payment-method.hooks.test.ts | 254 +++++++++ .../lifecycle-hooks/payment.hooks.test.ts | 93 ++++ .../lifecycle-hooks/policy.hooks.test.ts | 216 ++++++++ .../services/config-instance.test.ts | 35 ++ .../__tests__/services/log-instance.test.ts | 37 ++ .../__tests__/services/root.service.test.ts | 32 +- .../__tests__/services/stripe.service.test.ts | 54 +- stripe_collection_module/__tests__/setup.ts | 7 +- .../__tests__/test-helpers.ts | 123 +++++ .../__tests__/utils/error-types.test.ts | 201 +++++++ .../__tests__/utils/error.test.ts | 79 +++ .../__tests__/utils/logger.test.ts | 180 +++++++ .../__tests__/utils/retry.test.ts | 372 +++++++++++++ .../__tests__/utils/timeout.test.ts | 250 +++++++++ .../__tests__/webhook-hooks.test.ts | 498 ++++++++++++++++++ .../code/lifecycle-hooks/index.ts | 363 +------------ .../lifecycle-hooks/payment-method.hooks.ts | 199 +++++++ .../code/lifecycle-hooks/payment.hooks.ts | 55 ++ .../code/lifecycle-hooks/policy.hooks.ts | 124 +++++ .../code/webhook-hooks.ts | 216 ++------ stripe_collection_module/jest.config.js | 6 +- 27 files changed, 3799 insertions(+), 573 deletions(-) create mode 100644 stripe_collection_module/__tests__/adapters/stripe-to-root-adapter.test.ts create mode 100644 stripe_collection_module/__tests__/clients/root-client.test.ts create mode 100644 stripe_collection_module/__tests__/clients/stripe-client.test.ts create mode 100644 stripe_collection_module/__tests__/controllers/invoice-paid.controller.test.ts create mode 100644 stripe_collection_module/__tests__/controllers/payment-creation.controller.test.ts create mode 100644 stripe_collection_module/__tests__/core/container.setup.test.ts create mode 100644 stripe_collection_module/__tests__/lifecycle-hooks/payment-method.hooks.test.ts create mode 100644 stripe_collection_module/__tests__/lifecycle-hooks/payment.hooks.test.ts create mode 100644 stripe_collection_module/__tests__/lifecycle-hooks/policy.hooks.test.ts create mode 100644 stripe_collection_module/__tests__/services/config-instance.test.ts create mode 100644 stripe_collection_module/__tests__/services/log-instance.test.ts create mode 100644 stripe_collection_module/__tests__/test-helpers.ts create mode 100644 stripe_collection_module/__tests__/utils/error-types.test.ts create mode 100644 stripe_collection_module/__tests__/utils/error.test.ts create mode 100644 stripe_collection_module/__tests__/utils/logger.test.ts create mode 100644 stripe_collection_module/__tests__/utils/retry.test.ts create mode 100644 stripe_collection_module/__tests__/utils/timeout.test.ts create mode 100644 stripe_collection_module/__tests__/webhook-hooks.test.ts create mode 100644 stripe_collection_module/code/lifecycle-hooks/payment-method.hooks.ts create mode 100644 stripe_collection_module/code/lifecycle-hooks/payment.hooks.ts create mode 100644 stripe_collection_module/code/lifecycle-hooks/policy.hooks.ts diff --git a/stripe_collection_module/__tests__/adapters/stripe-to-root-adapter.test.ts b/stripe_collection_module/__tests__/adapters/stripe-to-root-adapter.test.ts new file mode 100644 index 0000000..cb99577 --- /dev/null +++ b/stripe_collection_module/__tests__/adapters/stripe-to-root-adapter.test.ts @@ -0,0 +1,174 @@ +/** + * StripeToRootAdapter Tests + */ + +import Stripe from 'stripe'; +import * as root from '@rootplatform/node-sdk'; +import StripeToRootAdapter from '../../code/adapters/stripe-to-root-adapter'; + +describe('StripeToRootAdapter', () => { + let adapter: StripeToRootAdapter; + + beforeEach(() => { + adapter = new StripeToRootAdapter(); + }); + + describe('convertInvoiceToRootPayment', () => { + it('should convert invoice with successful status', () => { + const mockInvoice = { + id: 'inv_123', + amount_paid: 10000, + } as any; + + const result = adapter.convertInvoiceToRootPayment(mockInvoice, { + status: root.PaymentStatus.Successful, + }); + + expect(result).toEqual({ + status: root.PaymentStatus.Successful, + failure_reason: undefined, + failure_action: root.FailureAction.BlockRetry, + }); + }); + + it('should convert invoice with failed status and custom failure reason', () => { + const mockInvoice = { + id: 'inv_123', + } as any; + + const result = adapter.convertInvoiceToRootPayment(mockInvoice, { + status: root.PaymentStatus.Failed, + failureReason: 'Card declined', + failureAction: root.FailureAction.AllowRetry, + }); + + expect(result).toEqual({ + status: root.PaymentStatus.Failed, + failure_reason: 'Card declined', + failure_action: root.FailureAction.AllowRetry, + }); + }); + + it('should use invoice last_finalization_error when no failure reason provided', () => { + const mockInvoice = { + id: 'inv_123', + last_finalization_error: { + message: 'Payment failed due to insufficient funds', + }, + } as any; + + const result = adapter.convertInvoiceToRootPayment(mockInvoice, { + status: root.PaymentStatus.Failed, + }); + + expect(result).toEqual({ + status: root.PaymentStatus.Failed, + failure_reason: 'Payment failed due to insufficient funds', + failure_action: root.FailureAction.BlockRetry, + }); + }); + + it('should handle invoice with no error information', () => { + const mockInvoice = { + id: 'inv_123', + } as any; + + const result = adapter.convertInvoiceToRootPayment(mockInvoice, { + status: root.PaymentStatus.Pending, + }); + + expect(result).toEqual({ + status: root.PaymentStatus.Pending, + failure_reason: undefined, + failure_action: root.FailureAction.BlockRetry, + }); + }); + + it('should prioritize provided failure reason over invoice error', () => { + const mockInvoice = { + id: 'inv_123', + last_finalization_error: { + message: 'Invoice error message', + }, + } as any; + + const result = adapter.convertInvoiceToRootPayment(mockInvoice, { + status: root.PaymentStatus.Failed, + failureReason: 'Custom error message', + }); + + expect(result.failure_reason).toBe('Custom error message'); + }); + }); + + describe('convertCustomerToAppData', () => { + it('should convert customer with all fields', () => { + const mockCustomer = { + id: 'cus_123', + email: 'test@example.com', + invoice_settings: { + default_payment_method: 'pm_123', + }, + created: 1640995200, // 2022-01-01 00:00:00 UTC + } as any; + + const result = adapter.convertCustomerToAppData(mockCustomer); + + expect(result).toEqual({ + stripe_customer_id: 'cus_123', + stripe_email: 'test@example.com', + stripe_default_payment_method: 'pm_123', + stripe_created_at: '2022-01-01T00:00:00.000Z', + }); + }); + + it('should handle customer with null email', () => { + const mockCustomer = { + id: 'cus_123', + email: null, + invoice_settings: { + default_payment_method: null, + }, + created: 1640995200, + } as any; + + const result = adapter.convertCustomerToAppData(mockCustomer); + + expect(result).toEqual({ + stripe_customer_id: 'cus_123', + stripe_email: null, + stripe_default_payment_method: null, + stripe_created_at: '2022-01-01T00:00:00.000Z', + }); + }); + + it('should handle customer with no default payment method', () => { + const mockCustomer = { + id: 'cus_456', + email: 'user@example.com', + invoice_settings: { + default_payment_method: null, + }, + created: 1672531200, // 2023-01-01 00:00:00 UTC + } as any; + + const result = adapter.convertCustomerToAppData(mockCustomer); + + expect(result.stripe_default_payment_method).toBeNull(); + expect(result.stripe_created_at).toBe('2023-01-01T00:00:00.000Z'); + }); + + it('should correctly convert Unix timestamp to ISO string', () => { + const mockCustomer = { + id: 'cus_789', + email: 'test@test.com', + invoice_settings: {}, + created: 0, // Epoch + } as any; + + const result = adapter.convertCustomerToAppData(mockCustomer); + + expect(result.stripe_created_at).toBe('1970-01-01T00:00:00.000Z'); + }); + }); +}); diff --git a/stripe_collection_module/__tests__/clients/root-client.test.ts b/stripe_collection_module/__tests__/clients/root-client.test.ts new file mode 100644 index 0000000..0d3b9a0 --- /dev/null +++ b/stripe_collection_module/__tests__/clients/root-client.test.ts @@ -0,0 +1,31 @@ +/** + * RootClient Tests + */ + +jest.mock('../../code/services/config-instance', () => ({ + getConfigService: jest.fn(() => ({ + get: jest.fn((key: string) => { + if (key === 'rootApiKey') return 'test_root_key'; + if (key === 'rootBaseUrl') return 'https://test.root.co.za'; + return null; + }), + })), +})); + +import rootClient from '../../code/clients/root-client'; + +describe('RootClient', () => { + it('should export a singleton instance', () => { + expect(rootClient).toBeDefined(); + expect(rootClient.SDK).toBeDefined(); + }); + + it('should have SDK property', () => { + expect(rootClient.SDK).toBeTruthy(); + }); + + it('should initialize SDK with configuration', () => { + // SDK should be initialized (we can't test internals but can verify it exists) + expect(typeof rootClient.SDK).toBe('object'); + }); +}); diff --git a/stripe_collection_module/__tests__/clients/stripe-client.test.ts b/stripe_collection_module/__tests__/clients/stripe-client.test.ts new file mode 100644 index 0000000..57da9de --- /dev/null +++ b/stripe_collection_module/__tests__/clients/stripe-client.test.ts @@ -0,0 +1,35 @@ +/** + * StripeClient Tests + */ + +jest.mock('../../code/services/config-instance'); + +import StripeClient from '../../code/clients/stripe-client'; +import { setupConfigMock } from '../test-helpers'; + +describe('StripeClient', () => { + beforeEach(() => { + setupConfigMock(); + }); + + it('should instantiate with Stripe SDK', () => { + const client = new StripeClient(); + + expect(client).toBeDefined(); + expect(client.stripeSDK).toBeDefined(); + }); + + it('should have stripeSDK property', () => { + const client = new StripeClient(); + + expect(client.stripeSDK).toBeTruthy(); + expect(typeof client.stripeSDK).toBe('object'); + }); + + it('should create new instance each time', () => { + const client1 = new StripeClient(); + const client2 = new StripeClient(); + + expect(client1).not.toBe(client2); + }); +}); diff --git a/stripe_collection_module/__tests__/controllers/invoice-paid.controller.test.ts b/stripe_collection_module/__tests__/controllers/invoice-paid.controller.test.ts new file mode 100644 index 0000000..e8fae80 --- /dev/null +++ b/stripe_collection_module/__tests__/controllers/invoice-paid.controller.test.ts @@ -0,0 +1,336 @@ +/** + * InvoicePaidController Tests + */ + +import Stripe from 'stripe'; +import { PaymentStatus } from '@rootplatform/node-sdk'; +import { InvoicePaidController } from '../../code/controllers/stripe-event-processors/invoice-paid.controller'; +import { + createMockLogService, + createMockRootService, + createMockStripeClient, +} from '../test-helpers'; + +describe('InvoicePaidController', () => { + let controller: InvoicePaidController; + let mockLogService: ReturnType; + let mockRootService: ReturnType; + let mockStripeClient: ReturnType; + + beforeEach(() => { + mockLogService = createMockLogService(); + mockRootService = createMockRootService(); + mockStripeClient = createMockStripeClient(); + + controller = new InvoicePaidController( + mockLogService as any, + mockRootService as any, + mockStripeClient as any + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('handle', () => { + it('should successfully process invoice with payment mappings', async () => { + const mockInvoice = { + id: 'inv_123', + amount_paid: 10000, + amount_due: 10000, + metadata: { + associatedRootPaymentIds: JSON.stringify([ + { + rootPaymentId: 'payment_123', + invoiceLineItemId: 'il_123', + }, + ]), + }, + } as any; + + (mockRootService.updatePaymentStatus as jest.Mock).mockResolvedValue( + undefined + ); + + await controller.handle(mockInvoice); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Processing invoice.paid event', + 'InvoicePaidController', + { + invoiceId: 'inv_123', + amount: 10000, + } + ); + + expect(mockRootService.updatePaymentStatus).toHaveBeenCalledWith({ + paymentId: 'payment_123', + status: PaymentStatus.Successful, + }); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Root payment updated to successful', + 'InvoicePaidController', + { + invoiceId: 'inv_123', + rootPaymentId: 'payment_123', + invoiceLineItemId: 'il_123', + } + ); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Successfully processed invoice.paid event', + 'InvoicePaidController', + { + invoiceId: 'inv_123', + paymentsUpdated: 1, + } + ); + }); + + it('should process multiple payment mappings', async () => { + const mockInvoice = { + id: 'inv_123', + amount_paid: 20000, + amount_due: 20000, + metadata: { + associatedRootPaymentIds: JSON.stringify([ + { + rootPaymentId: 'payment_123', + invoiceLineItemId: 'il_123', + }, + { + rootPaymentId: 'payment_456', + invoiceLineItemId: 'il_456', + }, + ]), + }, + } as any; + + (mockRootService.updatePaymentStatus as jest.Mock).mockResolvedValue( + undefined + ); + + await controller.handle(mockInvoice); + + expect(mockRootService.updatePaymentStatus).toHaveBeenCalledTimes(2); + expect(mockRootService.updatePaymentStatus).toHaveBeenCalledWith({ + paymentId: 'payment_123', + status: PaymentStatus.Successful, + }); + expect(mockRootService.updatePaymentStatus).toHaveBeenCalledWith({ + paymentId: 'payment_456', + status: PaymentStatus.Successful, + }); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Successfully processed invoice.paid event', + 'InvoicePaidController', + { + invoiceId: 'inv_123', + paymentsUpdated: 2, + } + ); + }); + + it('should skip invoice with zero amount_due', async () => { + const mockInvoice = { + id: 'inv_123', + amount_paid: 0, + amount_due: 0, + metadata: {}, + } as any; + + await controller.handle(mockInvoice); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Invoice has zero amount_due, skipping (already processed on invoice.created)', + 'InvoicePaidController', + { invoiceId: 'inv_123' } + ); + + expect(mockRootService.updatePaymentStatus).not.toHaveBeenCalled(); + }); + + it('should warn when no payment mappings found', async () => { + const mockInvoice = { + id: 'inv_123', + amount_paid: 10000, + amount_due: 10000, + metadata: {}, + } as any; + + await controller.handle(mockInvoice); + + expect(mockLogService.warn).toHaveBeenCalledWith( + 'No payment mappings found in invoice metadata', + 'InvoicePaidController', + { invoiceId: 'inv_123' } + ); + + expect(mockRootService.updatePaymentStatus).not.toHaveBeenCalled(); + }); + + it('should warn when metadata is missing associatedRootPaymentIds', async () => { + const mockInvoice = { + id: 'inv_123', + amount_paid: 10000, + amount_due: 10000, + metadata: { + someOtherField: 'value', + }, + } as any; + + await controller.handle(mockInvoice); + + expect(mockLogService.warn).toHaveBeenCalledWith( + 'No payment mappings found in invoice metadata', + 'InvoicePaidController', + { invoiceId: 'inv_123' } + ); + }); + + it('should handle invalid JSON in payment mappings', async () => { + const mockInvoice = { + id: 'inv_123', + amount_paid: 10000, + amount_due: 10000, + metadata: { + associatedRootPaymentIds: 'invalid json {', + }, + } as any; + + await controller.handle(mockInvoice); + + expect(mockLogService.error).toHaveBeenCalledWith( + 'Failed to parse payment mappings from invoice metadata', + 'InvoicePaidController', + expect.objectContaining({ + invoiceId: 'inv_123', + }) + ); + + expect(mockLogService.warn).toHaveBeenCalledWith( + 'No payment mappings found in invoice metadata', + 'InvoicePaidController', + { invoiceId: 'inv_123' } + ); + + expect(mockRootService.updatePaymentStatus).not.toHaveBeenCalled(); + }); + + it('should continue processing other payments if one fails', async () => { + const mockInvoice = { + id: 'inv_123', + amount_paid: 20000, + amount_due: 20000, + metadata: { + associatedRootPaymentIds: JSON.stringify([ + { + rootPaymentId: 'payment_123', + invoiceLineItemId: 'il_123', + }, + { + rootPaymentId: 'payment_456', + invoiceLineItemId: 'il_456', + }, + ]), + }, + } as any; + + const error = new Error('Payment update failed'); + (mockRootService.updatePaymentStatus as jest.Mock) + .mockRejectedValueOnce(error) + .mockResolvedValueOnce(undefined); + + await controller.handle(mockInvoice); + + expect(mockRootService.updatePaymentStatus).toHaveBeenCalledTimes(2); + + expect(mockLogService.error).toHaveBeenCalledWith( + 'Failed to update Root payment', + 'InvoicePaidController', + { + invoiceId: 'inv_123', + rootPaymentId: 'payment_123', + error: 'Payment update failed', + } + ); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Root payment updated to successful', + 'InvoicePaidController', + { + invoiceId: 'inv_123', + rootPaymentId: 'payment_456', + invoiceLineItemId: 'il_456', + } + ); + }); + + it('should handle empty payment mappings array', async () => { + const mockInvoice = { + id: 'inv_123', + amount_paid: 10000, + amount_due: 10000, + metadata: { + associatedRootPaymentIds: JSON.stringify([]), + }, + } as any; + + await controller.handle(mockInvoice); + + expect(mockLogService.warn).toHaveBeenCalledWith( + 'No payment mappings found in invoice metadata', + 'InvoicePaidController', + { invoiceId: 'inv_123' } + ); + + expect(mockRootService.updatePaymentStatus).not.toHaveBeenCalled(); + }); + + it('should handle all payments failing', async () => { + const mockInvoice = { + id: 'inv_123', + amount_paid: 10000, + amount_due: 10000, + metadata: { + associatedRootPaymentIds: JSON.stringify([ + { + rootPaymentId: 'payment_123', + invoiceLineItemId: 'il_123', + }, + ]), + }, + } as any; + + const error = new Error('Payment update failed'); + (mockRootService.updatePaymentStatus as jest.Mock).mockRejectedValue( + error + ); + + await controller.handle(mockInvoice); + + expect(mockLogService.error).toHaveBeenCalledWith( + 'Failed to update Root payment', + 'InvoicePaidController', + { + invoiceId: 'inv_123', + rootPaymentId: 'payment_123', + error: 'Payment update failed', + } + ); + + // Should still log final success message even if individual payments failed + expect(mockLogService.info).toHaveBeenCalledWith( + 'Successfully processed invoice.paid event', + 'InvoicePaidController', + { + invoiceId: 'inv_123', + paymentsUpdated: 1, + } + ); + }); + }); +}); diff --git a/stripe_collection_module/__tests__/controllers/payment-creation.controller.test.ts b/stripe_collection_module/__tests__/controllers/payment-creation.controller.test.ts new file mode 100644 index 0000000..67a0d66 --- /dev/null +++ b/stripe_collection_module/__tests__/controllers/payment-creation.controller.test.ts @@ -0,0 +1,298 @@ +/** + * PaymentCreationController Tests + */ + +import * as root from '@rootplatform/node-sdk'; +import Stripe from 'stripe'; +import { PaymentCreationController } from '../../code/controllers/root-event-processors/payment-creation.controller'; +import { + createMockLogService, + createMockRootService, + createMockStripeClient, +} from '../test-helpers'; + +describe('PaymentCreationController', () => { + let controller: PaymentCreationController; + let mockLogService: ReturnType; + let mockRootService: ReturnType; + let mockStripeClient: ReturnType; + let mockStripeSDK: any; + + beforeEach(() => { + mockLogService = createMockLogService(); + mockRootService = createMockRootService(); + mockStripeClient = createMockStripeClient(); + mockStripeSDK = mockStripeClient.stripeSDK; + + controller = new PaymentCreationController( + mockLogService as any, + mockRootService as any, + mockStripeClient as any + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('handle', () => { + const validParams = { + rootPaymentId: 'payment_123', + rootPolicyId: 'policy_456', + amount: 10000, + description: 'Premium payment', + status: root.PaymentStatus.Pending, + }; + + it('should successfully process valid payment creation', async () => { + const mockPolicy = { + policy_id: 'policy_456', + currency: 'ZAR', + app_data: { + stripe_customer_id: 'cus_123', + }, + } as any; + + const mockPaymentIntent = { + id: 'pi_123', + status: 'requires_payment_method', + } as Stripe.PaymentIntent; + + (mockRootService.getPolicy as jest.Mock).mockResolvedValue(mockPolicy); + (mockStripeSDK.paymentIntents.create as jest.Mock).mockResolvedValue( + mockPaymentIntent + ); + + await controller.handle(validParams); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Processing payment creation event', + 'PaymentCreationController', + { + rootPaymentId: 'payment_123', + rootPolicyId: 'policy_456', + amount: 10000, + } + ); + + expect(mockRootService.getPolicy).toHaveBeenCalledWith('policy_456'); + + expect(mockStripeSDK.paymentIntents.create).toHaveBeenCalledWith({ + amount: 10000, + currency: 'ZAR', + customer: 'cus_123', + description: 'Premium payment', + metadata: { + rootPaymentId: 'payment_123', + rootPolicyId: 'policy_456', + }, + payment_method_types: ['card'], + confirm: true, + off_session: true, + }); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Successfully created Stripe payment intent', + 'PaymentCreationController', + { + rootPaymentId: 'payment_123', + paymentIntentId: 'pi_123', + status: 'requires_payment_method', + } + ); + }); + + it('should skip payment with Stripe invoice description', async () => { + const paramsWithInvoice = { + ...validParams, + description: 'Stripe created invoice item: inv_123', + }; + + await controller.handle(paramsWithInvoice); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Skipping payment - already has associated Stripe invoice', + 'PaymentCreationController', + { + rootPaymentId: 'payment_123', + description: 'Stripe created invoice item: inv_123', + } + ); + + expect(mockRootService.getPolicy).not.toHaveBeenCalled(); + expect(mockStripeSDK.paymentIntents.create).not.toHaveBeenCalled(); + }); + + it('should skip payment with refund description', async () => { + const paramsWithRefund = { + ...validParams, + description: 'Refund for Stripe charge: ch_123', + }; + + await controller.handle(paramsWithRefund); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Skipping payment - already has associated Stripe invoice', + 'PaymentCreationController', + { + rootPaymentId: 'payment_123', + description: 'Refund for Stripe charge: ch_123', + } + ); + + expect(mockRootService.getPolicy).not.toHaveBeenCalled(); + }); + + it('should skip non-pending payment', async () => { + const paramsNotPending = { + ...validParams, + status: root.PaymentStatus.Successful, + }; + + await controller.handle(paramsNotPending); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Skipping payment - not in pending status', + 'PaymentCreationController', + { + rootPaymentId: 'payment_123', + status: root.PaymentStatus.Successful, + } + ); + + expect(mockRootService.getPolicy).not.toHaveBeenCalled(); + }); + + it('should throw error if policy missing stripe_customer_id', async () => { + const mockPolicy = { + policy_id: 'policy_456', + currency: 'ZAR', + app_data: {}, + } as any; + + (mockRootService.getPolicy as jest.Mock).mockResolvedValue(mockPolicy); + + await expect(controller.handle(validParams)).rejects.toThrow( + 'Policy policy_456 is missing stripe_customer_id in app_data' + ); + + expect(mockRootService.getPolicy).toHaveBeenCalledWith('policy_456'); + expect(mockStripeSDK.paymentIntents.create).not.toHaveBeenCalled(); + }); + + it('should throw error if policy has no app_data', async () => { + const mockPolicy = { + policy_id: 'policy_456', + currency: 'ZAR', + } as any; + + (mockRootService.getPolicy as jest.Mock).mockResolvedValue(mockPolicy); + + await expect(controller.handle(validParams)).rejects.toThrow( + 'Policy policy_456 is missing stripe_customer_id in app_data' + ); + }); + + it('should handle Stripe payment intent creation failure', async () => { + const mockPolicy = { + policy_id: 'policy_456', + currency: 'ZAR', + app_data: { + stripe_customer_id: 'cus_123', + }, + } as any; + + const stripeError = new Error('Insufficient funds'); + + (mockRootService.getPolicy as jest.Mock).mockResolvedValue(mockPolicy); + (mockStripeSDK.paymentIntents.create as jest.Mock).mockRejectedValue( + stripeError + ); + + await expect(controller.handle(validParams)).rejects.toThrow( + 'Insufficient funds' + ); + + expect(mockLogService.error).toHaveBeenCalledWith( + 'Failed to create Stripe payment intent', + 'PaymentCreationController', + { + error: 'Insufficient funds', + rootPaymentId: 'payment_123', + } + ); + }); + + it('should handle policy retrieval failure', async () => { + const error = new Error('Policy not found'); + + (mockRootService.getPolicy as jest.Mock).mockRejectedValue(error); + + await expect(controller.handle(validParams)).rejects.toThrow( + 'Policy not found' + ); + + expect(mockRootService.getPolicy).toHaveBeenCalledWith('policy_456'); + expect(mockStripeSDK.paymentIntents.create).not.toHaveBeenCalled(); + }); + + it('should pass through policy currency to payment intent', async () => { + const mockPolicy = { + policy_id: 'policy_456', + currency: 'USD', + app_data: { + stripe_customer_id: 'cus_123', + }, + } as any; + + const mockPaymentIntent = { + id: 'pi_123', + status: 'succeeded', + } as Stripe.PaymentIntent; + + (mockRootService.getPolicy as jest.Mock).mockResolvedValue(mockPolicy); + (mockStripeSDK.paymentIntents.create as jest.Mock).mockResolvedValue( + mockPaymentIntent + ); + + await controller.handle(validParams); + + expect(mockStripeSDK.paymentIntents.create).toHaveBeenCalledWith( + expect.objectContaining({ + currency: 'USD', + }) + ); + }); + + it('should include metadata in payment intent', async () => { + const mockPolicy = { + policy_id: 'policy_456', + currency: 'ZAR', + app_data: { + stripe_customer_id: 'cus_123', + }, + } as any; + + const mockPaymentIntent = { + id: 'pi_123', + status: 'succeeded', + } as Stripe.PaymentIntent; + + (mockRootService.getPolicy as jest.Mock).mockResolvedValue(mockPolicy); + (mockStripeSDK.paymentIntents.create as jest.Mock).mockResolvedValue( + mockPaymentIntent + ); + + await controller.handle(validParams); + + expect(mockStripeSDK.paymentIntents.create).toHaveBeenCalledWith( + expect.objectContaining({ + metadata: { + rootPaymentId: 'payment_123', + rootPolicyId: 'policy_456', + }, + }) + ); + }); + }); +}); diff --git a/stripe_collection_module/__tests__/core/container.setup.test.ts b/stripe_collection_module/__tests__/core/container.setup.test.ts new file mode 100644 index 0000000..ace0a70 --- /dev/null +++ b/stripe_collection_module/__tests__/core/container.setup.test.ts @@ -0,0 +1,104 @@ +/** + * Container Setup Tests + */ + +import { createContainer, getContainer, setContainer, resetContainer } from '../../code/core/container.setup'; +import { ServiceToken } from '../../code/core/container'; + +describe('Container Setup', () => { + describe('createContainer', () => { + it('should create a container with all services registered', () => { + const container = createContainer(); + + expect(container).toBeDefined(); + expect(container.has(ServiceToken.CONFIG_SERVICE)).toBe(true); + expect(container.has(ServiceToken.LOG_SERVICE)).toBe(true); + expect(container.has(ServiceToken.STRIPE_CLIENT)).toBe(true); + expect(container.has(ServiceToken.ROOT_CLIENT)).toBe(true); + expect(container.has(ServiceToken.ROOT_SERVICE)).toBe(true); + expect(container.has(ServiceToken.STRIPE_SERVICE)).toBe(true); + }); + + it('should allow resolving ConfigService', () => { + const container = createContainer(); + const configService = container.resolve(ServiceToken.CONFIG_SERVICE) as any; + + expect(configService).toBeDefined(); + expect(configService.get).toBeDefined(); + }); + + it('should allow resolving LogService', () => { + const container = createContainer(); + const logService = container.resolve(ServiceToken.LOG_SERVICE) as any; + + expect(logService).toBeDefined(); + expect(logService.info).toBeDefined(); + }); + + it('should have RootService registered', () => { + const container = createContainer(); + + expect(container.has(ServiceToken.ROOT_SERVICE)).toBe(true); + }); + + it('should have StripeService registered', () => { + const container = createContainer(); + + expect(container.has(ServiceToken.STRIPE_SERVICE)).toBe(true); + }); + + it('should have controllers registered', () => { + const container = createContainer(); + + expect(container.has(ServiceToken.INVOICE_PAID_CONTROLLER)).toBe(true); + expect(container.has(ServiceToken.PAYMENT_CREATION_CONTROLLER)).toBe(true); + }); + }); + + describe('getContainer', () => { + beforeEach(() => { + resetContainer(); + }); + + it('should return a container instance', () => { + const container = getContainer(); + + expect(container).toBeDefined(); + }); + + it('should return the same instance on multiple calls', () => { + const container1 = getContainer(); + const container2 = getContainer(); + + expect(container1).toBe(container2); + }); + + it('should create container if not exists', () => { + resetContainer(); + const container = getContainer(); + + expect(container).toBeDefined(); + expect(container.has(ServiceToken.LOG_SERVICE)).toBe(true); + }); + }); + + describe('setContainer', () => { + it('should allow setting a custom container', () => { + const customContainer = createContainer(); + setContainer(customContainer); + + const retrieved = getContainer(); + expect(retrieved).toBe(customContainer); + }); + }); + + describe('resetContainer', () => { + it('should reset the global container', () => { + const container1 = getContainer(); + resetContainer(); + const container2 = getContainer(); + + expect(container1).not.toBe(container2); + }); + }); +}); diff --git a/stripe_collection_module/__tests__/lifecycle-hooks/payment-method.hooks.test.ts b/stripe_collection_module/__tests__/lifecycle-hooks/payment-method.hooks.test.ts new file mode 100644 index 0000000..096d07a --- /dev/null +++ b/stripe_collection_module/__tests__/lifecycle-hooks/payment-method.hooks.test.ts @@ -0,0 +1,254 @@ +/** + * Payment Method Hooks Tests + */ + +import * as paymentMethodHooks from '../../code/lifecycle-hooks/payment-method.hooks'; +import { getContainer } from '../../code/core/container.setup'; +import { ServiceToken } from '../../code/core/container'; +import { + createMockLogService, + createMockStripeClient, + createMockConfigService, +} from '../test-helpers'; + +jest.mock('../../code/core/container.setup'); + +describe('Payment Method Hooks', () => { + let mockContainer: any; + let mockLogService: ReturnType; + let mockStripeClient: ReturnType; + let mockConfigService: ReturnType; + let mockRenderService: any; + + beforeEach(() => { + jest.clearAllMocks(); + + mockLogService = createMockLogService(); + mockStripeClient = createMockStripeClient(); + mockConfigService = createMockConfigService(); + + mockRenderService = { + renderCreatePaymentMethod: jest.fn(), + renderViewPaymentMethodSummary: jest.fn(), + }; + + mockContainer = { + resolve: jest.fn((token: symbol) => { + if (token === ServiceToken.LOG_SERVICE) return mockLogService; + if (token === ServiceToken.STRIPE_CLIENT) return mockStripeClient; + if (token === ServiceToken.CONFIG_SERVICE) return mockConfigService; + if (token === ServiceToken.RENDER_SERVICE) return mockRenderService; + return null; + }), + }; + + (getContainer as jest.Mock).mockReturnValue(mockContainer); + }); + + describe('createPaymentMethod', () => { + it('should return module data with setup intent', () => { + const setupIntent = { + id: 'seti_123', + usage: 'off_session', + object: 'setup_intent', + status: 'succeeded', + livemode: false, + payment_method: 'pm_123', + }; + + const result = paymentMethodHooks.createPaymentMethod({ + data: { setupIntent }, + }); + + expect(result).toEqual({ + module: { + id: 'seti_123', + usage: 'off_session', + object: 'setup_intent', + status: 'succeeded', + livemode: false, + payment_method: 'pm_123', + }, + }); + expect(mockLogService.info).toHaveBeenCalledWith( + 'Creating payment method', + 'createPaymentMethod', + { setupIntent } + ); + }); + + it('should return module data without setup intent', () => { + const data = { customField: 'value' }; + + const result = paymentMethodHooks.createPaymentMethod({ + data: data as any, + }); + + expect(result).toEqual({ + module: data, + }); + }); + + it('should handle empty data', () => { + const result = paymentMethodHooks.createPaymentMethod({}); + + expect(result).toEqual({ + module: undefined, + }); + }); + }); + + describe('renderCreatePaymentMethod', () => { + it('should render payment method creation form', async () => { + ( + mockStripeClient.stripeSDK.setupIntents.create as jest.Mock + ).mockResolvedValue({ + client_secret: 'seti_123_secret_456', + }); + mockRenderService.renderCreatePaymentMethod.mockReturnValue( + '
Payment Form
' + ); + + const result = await paymentMethodHooks.renderCreatePaymentMethod(); + + expect(mockStripeClient.stripeSDK.setupIntents.create).toHaveBeenCalledWith( + {} + ); + expect(mockRenderService.renderCreatePaymentMethod).toHaveBeenCalledWith({ + stripePublishableKey: 'pk_test_123', + setupIntentClientSecret: 'seti_123_secret_456', + }); + expect(result).toBe('
Payment Form
'); + }); + + it('should throw error if client secret is missing', async () => { + ( + mockStripeClient.stripeSDK.setupIntents.create as jest.Mock + ).mockResolvedValue({ + client_secret: null, + }); + + await expect( + paymentMethodHooks.renderCreatePaymentMethod() + ).rejects.toThrow('Setup intent client secret is missing'); + + expect(mockLogService.error).toHaveBeenCalled(); + }); + + it('should handle setup intent creation error', async () => { + ( + mockStripeClient.stripeSDK.setupIntents.create as jest.Mock + ).mockRejectedValue(new Error('Stripe API error')); + + await expect( + paymentMethodHooks.renderCreatePaymentMethod() + ).rejects.toThrow('Error creating setup intent: Stripe API error'); + + expect(mockLogService.error).toHaveBeenCalledWith( + 'Error rendering payment method form', + 'renderCreatePaymentMethod', + {}, + expect.any(Error) + ); + }); + }); + + describe('renderViewPaymentMethodSummary', () => { + it('should render payment method summary', async () => { + const mockPaymentMethod = { + id: 'pm_123', + type: 'card', + card: { + brand: 'visa', + last4: '4242', + }, + }; + + ( + mockStripeClient.stripeSDK.paymentMethods.retrieve as jest.Mock + ).mockResolvedValue(mockPaymentMethod); + mockRenderService.renderViewPaymentMethodSummary.mockReturnValue( + 'Visa ****4242' + ); + + const result = await paymentMethodHooks.renderViewPaymentMethodSummary({ + payment_method: 'pm_123', + }); + + expect( + mockStripeClient.stripeSDK.paymentMethods.retrieve + ).toHaveBeenCalledWith('pm_123'); + expect( + mockRenderService.renderViewPaymentMethodSummary + ).toHaveBeenCalledWith({ + payment_method: { + module: { + payment_method: 'pm_123', + }, + }, + }); + expect(result).toBe('Visa ****4242'); + }); + + it('should return error message if payment method ID is missing', async () => { + const result = await paymentMethodHooks.renderViewPaymentMethodSummary({ + payment_method: null, + }); + + expect(result).toContain('No payment method found'); + expect( + mockStripeClient.stripeSDK.paymentMethods.retrieve + ).not.toHaveBeenCalled(); + }); + + it('should handle payment method retrieval error', async () => { + ( + mockStripeClient.stripeSDK.paymentMethods.retrieve as jest.Mock + ).mockRejectedValue(new Error('Not found')); + + await expect( + paymentMethodHooks.renderViewPaymentMethodSummary({ + payment_method: 'pm_invalid', + }) + ).rejects.toThrow('Error retrieving payment method: Not found'); + + expect(mockLogService.error).toHaveBeenCalled(); + }); + }); + + describe('renderViewPaymentMethod', () => { + it('should return empty string (stub implementation)', () => { + const result = paymentMethodHooks.renderViewPaymentMethod(); + + expect(result).toBe(''); + }); + }); + + describe('afterPolicyPaymentMethodAssigned', () => { + it('should log payment method assignment', () => { + const policy = { policy_id: 'policy_123' }; + + paymentMethodHooks.afterPolicyPaymentMethodAssigned({ policy }); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Payment method assigned to policy', + 'afterPolicyPaymentMethodAssigned', + { policyId: 'policy_123' } + ); + }); + }); + + describe('afterPaymentMethodRemoved', () => { + it('should log payment method removal', () => { + const policy = { policy_id: 'policy_456' }; + + paymentMethodHooks.afterPaymentMethodRemoved({ policy }); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Payment method removed from policy', + 'afterPaymentMethodRemoved', + { policyId: 'policy_456' } + ); + }); + }); +}); diff --git a/stripe_collection_module/__tests__/lifecycle-hooks/payment.hooks.test.ts b/stripe_collection_module/__tests__/lifecycle-hooks/payment.hooks.test.ts new file mode 100644 index 0000000..0662455 --- /dev/null +++ b/stripe_collection_module/__tests__/lifecycle-hooks/payment.hooks.test.ts @@ -0,0 +1,93 @@ +/** + * Payment Hooks Tests + */ + +import * as paymentHooks from '../../code/lifecycle-hooks/payment.hooks'; +import { getContainer } from '../../code/core/container.setup'; +import { ServiceToken } from '../../code/core/container'; +import { createMockLogService } from '../test-helpers'; + +jest.mock('../../code/core/container.setup'); + +describe('Payment Hooks', () => { + let mockContainer: any; + let mockLogService: ReturnType; + + beforeEach(() => { + jest.clearAllMocks(); + + mockLogService = createMockLogService(); + + mockContainer = { + resolve: jest.fn((token: symbol) => { + if (token === ServiceToken.LOG_SERVICE) return mockLogService; + return null; + }), + }; + + (getContainer as jest.Mock).mockReturnValue(mockContainer); + }); + + describe('afterPaymentCreated', () => { + it('should log payment creation with payment and policy IDs', () => { + const policy = { policy_id: 'pol_123' }; + const payment = { + payment_id: 'pay_123', + amount: 10000, + currency: 'USD', + }; + + paymentHooks.afterPaymentCreated({ policy, payment }); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Payment created', + 'afterPaymentCreated', + { policyId: 'pol_123', paymentId: 'pay_123' } + ); + }); + + it('should log payment creation without IDs', () => { + const policy = {}; + const payment = {}; + + paymentHooks.afterPaymentCreated({ policy, payment }); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Payment created', + 'afterPaymentCreated', + { policyId: undefined, paymentId: undefined } + ); + }); + }); + + describe('afterPaymentUpdated', () => { + it('should log payment update with payment and policy IDs', () => { + const policy = { policy_id: 'pol_456' }; + const payment = { + payment_id: 'pay_456', + status: 'successful', + }; + + paymentHooks.afterPaymentUpdated({ policy, payment }); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Payment updated', + 'afterPaymentUpdated', + { policyId: 'pol_456', paymentId: 'pay_456' } + ); + }); + + it('should log payment update without IDs', () => { + const policy = {}; + const payment = {}; + + paymentHooks.afterPaymentUpdated({ policy, payment }); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Payment updated', + 'afterPaymentUpdated', + { policyId: undefined, paymentId: undefined } + ); + }); + }); +}); diff --git a/stripe_collection_module/__tests__/lifecycle-hooks/policy.hooks.test.ts b/stripe_collection_module/__tests__/lifecycle-hooks/policy.hooks.test.ts new file mode 100644 index 0000000..fe1c2d9 --- /dev/null +++ b/stripe_collection_module/__tests__/lifecycle-hooks/policy.hooks.test.ts @@ -0,0 +1,216 @@ +/** + * Policy Hooks Tests + */ + +import * as policyHooks from '../../code/lifecycle-hooks/policy.hooks'; +import { getContainer } from '../../code/core/container.setup'; +import { ServiceToken } from '../../code/core/container'; +import { createMockLogService } from '../test-helpers'; + +jest.mock('../../code/core/container.setup'); + +describe('Policy Hooks', () => { + let mockContainer: any; + let mockLogService: ReturnType; + + beforeEach(() => { + jest.clearAllMocks(); + + mockLogService = createMockLogService(); + + mockContainer = { + resolve: jest.fn((token: symbol) => { + if (token === ServiceToken.LOG_SERVICE) return mockLogService; + return null; + }), + }; + + (getContainer as jest.Mock).mockReturnValue(mockContainer); + }); + + describe('afterPolicyIssued', () => { + it('should log policy issuance', () => { + policyHooks.afterPolicyIssued(); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Policy issued', + 'afterPolicyIssued' + ); + }); + }); + + describe('afterPolicyUpdated', () => { + it('should log policy update with policy ID and updates', () => { + const policy = { + policy_id: 'policy_456', + }; + const updates = { + status: 'active', + sum_assured: 100000, + }; + + policyHooks.afterPolicyUpdated({ policy, updates }); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Policy updated', + 'afterPolicyUpdated', + { + policyId: 'policy_456', + updates: { status: 'active', sum_assured: 100000 }, + } + ); + }); + + it('should log policy update without IDs', () => { + const policy = {}; + const updates = {}; + + policyHooks.afterPolicyUpdated({ policy, updates }); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Policy updated', + 'afterPolicyUpdated', + { + policyId: undefined, + updates: {}, + } + ); + }); + }); + + describe('afterPolicyCancelled', () => { + it('should log policy cancellation with policy ID', () => { + const policy = { + policy_id: 'policy_111', + status: 'cancelled', + }; + + policyHooks.afterPolicyCancelled({ policy }); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Policy cancelled', + 'afterPolicyCancelled', + { policyId: 'policy_111' } + ); + }); + + it('should log policy cancellation without policy ID', () => { + const policy = {}; + + policyHooks.afterPolicyCancelled({ policy }); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Policy cancelled', + 'afterPolicyCancelled', + { policyId: undefined } + ); + }); + }); + + describe('afterPolicyExpired', () => { + it('should log policy expiration with policy ID', () => { + const policy = { + policy_id: 'policy_222', + status: 'expired', + }; + + policyHooks.afterPolicyExpired({ policy }); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Policy expired', + 'afterPolicyExpired', + { policyId: 'policy_222' } + ); + }); + + it('should log policy expiration without policy ID', () => { + const policy = {}; + + policyHooks.afterPolicyExpired({ policy }); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Policy expired', + 'afterPolicyExpired', + { policyId: undefined } + ); + }); + }); + + describe('afterPolicyLapsed', () => { + it('should log policy lapse with policy ID', () => { + const policy = { + policy_id: 'policy_333', + status: 'lapsed', + }; + + policyHooks.afterPolicyLapsed({ policy }); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Policy lapsed', + 'afterPolicyLapsed', + { policyId: 'policy_333' } + ); + }); + + it('should log policy lapse without policy ID', () => { + const policy = {}; + + policyHooks.afterPolicyLapsed({ policy }); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Policy lapsed', + 'afterPolicyLapsed', + { policyId: undefined } + ); + }); + }); + + describe('afterAlterationPackageApplied', () => { + it('should log alteration package application with policy ID and hook key', () => { + const policy = { + policy_id: 'policy_444', + }; + const alteration_package = { + package_id: 'pkg_123', + type: 'sum_assured_increase', + }; + const alteration_hook_key = 'increase_sum_assured'; + + policyHooks.afterAlterationPackageApplied({ + policy, + alteration_package, + alteration_hook_key, + }); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Alteration package applied', + 'afterAlterationPackageApplied', + { + policyId: 'policy_444', + alterationHookKey: 'increase_sum_assured', + } + ); + }); + + it('should log without policy ID', () => { + const policy = {}; + const alteration_package = {}; + const alteration_hook_key = 'test_key'; + + policyHooks.afterAlterationPackageApplied({ + policy, + alteration_package, + alteration_hook_key, + }); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Alteration package applied', + 'afterAlterationPackageApplied', + { + policyId: undefined, + alterationHookKey: 'test_key', + } + ); + }); + }); +}); diff --git a/stripe_collection_module/__tests__/services/config-instance.test.ts b/stripe_collection_module/__tests__/services/config-instance.test.ts new file mode 100644 index 0000000..bc4afba --- /dev/null +++ b/stripe_collection_module/__tests__/services/config-instance.test.ts @@ -0,0 +1,35 @@ +/** + * Config Instance Tests + */ + +import { + getConfigService, + isConfigServiceInitialized, +} from '../../code/services/config-instance'; + +describe('Config Instance', () => { + describe('getConfigService', () => { + it('should return a ConfigurationService instance', () => { + const service = getConfigService(); + + expect(service).toBeDefined(); + expect(service.get).toBeDefined(); + expect(typeof service.get).toBe('function'); + }); + + it('should return the same instance on multiple calls', () => { + const service1 = getConfigService(); + const service2 = getConfigService(); + + expect(service1).toBe(service2); + }); + }); + + describe('isConfigServiceInitialized', () => { + it('should return true after getConfigService is called', () => { + getConfigService(); + + expect(isConfigServiceInitialized()).toBe(true); + }); + }); +}); diff --git a/stripe_collection_module/__tests__/services/log-instance.test.ts b/stripe_collection_module/__tests__/services/log-instance.test.ts new file mode 100644 index 0000000..5a3ad5a --- /dev/null +++ b/stripe_collection_module/__tests__/services/log-instance.test.ts @@ -0,0 +1,37 @@ +/** + * Log Instance Tests + */ + +import { + getLogService, + isLogServiceInitialized, +} from '../../code/services/log-instance'; + +describe('Log Instance', () => { + describe('getLogService', () => { + it('should return a LogService instance', () => { + const service = getLogService(); + + expect(service).toBeDefined(); + expect(service.info).toBeDefined(); + expect(service.debug).toBeDefined(); + expect(service.warn).toBeDefined(); + expect(service.error).toBeDefined(); + }); + + it('should return the same instance on multiple calls', () => { + const service1 = getLogService(); + const service2 = getLogService(); + + expect(service1).toBe(service2); + }); + }); + + describe('isLogServiceInitialized', () => { + it('should return true after getLogService is called', () => { + getLogService(); + + expect(isLogServiceInitialized()).toBe(true); + }); + }); +}); diff --git a/stripe_collection_module/__tests__/services/root.service.test.ts b/stripe_collection_module/__tests__/services/root.service.test.ts index 4a3b524..911d05e 100644 --- a/stripe_collection_module/__tests__/services/root.service.test.ts +++ b/stripe_collection_module/__tests__/services/root.service.test.ts @@ -4,35 +4,20 @@ import * as root from '@rootplatform/node-sdk'; import { RootService } from '../../code/services/root.service'; -import { LogService } from '../../code/services/log.service'; -jest.mock('../../code/services/log.service'); +import { createMockLogService, createMockRootClient } from '../test-helpers'; describe('RootService', () => { let rootService: RootService; - let mockLogService: jest.Mocked; - let mockRootClient: any; + let mockLogService: ReturnType; + let mockRootClient: ReturnType; let mockRootSDK: any; beforeEach(() => { - // Create mock LogService - mockLogService = { - info: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - generateCorrelationId: jest.fn(), - } as any; - - // Create mock Root SDK - mockRootSDK = { - getPolicyById: jest.fn(), - updatePaymentsAsync: jest.fn(), - }; - - // Mock rootClient.SDK - mockRootClient = { SDK: mockRootSDK } as any; - - rootService = new RootService(mockLogService, mockRootClient); + mockLogService = createMockLogService(); + mockRootClient = createMockRootClient(); + mockRootSDK = mockRootClient.SDK; + + rootService = new RootService(mockLogService as any, mockRootClient as any); }); afterEach(() => { @@ -388,4 +373,3 @@ describe('RootService', () => { }); }); }); - diff --git a/stripe_collection_module/__tests__/services/stripe.service.test.ts b/stripe_collection_module/__tests__/services/stripe.service.test.ts index cf76cba..8c2f411 100644 --- a/stripe_collection_module/__tests__/services/stripe.service.test.ts +++ b/stripe_collection_module/__tests__/services/stripe.service.test.ts @@ -4,52 +4,26 @@ import Stripe from 'stripe'; import { StripeService } from '../../code/services/stripe.service'; -import { LogService } from '../../code/services/log.service'; -import StripeClient from '../../code/clients/stripe-client'; - -jest.mock('../../code/services/log.service'); +import { + createMockLogService, + createMockStripeClient, +} from '../test-helpers'; describe('StripeService', () => { let stripeService: StripeService; - let mockLogService: jest.Mocked; - let mockStripeClient: jest.Mocked; + let mockLogService: ReturnType; + let mockStripeClient: ReturnType; let mockStripeSDK: any; beforeEach(() => { - // Create mock LogService - mockLogService = { - info: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - generateCorrelationId: jest.fn(), - } as any; - - // Create mock Stripe SDK with jest.fn() for all methods - mockStripeSDK = { - customers: { - create: jest.fn(), - retrieve: jest.fn(), - update: jest.fn(), - }, - paymentIntents: { - create: jest.fn(), - }, - paymentMethods: { - retrieve: jest.fn(), - attach: jest.fn(), - }, - subscriptions: { - cancel: jest.fn(), - }, - }; - - // Create mock StripeClient - mockStripeClient = { - stripeSDK: mockStripeSDK, - } as any; - - stripeService = new StripeService(mockLogService, mockStripeClient); + mockLogService = createMockLogService(); + mockStripeClient = createMockStripeClient(); + mockStripeSDK = mockStripeClient.stripeSDK; + + stripeService = new StripeService( + mockLogService as any, + mockStripeClient as any + ); }); afterEach(() => { diff --git a/stripe_collection_module/__tests__/setup.ts b/stripe_collection_module/__tests__/setup.ts index 2185277..0116a58 100644 --- a/stripe_collection_module/__tests__/setup.ts +++ b/stripe_collection_module/__tests__/setup.ts @@ -12,8 +12,5 @@ process.env.ROOT_COLLECTION_MODULE_SECRET = 'test_secret'; process.env.STRIPE_SECRET_KEY = 'sk_test_123'; process.env.STRIPE_PUBLISHABLE_KEY = 'pk_test_123'; -// Extend Jest matchers if needed -// import '@testing-library/jest-dom'; - -// Set up any global test configuration here - +// Note: Global mocks removed - use test-helpers.ts setupConfigMock() instead +// This prevents conflicts when testing the actual config/log instance modules diff --git a/stripe_collection_module/__tests__/test-helpers.ts b/stripe_collection_module/__tests__/test-helpers.ts new file mode 100644 index 0000000..9067cd1 --- /dev/null +++ b/stripe_collection_module/__tests__/test-helpers.ts @@ -0,0 +1,123 @@ +/** + * Test Helpers + * + * Centralized mock factories and test utilities to reduce duplication + */ + +/** + * Create a mock LogService + * Common across controller and service tests + */ +export function createMockLogService() { + return { + info: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + generateCorrelationId: jest.fn().mockReturnValue('test-correlation-id'), + }; +} + +/** + * Create a mock ConfigService + * Returns test environment variables + */ +export function createMockConfigService() { + return { + get: jest.fn((key: string) => { + const config: Record = { + environment: 'test', + stripeSecretKey: 'sk_test_123', + stripePublishableKey: 'pk_test_123', + stripeWebhookSigningSecret: 'whsec_test_secret', + rootApiKey: 'test_root_key', + rootBaseUrl: 'https://test.root.co.za', + }; + return config[key] || null; + }), + }; +} + +/** + * Setup mock for config-instance module + * Call this in beforeEach when you need to mock getConfigService + */ +export function setupConfigMock() { + const { getConfigService } = require('../code/services/config-instance'); + getConfigService.mockReturnValue(createMockConfigService()); +} + +/** + * Create a mock Stripe SDK + * Common structure used across Stripe tests + */ +export function createMockStripeSDK() { + return { + customers: { + create: jest.fn(), + retrieve: jest.fn(), + update: jest.fn(), + }, + paymentIntents: { + create: jest.fn(), + retrieve: jest.fn(), + update: jest.fn(), + cancel: jest.fn(), + }, + paymentMethods: { + retrieve: jest.fn(), + attach: jest.fn(), + detach: jest.fn(), + }, + subscriptions: { + cancel: jest.fn(), + retrieve: jest.fn(), + update: jest.fn(), + }, + setupIntents: { + create: jest.fn(), + retrieve: jest.fn(), + }, + }; +} + +/** + * Create a mock Root SDK + * Common structure used across Root tests + */ +export function createMockRootSDK() { + return { + getPolicyById: jest.fn(), + updatePaymentsAsync: jest.fn(), + getPolicyPaymentMethod: jest.fn(), + updatePolicy: jest.fn(), + }; +} + +/** + * Create a mock RootClient + */ +export function createMockRootClient() { + return { + SDK: createMockRootSDK(), + }; +} + +/** + * Create a mock StripeClient + */ +export function createMockStripeClient() { + return { + stripeSDK: createMockStripeSDK(), + }; +} + +/** + * Create a mock RootService + */ +export function createMockRootService() { + return { + getPolicy: jest.fn(), + updatePaymentStatus: jest.fn(), + }; +} diff --git a/stripe_collection_module/__tests__/utils/error-types.test.ts b/stripe_collection_module/__tests__/utils/error-types.test.ts new file mode 100644 index 0000000..46c9712 --- /dev/null +++ b/stripe_collection_module/__tests__/utils/error-types.test.ts @@ -0,0 +1,201 @@ +/** + * Error Types Tests + */ + +import { + ErrorCategory, + EnhancedModuleError, + ValidationError, + NotFoundError, + NetworkError, + ServerError, + TimeoutError, + RateLimitError, + categorizeError, + isRetryableError, + formatErrorForLogging, +} from '../../code/utils/error-types'; + +describe('Error Types', () => { + describe('EnhancedModuleError', () => { + it('should create error with default values', () => { + const error = new EnhancedModuleError('Test error'); + + expect(error.message).toBe('Test error'); + expect(error.name).toBe('EnhancedModuleError'); + expect(error.category).toBe(ErrorCategory.UNKNOWN); + expect(error.retryable).toBe(false); + expect(error.timestamp).toBeInstanceOf(Date); + }); + + it('should create error with custom values', () => { + const error = new EnhancedModuleError( + 'Custom error', + ErrorCategory.VALIDATION, + true, + 400, + { field: 'email' } + ); + + expect(error.message).toBe('Custom error'); + expect(error.category).toBe(ErrorCategory.VALIDATION); + expect(error.retryable).toBe(true); + expect(error.statusCode).toBe(400); + expect(error.context).toEqual({ field: 'email' }); + }); + + it('should add request tracking information', () => { + const error = new EnhancedModuleError('Test'); + error.withRequestId('req_123', 'corr_456'); + + expect(error.requestId).toBe('req_123'); + expect(error.correlationId).toBe('corr_456'); + }); + + it('should use requestId as correlationId if not provided', () => { + const error = new EnhancedModuleError('Test'); + error.withRequestId('req_123'); + + expect(error.requestId).toBe('req_123'); + expect(error.correlationId).toBe('req_123'); + }); + }); + + describe('ValidationError', () => { + it('should create validation error', () => { + const error = new ValidationError('Invalid input'); + + expect(error.message).toBe('Invalid input'); + expect(error.category).toBe(ErrorCategory.VALIDATION); + expect(error.retryable).toBe(false); + expect(error.statusCode).toBe(400); + }); + + it('should accept context', () => { + const error = new ValidationError('Invalid', { field: 'email' }); + + expect(error.context).toEqual({ field: 'email' }); + }); + }); + + describe('NotFoundError', () => { + it('should create not found error', () => { + const error = new NotFoundError('Resource not found'); + + expect(error.message).toBe('Resource not found'); + expect(error.category).toBe(ErrorCategory.NOT_FOUND); + expect(error.retryable).toBe(false); + expect(error.statusCode).toBe(404); + }); + }); + + describe('NetworkError', () => { + it('should create network error', () => { + const error = new NetworkError('Connection failed'); + + expect(error.message).toBe('Connection failed'); + expect(error.category).toBe(ErrorCategory.NETWORK); + expect(error.retryable).toBe(true); + }); + }); + + describe('ServerError', () => { + it('should create server error', () => { + const error = new ServerError('Internal server error'); + + expect(error.message).toBe('Internal server error'); + expect(error.category).toBe(ErrorCategory.SERVER_ERROR); + expect(error.retryable).toBe(true); + expect(error.statusCode).toBe(500); + }); + }); + + describe('TimeoutError', () => { + it('should create timeout error', () => { + const error = new TimeoutError('Request timeout'); + + expect(error.message).toBe('Request timeout'); + expect(error.category).toBe(ErrorCategory.TIMEOUT); + expect(error.retryable).toBe(true); + expect(error.statusCode).toBe(504); + }); + }); + + describe('RateLimitError', () => { + it('should create rate limit error', () => { + const error = new RateLimitError('Too many requests'); + + expect(error.message).toBe('Too many requests'); + expect(error.category).toBe(ErrorCategory.RATE_LIMIT); + expect(error.retryable).toBe(true); + expect(error.statusCode).toBe(429); + }); + }); + + describe('categorizeError', () => { + it('should categorize EnhancedModuleError', () => { + const error = new ValidationError('Test'); + expect(categorizeError(error)).toBe(ErrorCategory.VALIDATION); + }); + + it('should categorize unknown errors', () => { + const error = new Error('Generic error'); + expect(categorizeError(error)).toBe(ErrorCategory.UNKNOWN); + }); + + it('should categorize timeout errors by message', () => { + const error = new Error('Request timeout'); + expect(categorizeError(error)).toBe(ErrorCategory.TIMEOUT); + }); + + it('should categorize network errors by code', () => { + const error: any = new Error('Connection refused'); + error.code = 'ECONNREFUSED'; + expect(categorizeError(error)).toBe(ErrorCategory.NETWORK); + }); + }); + + describe('isRetryableError', () => { + it('should identify retryable errors', () => { + const error = new NetworkError('Connection failed'); + expect(isRetryableError(error)).toBe(true); + }); + + it('should identify non-retryable errors', () => { + const error = new ValidationError('Invalid input'); + expect(isRetryableError(error)).toBe(false); + }); + + it('should handle non-EnhancedModuleError', () => { + const error = new Error('Generic'); + expect(isRetryableError(error)).toBe(false); + }); + }); + + describe('formatErrorForLogging', () => { + it('should format EnhancedModuleError', () => { + const error = new ValidationError('Test', { field: 'email' }); + error.withRequestId('req_123'); + + const formatted = formatErrorForLogging(error); + + expect(formatted.name).toBe('ValidationError'); + expect(formatted.message).toBe('Test'); + expect(formatted.category).toBe(ErrorCategory.VALIDATION); + expect(formatted.retryable).toBe(false); + expect(formatted.statusCode).toBe(400); + expect(formatted.requestId).toBe('req_123'); + expect(formatted.context).toEqual({ field: 'email' }); + }); + + it('should format generic errors', () => { + const error = new Error('Generic error'); + + const formatted = formatErrorForLogging(error); + + expect(formatted.name).toBe('Error'); + expect(formatted.message).toBe('Generic error'); + expect(formatted.category).toBe(ErrorCategory.UNKNOWN); + }); + }); +}); diff --git a/stripe_collection_module/__tests__/utils/error.test.ts b/stripe_collection_module/__tests__/utils/error.test.ts new file mode 100644 index 0000000..2e187b8 --- /dev/null +++ b/stripe_collection_module/__tests__/utils/error.test.ts @@ -0,0 +1,79 @@ +/** + * ModuleError Tests + */ + +jest.mock('../../code/services/config-instance'); + +import ModuleError from '../../code/utils/error'; +import { setupConfigMock } from '../test-helpers'; + +describe('ModuleError', () => { + beforeEach(() => { + setupConfigMock(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + it('should create error with message', () => { + const error = new ModuleError('Test error'); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(ModuleError); + expect(error.message).toContain('Test error'); + }); + + it('should include environment in message', () => { + const error = new ModuleError('Test error'); + + expect(error.message).toContain('[test'); + }); + + it('should include metadata in message', () => { + const metadata = { userId: '123', action: 'create' }; + const error = new ModuleError('Operation failed', metadata); + + expect(error.message).toContain('Operation failed'); + expect(error.message).toContain(JSON.stringify(metadata)); + }); + + it('should include caller information in message', () => { + const error = new ModuleError('Test'); + + // Message should include caller info from stack trace + expect(error.message).toMatch(/\[test \|.*\]/); + }); + + it('should handle metadata with various types', () => { + const metadata = { + string: 'value', + number: 42, + boolean: true, + array: [1, 2, 3], + nested: { key: 'value' }, + }; + const error = new ModuleError('Complex metadata', metadata); + + expect(error.message).toContain(JSON.stringify(metadata)); + }); + + it('should handle empty metadata', () => { + const error = new ModuleError('No metadata', {}); + + expect(error.message).toContain('No metadata'); + expect(error.message).toContain('{}'); + }); + + it('should handle undefined metadata', () => { + const error = new ModuleError('Undefined metadata', undefined); + + expect(error.message).toContain('Undefined metadata'); + }); + + it('should have stack trace', () => { + const error = new ModuleError('Test error'); + + expect(error.stack).toBeDefined(); + expect(error.stack).toContain('Test error'); + }); +}); diff --git a/stripe_collection_module/__tests__/utils/logger.test.ts b/stripe_collection_module/__tests__/utils/logger.test.ts new file mode 100644 index 0000000..db80718 --- /dev/null +++ b/stripe_collection_module/__tests__/utils/logger.test.ts @@ -0,0 +1,180 @@ +/** + * Logger Tests + */ + +jest.mock('../../code/services/config-instance'); + +import Logger from '../../code/utils/logger'; +import { setupConfigMock } from '../test-helpers'; + +describe('Logger', () => { + let consoleDebugSpy: jest.SpyInstance; + let consoleInfoSpy: jest.SpyInstance; + let consoleWarnSpy: jest.SpyInstance; + + beforeEach(() => { + setupConfigMock(); + + // Spy on console methods + consoleDebugSpy = jest.spyOn(console, 'debug').mockImplementation(); + consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(); + consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + }); + + afterEach(() => { + jest.clearAllMocks(); + consoleDebugSpy.mockRestore(); + consoleInfoSpy.mockRestore(); + consoleWarnSpy.mockRestore(); + }); + + describe('debug', () => { + it('should log debug message', () => { + Logger.debug('Debug message'); + + expect(consoleDebugSpy).toHaveBeenCalledTimes(1); + expect(consoleDebugSpy).toHaveBeenCalledWith( + expect.stringContaining('Debug message') + ); + }); + + it('should include environment in log', () => { + Logger.debug('Test message'); + + expect(consoleDebugSpy).toHaveBeenCalledWith( + expect.stringContaining('[TEST') + ); + }); + + it('should include metadata', () => { + const metadata = { userId: '123', action: 'create' }; + Logger.debug('Message with metadata', metadata); + + expect(consoleDebugSpy).toHaveBeenCalledWith( + expect.stringContaining(JSON.stringify(metadata)) + ); + }); + + it('should work without metadata', () => { + Logger.debug('Message without metadata'); + + expect(consoleDebugSpy).toHaveBeenCalledTimes(1); + expect(consoleDebugSpy).toHaveBeenCalledWith( + expect.stringContaining('Message without metadata') + ); + }); + }); + + describe('info', () => { + it('should log info message', () => { + Logger.info('Info message'); + + expect(consoleInfoSpy).toHaveBeenCalledTimes(1); + expect(consoleInfoSpy).toHaveBeenCalledWith( + expect.stringContaining('Info message') + ); + }); + + it('should include environment in log', () => { + Logger.info('Test message'); + + expect(consoleInfoSpy).toHaveBeenCalledWith( + expect.stringContaining('[TEST') + ); + }); + + it('should include metadata', () => { + const metadata = { status: 'success', count: 42 }; + Logger.info('Operation completed', metadata); + + expect(consoleInfoSpy).toHaveBeenCalledWith( + expect.stringContaining(JSON.stringify(metadata)) + ); + }); + + it('should handle complex metadata', () => { + const metadata = { + user: { id: '123', name: 'Test' }, + items: [1, 2, 3], + active: true, + }; + Logger.info('Complex metadata', metadata); + + expect(consoleInfoSpy).toHaveBeenCalledWith( + expect.stringContaining(JSON.stringify(metadata)) + ); + }); + }); + + describe('warn', () => { + it('should log warn message', () => { + Logger.warn('Warning message'); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining('Warning message') + ); + }); + + it('should include environment in log', () => { + Logger.warn('Test warning'); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining('[TEST') + ); + }); + + it('should include metadata', () => { + const metadata = { error: 'deprecated_api', version: '1.0' }; + Logger.warn('Deprecated API usage', metadata); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining(JSON.stringify(metadata)) + ); + }); + }); + + describe('caller information', () => { + it('should extract caller information from stack trace', () => { + Logger.info('Test caller'); + + // Should include caller information in the format [ENV | caller] + expect(consoleInfoSpy).toHaveBeenCalledWith( + expect.stringMatching(/\[TEST \|.*\]/) + ); + }); + }); + + describe('metadata serialization', () => { + it('should handle empty metadata', () => { + Logger.info('Empty metadata', {}); + + expect(consoleInfoSpy).toHaveBeenCalledWith( + expect.stringContaining('{}') + ); + }); + + it('should handle undefined metadata', () => { + Logger.info('Undefined metadata', undefined); + + expect(consoleInfoSpy).toHaveBeenCalledWith( + expect.stringContaining('undefined') + ); + }); + + it('should handle nested objects', () => { + const metadata = { + level1: { + level2: { + level3: 'deep', + }, + }, + }; + Logger.info('Nested metadata', metadata); + + expect(consoleInfoSpy).toHaveBeenCalledWith( + expect.stringContaining(JSON.stringify(metadata)) + ); + }); + }); +}); diff --git a/stripe_collection_module/__tests__/utils/retry.test.ts b/stripe_collection_module/__tests__/utils/retry.test.ts new file mode 100644 index 0000000..2e76043 --- /dev/null +++ b/stripe_collection_module/__tests__/utils/retry.test.ts @@ -0,0 +1,372 @@ +/** + * Retry Utility Tests + */ + +import { + retryWithBackoff, + retryWithJitter, + retryForErrors, + retryForNetworkErrors, + sleep, +} from '../../code/utils/retry'; + +describe('Retry Utilities', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe('retryWithBackoff', () => { + it('should succeed on first attempt', async () => { + const operation = jest.fn().mockResolvedValue('success'); + + const promise = retryWithBackoff(operation); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result).toBe('success'); + expect(operation).toHaveBeenCalledTimes(1); + }); + + it('should retry and eventually succeed', async () => { + const operation = jest + .fn() + .mockRejectedValueOnce(new Error('Fail 1')) + .mockRejectedValueOnce(new Error('Fail 2')) + .mockResolvedValue('success'); + + const promise = retryWithBackoff(operation, { initialDelay: 100 }); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result).toBe('success'); + expect(operation).toHaveBeenCalledTimes(3); + }); + + it('should throw error after max retries', async () => { + const operation = jest.fn().mockRejectedValue(new Error('Always fails')); + + const promise = retryWithBackoff(operation, { + maxRetries: 2, + initialDelay: 100, + }); + + jest.runAllTimersAsync(); + + await expect(promise).rejects.toThrow('Always fails'); + expect(operation).toHaveBeenCalledTimes(3); + }); + + it('should use exponential backoff', async () => { + const operation = jest + .fn() + .mockRejectedValueOnce(new Error('Fail 1')) + .mockRejectedValueOnce(new Error('Fail 2')) + .mockResolvedValue('success'); + + const promise = retryWithBackoff(operation, { + initialDelay: 100, + backoffMultiplier: 2, + }); + + await jest.advanceTimersByTimeAsync(0); + expect(operation).toHaveBeenCalledTimes(1); + + await jest.advanceTimersByTimeAsync(100); + expect(operation).toHaveBeenCalledTimes(2); + + await jest.advanceTimersByTimeAsync(200); + expect(operation).toHaveBeenCalledTimes(3); + + const result = await promise; + expect(result).toBe('success'); + }); + + it('should respect maxDelay', async () => { + const operation = jest + .fn() + .mockRejectedValueOnce(new Error('Fail 1')) + .mockRejectedValueOnce(new Error('Fail 2')) + .mockResolvedValue('success'); + + const promise = retryWithBackoff(operation, { + initialDelay: 1000, + maxDelay: 1500, + backoffMultiplier: 3, + }); + + await jest.advanceTimersByTimeAsync(0); + expect(operation).toHaveBeenCalledTimes(1); + + await jest.advanceTimersByTimeAsync(1000); + expect(operation).toHaveBeenCalledTimes(2); + + await jest.advanceTimersByTimeAsync(1500); + expect(operation).toHaveBeenCalledTimes(3); + + const result = await promise; + expect(result).toBe('success'); + }); + + it('should call shouldRetry predicate', async () => { + const operation = jest + .fn() + .mockRejectedValueOnce(new Error('Retryable')) + .mockRejectedValueOnce(new Error('Not retryable')); + + const shouldRetry = jest.fn((error: Error) => { + return error.message === 'Retryable'; + }); + + const promise = retryWithBackoff(operation, { + shouldRetry, + initialDelay: 100, + }); + + jest.runAllTimersAsync(); + + await expect(promise).rejects.toThrow('Not retryable'); + expect(operation).toHaveBeenCalledTimes(2); + expect(shouldRetry).toHaveBeenCalledTimes(2); + }); + + it('should call onRetry callback', async () => { + const operation = jest + .fn() + .mockRejectedValueOnce(new Error('Fail 1')) + .mockRejectedValueOnce(new Error('Fail 2')) + .mockResolvedValue('success'); + + const onRetry = jest.fn(); + + const promise = retryWithBackoff(operation, { + onRetry, + initialDelay: 100, + }); + + await jest.runAllTimersAsync(); + await promise; + + expect(onRetry).toHaveBeenCalledTimes(2); + expect(onRetry).toHaveBeenNthCalledWith(1, 1, expect.any(Error)); + expect(onRetry).toHaveBeenNthCalledWith(2, 2, expect.any(Error)); + }); + + it('should use default options', async () => { + const operation = jest + .fn() + .mockRejectedValueOnce(new Error('Fail')) + .mockResolvedValue('success'); + + const promise = retryWithBackoff(operation); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result).toBe('success'); + }); + }); + + describe('sleep', () => { + it('should resolve after specified time', async () => { + const promise = sleep(1000); + + await jest.advanceTimersByTimeAsync(999); + expect(promise).not.toHaveProperty('resolvedValue'); + + await jest.advanceTimersByTimeAsync(1); + await promise; + }); + }); + + describe('retryWithJitter', () => { + it('should add jitter to retry delay', async () => { + const operation = jest + .fn() + .mockRejectedValueOnce(new Error('Fail')) + .mockResolvedValue('success'); + + const promise = retryWithJitter(operation, { initialDelay: 100 }); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result).toBe('success'); + expect(operation).toHaveBeenCalledTimes(2); + }); + + it('should call custom onRetry after jitter', async () => { + const operation = jest + .fn() + .mockRejectedValueOnce(new Error('Fail')) + .mockResolvedValue('success'); + + const customOnRetry = jest.fn(); + + const promise = retryWithJitter(operation, { + initialDelay: 100, + onRetry: customOnRetry, + }); + + await jest.runAllTimersAsync(); + await promise; + + expect(customOnRetry).toHaveBeenCalledTimes(1); + }); + }); + + describe('retryForErrors', () => { + it('should retry only for specified error codes', async () => { + const retryableError: any = new Error('Network error'); + retryableError.code = 'NETWORK_ERROR'; + + const nonRetryableError: any = new Error('Auth error'); + nonRetryableError.code = 'AUTH_ERROR'; + + const operation = jest + .fn() + .mockRejectedValueOnce(retryableError) + .mockRejectedValueOnce(nonRetryableError); + + const promise = retryForErrors( + operation, + ['NETWORK_ERROR', 'TIMEOUT'], + { initialDelay: 100 } + ); + + jest.runAllTimersAsync(); + + await expect(promise).rejects.toThrow('Auth error'); + expect(operation).toHaveBeenCalledTimes(2); + }); + + it('should check statusCode as string', async () => { + const error: any = new Error('Server error'); + error.statusCode = 500; + + const operation = jest + .fn() + .mockRejectedValueOnce(error) + .mockResolvedValue('success'); + + const promise = retryForErrors(operation, ['500'], { initialDelay: 100 }); + + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result).toBe('success'); + }); + + it('should not retry if error code not in list', async () => { + const error: any = new Error('Unknown error'); + error.code = 'UNKNOWN'; + + const operation = jest.fn().mockRejectedValue(error); + + const promise = retryForErrors(operation, ['NETWORK_ERROR']); + + jest.runAllTimersAsync(); + + await expect(promise).rejects.toThrow('Unknown error'); + expect(operation).toHaveBeenCalledTimes(1); + }); + }); + + describe('retryForNetworkErrors', () => { + it('should retry on ECONNREFUSED', async () => { + const error: any = new Error('Connection refused'); + error.code = 'ECONNREFUSED'; + + const operation = jest + .fn() + .mockRejectedValueOnce(error) + .mockResolvedValue('success'); + + const promise = retryForNetworkErrors(operation, { initialDelay: 100 }); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result).toBe('success'); + expect(operation).toHaveBeenCalledTimes(2); + }); + + it('should retry on ETIMEDOUT', async () => { + const error: any = new Error('Timeout'); + error.code = 'ETIMEDOUT'; + + const operation = jest + .fn() + .mockRejectedValueOnce(error) + .mockResolvedValue('success'); + + const promise = retryForNetworkErrors(operation, { initialDelay: 100 }); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result).toBe('success'); + }); + + it('should retry on ENOTFOUND', async () => { + const error: any = new Error('Not found'); + error.code = 'ENOTFOUND'; + + const operation = jest + .fn() + .mockRejectedValueOnce(error) + .mockResolvedValue('success'); + + const promise = retryForNetworkErrors(operation, { initialDelay: 100 }); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result).toBe('success'); + }); + + it('should retry on 5xx errors', async () => { + const error: any = new Error('Server error'); + error.statusCode = 503; + + const operation = jest + .fn() + .mockRejectedValueOnce(error) + .mockResolvedValue('success'); + + const promise = retryForNetworkErrors(operation, { initialDelay: 100 }); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result).toBe('success'); + }); + + it('should retry on 429 rate limit', async () => { + const error: any = new Error('Rate limited'); + error.statusCode = 429; + + const operation = jest + .fn() + .mockRejectedValueOnce(error) + .mockResolvedValue('success'); + + const promise = retryForNetworkErrors(operation, { initialDelay: 100 }); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result).toBe('success'); + }); + + it('should not retry on 4xx client errors (except 429)', async () => { + const error: any = new Error('Bad request'); + error.statusCode = 400; + + const operation = jest.fn().mockRejectedValue(error); + + const promise = retryForNetworkErrors(operation); + + jest.runAllTimersAsync(); + + await expect(promise).rejects.toThrow('Bad request'); + expect(operation).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/stripe_collection_module/__tests__/utils/timeout.test.ts b/stripe_collection_module/__tests__/utils/timeout.test.ts new file mode 100644 index 0000000..3e0b4d4 --- /dev/null +++ b/stripe_collection_module/__tests__/utils/timeout.test.ts @@ -0,0 +1,250 @@ +/** + * Timeout Utility Tests + */ + +import { + withTimeout, + withTimeoutFallback, + withTimeoutError, + TimeoutError, +} from '../../code/utils/timeout'; + +describe('Timeout Utilities', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe('withTimeout', () => { + it('should return result if operation completes before timeout', async () => { + const operation = new Promise((resolve) => { + setTimeout(() => resolve('success'), 100); + }); + + const promise = withTimeout(operation, 200); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result).toBe('success'); + }); + + it('should throw timeout error if operation exceeds timeout', async () => { + const operation = new Promise((resolve) => { + setTimeout(() => resolve('success'), 300); + }); + + const promise = withTimeout(operation, 100); + + jest.runAllTimersAsync(); + + await expect(promise).rejects.toThrow('Operation timeout'); + }); + + it('should propagate operation errors', async () => { + const operation = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Operation failed')), 50); + }); + + const promise = withTimeout(operation, 200); + + jest.runAllTimersAsync(); + + await expect(promise).rejects.toThrow('Operation failed'); + }); + + it('should handle immediately resolved promise', async () => { + const operation = Promise.resolve('immediate'); + + const result = await withTimeout(operation, 100); + + expect(result).toBe('immediate'); + }); + + it('should handle immediately rejected promise', async () => { + const operation = Promise.reject(new Error('immediate error')); + + await expect(withTimeout(operation, 100)).rejects.toThrow( + 'immediate error' + ); + }); + }); + + describe('withTimeoutFallback', () => { + it('should return result if operation completes before timeout', async () => { + const operation = new Promise((resolve) => { + setTimeout(() => resolve('success'), 100); + }); + + const promise = withTimeoutFallback(operation, 200, 'fallback'); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result).toBe('success'); + }); + + it('should return fallback value on timeout', async () => { + const operation = new Promise((resolve) => { + setTimeout(() => resolve('success'), 300); + }); + + const promise = withTimeoutFallback(operation, 100, 'fallback'); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result).toBe('fallback'); + }); + + it('should return fallback object on timeout', async () => { + const operation = new Promise((resolve) => { + setTimeout(() => resolve({ data: 'real' }), 300); + }); + + const fallback = { data: 'fallback' }; + const promise = withTimeoutFallback(operation, 100, fallback); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result).toEqual(fallback); + }); + + it('should propagate operation errors', async () => { + const operation = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Operation failed')), 50); + }); + + const promise = withTimeoutFallback(operation, 200, 'fallback'); + + jest.runAllTimersAsync(); + + await expect(promise).rejects.toThrow('Operation failed'); + }); + + it('should handle null fallback value', async () => { + const operation = new Promise((resolve) => { + setTimeout(() => resolve('success'), 300); + }); + + const promise = withTimeoutFallback(operation, 100, null); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result).toBeNull(); + }); + + it('should handle undefined fallback value', async () => { + const operation = new Promise((resolve) => { + setTimeout(() => resolve('success'), 300); + }); + + const promise = withTimeoutFallback(operation, 100, undefined as any); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result).toBeUndefined(); + }); + }); + + describe('TimeoutError', () => { + it('should create error with timeout info', () => { + const error = new TimeoutError('Custom timeout', 5000, 'fetchData'); + + expect(error).toBeInstanceOf(Error); + expect(error.name).toBe('TimeoutError'); + expect(error.message).toBe('Custom timeout'); + expect(error.timeoutMs).toBe(5000); + expect(error.operation).toBe('fetchData'); + }); + + it('should create error without operation name', () => { + const error = new TimeoutError('Timeout occurred', 3000); + + expect(error.timeoutMs).toBe(3000); + expect(error.operation).toBeUndefined(); + }); + }); + + describe('withTimeoutError', () => { + it('should return result if operation completes', async () => { + const operation = new Promise((resolve) => { + setTimeout(() => resolve('success'), 100); + }); + + const promise = withTimeoutError(operation, 200, 'Timeout', 'testOp'); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result).toBe('success'); + }); + + it('should throw TimeoutError on timeout', async () => { + const operation = new Promise((resolve) => { + setTimeout(() => resolve('success'), 300); + }); + + const promise = withTimeoutError( + operation, + 100, + 'Custom timeout message', + 'myOperation' + ); + + jest.runAllTimersAsync(); + + await expect(promise).rejects.toThrow(TimeoutError); + await expect(promise).rejects.toThrow('Custom timeout message'); + + try { + await promise; + } catch (error) { + expect((error as TimeoutError).timeoutMs).toBe(100); + expect((error as TimeoutError).operation).toBe('myOperation'); + } + }); + + it('should use default message if not provided', async () => { + const operation = new Promise((resolve) => { + setTimeout(() => resolve('success'), 300); + }); + + const promise = withTimeoutError(operation, 100); + + jest.runAllTimersAsync(); + + await expect(promise).rejects.toThrow(TimeoutError); + await expect(promise).rejects.toThrow('Operation timeout'); + }); + + it('should propagate operation errors', async () => { + const operation = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Operation failed')), 50); + }); + + const promise = withTimeoutError(operation, 200, 'Timeout'); + + jest.runAllTimersAsync(); + + await expect(promise).rejects.toThrow('Operation failed'); + }); + + it('should work without operation name', async () => { + const operation = new Promise((resolve) => { + setTimeout(() => resolve('success'), 300); + }); + + const promise = withTimeoutError(operation, 100, 'Timed out'); + + jest.runAllTimersAsync(); + + await expect(promise).rejects.toThrow(TimeoutError); + + try { + await promise; + } catch (error) { + expect((error as TimeoutError).operation).toBeUndefined(); + } + }); + }); +}); diff --git a/stripe_collection_module/__tests__/webhook-hooks.test.ts b/stripe_collection_module/__tests__/webhook-hooks.test.ts new file mode 100644 index 0000000..3f8746b --- /dev/null +++ b/stripe_collection_module/__tests__/webhook-hooks.test.ts @@ -0,0 +1,498 @@ +/** + * Webhook Hooks Tests + * + * Tests for the Stripe webhook handler + */ + +import * as crypto from 'crypto'; +import { processWebhookRequest } from '../code/webhook-hooks'; +import { getContainer } from '../code/core/container.setup'; +import { ServiceToken } from '../code/core/container'; +import { StripeEvents } from '../code/interfaces/stripe-events'; +import { createMockLogService } from './test-helpers'; + +// Mock dependencies +jest.mock('../code/core/container.setup'); +jest.mock('../code/services/config-instance', () => ({ + getConfigService: jest.fn(() => ({ + get: jest.fn((key: string) => { + if (key === 'stripeWebhookSigningSecret') return 'whsec_test_secret'; + return null; + }), + })), +})); + +const { getConfigService } = require('../code/services/config-instance'); + +describe('Webhook Hooks', () => { + let mockContainer: any; + let mockLogService: ReturnType; + let mockInvoicePaidController: any; + + beforeEach(() => { + jest.clearAllMocks(); + + mockLogService = createMockLogService(); + mockInvoicePaidController = { + handle: jest.fn().mockResolvedValue(undefined), + }; + + mockContainer = { + resolve: jest.fn((token: symbol) => { + if (token === ServiceToken.LOG_SERVICE) return mockLogService; + if (token === ServiceToken.INVOICE_PAID_CONTROLLER) + return mockInvoicePaidController; + return null; + }), + }; + + (getContainer as jest.Mock).mockReturnValue(mockContainer); + + // Reset config mock to return fresh instance each time + (getConfigService as jest.Mock).mockReturnValue({ + get: jest.fn((key: string) => { + if (key === 'stripeWebhookSigningSecret') return 'whsec_test_secret'; + return null; + }), + }); + }); + + describe('processWebhookRequest', () => { + const createValidSignature = (body: string, timestamp: number): string => { + const webhookSecret = 'whsec_test_secret'; + const signedPayload = `${timestamp}.${body}`; + const signature = crypto + .createHmac('sha256', webhookSecret) + .update(signedPayload) + .digest('hex'); + return `t=${timestamp},v1=${signature}`; + }; + + const createWebhookRequest = ( + event: any, + signature?: string + ): any => { + const body = JSON.stringify(event); + const timestamp = Math.floor(Date.now() / 1000); + const validSignature = signature || createValidSignature(body, timestamp); + + return { + request: { + headers: { + 'stripe-signature': validSignature, + }, + body: Buffer.from(body), + }, + }; + }; + + describe('Signature Verification', () => { + it('should reject request with missing signature', async () => { + const event = { + id: 'evt_test', + type: StripeEvents.InvoicePaid, + data: { object: { id: 'in_test' } }, + }; + + const request = { + request: { + headers: {}, + body: Buffer.from(JSON.stringify(event)), + }, + }; + + const result = await processWebhookRequest(request); + + expect(result.response.status).toBe(403); + expect(JSON.parse(result.response.body)).toEqual({ + error: 'Invalid signature', + }); + expect(mockLogService.warn).toHaveBeenCalledWith( + 'Webhook signature verification failed', + 'WebhookHandler' + ); + }); + + it('should reject request with invalid signature format', async () => { + const event = { + id: 'evt_test', + type: StripeEvents.InvoicePaid, + data: { object: { id: 'in_test' } }, + }; + + const request = { + request: { + headers: { + 'stripe-signature': 'invalid_signature', + }, + body: Buffer.from(JSON.stringify(event)), + }, + }; + + const result = await processWebhookRequest(request); + + expect(result.response.status).toBe(403); + expect(JSON.parse(result.response.body)).toEqual({ + error: 'Invalid signature', + }); + }); + + it('should reject request with mismatched signature', async () => { + const event = { + id: 'evt_test', + type: StripeEvents.InvoicePaid, + data: { object: { id: 'in_test' } }, + }; + + const request = createWebhookRequest(event, 't=123456789,v1=wrongsig'); + + const result = await processWebhookRequest(request); + + expect(result.response.status).toBe(403); + expect(JSON.parse(result.response.body)).toEqual({ + error: 'Invalid signature', + }); + }); + + it('should accept request with valid signature', async () => { + const event = { + id: 'evt_test', + type: StripeEvents.InvoicePaid, + data: { + object: { + id: 'in_test', + amount_due: 1000, + metadata: {}, + }, + }, + }; + + const request = createWebhookRequest(event); + const result = await processWebhookRequest(request); + + expect(result.response.status).toBe(200); + expect(JSON.parse(result.response.body)).toEqual({ received: true }); + }); + }); + + describe('Event Routing', () => { + it('should route invoice.paid events to InvoicePaidController', async () => { + const invoice = { + id: 'in_test123', + amount_due: 5000, + currency: 'usd', + metadata: { + payment_mappings: JSON.stringify([ + { policy_id: 'pol_123', payment_id: 'pay_123' }, + ]), + }, + }; + + const event = { + id: 'evt_test', + type: StripeEvents.InvoicePaid, + data: { object: invoice }, + }; + + const request = createWebhookRequest(event); + const result = await processWebhookRequest(request); + + expect(mockLogService.info).toHaveBeenCalledWith( + 'Received Stripe webhook', + 'WebhookHandler', + { + eventType: StripeEvents.InvoicePaid, + eventId: 'evt_test', + } + ); + expect(mockInvoicePaidController.handle).toHaveBeenCalledWith(invoice); + expect(result.response.status).toBe(200); + expect(JSON.parse(result.response.body)).toEqual({ received: true }); + }); + + it('should log warning for unhandled event types', async () => { + const event = { + id: 'evt_test', + type: 'customer.created', + data: { object: { id: 'cus_test' } }, + }; + + const request = createWebhookRequest(event); + const result = await processWebhookRequest(request); + + expect(mockLogService.warn).toHaveBeenCalledWith( + 'Unhandled Stripe event type', + 'WebhookHandler', + { + eventType: 'customer.created', + } + ); + expect(result.response.status).toBe(200); + expect(JSON.parse(result.response.body)).toEqual({ received: true }); + }); + }); + + describe('Error Handling', () => { + it('should handle controller errors gracefully', async () => { + mockInvoicePaidController.handle.mockRejectedValue( + new Error('Controller processing failed') + ); + + const event = { + id: 'evt_test', + type: StripeEvents.InvoicePaid, + data: { + object: { + id: 'in_test', + amount_due: 1000, + metadata: {}, + }, + }, + }; + + const request = createWebhookRequest(event); + const result = await processWebhookRequest(request); + + expect(mockLogService.error).toHaveBeenCalledWith( + 'Error processing webhook', + 'WebhookHandler', + { + error: 'Controller processing failed', + } + ); + expect(result.response.status).toBe(500); + expect(JSON.parse(result.response.body)).toEqual({ + error: 'Internal server error', + }); + }); + + it('should handle JSON parsing errors', async () => { + const request = { + request: { + headers: { + 'stripe-signature': 't=123456789,v1=abcdef', + }, + body: Buffer.from('invalid json'), + }, + }; + + // Need to create a valid signature for invalid JSON + const timestamp = 123456789; + const webhookSecret = 'whsec_test_secret'; + const signedPayload = `${timestamp}.invalid json`; + const signature = crypto + .createHmac('sha256', webhookSecret) + .update(signedPayload) + .digest('hex'); + + request.request.headers['stripe-signature'] = `t=${timestamp},v1=${signature}`; + + const result = await processWebhookRequest(request); + + expect(mockLogService.error).toHaveBeenCalledWith( + 'Error processing webhook', + 'WebhookHandler', + expect.objectContaining({ + error: expect.stringContaining('JSON'), + }) + ); + expect(result.response.status).toBe(500); + }); + + it('should handle non-Error objects thrown', async () => { + mockInvoicePaidController.handle.mockRejectedValue('String error'); + + const event = { + id: 'evt_test', + type: StripeEvents.InvoicePaid, + data: { + object: { + id: 'in_test', + amount_due: 1000, + metadata: {}, + }, + }, + }; + + const request = createWebhookRequest(event); + const result = await processWebhookRequest(request); + + expect(result.response.status).toBe(500); + expect(JSON.parse(result.response.body)).toEqual({ + error: 'Internal server error', + }); + }); + }); + + describe('Response Format', () => { + it('should return proper response headers for success', async () => { + const event = { + id: 'evt_test', + type: 'customer.created', + data: { object: { id: 'cus_test' } }, + }; + + const request = createWebhookRequest(event); + const result = await processWebhookRequest(request); + + expect(result.response).toEqual({ + status: 200, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ received: true }), + }); + }); + + it('should return proper response headers for signature failure', async () => { + const event = { + id: 'evt_test', + type: StripeEvents.InvoicePaid, + data: { object: { id: 'in_test' } }, + }; + + const request = { + request: { + headers: {}, + body: Buffer.from(JSON.stringify(event)), + }, + }; + + const result = await processWebhookRequest(request); + + expect(result.response).toEqual({ + status: 403, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ error: 'Invalid signature' }), + }); + }); + + it('should return proper response headers for errors', async () => { + mockInvoicePaidController.handle.mockRejectedValue(new Error('Test')); + + const event = { + id: 'evt_test', + type: StripeEvents.InvoicePaid, + data: { + object: { + id: 'in_test', + amount_due: 1000, + metadata: {}, + }, + }, + }; + + const request = createWebhookRequest(event); + const result = await processWebhookRequest(request); + + expect(result.response).toEqual({ + status: 500, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ error: 'Internal server error' }), + }); + }); + }); + + describe('Signature Parsing Edge Cases', () => { + it('should handle signature with multiple v1 values (use last)', async () => { + const event = { + id: 'evt_test', + type: StripeEvents.InvoicePaid, + data: { + object: { + id: 'in_test', + amount_due: 1000, + metadata: {}, + }, + }, + }; + + const body = JSON.stringify(event); + const timestamp = Math.floor(Date.now() / 1000); + const webhookSecret = 'whsec_test_secret'; + const signedPayload = `${timestamp}.${body}`; + const validSig = crypto + .createHmac('sha256', webhookSecret) + .update(signedPayload) + .digest('hex'); + + // The signature parsing overwrites with the last v1 value + // So put the valid signature last + const request = { + request: { + headers: { + 'stripe-signature': `t=${timestamp},v1=wrongsig,v1=${validSig}`, + }, + body: Buffer.from(body), + }, + }; + + const result = await processWebhookRequest(request); + + expect(result.response.status).toBe(200); + }); + + it('should reject signature with missing timestamp', async () => { + const event = { + id: 'evt_test', + type: StripeEvents.InvoicePaid, + data: { object: { id: 'in_test' } }, + }; + + const request = { + request: { + headers: { + 'stripe-signature': 'v1=somesignature', + }, + body: Buffer.from(JSON.stringify(event)), + }, + }; + + const result = await processWebhookRequest(request); + + expect(result.response.status).toBe(403); + }); + + it('should reject signature with missing v1', async () => { + const event = { + id: 'evt_test', + type: StripeEvents.InvoicePaid, + data: { object: { id: 'in_test' } }, + }; + + const request = { + request: { + headers: { + 'stripe-signature': 't=123456789', + }, + body: Buffer.from(JSON.stringify(event)), + }, + }; + + const result = await processWebhookRequest(request); + + expect(result.response.status).toBe(403); + }); + + it('should handle signature comparison with different lengths gracefully', async () => { + const event = { + id: 'evt_test', + type: StripeEvents.InvoicePaid, + data: { object: { id: 'in_test' } }, + }; + + const request = { + request: { + headers: { + 'stripe-signature': 't=123456789,v1=short', + }, + body: Buffer.from(JSON.stringify(event)), + }, + }; + + const result = await processWebhookRequest(request); + + expect(result.response.status).toBe(403); + }); + }); + }); +}); + diff --git a/stripe_collection_module/code/lifecycle-hooks/index.ts b/stripe_collection_module/code/lifecycle-hooks/index.ts index 603b0d1..37d9fc2 100644 --- a/stripe_collection_module/code/lifecycle-hooks/index.ts +++ b/stripe_collection_module/code/lifecycle-hooks/index.ts @@ -2,339 +2,32 @@ * Lifecycle Hooks * * These are called by the Root platform at various points in the policy lifecycle. - * This is a simplified stub implementation with clear extension points. * - * Full implementation will be added when the Stripe/Root integration is built. - */ - -import { RenderService } from '../services/render.service'; -import { getLogService } from '../services/log-instance'; -import { getConfigService } from '../services/config-instance'; -import StripeClient from '../clients/stripe-client'; - -const renderService = new RenderService(); -const stripeClient = new StripeClient(); - -/** - * Called after a policy is issued - * - * TODO: Implement policy issued logic - */ -export function afterPolicyIssued(): void { - // Stub implementation -} - -/** - * Create payment method - returns module data structure - * - * This is called when a setup intent is completed. - */ -export function createPaymentMethod({ - data, -}: { - data?: { setupIntent?: any }; -}): { module: any } { - const logService = getLogService(); - logService.info('Creating payment method', 'createPaymentMethod', data); - - if (data?.setupIntent) { - return { - module: { - id: data.setupIntent.id, - usage: data.setupIntent.usage, - object: data.setupIntent.object, - status: data.setupIntent.status, - livemode: data.setupIntent.livemode, - payment_method: data.setupIntent.payment_method, - }, - }; - } - - return { - module: data, - }; -} - -/** - * Render payment method creation form - * - * Returns HTML form with Stripe Elements for capturing payment details. - */ -export async function renderCreatePaymentMethod(): Promise { - const logService = getLogService(); - logService.info( - 'Rendering payment method creation form', - 'renderCreatePaymentMethod' - ); - - try { - // Create Stripe setup intent - const setupIntent = await stripeClient.stripeSDK.setupIntents.create({}); - - if (!setupIntent.client_secret) { - throw new Error('Setup intent client secret is missing'); - } - - // Render form using RenderService - const config = getConfigService(); - return renderService.renderCreatePaymentMethod({ - stripePublishableKey: config.get('stripePublishableKey'), - setupIntentClientSecret: setupIntent.client_secret, - }); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : 'Unknown error'; - logService.error( - 'Error rendering payment method form', - 'renderCreatePaymentMethod', - {}, - error as Error - ); - throw new Error(`Error creating setup intent: ${errorMessage}`); - } -} - -/** - * Render payment method summary view (compact card) - * - * Returns HTML showing a brief summary of the payment method. - */ -export async function renderViewPaymentMethodSummary(params: { - payment_method: any; -}): Promise { - const logService = getLogService(); - logService.info( - 'Rendering payment method summary', - 'renderViewPaymentMethodSummary' - ); - - try { - const { payment_method: paymentMethod } = params; - - // Get payment method details from Stripe - const paymentMethodDetails = - await stripeClient.stripeSDK.paymentMethods.retrieve( - paymentMethod?.module?.payment_method as string - ); - - return renderService.renderViewPaymentMethodSummary({ - payment_method: paymentMethod, - paymentMethodDetails: { - card: paymentMethodDetails.card, - }, - }); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : 'Unknown error'; - logService.error( - 'Error rendering payment method summary', - 'renderViewPaymentMethodSummary', - {}, - error as Error - ); - throw new Error(`Error retrieving payment method: ${errorMessage}`); - } -} - -/** - * Render full payment method details view - * - * Returns HTML showing comprehensive payment method information. - */ -export function renderViewPaymentMethod(params: any): string { - const logService = getLogService(); - logService.info( - 'Rendering payment method details', - 'renderViewPaymentMethod' - ); - - const { payment_method: paymentMethod, policy } = params; - - return renderService.renderViewPaymentMethod({ - payment_method: paymentMethod, - policy, - }); -} - -/** - * Called after payment method is assigned to a policy - * - * TODO: Implement full payment method assignment workflow: - * - Get or create Stripe customer - * - Attach payment method to customer - * - Create or update subscription - */ -export function afterPolicyPaymentMethodAssigned({ - policy, -}: { - policy: any; -}): void { - const logService = getLogService(); - logService.info( - 'Payment method assigned to policy', - 'afterPolicyPaymentMethodAssigned', - { - policyId: policy.policy_id, - } - ); - - // TODO: Implement payment method assignment logic - // This will use PaymentMethodService to orchestrate the workflow -} - -/** - * Called after a payment is created - * - * TODO: Implement payment creation handling: - * - Create Stripe payment intent - * - Link to Root payment - */ -export function afterPaymentCreated({ - policy, - payment, -}: { - policy: any; - payment: any; -}): void { - const logService = getLogService(); - logService.info('Payment created', 'afterPaymentCreated', { - policyId: policy.policy_id, - paymentId: payment.payment_id, - }); - - // TODO: Implement payment creation logic -} - -/** - * Called after a payment is updated - * - * TODO: Implement payment update handling - */ -export function afterPaymentUpdated({ - policy, - payment, -}: { - policy: any; - payment: any; -}): void { - const logService = getLogService(); - logService.info('Payment updated', 'afterPaymentUpdated', { - policyId: policy.policy_id, - paymentId: payment.payment_id, - }); - - // TODO: Implement payment update logic -} - -/** - * Called after payment method is removed from policy - * - * TODO: Implement payment method removal: - * - Cancel subscriptions - * - Clean up Stripe resources - */ -export function afterPaymentMethodRemoved({ policy }: { policy: any }): void { - const logService = getLogService(); - logService.info('Payment method removed', 'afterPaymentMethodRemoved', { - policyId: policy.policy_id, - }); - - // TODO: Implement payment method removal logic -} - -/** - * Called after policy is cancelled - * - * TODO: Implement policy cancellation: - * - Cancel Stripe subscriptions - * - Handle refunds if needed - */ -export function afterPolicyCancelled({ policy }: { policy: any }): void { - const logService = getLogService(); - logService.info('Policy cancelled', 'afterPolicyCancelled', { - policyId: policy.policy_id, - }); - - // TODO: Implement policy cancellation logic -} - -/** - * Called after policy expires - * - * TODO: Implement policy expiration handling - */ -export function afterPolicyExpired({ policy }: { policy: any }): void { - const logService = getLogService(); - logService.info('Policy expired', 'afterPolicyExpired', { - policyId: policy.policy_id, - }); - - // TODO: Implement policy expiration logic -} - -/** - * Called after policy lapses - * - * TODO: Implement policy lapse handling - */ -export function afterPolicyLapsed({ policy }: { policy: any }): void { - const logService = getLogService(); - logService.info('Policy lapsed', 'afterPolicyLapsed', { - policyId: policy.policy_id, - }); - - // TODO: Implement policy lapse logic -} - -/** - * Called after policy is updated - * - * TODO: Implement policy update handling: - * - Update subscription amounts - * - Handle proration - */ -export function afterPolicyUpdated({ - policy, - updates, -}: { - policy: any; - updates: any; -}): void { - const logService = getLogService(); - logService.info('Policy updated', 'afterPolicyUpdated', { - policyId: policy.policy_id, - updates, - }); - - // TODO: Implement policy update logic -} - -/** - * Called after alteration package is applied - * - * TODO: Implement alteration handling: - * - Update subscription pricing - * - Handle billing frequency changes - */ -export function afterAlterationPackageApplied({ - policy, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - alteration_package, - alteration_hook_key, -}: { - policy: any; - alteration_package: any; - alteration_hook_key: string; -}): void { - const logService = getLogService(); - logService.info( - 'Alteration package applied', - 'afterAlterationPackageApplied', - { - policyId: policy.policy_id, - alterationHookKey: alteration_hook_key, - } - ); - - // TODO: Implement alteration logic -} + * Architecture: + * - Uses dependency injection via getContainer() + * - Organized by domain (payment methods, payments, policy) + * - Each hook resolves services as needed + */ + +// Payment Method Hooks +export { + createPaymentMethod, + renderCreatePaymentMethod, + renderViewPaymentMethodSummary, + renderViewPaymentMethod, + afterPolicyPaymentMethodAssigned, + afterPaymentMethodRemoved, +} from './payment-method.hooks'; + +// Payment Hooks +export { afterPaymentCreated, afterPaymentUpdated } from './payment.hooks'; + +// Policy Hooks +export { + afterPolicyIssued, + afterPolicyUpdated, + afterPolicyCancelled, + afterPolicyExpired, + afterPolicyLapsed, + afterAlterationPackageApplied, +} from './policy.hooks'; diff --git a/stripe_collection_module/code/lifecycle-hooks/payment-method.hooks.ts b/stripe_collection_module/code/lifecycle-hooks/payment-method.hooks.ts new file mode 100644 index 0000000..03994d9 --- /dev/null +++ b/stripe_collection_module/code/lifecycle-hooks/payment-method.hooks.ts @@ -0,0 +1,199 @@ +/** + * Payment Method Lifecycle Hooks + * + * Handles payment method creation, viewing, and assignment + */ + +import { getContainer } from '../core/container.setup'; +import { ServiceToken } from '../core/container'; +import { LogService } from '../services/log.service'; +import { RenderService } from '../services/render.service'; +import { ConfigurationService } from '../services/config.service'; +import StripeClient from '../clients/stripe-client'; + +/** + * Create payment method - returns module data structure + * + * This is called when a setup intent is completed. + */ +export function createPaymentMethod({ + data, +}: { + data?: { setupIntent?: any }; +}): { module: any } { + const container = getContainer(); + const logService = container.resolve(ServiceToken.LOG_SERVICE); + + logService.info('Creating payment method', 'createPaymentMethod', data); + + if (data?.setupIntent) { + return { + module: { + id: data.setupIntent.id, + usage: data.setupIntent.usage, + object: data.setupIntent.object, + status: data.setupIntent.status, + livemode: data.setupIntent.livemode, + payment_method: data.setupIntent.payment_method, + }, + }; + } + + return { + module: data, + }; +} + +/** + * Render payment method creation form + * + * Returns HTML form with Stripe Elements for capturing payment details. + */ +export async function renderCreatePaymentMethod(): Promise { + const container = getContainer(); + const logService = container.resolve(ServiceToken.LOG_SERVICE); + const renderService = container.resolve( + ServiceToken.RENDER_SERVICE + ); + const configService = container.resolve( + ServiceToken.CONFIG_SERVICE + ); + const stripeClient = container.resolve( + ServiceToken.STRIPE_CLIENT + ); + + logService.info( + 'Rendering payment method creation form', + 'renderCreatePaymentMethod' + ); + + try { + // Create Stripe setup intent + const setupIntent = await stripeClient.stripeSDK.setupIntents.create({}); + + if (!setupIntent.client_secret) { + throw new Error('Setup intent client secret is missing'); + } + + // Render form using RenderService + return renderService.renderCreatePaymentMethod({ + stripePublishableKey: configService.get('stripePublishableKey'), + setupIntentClientSecret: setupIntent.client_secret, + }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + logService.error( + 'Error rendering payment method form', + 'renderCreatePaymentMethod', + {}, + error as Error + ); + throw new Error(`Error creating setup intent: ${errorMessage}`); + } +} + +/** + * Render payment method summary view (compact card) + * + * Returns HTML showing a brief summary of the payment method. + */ +export async function renderViewPaymentMethodSummary(params: { + payment_method: any; +}): Promise { + const container = getContainer(); + const logService = container.resolve(ServiceToken.LOG_SERVICE); + const renderService = container.resolve( + ServiceToken.RENDER_SERVICE + ); + const stripeClient = container.resolve( + ServiceToken.STRIPE_CLIENT + ); + + logService.info( + 'Rendering payment method summary', + 'renderViewPaymentMethodSummary' + ); + + try { + const { payment_method: paymentMethod } = params; + + if (!paymentMethod) { + return '
No payment method found
'; + } + + // Retrieve payment method from Stripe + const stripePaymentMethod = + await stripeClient.stripeSDK.paymentMethods.retrieve( + paymentMethod as string + ); + + return renderService.renderViewPaymentMethodSummary({ + payment_method: { + module: { + payment_method: stripePaymentMethod.id, + }, + }, + }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + logService.error( + 'Error rendering payment method summary', + 'renderViewPaymentMethodSummary', + {}, + error as Error + ); + throw new Error(`Error retrieving payment method: ${errorMessage}`); + } +} + +/** + * Render detailed payment method view + * + * TODO: Implement full payment method details view + */ +export function renderViewPaymentMethod(): string { + // Stub - will be implemented when needed + return ''; +} + +/** + * Called after a payment method is assigned to a policy + * + * TODO: Implement payment method assignment logic + */ +export function afterPolicyPaymentMethodAssigned({ + policy, +}: { + policy: any; +}): void { + const container = getContainer(); + const logService = container.resolve(ServiceToken.LOG_SERVICE); + + logService.info( + 'Payment method assigned to policy', + 'afterPolicyPaymentMethodAssigned', + { policyId: policy.policy_id } + ); + + // Stub - implement your logic here +} + +/** + * Called after a payment method is removed from a policy + * + * TODO: Implement payment method removal logic + */ +export function afterPaymentMethodRemoved({ policy }: { policy: any }): void { + const container = getContainer(); + const logService = container.resolve(ServiceToken.LOG_SERVICE); + + logService.info( + 'Payment method removed from policy', + 'afterPaymentMethodRemoved', + { policyId: policy.policy_id } + ); + + // Stub - implement your logic here +} diff --git a/stripe_collection_module/code/lifecycle-hooks/payment.hooks.ts b/stripe_collection_module/code/lifecycle-hooks/payment.hooks.ts new file mode 100644 index 0000000..21433eb --- /dev/null +++ b/stripe_collection_module/code/lifecycle-hooks/payment.hooks.ts @@ -0,0 +1,55 @@ +/** + * Payment Lifecycle Hooks + * + * Handles payment creation and updates + */ + +import { getContainer } from '../core/container.setup'; +import { ServiceToken } from '../core/container'; +import { LogService } from '../services/log.service'; + +/** + * Called after a payment is created + * + * TODO: Implement payment creation logic + */ +export function afterPaymentCreated({ + policy, + payment, +}: { + policy: any; + payment: any; +}): void { + const container = getContainer(); + const logService = container.resolve(ServiceToken.LOG_SERVICE); + + logService.info('Payment created', 'afterPaymentCreated', { + policyId: policy.policy_id, + paymentId: payment.payment_id, + }); + + // Stub - implement your logic here +} + +/** + * Called after a payment is updated + * + * TODO: Implement payment update logic + */ +export function afterPaymentUpdated({ + policy, + payment, +}: { + policy: any; + payment: any; +}): void { + const container = getContainer(); + const logService = container.resolve(ServiceToken.LOG_SERVICE); + + logService.info('Payment updated', 'afterPaymentUpdated', { + policyId: policy.policy_id, + paymentId: payment.payment_id, + }); + + // Stub - implement your logic here +} diff --git a/stripe_collection_module/code/lifecycle-hooks/policy.hooks.ts b/stripe_collection_module/code/lifecycle-hooks/policy.hooks.ts new file mode 100644 index 0000000..cfc8f55 --- /dev/null +++ b/stripe_collection_module/code/lifecycle-hooks/policy.hooks.ts @@ -0,0 +1,124 @@ +/** + * Policy Lifecycle Hooks + * + * Handles various policy lifecycle events + */ + +import { getContainer } from '../core/container.setup'; +import { ServiceToken } from '../core/container'; +import { LogService } from '../services/log.service'; + +/** + * Called after a policy is issued + * + * TODO: Implement policy issued logic + */ +export function afterPolicyIssued(): void { + const container = getContainer(); + const logService = container.resolve(ServiceToken.LOG_SERVICE); + + logService.info('Policy issued', 'afterPolicyIssued'); + + // Stub - implement your logic here +} + +/** + * Called after a policy is updated + * + * TODO: Implement policy update logic + */ +export function afterPolicyUpdated({ + policy, + updates, +}: { + policy: any; + updates: any; +}): void { + const container = getContainer(); + const logService = container.resolve(ServiceToken.LOG_SERVICE); + + logService.info('Policy updated', 'afterPolicyUpdated', { + policyId: policy.policy_id, + updates, + }); + + // Stub - implement your logic here +} + +/** + * Called after a policy is cancelled + * + * TODO: Implement policy cancellation logic + */ +export function afterPolicyCancelled({ policy }: { policy: any }): void { + const container = getContainer(); + const logService = container.resolve(ServiceToken.LOG_SERVICE); + + logService.info('Policy cancelled', 'afterPolicyCancelled', { + policyId: policy.policy_id, + }); + + // Stub - implement your logic here +} + +/** + * Called after a policy expires + * + * TODO: Implement policy expiration logic + */ +export function afterPolicyExpired({ policy }: { policy: any }): void { + const container = getContainer(); + const logService = container.resolve(ServiceToken.LOG_SERVICE); + + logService.info('Policy expired', 'afterPolicyExpired', { + policyId: policy.policy_id, + }); + + // Stub - implement your logic here +} + +/** + * Called after a policy lapses + * + * TODO: Implement policy lapse logic + */ +export function afterPolicyLapsed({ policy }: { policy: any }): void { + const container = getContainer(); + const logService = container.resolve(ServiceToken.LOG_SERVICE); + + logService.info('Policy lapsed', 'afterPolicyLapsed', { + policyId: policy.policy_id, + }); + + // Stub - implement your logic here +} + +/** + * Called after an alteration package is applied to a policy + * + * TODO: Implement alteration package logic + */ +export function afterAlterationPackageApplied({ + policy, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + alteration_package, + alteration_hook_key, +}: { + policy: any; + alteration_package: any; + alteration_hook_key: string; +}): void { + const container = getContainer(); + const logService = container.resolve(ServiceToken.LOG_SERVICE); + + logService.info( + 'Alteration package applied', + 'afterAlterationPackageApplied', + { + policyId: policy.policy_id, + alterationHookKey: alteration_hook_key, + } + ); + + // Stub - implement your logic here +} diff --git a/stripe_collection_module/code/webhook-hooks.ts b/stripe_collection_module/code/webhook-hooks.ts index 5e57445..8f9c4ea 100644 --- a/stripe_collection_module/code/webhook-hooks.ts +++ b/stripe_collection_module/code/webhook-hooks.ts @@ -2,12 +2,12 @@ * Webhook Handler for Stripe Events * * This module handles incoming webhook requests from Stripe. - * It verifies signatures, validates events, and routes them to appropriate controllers. + * It verifies signatures and routes events to appropriate controllers. * * Architecture: - * - Uses dependency injection for controller resolution - * - Delegates to controllers for event processing - * - Focuses on routing and authentication only + * - Signature verification + * - Event routing via dependency injection + * - Minimal logic - delegates to controllers */ import * as crypto from 'crypto'; @@ -18,36 +18,25 @@ import { LogService } from './services/log.service'; import { StripeEvents } from './interfaces/stripe-events'; import { InvoicePaidController } from './controllers/stripe-event-processors/invoice-paid.controller'; import { getConfigService } from './services/config-instance'; -import ModuleError from './utils/error'; -import rootClient from './clients/root-client'; /** * Verify Stripe webhook signature - * - * @param request - Incoming webhook request - * @returns Response object if verification fails, undefined if successful */ -const verifyWebhookSignature = (request: any) => { - const { headers } = request.request; +const verifyWebhookSignature = (request: any): boolean => { + const { headers, body } = request.request; const stripeSignature: string = headers['stripe-signature']; if (!stripeSignature) { - return { - response: { - status: 403, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ error: 'Missing stripe-signature header' }), - }, - }; + return false; } - // Parse Stripe signature header - // Format: t=timestamp,v1=signature + // Parse signature header: t=timestamp,v1=signature const signature: { t?: string; v1?: string } = { t: undefined, v1: undefined, }; const elements = stripeSignature.split(','); + for (const rawElement of elements) { const [prefix, value] = rawElement.split('='); if (prefix === 't' || prefix === 'v1') { @@ -56,70 +45,28 @@ const verifyWebhookSignature = (request: any) => { } if (!signature.t || !signature.v1) { - return { - response: { - status: 403, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ error: 'Invalid stripe-signature format' }), - }, - }; + return false; } - const { body } = request.request; - const signedPayload = `${signature.t}.${body.toString('utf8')}`; - - // Get webhook secret from configuration + // Verify signature const config = getConfigService(); const webhookSecret = config.get('stripeWebhookSigningSecret'); - - // Compute expected signature + const signedPayload = `${signature.t}.${body.toString('utf8')}`; const expectedSignature = crypto .createHmac('sha256', webhookSecret) .update(signedPayload) .digest('hex'); - // Verify signature using timing-safe comparison try { - const signatureVerified = crypto.timingSafeEqual( + return crypto.timingSafeEqual( Buffer.from(signature.v1, 'hex') as unknown as Uint8Array, Buffer.from(expectedSignature, 'hex') as unknown as Uint8Array ); - - if (!signatureVerified) { - return { - response: { - status: 403, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ error: 'Invalid signature' }), - }, - }; - } } catch (error) { - // Buffer length mismatch or other error - return { - response: { - status: 403, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ error: 'Signature verification failed' }), - }, - }; + return false; } - - // Signature verified successfully - return undefined; }; -/** - * Create success response - */ -const successResponse = () => ({ - response: { - status: 200, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ received: true }), - }, -}); - /** * Process incoming Stripe webhook request * @@ -131,134 +78,71 @@ export const processWebhookRequest = async (request: any) => { const logService = container.resolve(ServiceToken.LOG_SERVICE); try { - // Step 1: Verify webhook signature - logService.debug('Verifying webhook signature', 'WebhookHandler'); - const authResult = verifyWebhookSignature(request); - if (authResult) { + // Verify webhook signature + if (!verifyWebhookSignature(request)) { logService.warn( 'Webhook signature verification failed', 'WebhookHandler' ); - return authResult; + return { + response: { + status: 403, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ error: 'Invalid signature' }), + }, + }; } - // Step 2: Parse webhook body - const parsedBody = JSON.parse(request.request.body as string); - const eventType: string = parsedBody.type; - + // Parse webhook event + const event = JSON.parse(request.request.body as string); logService.info('Received Stripe webhook', 'WebhookHandler', { - eventType, - eventId: parsedBody.id, - }); - - // Step 3: Extract policy ID from event - // Different event types store policy ID in different locations - const dataObject = parsedBody.data.object; - let policyId: string | undefined; - - switch (eventType) { - case StripeEvents.InvoiceCreated: - case StripeEvents.InvoicePaid: - policyId = dataObject.metadata?.rootPolicyId; - break; - case StripeEvents.PaymentIntentSucceeded: - case StripeEvents.PaymentIntentFailed: - policyId = dataObject.metadata?.rootPolicyId; - break; - // TODO: Add more event types as needed for your implementation - default: - break; - } - - if (!policyId) { - logService.info( - 'No policy ID found in event, skipping', - 'WebhookHandler', - { eventType } - ); - return successResponse(); - } - - logService.debug('Found policy ID in event', 'WebhookHandler', { - policyId, - eventType, + eventType: event.type, + eventId: event.id, }); - // Step 4: Verify policy is assigned to this collection module - let isAssigned = false; - try { - const paymentMethod = await rootClient.SDK.getPolicyPaymentMethod({ - policyId, - }); - isAssigned = !!paymentMethod.collection_module_definition_id; - } catch (error: any) { - logService.warn( - 'Failed to check payment method for policy', - 'WebhookHandler', - { policyId, error: error.message } - ); - } - - if (!isAssigned) { - logService.info( - 'Policy not assigned to this collection module, skipping', - 'WebhookHandler', - { policyId, eventType } - ); - return successResponse(); - } - - // Step 5: Route event to appropriate controller - const payload = parsedBody.data.object; - - logService.info('Processing Stripe event', 'WebhookHandler', { - eventType, - policyId, - }); - - switch (eventType) { + // Route to appropriate controller + switch (event.type) { case StripeEvents.InvoicePaid: { const controller = container.resolve( ServiceToken.INVOICE_PAID_CONTROLLER ); - await controller.handle(payload as Stripe.Invoice); + await controller.handle(event.data.object as Stripe.Invoice); break; } - // TODO: Add more event handlers as you implement them - // Example: - // case StripeEvents.InvoicePaymentFailed: { - // const controller = container.resolve( - // ServiceToken.INVOICE_PAYMENT_FAILED_CONTROLLER, + // Add more event handlers here as needed + // case StripeEvents.PaymentIntentSucceeded: { + // const controller = container.resolve( + // ServiceToken.PAYMENT_INTENT_SUCCEEDED_CONTROLLER // ); - // await controller.handle(payload as Stripe.Invoice); + // await controller.handle(event.data.object as Stripe.PaymentIntent); // break; // } default: logService.warn('Unhandled Stripe event type', 'WebhookHandler', { - eventType, + eventType: event.type, }); - // Return success even for unhandled events to prevent retries - return successResponse(); } - logService.info('Successfully processed Stripe event', 'WebhookHandler', { - eventType, - policyId, - }); - - return successResponse(); + return { + response: { + status: 200, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ received: true }), + }, + }; } catch (error: any) { logService.error('Error processing webhook', 'WebhookHandler', { error: error.message, - stack: error.stack, }); - // Re-throw as ModuleError for consistent error handling - throw new ModuleError('Webhook processing failed', { - error: error.message, - stack: error.stack, - }); + return { + response: { + status: 500, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ error: 'Internal server error' }), + }, + }; } }; diff --git a/stripe_collection_module/jest.config.js b/stripe_collection_module/jest.config.js index be3d975..3c9f286 100644 --- a/stripe_collection_module/jest.config.js +++ b/stripe_collection_module/jest.config.js @@ -13,6 +13,9 @@ module.exports = { '!code/env.sample.ts', '!code/sample.env.ts', '!code/main.ts', + '!code/utils/index.ts', + '!code/interfaces/**', + '!code/lifecycle-hooks/index.ts', ], coverageDirectory: 'coverage', coverageReporters: ['text', 'lcov', 'html'], @@ -32,6 +35,3 @@ module.exports = { resetMocks: true, restoreMocks: true, }; - - - From 5fd261cdf4dae7badee761818f2c5c3142b7b1ad Mon Sep 17 00:00:00 2001 From: Marcel Smuts Date: Sat, 8 Nov 2025 15:00:16 +0200 Subject: [PATCH 05/16] =?UTF-8?q?=F0=9F=91=B7=20ci:=20added=20test=20runne?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PULL_REQUEST_TEMPLATE.md | 0 .github/workflows/README.md | 179 +++++++++++++ .github/workflows/ci.yml | 114 ++++++++ README.md | 11 +- stripe_collection_module/docs/CI_CD.md | 250 ++++++++++++++++++ stripe_collection_module/jest.config.js | 2 +- 6 files changed, 549 insertions(+), 7 deletions(-) rename PULL_REQUEST_TEMPLATE.md => .github/PULL_REQUEST_TEMPLATE.md (100%) create mode 100644 .github/workflows/README.md create mode 100644 .github/workflows/ci.yml create mode 100644 stripe_collection_module/docs/CI_CD.md diff --git a/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md similarity index 100% rename from PULL_REQUEST_TEMPLATE.md rename to .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..42694dc --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,179 @@ +# GitHub Actions Workflows + +This directory contains automated CI/CD workflows for the Stripe Collection Module. + +## Available Workflows + +### CI Workflow (`ci.yml`) + +**Triggers:** +- On push to `main`, `master`, or `develop` branches +- On pull requests to `main`, `master`, or `develop` branches + +**What it does:** + +#### Test & Lint Job +1. **Checkout** - Gets the code +2. **Setup Node.js** - Installs Node.js 18.x with npm caching +3. **Install** - Runs `npm ci` for clean dependency install +4. **Lint** - Runs ESLint to check code quality +5. **Test** - Runs Jest tests with coverage +6. **Upload Coverage** - Sends coverage to Codecov (optional) +7. **Check Thresholds** - Verifies 70% coverage threshold +8. **Comment PR** - Posts coverage report as PR comment + +#### Build Job +1. **Checkout** - Gets the code +2. **Setup Node.js** - Installs Node.js 18.x +3. **Install** - Runs `npm ci` +4. **Build** - Compiles TypeScript to JavaScript +5. **Verify** - Checks that build artifacts were created + +## Viewing Results + +### On Pull Requests + +When you create a PR, the CI workflow will: +1. ✅ Run all tests and show results +2. 📊 Post coverage report as a comment +3. 🔍 Run linter and flag issues +4. 🏗️ Verify the build succeeds + +### Status Badges + +Add to your README: + +```markdown +[![CI](https://github.com/YOUR_USERNAME/YOUR_REPO/actions/workflows/ci.yml/badge.svg)](https://github.com/YOUR_USERNAME/YOUR_REPO/actions/workflows/ci.yml) +``` + +## Local Testing + +Before pushing, verify your changes locally: + +```bash +cd stripe_collection_module + +# Run linter +npm run lint + +# Run tests with coverage +npm test -- --coverage + +# Build +npm run build +``` + +## Configuration + +### Node Version + +The workflow uses Node.js 18.x to match the `.nvmrc` file. + +### Coverage Thresholds + +Tests must maintain 70% coverage across: +- Statements +- Branches +- Functions +- Lines + +### Caching + +The workflow caches npm dependencies for faster runs. + +## Troubleshooting + +### Failing Tests + +If tests fail in CI but pass locally: +1. Check Node.js version matches (18.x) +2. Clear local `node_modules` and reinstall +3. Run `npm ci` instead of `npm install` +4. Check for environment-specific code + +### Build Failures + +If build fails: +1. Run `npm run build` locally +2. Check TypeScript errors +3. Verify all imports are correct +4. Check `tsconfig.json` configuration + +### Coverage Failures + +If coverage is below threshold: +1. Run `npm test -- --coverage` locally +2. Check which files need tests +3. Add tests to increase coverage +4. See `docs/TESTING.md` for testing guide + +## Customization + +### Adding More Jobs + +To add a new job, edit `.github/workflows/ci.yml`: + +```yaml +new_job_name: + name: My New Job + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Do something + run: echo "Hello" +``` + +### Changing Node Version + +Update the matrix in `ci.yml`: + +```yaml +strategy: + matrix: + node-version: [18.x, 20.x] # Test multiple versions +``` + +### Optional: Codecov Integration + +To enable Codecov coverage reports: +1. Sign up at https://codecov.io +2. Add your repository +3. No additional config needed - workflow already includes Codecov step + +## Security + +### Secrets + +The workflow doesn't require secrets by default. If you add steps that need secrets: + +1. Add secrets in GitHub repo settings +2. Reference in workflow: + +```yaml +- name: Step using secret + env: + SECRET_NAME: ${{ secrets.SECRET_NAME }} + run: some-command +``` + +### Permissions + +The workflow has minimal permissions by default. It only needs: +- Read access to code +- Write access to comments (for coverage reporting) + +## Best Practices + +1. **Always run tests locally first** +2. **Keep CI fast** - Don't add unnecessary steps +3. **Fix failing tests immediately** - Don't merge with failing CI +4. **Review coverage reports** - Maintain high coverage +5. **Update workflows** - Keep actions up to date + +## Resources + +- [GitHub Actions Documentation](https://docs.github.com/en/actions) +- [Jest Documentation](https://jestjs.io/) +- [Codecov Documentation](https://docs.codecov.com/) + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..768cd01 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,114 @@ +name: CI + +on: + push: + branches: [main, master, develop] + pull_request: + branches: [main, master, develop] + +jobs: + test: + name: Test & Lint + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + cache-dependency-path: stripe_collection_module/package-lock.json + + - name: Install dependencies + run: | + cd stripe_collection_module + npm ci + + - name: Run linter + run: | + cd stripe_collection_module + npm run lint + + - name: Run tests with coverage + run: | + cd stripe_collection_module + npm test -- --coverage --ci + + - name: Check coverage thresholds + run: | + cd stripe_collection_module + npm test -- --coverage --ci --silent + + - name: Comment PR with coverage + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + continue-on-error: true + with: + script: | + const fs = require('fs'); + const coveragePath = './stripe_collection_module/coverage/coverage-summary.json'; + + if (fs.existsSync(coveragePath)) { + const coverage = JSON.parse(fs.readFileSync(coveragePath, 'utf8')); + const total = coverage.total; + + const comment = `## 📊 Test Coverage Report + + | Category | Coverage | + |----------|----------| + | **Statements** | ${total.statements.pct.toFixed(2)}% | + | **Branches** | ${total.branches.pct.toFixed(2)}% | + | **Functions** | ${total.functions.pct.toFixed(2)}% | + | **Lines** | ${total.lines.pct.toFixed(2)}% | + + ${total.statements.pct >= 70 ? '✅' : '❌'} Coverage threshold: 70% + `; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); + } + + build: + name: Build Check + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 18.x + cache: 'npm' + cache-dependency-path: stripe_collection_module/package-lock.json + + - name: Install dependencies + run: | + cd stripe_collection_module + npm ci + + - name: Build TypeScript + run: | + cd stripe_collection_module + npm run build + + - name: Check for build artifacts + run: | + cd stripe_collection_module + if [ ! -d "dist" ]; then + echo "Build artifacts not found!" + exit 1 + fi + echo "✅ Build successful" diff --git a/README.md b/README.md index 171025f..07db5b4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # Stripe Collection Module Template +[![CI](https://github.com/YOUR_USERNAME/YOUR_REPO/actions/workflows/ci.yml/badge.svg)](https://github.com/YOUR_USERNAME/YOUR_REPO/actions/workflows/ci.yml) +[![Coverage](https://img.shields.io/badge/coverage-92%25-brightgreen.svg)](https://github.com/YOUR_USERNAME/YOUR_REPO) +[![Node](https://img.shields.io/badge/node-18.x-brightgreen.svg)](https://nodejs.org) +[![TypeScript](https://img.shields.io/badge/typescript-5.x-blue.svg)](https://www.typescriptlang.org/) + A production-ready template for building collection modules that integrate Stripe with the Root Platform. Includes comprehensive testing, structured logging, and clear architecture patterns. --- @@ -162,12 +167,6 @@ npm run test:coverage npm run validate ``` -### Test Coverage - -- **Services**: 80%+ coverage -- **Core**: 90%+ coverage -- **Overall**: 70%+ coverage - See [docs/TESTING.md](./stripe_collection_module/docs/TESTING.md) for testing guide. --- diff --git a/stripe_collection_module/docs/CI_CD.md b/stripe_collection_module/docs/CI_CD.md new file mode 100644 index 0000000..845d262 --- /dev/null +++ b/stripe_collection_module/docs/CI_CD.md @@ -0,0 +1,250 @@ +# CI/CD Setup + +This document explains the Continuous Integration and Continuous Deployment (CI/CD) setup for the Stripe Collection Module. + +## Overview + +The project uses **GitHub Actions** for automated testing and quality checks on every pull request and push to main branches. + +## What Gets Tested + +### On Every Pull Request + +When you create or update a pull request, GitHub Actions automatically: + +1. ✅ **Runs all tests** +2. 📊 **Checks code coverage** (must be ≥70%) +3. 🔍 **Runs linter** (ESLint) +4. 🏗️ **Verifies build** (TypeScript compilation) +5. 💬 **Posts coverage report** as PR comment + +### Status Checks + +Pull requests show status checks: +- ✅ **Test & Lint** - All tests pass with coverage +- ✅ **Build Check** - TypeScript compiles successfully + +**You cannot merge** until all checks pass. + +## CI Workflow Details + +### Workflow File + +`.github/workflows/ci.yml` + +### Triggers + +- **Push** to `main`, `master`, or `develop` +- **Pull requests** to `main`, `master`, or `develop` + +### Jobs + +#### 1. Test & Lint Job + +```yaml +- Checkout code +- Setup Node.js 18.x +- Install dependencies (npm ci) +- Run ESLint +- Run Jest tests with coverage +- Upload coverage to Codecov +- Verify 70% threshold +- Comment coverage on PR +``` + +#### 2. Build Job + +```yaml +- Checkout code +- Setup Node.js 18.x +- Install dependencies +- Build TypeScript +- Verify dist/ created +``` + +## Coverage Requirements + +All code must maintain **70% coverage** across: + +| Metric | Threshold | +|--------|-----------| +| Statements | 70% | +| Branches | 70% | +| Functions | 70% | +| Lines | 70% | + +## Viewing CI Results + +### 1. Pull Request Page + +Check the **Checks** tab on your PR to see: +- Test results +- Linting results +- Build status +- Coverage report (posted as comment) + +### 2. Actions Tab + +Visit the **Actions** tab in GitHub to: +- View detailed logs +- Re-run failed workflows +- See all workflow runs + +## Local Pre-Push Checklist + +Before pushing code, run locally: + +```bash +cd stripe_collection_module + +# 1. Run linter +npm run lint + +# 2. Fix linting issues +npm run lint:fix + +# 3. Run all tests +npm test + +# 4. Check coverage +npm test -- --coverage + +# 5. Build +npm run build +``` + +**If all pass locally, CI should pass too!** + +## Debugging Failed CI + +### Tests Fail in CI but Pass Locally + +**Causes:** +- Different Node.js version +- Cached dependencies +- Environment-specific code + +**Solutions:** +```bash +# Use correct Node version +nvm use + +# Clean install +rm -rf node_modules package-lock.json +npm install + +# Run with CI flag +npm test -- --ci +``` + +### Coverage Below Threshold + +**Check what needs testing:** +```bash +npm test -- --coverage --verbose +``` + +**Add tests for uncovered files** - see [TESTING.md](./TESTING.md) + +### Linter Failures + +**Run locally:** +```bash +npm run lint +``` + +**Auto-fix:** +```bash +npm run lint:fix +``` + +### Build Failures + +**Check TypeScript errors:** +```bash +npm run build +``` + +## Coverage Reports + +### On Pull Requests + +The CI automatically posts a coverage report comment: + +``` +📊 Test Coverage Report + +| Category | Coverage | +|----------|----------| +| Statements | 92.91% | +| Branches | 84.77% | +| Functions | 95.38% | +| Lines | 93.49% | + +✅ Coverage threshold: 70% +``` + +## Pull Request Template + +The project includes a PR template that prompts you to: + +- Describe changes +- Verify testing +- Update documentation +- Complete checklist + +**Use the template** to ensure quality submissions. + +## Best Practices + +### ✅ DO + +- Run tests locally before pushing +- Fix failing CI immediately +- Maintain high test coverage +- Write meaningful commit messages +- Keep PRs focused and small + +### ❌ DON'T + +- Commit with failing tests +- Skip linting errors +- Merge with failing CI +- Commit without testing +- Disable coverage checks + +## Troubleshooting + +### "npm ci" Fails + +**Cause:** Outdated package-lock.json + +**Fix:** +```bash +npm install +git add package-lock.json +git commit -m "Update package-lock.json" +``` + +### Cache Issues + +**Clear GitHub Actions cache:** +1. Go to **Actions** tab +2. Click **Caches** +3. Delete relevant caches +4. Re-run workflow + +### Permissions Error + +**Ensure GitHub Actions is enabled:** +1. Go to **Settings** → **Actions** → **General** +2. Enable "Allow all actions" +3. Enable "Read and write permissions" + +## Resources + +- [GitHub Actions Docs](https://docs.github.com/en/actions) +- [Workflow Syntax](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions) +- [Jest CI Configuration](https://jestjs.io/docs/configuration#ci-boolean) +- [Node.js Action](https://github.com/actions/setup-node) + diff --git a/stripe_collection_module/jest.config.js b/stripe_collection_module/jest.config.js index 3c9f286..eed853a 100644 --- a/stripe_collection_module/jest.config.js +++ b/stripe_collection_module/jest.config.js @@ -18,7 +18,7 @@ module.exports = { '!code/lifecycle-hooks/index.ts', ], coverageDirectory: 'coverage', - coverageReporters: ['text', 'lcov', 'html'], + coverageReporters: ['text', 'lcov', 'html', 'json-summary'], coverageThreshold: { global: { branches: 70, From 6364162529fc04da691023bc87aaedd126cfb652 Mon Sep 17 00:00:00 2001 From: Marcel Smuts Date: Sat, 8 Nov 2025 15:03:19 +0200 Subject: [PATCH 06/16] =?UTF-8?q?=F0=9F=91=B7=20ci:=20fixed=20ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 10 ++++++++++ stripe_collection_module/docs/CI_CD.md | 11 ++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 768cd01..7a183d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,11 @@ jobs: cd stripe_collection_module npm ci + - name: Create env.ts for build + run: | + cd stripe_collection_module + cp code/env.sample.ts code/env.ts + - name: Run linter run: | cd stripe_collection_module @@ -99,6 +104,11 @@ jobs: cd stripe_collection_module npm ci + - name: Create env.ts for build + run: | + cd stripe_collection_module + cp code/env.sample.ts code/env.ts + - name: Build TypeScript run: | cd stripe_collection_module diff --git a/stripe_collection_module/docs/CI_CD.md b/stripe_collection_module/docs/CI_CD.md index 845d262..ebf45e3 100644 --- a/stripe_collection_module/docs/CI_CD.md +++ b/stripe_collection_module/docs/CI_CD.md @@ -45,9 +45,9 @@ Pull requests show status checks: - Checkout code - Setup Node.js 18.x - Install dependencies (npm ci) +- Create env.ts from env.sample.ts (for TypeScript compilation) - Run ESLint - Run Jest tests with coverage -- Upload coverage to Codecov - Verify 70% threshold - Comment coverage on PR ``` @@ -58,6 +58,7 @@ Pull requests show status checks: - Checkout code - Setup Node.js 18.x - Install dependencies +- Create env.ts from env.sample.ts (for TypeScript compilation) - Build TypeScript - Verify dist/ created ``` @@ -215,6 +216,14 @@ The project includes a PR template that prompts you to: ## Troubleshooting +### "Cannot find module '../env'" Error + +**Cause:** The `code/env.ts` file is gitignored and not in the repository. + +**Solution:** The CI workflow automatically creates `code/env.ts` from `code/env.sample.ts` before building. If this step fails, check that `env.sample.ts` exists and is valid. + +**Why this happens:** Environment files contain secrets and should never be committed. The CI uses the sample file as a placeholder for compilation. + ### "npm ci" Fails **Cause:** Outdated package-lock.json From 656f3cc6a848c4fed34e39db1651cd7ba6591682 Mon Sep 17 00:00:00 2001 From: Marcel Smuts Date: Sat, 8 Nov 2025 15:05:53 +0200 Subject: [PATCH 07/16] =?UTF-8?q?=F0=9F=91=B7=20ci:=20fixing=20ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 10 +--------- stripe_collection_module/docs/CI_CD.md | 3 +-- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a183d5..6b3f0ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,12 +113,4 @@ jobs: run: | cd stripe_collection_module npm run build - - - name: Check for build artifacts - run: | - cd stripe_collection_module - if [ ! -d "dist" ]; then - echo "Build artifacts not found!" - exit 1 - fi - echo "✅ Build successful" + echo "✅ TypeScript compilation successful" diff --git a/stripe_collection_module/docs/CI_CD.md b/stripe_collection_module/docs/CI_CD.md index ebf45e3..0f68cd5 100644 --- a/stripe_collection_module/docs/CI_CD.md +++ b/stripe_collection_module/docs/CI_CD.md @@ -59,8 +59,7 @@ Pull requests show status checks: - Setup Node.js 18.x - Install dependencies - Create env.ts from env.sample.ts (for TypeScript compilation) -- Build TypeScript -- Verify dist/ created +- Build TypeScript (type checking only - noEmit: true) ``` ## Coverage Requirements From c67d853b32d1800a901e975eab97b8d4b6ce4ed7 Mon Sep 17 00:00:00 2001 From: Marcel Smuts Date: Sat, 8 Nov 2025 15:09:08 +0200 Subject: [PATCH 08/16] =?UTF-8?q?=F0=9F=91=B7=20ci:=20split=20up=20test=20?= =?UTF-8?q?and=20lint=20jobs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 50 +++++++++++++++++--------- stripe_collection_module/docs/CI_CD.md | 29 +++++++++------ 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b3f0ac..0798d62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,22 +7,18 @@ on: branches: [main, master, develop] jobs: - test: - name: Test & Lint + lint: + name: Lint runs-on: ubuntu-latest - strategy: - matrix: - node-version: [18.x] - steps: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node.js ${{ matrix.node-version }} + - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: ${{ matrix.node-version }} + node-version: 18.x cache: 'npm' cache-dependency-path: stripe_collection_module/package-lock.json @@ -36,20 +32,40 @@ jobs: cd stripe_collection_module cp code/env.sample.ts code/env.ts - - name: Run linter + - name: Run ESLint run: | cd stripe_collection_module npm run lint - - name: Run tests with coverage + test: + name: Test + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 18.x + cache: 'npm' + cache-dependency-path: stripe_collection_module/package-lock.json + + - name: Install dependencies run: | cd stripe_collection_module - npm test -- --coverage --ci + npm ci + + - name: Create env.ts for build + run: | + cd stripe_collection_module + cp code/env.sample.ts code/env.ts - - name: Check coverage thresholds + - name: Run tests with coverage run: | cd stripe_collection_module - npm test -- --coverage --ci --silent + npm test -- --coverage --ci - name: Comment PR with coverage if: github.event_name == 'pull_request' @@ -63,19 +79,19 @@ jobs: if (fs.existsSync(coveragePath)) { const coverage = JSON.parse(fs.readFileSync(coveragePath, 'utf8')); const total = coverage.total; - + const comment = `## 📊 Test Coverage Report - + | Category | Coverage | |----------|----------| | **Statements** | ${total.statements.pct.toFixed(2)}% | | **Branches** | ${total.branches.pct.toFixed(2)}% | | **Functions** | ${total.functions.pct.toFixed(2)}% | | **Lines** | ${total.lines.pct.toFixed(2)}% | - + ${total.statements.pct >= 70 ? '✅' : '❌'} Coverage threshold: 70% `; - + github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, diff --git a/stripe_collection_module/docs/CI_CD.md b/stripe_collection_module/docs/CI_CD.md index 0f68cd5..72b003d 100644 --- a/stripe_collection_module/docs/CI_CD.md +++ b/stripe_collection_module/docs/CI_CD.md @@ -20,8 +20,9 @@ When you create or update a pull request, GitHub Actions automatically: ### Status Checks -Pull requests show status checks: -- ✅ **Test & Lint** - All tests pass with coverage +Pull requests show three independent status checks: +- ✅ **Lint** - ESLint passes with no errors +- ✅ **Test** - All tests pass with 70%+ coverage - ✅ **Build Check** - TypeScript compiles successfully **You cannot merge** until all checks pass. @@ -39,26 +40,34 @@ Pull requests show status checks: ### Jobs -#### 1. Test & Lint Job +#### 1. Lint Job ```yaml - Checkout code - Setup Node.js 18.x - Install dependencies (npm ci) -- Create env.ts from env.sample.ts (for TypeScript compilation) +- Create env.ts from env.sample.ts - Run ESLint -- Run Jest tests with coverage -- Verify 70% threshold -- Comment coverage on PR ``` -#### 2. Build Job +#### 2. Test Job ```yaml - Checkout code - Setup Node.js 18.x -- Install dependencies -- Create env.ts from env.sample.ts (for TypeScript compilation) +- Install dependencies (npm ci) +- Create env.ts from env.sample.ts +- Run Jest tests with coverage (275+ tests) +- Comment coverage report on PR +``` + +#### 3. Build Job + +```yaml +- Checkout code +- Setup Node.js 18.x +- Install dependencies (npm ci) +- Create env.ts from env.sample.ts - Build TypeScript (type checking only - noEmit: true) ``` From 44fbc3dd11bc94a4412f705c208576665220f830 Mon Sep 17 00:00:00 2001 From: Marcel Smuts Date: Tue, 11 Nov 2025 15:09:59 +0200 Subject: [PATCH 09/16] =?UTF-8?q?=E2=9C=A8=20feat:=20updated=20setup=20and?= =?UTF-8?q?=20deploy=20scripts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.sh | 445 +++++++++++++++--- stripe_collection_module/.gitignore | 3 + stripe_collection_module/.root-config.json | 12 +- .../__tests__/services/config.service.test.ts | 48 +- .../__tests__/setup.test.ts | 2 +- stripe_collection_module/__tests__/setup.ts | 39 +- stripe_collection_module/code/env.sample.ts | 15 +- .../code/services/config.service.ts | 25 +- .../adapters/README.md => docs/ADAPTERS.md} | 0 .../clients/README.md => docs/CLIENTS.md} | 0 .../README.md => docs/CONTROLLERS.md} | 0 .../{code/core/README.md => docs/CORE.md} | 0 .../README.md => docs/LIFECYCLE_HOOKS.md} | 0 .../services/README.md => docs/SERVICES.md} | 0 .../{code/utils/README.md => docs/UTILS.md} | 0 stripe_collection_module/package-lock.json | 5 +- stripe_collection_module/package.json | 16 +- stripe_collection_module/scripts/deploy.sh | 205 ++++++-- .../scripts/validate-config.sh | 29 +- stripe_collection_module/tsconfig.build.json | 11 +- stripe_collection_module/tsconfig.json | 10 +- 21 files changed, 698 insertions(+), 167 deletions(-) create mode 100644 stripe_collection_module/.gitignore rename stripe_collection_module/{code/adapters/README.md => docs/ADAPTERS.md} (100%) rename stripe_collection_module/{code/clients/README.md => docs/CLIENTS.md} (100%) rename stripe_collection_module/{code/controllers/README.md => docs/CONTROLLERS.md} (100%) rename stripe_collection_module/{code/core/README.md => docs/CORE.md} (100%) rename stripe_collection_module/{code/lifecycle-hooks/README.md => docs/LIFECYCLE_HOOKS.md} (100%) rename stripe_collection_module/{code/services/README.md => docs/SERVICES.md} (100%) rename stripe_collection_module/{code/utils/README.md => docs/UTILS.md} (100%) diff --git a/setup.sh b/setup.sh index 11777bf..07c1fe6 100755 --- a/setup.sh +++ b/setup.sh @@ -10,8 +10,13 @@ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' +CYAN='\033[0;36m' +MAGENTA='\033[0;35m' NC='\033[0m' # No Color +# Required Node version +REQUIRED_NODE_VERSION="18" + echo -e "${BLUE}╔════════════════════════════════════════════════════════╗${NC}" echo -e "${BLUE}║ Stripe Collection Module Template - Setup Wizard ║${NC}" echo -e "${BLUE}╚════════════════════════════════════════════════════════╝${NC}" @@ -23,10 +28,159 @@ if [ ! -f "stripe_collection_module/package.json" ]; then exit 1 fi +# Function to prompt with default value +prompt_with_default() { + local prompt="$1" + local default="$2" + local value + + if [ -n "$default" ]; then + read -p "$prompt [$default]: " value + echo "${value:-$default}" + else + read -p "$prompt: " value + echo "$value" + fi +} + +# Function to prompt for required value +prompt_required() { + local prompt="$1" + local value + + while [ -z "$value" ]; do + read -p "$prompt: " value + if [ -z "$value" ]; then + echo -e "${RED}❌ This value is required${NC}" + fi + done + + echo "$value" +} + +# Function to prompt for password/secret (no echo) +prompt_secret() { + local prompt="$1" + local value + + while [ -z "$value" ]; do + read -s -p "$prompt: " value + echo "" + if [ -z "$value" ]; then + echo -e "${RED}❌ This value is required${NC}" + fi + done + + echo "$value" +} + +# ============================================================================ +# Step 1: Check and setup Node.js version +# ============================================================================ +echo -e "${YELLOW}🔧 Step 1: Checking Node.js version...${NC}" + +CURRENT_NODE_VERSION=$(node -v 2>/dev/null | sed 's/v//' | cut -d. -f1) + +if [ -z "$CURRENT_NODE_VERSION" ]; then + echo -e "${RED}❌ Node.js is not installed${NC}" + echo -e "${BLUE}ℹ️ Please install Node.js ${REQUIRED_NODE_VERSION}.x first${NC}" + echo -e "${BLUE}ℹ️ Recommended: Install nvm (Node Version Manager)${NC}" + echo -e "${CYAN} curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash${NC}" + exit 1 +fi + +if [ "$CURRENT_NODE_VERSION" -lt "$REQUIRED_NODE_VERSION" ]; then + echo -e "${RED}❌ Node.js version ${CURRENT_NODE_VERSION}.x detected${NC}" + echo -e "${RED}❌ Node.js ${REQUIRED_NODE_VERSION}.x or higher is required${NC}" + + # Check if nvm is available + if command -v nvm &> /dev/null || [ -f "$HOME/.nvm/nvm.sh" ]; then + echo -e "${BLUE}ℹ️ NVM detected. Attempting to install and use Node.js ${REQUIRED_NODE_VERSION}...${NC}" + + # Load nvm if not already loaded + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + + # Install and use the required version + nvm install "$REQUIRED_NODE_VERSION" + nvm use "$REQUIRED_NODE_VERSION" + + echo -e "${GREEN}✓ Now using Node.js $(node -v)${NC}" + else + echo -e "${BLUE}ℹ️ Please install Node.js ${REQUIRED_NODE_VERSION}.x or use nvm${NC}" + exit 1 + fi +elif [ "$CURRENT_NODE_VERSION" -ne "$REQUIRED_NODE_VERSION" ]; then + echo -e "${YELLOW}⚠️ Node.js version ${CURRENT_NODE_VERSION}.x detected${NC}" + echo -e "${YELLOW}⚠️ Node.js ${REQUIRED_NODE_VERSION}.x is recommended${NC}" + + # Check if nvm is available + if command -v nvm &> /dev/null || [ -f "$HOME/.nvm/nvm.sh" ]; then + read -p "Switch to Node.js ${REQUIRED_NODE_VERSION}.x using nvm? (y/n) " -n 1 -r + echo "" + if [[ $REPLY =~ ^[Yy]$ ]]; then + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + + nvm install "$REQUIRED_NODE_VERSION" 2>/dev/null || true + nvm use "$REQUIRED_NODE_VERSION" + + echo -e "${GREEN}✓ Now using Node.js $(node -v)${NC}" + fi + fi +else + echo -e "${GREEN}✓ Node.js ${CURRENT_NODE_VERSION}.x detected (required: ${REQUIRED_NODE_VERSION}.x)${NC}" +fi + +# Ensure nvm use is called in the stripe_collection_module directory cd stripe_collection_module +if [ -f ".nvmrc" ]; then + if command -v nvm &> /dev/null || [ -f "$HOME/.nvm/nvm.sh" ]; then + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + nvm use 2>/dev/null || echo -e "${YELLOW}⚠️ Could not automatically switch Node version${NC}" + fi +fi +echo "" + +# ============================================================================ +# Step 2: Check for Root Platform CLI (rp) +# ============================================================================ +echo -e "${YELLOW}🔧 Step 2: Checking Root Platform CLI...${NC}" + +if command -v rp &> /dev/null; then + echo -e "${GREEN}✓ Root Platform CLI (rp) is installed${NC}" + rp --version 2>/dev/null || echo -e "${BLUE}ℹ️ rp version info not available${NC}" +else + echo -e "${YELLOW}⚠️ Root Platform CLI (rp) not found${NC}" + echo -e "${BLUE}ℹ️ The rp CLI is required for deploying Collection Modules${NC}" + + read -p "Install Root Platform CLI globally? (y/n) " -n 1 -r + echo "" + + if [[ $REPLY =~ ^[Yy]$ ]]; then + echo -e "${BLUE}Installing root-platform-cli globally...${NC}" + npm install -g root-platform-cli + + if command -v rp &> /dev/null; then + echo -e "${GREEN}✓ Root Platform CLI installed successfully${NC}" + rp --version 2>/dev/null || true + else + echo -e "${RED}❌ Installation failed. Please install manually:${NC}" + echo -e "${CYAN} npm install -g root-platform-cli${NC}" + echo -e "${YELLOW}⚠️ Continuing setup, but deployment will require rp CLI${NC}" + fi + else + echo -e "${YELLOW}⚠️ Skipping rp CLI installation${NC}" + echo -e "${BLUE}ℹ️ You can install it later with: npm install -g root-platform-cli${NC}" + fi +fi +echo "" -# Step 1: Install dependencies -echo -e "${YELLOW}📦 Step 1: Installing dependencies...${NC}" +# ============================================================================ +# Step 3: Install dependencies +# ============================================================================ +echo -e "${YELLOW}📦 Step 3: Installing dependencies...${NC}" if command -v npm &> /dev/null; then npm install echo -e "${GREEN}✓ Dependencies installed${NC}" @@ -36,8 +190,11 @@ else fi echo "" -# Step 2: Configure Root Platform settings -echo -e "${YELLOW}🔑 Step 2: Configuring Root Platform...${NC}" +# ============================================================================ +# Step 4: Configure Root Platform settings (.root-config.json) +# ============================================================================ +echo -e "${YELLOW}🔑 Step 4: Configuring Root Platform...${NC}" +echo "" # Check if .root-config.json needs setup if [ ! -f ".root-config.json" ]; then @@ -46,16 +203,19 @@ if [ ! -f ".root-config.json" ]; then fi # Check if this is the default template config -if grep -q "my_collection_module_cm_stripe" .root-config.json 2>/dev/null; then - echo -e "${BLUE}📝 Let's configure your Root Platform settings...${NC}" +if grep -q "my_collection_module_cm_stripe\|test_cm" .root-config.json 2>/dev/null; then + echo -e "${MAGENTA}╔════════════════════════════════════════════════════════╗${NC}" + echo -e "${MAGENTA}║ Root Platform Configuration ║${NC}" + echo -e "${MAGENTA}╚════════════════════════════════════════════════════════╝${NC}" + echo "" + echo -e "${CYAN}Please provide the following Root Platform settings:${NC}" echo "" # Prompt for configuration values - read -p "Collection Module Key (e.g., cm_stripe_yourcompany): " cm_key - read -p "Collection Module Name (e.g., Your Company Stripe Integration): " cm_name - read -p "Organization ID: " org_id - read -p "Root Platform Host (default: https://api.rootplatform.com): " host - host=${host:-https://api.rootplatform.com} + cm_key=$(prompt_required "Collection Module Key (e.g., cm_stripe_yourcompany)") + cm_name=$(prompt_required "Collection Module Name (e.g., Your Company Stripe Integration)") + org_id=$(prompt_required "Organization ID") + host=$(prompt_with_default "Root Platform Host" "https://api.rootplatform.com") # Update .root-config.json cat > .root-config.json << EOF @@ -70,52 +230,184 @@ if grep -q "my_collection_module_cm_stripe" .root-config.json 2>/dev/null; then "manualTransactions": [] } EOF + echo "" echo -e "${GREEN}✓ Created .root-config.json${NC}" else echo -e "${GREEN}✓ .root-config.json already configured${NC}" fi # Set up .root-auth +echo "" if [ ! -f ".root-auth" ]; then - if [ -f ".root-auth.sample" ]; then - echo "" - read -p "Root Platform API Key (will be stored in .root-auth): " api_key - echo "ROOT_API_KEY=${api_key}" > .root-auth - echo -e "${GREEN}✓ Created .root-auth${NC}" - echo -e "${BLUE}ℹ️ This file is gitignored for security${NC}" - else - echo -e "${YELLOW}⚠️ .root-auth.sample not found, skipping .root-auth creation${NC}" - fi + echo -e "${CYAN}Root Platform API Key Configuration:${NC}" + echo -e "${BLUE}ℹ️ Get this from: Root Platform → Settings → API Keys${NC}" + api_key=$(prompt_secret "Root Platform API Key") + echo "ROOT_API_KEY=${api_key}" > .root-auth + echo -e "${GREEN}✓ Created .root-auth${NC}" + echo -e "${BLUE}ℹ️ This file is gitignored for security${NC}" else echo -e "${GREEN}✓ .root-auth already exists${NC}" fi echo "" -# Step 3: Set up environment configuration -echo -e "${YELLOW}⚙️ Step 3: Setting up code environment configuration...${NC}" -if [ ! -f "code/env.ts" ]; then - cp code/env.sample.ts code/env.ts - echo -e "${GREEN}✓ Created code/env.ts from template${NC}" - echo -e "${BLUE}ℹ️ Please edit code/env.ts with your Stripe API keys and other configuration${NC}" -else - echo -e "${BLUE}ℹ️ code/env.ts already exists, skipping...${NC}" -fi +# ============================================================================ +# Step 5: Set up environment configuration (code/env.ts) +# ============================================================================ +echo -e "${YELLOW}⚙️ Step 5: Setting up environment configuration...${NC}" echo "" -# Step 4: Set up .nvmrc (optional) -echo -e "${YELLOW}🔧 Step 4: Node.js version...${NC}" -if [ -f ".nvmrc" ]; then - echo -e "${GREEN}✓ .nvmrc already configured${NC}" - if command -v nvm &> /dev/null; then - echo -e "${BLUE}ℹ️ Run 'nvm use' to switch to the correct Node.js version${NC}" +# Check if env.ts already exists +if [ -f "code/env.ts" ]; then + echo -e "${BLUE}ℹ️ code/env.ts already exists${NC}" + read -p "Do you want to reconfigure it? (y/n) " -n 1 -r + echo "" + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo -e "${GREEN}✓ Keeping existing code/env.ts${NC}" + echo "" + else + rm code/env.ts fi +fi + +if [ ! -f "code/env.ts" ]; then + echo -e "${MAGENTA}╔════════════════════════════════════════════════════════╗${NC}" + echo -e "${MAGENTA}║ Environment Configuration ║${NC}" + echo -e "${MAGENTA}╚════════════════════════════════════════════════════════╝${NC}" + echo "" + echo -e "${CYAN}Please provide the following configuration values:${NC}" + echo "" + + # Environment + echo -e "${YELLOW}Environment Settings:${NC}" + node_env=$(prompt_with_default "NODE_ENV (development/production)" "development") + echo "" + + # Stripe Webhook Secrets + echo -e "${YELLOW}Stripe Webhook Signing Secrets:${NC}" + echo -e "${BLUE}ℹ️ Get from: Stripe Dashboard → Developers → Webhooks → Signing secret${NC}" + stripe_webhook_secret_sandbox=$(prompt_with_default "Stripe Webhook Signing Secret (SANDBOX)" "whsec_xxxxx") + stripe_webhook_secret_production=$(prompt_with_default "Stripe Webhook Signing Secret (PRODUCTION)" "whsec_xxxxx") + echo "" + + # Stripe Product IDs + echo -e "${YELLOW}Stripe Product IDs:${NC}" + echo -e "${BLUE}ℹ️ Get from: Stripe Dashboard → Products${NC}" + stripe_product_id_sandbox=$(prompt_with_default "Stripe Product ID (SANDBOX)" "prod_xxxxx") + stripe_product_id_production=$(prompt_with_default "Stripe Product ID (PRODUCTION)" "prod_xxxxx") + echo "" + + # Stripe Publishable Keys + echo -e "${YELLOW}Stripe API Keys - Publishable (Public):${NC}" + echo -e "${BLUE}ℹ️ Get from: Stripe Dashboard → Developers → API keys${NC}" + stripe_publishable_key_sandbox=$(prompt_with_default "Stripe Publishable Key (SANDBOX)" "pk_test_xxxxx") + stripe_publishable_key_production=$(prompt_with_default "Stripe Publishable Key (PRODUCTION)" "pk_live_xxxxx") + echo "" + + # Stripe Secret Keys + echo -e "${YELLOW}Stripe API Keys - Secret (Private):${NC}" + echo -e "${BLUE}ℹ️ Get from: Stripe Dashboard → Developers → API keys${NC}" + echo -e "${RED}⚠️ NEVER expose these publicly!${NC}" + stripe_secret_key_sandbox=$(prompt_with_default "Stripe Secret Key (SANDBOX)" "sk_test_xxxxx") + stripe_secret_key_production=$(prompt_with_default "Stripe Secret Key (PRODUCTION)" "sk_live_xxxxx") + echo "" + + # Root Configuration + echo -e "${YELLOW}Root Platform Configuration:${NC}" + # Use the cm_key from .root-config.json if it exists + if [ -n "$cm_key" ]; then + root_collection_module_key="$cm_key" + echo -e "${BLUE}ℹ️ Using Collection Module Key from .root-config.json: ${root_collection_module_key}${NC}" + else + root_collection_module_key=$(prompt_with_default "Root Collection Module Key (must match .root-config.json)" "my_collection_module_cm_stripe") + fi + echo "" + + echo -e "${YELLOW}Root API Keys:${NC}" + echo -e "${BLUE}ℹ️ Get from: Root Platform → Settings → API Keys${NC}" + root_api_key_sandbox=$(prompt_with_default "Root API Key (SANDBOX)" "sandbox_xxxxx") + root_api_key_production=$(prompt_with_default "Root API Key (PRODUCTION)" "production_xxxxx") + echo "" + + echo -e "${YELLOW}Root API Base URLs:${NC}" + root_base_url_sandbox=$(prompt_with_default "Root Base URL (SANDBOX)" "https://sandbox.rootplatform.com/v1/insurance") + root_base_url_production=$(prompt_with_default "Root Base URL (PRODUCTION)" "https://api.rootplatform.com/v1/insurance") + echo "" + + # Optional Configuration + echo -e "${YELLOW}Optional Configuration:${NC}" + time_delay=$(prompt_with_default "Time delay for processing in milliseconds" "10000") + echo "" + + # Generate env.ts file + cat > code/env.ts << EOF +/** + * Environment Configuration + * + * IMPORTANT: Never commit this file to version control! + * The .gitignore file should exclude it. + * + * Generated by setup.sh on $(date) + */ + +// ============================================================================ +// ENVIRONMENT +// ============================================================================ +export const NODE_ENV = '${node_env}'; + +// ============================================================================ +// PAYMENT PROVIDER CONFIGURATION (Stripe) +// ============================================================================ + +// Webhook Signing Secrets +export const STRIPE_WEBHOOK_SIGNING_SECRET_LIVE = '${stripe_webhook_secret_production}'; +export const STRIPE_WEBHOOK_SIGNING_SECRET_TEST = '${stripe_webhook_secret_sandbox}'; + +// Product IDs +export const STRIPE_PRODUCT_ID_LIVE = '${stripe_product_id_production}'; +export const STRIPE_PRODUCT_ID_TEST = '${stripe_product_id_sandbox}'; + +// API Keys - Publishable (Public) +export const STRIPE_PUBLISHABLE_KEY_LIVE = '${stripe_publishable_key_production}'; +export const STRIPE_PUBLISHABLE_KEY_TEST = '${stripe_publishable_key_sandbox}'; + +// API Keys - Secret (Private) +// NEVER expose these publicly! +export const STRIPE_SECRET_KEY_LIVE = '${stripe_secret_key_production}'; +export const STRIPE_SECRET_KEY_TEST = '${stripe_secret_key_sandbox}'; + +// ============================================================================ +// ROOT PLATFORM CONFIGURATION +// ============================================================================ + +// Collection Module Key +export const ROOT_COLLECTION_MODULE_KEY = '${root_collection_module_key}'; + +// Root API Keys +export const ROOT_API_KEY_LIVE = '${root_api_key_production}'; +export const ROOT_API_KEY_SANDBOX = '${root_api_key_sandbox}'; + +// Root API Base URLs +export const ROOT_BASE_URL_LIVE = '${root_base_url_production}'; +export const ROOT_BASE_URL_SANDBOX = '${root_base_url_sandbox}'; + +// ============================================================================ +// OPTIONAL CONFIGURATION +// ============================================================================ + +export const TIME_DELAY_IN_MILLISECONDS = '${time_delay}'; +EOF + + echo -e "${GREEN}✓ Created code/env.ts with your configuration${NC}" + echo -e "${RED}⚠️ Remember: Never commit code/env.ts to version control!${NC}" else - echo -e "${BLUE}ℹ️ No .nvmrc found (optional)${NC}" + echo -e "${GREEN}✓ code/env.ts already exists and configured${NC}" fi echo "" -# Step 5: Run validation -echo -e "${YELLOW}✓ Step 5: Running validation checks...${NC}" +# ============================================================================ +# Step 6: Run validation +# ============================================================================ +echo -e "${YELLOW}✓ Step 6: Running validation checks...${NC}" if npm run lint > /dev/null 2>&1; then echo -e "${GREEN}✓ Linting passed${NC}" else @@ -125,43 +417,74 @@ fi if npm run build > /dev/null 2>&1; then echo -e "${GREEN}✓ Build successful${NC}" else - echo -e "${RED}❌ Build failed - please check your setup${NC}" + echo -e "${RED}❌ Build failed - please check your configuration${NC}" + echo -e "${BLUE}ℹ️ You may need to review code/env.ts and .root-config.json${NC}" fi echo "" -# Step 6: Next steps +# ============================================================================ +# Step 7: Summary and Next Steps +# ============================================================================ echo -e "${BLUE}╔════════════════════════════════════════════════════════╗${NC}" -echo -e "${BLUE}║ Setup Complete! Next Steps: ║${NC}" +echo -e "${BLUE}║ Setup Complete! 🎉 ║${NC}" echo -e "${BLUE}╚════════════════════════════════════════════════════════╝${NC}" echo "" -echo -e "${GREEN}1. Finish configuring your environment:${NC}" -echo -e " ${BLUE}→${NC} Edit ${YELLOW}stripe_collection_module/code/env.ts${NC}" -echo -e " ${BLUE}→${NC} Add your Stripe API keys and webhook secrets" -echo -e " ${BLUE}→${NC} Verify ${YELLOW}stripe_collection_module/.root-config.json${NC}" -echo -e " ${BLUE}→${NC} Verify ${YELLOW}stripe_collection_module/.root-auth${NC} has correct API key" +echo -e "${GREEN}Configuration Summary:${NC}" +echo -e " ${CYAN}✓${NC} Node.js version: $(node -v)" +if command -v rp &> /dev/null; then + echo -e " ${CYAN}✓${NC} Root Platform CLI (rp) installed" +else + echo -e " ${YELLOW}⚠${NC} Root Platform CLI (rp) not installed" +fi +echo -e " ${CYAN}✓${NC} Dependencies installed" +echo -e " ${CYAN}✓${NC} .root-config.json configured" +echo -e " ${CYAN}✓${NC} .root-auth configured" +echo -e " ${CYAN}✓${NC} code/env.ts configured" echo "" -echo -e "${GREEN}2. Customize for your use case:${NC}" -echo -e " ${BLUE}→${NC} Update ${YELLOW}README.md${NC} with your module details" +echo -e "${YELLOW}Files Created/Updated:${NC}" +echo -e " ${BLUE}→${NC} ${CYAN}stripe_collection_module/.root-config.json${NC}" +echo -e " ${BLUE}→${NC} ${CYAN}stripe_collection_module/.root-auth${NC}" +echo -e " ${BLUE}→${NC} ${CYAN}stripe_collection_module/code/env.ts${NC}" +echo "" +echo -e "${GREEN}Next Steps:${NC}" +echo "" +echo -e "${CYAN}1. Before running commands, ensure correct Node version:${NC}" +echo -e " ${YELLOW}cd stripe_collection_module && nvm use${NC}" +echo "" + +# Show rp CLI installation reminder if not installed +if ! command -v rp &> /dev/null; then + echo -e "${CYAN}2. Install Root Platform CLI (required for deployment):${NC}" + echo -e " ${YELLOW}npm install -g root-platform-cli${NC}" + echo "" + STEP_OFFSET=1 +else + STEP_OFFSET=0 +fi + +echo -e "${CYAN}$((2 + STEP_OFFSET)). Review and customize your code:${NC}" echo -e " ${BLUE}→${NC} Implement controllers in ${YELLOW}code/controllers/${NC}" echo -e " ${BLUE}→${NC} Add your business logic to services" +echo -e " ${BLUE}→${NC} Update ${YELLOW}README.md${NC} with your module details" +echo "" +echo -e "${CYAN}$((3 + STEP_OFFSET)). Development commands:${NC}" +echo -e " ${YELLOW}npm run lint${NC} - Check code quality" +echo -e " ${YELLOW}npm run test${NC} - Run tests" +echo -e " ${YELLOW}npm run test:integration${NC} - Run integration tests" +echo -e " ${YELLOW}npm run build${NC} - Build the module" +echo "" +echo -e "${CYAN}$((4 + STEP_OFFSET)). Deploy when ready:${NC}" +echo -e " ${BLUE}→${NC} Deployment uses ${YELLOW}rp push${NC} followed by publish" +echo -e " ${YELLOW}npm run deploy:sandbox${NC} - Deploy to sandbox" +echo -e " ${YELLOW}npm run deploy:production${NC} - Deploy to production" +echo -e " ${YELLOW}npm run deploy:dry-run:sandbox${NC} - Test deployment (sandbox)" +echo -e " ${YELLOW}npm run deploy:dry-run:production${NC} - Test deployment (production)" echo "" -echo -e "${GREEN}3. Review documentation:${NC}" +echo -e "${CYAN}$((5 + STEP_OFFSET)). Review documentation:${NC}" echo -e " ${BLUE}→${NC} ${YELLOW}stripe_collection_module/docs/SETUP.md${NC}" echo -e " ${BLUE}→${NC} ${YELLOW}stripe_collection_module/docs/DEPLOYMENT.md${NC}" echo -e " ${BLUE}→${NC} ${YELLOW}stripe_collection_module/docs/CUSTOMIZING.md${NC}" echo "" -echo -e "${GREEN}4. Development commands:${NC}" -echo -e " ${BLUE}→${NC} ${YELLOW}npm run lint${NC} - Check code quality" -echo -e " ${BLUE}→${NC} ${YELLOW}npm run test${NC} - Run tests" -echo -e " ${BLUE}→${NC} ${YELLOW}npm run build${NC} - Build the module" -echo "" -echo -e "${GREEN}5. Deploy when ready:${NC}" -echo -e " ${BLUE}→${NC} ${YELLOW}npm run deploy:sandbox${NC} - Deploy to sandbox" -echo -e " ${BLUE}→${NC} ${YELLOW}npm run deploy:production${NC} - Deploy to production" -echo "" -echo -e "${BLUE}📚 For detailed documentation, see:${NC}" -echo -e " ${YELLOW}stripe_collection_module/README.md${NC}" -echo "" echo -e "${GREEN}✨ Happy coding!${NC}" echo "" diff --git a/stripe_collection_module/.gitignore b/stripe_collection_module/.gitignore new file mode 100644 index 0000000..df42d51 --- /dev/null +++ b/stripe_collection_module/.gitignore @@ -0,0 +1,3 @@ +.root-auth +node_modules +code/**/env.* diff --git a/stripe_collection_module/.root-config.json b/stripe_collection_module/.root-config.json index e20a43d..d93c1d7 100644 --- a/stripe_collection_module/.root-config.json +++ b/stripe_collection_module/.root-config.json @@ -1,10 +1,8 @@ { - "collectionModuleKey": "test_cm", - "collectionModuleName": "Test CM", - "organizationId": "12345", - "host": "https://api.rootplatform.com", - "settings": { - "legacyCodeExecution": false - }, + "collectionModuleKey": "demo_stripe_v2", + "collectionModuleName": "Demo Stripe V2", + "organizationId": "c598807f-c59e-46ff-a4cb-99860be57a52", + "host": "https://sandbox.uk.rootplatform.com", + "settings": {}, "manualTransactions": [] } diff --git a/stripe_collection_module/__tests__/services/config.service.test.ts b/stripe_collection_module/__tests__/services/config.service.test.ts index 9483c97..2a5cf10 100644 --- a/stripe_collection_module/__tests__/services/config.service.test.ts +++ b/stripe_collection_module/__tests__/services/config.service.test.ts @@ -25,13 +25,13 @@ describe('ConfigurationService', () => { }); describe('Initialization', () => { - it('should initialize with default environment (development)', () => { + it('should initialize with default environment (sandbox)', () => { delete process.env.ENVIRONMENT; const config = new ConfigurationService({ skipValidation: true }); - expect(config.getEnvironment()).toBe('development'); - expect(config.isDevelopment()).toBe(true); + expect(config.getEnvironment()).toBe('sandbox'); + expect(config.isSandbox()).toBe(true); expect(config.isProduction()).toBe(false); }); @@ -42,7 +42,7 @@ describe('ConfigurationService', () => { expect(config.getEnvironment()).toBe('production'); expect(config.isProduction()).toBe(true); - expect(config.isDevelopment()).toBe(false); + expect(config.isSandbox()).toBe(false); }); it('should initialize with environment from options', () => { @@ -56,7 +56,7 @@ describe('ConfigurationService', () => { }); it('should prioritize options.environment over process.env.ENVIRONMENT', () => { - process.env.ENVIRONMENT = 'development'; + process.env.ENVIRONMENT = 'sandbox'; const config = new ConfigurationService({ environment: 'production', @@ -148,33 +148,33 @@ describe('ConfigurationService', () => { expect(config.get('stripeSecretKey')).toContain('sk_live'); }); - it('should load development configuration', () => { + it('should load sandbox configuration', () => { const config = new ConfigurationService({ - environment: 'development', + environment: 'sandbox', skipValidation: true, }); - expect(config.get('environment')).toBe('development'); - // Development should use TEST keys + expect(config.get('environment')).toBe('sandbox'); + // Sandbox should use TEST keys expect(config.get('stripePublishableKey')).toContain('pk_test'); expect(config.get('stripeSecretKey')).toContain('sk_test'); }); - it('should have different API keys for production vs development', () => { + it('should have different API keys for production vs sandbox', () => { const prodConfig = new ConfigurationService({ environment: 'production', skipValidation: true, }); - const devConfig = new ConfigurationService({ - environment: 'development', + const sandboxConfig = new ConfigurationService({ + environment: 'sandbox', skipValidation: true, }); expect(prodConfig.get('stripeSecretKey')).not.toBe( - devConfig.get('stripeSecretKey'), + sandboxConfig.get('stripeSecretKey') ); expect(prodConfig.get('rootApiKey')).not.toBe( - devConfig.get('rootApiKey'), + sandboxConfig.get('rootApiKey') ); }); }); @@ -194,8 +194,8 @@ describe('ConfigurationService', () => { }); it('should return correct environment name', () => { - const devConfig = new ConfigurationService({ - environment: 'development', + const sandboxConfig = new ConfigurationService({ + environment: 'sandbox', skipValidation: true, }); const prodConfig = new ConfigurationService({ @@ -203,7 +203,7 @@ describe('ConfigurationService', () => { skipValidation: true, }); - expect(devConfig.getEnvironment()).toBe('development'); + expect(sandboxConfig.getEnvironment()).toBe('sandbox'); expect(prodConfig.getEnvironment()).toBe('production'); }); @@ -214,17 +214,17 @@ describe('ConfigurationService', () => { }); expect(prodConfig.isProduction()).toBe(true); - expect(prodConfig.isDevelopment()).toBe(false); + expect(prodConfig.isSandbox()).toBe(false); }); - it('should correctly identify development environment', () => { - const devConfig = new ConfigurationService({ - environment: 'development', + it('should correctly identify sandbox environment', () => { + const sandboxConfig = new ConfigurationService({ + environment: 'sandbox', skipValidation: true, }); - expect(devConfig.isDevelopment()).toBe(true); - expect(devConfig.isProduction()).toBe(false); + expect(sandboxConfig.isSandbox()).toBe(true); + expect(sandboxConfig.isProduction()).toBe(false); }); }); @@ -287,7 +287,7 @@ describe('ConfigurationService', () => { fail('Should have thrown an error'); } catch (error: any) { expect(error.message).toContain('production'); - expect(error.message).toContain('development'); + expect(error.message).toContain('sandbox'); } }); }); diff --git a/stripe_collection_module/__tests__/setup.test.ts b/stripe_collection_module/__tests__/setup.test.ts index 793d203..4a9bcbc 100644 --- a/stripe_collection_module/__tests__/setup.test.ts +++ b/stripe_collection_module/__tests__/setup.test.ts @@ -8,7 +8,7 @@ describe('Jest Setup', () => { }); it('should have test environment variables configured', () => { - expect(process.env.ENVIRONMENT).toBe('development'); + expect(process.env.ENVIRONMENT).toBe('sandbox'); expect(process.env.NODE_ENV).toBe('test'); expect(process.env.ROOT_COLLECTION_MODULE_KEY).toBe( 'test_collection_module', diff --git a/stripe_collection_module/__tests__/setup.ts b/stripe_collection_module/__tests__/setup.ts index 0116a58..6191166 100644 --- a/stripe_collection_module/__tests__/setup.ts +++ b/stripe_collection_module/__tests__/setup.ts @@ -4,13 +4,38 @@ * This file runs before all tests */ +// Mock the env module to prevent tests from using actual user config +jest.mock('../code/env', () => ({ + NODE_ENV: 'sandbox', + + // Stripe Webhook Signing Secrets + STRIPE_WEBHOOK_SIGNING_SECRET_LIVE: 'whsec_live_test_secret', + STRIPE_WEBHOOK_SIGNING_SECRET_TEST: 'whsec_test_secret', + + // Stripe Product IDs + STRIPE_PRODUCT_ID_LIVE: 'prod_live_test_123', + STRIPE_PRODUCT_ID_TEST: 'prod_test_123', + + // Stripe API Keys - Publishable + STRIPE_PUBLISHABLE_KEY_LIVE: 'pk_live_test_key_123', + STRIPE_PUBLISHABLE_KEY_TEST: 'pk_test_key_123', + + // Stripe API Keys - Secret + STRIPE_SECRET_KEY_LIVE: 'sk_live_test_key_123', + STRIPE_SECRET_KEY_TEST: 'sk_test_key_123', + + // Root Platform Configuration + ROOT_COLLECTION_MODULE_KEY: 'test_collection_module', + ROOT_API_KEY_LIVE: 'production_test_api_key', + ROOT_API_KEY_SANDBOX: 'sandbox_test_api_key', + ROOT_BASE_URL_LIVE: 'https://api.rootplatform.com/v1/insurance', + ROOT_BASE_URL_SANDBOX: 'https://sandbox.rootplatform.com/v1/insurance', + + // Optional Configuration + TIME_DELAY_IN_MILLISECONDS: '10000', +})); + // Set up test environment variables -process.env.ENVIRONMENT = 'development'; +process.env.ENVIRONMENT = 'sandbox'; process.env.NODE_ENV = 'test'; process.env.ROOT_COLLECTION_MODULE_KEY = 'test_collection_module'; -process.env.ROOT_COLLECTION_MODULE_SECRET = 'test_secret'; -process.env.STRIPE_SECRET_KEY = 'sk_test_123'; -process.env.STRIPE_PUBLISHABLE_KEY = 'pk_test_123'; - -// Note: Global mocks removed - use test-helpers.ts setupConfigMock() instead -// This prevents conflicts when testing the actual config/log instance modules diff --git a/stripe_collection_module/code/env.sample.ts b/stripe_collection_module/code/env.sample.ts index 1d9aed2..c5ddde1 100644 --- a/stripe_collection_module/code/env.sample.ts +++ b/stripe_collection_module/code/env.sample.ts @@ -2,6 +2,7 @@ * Environment Configuration Sample * * Copy this file to env.ts and fill in your actual values. + * OR run the setup script: bash ../setup.sh * * IMPORTANT: Never commit env.ts to version control! * The .gitignore file should exclude it. @@ -10,11 +11,11 @@ // ============================================================================ // ENVIRONMENT // ============================================================================ -// Set to 'development' for testing or 'production' for live deployment -export const NODE_ENV = 'development'; +// Set to 'sandbox' for testing or 'production' for live deployment +export const NODE_ENV = 'sandbox'; // ============================================================================ -// PAYMENT PROVIDER CONFIGURATION (Stripe Example) +// PAYMENT PROVIDER CONFIGURATION (Stripe) // ============================================================================ // Webhook Signing Secrets @@ -22,7 +23,7 @@ export const NODE_ENV = 'development'; export const STRIPE_WEBHOOK_SIGNING_SECRET_LIVE = 'whsec_xxxxx'; export const STRIPE_WEBHOOK_SIGNING_SECRET_TEST = 'whsec_xxxxx'; -// Product IDs (if applicable to your provider) +// Product IDs // Get from: Stripe Dashboard → Products export const STRIPE_PRODUCT_ID_LIVE = 'prod_xxxxx'; export const STRIPE_PRODUCT_ID_TEST = 'prod_xxxxx'; @@ -71,9 +72,13 @@ export const TIME_DELAY_IN_MILLISECONDS = '10000'; // NOTES FOR DEPLOYMENT // ============================================================================ // +// Setup: +// - Run bash ../setup.sh for an interactive configuration wizard +// - The script will prompt for all required values with helpful defaults +// // Security: // - Rotate API keys regularly -// - Use different keys for test/production +// - Use different keys for sandbox/production // - Never commit env.ts to git // - Monitor CloudWatch Logs for suspicious activity // diff --git a/stripe_collection_module/code/services/config.service.ts b/stripe_collection_module/code/services/config.service.ts index 1048172..8416bb5 100644 --- a/stripe_collection_module/code/services/config.service.ts +++ b/stripe_collection_module/code/services/config.service.ts @@ -21,7 +21,7 @@ export interface EnvironmentConfig { export interface ConfigMap { production: EnvironmentConfig; - development: EnvironmentConfig; + sandbox: EnvironmentConfig; } export interface ConfigurationServiceOptions { @@ -31,7 +31,7 @@ export interface ConfigurationServiceOptions { export enum Environment { PRODUCTION = 'production', - DEVELOPMENT = 'development', + SANDBOX = 'sandbox', } /** @@ -51,7 +51,7 @@ export class ConfigurationService { constructor(options: ConfigurationServiceOptions = {}) { this.environment = - options.environment || process.env.ENVIRONMENT || Environment.DEVELOPMENT; + options.environment || process.env.ENVIRONMENT || Environment.SANDBOX; this.configs = this.buildConfigMap(); if (!options.skipValidation) { @@ -82,9 +82,9 @@ export class ConfigurationService { rootBaseUrl: env.ROOT_BASE_URL_LIVE, }; - const development: EnvironmentConfig = { + const sandbox: EnvironmentConfig = { ...baseConfig, - environment: Environment.DEVELOPMENT, + environment: Environment.SANDBOX, stripeWebhookSigningSecret: env.STRIPE_WEBHOOK_SIGNING_SECRET_TEST, stripePublishableKey: env.STRIPE_PUBLISHABLE_KEY_TEST, stripeSecretKey: env.STRIPE_SECRET_KEY_TEST, @@ -95,7 +95,7 @@ export class ConfigurationService { return { production, - development, + sandbox, }; } @@ -105,7 +105,7 @@ export class ConfigurationService { private validateEnvironment(): void { if (!this.environment) { throw new Error( - 'ENVIRONMENT is not set. Set ENVIRONMENT=production or ENVIRONMENT=development' + 'ENVIRONMENT is not set. Set ENVIRONMENT=production or ENVIRONMENT=sandbox' ); } @@ -198,10 +198,17 @@ export class ConfigurationService { } /** - * Check if running in development + * Check if running in sandbox + */ + public isSandbox(): boolean { + return this.environment === Environment.SANDBOX.toString(); + } + + /** + * @deprecated Use isSandbox() instead. This method is kept for backwards compatibility. */ public isDevelopment(): boolean { - return this.environment === Environment.DEVELOPMENT.toString(); + return this.isSandbox(); } /** diff --git a/stripe_collection_module/code/adapters/README.md b/stripe_collection_module/docs/ADAPTERS.md similarity index 100% rename from stripe_collection_module/code/adapters/README.md rename to stripe_collection_module/docs/ADAPTERS.md diff --git a/stripe_collection_module/code/clients/README.md b/stripe_collection_module/docs/CLIENTS.md similarity index 100% rename from stripe_collection_module/code/clients/README.md rename to stripe_collection_module/docs/CLIENTS.md diff --git a/stripe_collection_module/code/controllers/README.md b/stripe_collection_module/docs/CONTROLLERS.md similarity index 100% rename from stripe_collection_module/code/controllers/README.md rename to stripe_collection_module/docs/CONTROLLERS.md diff --git a/stripe_collection_module/code/core/README.md b/stripe_collection_module/docs/CORE.md similarity index 100% rename from stripe_collection_module/code/core/README.md rename to stripe_collection_module/docs/CORE.md diff --git a/stripe_collection_module/code/lifecycle-hooks/README.md b/stripe_collection_module/docs/LIFECYCLE_HOOKS.md similarity index 100% rename from stripe_collection_module/code/lifecycle-hooks/README.md rename to stripe_collection_module/docs/LIFECYCLE_HOOKS.md diff --git a/stripe_collection_module/code/services/README.md b/stripe_collection_module/docs/SERVICES.md similarity index 100% rename from stripe_collection_module/code/services/README.md rename to stripe_collection_module/docs/SERVICES.md diff --git a/stripe_collection_module/code/utils/README.md b/stripe_collection_module/docs/UTILS.md similarity index 100% rename from stripe_collection_module/code/utils/README.md rename to stripe_collection_module/docs/UTILS.md diff --git a/stripe_collection_module/package-lock.json b/stripe_collection_module/package-lock.json index 20d519a..644e423 100644 --- a/stripe_collection_module/package-lock.json +++ b/stripe_collection_module/package-lock.json @@ -1,9 +1,12 @@ { - "name": "stripe_collection_module", + "name": "stripe-collection-module", + "version": "1.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { + "name": "stripe-collection-module", + "version": "1.0.0", "dependencies": { "@rootplatform/node-sdk": "^0.0.7", "joi": "11.4.0", diff --git a/stripe_collection_module/package.json b/stripe_collection_module/package.json index 2d1030b..5c79895 100644 --- a/stripe_collection_module/package.json +++ b/stripe_collection_module/package.json @@ -1,11 +1,18 @@ { + "name": "stripe-collection-module", + "version": "1.0.0", + "main": "dist/main.js", + "types": "dist/main.d.ts", + "files": [ + "dist/**/*" + ], "dependencies": { - "@rootplatform/node-sdk": "^0.0.7", "joi": "11.4.0", "moment-timezone": "0.5.40", "node-fetch": "2.6.7", "phone": "2.4.22", - "stripe": "^19.2.1" + "stripe": "14.5.0", + "@rootplatform/node-sdk": "^0.0.4" }, "devDependencies": { "@jest/globals": "^30.2.0", @@ -48,10 +55,11 @@ "predeploy": "npm run validate && npm run test && npm run build", "deploy:sandbox": "bash scripts/deploy.sh sandbox", "deploy:production": "bash scripts/deploy.sh production", - "deploy:dry-run": "bash scripts/deploy.sh --dry-run" + "deploy:dry-run:sandbox": "bash scripts/deploy.sh --dry-run sandbox", + "deploy:dry-run:production": "bash scripts/deploy.sh --dry-run production" }, "engines": { "npm": ">=8.0.0", "node": ">=18.0.0" } -} \ No newline at end of file +} diff --git a/stripe_collection_module/scripts/deploy.sh b/stripe_collection_module/scripts/deploy.sh index ad64cce..271ff85 100755 --- a/stripe_collection_module/scripts/deploy.sh +++ b/stripe_collection_module/scripts/deploy.sh @@ -4,20 +4,23 @@ # Stripe Collection Module Deployment Script # # This script automates the deployment process to the Root Platform. +# It automatically loads configuration from: +# - .root-config.json (Organization ID, Module Key, Host) +# - .root-auth (API Key) +# - code/env.ts (Environment configuration) # # Usage: # ./scripts/deploy.sh [environment] [version] # # Examples: -# ./scripts/deploy.sh sandbox # Deploy to sandbox (no version tag) +# ./scripts/deploy.sh sandbox # Deploy to sandbox # ./scripts/deploy.sh sandbox v1.0.0 # Deploy to sandbox with tag # ./scripts/deploy.sh production v1.0.0 # Deploy to production with tag # # Prerequisites: -# - ROOT_API_KEY environment variable (or use -k flag) -# - ROOT_ORG_ID environment variable (or use -o flag) -# - ROOT_HOST environment variable (or use -h flag) -# - CM_KEY environment variable (or use -c flag) +# - Run bash ../setup.sh to configure required files +# - Root Platform CLI (rp) installed: npm install -g root-platform-cli +# - Configuration files: .root-config.json, .root-auth, code/env.ts ############################################################################### set -e # Exit on error @@ -41,11 +44,78 @@ SKIP_BUILD=false SKIP_TAG=false DRY_RUN=false -# Root Platform configuration +# Root Platform configuration (will be loaded from config files) ROOT_API_KEY="${ROOT_API_KEY:-}" ROOT_ORG_ID="${ROOT_ORG_ID:-}" -ROOT_HOST="${ROOT_HOST:-https://api.rootplatform.com}" -CM_KEY="${CM_KEY:-cm_stripe}" +ROOT_HOST="${ROOT_HOST:-}" +CM_KEY="${CM_KEY:-}" + +############################################################################### +# Configuration Loading Functions +############################################################################### + +load_root_config() { + local config_file="$PROJECT_DIR/.root-config.json" + + if [ ! -f "$config_file" ]; then + print_error ".root-config.json not found at $config_file" + print_info "Run the setup script first: bash ../setup.sh" + exit 1 + fi + + # Check if jq is available for JSON parsing + if command -v jq &> /dev/null; then + CM_KEY=$(jq -r '.collectionModuleKey // empty' "$config_file") + ROOT_ORG_ID=$(jq -r '.organizationId // empty' "$config_file") + ROOT_HOST=$(jq -r '.host // empty' "$config_file") + else + # Fallback to grep/sed if jq is not available + CM_KEY=$(grep -o '"collectionModuleKey"[[:space:]]*:[[:space:]]*"[^"]*"' "$config_file" | sed 's/.*"\([^"]*\)"$/\1/') + ROOT_ORG_ID=$(grep -o '"organizationId"[[:space:]]*:[[:space:]]*"[^"]*"' "$config_file" | sed 's/.*"\([^"]*\)"$/\1/') + ROOT_HOST=$(grep -o '"host"[[:space:]]*:[[:space:]]*"[^"]*"' "$config_file" | sed 's/.*"\([^"]*\)"$/\1/') + fi + + # Set defaults if not found + ROOT_HOST="${ROOT_HOST:-https://api.rootplatform.com}" +} + +load_root_auth() { + local auth_file="$PROJECT_DIR/.root-auth" + + if [ ! -f "$auth_file" ]; then + print_error ".root-auth not found at $auth_file" + print_info "Run the setup script first: bash ../setup.sh" + exit 1 + fi + + # Load ROOT_API_KEY from .root-auth file + # shellcheck disable=SC1090 + source "$auth_file" + + if [ -z "$ROOT_API_KEY" ]; then + print_error "ROOT_API_KEY not found in .root-auth" + exit 1 + fi +} + +load_env_config() { + local env_file="$PROJECT_DIR/code/env.ts" + + if [ ! -f "$env_file" ]; then + print_error "env.ts not found at $env_file" + print_info "Run the setup script first: bash ../setup.sh" + exit 1 + fi + + # The env.ts file is TypeScript, but we can still extract values + # We look for specific exports based on the environment + local node_env + node_env=$(grep -o "export const NODE_ENV = '[^']*'" "$env_file" | sed "s/export const NODE_ENV = '\([^']*\)'/\1/") + + if [ -n "$node_env" ]; then + print_info "Detected NODE_ENV in env.ts: $node_env" + fi +} ############################################################################### # Helper Functions @@ -81,15 +151,23 @@ Usage: $0 [OPTIONS] [version] Deployment script for Stripe Collection Module to Root Platform. +Configuration: + By default, this script automatically loads configuration from: + - .root-config.json (Organization ID, Module Key, Host) + - .root-auth (API Key) + - code/env.ts (Environment configuration) + + Run 'bash ../setup.sh' to configure these files interactively. + Arguments: environment Target environment: 'sandbox' or 'production' version Git tag version (e.g., v1.0.0) - optional for sandbox Options: - -k, --api-key KEY Root Platform API key (or set ROOT_API_KEY env var) - -o, --org-id ID Root organization ID (or set ROOT_ORG_ID env var) - -h, --host URL Root Platform host (default: https://api.rootplatform.com) - -c, --cm-key KEY Collection module key (default: cm_stripe) + -k, --api-key KEY Override Root Platform API key (from .root-auth) + -o, --org-id ID Override Root organization ID (from .root-config.json) + -h, --host URL Override Root Platform host (from .root-config.json) + -c, --cm-key KEY Override Collection module key (from .root-config.json) --skip-tests Skip running tests --skip-build Skip build step --skip-tag Skip creating git tag @@ -97,23 +175,25 @@ Options: --help Show this help message Examples: - # Deploy to sandbox + # Deploy to sandbox (using config files) $0 sandbox # Deploy to sandbox with version tag $0 sandbox v1.0.0 - # Deploy to production with explicit credentials - $0 -k "prod_key_123" -o "org_id" production v1.1.0 + # Deploy to production + $0 production v1.1.0 + + # Deploy with overridden API key + $0 -k "prod_key_123" production v1.1.0 # Dry run for production $0 --dry-run production v1.0.0 -Environment Variables: - ROOT_API_KEY Root Platform API key - ROOT_ORG_ID Root organization ID - ROOT_HOST Root Platform API URL - CM_KEY Collection module key +Configuration Files: + .root-config.json Organization ID, Module Key, API Host + .root-auth Root Platform API Key + code/env.ts Environment-specific configuration EOF } @@ -176,6 +256,29 @@ while [[ $# -gt 0 ]]; do esac done +############################################################################### +# Load Configuration from Files +############################################################################### + +print_info "Loading configuration from files..." + +# Load configuration from .root-config.json +if [ -z "$CM_KEY" ] || [ -z "$ROOT_ORG_ID" ] || [ -z "$ROOT_HOST" ]; then + load_root_config + print_info "Loaded configuration from .root-config.json" +fi + +# Load API key from .root-auth +if [ -z "$ROOT_API_KEY" ]; then + load_root_auth + print_info "Loaded API key from .root-auth" +fi + +# Load environment configuration from env.ts +load_env_config + +echo "" + ############################################################################### # Validate Arguments ############################################################################### @@ -199,16 +302,25 @@ fi if [ -z "$ROOT_API_KEY" ]; then print_error "ROOT_API_KEY is not set" + print_info "Could not load from .root-auth file" print_info "Set via environment variable or use -k flag" exit 1 fi if [ -z "$ROOT_ORG_ID" ]; then print_error "ROOT_ORG_ID is not set" + print_info "Could not load from .root-config.json" print_info "Set via environment variable or use -o flag" exit 1 fi +if [ -z "$CM_KEY" ]; then + print_error "CM_KEY is not set" + print_info "Could not load from .root-config.json" + print_info "Set via environment variable or use -c flag" + exit 1 +fi + ############################################################################### # Display Configuration ############################################################################### @@ -217,17 +329,21 @@ print_header "Deployment Configuration" echo "Environment: $ENVIRONMENT" echo "Version: ${VERSION:-}" -echo "Root Host: $ROOT_HOST" -echo "Organization ID: $ROOT_ORG_ID" -echo "Module Key: $CM_KEY" -echo "API Key: ${ROOT_API_KEY:0:10}..." echo "" -echo "Skip Tests: $SKIP_TESTS" -echo "Skip Build: $SKIP_BUILD" -echo "Skip Tag: $SKIP_TAG" -echo "Dry Run: $DRY_RUN" +echo "Configuration loaded from:" +echo " Root Host: $ROOT_HOST" +echo " Organization ID: $ROOT_ORG_ID" +echo " Module Key: $CM_KEY" +echo " API Key: ${ROOT_API_KEY:0:10}..." +echo "" +echo "Options:" +echo " Skip Tests: $SKIP_TESTS" +echo " Skip Build: $SKIP_BUILD" +echo " Skip Tag: $SKIP_TAG" +echo " Dry Run: $DRY_RUN" if [ "$DRY_RUN" = true ]; then + echo "" print_warning "DRY RUN MODE - No changes will be made" fi @@ -394,11 +510,40 @@ elif [ -n "$VERSION" ]; then print_warning "Skipping git tag (--skip-tag flag)" fi +############################################################################### +# Push Code to Root Platform (using rp CLI) +############################################################################### + +print_header "Step 6: Push Code with rp CLI" + +# Check if rp CLI is installed +if ! command -v rp &> /dev/null; then + print_error "Root Platform CLI (rp) is not installed" + print_info "Install it with: npm install -g root-platform-cli" + exit 1 +fi + +print_info "Using rp CLI to push code..." + +if [ "$DRY_RUN" = false ]; then + # Run rp push + if rp push; then + print_success "Code pushed successfully with rp CLI" + else + print_error "Failed to push code with rp CLI" + exit 1 + fi +else + print_info "Would execute: rp push" +fi + +echo "" + ############################################################################### # Publish to Root Platform ############################################################################### -print_header "Step 6: Publish to Root Platform" +print_header "Step 7: Publish to Root Platform" # Determine if sandbox or production if [ "$ENVIRONMENT" = "sandbox" ]; then @@ -446,7 +591,7 @@ fi # Post-deployment Steps ############################################################################### -print_header "Post-deployment" +print_header "Step 8: Post-deployment" echo "" print_success "Deployment to $ENVIRONMENT completed successfully!" diff --git a/stripe_collection_module/scripts/validate-config.sh b/stripe_collection_module/scripts/validate-config.sh index dd903c0..c57000f 100755 --- a/stripe_collection_module/scripts/validate-config.sh +++ b/stripe_collection_module/scripts/validate-config.sh @@ -1,7 +1,11 @@ #!/bin/bash # Configuration Validation Script -# Checks that all required environment variables are set +# Checks that all required configuration files are set up correctly +# and that the project builds successfully. +# +# Note: This script treats test failures as warnings, not errors. +# Configuration can be valid even if tests are failing. set -e @@ -98,14 +102,15 @@ else WARNINGS=$((WARNINGS + 1)) fi -# Run tests +# Run tests (non-blocking for configuration validation) info "Running tests..." if npm test > /dev/null 2>&1; then success "All tests passed" else - error "Tests failed" + warning "Tests failed" echo " Run 'npm test' to see detailed errors." - ERRORS=$((ERRORS + 1)) + echo " Note: Test failures don't prevent deployment, but should be fixed." + WARNINGS=$((WARNINGS + 1)) fi # Check Node version @@ -133,24 +138,24 @@ echo "──────────────────────── echo "" if [ $ERRORS -eq 0 ] && [ $WARNINGS -eq 0 ]; then - success "Configuration is valid! No errors or warnings." + success "All checks passed! No errors or warnings." echo "" echo "You're ready to:" - echo " - Run locally for testing" - echo " - Deploy to AWS Lambda" - echo " - Register webhooks with your provider" + echo " - Deploy to sandbox: npm run deploy:sandbox" + echo " - Deploy to production: npm run deploy:production" echo "" exit 0 elif [ $ERRORS -eq 0 ]; then - warning "Configuration is mostly valid with $WARNINGS warning(s)." + warning "Configuration is valid but with $WARNINGS warning(s)." echo "" - echo "Review warnings above and fix before deploying to production." + echo "Review warnings above. You can still deploy, but consider fixing them." echo "" exit 0 else - error "Configuration validation failed with $ERRORS error(s) and $WARNINGS warning(s)." + error "Configuration has $ERRORS critical error(s) and $WARNINGS warning(s)." echo "" - echo "Fix the errors above before deploying." + echo "Fix the critical errors above before deploying." + echo "Note: Warnings won't block deployment but should be addressed." echo "" exit 1 fi diff --git a/stripe_collection_module/tsconfig.build.json b/stripe_collection_module/tsconfig.build.json index 951ff9f..238d85b 100644 --- a/stripe_collection_module/tsconfig.build.json +++ b/stripe_collection_module/tsconfig.build.json @@ -3,6 +3,15 @@ "include": ["./code/**/*"], "exclude": ["./__tests__/**/*"], "compilerOptions": { - "skipLibCheck": false + "skipLibCheck": false, + "noEmit": false, + "outDir": "./dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "module": "commonjs", + "target": "es2022", + "moduleResolution": "node", + "esModuleInterop": true } } diff --git a/stripe_collection_module/tsconfig.json b/stripe_collection_module/tsconfig.json index fce63d0..0f1edd3 100644 --- a/stripe_collection_module/tsconfig.json +++ b/stripe_collection_module/tsconfig.json @@ -1,12 +1,10 @@ { "include": [ - "./code/**/*", - "./__tests__/**/*" + "./code/**/*" ], "compilerOptions": { "lib": [ - "es2022", - "dom" + "es2022" ], "module": "es2022", "target": "es2022", @@ -16,6 +14,8 @@ "moduleResolution": "Bundler", "downlevelIteration": true, "noEmit": true, - "types": ["jest"] + "types": [ + "jest" + ] } } From 45b9523f2fea634b9bdb7c9f267e1abd1a17972b Mon Sep 17 00:00:00 2001 From: Marcel Smuts Date: Tue, 11 Nov 2025 15:12:03 +0200 Subject: [PATCH 10/16] =?UTF-8?q?=F0=9F=92=9A=20ci:=20fixed=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- stripe_collection_module/tsconfig.build.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stripe_collection_module/tsconfig.build.json b/stripe_collection_module/tsconfig.build.json index 238d85b..1f11c86 100644 --- a/stripe_collection_module/tsconfig.build.json +++ b/stripe_collection_module/tsconfig.build.json @@ -3,7 +3,8 @@ "include": ["./code/**/*"], "exclude": ["./__tests__/**/*"], "compilerOptions": { - "skipLibCheck": false, + "lib": ["es2022", "dom"], + "skipLibCheck": true, "noEmit": false, "outDir": "./dist", "declaration": true, From 81f5f0a525b64d06894adcf33e2ae4c3ca45cc90 Mon Sep 17 00:00:00 2001 From: Marcel Smuts Date: Tue, 11 Nov 2025 16:46:18 +0200 Subject: [PATCH 11/16] =?UTF-8?q?=F0=9F=94=A8=20feat:=20updated=20setup=20?= =?UTF-8?q?to=20automatically=20add=20cm=20to=20Root.=20Fixed=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.sh | 66 ++++ .../__tests__/clients/root-client.test.ts | 14 +- .../invoice-paid.controller.test.ts | 336 ------------------ .../payment-creation.controller.test.ts | 298 ---------------- .../__tests__/core/container.setup.test.ts | 12 +- .../__tests__/services/root.service.test.ts | 50 ++- .../__tests__/test-helpers.ts | 15 +- .../__tests__/webhook-hooks.test.ts | 104 +----- .../code/clients/root-client.ts | 25 +- .../payment-creation.controller.ts | 169 --------- .../invoice-paid.controller.ts | 145 -------- .../code/core/container.setup.ts | 47 +-- .../code/core/container.ts | 7 +- .../code/services/root.service.ts | 4 +- .../code/webhook-hooks.ts | 27 +- stripe_collection_module/jest.config.js | 4 + stripe_collection_module/package-lock.json | 12 +- stripe_collection_module/package.json | 16 +- stripe_collection_module/scripts/deploy.sh | 7 +- 19 files changed, 184 insertions(+), 1174 deletions(-) delete mode 100644 stripe_collection_module/__tests__/controllers/invoice-paid.controller.test.ts delete mode 100644 stripe_collection_module/__tests__/controllers/payment-creation.controller.test.ts delete mode 100644 stripe_collection_module/code/controllers/root-event-processors/payment-creation.controller.ts delete mode 100644 stripe_collection_module/code/controllers/stripe-event-processors/invoice-paid.controller.ts diff --git a/setup.sh b/setup.sh index 07c1fe6..c87faba 100755 --- a/setup.sh +++ b/setup.sh @@ -234,6 +234,22 @@ EOF echo -e "${GREEN}✓ Created .root-config.json${NC}" else echo -e "${GREEN}✓ .root-config.json already configured${NC}" + + # Load values from existing .root-config.json + if command -v jq &> /dev/null; then + cm_key=$(jq -r '.collectionModuleKey' .root-config.json) + cm_name=$(jq -r '.collectionModuleName' .root-config.json) + org_id=$(jq -r '.organizationId' .root-config.json) + host=$(jq -r '.host' .root-config.json) + else + # Fallback to grep/sed if jq not available + cm_key=$(grep -o '"collectionModuleKey"[[:space:]]*:[[:space:]]*"[^"]*"' .root-config.json | sed 's/.*"\([^"]*\)"$/\1/') + cm_name=$(grep -o '"collectionModuleName"[[:space:]]*:[[:space:]]*"[^"]*"' .root-config.json | sed 's/.*"\([^"]*\)"$/\1/') + org_id=$(grep -o '"organizationId"[[:space:]]*:[[:space:]]*"[^"]*"' .root-config.json | sed 's/.*"\([^"]*\)"$/\1/') + host=$(grep -o '"host"[[:space:]]*:[[:space:]]*"[^"]*"' .root-config.json | sed 's/.*"\([^"]*\)"$/\1/') + fi + + echo -e "${BLUE}ℹ️ Loaded: CM Key=${cm_key}, Org ID=${org_id}${NC}" fi # Set up .root-auth @@ -247,6 +263,56 @@ if [ ! -f ".root-auth" ]; then echo -e "${BLUE}ℹ️ This file is gitignored for security${NC}" else echo -e "${GREEN}✓ .root-auth already exists${NC}" + # Load API key if we need to create the collection module + source .root-auth + api_key="$ROOT_API_KEY" +fi +echo "" + +# ============================================================================ +# Step 4b: Create Collection Module on Root Platform +# ============================================================================ +echo -e "${YELLOW}🚀 Step 4b: Creating Collection Module on Root Platform...${NC}" +echo "" + +if [ -n "$cm_key" ] && [ -n "$api_key" ]; then + echo -e "${BLUE}ℹ️ Attempting to create collection module: ${cm_key}${NC}" + echo -e "${BLUE}ℹ️ This is required before you can deploy${NC}" + echo "" + + # Create the collection module on Root Platform + CM_CREATE_RESPONSE=$(curl -X POST \ + -u "${api_key}:" \ + -H "Content-Type: application/json" \ + -d "{ + \"key\": \"${cm_key}\", + \"name\": \"${cm_name}\", + \"key_of_collection_module_to_clone\": \"blank_starter_template\" + }" \ + -w "\nHTTP_STATUS:%{http_code}" \ + -s \ + "${host}/v1/apps/${org_id}/insurance/collection-modules") + + # Extract HTTP status + CM_HTTP_STATUS=$(echo "$CM_CREATE_RESPONSE" | grep "HTTP_STATUS:" | cut -d: -f2) + CM_RESPONSE_BODY=$(echo "$CM_CREATE_RESPONSE" | sed '/HTTP_STATUS:/d') + + if [ "$CM_HTTP_STATUS" -ge 200 ] && [ "$CM_HTTP_STATUS" -lt 300 ]; then + echo -e "${GREEN}✓ Collection module created successfully on Root Platform${NC}" + elif [ "$CM_HTTP_STATUS" -eq 409 ] || echo "$CM_RESPONSE_BODY" | grep -q "already exists"; then + echo -e "${YELLOW}⚠️ Collection module already exists (this is OK)${NC}" + else + echo -e "${YELLOW}⚠️ Could not create collection module (HTTP $CM_HTTP_STATUS)${NC}" + echo -e "${BLUE}ℹ️ Response: $CM_RESPONSE_BODY${NC}" + echo "" + echo -e "${BLUE}ℹ️ You may need to create it manually:${NC}" + echo -e "${CYAN} curl -X POST -u '\$API_KEY:' \\${NC}" + echo -e "${CYAN} -H 'Content-Type: application/json' \\${NC}" + echo -e "${CYAN} -d '{\"key\":\"${cm_key}\",\"name\":\"${cm_name}\",\"key_of_collection_module_to_clone\":\"blank_starter_template\"}' \\${NC}" + echo -e "${CYAN} '${host}/v1/apps/${org_id}/insurance/collection-modules'${NC}" + fi +else + echo -e "${YELLOW}⚠️ Skipping collection module creation (missing configuration)${NC}" fi echo "" diff --git a/stripe_collection_module/__tests__/clients/root-client.test.ts b/stripe_collection_module/__tests__/clients/root-client.test.ts index 0d3b9a0..6ba61d8 100644 --- a/stripe_collection_module/__tests__/clients/root-client.test.ts +++ b/stripe_collection_module/__tests__/clients/root-client.test.ts @@ -15,17 +15,17 @@ jest.mock('../../code/services/config-instance', () => ({ import rootClient from '../../code/clients/root-client'; describe('RootClient', () => { - it('should export a singleton instance', () => { + it('should export the Root SDK namespace', () => { expect(rootClient).toBeDefined(); - expect(rootClient.SDK).toBeDefined(); }); - it('should have SDK property', () => { - expect(rootClient.SDK).toBeTruthy(); + it('should have getPolicyById function', () => { + expect(rootClient.getPolicyById).toBeDefined(); + expect(typeof rootClient.getPolicyById).toBe('function'); }); - it('should initialize SDK with configuration', () => { - // SDK should be initialized (we can't test internals but can verify it exists) - expect(typeof rootClient.SDK).toBe('object'); + it('should have updatePaymentsAsync function', () => { + expect(rootClient.updatePaymentsAsync).toBeDefined(); + expect(typeof rootClient.updatePaymentsAsync).toBe('function'); }); }); diff --git a/stripe_collection_module/__tests__/controllers/invoice-paid.controller.test.ts b/stripe_collection_module/__tests__/controllers/invoice-paid.controller.test.ts deleted file mode 100644 index e8fae80..0000000 --- a/stripe_collection_module/__tests__/controllers/invoice-paid.controller.test.ts +++ /dev/null @@ -1,336 +0,0 @@ -/** - * InvoicePaidController Tests - */ - -import Stripe from 'stripe'; -import { PaymentStatus } from '@rootplatform/node-sdk'; -import { InvoicePaidController } from '../../code/controllers/stripe-event-processors/invoice-paid.controller'; -import { - createMockLogService, - createMockRootService, - createMockStripeClient, -} from '../test-helpers'; - -describe('InvoicePaidController', () => { - let controller: InvoicePaidController; - let mockLogService: ReturnType; - let mockRootService: ReturnType; - let mockStripeClient: ReturnType; - - beforeEach(() => { - mockLogService = createMockLogService(); - mockRootService = createMockRootService(); - mockStripeClient = createMockStripeClient(); - - controller = new InvoicePaidController( - mockLogService as any, - mockRootService as any, - mockStripeClient as any - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('handle', () => { - it('should successfully process invoice with payment mappings', async () => { - const mockInvoice = { - id: 'inv_123', - amount_paid: 10000, - amount_due: 10000, - metadata: { - associatedRootPaymentIds: JSON.stringify([ - { - rootPaymentId: 'payment_123', - invoiceLineItemId: 'il_123', - }, - ]), - }, - } as any; - - (mockRootService.updatePaymentStatus as jest.Mock).mockResolvedValue( - undefined - ); - - await controller.handle(mockInvoice); - - expect(mockLogService.info).toHaveBeenCalledWith( - 'Processing invoice.paid event', - 'InvoicePaidController', - { - invoiceId: 'inv_123', - amount: 10000, - } - ); - - expect(mockRootService.updatePaymentStatus).toHaveBeenCalledWith({ - paymentId: 'payment_123', - status: PaymentStatus.Successful, - }); - - expect(mockLogService.info).toHaveBeenCalledWith( - 'Root payment updated to successful', - 'InvoicePaidController', - { - invoiceId: 'inv_123', - rootPaymentId: 'payment_123', - invoiceLineItemId: 'il_123', - } - ); - - expect(mockLogService.info).toHaveBeenCalledWith( - 'Successfully processed invoice.paid event', - 'InvoicePaidController', - { - invoiceId: 'inv_123', - paymentsUpdated: 1, - } - ); - }); - - it('should process multiple payment mappings', async () => { - const mockInvoice = { - id: 'inv_123', - amount_paid: 20000, - amount_due: 20000, - metadata: { - associatedRootPaymentIds: JSON.stringify([ - { - rootPaymentId: 'payment_123', - invoiceLineItemId: 'il_123', - }, - { - rootPaymentId: 'payment_456', - invoiceLineItemId: 'il_456', - }, - ]), - }, - } as any; - - (mockRootService.updatePaymentStatus as jest.Mock).mockResolvedValue( - undefined - ); - - await controller.handle(mockInvoice); - - expect(mockRootService.updatePaymentStatus).toHaveBeenCalledTimes(2); - expect(mockRootService.updatePaymentStatus).toHaveBeenCalledWith({ - paymentId: 'payment_123', - status: PaymentStatus.Successful, - }); - expect(mockRootService.updatePaymentStatus).toHaveBeenCalledWith({ - paymentId: 'payment_456', - status: PaymentStatus.Successful, - }); - - expect(mockLogService.info).toHaveBeenCalledWith( - 'Successfully processed invoice.paid event', - 'InvoicePaidController', - { - invoiceId: 'inv_123', - paymentsUpdated: 2, - } - ); - }); - - it('should skip invoice with zero amount_due', async () => { - const mockInvoice = { - id: 'inv_123', - amount_paid: 0, - amount_due: 0, - metadata: {}, - } as any; - - await controller.handle(mockInvoice); - - expect(mockLogService.info).toHaveBeenCalledWith( - 'Invoice has zero amount_due, skipping (already processed on invoice.created)', - 'InvoicePaidController', - { invoiceId: 'inv_123' } - ); - - expect(mockRootService.updatePaymentStatus).not.toHaveBeenCalled(); - }); - - it('should warn when no payment mappings found', async () => { - const mockInvoice = { - id: 'inv_123', - amount_paid: 10000, - amount_due: 10000, - metadata: {}, - } as any; - - await controller.handle(mockInvoice); - - expect(mockLogService.warn).toHaveBeenCalledWith( - 'No payment mappings found in invoice metadata', - 'InvoicePaidController', - { invoiceId: 'inv_123' } - ); - - expect(mockRootService.updatePaymentStatus).not.toHaveBeenCalled(); - }); - - it('should warn when metadata is missing associatedRootPaymentIds', async () => { - const mockInvoice = { - id: 'inv_123', - amount_paid: 10000, - amount_due: 10000, - metadata: { - someOtherField: 'value', - }, - } as any; - - await controller.handle(mockInvoice); - - expect(mockLogService.warn).toHaveBeenCalledWith( - 'No payment mappings found in invoice metadata', - 'InvoicePaidController', - { invoiceId: 'inv_123' } - ); - }); - - it('should handle invalid JSON in payment mappings', async () => { - const mockInvoice = { - id: 'inv_123', - amount_paid: 10000, - amount_due: 10000, - metadata: { - associatedRootPaymentIds: 'invalid json {', - }, - } as any; - - await controller.handle(mockInvoice); - - expect(mockLogService.error).toHaveBeenCalledWith( - 'Failed to parse payment mappings from invoice metadata', - 'InvoicePaidController', - expect.objectContaining({ - invoiceId: 'inv_123', - }) - ); - - expect(mockLogService.warn).toHaveBeenCalledWith( - 'No payment mappings found in invoice metadata', - 'InvoicePaidController', - { invoiceId: 'inv_123' } - ); - - expect(mockRootService.updatePaymentStatus).not.toHaveBeenCalled(); - }); - - it('should continue processing other payments if one fails', async () => { - const mockInvoice = { - id: 'inv_123', - amount_paid: 20000, - amount_due: 20000, - metadata: { - associatedRootPaymentIds: JSON.stringify([ - { - rootPaymentId: 'payment_123', - invoiceLineItemId: 'il_123', - }, - { - rootPaymentId: 'payment_456', - invoiceLineItemId: 'il_456', - }, - ]), - }, - } as any; - - const error = new Error('Payment update failed'); - (mockRootService.updatePaymentStatus as jest.Mock) - .mockRejectedValueOnce(error) - .mockResolvedValueOnce(undefined); - - await controller.handle(mockInvoice); - - expect(mockRootService.updatePaymentStatus).toHaveBeenCalledTimes(2); - - expect(mockLogService.error).toHaveBeenCalledWith( - 'Failed to update Root payment', - 'InvoicePaidController', - { - invoiceId: 'inv_123', - rootPaymentId: 'payment_123', - error: 'Payment update failed', - } - ); - - expect(mockLogService.info).toHaveBeenCalledWith( - 'Root payment updated to successful', - 'InvoicePaidController', - { - invoiceId: 'inv_123', - rootPaymentId: 'payment_456', - invoiceLineItemId: 'il_456', - } - ); - }); - - it('should handle empty payment mappings array', async () => { - const mockInvoice = { - id: 'inv_123', - amount_paid: 10000, - amount_due: 10000, - metadata: { - associatedRootPaymentIds: JSON.stringify([]), - }, - } as any; - - await controller.handle(mockInvoice); - - expect(mockLogService.warn).toHaveBeenCalledWith( - 'No payment mappings found in invoice metadata', - 'InvoicePaidController', - { invoiceId: 'inv_123' } - ); - - expect(mockRootService.updatePaymentStatus).not.toHaveBeenCalled(); - }); - - it('should handle all payments failing', async () => { - const mockInvoice = { - id: 'inv_123', - amount_paid: 10000, - amount_due: 10000, - metadata: { - associatedRootPaymentIds: JSON.stringify([ - { - rootPaymentId: 'payment_123', - invoiceLineItemId: 'il_123', - }, - ]), - }, - } as any; - - const error = new Error('Payment update failed'); - (mockRootService.updatePaymentStatus as jest.Mock).mockRejectedValue( - error - ); - - await controller.handle(mockInvoice); - - expect(mockLogService.error).toHaveBeenCalledWith( - 'Failed to update Root payment', - 'InvoicePaidController', - { - invoiceId: 'inv_123', - rootPaymentId: 'payment_123', - error: 'Payment update failed', - } - ); - - // Should still log final success message even if individual payments failed - expect(mockLogService.info).toHaveBeenCalledWith( - 'Successfully processed invoice.paid event', - 'InvoicePaidController', - { - invoiceId: 'inv_123', - paymentsUpdated: 1, - } - ); - }); - }); -}); diff --git a/stripe_collection_module/__tests__/controllers/payment-creation.controller.test.ts b/stripe_collection_module/__tests__/controllers/payment-creation.controller.test.ts deleted file mode 100644 index 67a0d66..0000000 --- a/stripe_collection_module/__tests__/controllers/payment-creation.controller.test.ts +++ /dev/null @@ -1,298 +0,0 @@ -/** - * PaymentCreationController Tests - */ - -import * as root from '@rootplatform/node-sdk'; -import Stripe from 'stripe'; -import { PaymentCreationController } from '../../code/controllers/root-event-processors/payment-creation.controller'; -import { - createMockLogService, - createMockRootService, - createMockStripeClient, -} from '../test-helpers'; - -describe('PaymentCreationController', () => { - let controller: PaymentCreationController; - let mockLogService: ReturnType; - let mockRootService: ReturnType; - let mockStripeClient: ReturnType; - let mockStripeSDK: any; - - beforeEach(() => { - mockLogService = createMockLogService(); - mockRootService = createMockRootService(); - mockStripeClient = createMockStripeClient(); - mockStripeSDK = mockStripeClient.stripeSDK; - - controller = new PaymentCreationController( - mockLogService as any, - mockRootService as any, - mockStripeClient as any - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('handle', () => { - const validParams = { - rootPaymentId: 'payment_123', - rootPolicyId: 'policy_456', - amount: 10000, - description: 'Premium payment', - status: root.PaymentStatus.Pending, - }; - - it('should successfully process valid payment creation', async () => { - const mockPolicy = { - policy_id: 'policy_456', - currency: 'ZAR', - app_data: { - stripe_customer_id: 'cus_123', - }, - } as any; - - const mockPaymentIntent = { - id: 'pi_123', - status: 'requires_payment_method', - } as Stripe.PaymentIntent; - - (mockRootService.getPolicy as jest.Mock).mockResolvedValue(mockPolicy); - (mockStripeSDK.paymentIntents.create as jest.Mock).mockResolvedValue( - mockPaymentIntent - ); - - await controller.handle(validParams); - - expect(mockLogService.info).toHaveBeenCalledWith( - 'Processing payment creation event', - 'PaymentCreationController', - { - rootPaymentId: 'payment_123', - rootPolicyId: 'policy_456', - amount: 10000, - } - ); - - expect(mockRootService.getPolicy).toHaveBeenCalledWith('policy_456'); - - expect(mockStripeSDK.paymentIntents.create).toHaveBeenCalledWith({ - amount: 10000, - currency: 'ZAR', - customer: 'cus_123', - description: 'Premium payment', - metadata: { - rootPaymentId: 'payment_123', - rootPolicyId: 'policy_456', - }, - payment_method_types: ['card'], - confirm: true, - off_session: true, - }); - - expect(mockLogService.info).toHaveBeenCalledWith( - 'Successfully created Stripe payment intent', - 'PaymentCreationController', - { - rootPaymentId: 'payment_123', - paymentIntentId: 'pi_123', - status: 'requires_payment_method', - } - ); - }); - - it('should skip payment with Stripe invoice description', async () => { - const paramsWithInvoice = { - ...validParams, - description: 'Stripe created invoice item: inv_123', - }; - - await controller.handle(paramsWithInvoice); - - expect(mockLogService.info).toHaveBeenCalledWith( - 'Skipping payment - already has associated Stripe invoice', - 'PaymentCreationController', - { - rootPaymentId: 'payment_123', - description: 'Stripe created invoice item: inv_123', - } - ); - - expect(mockRootService.getPolicy).not.toHaveBeenCalled(); - expect(mockStripeSDK.paymentIntents.create).not.toHaveBeenCalled(); - }); - - it('should skip payment with refund description', async () => { - const paramsWithRefund = { - ...validParams, - description: 'Refund for Stripe charge: ch_123', - }; - - await controller.handle(paramsWithRefund); - - expect(mockLogService.info).toHaveBeenCalledWith( - 'Skipping payment - already has associated Stripe invoice', - 'PaymentCreationController', - { - rootPaymentId: 'payment_123', - description: 'Refund for Stripe charge: ch_123', - } - ); - - expect(mockRootService.getPolicy).not.toHaveBeenCalled(); - }); - - it('should skip non-pending payment', async () => { - const paramsNotPending = { - ...validParams, - status: root.PaymentStatus.Successful, - }; - - await controller.handle(paramsNotPending); - - expect(mockLogService.info).toHaveBeenCalledWith( - 'Skipping payment - not in pending status', - 'PaymentCreationController', - { - rootPaymentId: 'payment_123', - status: root.PaymentStatus.Successful, - } - ); - - expect(mockRootService.getPolicy).not.toHaveBeenCalled(); - }); - - it('should throw error if policy missing stripe_customer_id', async () => { - const mockPolicy = { - policy_id: 'policy_456', - currency: 'ZAR', - app_data: {}, - } as any; - - (mockRootService.getPolicy as jest.Mock).mockResolvedValue(mockPolicy); - - await expect(controller.handle(validParams)).rejects.toThrow( - 'Policy policy_456 is missing stripe_customer_id in app_data' - ); - - expect(mockRootService.getPolicy).toHaveBeenCalledWith('policy_456'); - expect(mockStripeSDK.paymentIntents.create).not.toHaveBeenCalled(); - }); - - it('should throw error if policy has no app_data', async () => { - const mockPolicy = { - policy_id: 'policy_456', - currency: 'ZAR', - } as any; - - (mockRootService.getPolicy as jest.Mock).mockResolvedValue(mockPolicy); - - await expect(controller.handle(validParams)).rejects.toThrow( - 'Policy policy_456 is missing stripe_customer_id in app_data' - ); - }); - - it('should handle Stripe payment intent creation failure', async () => { - const mockPolicy = { - policy_id: 'policy_456', - currency: 'ZAR', - app_data: { - stripe_customer_id: 'cus_123', - }, - } as any; - - const stripeError = new Error('Insufficient funds'); - - (mockRootService.getPolicy as jest.Mock).mockResolvedValue(mockPolicy); - (mockStripeSDK.paymentIntents.create as jest.Mock).mockRejectedValue( - stripeError - ); - - await expect(controller.handle(validParams)).rejects.toThrow( - 'Insufficient funds' - ); - - expect(mockLogService.error).toHaveBeenCalledWith( - 'Failed to create Stripe payment intent', - 'PaymentCreationController', - { - error: 'Insufficient funds', - rootPaymentId: 'payment_123', - } - ); - }); - - it('should handle policy retrieval failure', async () => { - const error = new Error('Policy not found'); - - (mockRootService.getPolicy as jest.Mock).mockRejectedValue(error); - - await expect(controller.handle(validParams)).rejects.toThrow( - 'Policy not found' - ); - - expect(mockRootService.getPolicy).toHaveBeenCalledWith('policy_456'); - expect(mockStripeSDK.paymentIntents.create).not.toHaveBeenCalled(); - }); - - it('should pass through policy currency to payment intent', async () => { - const mockPolicy = { - policy_id: 'policy_456', - currency: 'USD', - app_data: { - stripe_customer_id: 'cus_123', - }, - } as any; - - const mockPaymentIntent = { - id: 'pi_123', - status: 'succeeded', - } as Stripe.PaymentIntent; - - (mockRootService.getPolicy as jest.Mock).mockResolvedValue(mockPolicy); - (mockStripeSDK.paymentIntents.create as jest.Mock).mockResolvedValue( - mockPaymentIntent - ); - - await controller.handle(validParams); - - expect(mockStripeSDK.paymentIntents.create).toHaveBeenCalledWith( - expect.objectContaining({ - currency: 'USD', - }) - ); - }); - - it('should include metadata in payment intent', async () => { - const mockPolicy = { - policy_id: 'policy_456', - currency: 'ZAR', - app_data: { - stripe_customer_id: 'cus_123', - }, - } as any; - - const mockPaymentIntent = { - id: 'pi_123', - status: 'succeeded', - } as Stripe.PaymentIntent; - - (mockRootService.getPolicy as jest.Mock).mockResolvedValue(mockPolicy); - (mockStripeSDK.paymentIntents.create as jest.Mock).mockResolvedValue( - mockPaymentIntent - ); - - await controller.handle(validParams); - - expect(mockStripeSDK.paymentIntents.create).toHaveBeenCalledWith( - expect.objectContaining({ - metadata: { - rootPaymentId: 'payment_123', - rootPolicyId: 'policy_456', - }, - }) - ); - }); - }); -}); diff --git a/stripe_collection_module/__tests__/core/container.setup.test.ts b/stripe_collection_module/__tests__/core/container.setup.test.ts index ace0a70..a8198a4 100644 --- a/stripe_collection_module/__tests__/core/container.setup.test.ts +++ b/stripe_collection_module/__tests__/core/container.setup.test.ts @@ -47,12 +47,12 @@ describe('Container Setup', () => { expect(container.has(ServiceToken.STRIPE_SERVICE)).toBe(true); }); - it('should have controllers registered', () => { - const container = createContainer(); - - expect(container.has(ServiceToken.INVOICE_PAID_CONTROLLER)).toBe(true); - expect(container.has(ServiceToken.PAYMENT_CREATION_CONTROLLER)).toBe(true); - }); + // TODO: Add controller registration tests when controllers are implemented + // it('should have controllers registered', () => { + // const container = createContainer(); + // + // expect(container.has(ServiceToken.YOUR_CONTROLLER)).toBe(true); + // }); }); describe('getContainer', () => { diff --git a/stripe_collection_module/__tests__/services/root.service.test.ts b/stripe_collection_module/__tests__/services/root.service.test.ts index 911d05e..7df2458 100644 --- a/stripe_collection_module/__tests__/services/root.service.test.ts +++ b/stripe_collection_module/__tests__/services/root.service.test.ts @@ -10,12 +10,10 @@ describe('RootService', () => { let rootService: RootService; let mockLogService: ReturnType; let mockRootClient: ReturnType; - let mockRootSDK: any; beforeEach(() => { mockLogService = createMockLogService(); mockRootClient = createMockRootClient(); - mockRootSDK = mockRootClient.SDK; rootService = new RootService(mockLogService as any, mockRootClient as any); }); @@ -46,11 +44,11 @@ describe('RootService', () => { created_at: '2024-01-01T00:00:00Z', } as any; - mockRootSDK.getPolicyById.mockResolvedValue(mockPolicy); + mockRootClient.getPolicyById.mockResolvedValue(mockPolicy); const result = await rootService.getPolicy('policy_123'); - expect(mockRootSDK.getPolicyById).toHaveBeenCalledWith({ + expect(mockRootClient.getPolicyById).toHaveBeenCalledWith({ policyId: 'policy_123', }); expect(result).toEqual(mockPolicy); @@ -62,7 +60,7 @@ describe('RootService', () => { it('should handle errors when getting policy', async () => { const error = new Error('Policy not found'); - mockRootSDK.getPolicyById.mockRejectedValue(error); + mockRootClient.getPolicyById.mockRejectedValue(error); await expect(rootService.getPolicy('policy_123')).rejects.toThrow( 'Policy not found' @@ -77,7 +75,7 @@ describe('RootService', () => { it('should handle network errors when getting policy', async () => { const error = new Error('Network timeout'); - mockRootSDK.getPolicyById.mockRejectedValue(error); + mockRootClient.getPolicyById.mockRejectedValue(error); await expect(rootService.getPolicy('policy_456')).rejects.toThrow( 'Network timeout' @@ -92,7 +90,7 @@ describe('RootService', () => { it('should handle API errors with error codes', async () => { const error = new Error('Unauthorized'); - mockRootSDK.getPolicyById.mockRejectedValue(error); + mockRootClient.getPolicyById.mockRejectedValue(error); await expect(rootService.getPolicy('policy_789')).rejects.toThrow( 'Unauthorized' @@ -108,7 +106,7 @@ describe('RootService', () => { describe('updatePaymentStatus', () => { it('should update payment status to successful', async () => { - mockRootSDK.updatePaymentsAsync.mockResolvedValue({}); + mockRootClient.updatePaymentsAsync.mockResolvedValue({}); const params = { paymentId: 'payment_123', @@ -117,7 +115,7 @@ describe('RootService', () => { await rootService.updatePaymentStatus(params); - expect(mockRootSDK.updatePaymentsAsync).toHaveBeenCalledWith({ + expect(mockRootClient.updatePaymentsAsync).toHaveBeenCalledWith({ paymentUpdates: [ { payment_id: 'payment_123', @@ -143,7 +141,7 @@ describe('RootService', () => { }); it('should update payment status to failed with reason', async () => { - mockRootSDK.updatePaymentsAsync.mockResolvedValue({}); + mockRootClient.updatePaymentsAsync.mockResolvedValue({}); const params = { paymentId: 'payment_456', @@ -154,7 +152,7 @@ describe('RootService', () => { await rootService.updatePaymentStatus(params); - expect(mockRootSDK.updatePaymentsAsync).toHaveBeenCalledWith({ + expect(mockRootClient.updatePaymentsAsync).toHaveBeenCalledWith({ paymentUpdates: [ { payment_id: 'payment_456', @@ -172,7 +170,7 @@ describe('RootService', () => { }); it('should update payment status to pending', async () => { - mockRootSDK.updatePaymentsAsync.mockResolvedValue({}); + mockRootClient.updatePaymentsAsync.mockResolvedValue({}); const params = { paymentId: 'payment_789', @@ -181,7 +179,7 @@ describe('RootService', () => { await rootService.updatePaymentStatus(params); - expect(mockRootSDK.updatePaymentsAsync).toHaveBeenCalledWith({ + expect(mockRootClient.updatePaymentsAsync).toHaveBeenCalledWith({ paymentUpdates: [ { payment_id: 'payment_789', @@ -194,7 +192,7 @@ describe('RootService', () => { }); it('should update payment status to refunded', async () => { - mockRootSDK.updatePaymentsAsync.mockResolvedValue({}); + mockRootClient.updatePaymentsAsync.mockResolvedValue({}); const params = { paymentId: 'payment_refund', @@ -203,7 +201,7 @@ describe('RootService', () => { await rootService.updatePaymentStatus(params); - expect(mockRootSDK.updatePaymentsAsync).toHaveBeenCalledWith({ + expect(mockRootClient.updatePaymentsAsync).toHaveBeenCalledWith({ paymentUpdates: [ { payment_id: 'payment_refund', @@ -218,7 +216,7 @@ describe('RootService', () => { it('should handle errors when updating payment status', async () => { const error = new Error('API error'); - mockRootSDK.updatePaymentsAsync.mockRejectedValue(error); + mockRootClient.updatePaymentsAsync.mockRejectedValue(error); const params = { paymentId: 'payment_123', @@ -239,7 +237,7 @@ describe('RootService', () => { it('should handle validation errors', async () => { const error = new Error('Invalid payment ID'); - mockRootSDK.updatePaymentsAsync.mockRejectedValue(error); + mockRootClient.updatePaymentsAsync.mockRejectedValue(error); const params = { paymentId: 'invalid_id', @@ -260,7 +258,7 @@ describe('RootService', () => { it('should handle network errors', async () => { const error = new Error('Connection timeout'); - mockRootSDK.updatePaymentsAsync.mockRejectedValue(error); + mockRootClient.updatePaymentsAsync.mockRejectedValue(error); const params = { paymentId: 'payment_timeout', @@ -279,7 +277,7 @@ describe('RootService', () => { }); it('should update payment status with all optional parameters', async () => { - mockRootSDK.updatePaymentsAsync.mockResolvedValue({}); + mockRootClient.updatePaymentsAsync.mockResolvedValue({}); const params = { paymentId: 'payment_full', @@ -290,7 +288,7 @@ describe('RootService', () => { await rootService.updatePaymentStatus(params); - expect(mockRootSDK.updatePaymentsAsync).toHaveBeenCalledWith({ + expect(mockRootClient.updatePaymentsAsync).toHaveBeenCalledWith({ paymentUpdates: [ { payment_id: 'payment_full', @@ -318,8 +316,8 @@ describe('RootService', () => { app_data: { stripe_customer_id: 'cus_123' }, } as any; - mockRootSDK.getPolicyById.mockResolvedValue(mockPolicy); - mockRootSDK.updatePaymentsAsync.mockResolvedValue({}); + mockRootClient.getPolicyById.mockResolvedValue(mockPolicy); + mockRootClient.updatePaymentsAsync.mockResolvedValue({}); // Get policy const policy = await rootService.getPolicy('policy_123'); @@ -336,7 +334,7 @@ describe('RootService', () => { }); it('should handle multiple payment status updates', async () => { - mockRootSDK.updatePaymentsAsync.mockResolvedValue({}); + mockRootClient.updatePaymentsAsync.mockResolvedValue({}); await rootService.updatePaymentStatus({ paymentId: 'payment_1', @@ -348,17 +346,17 @@ describe('RootService', () => { status: root.PaymentStatus.Successful, }); - expect(mockRootSDK.updatePaymentsAsync).toHaveBeenCalledTimes(2); + expect(mockRootClient.updatePaymentsAsync).toHaveBeenCalledTimes(2); expect(mockLogService.info).toHaveBeenCalledTimes(4); // 2 updates * 2 logs each }); it('should handle error after successful operation', async () => { - mockRootSDK.getPolicyById.mockResolvedValue({ + mockRootClient.getPolicyById.mockResolvedValue({ policy_id: 'policy_123', } as root.Policy); const error = new Error('Update failed'); - mockRootSDK.updatePaymentsAsync.mockRejectedValue(error); + mockRootClient.updatePaymentsAsync.mockRejectedValue(error); await rootService.getPolicy('policy_123'); diff --git a/stripe_collection_module/__tests__/test-helpers.ts b/stripe_collection_module/__tests__/test-helpers.ts index 9067cd1..2b14268 100644 --- a/stripe_collection_module/__tests__/test-helpers.ts +++ b/stripe_collection_module/__tests__/test-helpers.ts @@ -82,10 +82,10 @@ export function createMockStripeSDK() { } /** - * Create a mock Root SDK - * Common structure used across Root tests + * Create a mock Root SDK/Client + * The RootClient exports the SDK namespace directly, so mock functions should be at the top level */ -export function createMockRootSDK() { +export function createMockRootClient() { return { getPolicyById: jest.fn(), updatePaymentsAsync: jest.fn(), @@ -94,15 +94,6 @@ export function createMockRootSDK() { }; } -/** - * Create a mock RootClient - */ -export function createMockRootClient() { - return { - SDK: createMockRootSDK(), - }; -} - /** * Create a mock StripeClient */ diff --git a/stripe_collection_module/__tests__/webhook-hooks.test.ts b/stripe_collection_module/__tests__/webhook-hooks.test.ts index 3f8746b..edeae3d 100644 --- a/stripe_collection_module/__tests__/webhook-hooks.test.ts +++ b/stripe_collection_module/__tests__/webhook-hooks.test.ts @@ -27,21 +27,15 @@ const { getConfigService } = require('../code/services/config-instance'); describe('Webhook Hooks', () => { let mockContainer: any; let mockLogService: ReturnType; - let mockInvoicePaidController: any; beforeEach(() => { jest.clearAllMocks(); mockLogService = createMockLogService(); - mockInvoicePaidController = { - handle: jest.fn().mockResolvedValue(undefined), - }; mockContainer = { resolve: jest.fn((token: symbol) => { if (token === ServiceToken.LOG_SERVICE) return mockLogService; - if (token === ServiceToken.INVOICE_PAID_CONTROLLER) - return mockInvoicePaidController; return null; }), }; @@ -176,7 +170,7 @@ describe('Webhook Hooks', () => { }); describe('Event Routing', () => { - it('should route invoice.paid events to InvoicePaidController', async () => { + it('should log warning for invoice.paid events (not implemented)', async () => { const invoice = { id: 'in_test123', amount_due: 5000, @@ -205,12 +199,19 @@ describe('Webhook Hooks', () => { eventId: 'evt_test', } ); - expect(mockInvoicePaidController.handle).toHaveBeenCalledWith(invoice); + expect(mockLogService.info).toHaveBeenCalledWith( + 'Received unhandled Stripe event', + 'WebhookHandler', + { + eventType: StripeEvents.InvoicePaid, + eventId: 'evt_test', + } + ); expect(result.response.status).toBe(200); expect(JSON.parse(result.response.body)).toEqual({ received: true }); }); - it('should log warning for unhandled event types', async () => { + it('should log info for unhandled event types', async () => { const event = { id: 'evt_test', type: 'customer.created', @@ -220,11 +221,12 @@ describe('Webhook Hooks', () => { const request = createWebhookRequest(event); const result = await processWebhookRequest(request); - expect(mockLogService.warn).toHaveBeenCalledWith( - 'Unhandled Stripe event type', + expect(mockLogService.info).toHaveBeenCalledWith( + 'Received unhandled Stripe event', 'WebhookHandler', { eventType: 'customer.created', + eventId: 'evt_test', } ); expect(result.response.status).toBe(200); @@ -233,39 +235,6 @@ describe('Webhook Hooks', () => { }); describe('Error Handling', () => { - it('should handle controller errors gracefully', async () => { - mockInvoicePaidController.handle.mockRejectedValue( - new Error('Controller processing failed') - ); - - const event = { - id: 'evt_test', - type: StripeEvents.InvoicePaid, - data: { - object: { - id: 'in_test', - amount_due: 1000, - metadata: {}, - }, - }, - }; - - const request = createWebhookRequest(event); - const result = await processWebhookRequest(request); - - expect(mockLogService.error).toHaveBeenCalledWith( - 'Error processing webhook', - 'WebhookHandler', - { - error: 'Controller processing failed', - } - ); - expect(result.response.status).toBe(500); - expect(JSON.parse(result.response.body)).toEqual({ - error: 'Internal server error', - }); - }); - it('should handle JSON parsing errors', async () => { const request = { request: { @@ -299,29 +268,6 @@ describe('Webhook Hooks', () => { expect(result.response.status).toBe(500); }); - it('should handle non-Error objects thrown', async () => { - mockInvoicePaidController.handle.mockRejectedValue('String error'); - - const event = { - id: 'evt_test', - type: StripeEvents.InvoicePaid, - data: { - object: { - id: 'in_test', - amount_due: 1000, - metadata: {}, - }, - }, - }; - - const request = createWebhookRequest(event); - const result = await processWebhookRequest(request); - - expect(result.response.status).toBe(500); - expect(JSON.parse(result.response.body)).toEqual({ - error: 'Internal server error', - }); - }); }); describe('Response Format', () => { @@ -365,30 +311,6 @@ describe('Webhook Hooks', () => { }); }); - it('should return proper response headers for errors', async () => { - mockInvoicePaidController.handle.mockRejectedValue(new Error('Test')); - - const event = { - id: 'evt_test', - type: StripeEvents.InvoicePaid, - data: { - object: { - id: 'in_test', - amount_due: 1000, - metadata: {}, - }, - }, - }; - - const request = createWebhookRequest(event); - const result = await processWebhookRequest(request); - - expect(result.response).toEqual({ - status: 500, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ error: 'Internal server error' }), - }); - }); }); describe('Signature Parsing Edge Cases', () => { diff --git a/stripe_collection_module/code/clients/root-client.ts b/stripe_collection_module/code/clients/root-client.ts index 585239b..5257eda 100644 --- a/stripe_collection_module/code/clients/root-client.ts +++ b/stripe_collection_module/code/clients/root-client.ts @@ -1,16 +1,19 @@ +/** + * Root Platform Client + * + * Singleton instance of the Root SDK Client initialized with configuration. + * + * @example + * import rootClient from './clients/root-client'; + * const policy = await rootClient.getPolicyById({ policyId: 'pol_123' }); + */ import { RootSDKClient } from '@rootplatform/node-sdk'; import { getConfigService } from '../services/config-instance'; -class RootClient { - public SDK: RootSDKClient; +const config = getConfigService(); +const apiKey = config.get('rootApiKey'); +const baseUrl = config.get('rootBaseUrl'); - constructor() { - const config = getConfigService(); - this.SDK = new RootSDKClient( - config.get('rootApiKey'), - config.get('rootBaseUrl') - ); - } -} +const rootClient = new RootSDKClient(apiKey, baseUrl); -export default new RootClient(); +export default rootClient; diff --git a/stripe_collection_module/code/controllers/root-event-processors/payment-creation.controller.ts b/stripe_collection_module/code/controllers/root-event-processors/payment-creation.controller.ts deleted file mode 100644 index 00e5ba5..0000000 --- a/stripe_collection_module/code/controllers/root-event-processors/payment-creation.controller.ts +++ /dev/null @@ -1,169 +0,0 @@ -/** - * Payment Creation Event Controller - * - * Handles Root Platform payment creation lifecycle hook. - * When a payment is created on Root, this creates a corresponding payment intent on Stripe. - * - * Architecture: - * - Uses dependency injection for testability - * - Delegates business logic to services - * - Focuses on orchestration only - */ - -import * as root from '@rootplatform/node-sdk'; -import Stripe from 'stripe'; -import { LogService } from '../../services/log.service'; -import { RootService } from '../../services/root.service'; -import StripeClient from '../../clients/stripe-client'; - -export interface PaymentCreationParams { - rootPaymentId: string; - rootPolicyId: string; - amount: number; - description: string; - status: root.PaymentStatus; -} - -export class PaymentCreationController { - constructor( - private readonly logService: LogService, - private readonly rootService: RootService, - private readonly stripeClient: StripeClient - ) {} - - /** - * Handle payment creation event - * - * @param params - Payment creation parameters from Root Platform - */ - async handle(params: PaymentCreationParams): Promise { - this.logService.info( - 'Processing payment creation event', - 'PaymentCreationController', - { - rootPaymentId: params.rootPaymentId, - rootPolicyId: params.rootPolicyId, - amount: params.amount, - } - ); - - // 1. Validate payment should be processed - if (!this.shouldProcessPayment(params)) { - return; - } - - // 2. Get policy details from Root - const policy = await this.rootService.getPolicy(params.rootPolicyId); - - // 3. Validate policy has Stripe customer ID - const stripeCustomerId = policy.app_data?.stripe_customer_id; - if (!stripeCustomerId) { - throw new Error( - `Policy ${policy.policy_id} is missing stripe_customer_id in app_data` - ); - } - - // 4. Create payment intent on Stripe - const paymentIntent = await this.createStripePaymentIntent({ - ...params, - stripeCustomerId, - currency: policy.currency, - }); - - this.logService.info( - 'Successfully created Stripe payment intent', - 'PaymentCreationController', - { - rootPaymentId: params.rootPaymentId, - paymentIntentId: paymentIntent.id, - status: paymentIntent.status, - } - ); - } - - /** - * Determine if payment should be processed by this collection module - */ - private shouldProcessPayment(params: PaymentCreationParams): boolean { - // Skip if payment already has a Stripe invoice - if ( - params.description.includes('Stripe created invoice item:') || - params.description.includes('Refund for Stripe charge:') - ) { - this.logService.info( - 'Skipping payment - already has associated Stripe invoice', - 'PaymentCreationController', - { - rootPaymentId: params.rootPaymentId, - description: params.description, - } - ); - return false; - } - - // Only process pending payments - if (params.status !== root.PaymentStatus.Pending) { - this.logService.info( - 'Skipping payment - not in pending status', - 'PaymentCreationController', - { - rootPaymentId: params.rootPaymentId, - status: params.status, - } - ); - return false; - } - - return true; - } - - /** - * Create payment intent on Stripe - */ - private async createStripePaymentIntent(params: { - rootPaymentId: string; - rootPolicyId: string; - amount: number; - description: string; - stripeCustomerId: string; - currency: string; - }): Promise { - this.logService.info( - 'Creating Stripe payment intent', - 'PaymentCreationController', - { - amount: params.amount, - currency: params.currency, - } - ); - - try { - const paymentIntent = - await this.stripeClient.stripeSDK.paymentIntents.create({ - amount: params.amount, - currency: params.currency, - customer: params.stripeCustomerId, - description: params.description, - metadata: { - rootPaymentId: params.rootPaymentId, - rootPolicyId: params.rootPolicyId, - }, - payment_method_types: ['card'], - confirm: true, - off_session: true, - }); - - return paymentIntent; - } catch (error: any) { - this.logService.error( - 'Failed to create Stripe payment intent', - 'PaymentCreationController', - { - error: error.message, - rootPaymentId: params.rootPaymentId, - } - ); - throw error; - } - } -} diff --git a/stripe_collection_module/code/controllers/stripe-event-processors/invoice-paid.controller.ts b/stripe_collection_module/code/controllers/stripe-event-processors/invoice-paid.controller.ts deleted file mode 100644 index 3bcdd0f..0000000 --- a/stripe_collection_module/code/controllers/stripe-event-processors/invoice-paid.controller.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Invoice Paid Event Controller - * - * Handles Stripe invoice.paid webhook events. - * When a Stripe invoice is paid, this updates the associated Root payments to successful. - * - * Architecture: - * - Uses dependency injection for testability - * - Delegates business logic to services - * - Focuses on orchestration only - */ - -import Stripe from 'stripe'; -import { PaymentStatus } from '@rootplatform/node-sdk'; -import { LogService } from '../../services/log.service'; -import { RootService } from '../../services/root.service'; -import StripeClient from '../../clients/stripe-client'; - -interface InvoicePaymentMapping { - rootPaymentId: string; - invoiceLineItemId: string; -} - -export class InvoicePaidController { - constructor( - private readonly logService: LogService, - private readonly rootService: RootService, - private readonly stripeClient: StripeClient - ) {} - - /** - * Handle invoice.paid webhook event - * - * @param invoice - Stripe invoice object from webhook - */ - async handle(invoice: Stripe.Invoice): Promise { - this.logService.info( - 'Processing invoice.paid event', - 'InvoicePaidController', - { - invoiceId: invoice.id, - amount: invoice.amount_paid, - } - ); - - // 1. Validate invoice has amount to process - if (invoice.amount_due === 0) { - this.logService.info( - 'Invoice has zero amount_due, skipping (already processed on invoice.created)', - 'InvoicePaidController', - { invoiceId: invoice.id } - ); - return; - } - - // 2. Get payment mappings from invoice metadata - const paymentMappings = this.extractPaymentMappings(invoice); - - if (!paymentMappings || paymentMappings.length === 0) { - this.logService.warn( - 'No payment mappings found in invoice metadata', - 'InvoicePaidController', - { invoiceId: invoice.id } - ); - return; - } - - // 3. Update each Root payment to successful - await this.updateRootPayments(invoice.id, paymentMappings); - - this.logService.info( - 'Successfully processed invoice.paid event', - 'InvoicePaidController', - { - invoiceId: invoice.id, - paymentsUpdated: paymentMappings.length, - } - ); - } - - /** - * Extract payment mappings from invoice metadata - */ - private extractPaymentMappings( - invoice: Stripe.Invoice - ): InvoicePaymentMapping[] | null { - const metadata = invoice.metadata; - - if (!metadata?.associatedRootPaymentIds) { - return null; - } - - try { - return JSON.parse(metadata.associatedRootPaymentIds); - } catch (error: any) { - this.logService.error( - 'Failed to parse payment mappings from invoice metadata', - 'InvoicePaidController', - { - invoiceId: invoice.id, - error: error.message, - } - ); - return null; - } - } - - /** - * Update all Root payments associated with the invoice - */ - private async updateRootPayments( - invoiceId: string, - mappings: InvoicePaymentMapping[] - ): Promise { - for (const mapping of mappings) { - try { - await this.rootService.updatePaymentStatus({ - paymentId: mapping.rootPaymentId, - status: PaymentStatus.Successful, - }); - - this.logService.info( - 'Root payment updated to successful', - 'InvoicePaidController', - { - invoiceId, - rootPaymentId: mapping.rootPaymentId, - invoiceLineItemId: mapping.invoiceLineItemId, - } - ); - } catch (error: any) { - this.logService.error( - 'Failed to update Root payment', - 'InvoicePaidController', - { - invoiceId, - rootPaymentId: mapping.rootPaymentId, - error: error.message, - } - ); - // Continue processing other payments even if one fails - } - } - } -} diff --git a/stripe_collection_module/code/core/container.setup.ts b/stripe_collection_module/code/core/container.setup.ts index c1bbe58..1410a20 100644 --- a/stripe_collection_module/code/core/container.setup.ts +++ b/stripe_collection_module/code/core/container.setup.ts @@ -94,42 +94,17 @@ export function createContainer(): Container { ServiceLifetime.SINGLETON ); - // Register Controllers (Transient - new instance per request) - container.register( - ServiceToken.INVOICE_PAID_CONTROLLER, - (c) => { - const logService = c.resolve(ServiceToken.LOG_SERVICE); - const rootService = c.resolve(ServiceToken.ROOT_SERVICE); - const stripeClient = c.resolve(ServiceToken.STRIPE_CLIENT); - - const { - InvoicePaidController, - // eslint-disable-next-line unicorn/prefer-module - } = require('../controllers/stripe-event-processors/invoice-paid.controller'); - return new InvoicePaidController(logService, rootService, stripeClient); - }, - ServiceLifetime.TRANSIENT - ); - - container.register( - ServiceToken.PAYMENT_CREATION_CONTROLLER, - (c) => { - const logService = c.resolve(ServiceToken.LOG_SERVICE); - const rootService = c.resolve(ServiceToken.ROOT_SERVICE); - const stripeClient = c.resolve(ServiceToken.STRIPE_CLIENT); - - const { - PaymentCreationController, - // eslint-disable-next-line unicorn/prefer-module - } = require('../controllers/root-event-processors/payment-creation.controller'); - return new PaymentCreationController( - logService, - rootService, - stripeClient - ); - }, - ServiceLifetime.TRANSIENT - ); + // Register Controllers here + // Example: + // container.register( + // ServiceToken.YOUR_CONTROLLER, + // (c) => { + // const logService = c.resolve(ServiceToken.LOG_SERVICE); + // const yourService = c.resolve(ServiceToken.YOUR_SERVICE); + // return new YourController(logService, yourService); + // }, + // ServiceLifetime.TRANSIENT + // ); return container; } diff --git a/stripe_collection_module/code/core/container.ts b/stripe_collection_module/code/core/container.ts index 7a57392..0e15435 100644 --- a/stripe_collection_module/code/core/container.ts +++ b/stripe_collection_module/code/core/container.ts @@ -147,9 +147,6 @@ export const ServiceToken = { STRIPE_SERVICE: Symbol('StripeService'), RENDER_SERVICE: Symbol('RenderService'), - // Controllers - Stripe Events - INVOICE_PAID_CONTROLLER: Symbol('InvoicePaidController'), - - // Controllers - Root Events - PAYMENT_CREATION_CONTROLLER: Symbol('PaymentCreationController'), + // Add your controller tokens here: + // EXAMPLE_CONTROLLER: Symbol('ExampleController'), } as const; diff --git a/stripe_collection_module/code/services/root.service.ts b/stripe_collection_module/code/services/root.service.ts index c34f2ed..7a9b011 100644 --- a/stripe_collection_module/code/services/root.service.ts +++ b/stripe_collection_module/code/services/root.service.ts @@ -29,7 +29,7 @@ export class RootService { this.logService.debug(`Getting policy: ${policyId}`, 'RootService'); try { - const result = await this.rootClient.SDK.getPolicyById({ policyId }); + const result = await this.rootClient.getPolicyById({ policyId }); return result; } catch (error: any) { this.logService.error( @@ -48,7 +48,7 @@ export class RootService { this.logService.info('Updating payment status', 'RootService', params); try { - await this.rootClient.SDK.updatePaymentsAsync({ + await this.rootClient.updatePaymentsAsync({ paymentUpdates: [ { payment_id: params.paymentId, diff --git a/stripe_collection_module/code/webhook-hooks.ts b/stripe_collection_module/code/webhook-hooks.ts index 8f9c4ea..224ed07 100644 --- a/stripe_collection_module/code/webhook-hooks.ts +++ b/stripe_collection_module/code/webhook-hooks.ts @@ -11,12 +11,10 @@ */ import * as crypto from 'crypto'; -import Stripe from 'stripe'; import { getContainer } from './core/container.setup'; import { ServiceToken } from './core/container'; import { LogService } from './services/log.service'; import { StripeEvents } from './interfaces/stripe-events'; -import { InvoicePaidController } from './controllers/stripe-event-processors/invoice-paid.controller'; import { getConfigService } from './services/config-instance'; /** @@ -101,27 +99,30 @@ export const processWebhookRequest = async (request: any) => { }); // Route to appropriate controller + // TODO: Implement your event handlers here switch (event.type) { - case StripeEvents.InvoicePaid: { - const controller = container.resolve( - ServiceToken.INVOICE_PAID_CONTROLLER - ); - await controller.handle(event.data.object as Stripe.Invoice); - break; - } - - // Add more event handlers here as needed + // Example: Handle invoice.paid events + // case StripeEvents.InvoicePaid: { + // const controller = container.resolve( + // ServiceToken.YOUR_CONTROLLER + // ); + // await controller.handle(event.data.object); + // break; + // } + + // Example: Handle payment_intent.succeeded events // case StripeEvents.PaymentIntentSucceeded: { // const controller = container.resolve( // ServiceToken.PAYMENT_INTENT_SUCCEEDED_CONTROLLER // ); - // await controller.handle(event.data.object as Stripe.PaymentIntent); + // await controller.handle(event.data.object); // break; // } default: - logService.warn('Unhandled Stripe event type', 'WebhookHandler', { + logService.info('Received unhandled Stripe event', 'WebhookHandler', { eventType: event.type, + eventId: event.id, }); } diff --git a/stripe_collection_module/jest.config.js b/stripe_collection_module/jest.config.js index eed853a..461acac 100644 --- a/stripe_collection_module/jest.config.js +++ b/stripe_collection_module/jest.config.js @@ -34,4 +34,8 @@ module.exports = { clearMocks: true, resetMocks: true, restoreMocks: true, + + // Note: --forceExit flag is added in package.json scripts to suppress exit warnings + // Uncomment detectOpenHandles below to debug test leaks if needed + // detectOpenHandles: true, }; diff --git a/stripe_collection_module/package-lock.json b/stripe_collection_module/package-lock.json index 644e423..11a394c 100644 --- a/stripe_collection_module/package-lock.json +++ b/stripe_collection_module/package-lock.json @@ -7666,9 +7666,9 @@ } }, "node_modules/stripe": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-19.2.1.tgz", - "integrity": "sha512-eRc2T413316D7fAjSSEgPbJJ+4a8KY9rOTOb27aXd7bkw9ADO/3OxmIk7YWDhWvHgvxnEZ/29YjcmBBOu4mhrw==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-19.3.0.tgz", + "integrity": "sha512-3MbqRkw5LXb4LWP1LgIEYxUAYhYDDU5pcHZj4Xha6VWPnN1wrUmQ7Htsgm8wR584s0hn1aQg1lYD0Hi+F37E5g==", "dependencies": { "qs": "^6.11.0" }, @@ -14199,9 +14199,9 @@ "dev": true }, "stripe": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-19.2.1.tgz", - "integrity": "sha512-eRc2T413316D7fAjSSEgPbJJ+4a8KY9rOTOb27aXd7bkw9ADO/3OxmIk7YWDhWvHgvxnEZ/29YjcmBBOu4mhrw==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-19.3.0.tgz", + "integrity": "sha512-3MbqRkw5LXb4LWP1LgIEYxUAYhYDDU5pcHZj4Xha6VWPnN1wrUmQ7Htsgm8wR584s0hn1aQg1lYD0Hi+F37E5g==", "requires": { "qs": "^6.11.0" } diff --git a/stripe_collection_module/package.json b/stripe_collection_module/package.json index 5c79895..56d2a75 100644 --- a/stripe_collection_module/package.json +++ b/stripe_collection_module/package.json @@ -7,12 +7,12 @@ "dist/**/*" ], "dependencies": { + "@rootplatform/node-sdk": "^0.0.7", "joi": "11.4.0", "moment-timezone": "0.5.40", "node-fetch": "2.6.7", "phone": "2.4.22", - "stripe": "14.5.0", - "@rootplatform/node-sdk": "^0.0.4" + "stripe": "^19.2.1" }, "devDependencies": { "@jest/globals": "^30.2.0", @@ -44,12 +44,12 @@ "build": "rm -rf ./dist && tsc --project tsconfig.build.json", "build:watch": "tsc --project tsconfig.build.json --watch", "clean": "rm -rf ./dist ./coverage", - "test": "jest", - "test:unit": "jest --testPathPattern='__tests__/(services|core|utils)'", - "test:integration": "jest --testPathPattern='__tests__/integration'", + "test": "jest --forceExit", + "test:unit": "jest --forceExit --testPathPattern='__tests__/(services|core|utils)'", + "test:integration": "jest --forceExit --testPathPattern='__tests__/integration'", "test:watch": "jest --watch", - "test:coverage": "jest --coverage", - "test:ci": "jest --ci --coverage --maxWorkers=2", + "test:coverage": "jest --forceExit --coverage", + "test:ci": "jest --ci --forceExit --coverage --maxWorkers=2", "precommit": "npm run lint && npm run test", "prepush": "npm run lint && npm run test && npm run build", "predeploy": "npm run validate && npm run test && npm run build", @@ -62,4 +62,4 @@ "npm": ">=8.0.0", "node": ">=18.0.0" } -} +} \ No newline at end of file diff --git a/stripe_collection_module/scripts/deploy.sh b/stripe_collection_module/scripts/deploy.sh index 271ff85..7f9bf1e 100755 --- a/stripe_collection_module/scripts/deploy.sh +++ b/stripe_collection_module/scripts/deploy.sh @@ -559,9 +559,10 @@ print_info "Publishing to: $ENVIRONMENT" print_info "API URL: $API_URL" if [ "$DRY_RUN" = false ]; then - # Make the API call + # Make the API call with Basic Auth (API key as username, no password) + # Root Platform uses the API key as the username with empty password RESPONSE=$(curl -X POST \ - -H "Authorization: Basic ${ROOT_API_KEY}" \ + -u "${ROOT_API_KEY}:" \ -w "\nHTTP_STATUS:%{http_code}" \ -s \ "$API_URL") @@ -584,7 +585,7 @@ if [ "$DRY_RUN" = false ]; then exit 1 fi else - print_info "Would execute: curl -X POST -H 'Authorization: Basic ***' '$API_URL'" + print_info "Would execute: curl -X POST -u ':' '$API_URL'" fi ############################################################################### From 4e24cc471c3c4653b3debb51192301c86f51f4d1 Mon Sep 17 00:00:00 2001 From: Marcel Smuts Date: Tue, 11 Nov 2025 17:29:53 +0200 Subject: [PATCH 12/16] =?UTF-8?q?=F0=9F=90=9B=20fix:=20registering=20rende?= =?UTF-8?q?r=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/core/container.setup.test.ts | 39 ++++++++++++------- .../__tests__/test-helpers.ts | 2 +- .../code/core/container.setup.ts | 10 +++++ .../code/webhook-hooks.ts | 1 - 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/stripe_collection_module/__tests__/core/container.setup.test.ts b/stripe_collection_module/__tests__/core/container.setup.test.ts index a8198a4..d728416 100644 --- a/stripe_collection_module/__tests__/core/container.setup.test.ts +++ b/stripe_collection_module/__tests__/core/container.setup.test.ts @@ -2,14 +2,19 @@ * Container Setup Tests */ -import { createContainer, getContainer, setContainer, resetContainer } from '../../code/core/container.setup'; +import { + createContainer, + getContainer, + setContainer, + resetContainer, +} from '../../code/core/container.setup'; import { ServiceToken } from '../../code/core/container'; describe('Container Setup', () => { describe('createContainer', () => { it('should create a container with all services registered', () => { const container = createContainer(); - + expect(container).toBeDefined(); expect(container.has(ServiceToken.CONFIG_SERVICE)).toBe(true); expect(container.has(ServiceToken.LOG_SERVICE)).toBe(true); @@ -21,8 +26,10 @@ describe('Container Setup', () => { it('should allow resolving ConfigService', () => { const container = createContainer(); - const configService = container.resolve(ServiceToken.CONFIG_SERVICE) as any; - + const configService = container.resolve( + ServiceToken.CONFIG_SERVICE + ) as any; + expect(configService).toBeDefined(); expect(configService.get).toBeDefined(); }); @@ -30,27 +37,33 @@ describe('Container Setup', () => { it('should allow resolving LogService', () => { const container = createContainer(); const logService = container.resolve(ServiceToken.LOG_SERVICE) as any; - + expect(logService).toBeDefined(); expect(logService.info).toBeDefined(); }); it('should have RootService registered', () => { const container = createContainer(); - + expect(container.has(ServiceToken.ROOT_SERVICE)).toBe(true); }); it('should have StripeService registered', () => { const container = createContainer(); - + expect(container.has(ServiceToken.STRIPE_SERVICE)).toBe(true); }); + it('should have RenderService registered', () => { + const container = createContainer(); + + expect(container.has(ServiceToken.RENDER_SERVICE)).toBe(true); + }); + // TODO: Add controller registration tests when controllers are implemented // it('should have controllers registered', () => { // const container = createContainer(); - // + // // expect(container.has(ServiceToken.YOUR_CONTROLLER)).toBe(true); // }); }); @@ -62,21 +75,21 @@ describe('Container Setup', () => { it('should return a container instance', () => { const container = getContainer(); - + expect(container).toBeDefined(); }); it('should return the same instance on multiple calls', () => { const container1 = getContainer(); const container2 = getContainer(); - + expect(container1).toBe(container2); }); it('should create container if not exists', () => { resetContainer(); const container = getContainer(); - + expect(container).toBeDefined(); expect(container.has(ServiceToken.LOG_SERVICE)).toBe(true); }); @@ -86,7 +99,7 @@ describe('Container Setup', () => { it('should allow setting a custom container', () => { const customContainer = createContainer(); setContainer(customContainer); - + const retrieved = getContainer(); expect(retrieved).toBe(customContainer); }); @@ -97,7 +110,7 @@ describe('Container Setup', () => { const container1 = getContainer(); resetContainer(); const container2 = getContainer(); - + expect(container1).not.toBe(container2); }); }); diff --git a/stripe_collection_module/__tests__/test-helpers.ts b/stripe_collection_module/__tests__/test-helpers.ts index 2b14268..50c9004 100644 --- a/stripe_collection_module/__tests__/test-helpers.ts +++ b/stripe_collection_module/__tests__/test-helpers.ts @@ -1,6 +1,6 @@ /** * Test Helpers - * + * * Centralized mock factories and test utilities to reduce duplication */ diff --git a/stripe_collection_module/code/core/container.setup.ts b/stripe_collection_module/code/core/container.setup.ts index 1410a20..c63574f 100644 --- a/stripe_collection_module/code/core/container.setup.ts +++ b/stripe_collection_module/code/core/container.setup.ts @@ -94,6 +94,16 @@ export function createContainer(): Container { ServiceLifetime.SINGLETON ); + // Register RenderService + container.register( + ServiceToken.RENDER_SERVICE, + () => { + // eslint-disable-next-line unicorn/prefer-module + return new (require('../services/render.service').RenderService)(); + }, + ServiceLifetime.SINGLETON + ); + // Register Controllers here // Example: // container.register( diff --git a/stripe_collection_module/code/webhook-hooks.ts b/stripe_collection_module/code/webhook-hooks.ts index 224ed07..157f9b9 100644 --- a/stripe_collection_module/code/webhook-hooks.ts +++ b/stripe_collection_module/code/webhook-hooks.ts @@ -14,7 +14,6 @@ import * as crypto from 'crypto'; import { getContainer } from './core/container.setup'; import { ServiceToken } from './core/container'; import { LogService } from './services/log.service'; -import { StripeEvents } from './interfaces/stripe-events'; import { getConfigService } from './services/config-instance'; /** From 5e0f051fa5be813878e01b4508ff9176d1d2afcc Mon Sep 17 00:00:00 2001 From: Marcel Smuts Date: Wed, 12 Nov 2025 11:26:28 +0200 Subject: [PATCH 13/16] =?UTF-8?q?=F0=9F=94=A8=20fix:=20updated=20scripts?= =?UTF-8?q?=20to=20be=20more=20dynamic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.sh | 11 +- stripe_collection_module/.nvmrc | 6 +- stripe_collection_module/package-lock.json | 149 +++++++++++---------- stripe_collection_module/package.json | 2 +- stripe_collection_module/scripts/deploy.sh | 14 +- 5 files changed, 100 insertions(+), 82 deletions(-) diff --git a/setup.sh b/setup.sh index c87faba..6229f5b 100755 --- a/setup.sh +++ b/setup.sh @@ -14,8 +14,15 @@ CYAN='\033[0;36m' MAGENTA='\033[0;35m' NC='\033[0m' # No Color -# Required Node version -REQUIRED_NODE_VERSION="18" +# Read required Node version from .nvmrc +NVMRC_PATH="stripe_collection_module/.nvmrc" +if [ -f "$NVMRC_PATH" ]; then + REQUIRED_NODE_VERSION=$(cat "$NVMRC_PATH" | tr -d '\n\r') +else + # Fallback to default if .nvmrc doesn't exist + REQUIRED_NODE_VERSION="18" + echo -e "${YELLOW}⚠️ .nvmrc not found, using default Node version: ${REQUIRED_NODE_VERSION}${NC}" +fi echo -e "${BLUE}╔════════════════════════════════════════════════════════╗${NC}" echo -e "${BLUE}║ Stripe Collection Module Template - Setup Wizard ║${NC}" diff --git a/stripe_collection_module/.nvmrc b/stripe_collection_module/.nvmrc index 19b3677..209e3ef 100644 --- a/stripe_collection_module/.nvmrc +++ b/stripe_collection_module/.nvmrc @@ -1,5 +1 @@ -18 - - - - +20 diff --git a/stripe_collection_module/package-lock.json b/stripe_collection_module/package-lock.json index 11a394c..aa9ed45 100644 --- a/stripe_collection_module/package-lock.json +++ b/stripe_collection_module/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "dependencies": { "@rootplatform/node-sdk": "^0.0.7", - "joi": "11.4.0", + "joi": "^17.13.3", "moment-timezone": "0.5.40", "node-fetch": "2.6.7", "phone": "2.4.22", @@ -738,6 +738,19 @@ "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==" }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -1510,6 +1523,24 @@ "oazapfts": "^5.1.7" } }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, "node_modules/@sinclair/typebox": { "version": "0.34.41", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", @@ -4618,15 +4649,6 @@ "node": ">= 0.4" } }, - "node_modules/hoek": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.3.1.tgz", - "integrity": "sha512-v7E+yIjcHECn973i0xHm4kJkEpv3C8sbYS4344WXbzYqRyiDD7rjnnKo4hsJkejQBAFdRMUGNHySeSPKSH9Rqw==", - "deprecated": "This module has moved and is now available at @hapi/hoek. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -5062,17 +5084,6 @@ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true }, - "node_modules/isemail": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz", - "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==", - "dependencies": { - "punycode": "2.x.x" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5835,17 +5846,15 @@ } }, "node_modules/joi": { - "version": "11.4.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-11.4.0.tgz", - "integrity": "sha512-O7Uw+w/zEWgbL6OcHbyACKSj0PkQeUgmehdoXVSxt92QFCq4+1390Rwh5moI2K/OgC7D8RHRZqHZxT2husMJHA==", - "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).", + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", "dependencies": { - "hoek": "4.x.x", - "isemail": "3.x.x", - "topo": "2.x.x" - }, - "engines": { - "node": ">=4.0.0" + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" } }, "node_modules/js-tokens": { @@ -7872,18 +7881,6 @@ "node": ">=8.0" } }, - "node_modules/topo": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/topo/-/topo-2.1.1.tgz", - "integrity": "sha512-ZPrPP5nwzZy1fw9abHQH2k+YarTgp9UMAztcB3MmlcZSif63Eg+az05p6wTDaZmnqpS3Mk7K+2W60iHarlz8Ug==", - "deprecated": "This module has moved and is now available at @hapi/topo. Please update your dependencies as this version is no longer maintained and may contain bugs and security issues.", - "dependencies": { - "hoek": "4.x.x" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -9103,6 +9100,19 @@ "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==" }, + "@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, + "@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, "@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -9704,6 +9714,24 @@ "oazapfts": "^5.1.7" } }, + "@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" + }, + "@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, "@sinclair/typebox": { "version": "0.34.41", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", @@ -11975,11 +12003,6 @@ "function-bind": "^1.1.2" } }, - "hoek": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.3.1.tgz", - "integrity": "sha512-v7E+yIjcHECn973i0xHm4kJkEpv3C8sbYS4344WXbzYqRyiDD7rjnnKo4hsJkejQBAFdRMUGNHySeSPKSH9Rqw==" - }, "hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -12271,14 +12294,6 @@ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true }, - "isemail": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz", - "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==", - "requires": { - "punycode": "2.x.x" - } - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -12866,13 +12881,15 @@ } }, "joi": { - "version": "11.4.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-11.4.0.tgz", - "integrity": "sha512-O7Uw+w/zEWgbL6OcHbyACKSj0PkQeUgmehdoXVSxt92QFCq4+1390Rwh5moI2K/OgC7D8RHRZqHZxT2husMJHA==", + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", "requires": { - "hoek": "4.x.x", - "isemail": "3.x.x", - "topo": "2.x.x" + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" } }, "js-tokens": { @@ -14334,14 +14351,6 @@ "is-number": "^7.0.0" } }, - "topo": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/topo/-/topo-2.1.1.tgz", - "integrity": "sha512-ZPrPP5nwzZy1fw9abHQH2k+YarTgp9UMAztcB3MmlcZSif63Eg+az05p6wTDaZmnqpS3Mk7K+2W60iHarlz8Ug==", - "requires": { - "hoek": "4.x.x" - } - }, "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", diff --git a/stripe_collection_module/package.json b/stripe_collection_module/package.json index 56d2a75..629ddb7 100644 --- a/stripe_collection_module/package.json +++ b/stripe_collection_module/package.json @@ -8,7 +8,7 @@ ], "dependencies": { "@rootplatform/node-sdk": "^0.0.7", - "joi": "11.4.0", + "joi": "^17.13.3", "moment-timezone": "0.5.40", "node-fetch": "2.6.7", "phone": "2.4.22", diff --git a/stripe_collection_module/scripts/deploy.sh b/stripe_collection_module/scripts/deploy.sh index 7f9bf1e..29bbc2e 100755 --- a/stripe_collection_module/scripts/deploy.sh +++ b/stripe_collection_module/scripts/deploy.sh @@ -526,13 +526,19 @@ fi print_info "Using rp CLI to push code..." if [ "$DRY_RUN" = false ]; then - # Run rp push - if rp push; then - print_success "Code pushed successfully with rp CLI" - else + # Run rp push and capture output + RP_OUTPUT=$(rp push 2>&1) + RP_EXIT_CODE=$? + + echo "$RP_OUTPUT" + + # Check for errors in output (rp CLI may not always return proper exit codes) + if [ $RP_EXIT_CODE -ne 0 ] || echo "$RP_OUTPUT" | grep -qi "error\|failed\|unauthenticated"; then print_error "Failed to push code with rp CLI" exit 1 fi + + print_success "Code pushed successfully with rp CLI" else print_info "Would execute: rp push" fi From d0bab5888c8b35c351243b176bf9fc642c735c34 Mon Sep 17 00:00:00 2001 From: Marcel Smuts Date: Thu, 13 Nov 2025 15:07:58 +0200 Subject: [PATCH 14/16] =?UTF-8?q?=F0=9F=94=A8=20fix:=20fixed=20setup=20scr?= =?UTF-8?q?ipt=20with=20new=20collection=20modules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.sh b/setup.sh index 6229f5b..7e3b2d2 100755 --- a/setup.sh +++ b/setup.sh @@ -288,13 +288,14 @@ if [ -n "$cm_key" ] && [ -n "$api_key" ]; then echo "" # Create the collection module on Root Platform + # Note: Not specifying key_of_collection_module_to_clone creates a blank module + # This is correct for template setup - code will be deployed via rp push CM_CREATE_RESPONSE=$(curl -X POST \ -u "${api_key}:" \ -H "Content-Type: application/json" \ -d "{ \"key\": \"${cm_key}\", - \"name\": \"${cm_name}\", - \"key_of_collection_module_to_clone\": \"blank_starter_template\" + \"name\": \"${cm_name}\" }" \ -w "\nHTTP_STATUS:%{http_code}" \ -s \ @@ -315,7 +316,7 @@ if [ -n "$cm_key" ] && [ -n "$api_key" ]; then echo -e "${BLUE}ℹ️ You may need to create it manually:${NC}" echo -e "${CYAN} curl -X POST -u '\$API_KEY:' \\${NC}" echo -e "${CYAN} -H 'Content-Type: application/json' \\${NC}" - echo -e "${CYAN} -d '{\"key\":\"${cm_key}\",\"name\":\"${cm_name}\",\"key_of_collection_module_to_clone\":\"blank_starter_template\"}' \\${NC}" + echo -e "${CYAN} -d '{\"key\":\"${cm_key}\",\"name\":\"${cm_name}\"}' \\${NC}" echo -e "${CYAN} '${host}/v1/apps/${org_id}/insurance/collection-modules'${NC}" fi else From dce6600964bcd1258fa4f54b63f709f04d153145 Mon Sep 17 00:00:00 2001 From: Marcel Smuts Date: Thu, 13 Nov 2025 16:53:07 +0200 Subject: [PATCH 15/16] =?UTF-8?q?=E2=9C=A8=20feat:=20using=20package=20ver?= =?UTF-8?q?sion=20for=20deployment=20tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- stripe_collection_module/docs/DEPLOYMENT.md | 8 +++ stripe_collection_module/scripts/deploy.sh | 55 +++++++++++++++++---- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/stripe_collection_module/docs/DEPLOYMENT.md b/stripe_collection_module/docs/DEPLOYMENT.md index 0385699..577d055 100644 --- a/stripe_collection_module/docs/DEPLOYMENT.md +++ b/stripe_collection_module/docs/DEPLOYMENT.md @@ -153,6 +153,10 @@ After publishing, verify your deployment: Deploy to sandbox for testing: +```bash +npm run deploy:sandbox +``` + ```bash curl -X POST \ -H "Authorization: Basic {{sandbox_api_key}}" \ @@ -169,6 +173,10 @@ curl -X POST \ Deploy to production after thorough testing: +```bash +npm run deploy:production +``` + ```bash curl -X POST \ -H "Authorization: Basic {{production_api_key}}" \ diff --git a/stripe_collection_module/scripts/deploy.sh b/stripe_collection_module/scripts/deploy.sh index 29bbc2e..6d73796 100755 --- a/stripe_collection_module/scripts/deploy.sh +++ b/stripe_collection_module/scripts/deploy.sh @@ -13,9 +13,10 @@ # ./scripts/deploy.sh [environment] [version] # # Examples: -# ./scripts/deploy.sh sandbox # Deploy to sandbox -# ./scripts/deploy.sh sandbox v1.0.0 # Deploy to sandbox with tag -# ./scripts/deploy.sh production v1.0.0 # Deploy to production with tag +# ./scripts/deploy.sh sandbox # Deploy to sandbox (uses package.json version) +# ./scripts/deploy.sh sandbox v1.0.0 # Deploy to sandbox with specific version +# ./scripts/deploy.sh production # Deploy to production (uses package.json version) +# ./scripts/deploy.sh production v1.0.0 # Deploy to production with specific version # # Prerequisites: # - Run bash ../setup.sh to configure required files @@ -117,6 +118,30 @@ load_env_config() { fi } +load_package_version() { + local package_file="$PROJECT_DIR/package.json" + + if [ ! -f "$package_file" ]; then + print_error "package.json not found at $package_file" + exit 1 + fi + + # Check if jq is available for JSON parsing + if command -v jq &> /dev/null; then + PACKAGE_VERSION=$(jq -r '.version // empty' "$package_file") + else + # Fallback to grep/sed if jq is not available + PACKAGE_VERSION=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$package_file" | sed 's/.*"\([^"]*\)"$/\1/') + fi + + if [ -n "$PACKAGE_VERSION" ]; then + # Ensure version has 'v' prefix + if [[ ! "$PACKAGE_VERSION" =~ ^v ]]; then + PACKAGE_VERSION="v${PACKAGE_VERSION}" + fi + fi +} + ############################################################################### # Helper Functions ############################################################################### @@ -161,7 +186,7 @@ Configuration: Arguments: environment Target environment: 'sandbox' or 'production' - version Git tag version (e.g., v1.0.0) - optional for sandbox + version Git tag version (e.g., v1.0.0) - optional, defaults to package.json version Options: -k, --api-key KEY Override Root Platform API key (from .root-auth) @@ -175,20 +200,23 @@ Options: --help Show this help message Examples: - # Deploy to sandbox (using config files) + # Deploy to sandbox (uses package.json version) $0 sandbox - # Deploy to sandbox with version tag + # Deploy to sandbox with specific version tag $0 sandbox v1.0.0 - # Deploy to production + # Deploy to production (uses package.json version) + $0 production + + # Deploy to production with specific version $0 production v1.1.0 # Deploy with overridden API key $0 -k "prod_key_123" production v1.1.0 # Dry run for production - $0 --dry-run production v1.0.0 + $0 --dry-run production Configuration Files: .root-config.json Organization ID, Module Key, API Host @@ -277,6 +305,15 @@ fi # Load environment configuration from env.ts load_env_config +# Load version from package.json if not provided +if [ -z "$VERSION" ]; then + load_package_version + if [ -n "$PACKAGE_VERSION" ]; then + VERSION="$PACKAGE_VERSION" + print_info "Using version from package.json: $VERSION" + fi +fi + echo "" ############################################################################### @@ -296,7 +333,7 @@ fi if [ "$ENVIRONMENT" = "production" ] && [ -z "$VERSION" ]; then print_error "Version is required for production deployment" - print_info "Usage: $0 production v1.0.0" + print_info "Update package.json version and run the script again" exit 1 fi From 903fd919de0e7eaef700d58bcbba7a367fc5ce2f Mon Sep 17 00:00:00 2001 From: Marcel Smuts Date: Mon, 17 Nov 2025 10:41:47 +0200 Subject: [PATCH 16/16] =?UTF-8?q?=E2=9C=A8=20feat:=20finalized=20cm=20upda?= =?UTF-8?q?te?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- stripe_collection_module/.nvmrc | 1 + stripe_collection_module/.root-config.json | 6 +++--- stripe_collection_module/.root-config.json.sample | 1 + .../__tests__/helpers/factories.ts | 12 ++++-------- .../code/interfaces/stripe-events.ts | 5 +++++ stripe_collection_module/code/webhook-hooks.ts | 10 ++++++++++ stripe_collection_module/docs/ROOT_CONFIGURATION.md | 2 -- stripe_collection_module/docs/TESTING.md | 6 +----- stripe_collection_module/package.json | 4 ++-- 9 files changed, 27 insertions(+), 20 deletions(-) diff --git a/stripe_collection_module/.nvmrc b/stripe_collection_module/.nvmrc index 209e3ef..35f4978 100644 --- a/stripe_collection_module/.nvmrc +++ b/stripe_collection_module/.nvmrc @@ -1 +1,2 @@ 20 + diff --git a/stripe_collection_module/.root-config.json b/stripe_collection_module/.root-config.json index d93c1d7..5e688b3 100644 --- a/stripe_collection_module/.root-config.json +++ b/stripe_collection_module/.root-config.json @@ -1,8 +1,8 @@ { - "collectionModuleKey": "demo_stripe_v2", + "collectionModuleKey": "demo_stripe_v3", "collectionModuleName": "Demo Stripe V2", - "organizationId": "c598807f-c59e-46ff-a4cb-99860be57a52", - "host": "https://sandbox.uk.rootplatform.com", + "organizationId": "00000000-0000-0000-0000-000000000001", + "host": "http://localhost:4000", "settings": {}, "manualTransactions": [] } diff --git a/stripe_collection_module/.root-config.json.sample b/stripe_collection_module/.root-config.json.sample index 6716e4d..29ab2c5 100644 --- a/stripe_collection_module/.root-config.json.sample +++ b/stripe_collection_module/.root-config.json.sample @@ -10,3 +10,4 @@ } + diff --git a/stripe_collection_module/__tests__/helpers/factories.ts b/stripe_collection_module/__tests__/helpers/factories.ts index 7270ade..5035e29 100644 --- a/stripe_collection_module/__tests__/helpers/factories.ts +++ b/stripe_collection_module/__tests__/helpers/factories.ts @@ -9,7 +9,7 @@ import Stripe from 'stripe'; * Create a mock Stripe customer */ export const createMockStripeCustomer = ( - overrides?: Partial, + overrides?: Partial ): Stripe.Customer => { return { id: 'cus_test_123', @@ -28,7 +28,7 @@ export const createMockStripeCustomer = ( * Create a mock Stripe payment method */ export const createMockStripePaymentMethod = ( - overrides?: Partial, + overrides?: Partial ): Stripe.PaymentMethod => { return { id: 'pm_test_123', @@ -65,7 +65,7 @@ export const createMockStripePaymentMethod = ( * Create a mock Stripe subscription */ export const createMockStripeSubscription = ( - overrides?: Partial, + overrides?: Partial ): Stripe.Subscription => { return { id: 'sub_test_123', @@ -90,7 +90,7 @@ export const createMockStripeSubscription = ( * Create a mock Stripe invoice */ export const createMockStripeInvoice = ( - overrides?: Partial, + overrides?: Partial ): Stripe.Invoice => { return { id: 'in_test_123', @@ -169,7 +169,3 @@ export const createMockRootPayment = (overrides?: any): any => { ...overrides, }; }; - - - - diff --git a/stripe_collection_module/code/interfaces/stripe-events.ts b/stripe_collection_module/code/interfaces/stripe-events.ts index 61bb8e1..f834584 100644 --- a/stripe_collection_module/code/interfaces/stripe-events.ts +++ b/stripe_collection_module/code/interfaces/stripe-events.ts @@ -26,6 +26,11 @@ export const StripeEvents = { PaymentIntentSucceeded: 'payment_intent.succeeded', PaymentIntentFailed: 'payment_intent.payment_failed', PaymentIntentCanceled: 'payment_intent.canceled', + + // Setup Intent events + SetupIntentSucceeded: 'setup_intent.succeeded', + SetupIntentFailed: 'setup_intent.setup_attempt.failed', + SetupIntentCanceled: 'setup_intent.canceled', } as const; // Type for the event names diff --git a/stripe_collection_module/code/webhook-hooks.ts b/stripe_collection_module/code/webhook-hooks.ts index 157f9b9..9c4a48c 100644 --- a/stripe_collection_module/code/webhook-hooks.ts +++ b/stripe_collection_module/code/webhook-hooks.ts @@ -15,6 +15,7 @@ import { getContainer } from './core/container.setup'; import { ServiceToken } from './core/container'; import { LogService } from './services/log.service'; import { getConfigService } from './services/config-instance'; +import { StripeEvents } from './interfaces/stripe-events'; /** * Verify Stripe webhook signature @@ -100,6 +101,15 @@ export const processWebhookRequest = async (request: any) => { // Route to appropriate controller // TODO: Implement your event handlers here switch (event.type) { + // Handle setup intent succeeded - notify frontend + case StripeEvents.SetupIntentSucceeded: { + logService.info('Setup intent succeeded', 'WebhookHandler', { + setupIntentId: event.data.object.id, + paymentMethod: event.data.object.payment_method, + }); + break; + } + // Example: Handle invoice.paid events // case StripeEvents.InvoicePaid: { // const controller = container.resolve( diff --git a/stripe_collection_module/docs/ROOT_CONFIGURATION.md b/stripe_collection_module/docs/ROOT_CONFIGURATION.md index 49bf8fc..155afe9 100644 --- a/stripe_collection_module/docs/ROOT_CONFIGURATION.md +++ b/stripe_collection_module/docs/ROOT_CONFIGURATION.md @@ -323,5 +323,3 @@ If you encounter issues with Root Platform configuration: 2. Verify your API key in Root Platform dashboard 3. Review Root Platform logs for errors 4. Contact your Root Platform representative - - diff --git a/stripe_collection_module/docs/TESTING.md b/stripe_collection_module/docs/TESTING.md index b82d3d0..99cebf6 100644 --- a/stripe_collection_module/docs/TESTING.md +++ b/stripe_collection_module/docs/TESTING.md @@ -467,8 +467,4 @@ beforeEach(() => { - Check existing tests for examples - See Jest documentation: https://jestjs.io/ -- Ask in team chat - - - - +- Ask in team chat \ No newline at end of file diff --git a/stripe_collection_module/package.json b/stripe_collection_module/package.json index 629ddb7..c328b7a 100644 --- a/stripe_collection_module/package.json +++ b/stripe_collection_module/package.json @@ -7,12 +7,12 @@ "dist/**/*" ], "dependencies": { - "@rootplatform/node-sdk": "^0.0.7", "joi": "^17.13.3", "moment-timezone": "0.5.40", "node-fetch": "2.6.7", "phone": "2.4.22", - "stripe": "^19.2.1" + "stripe": "^19.2.1", + "@rootplatform/node-sdk": "^0.0.7" }, "devDependencies": { "@jest/globals": "^30.2.0",