diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c1d3ccd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +# Enforce LF line endings for all text files on all platforms +* text=auto eol=lf + +# Explicitly declare binary files to prevent corruption +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.woff binary +*.woff2 binary +*.ttf binary +*.otf binary +*.eot binary diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index fc872ed..db75d25 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -9,8 +9,13 @@ permissions: jobs: validate: - name: CI - PR Validation - runs-on: ubuntu-latest + name: CI - PR Validation (Node ${{ matrix.node-version }} / ${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + node-version: [20, 22] steps: - name: Checkout @@ -19,7 +24,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: 20 + node-version: ${{ matrix.node-version }} cache: npm - name: Install diff --git a/.prettierrc b/.prettierrc index bccee91..f504493 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,5 +2,6 @@ "semi": true, "singleQuote": false, "trailingComma": "all", - "printWidth": 100 + "printWidth": 100, + "endOfLine": "lf" } diff --git a/README.md b/README.md index 6f35ec5..64450ed 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,148 @@ -# NestJS DeveloperKit (Template) +# @ciscode/audit-kit -Template repository for building reusable NestJS npm packages. +AuditKit is a reusable NestJS module for immutable audit logging with clean architecture (`core` / `infra` / `nest`). -## What you get +It provides: -- ESM + CJS + Types build (tsup) -- Jest testing -- ESLint + Prettier -- Changesets (Release PR flow) -- Husky (pre-commit + pre-push) -- Enforced package architecture (core / infra / nest) with strict public API +- Framework-free core service (`AuditService`) +- Pluggable repositories (MongoDB, in-memory) +- Automatic change detection +- Configurable redaction, idempotency, and retention policies +- Cursor-based pagination for stable listing +- Observability hooks (OpenTelemetry-friendly observer port) +- Event streaming hooks (publisher port + default EventEmitter adapter) -## Scripts +## Install -- `npm run build` – build to `dist/` -- `npm test` – run tests -- `npm run typecheck` – TypeScript typecheck -- `npm run lint` – ESLint -- `npm run format` / `npm run format:write` – Prettier -- `npx changeset` – create a changeset +```bash +npm install @ciscode/audit-kit +``` -## Release flow (summary) +Peer dependencies are managed by consuming applications. -- Work on a `feature` branch from `develop` -- Merge to `develop` -- Add a changeset for user-facing changes: `npx changeset` -- Automation opens “Version Packages” PR into `master` -- Merge to `master`, tag `vX.Y.Z` to publish +## Quick Start -See `docs/RELEASE.md` for details. +```typescript +import { Module } from "@nestjs/common"; +import { AuditKitModule } from "@ciscode/audit-kit"; + +@Module({ + imports: [ + AuditKitModule.register({ + repository: { + type: "in-memory", + }, + redaction: { + enabled: true, + fields: ["actor.email", "metadata.password"], + mask: "[REDACTED]", + }, + idempotency: { + enabled: true, + keyStrategy: "idempotencyKey", + }, + }), + ], +}) +export class AppModule {} +``` + +## Usage + +```typescript +import { Injectable } from "@nestjs/common"; +import { ActorType, AuditActionType, AuditService } from "@ciscode/audit-kit"; + +@Injectable() +export class UserService { + constructor(private readonly auditService: AuditService) {} + + async updateUser(): Promise { + await this.auditService.log({ + actor: { id: "user-1", type: ActorType.USER, email: "user@example.com" }, + action: AuditActionType.UPDATE, + resource: { type: "user", id: "user-1" }, + metadata: { reason: "profile update" }, + idempotencyKey: "req-123", + }); + } +} +``` + +## Advanced Features + +### Cursor Pagination + +```typescript +const page1 = await auditService.queryWithCursor({ actorId: "user-1" }, { limit: 20 }); + +if (page1.hasMore) { + const page2 = await auditService.queryWithCursor( + { actorId: "user-1" }, + { limit: 20, cursor: page1.nextCursor }, + ); +} +``` + +### Observability Hooks + +Provide an observer to emit metrics/spans/log events (for OpenTelemetry, Prometheus, etc.): + +```typescript +AuditKitModule.register({ + repository: { type: "in-memory" }, + observer: { + onEvent(event) { + // event.operation, event.durationMs, event.success + console.log(event); + }, + }, +}); +``` + +### Event Streaming + +Emit domain events after successful audit creation: + +```typescript +AuditKitModule.register({ + repository: { type: "in-memory" }, + eventStreaming: { + enabled: true, + // optional custom publisher; default is EventEmitter adapter + publisher: { + publish(event) { + console.log(event.type, event.payload.id); + }, + }, + }, +}); +``` + +## Tooling + +- Tests: `npm test` +- Typecheck: `npm run typecheck` +- Lint: `npm run lint` +- Build: `npm run build` +- Mutation testing: `npm run mutation` +- Benchmarks: `npm run bench` + +## CI Compatibility Matrix + +PR validation runs on a matrix to catch environment regressions early: + +- Node.js: 20, 22 +- OS: ubuntu-latest, windows-latest + +See [.github/workflows/pr-validation.yml](.github/workflows/pr-validation.yml). + +## Release Flow (Summary) + +1. Work on a feature branch from `develop` +2. Add a changeset for user-facing changes: `npx changeset` +3. Merge into `develop` +4. Automation opens "Version Packages" PR into `master` +5. Merge and publish + +See [docs/RELEASE.md](docs/RELEASE.md) for details. diff --git a/benchmarks/audit-service.bench.ts b/benchmarks/audit-service.bench.ts new file mode 100644 index 0000000..9595b52 --- /dev/null +++ b/benchmarks/audit-service.bench.ts @@ -0,0 +1,255 @@ +/** + * ============================================================================ + * AUDIT SERVICE PERFORMANCE BENCHMARKS + * ============================================================================ + * + * Measures throughput and latency of core AuditService operations using + * Vitest's built-in bench runner (backed by tinybench). + * + * Run with: npm run bench + * + * Benchmarks cover: + * - log() — single audit log creation + * - logWithChanges() — creation with automatic change detection + * - query() — offset-paginated query + * - queryWithCursor() — cursor-paginated query + * - getByActor() — actor-based lookup + * - getByResource() — resource history lookup + * + * All benchmarks use InMemoryAuditRepository to isolate service logic + * from I/O, giving a reliable baseline for the core layer's overhead. + * + * @packageDocumentation + */ + +import { bench, describe, beforeAll } from "vitest"; + +import { AuditService } from "../src/core/audit.service"; +import type { IChangeDetector } from "../src/core/ports/change-detector.port"; +import type { IIdGenerator } from "../src/core/ports/id-generator.port"; +import type { ITimestampProvider } from "../src/core/ports/timestamp-provider.port"; +import { ActorType, AuditActionType } from "../src/core/types"; +import { InMemoryAuditRepository } from "../src/infra/repositories/in-memory/in-memory-audit.repository"; + +// ============================================================================ +// BENCHMARK INFRASTRUCTURE +// ============================================================================ + +let counter = 0; + +/** Minimal ID generator — avoids nanoid ESM overhead in benchmarks. */ +const idGenerator: IIdGenerator = { + generate: (opts) => `${opts?.prefix ?? ""}bench_${++counter}`, + generateBatch: (count, opts) => + Array.from({ length: count }, () => `${opts?.prefix ?? ""}bench_${++counter}`), + isValid: () => true, + extractMetadata: () => null, + getInfo: () => ({ + name: "bench", + version: "1.0.0", + defaultLength: 21, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + sortable: false, + encoding: null, + }), +}; + +/** Minimal timestamp provider. */ +const timestampProvider: ITimestampProvider = { + now: () => new Date(), + format: () => "", + parse: () => new Date(), + isValid: () => true, + startOfDay: () => new Date(), + endOfDay: () => new Date(), + diff: () => 0, + freeze: () => {}, + advance: () => {}, + unfreeze: () => {}, + getInfo: () => ({ + name: "bench", + version: "1.0.0", + source: "system-clock", + timezone: "utc" as const, + precision: "millisecond" as const, + frozen: false, + }), +}; + +/** Minimal change detector. */ +const changeDetector: IChangeDetector = { + detectChanges: async (before, after) => { + const changes: Record = {}; + const allKeys = new Set([...Object.keys(before), ...Object.keys(after)]); + for (const key of allKeys) { + if (before[key] !== after[key]) { + changes[key] = { from: before[key], to: after[key] }; + } + } + return changes; + }, + hasChanged: (before, after) => before !== after, + maskValue: () => "***", + formatChanges: (changes) => Object.keys(changes).join(", "), +}; + +/** Sample actor used across benchmarks. */ +const benchActor = { + id: "bench-user-1", + type: ActorType.USER as const, + name: "Bench User", +}; + +/** Sample resource used across benchmarks. */ +const benchResource = { + type: "order", + id: "order-bench-1", +}; + +// ============================================================================ +// log() — SINGLE LOG CREATION +// ============================================================================ + +describe("AuditService.log()", () => { + const repository = new InMemoryAuditRepository(); + const service = new AuditService(repository, idGenerator, timestampProvider, changeDetector); + + bench("log() — create audit log (no options)", async () => { + await service.log({ + actor: benchActor, + action: AuditActionType.UPDATE, + resource: benchResource, + }); + }); + + bench("log() — with metadata and reason", async () => { + await service.log({ + actor: benchActor, + action: AuditActionType.ACCESS, + resource: benchResource, + metadata: { reason: "GDPR request", requestId: "req-123" }, + reason: "User data export", + ipAddress: "127.0.0.1", + }); + }); +}); + +// ============================================================================ +// logWithChanges() — CHANGE DETECTION + CREATION +// ============================================================================ + +describe("AuditService.logWithChanges()", () => { + const repository = new InMemoryAuditRepository(); + const service = new AuditService(repository, idGenerator, timestampProvider, changeDetector); + + const before = { name: "Widget", price: 100, status: "draft", stock: 50 }; + const after = { name: "Widget Pro", price: 120, status: "published", stock: 45 }; + + bench("logWithChanges() — 4 field changes", async () => { + await service.logWithChanges({ + actor: benchActor, + action: AuditActionType.UPDATE, + resource: benchResource, + before, + after, + }); + }); +}); + +// ============================================================================ +// query() — OFFSET PAGINATION +// ============================================================================ + +describe("AuditService.query() — offset pagination", () => { + const repository = new InMemoryAuditRepository(); + const service = new AuditService(repository, idGenerator, timestampProvider, changeDetector); + + // Seed 500 logs before the benchmarks run + beforeAll(async () => { + const seeds = Array.from({ length: 500 }, (_, i) => ({ + id: `seed-${i}`, + timestamp: new Date(Date.now() - i * 1000), + actor: { id: `user-${i % 10}`, type: ActorType.USER as const, name: `User ${i % 10}` }, + action: i % 2 === 0 ? AuditActionType.UPDATE : AuditActionType.CREATE, + resource: { type: "order", id: `order-${i}` }, + })); + await Promise.all(seeds.map((log) => repository.create(log))); + }); + + bench("query() — first page, no filters", async () => { + await service.query({ limit: 20, page: 1 }); + }); + + bench("query() — filtered by actorId", async () => { + await service.query({ actorId: "user-3", limit: 20, page: 1 }); + }); + + bench("query() — filtered by action", async () => { + await service.query({ action: AuditActionType.UPDATE, limit: 20, page: 1 }); + }); +}); + +// ============================================================================ +// queryWithCursor() — CURSOR PAGINATION +// ============================================================================ + +describe("AuditService.queryWithCursor() — cursor pagination", () => { + const repository = new InMemoryAuditRepository(); + const service = new AuditService(repository, idGenerator, timestampProvider, changeDetector); + + let nextCursor: string | undefined; + + beforeAll(async () => { + const seeds = Array.from({ length: 500 }, (_, i) => ({ + id: `cursor-seed-${i}`, + timestamp: new Date(Date.now() - i * 1000), + actor: { id: `user-${i % 5}`, type: ActorType.USER as const, name: `User ${i % 5}` }, + action: AuditActionType.UPDATE, + resource: { type: "document", id: `doc-${i}` }, + })); + await Promise.all(seeds.map((log) => repository.create(log))); + + // Grab a cursor for the "next page" benchmark + const page1 = await service.queryWithCursor({}, { limit: 20 }); + nextCursor = page1.nextCursor; + }); + + bench("queryWithCursor() — first page", async () => { + await service.queryWithCursor({}, { limit: 20 }); + }); + + bench("queryWithCursor() — second page (using cursor)", async () => { + await service.queryWithCursor( + {}, + { limit: 20, ...(nextCursor === undefined ? {} : { cursor: nextCursor }) }, + ); + }); +}); + +// ============================================================================ +// getByActor() / getByResource() — LOOKUP METHODS +// ============================================================================ + +describe("AuditService — lookup methods", () => { + const repository = new InMemoryAuditRepository(); + const service = new AuditService(repository, idGenerator, timestampProvider, changeDetector); + + beforeAll(async () => { + const seeds = Array.from({ length: 200 }, (_, i) => ({ + id: `lookup-seed-${i}`, + timestamp: new Date(Date.now() - i * 500), + actor: { id: `actor-${i % 5}`, type: ActorType.USER as const, name: `Actor ${i % 5}` }, + action: AuditActionType.UPDATE, + resource: { type: "product", id: `product-${i % 20}` }, + })); + await Promise.all(seeds.map((log) => repository.create(log))); + }); + + bench("getByActor() — actor with ~40 logs", async () => { + await service.getByActor("actor-2"); + }); + + bench("getByResource() — resource with ~10 logs", async () => { + await service.getByResource("product", "product-5"); + }); +}); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 383fe83..cbb6c51 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -2,12 +2,19 @@ This repository is a template for NestJS _packages_ (not apps). +AuditKit is implemented as a CSR-style modular package with ports/adapters. + ## Layers (inside `src/`) - `core/`: pure logic (no Nest imports) + - Ports: repository, change detector, observer, event publisher + - Service: `AuditService` - `infra/`: adapters/implementations (may depend on `core/`) + - Repositories: MongoDB, in-memory + - Providers: id generator, timestamp, change detector, event publisher (EventEmitter) - Internal by default (not exported publicly) - `nest/`: Nest module wiring (DI, providers, DynamicModule) + - Validation and runtime option mapping ## Dependency Rules @@ -21,3 +28,22 @@ Consumers import only from the package root entrypoint. All public exports must go through `src/index.ts`. Folders like `infra/` are internal unless explicitly re-exported. + +## Data Flow + +1. App calls `AuditService.log()` or `AuditService.logWithChanges()`. +2. Core validates actor, applies idempotency and redaction policies. +3. Repository persists immutable audit entries. +4. Optional retention/archival cleanup runs after write. +5. Optional observer hook emits operation telemetry metadata. +6. Optional event publisher emits `audit.log.created` to a stream. + +## Querying Models + +- Offset pagination: `query()` for page/limit/sort use-cases. +- Cursor pagination: `queryWithCursor()` for stable keyset iteration. + +Cursor pagination uses an opaque cursor encoding `{ timestamp, id }` and sorts by: + +- `timestamp DESC` +- `id ASC` diff --git a/docs/RELEASE.md b/docs/RELEASE.md index f6fc8b1..48ccf47 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -18,6 +18,26 @@ Changesets controls versions and changelog via a “Version Packages” PR. 3. If it affects users, run: `npx changeset` 4. Open PR → `develop` +## Quality Gates (before PR) + +Run locally before opening a PR: + +1. `npm run typecheck` +2. `npm run lint` +3. `npm test` +4. Optional advanced checks: + - `npm run mutation` + - `npm run bench` + +## CI Compatibility Matrix + +PR validation runs across: + +- Node.js 20 and 22 +- Ubuntu and Windows runners + +This matrix helps detect runtime and tooling regressions before merge. + ## Release 1. Automation opens “Version Packages” PR from `develop` → `master` diff --git a/package-lock.json b/package-lock.json index cb2bf59..629a934 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,8 @@ "devDependencies": { "@changesets/cli": "^2.27.7", "@nestjs/testing": "^11.1.17", + "@stryker-mutator/core": "^9.0.0", + "@stryker-mutator/jest-runner": "^9.0.0", "@types/jest": "^29.5.14", "@types/node": "^22.10.7", "date-fns": "^4.1.0", @@ -30,7 +32,8 @@ "ts-node": "^10.9.2", "tsup": "^8.3.5", "typescript": "^5.7.3", - "typescript-eslint": "^8.50.1" + "typescript-eslint": "^8.50.1", + "vitest": "^3.0.0" }, "engines": { "node": ">=20" @@ -45,14 +48,28 @@ "rxjs": "^7" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@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==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -112,14 +129,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -128,6 +145,19 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "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", @@ -155,6 +185,38 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/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, + "license": "ISC", + "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", @@ -165,30 +227,44 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "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==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "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==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -197,12 +273,57 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { "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==", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, "engines": { "node": ">=6.9.0" } @@ -252,13 +373,13 @@ } }, "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==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -267,6 +388,42 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.27.1.tgz", + "integrity": "sha512-DTxe4LBPrtFdsWzgpmbBKevg3e9PBy+dXRt19kSbucbZvL2uqtdqwwpluL1jfxYE0wIDTFp1nTy/q6gNLsxXrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-decorators": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-explicit-resource-management": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-explicit-resource-management/-/plugin-proposal-explicit-resource-management-7.27.4.tgz", + "integrity": "sha512-1SwtCDdZWQvUU1i7wt/ihP7W38WjC3CSTOHAl+Xnbze8+bbMNjRvRQydnj0k9J1jPqCAZctBFp6NHJXkrVVmEA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-explicit-resource-management instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.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", @@ -322,6 +479,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz", + "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "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", @@ -491,13 +664,87 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-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==", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", + "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -517,33 +764,33 @@ } }, "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==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "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==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -551,9 +798,9 @@ } }, "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==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { @@ -1778,15 +2025,28 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@inquirer/external-editor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", - "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", "dev": true, "license": "MIT", "dependencies": { - "chardet": "^2.1.1", - "iconv-lite": "^0.7.0" + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -1800,65 +2060,395 @@ } } }, - "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==", + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "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==", + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", "dev": true, "license": "MIT", "dependencies": { - "sprintf-js": "~1.0.2" + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "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==", + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", "dev": true, "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "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==", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "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, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "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, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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, "license": "MIT", "dependencies": { @@ -2983,6 +3573,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -2990,15 +3587,28 @@ "dev": true, "license": "MIT" }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } }, "node_modules/@sinonjs/fake-timers": { "version": "10.3.0", @@ -3010,6 +3620,375 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@stryker-mutator/api": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/api/-/api-9.0.0.tgz", + "integrity": "sha512-UDgCJDYqhyUr206kXTK3DqwEEt4G60DhY61o77md67Q746XDjidrR1Vn93WdYomHLJYKd9jEwxnJcCQ6wWV8ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mutation-testing-metrics": "3.5.1", + "mutation-testing-report-schema": "3.5.1", + "tslib": "~2.8.0", + "typed-inject": "~5.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stryker-mutator/core": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/core/-/core-9.0.0.tgz", + "integrity": "sha512-Fnt4teVbgOpO+suSsVxF3r6wV1nui79DKchpWS2O9+9p3oiMJLq7/nMthaH9l+VTmD1xFdgRpcgtNdijg0XKsg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@inquirer/prompts": "^7.0.0", + "@stryker-mutator/api": "9.0.0", + "@stryker-mutator/instrumenter": "9.0.0", + "@stryker-mutator/util": "9.0.0", + "ajv": "~8.17.1", + "chalk": "~5.4.0", + "commander": "~13.1.0", + "diff-match-patch": "1.0.5", + "emoji-regex": "~10.4.0", + "execa": "~9.5.0", + "file-url": "~4.0.0", + "lodash.groupby": "~4.6.0", + "minimatch": "~9.0.5", + "mutation-testing-elements": "3.5.2", + "mutation-testing-metrics": "3.5.1", + "mutation-testing-report-schema": "3.5.1", + "npm-run-path": "~6.0.0", + "progress": "~2.0.3", + "rxjs": "~7.8.1", + "semver": "^7.6.3", + "source-map": "~0.7.4", + "tree-kill": "~1.2.2", + "tslib": "2.8.1", + "typed-inject": "~5.0.0", + "typed-rest-client": "~2.1.0" + }, + "bin": { + "stryker": "bin/stryker.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stryker-mutator/core/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@stryker-mutator/core/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@stryker-mutator/core/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@stryker-mutator/core/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@stryker-mutator/core/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stryker-mutator/core/node_modules/execa": { + "version": "9.5.3", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.3.tgz", + "integrity": "sha512-QFNnTvU3UjgWFy8Ef9iDHvIdcgZ344ebkwYx4/KLbR+CKQA4xBaHzv+iRpp86QfMHP8faFQLh8iOc57215y4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.3", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.0", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@stryker-mutator/core/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@stryker-mutator/core/node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@stryker-mutator/core/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@stryker-mutator/core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stryker-mutator/core/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@stryker-mutator/core/node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@stryker-mutator/core/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@stryker-mutator/core/node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@stryker-mutator/instrumenter": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/instrumenter/-/instrumenter-9.0.0.tgz", + "integrity": "sha512-hwB8bw7p0gjgTc0SCV+SPmH2l8KZAnm0/hyqs8j+kSVoxH8XeHxMfa4fQTN6U0GSZWRJK15OZZbAtDwANWCEhg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/core": "~7.27.0", + "@babel/generator": "~7.27.0", + "@babel/parser": "~7.27.0", + "@babel/plugin-proposal-decorators": "~7.27.0", + "@babel/plugin-proposal-explicit-resource-management": "^7.24.7", + "@babel/preset-typescript": "~7.27.0", + "@stryker-mutator/api": "9.0.0", + "@stryker-mutator/util": "9.0.0", + "angular-html-parser": "~9.1.0", + "semver": "~7.7.0", + "weapon-regex": "~1.3.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stryker-mutator/instrumenter/node_modules/@babel/core": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.7.tgz", + "integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.27.7", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.7", + "@babel/types": "^7.27.7", + "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/@stryker-mutator/instrumenter/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@stryker-mutator/instrumenter/node_modules/@babel/generator": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@stryker-mutator/instrumenter/node_modules/@babel/parser": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.7.tgz", + "integrity": "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@stryker-mutator/jest-runner": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/jest-runner/-/jest-runner-9.0.0.tgz", + "integrity": "sha512-kPAorgBrlwdW+MSR7x194Ao2bTZjiRG5pux0WNZsHHMFnUeCYO7IG1+WpzumzHWfhAYRh/zO7Ah2V/Tsv13U8A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stryker-mutator/api": "9.0.0", + "@stryker-mutator/util": "9.0.0", + "semver": "~7.7.0", + "tslib": "~2.8.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@stryker-mutator/core": "~9.0.0" + } + }, + "node_modules/@stryker-mutator/util": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@stryker-mutator/util/-/util-9.0.0.tgz", + "integrity": "sha512-o15AQkebTFKDc6m35Bn4eEfZZgm5q4XXWoAUU2eC+PiBQrR5gY+9LofuHsGnV9+eYeg31fjpTm2gy7ZopqpeTw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@tokenizer/inflate": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", @@ -3484,28 +4463,167 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "node_modules/@vitest/expect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.0.tgz", + "integrity": "sha512-Qx+cHyB59mWrQywT3/dZIIpSKwIpWbYFdBX2zixMYpOGZmbaP2jbbd4i/TAKJq/jBgSfww++d6YnrlGMFb2XBg==", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "@vitest/spy": "3.0.0", + "@vitest/utils": "3.0.0", + "chai": "^5.1.2", + "tinyrainbow": "^2.0.0" }, - "engines": { - "node": ">=0.4.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "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==", + "node_modules/@vitest/mocker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.0.tgz", + "integrity": "sha512-8ytqYjIRzAM90O7n8A0TCbziTnouIG+UGuMHmoRJpKh4vvah4uENw5UAMMNjdKCtzgMiTrZ9XU+xzwCwcxuxGQ==", "dev": true, "license": "MIT", + "dependencies": { + "@vitest/spy": "3.0.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.0.tgz", + "integrity": "sha512-6MCYobtatsgG3DlM+dk6njP+R+28iSUqWbJzXp/nuOy6SkAKzJ1wby3fDgimmy50TeK8g6y+E6rP12REyinYPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.0.0", + "pathe": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.0.tgz", + "integrity": "sha512-W0X6fJFJ3RbSThncSYUNSnXkMJFyXX9sOvxP1HSQRsWCLB1U3JnZc0SrLpLzcyByMUDXHsiXQ+x+xsr/G5fXNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.0.0", + "magic-string": "^0.30.17", + "pathe": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.0.tgz", + "integrity": "sha512-24y+MS04ZHZbbbfAvfpi9hM2oULePbiL6Dir8r1nFMN97hxuL0gEXKWRGmlLPwzKDtaOKNjtyTx0+GiZcWCxDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.0.tgz", + "integrity": "sha512-pfK5O3lRqeCG8mbV+Lr8lLUBicFRm5TlggF7bLZpzpo111LKhMN/tZRXvyOGOgbktxAR9bTf4x8U6RtHuFBTVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.0.tgz", + "integrity": "sha512-l300v2/4diHyv5ZiQOj6y/H6VbaTWM6i1c2lC3lUZ5nn9rv9C+WneS/wqyaGLwM37reoh/QkrrYMSMKdfnDZpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.0.0", + "loupe": "^3.1.2", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.0.tgz", + "integrity": "sha512-24y+MS04ZHZbbbfAvfpi9hM2oULePbiL6Dir8r1nFMN97hxuL0gEXKWRGmlLPwzKDtaOKNjtyTx0+GiZcWCxDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } }, "node_modules/acorn-walk": { "version": "8.3.5", @@ -3537,6 +4655,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/angular-html-parser": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/angular-html-parser/-/angular-html-parser-9.1.1.tgz", + "integrity": "sha512-/xDmnIkfPy7df52scKGGBnZ5Uods64nkf3xBHQSU6uOxwuVVfCFrH+Q/vBZFsc/BY7aJufWtkGjTZrBoyER49w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -3769,6 +4897,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -4166,6 +5304,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4200,6 +5355,16 @@ "dev": true, "license": "MIT" }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -4318,6 +5483,16 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -4577,6 +5752,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -4630,6 +5815,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, "node_modules/detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -4660,6 +5856,13 @@ "node": ">=0.3.1" } }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -4864,6 +6067,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -5287,6 +6497,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -5361,6 +6581,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extendable-error": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz", @@ -5426,6 +6656,23 @@ "license": "MIT", "peer": true }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -5464,6 +6711,22 @@ } } }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -5496,6 +6759,19 @@ "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, + "node_modules/file-url": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/file-url/-/file-url-4.0.0.tgz", + "integrity": "sha512-vRCdScQ6j3Ku6Kd7W1kZk9c++5SqD6Xz5Jotrjr/nkY714M14RFHy/AAVA2WQvpsqVAVgTbDrYyBpU205F0cLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -6417,6 +7693,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -6542,6 +7831,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -7374,6 +8676,13 @@ "node": ">=10" } }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", + "dev": true, + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7621,6 +8930,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -7707,6 +9023,13 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -7844,6 +9167,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -8000,6 +9330,40 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mutation-testing-elements": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/mutation-testing-elements/-/mutation-testing-elements-3.5.2.tgz", + "integrity": "sha512-1S6oHiIT3pAYp0mJb8TAyNnaNLHuOJmtDwNEw93bhA0ayjTAPrlNiW8zxivvKD4pjvrZEMUyQCaX+3EBZ4cemw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/mutation-testing-metrics": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/mutation-testing-metrics/-/mutation-testing-metrics-3.5.1.tgz", + "integrity": "sha512-mNgEcnhyBDckgoKg1kjG/4Uo3aBCW0WdVUxINVEazMTggPtqGfxaAlQ9GjItyudu/8S9DuspY3xUaIRLozFG9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mutation-testing-report-schema": "3.5.1" + } + }, + "node_modules/mutation-testing-report-schema": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/mutation-testing-report-schema/-/mutation-testing-report-schema-3.5.1.tgz", + "integrity": "sha512-tu5ATRxGH3sf2igiTKonxlCsWnWcD3CYr3IXGUym7yTh3Mj5NoJsu7bDkJY99uOrEp6hQByC2nRUPEGfe6EnAg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -8378,6 +9742,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -8443,6 +9820,16 @@ "dev": true, "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -8450,9 +9837,22 @@ "dev": true, "license": "ISC" }, - "node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", "dev": true, "license": "MIT", @@ -8574,6 +9974,35 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postcss-load-config": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", @@ -8617,6 +10046,25 @@ } } }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -8671,6 +10119,32 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -8712,6 +10186,22 @@ ], "license": "MIT" }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/quansync": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", @@ -8882,6 +10372,16 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -9071,7 +10571,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -9306,6 +10805,13 @@ "dev": true, "license": "MIT" }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -9382,6 +10888,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -9433,6 +10959,20 @@ "node": ">=8" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -9730,6 +11270,13 @@ "node": ">=0.8" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", @@ -9754,17 +11301,34 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" } }, "node_modules/tmpl": { @@ -10028,14 +11592,14 @@ "node": ">=8" } }, - "node_modules/tsup/node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "engines": { - "node": ">= 12" + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } }, "node_modules/type-check": { @@ -10152,6 +11716,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-inject": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/typed-inject/-/typed-inject-5.0.0.tgz", + "integrity": "sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/typed-rest-client": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-2.1.0.tgz", + "integrity": "sha512-Nel9aPbgSzRxfs1+4GoSB4wexCF+4Axlk7OSGVQCMa+4fWcyxIsN/YNmkp0xTT2iQzMD98h8yFLav/cNaULmRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "des.js": "^1.1.0", + "js-md4": "^0.3.2", + "qs": "^6.10.3", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + }, + "engines": { + "node": ">= 16.0.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -10256,6 +11847,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -10263,6 +11861,19 @@ "dev": true, "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -10326,63 +11937,718 @@ "node": ">=10.12.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, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" }, "engines": { - "node": ">=18" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "node_modules/vite-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.0.tgz", + "integrity": "sha512-V5p05fpAzkHM3aYChsHWV1RTeLAhPejbKX6MqiWWyuIfNcDgXq5p0GnYV6Wa4OAU588XC70XCJB9chRZsOh4yg==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "isexe": "^2.0.0" + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.5.4", + "pathe": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0" }, "bin": { - "node-which": "bin/node-which" + "vite-node": "vite-node.mjs" }, "engines": { - "node": ">= 8" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/vitest": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.0.tgz", + "integrity": "sha512-fwfPif+EV0jyms9h1Crb6rwJttH/KBzKrcUesjxHgldmc6R0FaMNLsd+Rgc17NoxzLcb/sYE2Xs9NQ/vnTBf6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "3.0.0", + "@vitest/mocker": "3.0.0", + "@vitest/pretty-format": "^3.0.0", + "@vitest/runner": "3.0.0", + "@vitest/snapshot": "3.0.0", + "@vitest/spy": "3.0.0", + "@vitest/utils": "3.0.0", + "chai": "^5.1.2", + "debug": "^4.4.0", + "expect-type": "^1.1.0", + "magic-string": "^0.30.17", + "pathe": "^2.0.0", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.0.0", + "@vitest/ui": "3.0.0", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "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, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/weapon-regex": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/weapon-regex/-/weapon-regex-1.3.6.tgz", + "integrity": "sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", @@ -10465,6 +12731,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -10680,6 +12963,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index 2bfa180..4d130d0 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,8 @@ "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", + "mutation": "stryker run", + "bench": "vitest bench", "changeset": "changeset", "version-packages": "changeset version", "release": "changeset publish", @@ -57,6 +59,8 @@ "devDependencies": { "@changesets/cli": "^2.27.7", "@nestjs/testing": "^11.1.17", + "@stryker-mutator/core": "^9.0.0", + "@stryker-mutator/jest-runner": "^9.0.0", "@types/jest": "^29.5.14", "@types/node": "^22.10.7", "date-fns": "^4.1.0", @@ -73,6 +77,7 @@ "ts-node": "^10.9.2", "tsup": "^8.3.5", "typescript": "^5.7.3", - "typescript-eslint": "^8.50.1" + "typescript-eslint": "^8.50.1", + "vitest": "^3.0.0" } } diff --git a/src/core/audit.service.spec.ts b/src/core/audit.service.spec.ts index 297ce9e..f085fc4 100644 --- a/src/core/audit.service.spec.ts +++ b/src/core/audit.service.spec.ts @@ -34,7 +34,6 @@ import type { IIdGenerator } from "./ports/id-generator.port"; import type { ITimestampProvider } from "./ports/timestamp-provider.port"; import type { AuditLog, ChangeSet } from "./types"; import { ActorType, AuditActionType } from "./types"; - // ============================================================================ // MOCK IMPLEMENTATIONS // ============================================================================ @@ -53,6 +52,7 @@ const createMockRepository = (): jest.Mocked => ({ exists: jest.fn(), deleteOlderThan: jest.fn(), archiveOlderThan: jest.fn(), + queryWithCursor: jest.fn(), }); /** @@ -384,6 +384,104 @@ describe("AuditService", () => { expect(result.success).toBe(true); } }); + + it("should redact configured PII fields before persistence", async () => { + const redactingService = new AuditService( + mockRepository, + mockIdGenerator, + mockTimestampProvider, + mockChangeDetector, + { + piiRedaction: { + enabled: true, + fields: ["actor.email", "metadata.secret", "ipAddress"], + mask: "***", + }, + }, + ); + + mockRepository.create.mockResolvedValue(expectedAuditLog); + + await redactingService.log({ + ...validDto, + actor: { + ...validActor, + email: "john@example.com", + }, + metadata: { secret: "top-secret" }, + ipAddress: MOCK_IP_ADDRESS_2, + }); + + expect(mockRepository.create).toHaveBeenCalledWith( + expect.objectContaining({ + actor: expect.objectContaining({ email: "***" }), + metadata: expect.objectContaining({ secret: "***" }), + ipAddress: "***", + }), + ); + }); + + it("should deduplicate writes when idempotency key already exists", async () => { + const idempotentService = new AuditService( + mockRepository, + mockIdGenerator, + mockTimestampProvider, + mockChangeDetector, + { + idempotency: { + enabled: true, + keyStrategy: "idempotencyKey", + }, + }, + ); + + mockRepository.query.mockResolvedValue({ + data: [expectedAuditLog], + page: 1, + limit: 1, + total: 1, + pages: 1, + }); + + const result = await idempotentService.log({ + ...validDto, + idempotencyKey: "idem-1", + }); + + expect(result.success).toBe(true); + expect(result.data).toEqual(expectedAuditLog); + expect(result.metadata?.idempotentHit).toBe(true); + expect(mockRepository.create).not.toHaveBeenCalled(); + }); + + it("should run retention after write when enabled", async () => { + const retentionService = new AuditService( + mockRepository, + mockIdGenerator, + mockTimestampProvider, + mockChangeDetector, + { + retention: { + enabled: true, + retentionDays: 30, + autoCleanupOnWrite: true, + archiveBeforeDelete: true, + }, + }, + ); + + mockRepository.create.mockResolvedValue(expectedAuditLog); + (mockRepository.archiveOlderThan as jest.Mock).mockResolvedValue(2); + (mockRepository.deleteOlderThan as jest.Mock).mockResolvedValue(3); + + const result = await retentionService.log(validDto); + + expect(mockRepository.archiveOlderThan).toHaveBeenCalled(); + expect(mockRepository.deleteOlderThan).toHaveBeenCalled(); + expect(result.metadata?.retention).toBeDefined(); + expect(result.metadata?.retention?.archived).toBe(2); + expect(result.metadata?.retention?.deleted).toBe(3); + }); }); // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -754,4 +852,195 @@ describe("AuditService", () => { ); }); }); + + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + // queryWithCursor() - Cursor-Based Pagination + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + describe("queryWithCursor()", () => { + it("should return cursor-paginated results when repository supports it", async () => { + // Arrange + const cursorResult = { + data: [expectedAuditLog], + hasMore: true, + limit: 10, + nextCursor: "eyJ0IjoxNzQ2OTk0MDAwMDAwLCJpZCI6ImF1ZGl0XzEyMyJ9", + }; + (mockRepository as any).queryWithCursor = jest.fn().mockResolvedValue(cursorResult); + + // Act + const result = await service.queryWithCursor({}, { limit: 10 }); + + // Assert + expect(result).toEqual(cursorResult); + expect((mockRepository as any).queryWithCursor).toHaveBeenCalledWith(expect.any(Object), { + limit: 10, + }); + }); + + it("should pass filter fields from DTO to repository", async () => { + // Arrange + (mockRepository as any).queryWithCursor = jest.fn().mockResolvedValue({ + data: [], + hasMore: false, + limit: 20, + }); + + // Act + await service.queryWithCursor( + { + actorId: "user-1", + action: AuditActionType.UPDATE, + resourceType: "order", + }, + { limit: 20 }, + ); + + // Assert: filter fields were passed + expect((mockRepository as any).queryWithCursor).toHaveBeenCalledWith( + expect.objectContaining({ + actorId: "user-1", + action: AuditActionType.UPDATE, + resourceType: "order", + }), + { limit: 20 }, + ); + }); + + it("should throw when repository does not support queryWithCursor", async () => { + // Arrange: create a repo mock that explicitly lacks queryWithCursor + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { queryWithCursor: _omit, ...repoWithoutCursor } = createMockRepository(); + const svc = new AuditService( + repoWithoutCursor as any, + mockIdGenerator, + mockTimestampProvider, + ); + + // Act & Assert + await expect(svc.queryWithCursor({})).rejects.toThrow("Cursor pagination is not supported"); + }); + }); + + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + // Observer - Observability Hooks + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + describe("observer hooks", () => { + it("should call observer.onEvent after a successful log()", async () => { + // Arrange + const onEvent = jest.fn(); + const observerService = new AuditService( + mockRepository, + mockIdGenerator, + mockTimestampProvider, + mockChangeDetector, + { observer: { onEvent } }, + ); + mockRepository.create.mockResolvedValue(expectedAuditLog); + + // Act + await observerService.log(validDto); + + // Assert: observer was called with a success event + expect(onEvent).toHaveBeenCalledWith( + expect.objectContaining({ operation: "create", success: true }), + ); + }); + + it("should call observer.onEvent with an error event when log() fails", async () => { + // Arrange + const onEvent = jest.fn(); + const observerService = new AuditService( + mockRepository, + mockIdGenerator, + mockTimestampProvider, + mockChangeDetector, + { observer: { onEvent } }, + ); + mockRepository.create.mockRejectedValue(new Error("DB error")); + + // Act — log() swallows errors and returns success:false + const result = await observerService.log(validDto); + + // Assert: result is failure + expect(result.success).toBe(false); + + // Assert: observer was called with a fail event + expect(onEvent).toHaveBeenCalledWith( + expect.objectContaining({ + operation: "create", + success: false, + error: expect.any(Error), + }), + ); + }); + + it("should not throw if observer.onEvent throws", async () => { + // Arrange: observer that always throws + const breakingObserver = { + onEvent: jest.fn().mockImplementation(() => { + throw new Error("observer error"); + }), + }; + const observerService = new AuditService( + mockRepository, + mockIdGenerator, + mockTimestampProvider, + mockChangeDetector, + { observer: breakingObserver }, + ); + mockRepository.create.mockResolvedValue(expectedAuditLog); + + // Act & Assert: should not throw despite the observer error + await expect(observerService.log(validDto)).resolves.not.toThrow(); + }); + }); + + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + // Event Publisher - Event Streaming Hooks + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + describe("event publisher hooks", () => { + it("should publish audit.log.created after successful create", async () => { + // Arrange + const publish = jest.fn(); + const eventService = new AuditService( + mockRepository, + mockIdGenerator, + mockTimestampProvider, + mockChangeDetector, + { eventPublisher: { publish } }, + ); + mockRepository.create.mockResolvedValue(expectedAuditLog); + + // Act + await eventService.log(validDto); + + // Assert + expect(publish).toHaveBeenCalledWith( + expect.objectContaining({ type: "audit.log.created", payload: expectedAuditLog }), + ); + }); + + it("should not throw if event publisher throws", async () => { + // Arrange + const eventPublisher = { + publish: jest.fn().mockImplementation(() => { + throw new Error("publisher error"); + }), + }; + const eventService = new AuditService( + mockRepository, + mockIdGenerator, + mockTimestampProvider, + mockChangeDetector, + { eventPublisher }, + ); + mockRepository.create.mockResolvedValue(expectedAuditLog); + + // Act & Assert + await expect(eventService.log(validDto)).resolves.not.toThrow(); + }); + }); }); diff --git a/src/core/audit.service.ts b/src/core/audit.service.ts index 94eac8c..f00fe7c 100644 --- a/src/core/audit.service.ts +++ b/src/core/audit.service.ts @@ -30,11 +30,25 @@ import type { CreateAuditLogDto, CreateAuditLogWithChanges, QueryAuditLogsDto } from "./dtos"; import { InvalidActorError, InvalidChangeSetError } from "./errors"; +import { + AUDIT_EVENT_TYPES, + type AuditEvent, + type IAuditEventPublisher, +} from "./ports/audit-event-publisher.port"; +import type { AuditObserverEvent, IAuditObserver } from "./ports/audit-observer.port"; import type { IAuditLogRepository } from "./ports/audit-repository.port"; import type { IChangeDetector } from "./ports/change-detector.port"; import type { IIdGenerator } from "./ports/id-generator.port"; import type { ITimestampProvider } from "./ports/timestamp-provider.port"; -import type { AuditLog, AuditLogFilters, PageResult, ChangeSet, PageOptions } from "./types"; +import type { + AuditLog, + AuditLogFilters, + CursorPageOptions, + CursorPageResult, + PageResult, + ChangeSet, + PageOptions, +} from "./types"; // ============================================================================ // AUDIT SERVICE RESULT TYPES @@ -78,9 +92,56 @@ export interface CreateAuditLogResult { * Number of fields changed (if applicable) */ fieldCount?: number; + + /** + * Whether the operation reused an existing log due to idempotency. + */ + idempotentHit?: boolean; + + /** + * Retention processing summary. + */ + retention?: { + archived: number; + deleted: number; + cutoffDate: Date; + }; }; } +/** + * Runtime options for advanced audit service behavior. + */ +export interface AuditServiceOptions { + piiRedaction?: { + enabled?: boolean; + fields?: string[]; + mask?: string; + }; + idempotency?: { + enabled?: boolean; + keyStrategy?: "idempotencyKey" | "requestId"; + }; + retention?: { + enabled?: boolean; + retentionDays?: number; + autoCleanupOnWrite?: boolean; + archiveBeforeDelete?: boolean; + }; + /** + * Optional observability observer. + * Called after each operation with timing and outcome metadata. + * Observer errors are swallowed and never affect core operations. + */ + observer?: IAuditObserver; + + /** + * Optional audit event publisher for event streaming integrations. + * Publisher errors are swallowed and never affect core operations. + */ + eventPublisher?: IAuditEventPublisher; +} + // ============================================================================ // MAIN AUDIT SERVICE // ============================================================================ @@ -124,6 +185,8 @@ export class AuditService { private readonly _timestampProvider: ITimestampProvider, // eslint-disable-next-line no-unused-vars private readonly _changeDetector?: IChangeDetector, + // eslint-disable-next-line no-unused-vars + private readonly _options?: AuditServiceOptions, ) {} // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -170,6 +233,20 @@ export class AuditService { // Cast to Actor because CreateAuditLogDto's actor has optional fields but satisfies the interface this.validateActor(dto.actor as AuditLog["actor"]); + // Check idempotency before creating a new entry. + const existing = await this.findExistingByIdempotency(dto); + if (existing) { + return { + success: true, + data: existing, + metadata: { + duration: Date.now() - startTime, + fieldCount: dto.changes ? Object.keys(dto.changes).length : 0, + idempotentHit: true, + }, + }; + } + // Generate a unique ID for this audit log entry const id = this._idGenerator.generate({ prefix: "audit_" }); @@ -186,44 +263,45 @@ export class AuditService { }; // Add optional fields only if they're defined - if (dto.changes !== undefined) { - (auditLog as any).changes = dto.changes; - } - if (dto.metadata !== undefined) { - (auditLog as any).metadata = dto.metadata; - } - if (dto.ipAddress !== undefined) { - (auditLog as any).ipAddress = dto.ipAddress; - } - if (dto.userAgent !== undefined) { - (auditLog as any).userAgent = dto.userAgent; - } - if (dto.requestId !== undefined) { - (auditLog as any).requestId = dto.requestId; - } - if (dto.sessionId !== undefined) { - (auditLog as any).sessionId = dto.sessionId; - } - if (dto.reason !== undefined) { - (auditLog as any).reason = dto.reason; - } + this.assignOptionalFields(auditLog, dto); + + const logToPersist = this.applyPiiRedaction(auditLog); // Persist the audit log to the repository - const created = await this._repository.create(auditLog); + const created = await this._repository.create(logToPersist); + + const retentionResult = await this.runRetentionIfEnabled(timestamp); // Calculate operation duration const duration = Date.now() - startTime; // Return success result with metadata + const metadata: NonNullable = { + duration, + fieldCount: dto.changes ? Object.keys(dto.changes).length : 0, + idempotentHit: false, + }; + + if (retentionResult) { + metadata.retention = retentionResult; + } + + this.publishAuditCreatedEvent(created); + this.notifyObserver({ operation: "create", durationMs: duration, success: true }); + return { success: true, data: created, - metadata: { - duration, - fieldCount: dto.changes ? Object.keys(dto.changes).length : 0, - }, + metadata, }; } catch (error) { + const duration = Date.now() - startTime; + this.notifyObserver({ + operation: "create", + durationMs: duration, + success: false, + error: error instanceof Error ? error : new Error(String(error)), + }); // Return failure result with error details return { success: false, @@ -488,6 +566,7 @@ export class AuditService { if (dto.endDate !== undefined) filters.endDate = dto.endDate; if (dto.ipAddress !== undefined) filters.ipAddress = dto.ipAddress; if (dto.search !== undefined) filters.search = dto.search; + if (dto.idempotencyKey !== undefined) filters.idempotencyKey = dto.idempotencyKey; if (dto.page !== undefined) filters.page = dto.page; if (dto.limit !== undefined) filters.limit = dto.limit; if (dto.sort !== undefined) filters.sort = dto.sort; @@ -495,6 +574,72 @@ export class AuditService { return this._repository.query(filters); } + /** + * Queries audit logs using cursor-based (keyset) pagination. + * + * Unlike offset pagination (`query()`), cursor pagination is stable — it + * won't skip or duplicate items when records are inserted between requests. + * Results are sorted by `timestamp DESC, id ASC`. + * + * Requires the configured repository to support `queryWithCursor`. + * + * @param filters - Filter criteria (same fields as `query()`, minus pagination) + * @param cursorOptions - Cursor and limit options + * @returns Cursor-paginated result + * @throws {Error} If the configured repository does not support cursor pagination + * + * @example First page + * ```typescript + * const page1 = await service.queryWithCursor( + * { actorId: 'user-1' }, + * { limit: 10 }, + * ); + * ``` + * + * @example Subsequent page + * ```typescript + * if (page1.hasMore) { + * const page2 = await service.queryWithCursor( + * { actorId: 'user-1' }, + * { limit: 10, cursor: page1.nextCursor }, + * ); + * } + * ``` + */ + async queryWithCursor( + filters: Partial, + cursorOptions?: CursorPageOptions, + ): Promise> { + const startTime = Date.now(); + try { + if (!this._repository.queryWithCursor) { + throw new Error( + "Cursor pagination is not supported by the configured repository. " + + "Ensure your repository adapter implements queryWithCursor().", + ); + } + + const result = await this._repository.queryWithCursor(filters, cursorOptions); + + this.notifyObserver({ + operation: "queryWithCursor", + durationMs: Date.now() - startTime, + success: true, + meta: { count: result.data.length, hasMore: result.hasMore }, + }); + + return result; + } catch (error) { + this.notifyObserver({ + operation: "queryWithCursor", + durationMs: Date.now() - startTime, + success: false, + error: error instanceof Error ? error : new Error(String(error)), + }); + throw error; + } + } + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // CHANGE DETECTION - Comparing Object States // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -554,6 +699,21 @@ export class AuditService { // VALIDATION - Business Rule Enforcement // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + /** + * Assigns optional fields from a DTO onto an AuditLog object. + * Extracted to keep `log()` cognitive complexity within acceptable bounds. + */ + private assignOptionalFields(auditLog: AuditLog, dto: CreateAuditLogDto): void { + if (dto.changes !== undefined) (auditLog as any).changes = dto.changes; + if (dto.metadata !== undefined) (auditLog as any).metadata = dto.metadata; + if (dto.ipAddress !== undefined) (auditLog as any).ipAddress = dto.ipAddress; + if (dto.userAgent !== undefined) (auditLog as any).userAgent = dto.userAgent; + if (dto.requestId !== undefined) (auditLog as any).requestId = dto.requestId; + if (dto.sessionId !== undefined) (auditLog as any).sessionId = dto.sessionId; + if (dto.idempotencyKey !== undefined) (auditLog as any).idempotencyKey = dto.idempotencyKey; + if (dto.reason !== undefined) (auditLog as any).reason = dto.reason; + } + /** * Validates an actor (ensures all required fields are present and valid). * @@ -580,4 +740,195 @@ export class AuditService { throw InvalidActorError.invalidType(actor.type); } } + + /** + * Finds an existing audit log when idempotency is enabled and a key is present. + */ + private async findExistingByIdempotency(dto: CreateAuditLogDto): Promise { + if (!this._options?.idempotency?.enabled) { + return null; + } + + const strategy = this._options.idempotency.keyStrategy ?? "idempotencyKey"; + const key = strategy === "requestId" ? dto.requestId : dto.idempotencyKey; + if (!key) { + return null; + } + + const queryFilters: Partial & Partial = { + page: 1, + limit: 1, + sort: "-timestamp", + }; + + if (strategy === "idempotencyKey") { + queryFilters.idempotencyKey = key; + } else { + queryFilters.requestId = key; + } + + const existing = await this._repository.query(queryFilters); + + return existing.data[0] ?? null; + } + + /** + * Applies configured PII redaction to selected fields before persistence. + */ + private applyPiiRedaction(log: AuditLog): AuditLog { + if (!this._options?.piiRedaction?.enabled) { + return log; + } + + const redactionPaths = + this._options.piiRedaction.fields && this._options.piiRedaction.fields.length > 0 + ? this._options.piiRedaction.fields + : ["actor.email", "metadata.password", "metadata.token", "metadata.authorization"]; + const mask = this._options.piiRedaction.mask ?? "[REDACTED]"; + + const cloned = this.deepClone(log); + for (const path of redactionPaths) { + this.redactPath(cloned as unknown as Record, path, mask); + } + + return cloned; + } + + /** + * Applies retention policy after writes when configured. + */ + private async runRetentionIfEnabled( + timestamp: Date, + ): Promise<{ archived: number; deleted: number; cutoffDate: Date } | undefined> { + if (!this._options?.retention?.enabled || !this._options.retention.autoCleanupOnWrite) { + return undefined; + } + + const retentionDays = this._options.retention.retentionDays; + if (!retentionDays || retentionDays <= 0) { + return undefined; + } + + const cutoffDate = new Date(timestamp); + cutoffDate.setDate(cutoffDate.getDate() - retentionDays); + + let archived = 0; + if (this._options.retention.archiveBeforeDelete && this._repository.archiveOlderThan) { + archived = await this._repository.archiveOlderThan(cutoffDate); + } + + let deleted = 0; + if (this._repository.deleteOlderThan) { + deleted = await this._repository.deleteOlderThan(cutoffDate); + } + + return { archived, deleted, cutoffDate }; + } + + /** + * Redacts one dot-path inside a mutable object. + */ + private redactPath(target: Record, path: string, mask: string): void { + const segments = path.split(".").filter(Boolean); + if (segments.length === 0) { + return; + } + + let cursor: Record = target; + for (let i = 0; i < segments.length - 1; i += 1) { + const segment = segments[i]; + if (!segment) { + return; + } + + const next = cursor[segment]; + if (!next || typeof next !== "object" || Array.isArray(next)) { + return; + } + cursor = next as Record; + } + + const leaf = segments.at(-1); + if (!leaf) { + return; + } + + if (leaf in cursor) { + cursor[leaf] = mask; + } + } + + /** + * Deep clones plain objects while preserving Date instances. + */ + private deepClone(value: T): T { + if (value instanceof Date) { + return new Date(value) as T; + } + + if (Array.isArray(value)) { + return value.map((item) => this.deepClone(item)) as T; + } + + if (value && typeof value === "object") { + const source = value as Record; + const cloned: Record = {}; + for (const [key, item] of Object.entries(source)) { + cloned[key] = this.deepClone(item); + } + return cloned as T; + } + + return value; + } + + /** + * Notifies the configured observer with an operation event. + * + * Errors thrown by the observer are swallowed intentionally — observability + * hooks must never disrupt core audit operations. + */ + private notifyObserver(event: AuditObserverEvent): void { + if (!this._options?.observer) { + return; + } + try { + const result = this._options.observer.onEvent(event); + if (result instanceof Promise) { + result.catch(() => { + // Observer async errors are intentionally ignored + }); + } + } catch { + // Observer sync errors are intentionally ignored + } + } + + /** + * Publishes a created audit-log event through the configured publisher. + * + * Errors are swallowed intentionally to avoid impacting business logic. + */ + private publishAuditCreatedEvent(log: AuditLog): void { + if (!this._options?.eventPublisher) { + return; + } + + const event: AuditEvent = { + type: AUDIT_EVENT_TYPES.CREATED, + emittedAt: new Date(), + payload: log, + }; + + try { + const result = this._options.eventPublisher.publish(event); + if (result instanceof Promise) { + result.catch(() => { + // Publisher async errors are intentionally ignored + }); + } + } catch { + // Publisher sync errors are intentionally ignored + } + } } diff --git a/src/core/dtos/audit-log-response.dto.ts b/src/core/dtos/audit-log-response.dto.ts index 67c6ece..7c97ba4 100644 --- a/src/core/dtos/audit-log-response.dto.ts +++ b/src/core/dtos/audit-log-response.dto.ts @@ -129,6 +129,9 @@ export const AuditLogResponseDtoSchema = z.object({ /** Session ID */ sessionId: z.string().optional(), + /** Idempotency key */ + idempotencyKey: z.string().optional(), + // ───────────────────────────────────────────────────────────────────────── // COMPLIANCE // ───────────────────────────────────────────────────────────────────────── diff --git a/src/core/dtos/create-audit-log.dto.ts b/src/core/dtos/create-audit-log.dto.ts index ebe6f02..349cf6d 100644 --- a/src/core/dtos/create-audit-log.dto.ts +++ b/src/core/dtos/create-audit-log.dto.ts @@ -180,6 +180,9 @@ export const CreateAuditLogDtoSchema = z.object({ /** Session ID (if applicable) */ sessionId: z.string().optional(), + /** Idempotency key used to deduplicate retried writes */ + idempotencyKey: z.string().min(1, "Idempotency key cannot be empty").max(128).optional(), + /** * Human-readable reason or justification. * May be required by compliance policies for sensitive operations. diff --git a/src/core/dtos/query-audit-logs.dto.ts b/src/core/dtos/query-audit-logs.dto.ts index e71ad37..5779b28 100644 --- a/src/core/dtos/query-audit-logs.dto.ts +++ b/src/core/dtos/query-audit-logs.dto.ts @@ -228,6 +228,12 @@ export const QueryAuditLogsDtoSchema = z.object({ */ sessionId: z.string().optional(), + /** + * Filter by idempotency key. + * Example: Find the deduplicated audit write for a retried request + */ + idempotencyKey: z.string().optional(), + // ───────────────────────────────────────────────────────────────────────── // FULL-TEXT SEARCH // ───────────────────────────────────────────────────────────────────────── diff --git a/src/core/index.ts b/src/core/index.ts index 4178df0..9c81899 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -49,9 +49,15 @@ export { // Main Entity - Audit Log type AuditLog, - // Query & Pagination Types + // Offset Pagination Types type PageOptions, type PageResult, + + // Cursor Pagination Types + type CursorPageOptions, + type CursorPageResult, + + // Filter Types type AuditLogFilters, // Type Guards - Runtime type checking @@ -124,6 +130,17 @@ export { type TimestampFormat, type TimezoneOption, type TimestampProviderInfo, + + // Audit Observer Port - Observability hooks + type IAuditObserver, + type AuditObserverEvent, + type AuditOperationType, + + // Audit Event Publisher Port - Event streaming + AUDIT_EVENT_TYPES, + type IAuditEventPublisher, + type AuditEvent, + type AuditEventType, } from "./ports"; // ============================================================================ diff --git a/src/core/ports/audit-event-publisher.port.ts b/src/core/ports/audit-event-publisher.port.ts new file mode 100644 index 0000000..a886a72 --- /dev/null +++ b/src/core/ports/audit-event-publisher.port.ts @@ -0,0 +1,58 @@ +/** + * ============================================================================ + * AUDIT EVENT PUBLISHER PORT - EVENT STREAMING ABSTRACTION + * ============================================================================ + * + * Port interface for emitting audit lifecycle events to external event + * streaming systems (Kafka, RabbitMQ, NATS, SNS/SQS, etc.). + * + * This keeps core logic independent from any specific event bus SDK. + * + * @packageDocumentation + */ + +import type { AuditLog } from "../types"; + +// ESLint disable for interface method parameters (they're part of the contract) +/* eslint-disable no-unused-vars */ + +/** + * Event name constants emitted by AuditService. + */ +export const AUDIT_EVENT_TYPES = { + CREATED: "audit.log.created", +} as const; + +/** + * Supported event types. + */ +export type AuditEventType = (typeof AUDIT_EVENT_TYPES)[keyof typeof AUDIT_EVENT_TYPES]; + +/** + * Payload for audit streaming events. + * + * Includes the full persisted audit log entry and metadata useful for + * downstream subscribers. + */ +export interface AuditEvent { + /** Event type name */ + type: AuditEventType; + + /** Event creation timestamp */ + emittedAt: Date; + + /** Persisted audit log entity */ + payload: AuditLog; +} + +/** + * Port for publishing audit events to an event stream. + */ +export interface IAuditEventPublisher { + /** + * Publishes an audit event to the configured stream/broker. + * + * @param event - The event to publish + */ + publish(_event: AuditEvent): Promise | void; +} diff --git a/src/core/ports/audit-observer.port.ts b/src/core/ports/audit-observer.port.ts new file mode 100644 index 0000000..18b17b7 --- /dev/null +++ b/src/core/ports/audit-observer.port.ts @@ -0,0 +1,136 @@ +/** + * ============================================================================ + * AUDIT OBSERVER PORT - OBSERVABILITY HOOKS + * ============================================================================ + * + * Port interface for plugging in observability integrations + * (OpenTelemetry, Prometheus, Datadog, custom logging, etc.). + * + * This port intentionally keeps consumer-facing: AuditKit calls `onEvent()` + * after each significant operation so that consumers can emit spans, metrics, + * or structured log events without AuditKit depending on any specific SDK. + * + * SECURITY: Event objects deliberately exclude audit log content, actor PII, + * and resource metadata — they carry only timing and operational metadata. + * + * Architecture: + * - This is a PORT (interface) defined in core/ — framework-free + * - Concrete implementations live in infra/ or in consuming applications + * - Wire the observer via AuditKitModuleOptions.observer + * + * @packageDocumentation + */ + +// ESLint disable for interface method parameters (they're part of the contract) +/* eslint-disable no-unused-vars */ + +// ============================================================================ +// OPERATION TYPES +// ============================================================================ + +/** + * Names of operations emitted as events by AuditService. + */ +export type AuditOperationType = + | "create" + | "createWithChanges" + | "query" + | "queryWithCursor" + | "getById" + | "getByActor" + | "getByResource" + | "detectChanges"; + +// ============================================================================ +// EVENT TYPES +// ============================================================================ + +/** + * Observability event emitted after each AuditService operation. + * + * Designed to be forwarded to OpenTelemetry tracer spans, Prometheus + * counters/histograms, structured loggers, or any custom sink. + * + * @example OpenTelemetry integration + * ```typescript + * class OtelAuditObserver implements IAuditObserver { + * async onEvent(event: AuditObserverEvent): Promise { + * const span = tracer.startSpan(`audit.${event.operation}`, { + * attributes: { + * 'audit.success': event.success, + * 'audit.duration_ms': event.durationMs, + * ...event.meta, + * }, + * }); + * if (!event.success && event.error) { + * span.recordException(event.error); + * } + * span.end(); + * } + * } + * ``` + */ +export interface AuditObserverEvent { + /** + * The AuditService operation that produced this event. + */ + operation: AuditOperationType; + + /** + * Wall-clock duration of the operation in milliseconds. + */ + durationMs: number; + + /** + * Whether the operation completed successfully. + */ + success: boolean; + + /** + * The caught error if `success` is false. + */ + error?: Error; + + /** + * Safe key/value metadata (no PII, no raw entity content). + * + * Examples: `{ 'result.count': 25, 'idempotent_hit': true }`. + */ + meta?: Record; +} + +// ============================================================================ +// OBSERVER PORT +// ============================================================================ + +/** + * Port for observability integrations with AuditService. + * + * Implement this interface to subscribe to operation events without + * coupling AuditKit to a specific observability library. + * + * Key guarantees: + * - Called after every `AuditService` operation (success and failure) + * - Observer errors are swallowed — they never affect core operations + * - Events contain no PII or sensitive audit log content + * + * @example Custom metrics observer + * ```typescript + * class MetricsAuditObserver implements IAuditObserver { + * onEvent(event: AuditObserverEvent): void { + * metricsClient.histogram('audit.operation.duration', event.durationMs, { + * operation: event.operation, + * success: String(event.success), + * }); + * } + * } + * ``` + */ +export interface IAuditObserver { + /** + * Called after each AuditService operation. + * + * @param event - Observability event with timing and outcome metadata + */ + onEvent(_event: AuditObserverEvent): void | Promise; +} diff --git a/src/core/ports/audit-repository.port.ts b/src/core/ports/audit-repository.port.ts index 727dedb..6507034 100644 --- a/src/core/ports/audit-repository.port.ts +++ b/src/core/ports/audit-repository.port.ts @@ -24,7 +24,14 @@ * @packageDocumentation */ -import type { AuditLog, AuditLogFilters, PageOptions, PageResult } from "../types"; +import type { + AuditLog, + AuditLogFilters, + CursorPageOptions, + CursorPageResult, + PageOptions, + PageResult, +} from "../types"; // ESLint disable for interface method parameters (they're part of the contract, not actual code) /* eslint-disable no-unused-vars */ @@ -284,4 +291,39 @@ export interface IAuditLogRepository { * @throws Error if archival fails or not supported */ archiveOlderThan?(_beforeDate: Date): Promise; + + /** + * Queries audit logs using cursor-based pagination. + * + * Unlike offset pagination (`query()`), cursor pagination is stable: + * it won't skip or duplicate items when records are inserted or deleted + * between pages. Results are always sorted by `timestamp DESC, id ASC`. + * + * @param filters - Filter criteria (same as `query()`, except page/limit/sort are ignored) + * @param options - Cursor and limit options + * @returns Cursor-paginated result with an opaque `nextCursor` for the next page + * @throws Error if the cursor is invalid or query execution fails + * + * @example First page + * ```typescript + * const page1 = await repository.queryWithCursor( + * { actorId: 'user-1' }, + * { limit: 10 }, + * ); + * ``` + * + * @example Next page + * ```typescript + * if (page1.hasMore) { + * const page2 = await repository.queryWithCursor( + * { actorId: 'user-1' }, + * { limit: 10, cursor: page1.nextCursor }, + * ); + * } + * ``` + */ + queryWithCursor?( + _filters: Partial, + _options?: CursorPageOptions, + ): Promise>; } diff --git a/src/core/ports/index.ts b/src/core/ports/index.ts index a4f5a38..637623a 100644 --- a/src/core/ports/index.ts +++ b/src/core/ports/index.ts @@ -63,3 +63,24 @@ export { type TimezoneOption, type TimestampProviderInfo, } from "./timestamp-provider.port"; + +// ============================================================================ +// AUDIT OBSERVER PORT - Observability Hooks +// ============================================================================ + +export { + type IAuditObserver, + type AuditObserverEvent, + type AuditOperationType, +} from "./audit-observer.port"; + +// ============================================================================ +// AUDIT EVENT PUBLISHER PORT - Event Streaming Hooks +// ============================================================================ + +export { + AUDIT_EVENT_TYPES, + type IAuditEventPublisher, + type AuditEvent, + type AuditEventType, +} from "./audit-event-publisher.port"; diff --git a/src/core/types.ts b/src/core/types.ts index ba8118c..8007de8 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -248,6 +248,9 @@ export interface AuditLog { /** Session ID (if applicable) */ sessionId?: string; + /** Idempotency key for deduplicating repeated writes */ + idempotencyKey?: string; + // ───────────────────────────────────────────────────────────────────────── // COMPLIANCE - Justification // ───────────────────────────────────────────────────────────────────────── @@ -301,6 +304,47 @@ export interface PageResult { pages: number; } +/** + * Options for cursor-based pagination. + * + * Cursor pagination avoids the instability of offset-based pagination + * (e.g., skipped or duplicated items when records are added/removed mid-query). + * Results are always sorted by `timestamp DESC, id ASC`. + */ +export interface CursorPageOptions { + /** + * Opaque cursor string returned from the previous page's `nextCursor`. + * Omit to retrieve the first page. + */ + cursor?: string; + + /** + * Maximum number of items per page. + * @default 20 + */ + limit?: number; +} + +/** + * Result of a cursor-based paginated query. + */ +export interface CursorPageResult { + /** Items for the current page */ + data: T[]; + + /** + * Opaque cursor for the next page. + * Absent when `hasMore` is false — do not pass to the next call. + */ + nextCursor?: string; + + /** Whether more items exist after this page */ + hasMore: boolean; + + /** Effective page size used for this result */ + limit: number; +} + /** * Filter options for querying audit logs. * @@ -340,6 +384,9 @@ export interface AuditLogFilters { /** Filter by session ID */ sessionId?: string; + /** Filter by idempotency key */ + idempotencyKey?: string; + /** Free-text search across multiple fields */ search?: string; diff --git a/src/infra/providers/events/event-emitter-audit-event.publisher.ts b/src/infra/providers/events/event-emitter-audit-event.publisher.ts new file mode 100644 index 0000000..ced057c --- /dev/null +++ b/src/infra/providers/events/event-emitter-audit-event.publisher.ts @@ -0,0 +1,51 @@ +/** + * ============================================================================ + * EVENT EMITTER AUDIT EVENT PUBLISHER + * ============================================================================ + * + * Default in-process event streaming adapter using Node.js EventEmitter. + * + * Useful for: + * - Local integrations without external broker + * - Testing event-stream behavior + * - Bridging to app-level subscribers that forward to Kafka/RabbitMQ + * + * @packageDocumentation + */ + +import { EventEmitter } from "node:events"; + +import type { + AuditEvent, + IAuditEventPublisher, +} from "../../../core/ports/audit-event-publisher.port"; + +/** + * EventEmitter-based implementation of IAuditEventPublisher. + */ +export class EventEmitterAuditEventPublisher implements IAuditEventPublisher { + private readonly emitter: EventEmitter; + + /** + * Creates a new publisher. + * + * @param emitter - EventEmitter instance (shared app bus or dedicated one) + */ + constructor(emitter: EventEmitter = new EventEmitter()) { + this.emitter = emitter; + } + + /** + * Publishes an audit event on the emitter channel named by event.type. + */ + publish(event: AuditEvent): void { + this.emitter.emit(event.type, event); + } + + /** + * Exposes the emitter for consumers that want to subscribe in-process. + */ + getEmitter(): EventEmitter { + return this.emitter; + } +} diff --git a/src/infra/providers/events/index.ts b/src/infra/providers/events/index.ts new file mode 100644 index 0000000..5d6e4ff --- /dev/null +++ b/src/infra/providers/events/index.ts @@ -0,0 +1 @@ +export { EventEmitterAuditEventPublisher } from "./event-emitter-audit-event.publisher"; diff --git a/src/infra/providers/index.ts b/src/infra/providers/index.ts index de578bd..1320fe9 100644 --- a/src/infra/providers/index.ts +++ b/src/infra/providers/index.ts @@ -16,3 +16,4 @@ export * from "./id-generator"; export * from "./timestamp"; export * from "./change-detector"; +export * from "./events"; diff --git a/src/infra/repositories/cursor.util.ts b/src/infra/repositories/cursor.util.ts new file mode 100644 index 0000000..c918ae5 --- /dev/null +++ b/src/infra/repositories/cursor.util.ts @@ -0,0 +1,64 @@ +/** + * ============================================================================ + * CURSOR UTILITY - OPAQUE CURSOR ENCODING FOR CURSOR PAGINATION + * ============================================================================ + * + * Provides encode/decode helpers that turn an internal `{ t, id }` pair into + * an opaque base64url string that is safe to surface in API responses. + * + * Cursor format (before encoding): + * { t: number, id: string } + * where `t` is the timestamp in milliseconds and `id` is the audit log ID. + * + * Why base64url? Pure ascii, URL-safe, and hides the internal structure from + * API consumers (opaque cursor contract). + * + * @packageDocumentation + */ + +/** + * Internal cursor data (before encoding). + */ +export interface CursorData { + /** Timestamp in milliseconds */ + t: number; + /** Audit log ID */ + id: string; +} + +/** + * Encodes a cursor data object into an opaque base64url string. + * + * @param data - The cursor data to encode + * @returns Base64url-encoded cursor string + */ +export function encodeCursor(data: CursorData): string { + return Buffer.from(JSON.stringify(data)).toString("base64url"); +} + +/** + * Decodes an opaque cursor string back into cursor data. + * + * @param cursor - The base64url cursor string + * @returns Decoded cursor data + * @throws Error if the cursor is malformed + */ +export function decodeCursor(cursor: string): CursorData { + try { + const json = Buffer.from(cursor, "base64url").toString("utf8"); + const parsed = JSON.parse(json) as unknown; + + if ( + typeof parsed !== "object" || + parsed === null || + typeof (parsed as Record)["t"] !== "number" || + typeof (parsed as Record)["id"] !== "string" + ) { + throw new Error("Cursor has unexpected shape"); + } + + return parsed as CursorData; + } catch { + throw new Error(`Invalid pagination cursor: "${cursor}"`); + } +} diff --git a/src/infra/repositories/in-memory/in-memory-audit.repository.ts b/src/infra/repositories/in-memory/in-memory-audit.repository.ts index 22edba0..438993e 100644 --- a/src/infra/repositories/in-memory/in-memory-audit.repository.ts +++ b/src/infra/repositories/in-memory/in-memory-audit.repository.ts @@ -32,7 +32,18 @@ */ import type { IAuditLogRepository } from "../../../core/ports/audit-repository.port"; -import type { AuditLog, AuditLogFilters, PageOptions, PageResult } from "../../../core/types"; +import type { + AuditLog, + AuditLogFilters, + CursorPageOptions, + CursorPageResult, + PageOptions, + PageResult, +} from "../../../core/types"; +import { decodeCursor, encodeCursor } from "../cursor.util"; + +// eslint-disable-next-line no-unused-vars +type ArchiveHandler = (logs: AuditLog[]) => Promise | void; /** * In-memory implementation of audit log repository. @@ -74,12 +85,15 @@ export class InMemoryAuditRepository implements IAuditLogRepository { */ private readonly logs = new Map(); + private readonly archiveHandler: ArchiveHandler | undefined; + /** * Creates a new in-memory repository. * * @param initialData - Optional initial audit logs (for testing) */ - constructor(initialData?: AuditLog[]) { + constructor(initialData?: AuditLog[], archiveHandler?: ArchiveHandler) { + this.archiveHandler = archiveHandler; if (initialData) { initialData.forEach((log) => this.logs.set(log.id, log)); } @@ -258,10 +272,88 @@ export class InMemoryAuditRepository implements IAuditLogRepository { return deleted; } + /** + * Archives audit logs older than the specified date. + * + * If no archive handler is configured, this is a no-op. + * + * @param beforeDate - Archive logs older than this date + * @returns Number of archived logs + */ + async archiveOlderThan(beforeDate: Date): Promise { + if (!this.archiveHandler) { + return 0; + } + + const logsToArchive = Array.from(this.logs.values()).filter( + (log) => log.timestamp < beforeDate, + ); + if (logsToArchive.length === 0) { + return 0; + } + + await this.archiveHandler(logsToArchive.map((log) => this.deepCopy(log))); + return logsToArchive.length; + } + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // UTILITY METHODS (Testing Support) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + /** + * Queries audit logs with cursor-based pagination. + * + * Results are sorted by `timestamp DESC, id ASC` for consistency with the + * MongoDB adapter. A base64url cursor encodes `{ t, id }` of the last item. + * + * @param filters - Filter criteria + * @param options - Cursor and limit options + * @returns Cursor-paginated result + */ + async queryWithCursor( + filters: Partial, + options?: CursorPageOptions, + ): Promise> { + const limit = options?.limit ?? 20; + + // Apply filters and sort: timestamp DESC, id ASC + let sorted = Array.from(this.logs.values()) + .filter((log) => this.matchesFilters(log, filters)) + .sort((a, b) => { + const timeDiff = b.timestamp.getTime() - a.timestamp.getTime(); + if (timeDiff !== 0) return timeDiff; + return a.id.localeCompare(b.id); + }); + + // Apply cursor constraint (skip everything up to and including the cursor item) + if (options?.cursor) { + const cursorData = decodeCursor(options.cursor); + const idx = sorted.findIndex( + (log) => + log.timestamp.getTime() < cursorData.t || + (log.timestamp.getTime() === cursorData.t && log.id > cursorData.id), + ); + sorted = idx >= 0 ? sorted.slice(idx) : []; + } + + const hasMore = sorted.length > limit; + const page = sorted.slice(0, limit); + const data = page.map((log) => this.deepCopy(log)); + + const lastItem = data.at(-1); + const result: CursorPageResult = { + data, + hasMore, + limit, + }; + + if (hasMore && lastItem) { + result.nextCursor = encodeCursor({ t: lastItem.timestamp.getTime(), id: lastItem.id }); + } + + return result; + } + /** * Clears all audit logs. * Useful for cleanup between tests. @@ -298,46 +390,60 @@ export class InMemoryAuditRepository implements IAuditLogRepository { * @returns True if log matches all filters */ private matchesFilters(log: AuditLog, filters: Partial): boolean { - // Actor filters + return ( + this.matchesActorFilters(log, filters) && + this.matchesResourceFilters(log, filters) && + this.matchesDateAndActionFilters(log, filters) && + this.matchesMetadataFilters(log, filters) && + this.matchesSearchFilter(log, filters) + ); + } + + private matchesActorFilters(log: AuditLog, filters: Partial): boolean { if (filters.actorId && log.actor.id !== filters.actorId) return false; if (filters.actorType && log.actor.type !== filters.actorType) return false; + return true; + } - // Resource filters + private matchesResourceFilters(log: AuditLog, filters: Partial): boolean { if (filters.resourceType && log.resource.type !== filters.resourceType) return false; if (filters.resourceId && log.resource.id !== filters.resourceId) return false; + return true; + } - // Action filter + private matchesDateAndActionFilters(log: AuditLog, filters: Partial): boolean { if (filters.action && log.action !== filters.action) return false; if (filters.actions && !filters.actions.includes(log.action)) return false; - - // Date range if (filters.startDate && log.timestamp < filters.startDate) return false; if (filters.endDate && log.timestamp > filters.endDate) return false; + return true; + } - // Other filters + private matchesMetadataFilters(log: AuditLog, filters: Partial): boolean { if (filters.ipAddress && log.ipAddress !== filters.ipAddress) return false; if (filters.requestId && log.requestId !== filters.requestId) return false; if (filters.sessionId && log.sessionId !== filters.sessionId) return false; - - // Simple text search (searches in action, resource type, actor name) - if (filters.search) { - const searchLower = filters.search.toLowerCase(); - const searchableText = [ - log.action, - log.resource.type, - log.actor.name || "", - log.actionDescription || "", - log.reason || "", - ] - .join(" ") - .toLowerCase(); - - if (!searchableText.includes(searchLower)) return false; - } - + if (filters.idempotencyKey && log.idempotencyKey !== filters.idempotencyKey) return false; return true; } + private matchesSearchFilter(log: AuditLog, filters: Partial): boolean { + if (!filters.search) return true; + + const searchLower = filters.search.toLowerCase(); + const searchableText = [ + log.action, + log.resource.type, + log.actor.name ?? "", + log.actionDescription ?? "", + log.reason ?? "", + ] + .join(" ") + .toLowerCase(); + + return searchableText.includes(searchLower); + } + /** * Sorts audit logs by timestamp. * diff --git a/src/infra/repositories/mongodb/audit-log.schema.ts b/src/infra/repositories/mongodb/audit-log.schema.ts index 54f8ad9..502e7df 100644 --- a/src/infra/repositories/mongodb/audit-log.schema.ts +++ b/src/infra/repositories/mongodb/audit-log.schema.ts @@ -126,12 +126,15 @@ export const AuditLogSchema = new Schema( }, requestId: { type: String, - index: true, }, sessionId: { type: String, index: true, }, + idempotencyKey: { + type: String, + sparse: true, + }, reason: { type: String, }, diff --git a/src/infra/repositories/mongodb/mongo-audit.repository.ts b/src/infra/repositories/mongodb/mongo-audit.repository.ts index ff0b8ba..f662dcf 100644 --- a/src/infra/repositories/mongodb/mongo-audit.repository.ts +++ b/src/infra/repositories/mongodb/mongo-audit.repository.ts @@ -22,10 +22,21 @@ import type { Model } from "mongoose"; import type { IAuditLogRepository } from "../../../core/ports/audit-repository.port"; -import type { AuditLog, AuditLogFilters, PageOptions, PageResult } from "../../../core/types"; +import type { + AuditLog, + AuditLogFilters, + CursorPageOptions, + CursorPageResult, + PageOptions, + PageResult, +} from "../../../core/types"; +import { decodeCursor, encodeCursor } from "../cursor.util"; import type { AuditLogDocument } from "./audit-log.schema"; +// eslint-disable-next-line no-unused-vars +type ArchiveHandler = (logs: AuditLog[]) => Promise | void; + /** * MongoDB implementation of audit log repository. * @@ -52,13 +63,18 @@ import type { AuditLogDocument } from "./audit-log.schema"; * ``` */ export class MongoAuditRepository implements IAuditLogRepository { + private readonly model: Model; + private readonly archiveHandler: ArchiveHandler | undefined; + /** * Creates a new MongoDB audit repository. * * @param model - Mongoose model for AuditLog */ - // eslint-disable-next-line no-unused-vars - constructor(private readonly model: Model) {} + constructor(model: Model, archiveHandler?: ArchiveHandler) { + this.model = model; + this.archiveHandler = archiveHandler; + } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // CREATE OPERATIONS @@ -209,6 +225,89 @@ export class MongoAuditRepository implements IAuditLogRepository { return result.deletedCount || 0; } + /** + * Archives audit logs older than the specified date. + * + * If no archive handler is configured, this is a no-op. + * + * @param beforeDate - Archive logs older than this date + * @returns Number of archived logs + */ + async archiveOlderThan(beforeDate: Date): Promise { + if (!this.archiveHandler) { + return 0; + } + + const documents = await this.model + .find({ timestamp: { $lt: beforeDate } }) + .lean() + .exec(); + if (documents.length === 0) { + return 0; + } + + const logs = documents.map((doc) => this.toPlainObject(doc)); + await this.archiveHandler(logs); + return logs.length; + } + + /** + * Queries audit logs using cursor-based pagination. + * + * Results are sorted by `timestamp DESC, id ASC`. + * The cursor encodes the `{ timestamp, id }` of the last returned item. + * + * @param filters - Filter criteria + * @param options - Cursor and limit options + * @returns Cursor-paginated result + */ + async queryWithCursor( + filters: Partial, + options?: CursorPageOptions, + ): Promise> { + const limit = options?.limit ?? 20; + const query = this.buildQuery(filters); + + // Apply cursor constraint when provided + if (options?.cursor) { + const cursorData = decodeCursor(options.cursor); + const cursorDate = new Date(cursorData.t); + const cursorId = cursorData.id; + + // "After cursor" in descending-timestamp + ascending-id order: + // timestamp < cursorDate OR (timestamp == cursorDate AND id > cursorId) + query["$or"] = [ + { timestamp: { $lt: cursorDate } }, + { timestamp: cursorDate, id: { $gt: cursorId } }, + ]; + } + + // Fetch limit+1 to detect whether more pages exist + const documents = await this.model + .find(query) + .sort({ timestamp: -1, id: 1 }) + .limit(limit + 1) + .lean() + .exec(); + + const hasMore = documents.length > limit; + const pageDocuments = documents.slice(0, limit); + const data = pageDocuments.map((doc) => this.toPlainObject(doc)); + + const lastItem = data.at(-1); + const result: CursorPageResult = { + data, + hasMore, + limit, + }; + + if (hasMore && lastItem) { + result.nextCursor = encodeCursor({ t: lastItem.timestamp.getTime(), id: lastItem.id }); + } + + return result; + } + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // PRIVATE HELPER METHODS // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -224,40 +323,54 @@ export class MongoAuditRepository implements IAuditLogRepository { */ private buildQuery(filters: Partial): Record { const query: Record = {}; + this.applyActorFilters(query, filters); + this.applyResourceFilters(query, filters); + this.applyActionFilter(query, filters); + this.applyDateRangeFilter(query, filters); + this.applyMetadataFilters(query, filters); + if (filters.search) query.$text = { $search: filters.search }; + return query; + } - // Actor filters + private applyActorFilters(query: Record, filters: Partial): void { if (filters.actorId) query["actor.id"] = filters.actorId; if (filters.actorType) query["actor.type"] = filters.actorType; + } - // Resource filters + private applyResourceFilters( + query: Record, + filters: Partial, + ): void { if (filters.resourceType) query["resource.type"] = filters.resourceType; if (filters.resourceId) query["resource.id"] = filters.resourceId; + } - // Action filter (can be single action or array) + private applyActionFilter(query: Record, filters: Partial): void { if (filters.action) { query.action = filters.action; } else if (filters.actions && filters.actions.length > 0) { query.action = { $in: filters.actions }; } + } - // Date range filters - if (filters.startDate || filters.endDate) { - query.timestamp = {}; - if (filters.startDate) query.timestamp.$gte = filters.startDate; - if (filters.endDate) query.timestamp.$lte = filters.endDate; - } + private applyDateRangeFilter( + query: Record, + filters: Partial, + ): void { + if (!filters.startDate && !filters.endDate) return; + query.timestamp = {}; + if (filters.startDate) query.timestamp.$gte = filters.startDate; + if (filters.endDate) query.timestamp.$lte = filters.endDate; + } - // Other filters + private applyMetadataFilters( + query: Record, + filters: Partial, + ): void { if (filters.ipAddress) query.ipAddress = filters.ipAddress; if (filters.requestId) query.requestId = filters.requestId; if (filters.sessionId) query.sessionId = filters.sessionId; - - // Full-text search (if text index is configured) - if (filters.search) { - query.$text = { $search: filters.search }; - } - - return query; + if (filters.idempotencyKey) query.idempotencyKey = filters.idempotencyKey; } /** diff --git a/src/nest/interfaces.ts b/src/nest/interfaces.ts index a37dcc5..14330f6 100644 --- a/src/nest/interfaces.ts +++ b/src/nest/interfaces.ts @@ -15,8 +15,14 @@ import type { ModuleMetadata, Type } from "@nestjs/common"; import type { Model } from "mongoose"; +import type { IAuditEventPublisher } from "../core/ports/audit-event-publisher.port"; +import type { IAuditObserver } from "../core/ports/audit-observer.port"; +import type { AuditLog } from "../core/types"; import type { AuditLogDocument } from "../infra/repositories/mongodb/audit-log.schema"; +// eslint-disable-next-line no-unused-vars +export type ArchiveHandler = (logs: AuditLog[]) => Promise | void; + // ============================================================================ // REPOSITORY CONFIGURATION // ============================================================================ @@ -132,6 +138,65 @@ export interface ChangeDetectorConfig { type?: "deep-diff"; } +/** + * PII redaction configuration. + */ +export interface RedactionConfig { + /** Enable/disable redaction before persistence. */ + enabled?: boolean; + + /** Dot-path fields to redact (e.g. actor.email, metadata.token). */ + fields?: string[]; + + /** Replacement mask value. */ + mask?: string; +} + +/** + * Idempotency configuration. + */ +export interface IdempotencyConfig { + /** Enable/disable write deduplication. */ + enabled?: boolean; + + /** Which field is used as dedupe key. */ + keyStrategy?: "idempotencyKey" | "requestId"; +} + +/** + * Retention and archival configuration. + */ +export interface RetentionConfig { + /** Enable retention processing. */ + enabled?: boolean; + + /** Number of days to retain hot data. */ + retentionDays?: number; + + /** Whether to run retention cleanup after each write. */ + autoCleanupOnWrite?: boolean; + + /** Archive logs before delete operations. */ + archiveBeforeDelete?: boolean; + + /** Optional callback receiving logs selected for archival. */ + archiveHandler?: ArchiveHandler; +} + +/** + * Event streaming configuration. + */ +export interface EventStreamingConfig { + /** Enable/disable audit event emission. */ + enabled?: boolean; + + /** + * Optional event publisher adapter. + * When omitted and enabled=true, a default EventEmitter publisher is used. + */ + publisher?: IAuditEventPublisher; +} + // ============================================================================ // MAIN MODULE OPTIONS // ============================================================================ @@ -204,6 +269,45 @@ export interface AuditKitModuleOptions { * Optional - defaults to deep-diff detector. */ changeDetector?: ChangeDetectorConfig; + + /** + * PII redaction policy applied before persistence. + */ + redaction?: RedactionConfig; + + /** + * Idempotency policy to deduplicate repeated writes. + */ + idempotency?: IdempotencyConfig; + + /** + * Retention and archival policy. + */ + retention?: RetentionConfig; + + /** + * Observability observer (OpenTelemetry, metrics, custom logging, etc.). + * When provided, AuditService calls `observer.onEvent()` after each operation. + * Observer errors are swallowed and never affect core operations. + * + * @example + * ```typescript + * AuditKitModule.register({ + * repository: { type: 'in-memory' }, + * observer: { + * onEvent(event) { + * console.log(`[audit] ${event.operation} in ${event.durationMs}ms`); + * }, + * }, + * }); + * ``` + */ + observer?: IAuditObserver; + + /** + * Event streaming settings for emitting audit lifecycle events. + */ + eventStreaming?: EventStreamingConfig; } // ============================================================================ diff --git a/src/nest/module.spec.ts b/src/nest/module.spec.ts index 8eb3029..03e23e9 100644 --- a/src/nest/module.spec.ts +++ b/src/nest/module.spec.ts @@ -475,17 +475,54 @@ describe("AuditKitModule", () => { }); it("should throw for mongodb config without uri or model", async () => { - await expect( - Test.createTestingModule({ - imports: [ - AuditKitModule.register({ - repository: { - type: "mongodb", + expect(() => + AuditKitModule.register({ + repository: { + type: "mongodb", + }, + }), + ).toThrow("MongoDB repository requires either 'uri' or 'model' to be configured"); + }); + + it("should throw for invalid retention days", async () => { + expect(() => + AuditKitModule.register({ + repository: { type: "in-memory" }, + retention: { + enabled: true, + retentionDays: 0, + }, + }), + ).toThrow("Retention requires a positive integer 'retentionDays'"); + }); + + it("should throw when archive-before-delete has no handler", async () => { + expect(() => + AuditKitModule.register({ + repository: { type: "in-memory" }, + retention: { + enabled: true, + retentionDays: 30, + archiveBeforeDelete: true, + }, + }), + ).toThrow("Retention with archiveBeforeDelete=true requires an archiveHandler"); + }); + + it("should throw when event streaming publisher is configured but disabled", async () => { + expect(() => + AuditKitModule.register({ + repository: { type: "in-memory" }, + eventStreaming: { + enabled: false, + publisher: { + publish: () => { + // no-op }, - }), - ], - }).compile(), - ).rejects.toThrow("MongoDB repository requires either 'uri' or 'model' to be configured"); + }, + }, + }), + ).toThrow("Event streaming publisher is configured but event streaming is disabled"); }); }); }); diff --git a/src/nest/module.ts b/src/nest/module.ts index 755a579..fbd1aef 100644 --- a/src/nest/module.ts +++ b/src/nest/module.ts @@ -28,6 +28,7 @@ import type { IChangeDetector } from "../core/ports/change-detector.port"; import type { IIdGenerator } from "../core/ports/id-generator.port"; import type { ITimestampProvider } from "../core/ports/timestamp-provider.port"; import { DeepDiffChangeDetector } from "../infra/providers/change-detector/deep-diff-change-detector"; +import { EventEmitterAuditEventPublisher } from "../infra/providers/events/event-emitter-audit-event.publisher"; import { NanoidIdGenerator } from "../infra/providers/id-generator/nanoid-id-generator"; import { SystemTimestampProvider } from "../infra/providers/timestamp/system-timestamp-provider"; import { InMemoryAuditRepository } from "../infra/repositories/in-memory/in-memory-audit.repository"; @@ -42,6 +43,11 @@ import { TIMESTAMP_PROVIDER, } from "./constants"; import type { AuditKitModuleAsyncOptions, AuditKitModuleOptions } from "./interfaces"; +import { + getArchiveHandler, + toAuditServiceRuntimeOptions, + validateAuditKitModuleOptions, +} from "./options.validation"; import { createAuditKitAsyncProviders, createAuditKitProviders } from "./providers"; // ============================================================================ @@ -164,6 +170,7 @@ export class AuditKitModule { * ``` */ static register(options: AuditKitModuleOptions): DynamicModule { + validateAuditKitModuleOptions(options); const providers = createAuditKitProviders(options); return { @@ -320,13 +327,14 @@ export class AuditKitModule { useFactory: async ( moduleOptions: AuditKitModuleOptions, ): Promise => { + validateAuditKitModuleOptions(moduleOptions); const config = moduleOptions.repository; switch (config.type) { case "mongodb": { // If a model is provided, use it directly if (config.model) { - return new MongoAuditRepository(config.model); + return new MongoAuditRepository(config.model, getArchiveHandler(moduleOptions)); } // Otherwise, create a connection and model @@ -343,12 +351,12 @@ export class AuditKitModule { const connection = await connect(config.uri, connectionOptions as ConnectOptions); const model = connection.model("AuditLog", AuditLogSchema); - return new MongoAuditRepository(model); + return new MongoAuditRepository(model, getArchiveHandler(moduleOptions)); } case "in-memory": default: - return new InMemoryAuditRepository(); + return new InMemoryAuditRepository(undefined, getArchiveHandler(moduleOptions)); } }, inject: [AUDIT_KIT_OPTIONS], @@ -361,10 +369,28 @@ export class AuditKitModule { idGenerator: IIdGenerator, timestampProvider: ITimestampProvider, changeDetector: IChangeDetector, + moduleOptions: AuditKitModuleOptions, ) => { - return new AuditService(repository, idGenerator, timestampProvider, changeDetector); + const runtimeOptions = toAuditServiceRuntimeOptions(moduleOptions); + if (moduleOptions.eventStreaming?.enabled && !runtimeOptions.eventPublisher) { + runtimeOptions.eventPublisher = new EventEmitterAuditEventPublisher(); + } + + return new AuditService( + repository, + idGenerator, + timestampProvider, + changeDetector, + runtimeOptions, + ); }, - inject: [AUDIT_REPOSITORY, ID_GENERATOR, TIMESTAMP_PROVIDER, CHANGE_DETECTOR], + inject: [ + AUDIT_REPOSITORY, + ID_GENERATOR, + TIMESTAMP_PROVIDER, + CHANGE_DETECTOR, + AUDIT_KIT_OPTIONS, + ], }, ], exports: [AuditService, AUDIT_REPOSITORY, ID_GENERATOR, TIMESTAMP_PROVIDER, CHANGE_DETECTOR], diff --git a/src/nest/options.validation.ts b/src/nest/options.validation.ts new file mode 100644 index 0000000..f8f47c2 --- /dev/null +++ b/src/nest/options.validation.ts @@ -0,0 +1,150 @@ +/** + * ============================================================================ + * AUDIT KIT MODULE OPTIONS VALIDATION + * ============================================================================ + * + * Centralized runtime validation for module options. + * + * @packageDocumentation + */ + +import type { IAuditEventPublisher } from "../core/ports/audit-event-publisher.port"; +import type { IAuditObserver } from "../core/ports/audit-observer.port"; + +import type { AuditKitModuleOptions } from "./interfaces"; + +/** + * Runtime options passed to AuditService from module configuration. + */ +export interface AuditServiceRuntimeOptions { + piiRedaction?: { + enabled?: boolean; + fields?: string[]; + mask?: string; + }; + idempotency?: { + enabled?: boolean; + keyStrategy?: "idempotencyKey" | "requestId"; + }; + retention?: { + enabled?: boolean; + retentionDays?: number; + autoCleanupOnWrite?: boolean; + archiveBeforeDelete?: boolean; + }; + /** Observability observer wired from module options. */ + observer?: IAuditObserver; + + /** Event publisher wired from module options. */ + eventPublisher?: IAuditEventPublisher; +} + +/** + * Validates module options and throws a descriptive Error on invalid configuration. + */ +export function validateAuditKitModuleOptions(options: AuditKitModuleOptions): void { + validateRepository(options); + validateRedaction(options); + validateRetention(options); + validateIdempotency(options); + validateEventStreaming(options); +} + +function validateRepository(options: AuditKitModuleOptions): void { + if (!options?.repository) { + throw new Error("AuditKitModule options must include a repository configuration"); + } + + if ( + options.repository.type === "mongodb" && + !options.repository.uri && + !options.repository.model + ) { + throw new Error("MongoDB repository requires either 'uri' or 'model' to be configured"); + } +} + +function validateRedaction(options: AuditKitModuleOptions): void { + const fields = options.redaction?.fields; + if (fields && fields.some((field) => typeof field !== "string" || field.trim().length === 0)) { + throw new Error("Redaction fields must be non-empty strings"); + } +} + +function validateRetention(options: AuditKitModuleOptions): void { + if (!options.retention?.enabled) return; + + const { retentionDays, archiveBeforeDelete, archiveHandler } = options.retention; + if (!Number.isInteger(retentionDays) || (retentionDays as number) <= 0) { + throw new Error("Retention requires a positive integer 'retentionDays'"); + } + + if (archiveBeforeDelete && archiveHandler === undefined) { + throw new Error("Retention with archiveBeforeDelete=true requires an archiveHandler"); + } +} + +function validateIdempotency(options: AuditKitModuleOptions): void { + if (options.idempotency?.keyStrategy === "requestId" && options.idempotency?.enabled === false) { + throw new Error("Idempotency key strategy is configured but idempotency is disabled"); + } +} + +function validateEventStreaming(options: AuditKitModuleOptions): void { + if (options.eventStreaming?.enabled === false && options.eventStreaming?.publisher) { + throw new Error("Event streaming publisher is configured but event streaming is disabled"); + } +} + +/** + * Maps module options to runtime options consumed by AuditService. + */ +export function toAuditServiceRuntimeOptions( + options: AuditKitModuleOptions, +): AuditServiceRuntimeOptions { + const runtimeOptions: AuditServiceRuntimeOptions = {}; + + if (options.redaction) { + runtimeOptions.piiRedaction = options.redaction; + } + + if (options.idempotency) { + runtimeOptions.idempotency = options.idempotency; + } + + if (options.retention) { + const retention: NonNullable = {}; + if (options.retention.enabled !== undefined) retention.enabled = options.retention.enabled; + if (options.retention.retentionDays !== undefined) { + retention.retentionDays = options.retention.retentionDays; + } + if (options.retention.autoCleanupOnWrite !== undefined) { + retention.autoCleanupOnWrite = options.retention.autoCleanupOnWrite; + } + if (options.retention.archiveBeforeDelete !== undefined) { + retention.archiveBeforeDelete = options.retention.archiveBeforeDelete; + } + runtimeOptions.retention = retention; + } + + if (options.observer) { + runtimeOptions.observer = options.observer; + } + + if (options.eventStreaming?.enabled && options.eventStreaming?.publisher) { + runtimeOptions.eventPublisher = options.eventStreaming.publisher; + } + + return runtimeOptions; +} + +/** + * Extracts archive handler function from module options. + */ +export function getArchiveHandler( + options: AuditKitModuleOptions, +): AuditKitModuleOptions["retention"] extends undefined + ? undefined + : NonNullable["archiveHandler"] { + return options.retention?.archiveHandler; +} diff --git a/src/nest/providers.ts b/src/nest/providers.ts index 188c0ad..2bd59d1 100644 --- a/src/nest/providers.ts +++ b/src/nest/providers.ts @@ -23,6 +23,7 @@ import type { IChangeDetector } from "../core/ports/change-detector.port"; import type { IIdGenerator } from "../core/ports/id-generator.port"; import type { ITimestampProvider } from "../core/ports/timestamp-provider.port"; import { DeepDiffChangeDetector } from "../infra/providers/change-detector/deep-diff-change-detector"; +import { EventEmitterAuditEventPublisher } from "../infra/providers/events/event-emitter-audit-event.publisher"; import { NanoidIdGenerator } from "../infra/providers/id-generator/nanoid-id-generator"; import { SystemTimestampProvider } from "../infra/providers/timestamp/system-timestamp-provider"; import { InMemoryAuditRepository } from "../infra/repositories/in-memory/in-memory-audit.repository"; @@ -37,6 +38,11 @@ import { TIMESTAMP_PROVIDER, } from "./constants"; import type { AuditKitModuleOptions } from "./interfaces"; +import { + getArchiveHandler, + toAuditServiceRuntimeOptions, + validateAuditKitModuleOptions, +} from "./options.validation"; // ============================================================================ // PROVIDER FACTORY @@ -59,6 +65,8 @@ import type { AuditKitModuleOptions } from "./interfaces"; * @internal */ export function createAuditKitProviders(options: AuditKitModuleOptions): Provider[] { + validateAuditKitModuleOptions(options); + return [ // Configuration provider { @@ -144,7 +152,7 @@ export function createAuditKitProviders(options: AuditKitModuleOptions): Provide case "mongodb": { // If a model is provided, use it directly if (config.model) { - return new MongoAuditRepository(config.model); + return new MongoAuditRepository(config.model, getArchiveHandler(options)); } // Otherwise, create a connection and model @@ -162,12 +170,12 @@ export function createAuditKitProviders(options: AuditKitModuleOptions): Provide const connection = await connect(config.uri, connectionOptions as ConnectOptions); const model = connection.model("AuditLog", AuditLogSchema); - return new MongoAuditRepository(model); + return new MongoAuditRepository(model, getArchiveHandler(options)); } case "in-memory": default: - return new InMemoryAuditRepository(); + return new InMemoryAuditRepository(undefined, getArchiveHandler(options)); } }, }, @@ -181,7 +189,18 @@ export function createAuditKitProviders(options: AuditKitModuleOptions): Provide timestampProvider: ITimestampProvider, changeDetector: IChangeDetector, ) => { - return new AuditService(repository, idGenerator, timestampProvider, changeDetector); + const runtimeOptions = toAuditServiceRuntimeOptions(options); + if (options.eventStreaming?.enabled && !runtimeOptions.eventPublisher) { + runtimeOptions.eventPublisher = new EventEmitterAuditEventPublisher(); + } + + return new AuditService( + repository, + idGenerator, + timestampProvider, + changeDetector, + runtimeOptions, + ); }, inject: [AUDIT_REPOSITORY, ID_GENERATOR, TIMESTAMP_PROVIDER, CHANGE_DETECTOR], }, @@ -211,7 +230,11 @@ export function createAuditKitAsyncProviders(options: { return [ { provide: AUDIT_KIT_OPTIONS, - useFactory: options.useFactory, + useFactory: async (...args: any[]) => { + const resolved = await options.useFactory!(...args); + validateAuditKitModuleOptions(resolved); + return resolved; + }, inject: options.inject ?? [], }, ]; @@ -224,7 +247,9 @@ export function createAuditKitAsyncProviders(options: { useFactory: async (optionsFactory: { createAuditKitOptions: () => Promise | AuditKitModuleOptions; }) => { - return await optionsFactory.createAuditKitOptions(); + const resolved = await optionsFactory.createAuditKitOptions(); + validateAuditKitModuleOptions(resolved); + return resolved; }, inject: [options.useClass], }, @@ -242,7 +267,9 @@ export function createAuditKitAsyncProviders(options: { useFactory: async (optionsFactory: { createAuditKitOptions: () => Promise | AuditKitModuleOptions; }) => { - return await optionsFactory.createAuditKitOptions(); + const resolved = await optionsFactory.createAuditKitOptions(); + validateAuditKitModuleOptions(resolved); + return resolved; }, inject: [options.useExisting], }, diff --git a/stryker.config.json b/stryker.config.json new file mode 100644 index 0000000..951ff83 --- /dev/null +++ b/stryker.config.json @@ -0,0 +1,31 @@ +{ + "packageManager": "npm", + "testRunner": "jest", + "mutate": [ + "src/core/audit.service.ts", + "src/core/dtos/**/*.ts", + "src/core/errors/**/*.ts", + "src/infra/repositories/**/*.ts", + "src/nest/options.validation.ts" + ], + "reporters": ["html", "progress"], + "coverageAnalysis": "perTest", + "thresholds": { + "high": 80, + "low": 60, + "break": 0 + }, + "jest": { + "projectType": "custom", + "config": { + "testEnvironment": "node", + "transform": { + "^.+\\.ts$": ["ts-jest", { "tsconfig": "tsconfig.json" }] + }, + "transformIgnorePatterns": ["node_modules/(?!(nanoid)/)"], + "testMatch": ["/src/**/*.spec.ts"] + } + }, + "ignorePatterns": ["src/**/index.ts", "src/**/*.d.ts"], + "tsconfigFile": "tsconfig.json" +} diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 26f1fe1..08cfc98 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -1,5 +1,12 @@ { "extends": "./tsconfig.json", - "include": ["src/**/*.ts", "test/**/*.ts", "__mocks__/**/*.ts", "*.ts", "*.js"], + "include": [ + "src/**/*.ts", + "test/**/*.ts", + "benchmarks/**/*.ts", + "__mocks__/**/*.ts", + "*.ts", + "*.js" + ], "exclude": ["dist", "node_modules"] } diff --git a/tsconfig.json b/tsconfig.json index 63ab110..0bfe6d8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,9 +14,10 @@ "esModuleInterop": true, "resolveJsonModule": true, "skipLibCheck": true, - "types": ["jest"], - "baseUrl": "." + "types": ["jest", "node"], + "baseUrl": ".", + "ignoreDeprecations": "5.0" }, - "include": ["src/**/*.ts", "test/**/*.ts"], + "include": ["src/**/*.ts", "test/**/*.ts", "benchmarks/**/*.ts"], "exclude": ["dist", "node_modules"] } diff --git a/vitest.config.ts b/vitest.config.ts index 639dd47..60d0ef5 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -16,5 +16,9 @@ export default defineConfig({ }, globals: true, watch: false, + benchmark: { + include: ["benchmarks/**/*.bench.ts"], + exclude: ["**/node_modules/**"], + }, }, });