commit d1c65dbf1e999a0299ef6facc08c9e4f44326cfb Author: Kevin L'Hereec Date: Fri May 29 16:46:37 2026 +0200 chore: initial commit — AdonisJS v7 TR-069 ACS - Docker infrastructure (Dockerfile, docker-compose.yml, genieacs-sim) - AdonisJS v7 app with Inertia.js + Vue 3 - MongoDB integration - CWMP/TR-069 state machine engine - Configuration engine with compliance tracking - Branding: ADAM ACS (favicon, navbar, locales) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> diff --git a/.adonisjs/client/data.d.ts b/.adonisjs/client/data.d.ts new file mode 100644 index 0000000..ee014a4 --- /dev/null +++ b/.adonisjs/client/data.d.ts @@ -0,0 +1,15 @@ +/** + * This file is automatically generated. + * DO NOT EDIT manually + */ + +/// +import type { InferData, InferVariants } from '@adonisjs/core/types/transformers' +import type UserTransformer from '#transformers/user_transformer' + +export namespace Data { + export type User = InferData + export namespace User { + export type Variants = InferVariants + } +} diff --git a/.adonisjs/client/manifest.d.ts b/.adonisjs/client/manifest.d.ts new file mode 100644 index 0000000..9fc3356 --- /dev/null +++ b/.adonisjs/client/manifest.d.ts @@ -0,0 +1,9 @@ +/** + * This file is automatically generated. + * DO NOT EDIT manually + */ + +/// +/// +/// +/// diff --git a/.adonisjs/client/registry/index.ts b/.adonisjs/client/registry/index.ts new file mode 100644 index 0000000..298b217 --- /dev/null +++ b/.adonisjs/client/registry/index.ts @@ -0,0 +1,47 @@ +/* eslint-disable prettier/prettier */ +import type { AdonisEndpoint } from '@tuyau/core/types' +import type { Registry } from './schema.d.ts' +import type { ApiDefinition } from './tree.d.ts' + +const placeholder: any = {} + +const routes = { + 'auth.new_account.store': { + methods: ["POST"], + pattern: '/api/v1/auth/signup', + tokens: [{"old":"/api/v1/auth/signup","type":0,"val":"api","end":""},{"old":"/api/v1/auth/signup","type":0,"val":"v1","end":""},{"old":"/api/v1/auth/signup","type":0,"val":"auth","end":""},{"old":"/api/v1/auth/signup","type":0,"val":"signup","end":""}], + types: placeholder as Registry['auth.new_account.store']['types'], + }, + 'auth.access_tokens.store': { + methods: ["POST"], + pattern: '/api/v1/auth/login', + tokens: [{"old":"/api/v1/auth/login","type":0,"val":"api","end":""},{"old":"/api/v1/auth/login","type":0,"val":"v1","end":""},{"old":"/api/v1/auth/login","type":0,"val":"auth","end":""},{"old":"/api/v1/auth/login","type":0,"val":"login","end":""}], + types: placeholder as Registry['auth.access_tokens.store']['types'], + }, + 'profile.profile.show': { + methods: ["GET","HEAD"], + pattern: '/api/v1/account/profile', + tokens: [{"old":"/api/v1/account/profile","type":0,"val":"api","end":""},{"old":"/api/v1/account/profile","type":0,"val":"v1","end":""},{"old":"/api/v1/account/profile","type":0,"val":"account","end":""},{"old":"/api/v1/account/profile","type":0,"val":"profile","end":""}], + types: placeholder as Registry['profile.profile.show']['types'], + }, + 'profile.access_tokens.destroy': { + methods: ["POST"], + pattern: '/api/v1/account/logout', + tokens: [{"old":"/api/v1/account/logout","type":0,"val":"api","end":""},{"old":"/api/v1/account/logout","type":0,"val":"v1","end":""},{"old":"/api/v1/account/logout","type":0,"val":"account","end":""},{"old":"/api/v1/account/logout","type":0,"val":"logout","end":""}], + types: placeholder as Registry['profile.access_tokens.destroy']['types'], + }, +} as const satisfies Record + +export { routes } + +export const registry = { + routes, + $tree: {} as ApiDefinition, +} + +declare module '@tuyau/core/types' { + export interface UserRegistry { + routes: typeof routes + $tree: ApiDefinition + } +} diff --git a/.adonisjs/client/registry/schema.d.ts b/.adonisjs/client/registry/schema.d.ts new file mode 100644 index 0000000..82c02fd --- /dev/null +++ b/.adonisjs/client/registry/schema.d.ts @@ -0,0 +1,58 @@ +/* eslint-disable prettier/prettier */ +/// + +import type { ExtractBody, ExtractErrorResponse, ExtractQuery, ExtractQueryForGet, ExtractResponse } from '@tuyau/core/types' +import type { InferInput, SimpleError } from '@vinejs/vine/types' + +export type ParamValue = string | number | bigint | boolean + +export interface Registry { + 'auth.new_account.store': { + methods: ["POST"] + pattern: '/api/v1/auth/signup' + types: { + body: ExtractBody> + paramsTuple: [] + params: {} + query: ExtractQuery> + response: ExtractResponse>> + errorResponse: ExtractErrorResponse>> | { status: 422; response: { errors: SimpleError[] } } + } + } + 'auth.access_tokens.store': { + methods: ["POST"] + pattern: '/api/v1/auth/login' + types: { + body: ExtractBody> + paramsTuple: [] + params: {} + query: ExtractQuery> + response: ExtractResponse>> + errorResponse: ExtractErrorResponse>> | { status: 422; response: { errors: SimpleError[] } } + } + } + 'profile.profile.show': { + methods: ["GET","HEAD"] + pattern: '/api/v1/account/profile' + types: { + body: {} + paramsTuple: [] + params: {} + query: {} + response: ExtractResponse>> + errorResponse: ExtractErrorResponse>> + } + } + 'profile.access_tokens.destroy': { + methods: ["POST"] + pattern: '/api/v1/account/logout' + types: { + body: {} + paramsTuple: [] + params: {} + query: {} + response: ExtractResponse>> + errorResponse: ExtractErrorResponse>> + } + } +} diff --git a/.adonisjs/client/registry/tree.d.ts b/.adonisjs/client/registry/tree.d.ts new file mode 100644 index 0000000..3964805 --- /dev/null +++ b/.adonisjs/client/registry/tree.d.ts @@ -0,0 +1,21 @@ +/* eslint-disable prettier/prettier */ +import type { routes } from './index.ts' + +export interface ApiDefinition { + auth: { + newAccount: { + store: typeof routes['auth.new_account.store'] + } + accessTokens: { + store: typeof routes['auth.access_tokens.store'] + } + } + profile: { + profile: { + show: typeof routes['profile.profile.show'] + } + accessTokens: { + destroy: typeof routes['profile.access_tokens.destroy'] + } + } +} diff --git a/.adonisjs/server/controllers.ts b/.adonisjs/server/controllers.ts new file mode 100644 index 0000000..ec42cd6 --- /dev/null +++ b/.adonisjs/server/controllers.ts @@ -0,0 +1,14 @@ +/** + * This file is automatically generated. + * DO NOT EDIT manually + */ + +export const controllers = { + Commands: () => import('#controllers/commands_controller'), + ConfigurationsV1: () => import('#controllers/configurations_v1_controller'), + Cwmp: () => import('#controllers/cwmp_controller'), + Devices: () => import('#controllers/devices_controller'), + FirmwareCatalog: () => import('#controllers/firmware_catalog_controller'), + Health: () => import('#controllers/health_controller'), + Pages: () => import('#controllers/pages_controller'), +} diff --git a/.adonisjs/server/events.ts b/.adonisjs/server/events.ts new file mode 100644 index 0000000..a74e59b --- /dev/null +++ b/.adonisjs/server/events.ts @@ -0,0 +1,6 @@ +/** + * This file is automatically generated. + * DO NOT EDIT manually + */ + +export const events = {} diff --git a/.adonisjs/server/listeners.ts b/.adonisjs/server/listeners.ts new file mode 100644 index 0000000..05f35f8 --- /dev/null +++ b/.adonisjs/server/listeners.ts @@ -0,0 +1,6 @@ +/** + * This file is automatically generated. + * DO NOT EDIT manually + */ + +export const listeners = {} diff --git a/.adonisjs/server/routes.d.ts b/.adonisjs/server/routes.d.ts new file mode 100644 index 0000000..9aa155f --- /dev/null +++ b/.adonisjs/server/routes.d.ts @@ -0,0 +1,26 @@ +import '@adonisjs/core/types/http' + +type ParamValue = string | number | bigint | boolean + +export type ScannedRoutes = { + ALL: { + 'auth.new_account.store': { paramsTuple?: []; params?: {} } + 'auth.access_tokens.store': { paramsTuple?: []; params?: {} } + 'profile.profile.show': { paramsTuple?: []; params?: {} } + 'profile.access_tokens.destroy': { paramsTuple?: []; params?: {} } + } + GET: { + 'profile.profile.show': { paramsTuple?: []; params?: {} } + } + HEAD: { + 'profile.profile.show': { paramsTuple?: []; params?: {} } + } + POST: { + 'auth.new_account.store': { paramsTuple?: []; params?: {} } + 'auth.access_tokens.store': { paramsTuple?: []; params?: {} } + 'profile.access_tokens.destroy': { paramsTuple?: []; params?: {} } + } +} +declare module '@adonisjs/core/types/http' { + export interface RoutesList extends ScannedRoutes {} +} \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..97cb37d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +node_modules +build +.git +.idea +.env diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f830f40 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +# http://editorconfig.org + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.json] +insert_final_newline = unset + +[**.min.js] +indent_style = unset +insert_final_newline = unset + +[MakeFile] +indent_style = space + +[*.md] +trim_trailing_whitespace = false diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0d4257f --- /dev/null +++ b/.env.example @@ -0,0 +1,31 @@ +# Node +TZ=UTC +PORT=8080 +HOST=0.0.0.0 +NODE_ENV=development + +# App +LOG_LEVEL=info +APP_KEY=your-app-key-here-min-16-chars!! +APP_URL=http://${HOST}:${PORT} + +# Session +SESSION_DRIVER=cookie + +# MongoDB +MONGODB_URI=mongodb://adam:adam@localhost:27017/adam-acs?authSource=admin +MONGODB_DB_NAME=adam-acs + +# Observability +OTEL_EXPORTER_URI=http://localhost:4318 +OTEL_SAMPLER_MODE=parentbased_traceidratio +OTEL_SAMPLER_RATIO=0.1 +OTEL_SERVICE_NAME=adam-acs + +# Remote logging +LOG_HOST=localhost +LOG_PORT=514 +LOG_PROTOCOL=udp4 + +# Security +HASH_KEY=0123456789abcdef0123456789abcdef \ No newline at end of file diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..28d2da0 --- /dev/null +++ b/.env.test @@ -0,0 +1 @@ +SESSION_DRIVER=memory diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd7579a --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Dependencies and AdonisJS build +node_modules +build +tmp/* +!tmp/.gitkeep + +# Secrets +.env +.env.local +.env.production.local +.env.development.local + +# Frontend assets compiled code +public/assets + +# Build tools specific +npm-debug.log +yarn-error.log + +# Editors specific +.fleet +.idea +.vscode + +# Platform specific +.DS_Store diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..8e91c7d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +.adonisjs +node_modules +build diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5401dab --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +# Build stage +FROM node:24-alpine AS builder + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm install + +COPY . . +RUN node ace build + +# Production stage +FROM node:24-alpine AS production + +WORKDIR /app + +ENV NODE_ENV=production +ENV PORT=8080 +ENV HOST=0.0.0.0 + +COPY --from=builder /app/build ./ +RUN npm install --omit=dev + +RUN addgroup -g 1001 -S appgroup && \ + adduser -u 1001 -S appuser -G appgroup + +USER appuser + +EXPOSE 8080 + +CMD ["node", "bin/server.js"] diff --git a/Dockerfile.genieacs-sim b/Dockerfile.genieacs-sim new file mode 100644 index 0000000..a031844 --- /dev/null +++ b/Dockerfile.genieacs-sim @@ -0,0 +1,10 @@ +FROM node:20-alpine + +RUN apk add --no-cache build-base git && \ + rm -rf /var/cache/apk/* + +WORKDIR /app +RUN git clone https://github.com/genieacs/genieacs-sim.git . && \ + npm install + +ENTRYPOINT ["./genieacs-sim"] diff --git a/ace.js b/ace.js new file mode 100644 index 0000000..0eefd20 --- /dev/null +++ b/ace.js @@ -0,0 +1,27 @@ +/* +|-------------------------------------------------------------------------- +| JavaScript entrypoint for running ace commands +|-------------------------------------------------------------------------- +| +| DO NOT MODIFY THIS FILE AS IT WILL BE OVERRIDDEN DURING THE BUILD +| PROCESS. +| +| See docs.adonisjs.com/guides/typescript-build-process#creating-production-build +| +| Since, we cannot run TypeScript source code using "node" binary, we need +| a JavaScript entrypoint to run ace commands. +| +| This file registers the "ts-node/esm" hook with the Node.js module system +| and then imports the "bin/console.ts" file. +| +*/ + +/** + * Register hook to process TypeScript files using @poppinss/ts-exec + */ +import '@poppinss/ts-exec' + +/** + * Import ace console entrypoint + */ +await import('./bin/console.js') diff --git a/adonisrc.ts b/adonisrc.ts new file mode 100644 index 0000000..3ae8785 --- /dev/null +++ b/adonisrc.ts @@ -0,0 +1,129 @@ +import { indexEntities } from '@adonisjs/core' +import { defineConfig } from '@adonisjs/core/app' +import { generateRegistry } from '@tuyau/core/hooks' + +export default defineConfig({ + /* + |-------------------------------------------------------------------------- + | Experimental flags + |-------------------------------------------------------------------------- + | + | The following features will be enabled by default in the next major release + | of AdonisJS. You can opt into them today to avoid any breaking changes + | during upgrade. + | + */ + experimental: {}, + + /* + |-------------------------------------------------------------------------- + | Commands + |-------------------------------------------------------------------------- + | + | List of ace commands to register from packages. The application commands + | will be scanned automatically from the "./commands" directory. + | + */ + commands: [ + () => import('@adonisjs/core/commands'), + () => import('@adonisjs/session/commands'), + ], + + /* + |-------------------------------------------------------------------------- + | Service providers + |-------------------------------------------------------------------------- + | + | List of service providers to import and register when booting the + | application + | + */ + providers: [ + () => import('@adonisjs/core/providers/app_provider'), + () => import('@adonisjs/core/providers/hash_provider'), + { + file: () => import('@adonisjs/core/providers/repl_provider'), + environment: ['repl', 'test'], + }, + () => import('@adonisjs/core/providers/vinejs_provider'), + () => import('@adonisjs/core/providers/edge_provider'), + () => import('@adonisjs/session/session_provider'), + () => import('@adonisjs/shield/shield_provider'), + () => import('@adonisjs/static/static_provider'), + () => import('@adonisjs/cors/cors_provider'), + () => import('@adonisjs/vite/vite_provider'), + () => import('@adonisjs/inertia/inertia_provider'), + () => import('#providers/mongodb_provider'), + ], + + /* + |-------------------------------------------------------------------------- + | Preloads + |-------------------------------------------------------------------------- + | + | List of modules to import before starting the application. + | + */ + preloads: [ + () => import('#start/routes'), + () => import('#start/kernel'), + ], + + /* + |-------------------------------------------------------------------------- + | Tests + |-------------------------------------------------------------------------- + | + | List of test suites to organize tests by their type. Feel free to remove + | and add additional suites. + | + */ + tests: { + suites: [ + { + files: ['tests/unit/**/*.spec.{ts,js}'], + name: 'unit', + timeout: 2000, + }, + { + files: ['tests/functional/**/*.spec.{ts,js}'], + name: 'functional', + timeout: 30000, + }, + { + files: ['tests/integration/**/*.spec.{ts,js}'], + name: 'integration', + timeout: 60000, + }, + { + files: ['tests/property/**/*.spec.{ts,js}'], + name: 'property', + timeout: 30000, + }, + ], + forceExit: false, + }, + + /* + |-------------------------------------------------------------------------- + | Metafiles + |-------------------------------------------------------------------------- + | + | A collection of files you want to copy to the build folder when creating + | the production build. + | + */ + metaFiles: [ + { pattern: 'resources/views/**/*.edge', reloadServer: false }, + { pattern: 'public/**', reloadServer: false }, + { pattern: 'resources/xsd/**', reloadServer: false }, + ], + + hooks: { + init: [ + indexEntities({ transformers: { enabled: true } }), + generateRegistry(), + ], + buildStarting: [() => import('@adonisjs/vite/build_hook')], + }, +}) diff --git a/app/controllers/commands_controller.ts b/app/controllers/commands_controller.ts new file mode 100644 index 0000000..4c33bee --- /dev/null +++ b/app/controllers/commands_controller.ts @@ -0,0 +1,31 @@ +import type { HttpContext } from '@adonisjs/core/http' +import CommandService from '#services/command_service' +import { createCommandValidator } from '#validators/command' + +export default class CommandsController { + private commandService = new CommandService() + + async index({ response }: HttpContext) { + const commands = await this.commandService.getCommands() + return response.json(commands) + } + + async show({ params, response }: HttpContext) { + const commands = await this.commandService.getCommandsByDevice(params.id) + return response.json(commands) + } + + async store({ request, response }: HttpContext) { + const payload = await request.validateUsing(createCommandValidator) + + const command = await this.commandService.createCommand({ + deviceIdentifier: payload.deviceIdentifier, + action: payload.action as any, + parameters: payload.parameters as Record, + mode: payload.mode as any, + priority: payload.priority, + }) + + return response.status(201).json(command) + } +} diff --git a/app/controllers/configurations_v1_controller.ts b/app/controllers/configurations_v1_controller.ts new file mode 100644 index 0000000..0b98594 --- /dev/null +++ b/app/controllers/configurations_v1_controller.ts @@ -0,0 +1,279 @@ +import type { HttpContext } from '@adonisjs/core/http' +import ConfigurationService, { + type ConfigurationFilters, + type CreateConfigurationDto, + type UpdateConfigurationDto, +} from '#services/configuration_service' +import ConfigurationEngine from '#services/configuration_engine' +import logger from '@adonisjs/core/services/logger' + +/** + * Configurations API Controller + * + * Handles REST endpoints for Configuration management: + * - GET /api/v1/configurations (list with pagination and filters) + * - GET /api/v1/configurations/:id (get single with compliance stats) + * - POST /api/v1/configurations (create with validation and overlap detection) + * - PUT /api/v1/configurations/:id (update with validation and overlap detection) + * - DELETE /api/v1/configurations/:id (delete) + * - PATCH /api/v1/configurations/:id/toggle (toggle enabled status) + * - PATCH /api/v1/configurations/:id/tags (add/remove tags) + * - GET /api/v1/configurations/:id/history (version history) + * - POST /api/v1/configurations/:id/dry-run (dry-run evaluation) + */ +export default class ConfigurationsController { + private configService: ConfigurationService + private engine: ConfigurationEngine + + constructor() { + this.configService = new ConfigurationService() + this.engine = new ConfigurationEngine() + } + + /** + * GET /api/v1/configurations + * List configurations with pagination and filters + */ + async index({ request, response }: HttpContext) { + try { + const filters: ConfigurationFilters = { + page: Number(request.input('page', 1)), + perPage: Number(request.input('perPage', 20)), + tag: request.input('tag'), + enabled: request.input('enabled') !== undefined ? request.input('enabled') === 'true' : undefined, + productClass: request.input('productClass'), + } + + const result = await this.configService.findAll(filters) + return response.ok(result) + } catch (error: any) { + logger.error({ error: error.message }, '[ConfigAPI] List configurations failed') + return response.status(500).json({ message: 'Failed to list configurations' }) + } + } + + /** + * GET /api/v1/configurations/:id + * Get single configuration with compliance statistics + */ + async show({ params, response }: HttpContext) { + try { + const config = await this.configService.findById(params.id) + if (!config) { + return response.status(404).json({ message: 'Configuration not found' }) + } + + // TODO: Add compliance statistics from device compliance info + return response.ok(config) + } catch (error: any) { + logger.error({ error: error.message, id: params.id }, '[ConfigAPI] Get configuration failed') + return response.status(500).json({ message: 'Failed to retrieve configuration' }) + } + } + + /** + * POST /api/v1/configurations + * Create new configuration with validation and overlap detection + */ + async store({ request, response }: HttpContext) { + try { + const data = request.only([ + 'identifier', + 'name', + 'version', + 'priority', + 'policy', + 'enabled', + 'tags', + 'criteria', + 'inventory', + 'desiredParameters', + 'firmwareTarget', + 'schedule', + ]) as CreateConfigurationDto + + // Check for overlaps + const overlapResult = await this.configService.detectOverlap(data.criteria || {}, data.priority) + if (overlapResult && overlapResult.overlappingConfigurations && overlapResult.overlappingConfigurations.length > 0 && !request.input('ignoreOverlap')) { + return response.status(409).json({ + message: 'Configuration overlaps with existing ones', + overlaps: overlapResult.overlappingConfigurations, + }) + } + + // Create configuration + const config = await this.configService.create(data) + return response.status(201).json(config) + } catch (error: any) { + logger.error({ error: error.message }, '[ConfigAPI] Create configuration failed') + if (error.field) { + return response.status(422).json({ message: error.message, field: error.field }) + } + return response.status(500).json({ message: 'Failed to create configuration' }) + } + } + + /** + * PUT /api/v1/configurations/:id + * Update configuration with validation and overlap detection + */ + async update({ params, request, response }: HttpContext) { + try { + const data = request.only([ + 'name', + 'version', + 'priority', + 'policy', + 'enabled', + 'tags', + 'criteria', + 'inventory', + 'desiredParameters', + 'firmwareTarget', + 'schedule', + ]) as UpdateConfigurationDto + + // Check for overlaps if priority changed + if (data.priority) { + const overlapResult = await this.configService.detectOverlap( + data.criteria || {}, + data.priority, + params.id + ) + if (overlapResult && overlapResult.overlappingConfigurations && overlapResult.overlappingConfigurations.length > 0 && !request.input('ignoreOverlap')) { + return response.status(409).json({ + message: 'Configuration overlaps with existing ones', + overlaps: overlapResult.overlappingConfigurations, + }) + } + } + + // Update configuration + const config = await this.configService.update(params.id, data) + if (!config) { + return response.status(404).json({ message: 'Configuration not found' }) + } + + return response.ok(config) + } catch (error: any) { + logger.error({ error: error.message, id: params.id }, '[ConfigAPI] Update configuration failed') + if (error.field) { + return response.status(422).json({ message: error.message, field: error.field }) + } + return response.status(500).json({ message: 'Failed to update configuration' }) + } + } + + /** + * DELETE /api/v1/configurations/:id + * Delete configuration + */ + async destroy({ params, response }: HttpContext) { + try { + await this.configService.delete(params.id) + return response.status(204).send(null) + } catch (error: any) { + logger.error({ error: error.message, id: params.id }, '[ConfigAPI] Delete configuration failed') + return response.status(500).json({ message: 'Failed to delete configuration' }) + } + } + + /** + * PATCH /api/v1/configurations/:id/toggle + * Toggle enabled status + */ + async toggleEnabled({ params, request, response }: HttpContext) { + try { + const enabled = request.input('enabled') as boolean + const config = await this.configService.toggleEnabled(params.id, enabled) + if (!config) { + return response.status(404).json({ message: 'Configuration not found' }) + } + return response.ok(config) + } catch (error: any) { + logger.error({ error: error.message, id: params.id }, '[ConfigAPI] Toggle configuration failed') + return response.status(500).json({ message: 'Failed to toggle configuration' }) + } + } + + /** + * PATCH /api/v1/configurations/:id/tags + * Add or remove tags + */ + async manageTags({ params, request, response }: HttpContext) { + try { + const action = request.input('action') as 'add' | 'remove' + const tag = request.input('tag') as string + + if (!action || !['add', 'remove'].includes(action)) { + return response.status(422).json({ message: 'Invalid action: must be "add" or "remove"' }) + } + + if (!tag) { + return response.status(422).json({ message: 'Tag is required' }) + } + + const config = await this.configService.findById(params.id) + if (!config) { + return response.status(404).json({ message: 'Configuration not found' }) + } + + if (action === 'add') { + if (!config.tags.includes(tag)) { + config.tags.push(tag) + } + } else { + const index = config.tags.indexOf(tag) + if (index > -1) { + config.tags.splice(index, 1) + } + } + + const updated = await this.configService.update(params.id, { tags: config.tags }) + return response.ok(updated) + } catch (error: any) { + logger.error({ error: error.message, id: params.id }, '[ConfigAPI] Manage tags failed') + return response.status(500).json({ message: 'Failed to manage tags' }) + } + } + + /** + * GET /api/v1/configurations/:id/history + * Get configuration version history + */ + async history({ params, request, response }: HttpContext) { + try { + const page = Number(request.input('page', 1)) + const limit = Number(request.input('limit', 10)) + + const result = await this.configService.getHistory(params.id, page, limit) + return response.ok(result) + } catch (error: any) { + logger.error({ error: error.message, id: params.id }, '[ConfigAPI] Get history failed') + return response.status(500).json({ message: 'Failed to retrieve configuration history' }) + } + } + + /** + * POST /api/v1/configurations/:id/dry-run + * Perform dry-run evaluation for a device + */ + async dryRun({ params, request, response }: HttpContext) { + try { + const deviceIdentifier = request.input('deviceIdentifier') as string + + if (!deviceIdentifier) { + return response.status(422).json({ message: 'deviceIdentifier is required' }) + } + + const result = await this.engine.dryRun(params.id, deviceIdentifier) + return response.ok(result) + } catch (error: any) { + logger.error({ error: error.message, id: params.id }, '[ConfigAPI] Dry-run failed') + if (error.message.includes('not found')) { + return response.status(404).json({ message: error.message }) + } + return response.status(500).json({ message: 'Dry-run failed' }) + } + } +} diff --git a/app/controllers/cwmp_controller.ts b/app/controllers/cwmp_controller.ts new file mode 100644 index 0000000..6d324b1 --- /dev/null +++ b/app/controllers/cwmp_controller.ts @@ -0,0 +1,203 @@ +import type { HttpContext } from '@adonisjs/core/http' +import { buildSoapEnvelope } from '../protocol/soap.js' +import { CWMPMessageBuilder } from '../protocol/common/cwmp_message_builder.js' +import { StateMachineManager } from '../services/cwmp/state_machine_manager.js' +import { Device } from '../domain/device/device.js' +import { DeviceId } from '../domain/device/device_id.js' +import { DeviceParameter, DeviceProperty } from '../domain/device/device_parameter.js' +import DeviceModel from '../models/device.js' +import { getDeviceHashIdentifier } from '../utils/device_utils.js' +import logger from '@adonisjs/core/services/logger' + +export default class CwmpController { + private static managerInitialized = false + + private getManager(): StateMachineManager { + const manager = StateMachineManager.getInstance() + if (!CwmpController.managerInitialized) { + manager.startCleanupTimer(60000) + CwmpController.managerInitialized = true + } + return manager + } + + /** + * POST /cwmp10 + * + * Main CWMP/TR-069 endpoint. The cwmp_middleware has already: + * - Parsed the raw XML into a SOAP envelope + * - Extracted the CWMP message type and data + * - Validated content-type + * - Attached everything to request.cwmp + */ + async handle({ request, response }: HttpContext) { + const manager = this.getManager() + const { sessionId, remoteKey, envelope, message, isEmpty } = request.cwmp + + try { + // Empty body = CPE polling, continue the state machine + if (isEmpty) { + manager.ensureSessionForRemote(remoteKey, sessionId) + + if (manager.hasSession(sessionId)) { + const result = await manager.continue(sessionId) + if (result && result !== '') { + return this.sendSoap(response, result) + } + } + return response.status(204).send('') + } + + // Handle Inform + if (message?.type === 'Inform') { + const result = await this.handleInform(message.data, sessionId, envelope?.header?.id, remoteKey, manager) + return this.sendSoap(response, result) + } + + // Handle TransferComplete + if (message?.type === 'TransferComplete') { + const resp = buildSoapEnvelope(undefined, '') + return this.sendSoap(response, resp) + } + + // Other CWMP responses - route to state machine + manager.ensureSessionForRemote(remoteKey, sessionId) + + if (message && manager.hasSession(sessionId)) { + const { success, payload } = this.getMessageInfo(message) + const result = await manager.handleCWMPResponse(sessionId, success, payload) + if (result && result !== '') { + return this.sendSoap(response, result) + } + } + + return response.status(204).send('') + } catch (error: any) { + // Return SOAP Fault per TR-069 spec (always HTTP 200) + logger.error({ error: error?.message, stack: error?.stack }, 'CWMP error handling request') + const faultXml = this.buildSoapFault(error?.message || 'Internal error') + return this.sendSoap(response, faultXml) + } + } + + // ─── Inform Handling ─────────────────────────────────────────────────────── + + private async handleInform( + informData: any, + session: string, + cwmpId: string | undefined, + remoteKey: string, + manager: StateMachineManager + ): Promise { + logger.info({ informKeys: Object.keys(informData || {}), deviceId: informData?.DeviceId }, 'CWMP handleInform called') + const deviceIdStruct = informData?.DeviceId || {} + const deviceId = DeviceId.fromInform(deviceIdStruct) + + // Load or create device + let device = await this.loadDevice(deviceId) + if (!device) { + device = new Device(deviceId) + } + device.updateLastSeen() + + // Process inform parameter values + const paramValues = informData?.ParameterList?.ParameterValueStruct || [] + if (paramValues.length > 0) { + device.updateFromValues(paramValues) + } + device.setTmpParamValues(paramValues) + + // Persist device + const hashId = getDeviceHashIdentifier({ + manufacturer: deviceId.manufacturer, + oui: deviceId.oui, + productClass: deviceId.productClass, + serialNumber: deviceId.serialNumber, + }) + await DeviceModel.createOrUpdate({ + identifier: hashId, + deviceId, + properties: device.getProperties().map((p) => p.toSnapshot()), + parameters: device.getParameters().map((p) => p.toSnapshot()), + updatedAt: new Date(), + }) + + // Extract events + const events: string[] = Array.isArray(informData?.Event?.EventStruct) + ? informData.Event.EventStruct.map((e: any) => e?.EventCode).filter((c: any): c is string => !!c) + : [] + + logger.info({ events }, 'CWMP Inform events extracted') + + // Start state machine + await manager.startStateMachine(device, session, { events }) + manager.bindRemoteToDevice(remoteKey, device.deviceId) + + // After Inform for onboarding events, enqueue critical reads + const onlyConnReq = events.length > 0 && events.every((e) => e === '6 CONNECTION REQUEST') + if (!onlyConnReq) { + try { + const criticalPaths = [ + 'InternetGatewayDevice.DeviceInfo.SpecVersion', + 'InternetGatewayDevice.DeviceInfo.HardwareVersion', + 'InternetGatewayDevice.DeviceInfo.SoftwareVersion', + 'InternetGatewayDevice.ManagementServer.ConnectionRequestURL', + ] + await manager.enqueueImmediate(device, 'get_parameter_values', criticalPaths) + } catch (_e) { + // Non-critical + } + } + + return CWMPMessageBuilder.informResponse(cwmpId) + } + + // ─── Helpers ─────────────────────────────────────────────────────────────── + + private sendSoap(response: any, xml: string) { + response.header('Content-Type', 'text/xml; charset=utf-8') + response.header('SOAPAction', '""') + return response.send(xml) + } + + private getMessageInfo(message: { type: string; data: any }): { success: boolean; payload: any } { + const failTypes = ['Fault'] + return { + success: !failTypes.includes(message.type), + payload: message.data, + } + } + + private buildSoapFault(reason: string): string { + const body = ` + Server + ${reason} + + + 8002 + ${reason} + + + ` + return buildSoapEnvelope(undefined, body) + } + + private async loadDevice(deviceId: DeviceId): Promise { + try { + const record = await DeviceModel.findByDeviceId(deviceId) + if (!record) return null + + const device = new Device( + new DeviceId(record.deviceId.manufacturer, record.deviceId.oui, record.deviceId.productClass, record.deviceId.serialNumber), + record.createdAt + ) + + record.properties?.forEach((p: any) => device.setProperty(new DeviceProperty(p.name, p.value, p.type))) + record.parameters?.forEach((p: any) => device.setParameter(new DeviceParameter(p.name, p.value, p.type))) + + return device + } catch (_error) { + return null + } + } +} diff --git a/app/controllers/devices_controller.ts b/app/controllers/devices_controller.ts new file mode 100644 index 0000000..84a75e1 --- /dev/null +++ b/app/controllers/devices_controller.ts @@ -0,0 +1,94 @@ +import type { HttpContext } from '@adonisjs/core/http' +import DeviceService from '#services/device_service' +import { PaginationData } from '#services/device_service' +import { CommandType } from '../domain/commands/command.js' +import DeviceModel from '#models/device' + +export default class DevicesController { + private deviceService = new DeviceService() + + async criteriaValues({ response }: HttpContext) { + const values = await DeviceModel.distinctCriteriaValues() + return response.json(values) + } + + async index({ request, response }: HttpContext) { + const offset = Number(request.qs().offset) || 0 + const limit = Number(request.qs().limit) || 100 + + const { data, nbDocuments, pager } = await this.deviceService.getDevices(offset, limit) + + response.header('x-total-count', nbDocuments.toString()) + response.header('link', this.generateLinkHeader(request, pager)) + return response.json(data) + } + + async show({ params, response }: HttpContext) { + const device = await this.deviceService.getDeviceByIdentifier(params.identifier) + if (!device) { + return response.status(404).json({ message: 'Device not found' }) + } + return response.json(device) + } + + async destroy({ params, response }: HttpContext) { + const deleted = await this.deviceService.deleteByIdentifier(params.identifier) + if (!deleted) { + return response.status(404).json({ message: 'Device not found' }) + } + return response.json({ message: 'Device deleted successfully' }) + } + + async executeCommand({ params, request, response }: HttpContext) { + try { + const body = request.body() as { + action: CommandType + parameters?: Record + mode?: string + priority?: number + } + await this.deviceService.executeCommand(params.identifier, body) + return response.status(201).json({ message: 'Command created' }) + } catch (error: any) { + if (error.message?.includes('not found')) { + return response.status(404).json({ message: error.message }) + } + if (error.message?.includes('Duplicate')) { + return response.status(409).json({ message: error.message }) + } + return response.status(400).json({ message: error.message }) + } + } + + async refresh({ params, response }: HttpContext) { + try { + await this.deviceService.refreshDevice(params.identifier) + return response.status(200).json({ message: 'Refresh command sent' }) + } catch (error: any) { + if (error.message?.includes('not found')) { + return response.status(404).json({ message: error.message }) + } + return response.status(500).json({ message: error.message }) + } + } + + private generateLinkHeader(request: any, pager: PaginationData): string { + const baseUrl = `${request.protocol()}://${request.host()}${request.url().split('?')[0]}` + const parts: string[] = [] + + if (pager.first) { + parts.push(`<${baseUrl}?offset=${pager.first.offset}&limit=${pager.first.limit}>; rel="first"`) + } + if (pager.previous) { + parts.push(`<${baseUrl}?offset=${pager.previous.offset}&limit=${pager.previous.limit}>; rel="prev"`) + } + if (pager.next) { + parts.push(`<${baseUrl}?offset=${pager.next.offset}&limit=${pager.next.limit}>; rel="next"`) + } + if (pager.last) { + parts.push(`<${baseUrl}?offset=${pager.last.offset}&limit=${pager.last.limit}>; rel="last"`) + } + + return parts.join(',') + } +} diff --git a/app/controllers/firmware_catalog_controller.ts b/app/controllers/firmware_catalog_controller.ts new file mode 100644 index 0000000..79009a9 --- /dev/null +++ b/app/controllers/firmware_catalog_controller.ts @@ -0,0 +1,192 @@ +import type { HttpContext } from '@adonisjs/core/http' +import FirmwareCatalogService, { + type CreateFirmwareEntryDto, + type CreateMigrationRouteDto, +} from '#services/firmware_catalog_service' +import logger from '@adonisjs/core/services/logger' + +/** + * Firmware Catalog API Controller + * + * Handles REST endpoints for Firmware Catalog management: + * - GET /api/v1/firmware-catalog (list entries grouped by productClass) + * - POST /api/v1/firmware-catalog/entries (create firmware entry) + * - DELETE /api/v1/firmware-catalog/entries/:id (delete firmware entry) + * - POST /api/v1/firmware-catalog/routes (create migration route) + * - PUT /api/v1/firmware-catalog/routes/:id (update migration route) + * - DELETE /api/v1/firmware-catalog/routes/:id (delete migration route) + */ +export default class FirmwareCatalogController { + private catalogService: FirmwareCatalogService + + constructor() { + this.catalogService = new FirmwareCatalogService() + } + + /** + * GET /api/v1/firmware-catalog/entries + * List firmware entries with optional search + */ + async listEntries({ request, response }: HttpContext) { + try { + const productClass = request.input('productClass') + const entries = await this.catalogService.findEntries(productClass || undefined) + return response.ok({ data: entries, total: entries.length }) + } catch (error: any) { + logger.error({ error: error.message }, '[FirmwareAPI] List entries failed') + return response.status(500).json({ message: 'Failed to list firmware entries' }) + } + } + + /** + * GET /api/v1/firmware-catalog/entries/:id + * Get a single firmware entry by identifier + */ + async showEntry({ params, response }: HttpContext) { + try { + const entries = await this.catalogService.findEntries() + const entry = entries.find((e) => e.identifier === params.id) + if (!entry) return response.status(404).json({ message: 'Firmware entry not found' }) + return response.ok({ data: entry }) + } catch (error: any) { + logger.error({ error: error.message }, '[FirmwareAPI] Show entry failed') + return response.status(500).json({ message: 'Failed to load firmware entry' }) + } + } + + /** + * GET /api/v1/firmware-catalog/routes + * List all migration routes + */ + async listRoutes({ response }: HttpContext) { + try { + const routes = await this.catalogService.findAllRoutes() + return response.ok({ data: routes, total: routes.length }) + } catch (error: any) { + logger.error({ error: error.message }, '[FirmwareAPI] List routes failed') + return response.status(500).json({ message: 'Failed to list migration routes' }) + } + } + + /** + * PUT /api/v1/firmware-catalog/entries/:id + * Update a firmware entry + */ + async updateEntry({ params, request, response }: HttpContext) { + try { + const data = request.only([ + 'productClass', 'version', 'downloadUrl', 'checksum', 'checksumType', 'fileSize', + ]) + const entry = await this.catalogService.updateEntry(params.id, data) + return response.ok(entry) + } catch (error: any) { + logger.error({ error: error.message }, '[FirmwareAPI] Update entry failed') + return response.status(400).json({ message: error.message }) + } + } + + /** + * GET /api/v1/firmware-catalog + * List all firmware entries, optionally filtered by productClass + */ + async index({ request, response }: HttpContext) { + try { + const productClass = request.input('productClass') + const entries = await this.catalogService.findEntries(productClass) + return response.ok({ entries }) + } catch (error: any) { + logger.error({ error: error.message }, '[FirmwareAPI] List entries failed') + return response.status(500).json({ message: 'Failed to list firmware entries' }) + } + } + + /** + * POST /api/v1/firmware-catalog/entries + * Create new firmware entry + */ + async createEntry({ request, response }: HttpContext) { + try { + const data = request.only([ + 'identifier', + 'productClass', + 'version', + 'downloadUrl', + 'checksum', + 'checksumType', + 'fileSize', + ]) as CreateFirmwareEntryDto + + const entry = await this.catalogService.createEntry(data) + return response.status(201).json(entry) + } catch (error: any) { + logger.error({ error: error.message }, '[FirmwareAPI] Create entry failed') + return response.status(400).json({ message: error.message }) + } + } + + /** + * DELETE /api/v1/firmware-catalog/entries/:id + * Delete firmware entry + */ + async deleteEntry({ params, response }: HttpContext) { + try { + await this.catalogService.deleteEntry(params.id) + return response.status(204).send(null) + } catch (error: any) { + logger.error({ error: error.message, id: params.id }, '[FirmwareAPI] Delete entry failed') + return response.status(500).json({ message: 'Failed to delete firmware entry' }) + } + } + + /** + * POST /api/v1/firmware-catalog/routes + * Create new migration route + */ + async createRoute({ request, response }: HttpContext) { + try { + const data = request.only([ + 'identifier', + 'productClass', + 'sourceVersionPattern', + 'targetVersion', + 'downloadUrl', + 'priority', + ]) as CreateMigrationRouteDto + + const route = await this.catalogService.createMigrationRoute(data) + return response.status(201).json(route) + } catch (error: any) { + logger.error({ error: error.message }, '[FirmwareAPI] Create route failed') + return response.status(400).json({ message: error.message }) + } + } + + /** + * PUT /api/v1/firmware-catalog/routes/:id + * Update migration route + */ + async updateRoute({ params: _params, request: _request, response }: HttpContext) { + try { + // TODO: Implement update method in FirmwareCatalogService + // For now, return not implemented + return response.status(501).json({ message: 'Not implemented' }) + } catch (error: any) { + logger.error({ error: error.message, id: _params.id }, '[FirmwareAPI] Update route failed') + return response.status(500).json({ message: 'Failed to update migration route' }) + } + } + + /** + * DELETE /api/v1/firmware-catalog/routes/:id + * Delete migration route + */ + async deleteRoute({ params, response }: HttpContext) { + try { + await this.catalogService.deleteMigrationRoute(params.id) + return response.status(204).send(null) + } catch (error: any) { + logger.error({ error: error.message, id: params.id }, '[FirmwareAPI] Delete route failed') + return response.status(500).json({ message: 'Failed to delete migration route' }) + } + } +} diff --git a/app/controllers/health_controller.ts b/app/controllers/health_controller.ts new file mode 100644 index 0000000..d995ea3 --- /dev/null +++ b/app/controllers/health_controller.ts @@ -0,0 +1,18 @@ +import type { HttpContext } from '@adonisjs/core/http' +import { getMongoDb } from '#providers/mongodb_provider' + +export default class HealthController { + async checklife({ response }: HttpContext) { + return response.json({ status: 'OK' }) + } + + async checkservice({ response }: HttpContext) { + try { + const db = getMongoDb() + await db.command({ ping: 1 }) + return response.json({ status: 'OK', version: '1.0.0' }) + } catch { + return response.status(503).json({ status: 'KO', version: '1.0.0' }) + } + } +} diff --git a/app/controllers/pages_controller.ts b/app/controllers/pages_controller.ts new file mode 100644 index 0000000..5bb61cd --- /dev/null +++ b/app/controllers/pages_controller.ts @@ -0,0 +1,132 @@ +import type { HttpContext } from '@adonisjs/core/http' +import DeviceService from '#services/device_service' +import CommandService from '#services/command_service' + +export default class PagesController { + private deviceService = new DeviceService() + private commandService = new CommandService() + + async home({ inertia }: HttpContext) { + const { data } = await this.deviceService.getDevices(0, 200) + return (inertia as any).render('Home', { devices: data, message: '' }) + } + + async deviceDetail({ params, inertia, response }: HttpContext) { + const device = await this.deviceService.getDeviceByIdentifier(params.identifier) + if (!device) { + return response.status(404).json({ message: 'Device not found' }) + } + + // Get pending commands (max 5) + last processed command + const allCommands = await this.commandService.getCommandsByDevice(params.identifier) + const pending = allCommands + .filter((c: any) => ['PENDING', 'WAITING', 'SENT'].includes(c.status)) + .slice(0, 5) + const lastProcessed = allCommands + .filter((c: any) => ['COMPLETED', 'FAILED'].includes(c.status)) + .sort((a: any, b: any) => new Date(b.updatedAt || b.createdAt).getTime() - new Date(a.updatedAt || a.createdAt).getTime()) + [0] + const commands = lastProcessed ? [...pending, lastProcessed] : pending + + return (inertia as any).render('DeviceDetail', { device, commands }) + } + + async commands({ inertia, request }: HttpContext) { + const deviceFilter = request.qs().device || '' + const statusFilter = request.qs().status || '' + const actionFilter = request.qs().action || '' + const modeFilter = request.qs().mode || '' + + let commands: any[] = await this.commandService.getCommands() + + if (deviceFilter) { + commands = commands.filter((c: any) => c.deviceIdentifier === deviceFilter) + } + if (statusFilter) { + commands = commands.filter((c: any) => c.status === statusFilter) + } + if (actionFilter) { + commands = commands.filter((c: any) => c.action === actionFilter) + } + if (modeFilter) { + commands = commands.filter((c: any) => c.mode === modeFilter) + } + + return (inertia as any).render('Commands', { + commands, + filters: { device: deviceFilter, status: statusFilter, action: actionFilter, mode: modeFilter }, + }) + } + + async commandsFleet({ inertia, session }: HttpContext) { + const devices = (session as any).get('fleetDevices', []) + const commands = await this.commandService.getCommands() + return (inertia as any).render('CommandsFleet', { devices, commands }) + } + + async storeFleetDevices({ request, response, session }: HttpContext) { + const { devices } = request.body() + ;(session as any).put('fleetDevices', devices || []) + return response.redirect().toPath('/commands/fleet') + } + + async refreshDevice({ params, response }: HttpContext) { + // Create a REFRESH command and send Connection Request to wake the CPE + await this.deviceService.refreshDevice(params.identifier) + return response.redirect().back() + } + + async getModel({ params, request }: HttpContext) { + const { modelPath } = request.body() + await this.deviceService.executeCommand(params.identifier, { + action: 'GET_PARAMETER_NAMES' as any, + parameters: { parameterPath: modelPath, nextLevel: true }, + mode: 'I', + }) + } + + async storeFleetCommand({ request, response, session }: HttpContext) { + const { action, parameters, mode, priority, devices } = request.body() + const deviceList = devices || session.get('fleetDevices', []) + + for (const deviceIdentifier of deviceList) { + await this.commandService.createCommand({ + deviceIdentifier, + action, + parameters: parameters || {}, + mode, + priority, + }) + } + + return response.redirect().toPath('/commands/fleet') + } + + async configurationsList({ inertia }: HttpContext) { + return (inertia as any).render('configurations/ConfigurationsList') + } + + async configurationCreate({ inertia }: HttpContext) { + return (inertia as any).render('configurations/ConfigurationCreate') + } + + async configurationEdit({ params, inertia }: HttpContext) { + return (inertia as any).render('configurations/ConfigurationEdit', { id: params.id }) + } + + async configurationDetail({ params, inertia }: HttpContext) { + return (inertia as any).render('configurations/ConfigurationDetail', { id: params.id }) + } + + async firmwareCatalog({ inertia }: HttpContext) { + return (inertia as any).render('firmware_catalog/FirmwareCatalog') + } + + async firmwareCatalogCreate({ inertia }: HttpContext) { + return (inertia as any).render('firmware_catalog/FirmwareCatalogCreate') + } + + async firmwareCatalogEdit({ params, inertia }: HttpContext) { + return (inertia as any).render('firmware_catalog/FirmwareCatalogEdit', { id: params.id }) + } +} diff --git a/app/domain/commands/command.ts b/app/domain/commands/command.ts new file mode 100644 index 0000000..c31174c --- /dev/null +++ b/app/domain/commands/command.ts @@ -0,0 +1,145 @@ +import { DeviceId } from '../device/device_id.js' +import { ModeType } from '../shared/mode.js' +import { ObjectId } from 'mongodb' + +export type CommandId = string + +export enum CommandType { + SET_PARAMETER_VALUES = 'SET_PARAMETER_VALUES', + GET_PARAMETER_VALUES = 'GET_PARAMETER_VALUES', + GET_PARAMETER_NAMES = 'GET_PARAMETER_NAMES', + SET_PARAMETER_ATTRIBUTES = 'SET_PARAMETER_ATTRIBUTES', + GET_PARAMETER_ATTRIBUTES = 'GET_PARAMETER_ATTRIBUTES', + DOWNLOAD = 'DOWNLOAD', + UPLOAD = 'UPLOAD', + REBOOT = 'REBOOT', + FACTORY_RESET = 'FACTORY_RESET', + SCHEDULE_INFORM = 'SCHEDULE_INFORM', + REFRESH = 'REFRESH', +} + +export enum CommandStatus { + PENDING = 'PENDING', + SENT = 'SENT', + WAITING = 'WAITING', + FAILED = 'FAILED', + CANCELLED = 'CANCELLED', + COMPLETED = 'COMPLETED', +} + +export enum Priority { + LOW = 0, + NORMAL = 1, + HIGH = 2, + CRITICAL = 3, +} + +export type CommandParameters = Record + +export interface CommandResult { + success: boolean + data?: any + message?: string + timestamp: Date +} + +export interface CommandRecord { + _id?: ObjectId + deviceIdentifier: string + mode: ModeType + action: CommandType + parameters: Record + status: CommandStatus + createdAt: Date + updatedAt: Date + executedAt?: Date + completedAt?: Date + priority: number +} + +export class Command { + private _status: CommandStatus + private _executedAt: Date | null = null + private _result: CommandResult | null = null + private _error: string | null = null + + constructor( + public readonly id: CommandId, + public readonly deviceId: DeviceId, + public readonly type: CommandType, + public readonly parameters: CommandParameters = {}, + public readonly priority: Priority = Priority.NORMAL, + public readonly createdAt: Date = new Date(), + status: CommandStatus = CommandStatus.PENDING + ) { + this._status = status + } + + public get status(): CommandStatus { + return this._status + } + + public get executedAt(): Date | null { + return this._executedAt + } + + public get result(): CommandResult | null { + return this._result + } + + public get error(): string | null { + return this._error + } + + public markAsSent(): void { + if (this._status !== CommandStatus.PENDING) { + throw new Error(`Cannot mark command as sent. Current status: ${this._status}`) + } + this._status = CommandStatus.SENT + this._executedAt = new Date() + } + + public markAsWaiting(): void { + if (this._status !== CommandStatus.SENT) { + throw new Error(`Cannot mark command as waiting. Current status: ${this._status}`) + } + this._status = CommandStatus.WAITING + } + + public complete(result: CommandResult): void { + if (![CommandStatus.SENT, CommandStatus.WAITING].includes(this._status)) { + throw new Error(`Cannot complete command. Current status: ${this._status}`) + } + this._status = CommandStatus.COMPLETED + this._result = result + this._executedAt = new Date() + } + + public fail(error: string): void { + if (this._status === CommandStatus.COMPLETED) { + throw new Error('Cannot fail a successful command') + } + this._status = CommandStatus.FAILED + this._error = error + this._executedAt = new Date() + } + + public cancel(): void { + if ([CommandStatus.COMPLETED, CommandStatus.FAILED].includes(this._status)) { + throw new Error(`Cannot cancel command with status: ${this._status}`) + } + this._status = CommandStatus.CANCELLED + } + + public canExecute(): boolean { + return this._status === CommandStatus.PENDING + } + + public isCompleted(): boolean { + return [CommandStatus.COMPLETED, CommandStatus.FAILED, CommandStatus.CANCELLED].includes(this._status) + } + + public hasExpired(timeoutMinutes: number = 30): boolean { + return (Date.now() - this.createdAt.getTime()) / (1000 * 60) > timeoutMinutes + } +} diff --git a/app/domain/configuration/compliance.ts b/app/domain/configuration/compliance.ts new file mode 100644 index 0000000..33d3ca2 --- /dev/null +++ b/app/domain/configuration/compliance.ts @@ -0,0 +1,12 @@ +export enum ComplianceStatus { + COMPLIANT = 'compliant', + IN_PROGRESS = 'in_progress', + FAILED = 'failed', +} + +export interface DeviceComplianceInfo { + status: ComplianceStatus + configurationIdentifier: string + lastEvaluatedAt: Date + lastError?: string +} diff --git a/app/domain/configuration/configuration.ts b/app/domain/configuration/configuration.ts new file mode 100644 index 0000000..c1a1dd5 --- /dev/null +++ b/app/domain/configuration/configuration.ts @@ -0,0 +1,58 @@ +export enum PolicyType { + ONCE = 'once', + MAINTAIN = 'maintain', +} + +export interface ParameterCriteria { + name: string // TR-069 path, e.g. "InternetGatewayDevice.Time.Enable" + value: string | number | boolean // expected value (compared as string) +} + +export interface ConfigurationCriteria { + manufacturer?: string // exact or glob pattern + oui?: string // exact or glob pattern + productClass?: string // exact or glob pattern + serialNumber?: string // exact or glob pattern + parameterCriteria?: ParameterCriteria[] // match on known device parameter values +} + +export interface InventoryPath { + path: string // TR-069 path, e.g. "InternetGatewayDevice.DeviceInfo." or full param name +} + +export interface DesiredParameter { + name: string // Full TR-069 path + value: string | number | boolean // Target value + type: ParameterValueType // 'string' | 'int' | 'unsignedInt' | 'boolean' | 'dateTime' +} + +export type ParameterValueType = 'string' | 'int' | 'unsignedInt' | 'boolean' | 'dateTime' + +export interface ScheduleWindow { + daysOfWeek: number[] // 0=Sunday, 1=Monday, ..., 6=Saturday (at least one required) + startHour: number // 0-23 + endHour: number // 0-23, must differ from startHour; if endHour < startHour, spans midnight + timezone: string // IANA timezone, e.g. "Europe/Paris" +} + +export interface FirmwareTarget { + productClass: string + targetVersion: string +} + +export interface Configuration { + identifier: string + name: string + version: string // semver, e.g. "1.0.0" + priority: number // positive integer, higher wins + policy: PolicyType + enabled: boolean + tags: string[] + criteria: ConfigurationCriteria + inventory: InventoryPath[] + desiredParameters: DesiredParameter[] + firmwareTarget?: FirmwareTarget + schedule?: ScheduleWindow + createdAt: Date + updatedAt: Date +} diff --git a/app/domain/configuration/configuration_version.ts b/app/domain/configuration/configuration_version.ts new file mode 100644 index 0000000..9a50a9e --- /dev/null +++ b/app/domain/configuration/configuration_version.ts @@ -0,0 +1,8 @@ +import { Configuration } from './configuration.js' + +export interface ConfigurationVersion { + configurationIdentifier: string + versionNumber: number + snapshot: Configuration // Full Configuration state at that point + modifiedAt: Date +} diff --git a/app/domain/device/device.ts b/app/domain/device/device.ts new file mode 100644 index 0000000..dd3cf95 --- /dev/null +++ b/app/domain/device/device.ts @@ -0,0 +1,249 @@ +import { DeviceId } from './device_id.js' +import { DeviceParameter, DeviceProperty, ParameterSnapshot, PropertySnapshot } from './device_parameter.js' +import { ParameterType } from '../shared/types.js' +import { createHash } from 'node:crypto' + +export enum OnboardingStatus { + PENDING = 'PENDING', + DISCOVERING = 'DISCOVERING', + PROVISIONING = 'PROVISIONING', + COMPLETED = 'COMPLETED', + FAILED = 'FAILED', +} + +export class Device { + public tmpParamValues: any[] = [] + public tmpPC: any = null + private _parameters: Map = new Map() + private _properties: Map = new Map() + private _session: any = null + private _lastSeen: Date = new Date() + private _onboardingStatus: OnboardingStatus = OnboardingStatus.PENDING + + constructor( + public readonly deviceId: DeviceId, + public readonly createdAt: Date = new Date() + ) {} + + public get session(): any { + return this._session + } + + public get lastSeen(): Date { + return this._lastSeen + } + + public get onboardingStatus(): OnboardingStatus { + return this._onboardingStatus + } + + public get identifier(): string { + const raw = `${this.deviceId.manufacturer}-${this.deviceId.oui}-${this.deviceId.productClass}-${this.deviceId.serialNumber}` + return createHash('sha256').update(raw).digest('hex').substring(0, 24) + } + + public attachSession(session: any): void { + this._session = session + this._lastSeen = new Date() + } + + public detachSession(): void { + if (this._session) { + this._session = null + } + } + + public isOnline(): boolean { + return this._session !== null + } + + public updateOnboardingStatus(status: OnboardingStatus): void { + this._onboardingStatus = status + this._lastSeen = new Date() + } + + public setParameter(parameter: DeviceParameter): void { + this._parameters.set(parameter.name, parameter) + } + + public getParameter(name: string): DeviceParameter | null { + return this._parameters.get(name) || null + } + + public getParameters(): DeviceParameter[] { + return Array.from(this._parameters.values()) + } + + public updateParameter(name: string, value: any): void { + const parameter = this._parameters.get(name) + if (parameter) { + parameter.updateValue(value) + } + } + + public setProperty(property: DeviceProperty): void { + this._properties.set(property.name, property) + } + + public getProperty(name: string): DeviceProperty | null { + return this._properties.get(name) || null + } + + public getProperties(): DeviceProperty[] { + return Array.from(this._properties.values()) + } + + public updateProperty(name: string, value: any): void { + const property = this._properties.get(name) + if (property) { + property.updateValue(value) + } + } + + public getConnectionRequestURL(): URL | null { + for (const property of this._properties.values()) { + const url = property.getConnectionRequestURL() + if (url) return url + } + return null + } + + public canExecuteCommands(): boolean { + return this.isOnline() && this._onboardingStatus === OnboardingStatus.COMPLETED + } + + public setTmpParamValues(values: any[]): void { + this.tmpParamValues = values + } + + public getTmpParamValues(): any[] { + return this.tmpParamValues + } + + public setTmpPC(value: any): void { + this.tmpPC = value + } + + public getTmpPC(): any { + return this.tmpPC + } + + public clearTmpValues(): void { + this.tmpParamValues = [] + this.tmpPC = null + } + + public organizeParameters(response: any, timestamp: Date): void { + const infoList = response?.ParameterList?.ParameterInfoStruct || [] + for (const info of infoList) { + const name = info?.Name + if (!name) continue + // Skip object paths (ending with dot) - only store leaf parameters + if (name.endsWith('.')) continue + const isWritable = !!info?.Writable + + if (this._parameters.has(name) || this._properties.has(name)) { + if (isWritable && this._properties.has(name)) { + const prop = this._properties.get(name)! + this._properties.delete(name) + this.setParameter(new DeviceParameter(name, prop.value, prop.type)) + } else if (!isWritable && this._parameters.has(name)) { + const param = this._parameters.get(name)! + this._parameters.delete(name) + this.setProperty(new DeviceProperty(name, param.value, param.type)) + } + continue + } + + if (isWritable) { + this.setParameter(new DeviceParameter(name, null, ParameterType.UNKNOWN)) + } else { + this.setProperty(new DeviceProperty(name, null, ParameterType.UNKNOWN)) + } + } + this._lastSeen = timestamp + } + + public updateFromValues(paramValues: any[]): void { + for (const pv of paramValues || []) { + const name = pv?.Name + const valueObj = pv?.Value + const hasTextNode = valueObj && Object.prototype.hasOwnProperty.call(valueObj as any, '_') + const hasTypeAttr = valueObj && (valueObj['$xsi:type'] || valueObj?.$?.Type) + const val = hasTextNode ? valueObj._ : (hasTypeAttr ? '' : undefined) + const typeStr = valueObj?.['$xsi:type'] || valueObj?.$?.Type + const mappedType = this.mapType(typeStr) + + if (!name) continue + if (val === undefined && !hasTypeAttr) continue + + if (this._parameters.has(name)) { + const existing = this._parameters.get(name)! + if (mappedType && existing.type === ParameterType.UNKNOWN) { + const snap = existing.toSnapshot() + this._parameters.set( + name, + new DeviceParameter(name, snap.value, mappedType, snap.createdAt, snap.updatedAt, snap.notification, snap.accessList) + ) + } + this.updateParameter(name, val) + } else if (this._properties.has(name)) { + const existing = this._properties.get(name)! + if (mappedType && existing.type === ParameterType.UNKNOWN) { + const snap = existing.toSnapshot() + this._properties.set( + name, + new DeviceProperty(name, snap.value, mappedType, snap.createdAt, snap.updatedAt, snap.notification, snap.accessList) + ) + } + this.updateProperty(name, val) + } else { + this.setProperty(new DeviceProperty(name, val ?? null, mappedType || ParameterType.UNKNOWN)) + } + } + this._lastSeen = new Date() + } + + private mapType(typeStr: string | undefined): ParameterType | undefined { + switch (typeStr) { + case 'xsd:string': + return ParameterType.STRING + case 'xsd:int': + return ParameterType.INT + case 'xsd:unsignedInt': + return ParameterType.UNSIGNED_INT + case 'xsd:boolean': + return ParameterType.BOOLEAN + default: + return undefined + } + } + + public toSnapshot(): DeviceSnapshot { + return { + deviceId: this.deviceId, + identifier: this.identifier, + createdAt: this.createdAt, + lastSeen: this._lastSeen, + onboardingStatus: this._onboardingStatus, + isOnline: this.isOnline(), + parameters: this.getParameters().map((p) => p.toSnapshot()), + properties: this.getProperties().map((p) => p.toSnapshot()), + } + } + + public updateLastSeen(): void { + this._lastSeen = new Date() + } +} + +export interface DeviceSnapshot { + deviceId: DeviceId + identifier: string + createdAt: Date + lastSeen: Date + onboardingStatus: OnboardingStatus + isOnline: boolean + parameters: ParameterSnapshot[] + properties: PropertySnapshot[] +} diff --git a/app/domain/device/device_id.ts b/app/domain/device/device_id.ts new file mode 100644 index 0000000..45c8e8a --- /dev/null +++ b/app/domain/device/device_id.ts @@ -0,0 +1,45 @@ +export class DeviceId { + constructor( + public readonly manufacturer: string, + public readonly oui: string, + public readonly productClass: string, + public readonly serialNumber: string + ) { + this.validate() + } + + private validate(): void { + if (!this.manufacturer?.trim()) { + throw new Error('Manufacturer is required') + } + if (!this.serialNumber?.trim()) { + throw new Error('Serial number is required') + } + } + + public toIdentifier(): string { + return `${this.manufacturer}-${this.oui}-${this.productClass}-${this.serialNumber}` + } + + public equals(other: DeviceId): boolean { + return ( + this.manufacturer === other.manufacturer && + this.oui === other.oui && + this.productClass === other.productClass && + this.serialNumber === other.serialNumber + ) + } + + public static fromInform(informData: any): DeviceId { + return new DeviceId( + String(informData?.Manufacturer ?? ''), + String(informData?.OUI ?? ''), + String(informData?.ProductClass ?? ''), + String(informData?.SerialNumber ?? '') + ) + } + + public toString(): string { + return this.toIdentifier() + } +} diff --git a/app/domain/device/device_parameter.ts b/app/domain/device/device_parameter.ts new file mode 100644 index 0000000..8987502 --- /dev/null +++ b/app/domain/device/device_parameter.ts @@ -0,0 +1,110 @@ +import { ParameterType } from '../shared/types.js' + +export interface ParameterSnapshot { + name: string + value: any + type: ParameterType + createdAt?: Date + updatedAt?: Date + notification?: number + accessList?: string[] +} + +export interface PropertySnapshot { + name: string + value: any + type: ParameterType + createdAt?: Date + updatedAt?: Date + notification?: number + accessList?: string[] +} + +export class DeviceParameter { + constructor( + public readonly name: string, + private _value: any, + public type: ParameterType = ParameterType.UNKNOWN, + public createdAt: Date = new Date(), + public updatedAt: Date = new Date(), + public notification?: number, + public accessList?: string[] + ) {} + + get value(): any { + return this._value + } + + updateValue(value: any): void { + this._value = value + this.updatedAt = new Date() + } + + updateAttributes(notification?: number, accessList?: string[]): void { + if (notification !== undefined) this.notification = notification + if (accessList !== undefined) this.accessList = accessList + this.updatedAt = new Date() + } + + toSnapshot(): ParameterSnapshot { + return { + name: this.name, + value: this._value, + type: this.type, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + notification: this.notification, + accessList: this.accessList, + } + } +} + +export class DeviceProperty { + constructor( + public readonly name: string, + private _value: any, + public type: ParameterType = ParameterType.UNKNOWN, + public createdAt: Date = new Date(), + public updatedAt: Date = new Date(), + public notification?: number, + public accessList?: string[] + ) {} + + get value(): any { + return this._value + } + + updateValue(value: any): void { + this._value = value + this.updatedAt = new Date() + } + + updateAttributes(notification?: number, accessList?: string[]): void { + if (notification !== undefined) this.notification = notification + if (accessList !== undefined) this.accessList = accessList + this.updatedAt = new Date() + } + + getConnectionRequestURL(): URL | null { + if (this.name.includes('ConnectionRequestURL') && this._value) { + try { + return new URL(this._value) + } catch { + return null + } + } + return null + } + + toSnapshot(): PropertySnapshot { + return { + name: this.name, + value: this._value, + type: this.type, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + notification: this.notification, + accessList: this.accessList, + } + } +} diff --git a/app/domain/device/device_record.ts b/app/domain/device/device_record.ts new file mode 100644 index 0000000..a5041d4 --- /dev/null +++ b/app/domain/device/device_record.ts @@ -0,0 +1,14 @@ +import { DeviceId } from './device_id.js' +import { PropertySnapshot, ParameterSnapshot } from './device_parameter.js' +import { DeviceComplianceInfo } from '../configuration/compliance.js' + +export interface DeviceRecord { + identifier?: string + deviceId: DeviceId + properties: PropertySnapshot[] + parameters: ParameterSnapshot[] + compliance?: DeviceComplianceInfo + provisioningCode?: string + updatedAt?: Date + createdAt?: Date +} diff --git a/app/domain/firmware/firmware_catalog.ts b/app/domain/firmware/firmware_catalog.ts new file mode 100644 index 0000000..8f82d21 --- /dev/null +++ b/app/domain/firmware/firmware_catalog.ts @@ -0,0 +1,25 @@ +export interface FirmwareEntry { + identifier: string + productClass: string + version: string + downloadUrl: string + checksum: string + checksumType: 'md5' | 'sha256' + fileSize: number + createdAt: Date +} + +export interface MigrationRoute { + identifier: string + productClass: string + sourceVersionPattern: string + targetVersion: string + downloadUrl: string + priority: number + createdAt: Date +} + +export interface FirmwareCatalog { + entries: FirmwareEntry[] + migrationRoutes: MigrationRoute[] +} diff --git a/app/domain/firmware/migration_step.ts b/app/domain/firmware/migration_step.ts new file mode 100644 index 0000000..776f003 --- /dev/null +++ b/app/domain/firmware/migration_step.ts @@ -0,0 +1,7 @@ +export interface MigrationStep { + fromVersion: string + toVersion: string + downloadUrl: string + fileSize: number + checksum: string +} diff --git a/app/domain/shared/mode.ts b/app/domain/shared/mode.ts new file mode 100644 index 0000000..6e31407 --- /dev/null +++ b/app/domain/shared/mode.ts @@ -0,0 +1,16 @@ +export enum ModeType { + NI = 'NI', + I = 'I', +} + +export function isModeType(value: any): value is ModeType { + return value === ModeType.NI || value === ModeType.I +} + +export function parseMode(value?: string): ModeType | undefined { + if (!value) return undefined + const upper = value.toUpperCase() + if (upper === ModeType.NI) return ModeType.NI + if (upper === ModeType.I) return ModeType.I + return undefined +} diff --git a/app/domain/shared/types.ts b/app/domain/shared/types.ts new file mode 100644 index 0000000..ca4532c --- /dev/null +++ b/app/domain/shared/types.ts @@ -0,0 +1,7 @@ +export enum ParameterType { + STRING = 'xsd:string', + UNSIGNED_INT = 'xsd:unsignedInt', + INT = 'xsd:int', + BOOLEAN = 'xsd:boolean', + UNKNOWN = 'unknown', +} diff --git a/app/exceptions/handler.ts b/app/exceptions/handler.ts new file mode 100644 index 0000000..ba2fc34 --- /dev/null +++ b/app/exceptions/handler.ts @@ -0,0 +1,14 @@ +import app from '@adonisjs/core/services/app' +import { HttpContext, ExceptionHandler } from '@adonisjs/core/http' + +export default class HttpExceptionHandler extends ExceptionHandler { + protected debug = !app.inProduction + + async handle(error: unknown, ctx: HttpContext) { + return super.handle(error, ctx) + } + + async report(error: unknown, ctx: HttpContext) { + return super.report(error, ctx) + } +} diff --git a/app/exceptions/index.ts b/app/exceptions/index.ts new file mode 100644 index 0000000..7e4a148 --- /dev/null +++ b/app/exceptions/index.ts @@ -0,0 +1,21 @@ +import { Exception } from '@adonisjs/core/exceptions' + +export class NotFoundException extends Exception { + static status = 404 + static code = 'E_NOT_FOUND' +} + +export class BadRequestException extends Exception { + static status = 400 + static code = 'E_BAD_REQUEST' +} + +export class UnprocessableEntityException extends Exception { + static status = 422 + static code = 'E_UNPROCESSABLE' +} + +export class DatabaseException extends Exception { + static status = 503 + static code = 'E_DATABASE_ERROR' +} diff --git a/app/middleware/container_bindings_middleware.ts b/app/middleware/container_bindings_middleware.ts new file mode 100644 index 0000000..8f5322b --- /dev/null +++ b/app/middleware/container_bindings_middleware.ts @@ -0,0 +1,19 @@ +import { Logger } from '@adonisjs/core/logger' +import { HttpContext } from '@adonisjs/core/http' +import { type NextFn } from '@adonisjs/core/types/http' + +/** + * The container bindings middleware binds classes to their request + * specific value using the container resolver. + * + * - We bind "HttpContext" class to the "ctx" object + * - And bind "Logger" class to the "ctx.logger" object + */ +export default class ContainerBindingsMiddleware { + handle(ctx: HttpContext, next: NextFn) { + ctx.containerResolver.bindValue(HttpContext, ctx) + ctx.containerResolver.bindValue(Logger, ctx.logger) + + return next() + } +} diff --git a/app/middleware/cwmp_logger_middleware.ts b/app/middleware/cwmp_logger_middleware.ts new file mode 100644 index 0000000..4c0a289 --- /dev/null +++ b/app/middleware/cwmp_logger_middleware.ts @@ -0,0 +1,62 @@ +import type { HttpContext } from '@adonisjs/core/http' +import type { NextFn } from '@adonisjs/core/types/http' +// @ts-ignore - xml-formatter types conflict with ESM interop +import formatXml from 'xml-formatter' + +/** + * CWMP Logger Middleware + * + * Middleware dédié à l'observation des messages TR-069 XML. + * Trace les échanges CPE↔ACS (requêtes entrantes et réponses sortantes) + * avec le XML pretty-printed directement sur stdout pour lisibilité humaine. + * + * Doit être placé avant le cwmp_middleware dans la chaîne. + */ +export default class CwmpLoggerMiddleware { + private prettyXml(xml: string): string { + try { + return (formatXml as any)(xml, { indentation: ' ', collapseContent: true, lineSeparator: '\n' }) + } catch { + return xml + } + } + + private log(direction: string, sessionId: string, xml: string) { + const timestamp = new Date().toISOString() + const separator = '─'.repeat(80) + process.stdout.write( + `\n${separator}\n` + + `[TR-069] ${direction} | ${timestamp} | ${sessionId}\n` + + `${separator}\n` + + `${xml}\n` + + `${separator}\n\n` + ) + } + + async handle({ request, response }: HttpContext, next: NextFn) { + const rawBody = request.raw() || '' + + const socket = request.request?.socket + const remoteIp = request.ip() + const remotePort = socket?.remotePort || 0 + const localAddress = socket?.localAddress || 'unknown' + const localPort = socket?.localPort || 0 + const sessionId = `${remoteIp}:${remotePort}->${localAddress}:${localPort}` + + // Log incoming TR-069 message + if (rawBody && rawBody.trim() !== '') { + this.log('CPE→ACS', sessionId, this.prettyXml(rawBody)) + } + + // Intercept response to log outgoing XML + const originalSend = response.send.bind(response) + response.send = (body: any) => { + if (typeof body === 'string' && body.includes('soap-env:Envelope')) { + this.log('ACS→CPE', sessionId, this.prettyXml(body)) + } + return originalSend(body) + } + + return next() + } +} diff --git a/app/middleware/cwmp_middleware.ts b/app/middleware/cwmp_middleware.ts new file mode 100644 index 0000000..9d8c8f0 --- /dev/null +++ b/app/middleware/cwmp_middleware.ts @@ -0,0 +1,88 @@ +import type { HttpContext } from '@adonisjs/core/http' +import type { NextFn } from '@adonisjs/core/types/http' +import { parseSoapXml, extractCwmpMessage } from '../protocol/soap.js' + +/** + * CWMP Middleware + * + * Handles the specifics of TR-069/CWMP requests: + * - Parses raw XML body into a SOAP envelope + * - Extracts the CWMP message from the envelope + * - Attaches parsed data to the HTTP context for the controller + * - Validates content-type (must be text/xml or empty for polling) + */ +export default class CwmpMiddleware { + async handle({ request, response, logger }: HttpContext, next: NextFn) { + const contentType = request.header('content-type') || '' + const rawBody = request.raw() || '' + + // Derive session identifiers from the TCP socket + const socket = request.request?.socket + const remoteIp = request.ip() + const remotePort = socket?.remotePort || 0 + const localAddress = socket?.localAddress || 'unknown' + const localPort = socket?.localPort || 0 + + const sessionId = `${remoteIp}:${remotePort}->${localAddress}:${localPort}` + const remoteKey = `${remoteIp}:${remotePort}` + + // Attach CWMP context to the request + request.cwmp = { + sessionId, + remoteKey, + rawBody, + envelope: null, + message: null, + isEmpty: !rawBody || rawBody.trim() === '', + } + + // Empty body = CPE polling for next instruction (valid in TR-069) + if (request.cwmp.isEmpty) { + return next() + } + + // Validate content type for non-empty requests + const isXml = contentType.includes('text/xml') || + contentType.includes('application/xml') || + contentType.includes('application/soap+xml') || + contentType === '' // Some CPEs don't set content-type + + if (!isXml && rawBody.trim() !== '') { + logger.warn({ sessionId, contentType }, 'CWMP invalid content-type') + response.status(415).send('Unsupported Media Type: expected text/xml') + return + } + + // Parse SOAP XML + try { + const envelope = parseSoapXml(rawBody) + request.cwmp.envelope = envelope + + // Extract CWMP message from envelope body + const message = extractCwmpMessage(envelope.body) + request.cwmp.message = message + } catch (error: any) { + logger.error({ sessionId, error: error.message }, 'CWMP XML parse error') + response.status(400).send('Malformed SOAP/XML') + return + } + + return next() + } +} + +/** + * Extend the AdonisJS Request interface to include CWMP context. + */ +declare module '@adonisjs/core/http' { + interface HttpRequest { + cwmp: { + sessionId: string + remoteKey: string + rawBody: string + envelope: any | null + message: { type: string; data: any } | null + isEmpty: boolean + } + } +} diff --git a/app/middleware/inertia_middleware.ts b/app/middleware/inertia_middleware.ts new file mode 100644 index 0000000..2a4454f --- /dev/null +++ b/app/middleware/inertia_middleware.ts @@ -0,0 +1,17 @@ +import BaseInertiaMiddleware from '@adonisjs/inertia/inertia_middleware' +import type { HttpContext } from '@adonisjs/core/http' +import type { NextFn } from '@adonisjs/core/types/http' + +export default class InertiaMiddleware extends BaseInertiaMiddleware { + async share(ctx: HttpContext) { + return { + errors: ctx.session?.flashMessages.get('errors'), + } + } + + async handle(ctx: HttpContext, next: NextFn) { + await this.init(ctx) + await next() + this.dispose(ctx) + } +} diff --git a/app/models/command.ts b/app/models/command.ts new file mode 100644 index 0000000..b558008 --- /dev/null +++ b/app/models/command.ts @@ -0,0 +1,39 @@ +import { BaseModel } from '#database/base_model' +import { CommandRecord, CommandStatus } from '../domain/commands/command.js' +import { ModeType } from '../domain/shared/mode.js' +import { ObjectId } from 'mongodb' + +export default class CommandModel extends BaseModel { + static collectionName = 'commands' + + static async findByDevice(deviceIdentifier: string): Promise { + const collection = this.getCollection() + return collection.find({ deviceIdentifier }).sort({ createdAt: -1 }).toArray() as unknown as Promise + } + + static async findPending(deviceIdentifier: string, mode?: ModeType): Promise { + const filter: any = { deviceIdentifier, status: CommandStatus.PENDING } + if (mode) filter.mode = mode + const collection = this.getCollection() + return collection.find(filter).sort({ priority: 1 }).toArray() as unknown as Promise + } + + static async createCommand(command: Omit): Promise { + const collection = this.getCollection() + const doc = { ...command, createdAt: new Date(), updatedAt: new Date() } + const result = await collection.insertOne(doc) + return { ...doc, _id: result.insertedId } as CommandRecord + } + + static async updateCommand(id: ObjectId, update: Partial): Promise { + const collection = this.getCollection() + await collection.updateOne({ _id: id }, { $set: { ...update, updatedAt: new Date() } }) + } + + static async deleteById(id: string | ObjectId): Promise { + const collection = this.getCollection() + const objectId = typeof id === 'string' ? new ObjectId(id) : id + const result = await collection.deleteOne({ _id: objectId }) + return result.deletedCount === 1 + } +} diff --git a/app/models/configuration.ts b/app/models/configuration.ts new file mode 100644 index 0000000..bd4eba4 --- /dev/null +++ b/app/models/configuration.ts @@ -0,0 +1,37 @@ +import { BaseModel } from '#database/base_model' +import { Configuration } from '../domain/configuration/configuration.js' +import { Filter, Sort } from 'mongodb' + +export default class ConfigurationModel extends BaseModel { + static collectionName = 'configurations' + + static async findByIdentifier(identifier: string): Promise { + return this.findOne({ identifier }) as Promise + } + + static async findAllEnabled(sort: Sort = { priority: -1 }): Promise { + const collection = this.getCollection() + return collection.find({ enabled: true }).sort(sort).toArray() as unknown as Promise + } + + static async paginateConfigurations( + page: number, + perPage: number, + filter: Filter = {}, + sort: Sort = { priority: -1 } + ): Promise<{ data: Configuration[]; total: number; page: number; perPage: number; lastPage: number }> { + return this.paginate(page, perPage, filter, sort) + } + + static async createIfNotExists(data: Partial): Promise { + const existing = await this.findOne({ identifier: data.identifier }) + if (existing) { + throw new Error(`Configuration with identifier '${data.identifier}' already exists`) + } + return this.create(data) as Promise + } + + static async deleteByIdentifier(identifier: string): Promise { + return this.deleteOne({ identifier }) + } +} diff --git a/app/models/configuration_version.ts b/app/models/configuration_version.ts new file mode 100644 index 0000000..2d9fd2d --- /dev/null +++ b/app/models/configuration_version.ts @@ -0,0 +1,40 @@ +import { BaseModel } from '#database/base_model' +import { ConfigurationVersion } from '../domain/configuration/configuration_version.js' +import { Sort } from 'mongodb' + +export default class ConfigurationVersionModel extends BaseModel { + static collectionName = 'configuration_versions' + + static async findByConfigurationIdentifier( + configurationIdentifier: string, + sort: Sort = { versionNumber: -1 } + ): Promise { + const collection = this.getCollection() + return collection + .find({ configurationIdentifier }) + .sort(sort) + .toArray() as unknown as Promise + } + + static async paginateVersions( + configurationIdentifier: string, + page: number, + perPage: number + ): Promise<{ data: ConfigurationVersion[]; total: number; page: number; perPage: number; lastPage: number }> { + return this.paginate(page, perPage, { configurationIdentifier }, { versionNumber: -1 }) + } + + static async getLatestVersionNumber(configurationIdentifier: string): Promise { + const collection = this.getCollection() + const latest = await collection + .find({ configurationIdentifier }) + .sort({ versionNumber: -1 }) + .limit(1) + .toArray() + return latest.length > 0 ? (latest[0] as any).versionNumber : 0 + } + + static async deleteByConfigurationIdentifier(configurationIdentifier: string): Promise { + return this.deleteMany({ configurationIdentifier }) + } +} diff --git a/app/models/device.ts b/app/models/device.ts new file mode 100644 index 0000000..f8159b4 --- /dev/null +++ b/app/models/device.ts @@ -0,0 +1,131 @@ +import { BaseModel } from '#database/base_model' +import { DeviceRecord } from '../domain/device/device_record.js' +import { DeviceId } from '../domain/device/device_id.js' +import { DeviceComplianceInfo } from '../domain/configuration/compliance.js' +import { Filter, Sort } from 'mongodb' + +export default class DeviceModel extends BaseModel { + static collectionName = 'devices' + + static async findByIdentifier(identifier: string): Promise { + return this.findOne({ identifier }) + } + + static async findByDeviceId(deviceId: DeviceId): Promise { + return this.findOne({ + 'deviceId.manufacturer': deviceId.manufacturer, + 'deviceId.oui': deviceId.oui, + 'deviceId.productClass': deviceId.productClass, + 'deviceId.serialNumber': deviceId.serialNumber, + }) + } + + static async paginateDevices( + offset: number, + limit: number, + filter: Filter = {} + ): Promise<{ data: DeviceRecord[]; total: number }> { + const collection = this.getCollection() + const total = await collection.countDocuments(filter) + const data = (await collection + .find(filter, { projection: { _id: 0 } }) + .skip(offset) + .limit(limit) + .sort({ updatedAt: -1 } as Sort) + .toArray()) as unknown as DeviceRecord[] + return { data, total } + } + + static async createOrUpdate(device: Partial): Promise { + const collection = this.getCollection() + let filter: Filter = {} + + if (device.deviceId) { + filter = { deviceId: device.deviceId } + } else if (device.identifier) { + filter = { identifier: device.identifier } + } else { + throw new Error('Either identifier or deviceId must be provided') + } + + const result = await collection.findOneAndUpdate( + filter, + { + $set: { ...device, updatedAt: new Date() }, + $setOnInsert: { createdAt: new Date() }, + }, + { upsert: true, returnDocument: 'after', projection: { _id: 0 } } + ) + + return result as unknown as DeviceRecord + } + + static async updateCompliance( + identifier: string, + compliance: DeviceComplianceInfo + ): Promise { + await this.updateOne({ identifier }, { compliance }) + } + + static async updateProvisioningCode( + identifier: string, + provisioningCode: string + ): Promise { + await this.updateOne({ identifier }, { provisioningCode }) + } + + static async findByComplianceStatus(status: string): Promise { + const collection = this.getCollection() + return collection + .find({ 'compliance.status': status }) + .toArray() as unknown as Promise + } + + static async findByConfigurationIdentifier(configurationIdentifier: string): Promise { + const collection = this.getCollection() + return collection + .find({ 'compliance.configurationIdentifier': configurationIdentifier }) + .toArray() as unknown as Promise + } + + static async distinctCriteriaValues(): Promise<{ + manufacturer: string[] + oui: string[] + productClass: string[] + serialNumber: string[] + }> { + const collection = this.getCollection() + const [manufacturer, oui, productClass, serialNumber] = await Promise.all([ + collection.distinct('deviceId.manufacturer'), + collection.distinct('deviceId.oui'), + collection.distinct('deviceId.productClass'), + collection.distinct('deviceId.serialNumber'), + ]) + return { + manufacturer: (manufacturer as string[]).filter(Boolean).sort(), + oui: (oui as string[]).filter(Boolean).sort(), + productClass: (productClass as string[]).filter(Boolean).sort(), + serialNumber: (serialNumber as string[]).filter(Boolean).sort(), + } + } + + static async countByComplianceStatus( + configurationIdentifier: string + ): Promise<{ compliant: number; in_progress: number; failed: number }> { + const collection = this.getCollection() + const results = await collection + .aggregate([ + { $match: { 'compliance.configurationIdentifier': configurationIdentifier } }, + { $group: { _id: '$compliance.status', count: { $sum: 1 } } }, + ]) + .toArray() + + const counts = { compliant: 0, in_progress: 0, failed: 0 } + for (const r of results) { + if (r._id === 'compliant') counts.compliant = r.count + else if (r._id === 'in_progress') counts.in_progress = r.count + else if (r._id === 'failed') counts.failed = r.count + } + return counts + } +} diff --git a/app/models/firmware_entry.ts b/app/models/firmware_entry.ts new file mode 100644 index 0000000..9307b10 --- /dev/null +++ b/app/models/firmware_entry.ts @@ -0,0 +1,34 @@ +import { BaseModel } from '#database/base_model' +import { FirmwareEntry } from '../domain/firmware/firmware_catalog.js' + +export default class FirmwareEntryModel extends BaseModel { + static collectionName = 'firmware_entries' + + static async findByIdentifier(identifier: string): Promise { + return this.findOne({ identifier }) as Promise + } + + static async findByProductClass(productClass: string): Promise { + const collection = this.getCollection() + return collection.find({ productClass }).toArray() as unknown as Promise + } + + static async findByProductClassAndVersion( + productClass: string, + version: string + ): Promise { + return this.findOne({ productClass, version }) as Promise + } + + static async createIfNotExists(data: Partial): Promise { + const existing = await this.findOne({ identifier: data.identifier }) + if (existing) { + throw new Error(`Firmware entry with identifier '${data.identifier}' already exists`) + } + return this.create(data) as Promise + } + + static async deleteByIdentifier(identifier: string): Promise { + return this.deleteOne({ identifier }) + } +} diff --git a/app/models/migration_route.ts b/app/models/migration_route.ts new file mode 100644 index 0000000..6c26387 --- /dev/null +++ b/app/models/migration_route.ts @@ -0,0 +1,31 @@ +import { BaseModel } from '#database/base_model' +import { MigrationRoute } from '../domain/firmware/firmware_catalog.js' +import { Sort } from 'mongodb' + +export default class MigrationRouteModel extends BaseModel { + static collectionName = 'migration_routes' + + static async findByIdentifier(identifier: string): Promise { + return this.findOne({ identifier }) as Promise + } + + static async findByProductClass( + productClass: string, + sort: Sort = { priority: -1 } + ): Promise { + const collection = this.getCollection() + return collection.find({ productClass }).sort(sort).toArray() as unknown as Promise + } + + static async createIfNotExists(data: Partial): Promise { + const existing = await this.findOne({ identifier: data.identifier }) + if (existing) { + throw new Error(`Migration route with identifier '${data.identifier}' already exists`) + } + return this.create(data) as Promise + } + + static async deleteByIdentifier(identifier: string): Promise { + return this.deleteOne({ identifier }) + } +} diff --git a/app/protocol/common/cwmp_message_builder.ts b/app/protocol/common/cwmp_message_builder.ts new file mode 100644 index 0000000..277c2a1 --- /dev/null +++ b/app/protocol/common/cwmp_message_builder.ts @@ -0,0 +1,150 @@ +import { buildSoapEnvelope } from '../soap.js' + +export class CWMPMessageBuilder { + static informResponse(cwmpId?: string): string { + const body = ` + 1 + ` + return buildSoapEnvelope(cwmpId, body) + } + + static getParameterNames(cwmpId: string | undefined, parameterPath: string, nextLevel: boolean = true): string { + const body = ` + ${parameterPath} + ${nextLevel ? '1' : '0'} + ` + return buildSoapEnvelope(cwmpId, body) + } + + static getParameterValues(cwmpId: string | undefined, parameterNames: string[]): string { + const names = parameterNames.map((n) => `${n}`).join('\n ') + const body = ` + + ${names} + + ` + return buildSoapEnvelope(cwmpId, body) + } + + static setParameterValues(cwmpId: string | undefined, parameters: { name: string; value: string; type: string }[]): string { + const params = parameters + .map( + (p) => ` + ${p.name} + ${p.value} + ` + ) + .join('\n ') + const body = ` + + ${params} + + papaye-v2 + ` + return buildSoapEnvelope(cwmpId, body) + } + + static download( + cwmpId: string | undefined, + params: { + commandKey?: string + fileType?: string + url: string + username?: string + password?: string + fileSize?: number + targetFileName?: string + delaySeconds?: number + successUrl?: string + failureUrl?: string + } + ): string { + const body = ` + ${params.commandKey || ''} + ${params.fileType || '1 Firmware Upgrade Image'} + ${params.url} + ${params.username || ''} + ${params.password || ''} + ${params.fileSize || 0} + ${params.targetFileName || ''} + ${params.delaySeconds || 0} + ${params.successUrl || ''} + ${params.failureUrl || ''} + ` + return buildSoapEnvelope(cwmpId, body) + } + + static upload( + cwmpId: string | undefined, + params: { commandKey?: string; fileType?: string; url: string; username?: string; password?: string; delaySeconds?: number } + ): string { + const body = ` + ${params.commandKey || ''} + ${params.fileType || '3 Vendor Configuration File'} + ${params.url} + ${params.username || ''} + ${params.password || ''} + ${params.delaySeconds || 0} + ` + return buildSoapEnvelope(cwmpId, body) + } + + static reboot(cwmpId?: string, commandKey?: string): string { + const body = ` + ${commandKey || ''} + ` + return buildSoapEnvelope(cwmpId, body) + } + + static factoryReset(cwmpId?: string): string { + const body = `` + return buildSoapEnvelope(cwmpId, body) + } + + static scheduleInform(cwmpId: string | undefined, delaySeconds: number, commandKey?: string): string { + const body = ` + ${delaySeconds} + ${commandKey || ''} + ` + return buildSoapEnvelope(cwmpId, body) + } + + static getParameterAttributes(cwmpId: string | undefined, parameterNames: string[]): string { + const names = parameterNames.map((n) => `${n}`).join('\n ') + const body = ` + + ${names} + + ` + return buildSoapEnvelope(cwmpId, body) + } + + static setParameterAttributes( + cwmpId: string | undefined, + attributes: { name: string; notificationChange: boolean; notification: number; accessListChange: boolean; accessList: string[] }[] + ): string { + const attrs = attributes + .map( + (a) => ` + ${a.name} + ${a.notificationChange ? '1' : '0'} + ${a.notification} + ${a.accessListChange ? '1' : '0'} + + ${a.accessList.map((s) => `${s}`).join('')} + + ` + ) + .join('') + const body = ` + + ${attrs} + + ` + return buildSoapEnvelope(cwmpId, body) + } + + static emptyResponse(): string { + return '' + } +} diff --git a/app/protocol/soap.ts b/app/protocol/soap.ts new file mode 100644 index 0000000..ed27cdb --- /dev/null +++ b/app/protocol/soap.ts @@ -0,0 +1,97 @@ +import { XMLParser } from 'fast-xml-parser' + +export const NAMESPACES = { + 'soap-env': 'http://schemas.xmlsoap.org/soap/envelope/', + 'soap-enc': 'http://schemas.xmlsoap.org/soap/encoding/', + xsd: 'http://www.w3.org/2001/XMLSchema', + xsi: 'http://www.w3.org/2001/XMLSchema-instance', + cwmp: 'urn:dslforum-org:cwmp-1-0', +} + +export const CWMP_VERSIONS: Record = { + 'urn:dslforum-org:cwmp-1-0': '1.0', + 'urn:dslforum-org:cwmp-1-1': '1.1', + 'urn:dslforum-org:cwmp-1-2': '1.2', + 'urn:dslforum-org:cwmp-1-3': '1.3', + 'urn:dslforum-org:cwmp-1-4': '1.4', +} + +const xmlParser = new XMLParser({ + ignoreAttributes: false, + attributeNamePrefix: '$', + textNodeName: '_', + numberParseOptions: { leadingZeros: false, hex: false }, + isArray: (name) => { + const arrayTags = ['ParameterValueStruct', 'ParameterInfoStruct', 'EventStruct', 'string'] + return arrayTags.includes(name) + }, +}) + +export interface SoapEnvelope { + header: SoapHeader + body: any + cwmpVersion: string +} + +export interface SoapHeader { + id?: string + holdRequests?: boolean + noMoreRequests?: boolean +} + +export interface CwmpMessage { + type: string + data: any +} + +export function parseSoapXml(xml: string): SoapEnvelope { + const parsed = xmlParser.parse(xml) + const envelope = parsed['soap-env:Envelope'] || parsed['SOAP-ENV:Envelope'] || parsed['soapenv:Envelope'] || {} + const header = envelope['soap-env:Header'] || envelope['SOAP-ENV:Header'] || envelope['soapenv:Header'] || {} + const body = envelope['soap-env:Body'] || envelope['SOAP-ENV:Body'] || envelope['soapenv:Body'] || {} + + // Detect CWMP version from namespace + let cwmpVersion = '1.0' + for (const [, value] of Object.entries(envelope)) { + if (typeof value === 'string' && value.startsWith('urn:dslforum-org:cwmp-')) { + cwmpVersion = CWMP_VERSIONS[value] || '1.0' + break + } + } + + const soapHeader: SoapHeader = {} + const idNode = header['cwmp:ID'] || header['cwmp:Id'] + if (idNode) { + soapHeader.id = typeof idNode === 'string' ? idNode : idNode._ || idNode + } + + return { header: soapHeader, body, cwmpVersion } +} + +export function extractCwmpMessage(body: any): CwmpMessage | null { + for (const key of Object.keys(body)) { + if (key.startsWith('cwmp:')) { + const type = key.replace('cwmp:', '') + return { type, data: body[key] } + } + } + return null +} + +export function buildSoapEnvelope(cwmpId: string | undefined, bodyContent: string): string { + const id = cwmpId || Math.random().toString(36).substring(2, 10) + return ` + + + ${id} + + + ${bodyContent} + +` +} diff --git a/app/services/command_service.ts b/app/services/command_service.ts new file mode 100644 index 0000000..38a2a4e --- /dev/null +++ b/app/services/command_service.ts @@ -0,0 +1,58 @@ +import CommandModel from '#models/command' +import { CommandRecord, CommandStatus, CommandType } from '../domain/commands/command.js' +import { ModeType } from '../domain/shared/mode.js' +import { Priority } from '../domain/commands/command.js' + +export default class CommandService { + async getCommands() { + const collection = CommandModel.getCollection() + return collection.find({}).sort({ createdAt: -1 }).toArray() + } + + async getCommandsByDevice(deviceIdentifier: string) { + return CommandModel.findByDevice(deviceIdentifier) + } + + async createCommand(data: { + deviceIdentifier: string + action: CommandType + parameters: Record + mode?: ModeType + priority?: number + }): Promise { + return CommandModel.createCommand({ + deviceIdentifier: data.deviceIdentifier, + action: data.action, + parameters: data.parameters, + mode: data.mode || ModeType.NI, + status: CommandStatus.PENDING, + priority: data.priority ?? Priority.NORMAL, + createdAt: new Date(), + updatedAt: new Date(), + }) + } + + async getICommandsForDeviceWithStatus(deviceIdentifier: string, status: CommandStatus): Promise { + const collection = CommandModel.getCollection() + return collection + .find({ deviceIdentifier, mode: ModeType.I, status }) + .sort({ priority: 1 }) + .toArray() as unknown as Promise + } + + async getNICommandsForDeviceWithStatus(deviceIdentifier: string, status: CommandStatus): Promise { + const collection = CommandModel.getCollection() + return collection + .find({ deviceIdentifier, mode: ModeType.NI, status }) + .sort({ priority: 1 }) + .toArray() as unknown as Promise + } + + async updateCommandStatus(id: any, update: Partial): Promise { + await CommandModel.updateCommand(id, update) + } + + async deleteCommand(id: string): Promise { + return CommandModel.deleteById(id) + } +} diff --git a/app/services/configuration_engine.ts b/app/services/configuration_engine.ts new file mode 100644 index 0000000..7b3958b --- /dev/null +++ b/app/services/configuration_engine.ts @@ -0,0 +1,408 @@ +import ConfigurationModel from '#models/configuration' +import DeviceModel from '#models/device' +import FirmwareCatalogService from './firmware_catalog_service.js' +import type { Configuration, ParameterCriteria, ParameterValueType } from '../domain/configuration/configuration.js' +import type { Device } from '../domain/device/device.js' +import type { MigrationStep } from '../domain/firmware/migration_step.js' +import { isWithinSchedule } from '../utils/schedule_window.js' +import { matchGlob } from '../utils/glob_match.js' +import { computeContentHash, parseProvisioningCode } from '../utils/provisioning_code.js' + +/** + * Result of a single device configuration evaluation. + * Contains all the actions to be executed by the state machine. + */ +export interface EngineResult { + configuration: Configuration | null + inventoryPaths: string[] + parameterDiffs: ParameterDiff[] + firmwareMigrationPath: MigrationStep[] + isAlreadyCompliant: boolean + skipFirmware: boolean // true if outside schedule window +} + +/** + * A single parameter difference (desired vs current). + */ +export interface ParameterDiff { + name: string + currentValue: string | number | boolean | null + targetValue: string | number | boolean + type: ParameterValueType +} + +/** + * Result of a dry-run evaluation. + */ +export interface DryRunResult { + configurationId: string + deviceIdentifier: string + evaluation: EngineResult + timestamp: Date +} + +/** + * ConfigurationEngine evaluates device compliance against configurations. + * + * Responsibilities: + * - Find matching enabled configurations for a device + * - Select the highest priority configuration + * - Compute parameter differences (desired vs current) + * - Handle policy "once" via ProvisioningCode comparison + * - Resolve firmware migration paths + * - Check schedule windows for firmware operations + */ +export default class ConfigurationEngine { + private firmwareService: FirmwareCatalogService + + constructor() { + this.firmwareService = new FirmwareCatalogService() + } + + /** + * Evaluate device compliance against all matching configurations. + * + * Steps: + * 1. Load all enabled configurations + * 2. Filter by criteria (glob matching against DeviceId) + * 3. Select highest priority configuration + * 4. Check if already compliant via ProvisioningCode (policy "once") + * 5. Compute parameter differences + * 6. Resolve firmware migration path + * 7. Check schedule window for firmware operations + */ + async evaluate(device: Device): Promise { + // 1. Load all enabled configurations + const allConfigs = await ConfigurationModel.find({ enabled: true }) + + // 2. Filter by criteria (glob matching against DeviceId) + const matchingConfigs = allConfigs.filter((config) => this.matchesCriteria(device, config)) + + // 3. Select highest priority configuration + let selectedConfig: Configuration | null = null + if (matchingConfigs.length > 0) { + selectedConfig = matchingConfigs.reduce((prev, curr) => + curr.priority > prev.priority ? curr : prev + ) + } + + // If no matching configuration, device is already compliant (nothing to do) + if (!selectedConfig) { + return { + configuration: null, + inventoryPaths: [], + parameterDiffs: [], + firmwareMigrationPath: [], + isAlreadyCompliant: true, + skipFirmware: false, + } + } + + // 4. Check if already compliant via ProvisioningCode (policy "once") + if (!this.shouldApplyOnce(device, selectedConfig)) { + return { + configuration: selectedConfig, + inventoryPaths: [], + parameterDiffs: [], + firmwareMigrationPath: [], + isAlreadyCompliant: true, + skipFirmware: false, + } + } + + // 5. Compute parameter differences + const parameterDiffs = await this.computeParameterDiffs(device, selectedConfig) + + // 6. Resolve firmware migration path + let firmwareMigrationPath: MigrationStep[] = [] + let skipFirmware = false + + if (selectedConfig.firmwareTarget) { + // Check schedule window for firmware operations + if (!isWithinSchedule(selectedConfig.schedule)) { + skipFirmware = true + } else { + // Get current device version (from properties or parameter) + const currentVersion = this.getDeviceVersion(device, selectedConfig.firmwareTarget.productClass) + + if (currentVersion && currentVersion !== selectedConfig.firmwareTarget.targetVersion) { + firmwareMigrationPath = await this.firmwareService.resolveUpgradePath( + selectedConfig.firmwareTarget.productClass, + currentVersion, + selectedConfig.firmwareTarget.targetVersion + ) + } + } + } + + // Check if compliant (no diffs and no firmware needed) + const isCompliant = + parameterDiffs.length === 0 && + firmwareMigrationPath.length === 0 + + return { + configuration: selectedConfig, + inventoryPaths: selectedConfig.inventory.map((inv) => inv.path), + parameterDiffs, + firmwareMigrationPath, + isAlreadyCompliant: isCompliant, + skipFirmware, + } + } + + /** + * Dry-run evaluation for a specific configuration on a device. + * Similar to evaluate() but can be called explicitly by the user. + */ + async dryRun(configId: string, deviceIdentifier: string): Promise { + const configuration = await ConfigurationModel.findOne({ identifier: configId }) + if (!configuration) { + throw new Error(`Configuration ${configId} not found`) + } + + const device = await DeviceModel.findOne({ + 'deviceId.id': deviceIdentifier, + }) + if (!device) { + throw new Error(`Device ${deviceIdentifier} not found`) + } + + // Check if configuration matches device criteria + if (!this.matchesCriteria(device, configuration)) { + throw new Error(`Configuration ${configId} does not match device ${deviceIdentifier}`) + } + + const evaluation = await this.evaluateSpecific(device, configuration) + + return { + configurationId: configId, + deviceIdentifier, + evaluation, + timestamp: new Date(), + } + } + + /** + * Evaluate a specific configuration against a device. + * Used by dry-run and potentially other flows. + */ + private async evaluateSpecific(device: Device, config: Configuration): Promise { + // Check policy "once" + if (!this.shouldApplyOnce(device, config)) { + return { + configuration: config, + inventoryPaths: [], + parameterDiffs: [], + firmwareMigrationPath: [], + isAlreadyCompliant: true, + skipFirmware: false, + } + } + + // Compute parameter differences + const parameterDiffs = await this.computeParameterDiffs(device, config) + + // Resolve firmware migration path + let firmwareMigrationPath: MigrationStep[] = [] + let skipFirmware = false + + if (config.firmwareTarget) { + if (!isWithinSchedule(config.schedule)) { + skipFirmware = true + } else { + const currentVersion = this.getDeviceVersion(device, config.firmwareTarget.productClass) + if (currentVersion && currentVersion !== config.firmwareTarget.targetVersion) { + firmwareMigrationPath = await this.firmwareService.resolveUpgradePath( + config.firmwareTarget.productClass, + currentVersion, + config.firmwareTarget.targetVersion + ) + } + } + } + + const isCompliant = parameterDiffs.length === 0 && firmwareMigrationPath.length === 0 + + return { + configuration: config, + inventoryPaths: config.inventory.map((inv) => inv.path), + parameterDiffs, + firmwareMigrationPath, + isAlreadyCompliant: isCompliant, + skipFirmware, + } + } + + /** + * Check if a device matches all criteria of a configuration. + * Uses glob matching for each criterion. + */ + private matchesCriteria(device: Device, config: Configuration): boolean { + const criteria = config.criteria + + // manufacturer + if (criteria.manufacturer) { + const deviceValue = device.deviceId.manufacturer || '' + if (!matchGlob(criteria.manufacturer, deviceValue)) { + return false + } + } + + // oui + if (criteria.oui) { + const deviceValue = device.deviceId.oui || '' + if (!matchGlob(criteria.oui, deviceValue)) { + return false + } + } + + // productClass + if (criteria.productClass) { + const deviceValue = device.deviceId.productClass || '' + if (!matchGlob(criteria.productClass, deviceValue)) { + return false + } + } + + // serialNumber + if (criteria.serialNumber) { + const deviceValue = device.deviceId.serialNumber || '' + if (!matchGlob(criteria.serialNumber, deviceValue)) { + return false + } + } + + // parameterCriteria: all conditions must match a known device parameter value + if (Array.isArray(criteria.parameterCriteria) && criteria.parameterCriteria.length > 0) { + const allParams = [...(device as any).getParameters?.() ?? [], ...(device as any).getProperties?.() ?? []] + for (const pc of criteria.parameterCriteria as ParameterCriteria[]) { + const known = allParams.find((p: any) => p.name === pc.name) + if (known === undefined || known.value === null || known.value === undefined) { + return false + } + if (String(known.value) !== String(pc.value)) { + return false + } + } + } + + return true + } + + /** + * Determine if policy "once" should allow application of the configuration. + * + * - For policy "maintain": always apply + * - For policy "once": apply if device doesn't have the provisioning code + * (indicating content has changed or never applied) + */ + private shouldApplyOnce(device: Device, config: Configuration): boolean { + if (config.policy === 'maintain') { + return true + } + + if (config.policy !== 'once') { + return true + } + + // For "once" policy, check if device has matching provisioning code + const deviceProvCode = (device as any).provisioningCode + if (!deviceProvCode) { + return true // No provisioning code yet, apply it + } + + // Parse the provisioning code + const parsed = parseProvisioningCode(deviceProvCode) + if (!parsed) { + return true // Invalid format, re-apply + } + + // Check if identifiers match and content hasn't changed + if (parsed.identifier !== config.identifier) { + return true // Different configuration, apply + } + + const currentHash = computeContentHash(config) + if (parsed.hash !== currentHash) { + return true // Content changed, re-apply + } + + // Same configuration, same content hash => already applied + return false + } + + /** + * Compute parameter differences between desired and current state. + */ + private async computeParameterDiffs( + device: Device, + config: Configuration + ): Promise { + const diffs: ParameterDiff[] = [] + + for (const desired of config.desiredParameters) { + // Get current value from device + const currentValue = this.getDeviceParameterValue(device, desired.name) + + // Compare values + if (currentValue !== desired.value) { + diffs.push({ + name: desired.name, + currentValue, + targetValue: desired.value, + type: desired.type, + }) + } + } + + return diffs + } + + /** + * Get the current value of a parameter from the device. + * Returns null if not found. + */ + private getDeviceParameterValue(device: Device, paramName: string): string | number | boolean | null { + const param = (device as any).getParameter?.(paramName) + if (!param) { + return null + } + + const value = param.value + if (value === undefined || value === null) { + return null + } + + // Type coercion based on stored value type + if (typeof value === 'boolean') { + return value + } + if (typeof value === 'number') { + return value + } + + return String(value) + } + + /** + * Get the device's current firmware version. + * Looks in device properties or parameters (e.g., "Device.DeviceInfo.SoftwareVersion") + */ + private getDeviceVersion(device: Device, productClass: string): string | null { + // Try common firmware version parameters + const versionParams = [ + 'Device.DeviceInfo.SoftwareVersion', + 'InternetGatewayDevice.DeviceInfo.SoftwareVersion', + `Device.${productClass}.FirmwareVersion`, + ] + + for (const paramName of versionParams) { + const value = this.getDeviceParameterValue(device, paramName) + if (value && typeof value === 'string') { + return value + } + } + + return null + } +} diff --git a/app/services/configuration_service.ts b/app/services/configuration_service.ts new file mode 100644 index 0000000..77c7f8b --- /dev/null +++ b/app/services/configuration_service.ts @@ -0,0 +1,541 @@ +import ConfigurationModel from '#models/configuration' +import ConfigurationVersionModel from '#models/configuration_version' +import { + type Configuration, + type ConfigurationCriteria, + type DesiredParameter, + type InventoryPath, + PolicyType, +} from '../domain/configuration/configuration.js' +import type { ConfigurationVersion } from '../domain/configuration/configuration_version.js' +import { isParameterCoveredByInventory, suggestInventoryPath } from '../utils/inventory_coverage.js' +import { matchGlob } from '../utils/glob_match.js' + +/** + * Allowed criteria fields — DeviceId attributes (immutable) + parameterCriteria (evaluated dynamically). + */ +const ALLOWED_CRITERIA_FIELDS: ReadonlySet = new Set([ + 'manufacturer', + 'oui', + 'productClass', + 'serialNumber', + 'parameterCriteria', +]) + +const IDENTIFIER_REGEX = /^[A-Za-z0-9][A-Za-z0-9_-]*$/ +const SEMVER_REGEX = /^\d+\.\d+\.\d+$/ + +export interface ConfigurationFilters { + page?: number + perPage?: number + tag?: string + enabled?: boolean + productClass?: string +} + +export interface CreateConfigurationDto { + identifier: string + name: string + version: string + priority: number + policy: PolicyType | string + enabled?: boolean + tags?: string[] + criteria: ConfigurationCriteria + inventory: InventoryPath[] + desiredParameters: DesiredParameter[] + firmwareTarget?: { productClass: string; targetVersion: string } + schedule?: { + daysOfWeek: number[] + startHour: number + endHour: number + timezone: string + } +} + +export type UpdateConfigurationDto = Partial> + +export interface OverlapWarning { + type: 'overlap_different_priority' + overlappingConfigurations: Array<{ identifier: string; priority: number }> +} + +export type OverlapResult = OverlapWarning | null + +export class ValidationError extends Error { + public readonly field: string + public readonly statusCode: number + + constructor(message: string, field: string = '', statusCode: number = 422) { + super(message) + this.name = 'ValidationError' + this.field = field + this.statusCode = statusCode + } +} + +export default class ConfigurationService { + /** + * Create a new Configuration after validation. + */ + async create(data: CreateConfigurationDto): Promise { + this.validateIdentifier(data.identifier) + this.validateVersion(data.version) + this.validatePriority(data.priority) + this.validatePolicy(data.policy) + this.validateCriteria(data.criteria) + this.validateParameterCriteriaDeadlock(data.criteria, data.desiredParameters) + this.validateInventoryCoverage(data.desiredParameters, data.inventory) + + // Detect overlap — throws on same-priority conflict, returns warning for different-priority + const overlapResult = await this.detectOverlap(data.criteria, data.priority) + + const configuration: Partial = { + identifier: data.identifier, + name: data.name, + version: data.version, + priority: data.priority, + policy: data.policy as PolicyType, + enabled: data.enabled ?? true, + tags: data.tags ?? [], + criteria: data.criteria, + inventory: data.inventory, + desiredParameters: data.desiredParameters, + firmwareTarget: data.firmwareTarget, + schedule: data.schedule, + } + + const created = await ConfigurationModel.createIfNotExists(configuration) + + if (overlapResult) { + return { ...created, warning: overlapResult } + } + + return created + } + + /** + * Return a paginated list of Configurations with optional filters. + */ + async findAll(filters: ConfigurationFilters = {}) { + const page = filters.page ?? 1 + const perPage = filters.perPage ?? 25 + const mongoFilter: Record = {} + + if (filters.tag) { + mongoFilter.tags = filters.tag + } + + if (filters.enabled !== undefined) { + mongoFilter.enabled = filters.enabled + } + + if (filters.productClass) { + mongoFilter['criteria.productClass'] = filters.productClass + } + + return ConfigurationModel.paginateConfigurations(page, perPage, mongoFilter) + } + + /** + * Find a Configuration by its unique identifier. + */ + async findById(identifier: string): Promise { + return ConfigurationModel.findByIdentifier(identifier) + } + + /** + * Update an existing Configuration after validation. + * Creates a version snapshot of the current state before applying changes. + */ + async update(identifier: string, data: UpdateConfigurationDto): Promise { + const existing = await ConfigurationModel.findByIdentifier(identifier) + if (!existing) { + throw new ValidationError(`Configuration '${identifier}' not found`, 'identifier', 404) + } + + if (data.version !== undefined) { + this.validateVersion(data.version) + } + if (data.priority !== undefined) { + this.validatePriority(data.priority) + } + if (data.policy !== undefined) { + this.validatePolicy(data.policy) + } + if (data.criteria !== undefined) { + this.validateCriteria(data.criteria) + } + + // Validate inventory coverage with merged data + const finalDesiredParameters = data.desiredParameters ?? existing.desiredParameters + const finalInventory = data.inventory ?? existing.inventory + const finalCriteria2 = data.criteria ?? existing.criteria + this.validateParameterCriteriaDeadlock(finalCriteria2, finalDesiredParameters) + this.validateInventoryCoverage(finalDesiredParameters, finalInventory) + + // Detect overlap if criteria or priority changed + let overlapResult: OverlapResult = null + const finalCriteria = data.criteria ?? existing.criteria + const finalPriority = data.priority ?? existing.priority + if (data.criteria !== undefined || data.priority !== undefined) { + overlapResult = await this.detectOverlap(finalCriteria, finalPriority, identifier) + } + + // Create a version snapshot BEFORE applying changes + const latestVersionNumber = await ConfigurationVersionModel.getLatestVersionNumber(identifier) + const versionSnapshot: Omit = { + configurationIdentifier: identifier, + versionNumber: latestVersionNumber + 1, + snapshot: existing, + modifiedAt: new Date(), + } + await ConfigurationVersionModel.create(versionSnapshot) + + // Build update payload (only provided fields) + const updatePayload: Record = {} + if (data.name !== undefined) updatePayload.name = data.name + if (data.version !== undefined) updatePayload.version = data.version + if (data.priority !== undefined) updatePayload.priority = data.priority + if (data.policy !== undefined) updatePayload.policy = data.policy + if (data.enabled !== undefined) updatePayload.enabled = data.enabled + if (data.tags !== undefined) updatePayload.tags = data.tags + if (data.criteria !== undefined) updatePayload.criteria = data.criteria + if (data.inventory !== undefined) updatePayload.inventory = data.inventory + if (data.desiredParameters !== undefined) updatePayload.desiredParameters = data.desiredParameters + if (data.firmwareTarget !== undefined) updatePayload.firmwareTarget = data.firmwareTarget + if (data.schedule !== undefined) updatePayload.schedule = data.schedule + + await ConfigurationModel.updateOne({ identifier }, updatePayload) + + const updated = await ConfigurationModel.findByIdentifier(identifier) + + if (overlapResult) { + return { ...updated!, warning: overlapResult } + } + + return updated! + } + + /** + * Get the version history of a Configuration with pagination. + * Returns versions in reverse chronological order. + */ + async getHistory( + identifier: string, + page: number = 1, + limit: number = 25 + ): Promise<{ data: ConfigurationVersion[]; total: number; page: number; perPage: number; lastPage: number }> { + const existing = await ConfigurationModel.findByIdentifier(identifier) + if (!existing) { + throw new ValidationError(`Configuration '${identifier}' not found`, 'identifier', 404) + } + + return ConfigurationVersionModel.paginateVersions(identifier, page, limit) + } + + /** + * Toggle the enabled flag of a Configuration without modifying other fields. + */ + async toggleEnabled(identifier: string, enabled: boolean): Promise { + const existing = await ConfigurationModel.findByIdentifier(identifier) + if (!existing) { + throw new ValidationError(`Configuration '${identifier}' not found`, 'identifier', 404) + } + + await ConfigurationModel.updateOne({ identifier }, { enabled }) + + const updated = await ConfigurationModel.findByIdentifier(identifier) + return updated! + } + + /** + * Add and/or remove tags from a Configuration without modifying other fields. + */ + async updateTags( + identifier: string, + add?: string[], + remove?: string[] + ): Promise { + const existing = await ConfigurationModel.findByIdentifier(identifier) + if (!existing) { + throw new ValidationError(`Configuration '${identifier}' not found`, 'identifier', 404) + } + + let tags = [...existing.tags] + + // Add tags (avoid duplicates) + if (add && add.length > 0) { + for (const tag of add) { + if (!tags.includes(tag)) { + tags.push(tag) + } + } + } + + // Remove tags + if (remove && remove.length > 0) { + tags = tags.filter((tag) => !remove.includes(tag)) + } + + await ConfigurationModel.updateOne({ identifier }, { tags }) + + const updated = await ConfigurationModel.findByIdentifier(identifier) + return updated! + } + + /** + * Delete a Configuration and all its associated versions. + */ + async delete(identifier: string): Promise { + const deleted = await ConfigurationModel.deleteByIdentifier(identifier) + if (deleted) { + await ConfigurationVersionModel.deleteByConfigurationIdentifier(identifier) + } + return deleted + } + + // ─── Overlap Detection ───────────────────────────────────────────────────── + + /** + * Detect overlap between a Configuration's criteria and existing enabled Configurations. + * + * Two configurations overlap if their criteria could match the same device. + * - If both define the same field, check if patterns could match overlapping values + * - If one doesn't define a field, it matches all values → potential overlap + * + * @param criteria - The criteria to check for overlap + * @param priority - The priority of the configuration being created/updated + * @param excludeId - Optional identifier to exclude (for updates) + * @returns OverlapWarning if different-priority overlap found, null if no overlap + * @throws ValidationError with 409 if same-priority overlap found + */ + async detectOverlap( + criteria: ConfigurationCriteria, + priority: number, + excludeId?: string + ): Promise { + const allEnabled = await ConfigurationModel.findAllEnabled() + + const samePriorityConflicts: Array<{ identifier: string; priority: number }> = [] + const differentPriorityOverlaps: Array<{ identifier: string; priority: number }> = [] + + for (const existing of allEnabled) { + // Skip the configuration being updated + if (excludeId && existing.identifier === excludeId) { + continue + } + + if (this.criteriaOverlap(criteria, existing.criteria)) { + if (existing.priority === priority) { + samePriorityConflicts.push({ + identifier: existing.identifier, + priority: existing.priority, + }) + } else { + differentPriorityOverlaps.push({ + identifier: existing.identifier, + priority: existing.priority, + }) + } + } + } + + // Same-priority overlap → reject with 409 + if (samePriorityConflicts.length > 0) { + const identifiers = samePriorityConflicts.map((c) => c.identifier).join(', ') + throw new ValidationError( + `Priority conflict: configurations with the same priority ${priority} have overlapping criteria: ${identifiers}`, + 'priority', + 409 + ) + } + + // Different-priority overlap → return warning + if (differentPriorityOverlaps.length > 0) { + return { + type: 'overlap_different_priority', + overlappingConfigurations: differentPriorityOverlaps, + } + } + + return null + } + + /** + * Check if two sets of criteria could potentially match the same device. + * + * Conservative approach: + * - If one criteria doesn't define a field, it matches ALL values → potential overlap + * - If both define the same field, check if patterns could overlap using glob matching + */ + private criteriaOverlap( + criteriaA: ConfigurationCriteria, + criteriaB: ConfigurationCriteria + ): boolean { + const fields: Array = [ + 'manufacturer', + 'oui', + 'productClass', + 'serialNumber', + ] + + for (const field of fields) { + const patternA = criteriaA[field] as string | undefined + const patternB = criteriaB[field] as string | undefined + + // If either doesn't define the field, it matches all → potential overlap on this field + if (!patternA || !patternB) { + continue + } + + // Both define the field — check if patterns could match overlapping values + if (!this.patternsCouldOverlap(patternA, patternB)) { + return false + } + } + + // All fields are potentially overlapping + return true + } + + /** + * Determine if two glob patterns could match overlapping sets of values. + * + * Conservative approach: if one pattern matches the other (or vice versa), + * they could overlap. Also, if both are identical, they definitely overlap. + * For complex cases, we assume potential overlap (conservative). + */ + private patternsCouldOverlap(patternA: string, patternB: string): boolean { + // Identical patterns always overlap + if (patternA === patternB) { + return true + } + + // Check if one pattern could match a value that the other also matches. + // Simple heuristic: try matching each pattern against the other as a literal value. + // If patternA matches patternB (as a value) or vice versa, they could overlap. + if (matchGlob(patternA, patternB) || matchGlob(patternB, patternA)) { + return true + } + + // If either contains wildcards, be conservative and check for prefix overlap + const hasWildcardA = patternA.includes('*') || patternA.includes('?') + const hasWildcardB = patternB.includes('*') || patternB.includes('?') + + if (hasWildcardA || hasWildcardB) { + // Extract the fixed prefix before the first wildcard + const prefixA = patternA.split(/[*?]/)[0] + const prefixB = patternB.split(/[*?]/)[0] + + // If one prefix starts with the other, patterns could overlap + if (prefixA.startsWith(prefixB) || prefixB.startsWith(prefixA)) { + return true + } + } + + // No overlap detected + return false + } + + // ─── Validation Methods ────────────────────────────────────────────────────── + + private validateIdentifier(identifier: string): void { + if (!identifier || !IDENTIFIER_REGEX.test(identifier)) { + throw new ValidationError( + `Invalid identifier '${identifier}'. Must start with alphanumeric and contain only alphanumeric, hyphens, or underscores.`, + 'identifier' + ) + } + } + + private validateVersion(version: string): void { + if (!version || !SEMVER_REGEX.test(version)) { + throw new ValidationError( + `Invalid version '${version}'. Must be a valid semver format (e.g. "1.0.0").`, + 'version' + ) + } + } + + private validatePriority(priority: number): void { + if (!Number.isInteger(priority) || priority <= 0) { + throw new ValidationError( + `Invalid priority '${priority}'. Must be a positive integer greater than 0.`, + 'priority' + ) + } + } + + private validatePolicy(policy: string): void { + if (policy !== PolicyType.ONCE && policy !== PolicyType.MAINTAIN) { + throw new ValidationError( + `Invalid policy '${policy}'. Must be "once" or "maintain".`, + 'policy' + ) + } + } + + private validateCriteria(criteria: ConfigurationCriteria): void { + const fields = Object.keys(criteria) + for (const field of fields) { + if (!ALLOWED_CRITERIA_FIELDS.has(field)) { + throw new ValidationError( + `Invalid criteria field '${field}'. Only DeviceId fields are allowed: ${[...ALLOWED_CRITERIA_FIELDS].join(', ')}.`, + 'criteria' + ) + } + } + } + + /** + * Warn if a parameterCriteria path is also in desiredParameters — this creates a risk of + * the config never matching (deadlock) if the device doesn't already have that parameter value. + * We throw a ValidationError to inform the caller of this dangerous overlap. + */ + private validateParameterCriteriaDeadlock( + criteria: ConfigurationCriteria, + desiredParameters: DesiredParameter[] + ): void { + const pcNames = (criteria.parameterCriteria || []).map((p) => p.name) + const dpNames = new Set((desiredParameters || []).map((p) => p.name)) + const conflicts = pcNames.filter((name) => dpNames.has(name)) + if (conflicts.length > 0) { + throw new ValidationError( + `Deadlock risk: parameter(s) [${conflicts.join(', ')}] appear in both criteria.parameterCriteria and desiredParameters. ` + + `A device that does not yet have these values will never match, and the config will never apply.`, + 'criteria.parameterCriteria' + ) + } + } + + private validateInventoryCoverage( + desiredParameters: DesiredParameter[], + inventory: InventoryPath[] + ): void { + if (!desiredParameters || desiredParameters.length === 0) { + return + } + + const uncoveredParams: Array<{ name: string; suggestedPath: string }> = [] + + for (const param of desiredParameters) { + if (!isParameterCoveredByInventory(param.name, inventory)) { + uncoveredParams.push({ + name: param.name, + suggestedPath: suggestInventoryPath(param.name), + }) + } + } + + if (uncoveredParams.length > 0) { + const details = uncoveredParams + .map((p) => `"${p.name}" (suggest adding "${p.suggestedPath}" to inventory)`) + .join(', ') + throw new ValidationError( + `Desired parameters not covered by inventory: ${details}`, + 'desiredParameters' + ) + } + } +} diff --git a/app/services/cwmp/action_execution_helper.ts b/app/services/cwmp/action_execution_helper.ts new file mode 100644 index 0000000..68b2566 --- /dev/null +++ b/app/services/cwmp/action_execution_helper.ts @@ -0,0 +1,62 @@ +import { Device } from '../../domain/device/device.js' +import { DeviceActionExecutor } from './device_action_executor.js' +import logger from '@adonisjs/core/services/logger' + +/** + * Action types + */ +export enum ActionType { + SET_PARAMETER_VALUES = 'SET_PARAMETER_VALUES', + SET_PARAMETER_ATTRIBUTES = 'SET_PARAMETER_ATTRIBUTES', + SCHEDULE_INFORM = 'SCHEDULE_INFORM', + DOWNLOAD = 'DOWNLOAD', + UPLOAD = 'UPLOAD', + FACTORY_RESET = 'FACTORY_RESET', + REBOOT = 'REBOOT', +} + +/** + * Dispatches an action to the corresponding DeviceActionExecutor method + */ +export async function executeAction( + deviceAction: DeviceActionExecutor, + device: Device, + actionType: ActionType, + parameters: any[], + callback: ((result: unknown) => Promise) = () => Promise.resolve('') +): Promise { + switch (actionType) { + case ActionType.SET_PARAMETER_VALUES: + return deviceAction.applySetParameterValues(device, parameters as any[], callback as (r: boolean) => Promise) + + case ActionType.SET_PARAMETER_ATTRIBUTES: + return deviceAction.applySetParameterAttributes(device, parameters as any[], callback as (r: boolean) => Promise) + + case ActionType.SCHEDULE_INFORM: + return deviceAction.applyScheduleInform(device, parameters[0] as any, callback as (r: boolean) => Promise) + + case ActionType.DOWNLOAD: { + const p = parameters[0] + return deviceAction.applyDownload(device, p, callback as (r: boolean) => Promise) + } + + case ActionType.UPLOAD: { + const p = parameters[0] + return deviceAction.applyUpload(device, p, callback as (r: boolean) => Promise) + } + + case ActionType.FACTORY_RESET: { + const p = parameters?.[0] + return deviceAction.applyFactoryReset(device, p, callback as (r: boolean) => Promise) + } + + case ActionType.REBOOT: { + const p = parameters[0] + return deviceAction.applyReboot(device, p, callback as (r: boolean) => Promise) + } + + default: + logger.error(`Unknown action type: ${actionType}`) + return '' + } +} diff --git a/app/services/cwmp/configuration_evaluation_state.ts b/app/services/cwmp/configuration_evaluation_state.ts new file mode 100644 index 0000000..c103f50 --- /dev/null +++ b/app/services/cwmp/configuration_evaluation_state.ts @@ -0,0 +1,269 @@ +import ConfigurationEngine from '../configuration_engine.js' +import type { EngineResult } from '../configuration_engine.js' +import type { DeviceStateResult, DeviceContext } from './main_state_machine.js' +import DeviceModel from '#models/device' +import logger from '@adonisjs/core/services/logger' +import { ComplianceStatus } from '../../domain/configuration/compliance.js' +import { buildProvisioningCode } from '../../utils/provisioning_code.js' + +/** + * ConfigurationEvaluationState + * + * Stateful state that drives the full configuration evaluation flow: + * 1. On first handle(): calls ConfigurationEngine.evaluate(), sets compliance to "in_progress" + * 2. Processes inventory reads one path at a time (GetParameterNames RPC) + * 3. Processes all parameter diffs in one SetParameterValues RPC + * 4. If firmware needed, transitions to FirmwareUpgradeState + * 5. Otherwise, transitions to ComplianceUpdateState + */ +export class ConfigurationEvaluationState { + name = 'configuration-evaluation' + private engine: ConfigurationEngine + private engineResult: EngineResult | null = null + private inventoryQueue: string[] = [] + private pendingValuesQueue: string[] = [] + private paramDiffs: Array<{ name: string; value: string | number | boolean; type?: string }> = [] + private initialized = false + + constructor() { + this.engine = new ConfigurationEngine() + } + + async handle(context: DeviceContext): Promise { + try { + if (!this.initialized) { + this.initialized = true + + this.engineResult = await this.engine.evaluate(context.device) + logger.debug( + { + identifier: context.device.identifier, + configId: this.engineResult.configuration?.identifier, + inventoryPaths: this.engineResult.inventoryPaths.length, + paramDiffs: this.engineResult.parameterDiffs.length, + firmwareSteps: this.engineResult.firmwareMigrationPath.length, + isAlreadyCompliant: this.engineResult.isAlreadyCompliant, + }, + '[ConfigEval] Evaluation complete' + ) + + await DeviceModel.updateOne( + { identifier: context.device.identifier }, + { + 'compliance.status': ComplianceStatus.IN_PROGRESS, + 'compliance.lastEvaluatedAt': new Date(), + 'compliance.configurationIdentifier': this.engineResult.configuration?.identifier || null, + } + ) + + // Always populate inventory queue (discovery happens regardless of compliance) + this.inventoryQueue = [...this.engineResult.inventoryPaths] + + if (!this.engineResult.isAlreadyCompliant) { + this.paramDiffs = this.engineResult.parameterDiffs.map((d) => ({ + name: d.name, + value: d.targetValue, + type: d.type, + })) + } + } + + return this.processNextAction(context) + } catch (error: any) { + logger.error({ identifier: context.device.identifier, error: error.message }, '[ConfigEval] Evaluation failed') + + await DeviceModel.updateOne( + { identifier: context.device.identifier }, + { + 'compliance.status': ComplianceStatus.FAILED, + 'compliance.lastError': error.message, + 'compliance.lastEvaluatedAt': new Date(), + } + ).catch(() => {}) + + return { isComplete: true } + } + } + + private async processNextAction(context: DeviceContext): Promise { + // 1. Inventory phase 1: GetParameterNames per subtree path + if (this.inventoryQueue.length > 0) { + const path = this.inventoryQueue.shift()! + logger.debug({ identifier: context.device.identifier, path }, '[ConfigEval] Inventory GetParameterNames') + const response = await context.services.deviceAction.getParameterNames( + context.device, + path, + async (result) => { + // Collect leaf names into pendingValuesQueue for phase 2 + const infoList = result.data?.ParameterList?.ParameterInfoStruct || [] + const leafNames: string[] = infoList + .map((info: any) => info?.Name) + .filter((name: any): name is string => !!name && !name.endsWith('.')) + if (leafNames.length > 0) { + this.pendingValuesQueue.push(...leafNames) + } + return '' + } + ) + return { response } + } + + // 2. Inventory phase 2: GetParameterValues for discovered leaf names + if (this.pendingValuesQueue.length > 0) { + const leaves = this.pendingValuesQueue.splice(0) + logger.debug({ identifier: context.device.identifier, count: leaves.length }, '[ConfigEval] Inventory GetParameterValues') + const response = await context.services.deviceAction.getParameterValues( + context.device, + leaves, + () => Promise.resolve('') + ) + return { response } + } + + // 3. All parameter diffs in one SetParameterValues RPC + if (this.paramDiffs.length > 0) { + const diffs = this.paramDiffs.splice(0) + logger.debug({ identifier: context.device.identifier, count: diffs.length }, '[ConfigEval] Applying parameter diffs') + const params = diffs.map((d) => ({ parameterName: d.name, value: d.value, type: d.type })) + const response = await context.services.deviceAction.applySetParameterValues( + context.device, + params, + () => Promise.resolve('') + ) + return { response } + } + + // 4. Firmware upgrade (if needed and within schedule) + if (this.engineResult!.firmwareMigrationPath.length > 0 && !this.engineResult!.skipFirmware) { + logger.debug( + { identifier: context.device.identifier, steps: this.engineResult!.firmwareMigrationPath.length }, + '[ConfigEval] Transitioning to firmware upgrade' + ) + return { nextState: new FirmwareUpgradeState(this.engineResult!.firmwareMigrationPath, 0, this.engineResult!) } + } + + // 5. All done + return { nextState: new ComplianceUpdateState(this.engineResult!) } + } +} + +/** + * FirmwareUpgradeState + * + * Sends a Download RPC for the current migration step. + * After the RPC, the session ends (device reboots and re-Informs for next step). + */ +export class FirmwareUpgradeState { + name = 'firmware-upgrade' + private migrationSteps: any[] + private currentStepIndex: number + private engineResult: EngineResult + + constructor(migrationSteps: any[], currentStepIndex: number = 0, engineResult: EngineResult) { + this.migrationSteps = migrationSteps + this.currentStepIndex = currentStepIndex + this.engineResult = engineResult + } + + async handle(context: DeviceContext): Promise { + const step = this.migrationSteps[this.currentStepIndex] + + if (!step) { + return { nextState: new ComplianceUpdateState(this.engineResult) } + } + + logger.debug( + { identifier: context.device.identifier, step: this.currentStepIndex + 1, total: this.migrationSteps.length }, + '[FirmwareUpgrade] Sending Download RPC' + ) + + try { + const response = await context.services.deviceAction.applyDownload( + context.device, + { + commandKey: `fw-upgrade-step${this.currentStepIndex}-${Date.now()}`, + fileType: '1 Firmware Upgrade Image', + url: step.downloadUrl, + fileSize: step.fileSize || 0, + targetFileName: '', + }, + async () => '' + ) + + return { response } + } catch (error: any) { + logger.error({ identifier: context.device.identifier, error: error.message }, '[FirmwareUpgrade] Download RPC failed') + + await DeviceModel.updateOne( + { identifier: context.device.identifier }, + { + 'compliance.status': ComplianceStatus.FAILED, + 'compliance.lastError': `Firmware upgrade failed: ${error.message}`, + 'compliance.lastEvaluatedAt': new Date(), + } + ).catch(() => {}) + + return { isComplete: true } + } + } +} + +/** + * ComplianceUpdateState + * + * Finalizes device compliance: + * - Updates compliance status to "conforme" in DB + * - For policy "once": writes ProvisioningCode to device + * - Transitions to ExecutingCommandsState to process pending user commands + */ +export class ComplianceUpdateState { + name = 'compliance-update' + private engineResult: EngineResult + private started = false + + constructor(engineResult: EngineResult) { + this.engineResult = engineResult + } + + async handle(context: DeviceContext): Promise { + const { ExecutingCommandsState } = await import('./main_state_machine.js') + + if (!this.started) { + this.started = true + + const updateData: Record = { + 'compliance.status': ComplianceStatus.COMPLIANT, + 'compliance.lastEvaluatedAt': new Date(), + 'compliance.lastError': null, + } + + if (this.engineResult.configuration) { + updateData['compliance.configurationIdentifier'] = this.engineResult.configuration.identifier + } + + // Always build and write provisioning code to device (functional for "once", informative for "maintain") + if (this.engineResult.configuration) { + const provCode = buildProvisioningCode(this.engineResult.configuration) + updateData.provisioningCode = provCode + + await DeviceModel.updateOne({ identifier: context.device.identifier }, updateData).catch(() => {}) + + context.device.tmpPC = provCode + logger.debug({ identifier: context.device.identifier, provCode, policy: this.engineResult.configuration.policy }, '[ComplianceUpdate] Writing provisioning code') + + const response = await context.services.deviceAction.insertProvisioningCode( + context.device, + () => Promise.resolve('') + ) + return { response } + } + + await DeviceModel.updateOne({ identifier: context.device.identifier }, updateData).catch(() => {}) + logger.debug({ identifier: context.device.identifier }, '[ComplianceUpdate] Compliance finalized (no configuration)') + return { nextState: new ExecutingCommandsState(true) } + } + + // Second call (after provCode write response): execute user commands + return { nextState: new ExecutingCommandsState(true) } + } +} diff --git a/app/services/cwmp/device_action_executor.ts b/app/services/cwmp/device_action_executor.ts new file mode 100644 index 0000000..207272a --- /dev/null +++ b/app/services/cwmp/device_action_executor.ts @@ -0,0 +1,377 @@ +import { Device } from '../../domain/device/device.js' +import { ParameterType } from '../../domain/shared/types.js' +import { CWMPTaskQueue, ResultStatus, TaskResult } from './task_queue.js' +import DeviceModel from '../../models/device.js' +import logger from '@adonisjs/core/services/logger' + +// Parameter types (simplified from legacy provisioning domain) +interface SetParameterValue { + parameterName: string + value: string | number | boolean + type?: string +} + +interface SetParameterAttribute { + name: string + notificationChange: boolean + notification: number + accessListChange: boolean + accessList: string[] +} + +interface SetScheduleInform { + delayInSeconds: number + commandKey?: string +} + +interface DownloadParameter { + commandKey: string + fileType: string + url: string + username?: string + password?: string + fileSize?: number + targetFileName?: string + delaySeconds?: number + successUrl?: string + failureUrl?: string +} + +interface UploadParameter { + commandKey: string + fileType: string + url: string + username?: string + password?: string + delayInSeconds?: number +} + +interface FactoryResetParameter { + commandKey?: string +} + +interface RebootParameter { + commandKey?: string +} + +/** + * Executes CWMP actions on devices via the TaskQueue + * Each action wraps a callback that persists state on success + */ +export class DeviceActionExecutor { + private taskQueue: CWMPTaskQueue + + constructor(taskQueue: CWMPTaskQueue) { + this.taskQueue = taskQueue + } + + private async persistDevice(device: Device): Promise { + try { + const snapshot = device.toSnapshot() + await DeviceModel.createOrUpdate({ + identifier: snapshot.identifier, + deviceId: snapshot.deviceId, + properties: snapshot.properties, + parameters: snapshot.parameters, + updatedAt: new Date(), + }) + } catch (e) { + logger.error(`Failed to persist device: ${e}`) + } + } + + async getParameterValues( + device: Device, + parameterPaths: string[], + callback: (result: TaskResult) => Promise + ): Promise { + const wrappedCallback = async (result: TaskResult) => { + if (result.status === ResultStatus.Success) { + try { + const resp: any = result.data + const list = resp?.ParameterList?.ParameterValueStruct + if (Array.isArray(list) && list.length > 0) { + device.updateFromValues(list) + await this.persistDevice(device) + } + } catch (e) { + logger.error(`Error updating device from parameter values: ${e}`) + } + } + return await callback(result) + } + + return this.taskQueue.addTask(device.deviceId, 'get_parameter_values', parameterPaths, wrappedCallback) + } + + async getParameterAttributes( + device: Device, + parameterList: string[], + callback: (result: TaskResult) => Promise + ): Promise { + const wrappedCallback = async (result: TaskResult) => { + if (result.status === ResultStatus.Success) { + try { + const resp: any = result.data + const list = resp?.ParameterList?.ParameterAttributeStruct + if (Array.isArray(list) && list.length > 0) { + for (const attr of list) { + const name: string | undefined = attr?.Name + if (!name) continue + const notification: number | undefined = typeof attr?.Notification === 'number' ? attr.Notification : undefined + const accessList: string[] | undefined = attr?.AccessList?.string + + const param = device.getParameter(name) + if (param) { + param.updateAttributes?.(notification, accessList) + continue + } + const prop = device.getProperty(name) + if (prop) { + prop.updateAttributes?.(notification, accessList) + } + } + await this.persistDevice(device) + } + } catch (e) { + logger.error(`Error updating device from parameter attributes: ${e}`) + } + } + return await callback(result) + } + + return this.taskQueue.addTask(device.deviceId, 'get_parameter_attributes', parameterList, wrappedCallback) + } + + async getParameterNames( + device: Device, + parameterName: string, + callback: (result: TaskResult) => Promise + ): Promise { + const wrappedCallback = async (result: TaskResult) => { + if (result.status === ResultStatus.Success) { + try { + device.organizeParameters(result.data as any, new Date()) + await this.persistDevice(device) + } catch (e) { + logger.error(`Error organizing parameters: ${e}`) + } + } + return await callback(result) + } + + return this.taskQueue.addTask(device.deviceId, 'get_parameter_names', parameterName, wrappedCallback) + } + + async applySetParameterValues( + device: Device, + parameters: SetParameterValue[], + callback: (result: boolean) => Promise + ): Promise { + const existingParameters = device.getParameters() + + const inferType = (val: any): string => { + const t = typeof val + switch (t) { + case 'boolean': return 'xsd:boolean' + case 'number': return Number.isInteger(val) ? 'xsd:int' : 'xsd:string' + default: return 'xsd:string' + } + } + + const cwmpTypeMap: Record = { + string: 'xsd:string', + unsignedInt: 'xsd:unsignedInt', + int: 'xsd:int', + boolean: 'xsd:boolean', + dateTime: 'xsd:dateTime', + } + + const pvStructs = parameters.map((p) => { + const existing = existingParameters.find((ep) => ep.name === p.parameterName) + // Priority: 1) explicit type from config, 2) known device type, 3) inferred from JS value + let xsdType: string + if (p.type && cwmpTypeMap[p.type]) { + xsdType = cwmpTypeMap[p.type] + } else if (existing?.type && existing.type !== ParameterType.UNKNOWN) { + xsdType = existing.type // already in xsd: format (e.g. 'xsd:string') + } else { + xsdType = inferType(p.value) + } + return { + Name: p.parameterName, + Value: { _: p.value, $: { Type: xsdType } }, + } + }) + + const parameterKey = `SetPValues_${device.deviceId.serialNumber}_${Date.now()}` + + return this.taskQueue.addTask( + device.deviceId, + 'set_parameter_values', + { parameterValueList: pvStructs, parameterKey }, + async (result: TaskResult) => { + if (result.status === ResultStatus.Success) { + device.updateFromValues(pvStructs as any) + await this.persistDevice(device) + return await callback(true) + } + return await callback(false) + } + ) + } + + async applySetParameterAttributes( + device: Device, + parameters: SetParameterAttribute[], + callback: (result: boolean) => Promise + ): Promise { + const attrs = parameters.map((p) => ({ + name: p.name, + notificationChange: p.notificationChange ?? false, + notification: p.notification ?? 0, + accessListChange: p.accessListChange ?? false, + accessList: (p.accessList as any)?.string || [], + })) + + return this.taskQueue.addTask( + device.deviceId, + 'set_parameter_attributes', + attrs, + async (result: TaskResult) => { + if (result.status === ResultStatus.Success) { + return await callback(true) + } + return await callback(false) + } + ) + } + + async applyScheduleInform( + device: Device, + parameter: SetScheduleInform, + callback: (result: boolean) => Promise + ): Promise { + const commandKey = parameter.commandKey || `ScheduleInform_${device.deviceId.serialNumber}_${Date.now()}` + return this.taskQueue.addTask( + device.deviceId, + 'schedule_inform', + { delayInSeconds: parameter.delayInSeconds, commandKey }, + async (result: TaskResult) => { + return await callback(result.status === ResultStatus.Success) + } + ) + } + + async applyFactoryReset( + _device: Device, + _parameter: FactoryResetParameter, + callback: (result: boolean) => Promise + ): Promise { + return this.taskQueue.addTask( + _device.deviceId, + 'factory_reset', + {}, + async (result: TaskResult) => { + return await callback(result.status === ResultStatus.Success) + } + ) + } + + async applyReboot( + device: Device, + parameter: RebootParameter, + callback: (result: boolean) => Promise + ): Promise { + return this.taskQueue.addTask( + device.deviceId, + 'reboot', + { commandKey: parameter.commandKey || '' }, + async (result: TaskResult) => { + return await callback(result.status === ResultStatus.Success) + } + ) + } + + async applyDownload( + device: Device, + parameter: DownloadParameter, + callback: (result: boolean) => Promise + ): Promise { + return this.taskQueue.addTask( + device.deviceId, + 'download', + { + commandKey: parameter.commandKey, + fileType: parameter.fileType, + url: parameter.url, + username: parameter.username, + password: parameter.password, + fileSize: parameter.fileSize, + targetFileName: parameter.targetFileName, + delaySeconds: parameter.delaySeconds, + successUrl: parameter.successUrl, + failureUrl: parameter.failureUrl, + }, + async (result: TaskResult) => { + return await callback(result.status === ResultStatus.Success) + } + ) + } + + async applyUpload( + device: Device, + parameter: UploadParameter, + callback: (result: boolean) => Promise + ): Promise { + return this.taskQueue.addTask( + device.deviceId, + 'upload', + { + commandKey: parameter.commandKey, + fileType: parameter.fileType, + url: parameter.url, + username: parameter.username, + password: parameter.password, + delaySeconds: parameter.delayInSeconds, + }, + async (result: TaskResult) => { + return await callback(result.status === ResultStatus.Success) + } + ) + } + + async insertProvisioningCode( + device: Device, + callback: (result: boolean) => Promise + ): Promise { + const allParams = device.getParameters() + const allProps = device.getProperties() + + const knownPC = [...allParams, ...allProps].find((p: any) => /(^|\.)ProvisioningCode$/.test(p?.name)) + + const hasDeviceTree = [...allParams, ...allProps].some((p: any) => typeof p?.name === 'string' && p.name.startsWith('Device.')) + const hasIgdTree = [...allParams, ...allProps].some((p: any) => typeof p?.name === 'string' && p.name.startsWith('InternetGatewayDevice.')) + + const inferredPath = hasDeviceTree ? 'Device.DeviceInfo.ProvisioningCode' : 'InternetGatewayDevice.DeviceInfo.ProvisioningCode' + const provisioningPath = knownPC?.name || (hasIgdTree && !hasDeviceTree ? 'InternetGatewayDevice.DeviceInfo.ProvisioningCode' : inferredPath) + + const parameterKey = `SetPValues_${device.deviceId.serialNumber}_${Date.now()}` + const pvStructs = [{ Name: provisioningPath, Value: { _: device.tmpPC, $: { Type: 'xsd:string' } } }] + + return this.taskQueue.addTask( + device.deviceId, + 'set_parameter_values', + { parameterValueList: pvStructs, parameterKey }, + async (result: TaskResult) => { + if (result.status === ResultStatus.Success) { + device.updateFromValues(pvStructs as any) + await this.persistDevice(device) + device.tmpPC = '' + return await callback(true) + } + return await callback(false) + } + ) + } +} diff --git a/app/services/cwmp/immediate_command_state_machine.ts b/app/services/cwmp/immediate_command_state_machine.ts new file mode 100644 index 0000000..18621ef --- /dev/null +++ b/app/services/cwmp/immediate_command_state_machine.ts @@ -0,0 +1,64 @@ +import { Device } from '../../domain/device/device.js' +import { CWMPTaskQueue } from './task_queue.js' +import { DeviceActionExecutor } from './device_action_executor.js' +import { ExecutingCommandsState, DeviceContext, DeviceStateResult } from './main_state_machine.js' +import logger from '@adonisjs/core/services/logger' + +/** + * State machine for "6 CONNECTION REQUEST" events. + * Only executes pending Immediate (I) commands - does NOT include NI commands. + */ +export class ImmediateCommandStateMachine { + private currentState: ExecutingCommandsState + private context: DeviceContext + + constructor(device: Device, session: string, taskQueue: CWMPTaskQueue) { + // includeNI = false: only execute Immediate commands + this.currentState = new ExecutingCommandsState(false) + this.context = { + device, + session, + taskQueue, + services: { + deviceAction: new DeviceActionExecutor(taskQueue), + }, + } + } + + async start(): Promise { + logger.info(`Starting immediate-command state machine for device ${this.context.device.deviceId?.serialNumber}`) + return this.execute() + } + + async handleResponse(success: boolean, message: any): Promise { + this.context.message = { success, data: message } + return this.execute() + } + + getCurrentStateName(): string { + return this.currentState.name + } + + isCompleted(): boolean { + return this.currentState.name === 'completed' + } + + private async execute(): Promise { + try { + const result: DeviceStateResult = await this.currentState.handle(this.context) + + if (result.isComplete) { + return '' + } + + if (result.response && result.response !== '') { + return result.response + } + + return '' + } catch (error) { + logger.error(`Immediate-command state machine error: ${error}`) + return '' + } + } +} diff --git a/app/services/cwmp/main_state_machine.ts b/app/services/cwmp/main_state_machine.ts new file mode 100644 index 0000000..9b109fa --- /dev/null +++ b/app/services/cwmp/main_state_machine.ts @@ -0,0 +1,366 @@ +import { Device } from '../../domain/device/device.js' +import { CWMPTaskQueue } from './task_queue.js' +import { DeviceActionExecutor } from './device_action_executor.js' +import { getDeviceHashIdentifier } from '../../utils/device_utils.js' +import { CommandStatus, CommandType } from '../../domain/commands/command.js' +import DeviceModel from '../../models/device.js' +import CommandModel from '../../models/command.js' +import { ModeType } from '../../domain/shared/mode.js' +import logger from '@adonisjs/core/services/logger' +import { ConfigurationEvaluationState } from './configuration_evaluation_state.js' + +// ─── Interfaces ──────────────────────────────────────────────────────────────── + +export interface DeviceContext { + device: Device + session: string + message?: { success: boolean; data: any } + taskQueue: CWMPTaskQueue + services: { + deviceAction: DeviceActionExecutor + } +} + +export interface DeviceStateResult { + nextState?: DeviceState + response?: string + isComplete?: boolean +} + +// ─── Abstract State ──────────────────────────────────────────────────────────── + +abstract class DeviceState { + abstract name: string + abstract handle(context: DeviceContext): Promise +} + +// ─── States ──────────────────────────────────────────────────────────────────── + +class IdleState extends DeviceState { + name = 'idle' + + async handle(context: DeviceContext): Promise { + const parameterQueue = context.device.tmpParamValues.map((p: any) => p.Name).filter((n: any): n is string => !!n) + if (parameterQueue.length === 0) { + return { nextState: new ConfigurationEvaluationState(), response: '' } + } + return { nextState: new DiscoveringParametersState(parameterQueue), response: '' } + } +} + +class DiscoveringParametersState extends DeviceState { + name = 'discovering-parameters' + private parameterQueue: string[] + private started = false + + constructor(parameterQueue: string[]) { + super() + this.parameterQueue = parameterQueue + } + + async handle(context: DeviceContext): Promise { + if (!this.started) { + this.started = true + if (this.parameterQueue.length === 0) { + return { nextState: new ConfigurationEvaluationState(), response: '' } + } + return { response: await this.processNext(context) } + } + + // Process response and continue + if (this.parameterQueue.length > 0) { + return { response: await this.processNext(context) } + } + + return { nextState: new ConfigurationEvaluationState(), response: '' } + } + + private async processNext(context: DeviceContext): Promise { + if (this.parameterQueue.length === 0) return '' + const current = this.parameterQueue.shift()! + return context.services.deviceAction.getParameterNames(context.device, current, () => Promise.resolve('')) + } +} + +export class ExecutingCommandsState extends DeviceState { + name = 'executing-commands' + private commandsQueue: any[] = [] + private started = false + private currentCommandRecord: any | null = null + private includeNI: boolean + + constructor(includeNI: boolean = true) { + super() + this.includeNI = includeNI + } + + async handle(context: DeviceContext): Promise { + if (!this.started) { + const hashId = getDeviceHashIdentifier({ + manufacturer: context.device.deviceId.manufacturer, + oui: context.device.deviceId.oui, + productClass: context.device.deviceId.productClass, + serialNumber: context.device.deviceId.serialNumber, + }) + + const collection = CommandModel.getCollection() + const iWaiting = await collection.find({ deviceIdentifier: hashId, mode: ModeType.I, status: CommandStatus.WAITING }).sort({ priority: 1 }).toArray() + const iPending = await collection.find({ deviceIdentifier: hashId, mode: ModeType.I, status: CommandStatus.PENDING }).sort({ priority: 1 }).toArray() + const niPending = this.includeNI + ? await collection.find({ deviceIdentifier: hashId, mode: ModeType.NI, status: CommandStatus.PENDING }).sort({ priority: 1 }).toArray() + : [] + + this.commandsQueue = [...iWaiting, ...iPending, ...niPending] + this.started = true + + if (this.commandsQueue.length === 0) { + if (this.includeNI) { + return { nextState: new CompletedState(), isComplete: true, response: '' } + } + return { response: '' } + } + + return { response: await this.processNextCommand(context) } + } + + // Handle response for last command + if (this.currentCommandRecord) { + const now = new Date() + if (context.message?.success) { + this.currentCommandRecord.status = CommandStatus.COMPLETED + this.currentCommandRecord.completedAt = now + this.currentCommandRecord.executedAt = now + + // Optimistic update for SPV + if (this.currentCommandRecord.action === CommandType.SET_PARAMETER_VALUES) { + const params = Array.isArray(this.currentCommandRecord.parameters) ? this.currentCommandRecord.parameters : [] + const pvStructs = params.map((p: any) => ({ + Name: p?.name, + Value: { _: p?.value, $: { Type: 'xsd:string' } }, + })) + if (pvStructs.length > 0) { + context.device.updateFromValues(pvStructs) + } + } + } else { + this.currentCommandRecord.status = CommandStatus.FAILED + this.currentCommandRecord.executedAt = now + } + + try { + await CommandModel.updateCommand(this.currentCommandRecord._id, { + status: this.currentCommandRecord.status, + executedAt: now, + completedAt: this.currentCommandRecord.completedAt, + updatedAt: now, + }) + } catch (err) { + logger.error(`Failed to update command: ${err}`) + } + + this.currentCommandRecord = null + } + + if (this.commandsQueue.length > 0) { + return { response: await this.processNextCommand(context) } + } + + // All commands done + if (this.includeNI) { + return { nextState: new CompletedState(), isComplete: true, response: '' } + } + return { response: '' } + } + + private async processNextCommand(context: DeviceContext): Promise { + if (this.commandsQueue.length === 0) return '' + + const commandRecord = this.commandsQueue.shift()! + this.currentCommandRecord = commandRecord + + // Mark as SENT + const now = new Date() + commandRecord.status = CommandStatus.SENT + commandRecord.executedAt = now + try { + await CommandModel.updateCommand(commandRecord._id, { status: CommandStatus.SENT, executedAt: now, updatedAt: now }) + } catch (err) { + logger.error(`Failed to mark command as SENT: ${err}`) + } + + // Clear pending tasks for this device before issuing command + try { + context.taskQueue.clearPendingTasksForDevice(context.device.deviceId) + } catch {} + + const action = commandRecord.action as CommandType + const params = commandRecord.parameters + + // Map CommandType to CWMP request + switch (action) { + case CommandType.GET_PARAMETER_VALUES: { + const names: string[] = Array.isArray(params) ? params : params?.names || params?.paths || params?.parameterNames || [] + return context.services.deviceAction.getParameterValues(context.device, names, () => Promise.resolve('')) + } + case CommandType.GET_PARAMETER_NAMES: { + const name: string = typeof params === 'string' ? params : (params?.name || params?.path || params?.parameterPath || '') + return context.services.deviceAction.getParameterNames(context.device, name, () => Promise.resolve('')) + } + case CommandType.GET_PARAMETER_ATTRIBUTES: { + const names: string[] = Array.isArray(params) ? params : params?.names || params?.paths || params?.parameterNames || [] + return context.services.deviceAction.getParameterAttributes(context.device, names, () => Promise.resolve('')) + } + case CommandType.SET_PARAMETER_VALUES: { + const rawParams = Array.isArray(params) + ? params + : params?.parameterValues || params?.parameters || [] + const spvParams = rawParams.map((p: any) => ({ + parameterName: p.parameterName ?? p.name, + value: p.value, + type: p.type, + })) + return context.services.deviceAction.applySetParameterValues(context.device, spvParams, () => Promise.resolve('')) + } + case CommandType.SET_PARAMETER_ATTRIBUTES: { + const spaParams = Array.isArray(params) ? params : params?.attributes || params?.parameterAttributes || [] + return context.services.deviceAction.applySetParameterAttributes(context.device, spaParams, () => Promise.resolve('')) + } + case CommandType.DOWNLOAD: { + return context.services.deviceAction.applyDownload(context.device, params || {}, () => Promise.resolve('')) + } + case CommandType.UPLOAD: { + return context.services.deviceAction.applyUpload(context.device, params || {}, () => Promise.resolve('')) + } + case CommandType.REBOOT: { + return context.services.deviceAction.applyReboot(context.device, params || {}, () => Promise.resolve('')) + } + case CommandType.FACTORY_RESET: { + return context.services.deviceAction.applyFactoryReset(context.device, {}, () => Promise.resolve('')) + } + case CommandType.SCHEDULE_INFORM: { + return context.services.deviceAction.applyScheduleInform( + context.device, + { delayInSeconds: params?.delayInSeconds || 300, commandKey: params?.commandKey }, + () => Promise.resolve('') + ) + } + case CommandType.REFRESH: { + // Refresh = GetParameterValues on all known leaf parameters that have non-empty values + const allNames = [ + ...context.device.getParameters().filter((p) => p.value != null && p.value !== '').map((p) => p.name), + ...context.device.getProperties().filter((p) => p.value != null && p.value !== '').map((p) => p.name), + ].filter((name) => !!name && !name.endsWith('.')) + if (allNames.length > 0) { + return context.services.deviceAction.getParameterValues(context.device, allNames, () => Promise.resolve('')) + } + return '' + } + default: + logger.error(`Unknown command type: ${action}`) + // Mark as failed + commandRecord.status = CommandStatus.FAILED + await CommandModel.updateCommand(commandRecord._id, { status: CommandStatus.FAILED, updatedAt: new Date() }) + this.currentCommandRecord = null + if (this.commandsQueue.length > 0) { + return this.processNextCommand(context) + } + return '' + } + } +} + +class CompletedState extends DeviceState { + name = 'completed' + + async handle(context: DeviceContext): Promise { + // Persist device on completion + try { + const snapshot = context.device.toSnapshot() + await DeviceModel.createOrUpdate({ + identifier: snapshot.identifier, + deviceId: snapshot.deviceId, + properties: snapshot.properties, + parameters: snapshot.parameters, + updatedAt: new Date(), + }) + } catch (e) { + logger.error(`Failed to persist device on completion: ${e}`) + } + return { isComplete: true, response: '' } + } +} + +// ─── Main State Machine ──────────────────────────────────────────────────────── + +export class MainStateMachine { + private currentState: DeviceState + private context: DeviceContext + + constructor(device: Device, session: string, taskQueue: CWMPTaskQueue) { + this.currentState = new IdleState() + this.context = { + device, + session, + taskQueue, + services: { + deviceAction: new DeviceActionExecutor(taskQueue), + }, + } + } + + async start(): Promise { + return this.executeCurrentState() + } + + async handleResponse(success: boolean, message: any): Promise { + this.context.message = { success, data: message } + return this.executeCurrentState() + } + + isCompleted(): boolean { + return this.currentState.name === 'completed' + } + + getCurrentStateName(): string { + return this.currentState.name + } + + private async executeCurrentState(): Promise { + let safetyGuard = 0 + + while (safetyGuard++ < 15) { + try { + logger.debug({ state: this.currentState.name, iteration: safetyGuard }, '[StateMachine] Executing state') + const result = await this.currentState.handle(this.context) + + if (result.nextState) { + logger.debug({ from: this.currentState.name, to: result.nextState.name }, '[StateMachine] State transition') + this.currentState = result.nextState + } + + if (result.isComplete) { + logger.info({ finalState: this.currentState.name }, '[StateMachine] Completed') + return '' + } + + if (result.response && result.response !== '') { + logger.debug({ state: this.currentState.name, responseLength: result.response.length }, '[StateMachine] Sending response to CPE') + return result.response + } + + if (!result.nextState) { + return '' + } + } catch (error) { + logger.error(`Error in state ${this.currentState.name}: ${error}`) + // On error, try to complete gracefully + this.currentState = new CompletedState() + await this.currentState.handle(this.context) + return '' + } + } + + logger.warn('Main state machine safety guard reached') + return '' + } +} diff --git a/app/services/cwmp/state_machine_manager.ts b/app/services/cwmp/state_machine_manager.ts new file mode 100644 index 0000000..93414c5 --- /dev/null +++ b/app/services/cwmp/state_machine_manager.ts @@ -0,0 +1,383 @@ +import { EventEmitter } from 'node:events' +import { Device } from '../../domain/device/device.js' +import { DeviceId } from '../../domain/device/device_id.js' +import { CWMPTaskQueue, ResultStatus } from './task_queue.js' +import { MainStateMachine } from './main_state_machine.js' +import { ImmediateCommandStateMachine } from './immediate_command_state_machine.js' +import { getDeviceHashIdentifier } from '../../utils/device_utils.js' +import { CommandStatus } from '../../domain/commands/command.js' +import { ModeType } from '../../domain/shared/mode.js' +import CommandModel from '../../models/command.js' +import logger from '@adonisjs/core/services/logger' + +export enum MachineStatus { + CREATED = 'created', + RUNNING = 'running', + COMPLETED = 'completed', + FAILED = 'failed', +} + +export interface StateMachineInstance { + id: string + deviceId: DeviceId + session: string + stateMachine: MainStateMachine | ImmediateCommandStateMachine + status: MachineStatus + createdAt: Date + completedAt?: Date + currentState: string + lastTaskId?: string + events?: string[] +} + +/** + * Central state machine manager with TCP-socket-based session tracking, + * device preemption, and CWMP task queue integration. + */ +export class StateMachineManager extends EventEmitter { + private taskQueue: CWMPTaskQueue + private machines: Map = new Map() + private deviceSessions: Map = new Map() // deviceKey -> sessionId + private sessionMachines: Map = new Map() // sessionId -> machineId + private remoteToDevice: Map = new Map() // remoteKey -> deviceKey + private cleanupInterval: ReturnType | null = null + + private static instance: StateMachineManager | null = null + + constructor() { + super() + this.taskQueue = new CWMPTaskQueue() + this.setupTaskQueueListeners() + } + + static getInstance(): StateMachineManager { + if (!StateMachineManager.instance) { + StateMachineManager.instance = new StateMachineManager() + } + return StateMachineManager.instance + } + + getTaskQueue(): CWMPTaskQueue { + return this.taskQueue + } + + startCleanupTimer(intervalMs: number = 60000): void { + if (this.cleanupInterval) return + this.cleanupInterval = setInterval(() => this.cleanupCompletedMachines(), intervalMs) + } + + stopCleanupTimer(): void { + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval) + this.cleanupInterval = null + } + } + + /** + * Start a state machine for a device session + */ + async startStateMachine( + device: Device, + session: string, + opts?: { events?: string[] } + ): Promise { + const deviceKey = JSON.stringify(device.deviceId) + const evts = opts?.events || [] + const lastEvent = this.normalizeEvent(evts.length > 0 ? evts[evts.length - 1] : '') + + // Check for NI commands that force MainStateMachine + let forceMainForNI = false + try { + const hashId = getDeviceHashIdentifier({ + manufacturer: device.deviceId.manufacturer, + oui: device.deviceId.oui, + productClass: device.deviceId.productClass, + serialNumber: device.deviceId.serialNumber, + }) + const collection = CommandModel.getCollection() + const niCount = await collection.countDocuments({ + deviceIdentifier: hashId, + mode: ModeType.NI, + status: { $in: [CommandStatus.PENDING, CommandStatus.WAITING] }, + }) + forceMainForNI = niCount > 0 + } catch (e) { + logger.warn(`Failed to check NI commands: ${e}`) + } + + // Handle preemption of existing machine + const existingSessionId = this.deviceSessions.get(deviceKey) + if (existingSessionId) { + const existingMachineId = this.sessionMachines.get(existingSessionId) + if (existingMachineId) { + const existingMachine = this.machines.get(existingMachineId) + if (existingMachine && existingMachine.status === MachineStatus.RUNNING) { + const isImmediate = existingMachine.stateMachine instanceof ImmediateCommandStateMachine + const onboardingEvent = ['0 BOOTSTRAP', '1 BOOT', '2 PERIODIC', '3 SCHEDULED'].includes(lastEvent) + const shouldPreempt = isImmediate && (forceMainForNI || onboardingEvent) + + if (shouldPreempt) { + // Rebind session if changed + if (existingSessionId !== session) { + this.sessionMachines.delete(existingSessionId) + this.sessionMachines.set(session, existingMachineId) + this.deviceSessions.set(deviceKey, session) + existingMachine.session = session + } + // Replace with MainStateMachine + const newMachine = new MainStateMachine(device, session, this.taskQueue) + ;(existingMachine as any).stateMachine = newMachine + existingMachine.events = evts + await newMachine.start() + existingMachine.currentState = newMachine.getCurrentStateName() + if (newMachine.isCompleted()) this.completeMachine(existingMachine) + return existingMachine + } + + // Rebind session to existing machine + if (existingSessionId !== session) { + this.sessionMachines.delete(existingSessionId) + this.sessionMachines.set(session, existingMachineId) + this.deviceSessions.set(deviceKey, session) + existingMachine.session = session + } + return existingMachine + } + } + } + + // Create new machine + const machineId = `${deviceKey}_${session}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + + let stateMachine: MainStateMachine | ImmediateCommandStateMachine + if (forceMainForNI) { + stateMachine = new MainStateMachine(device, session, this.taskQueue) + } else if (lastEvent === '6 CONNECTION REQUEST') { + stateMachine = new ImmediateCommandStateMachine(device, session, this.taskQueue) + } else if (['0 BOOTSTRAP', '1 BOOT', '2 PERIODIC', '3 SCHEDULED'].includes(lastEvent) || !lastEvent) { + stateMachine = new MainStateMachine(device, session, this.taskQueue) + } else { + // Unknown event - use MainStateMachine as default + stateMachine = new MainStateMachine(device, session, this.taskQueue) + } + + const machineInstance: StateMachineInstance = { + id: machineId, + deviceId: device.deviceId, + session, + stateMachine, + status: MachineStatus.CREATED, + createdAt: new Date(), + currentState: stateMachine.getCurrentStateName(), + events: evts, + } + + this.machines.set(machineId, machineInstance) + this.deviceSessions.set(deviceKey, session) + this.sessionMachines.set(session, machineId) + + // Start the machine + try { + machineInstance.status = MachineStatus.RUNNING + await stateMachine.start() + machineInstance.currentState = stateMachine.getCurrentStateName() + + if (stateMachine.isCompleted()) { + this.completeMachine(machineInstance) + } + } catch (error) { + machineInstance.status = MachineStatus.FAILED + machineInstance.completedAt = new Date() + logger.error(`Failed to start state machine: ${error}`) + } + + return machineInstance + } + + /** + * Bind a remote endpoint to a device for session rebinding + */ + bindRemoteToDevice(remoteKey: string, deviceId: DeviceId): void { + this.remoteToDevice.set(remoteKey, JSON.stringify(deviceId)) + } + + /** + * Ensure session is bound when TCP connection changes + */ + ensureSessionForRemote(remoteKey: string, newSession: string): boolean { + const deviceKey = this.remoteToDevice.get(remoteKey) + if (!deviceKey) return false + + const existingSession = this.deviceSessions.get(deviceKey) + if (!existingSession) return false + + const machineId = this.sessionMachines.get(existingSession) + if (!machineId) return false + + const machine = this.machines.get(machineId) + if (!machine) return false + + if (existingSession !== newSession) { + this.sessionMachines.delete(existingSession) + this.sessionMachines.set(newSession, machineId) + this.deviceSessions.set(deviceKey, newSession) + machine.session = newSession + } + + return true + } + + /** + * Continue when CPE sends empty body + */ + async continue(session: string): Promise { + const machineId = this.sessionMachines.get(session) + if (!machineId) return '' + + const machineInstance = this.machines.get(machineId) + if (!machineInstance || machineInstance.status !== MachineStatus.RUNNING) return '' + + try { + // Serve queued tasks first + const pendingTask = this.taskQueue.getTaskForDevice(machineInstance.deviceId) + if (pendingTask) { + const builder = CWMPTaskQueue.task_map[pendingTask.task] + if (typeof builder === 'function') { + const envelope = builder(pendingTask.props) + if (pendingTask.id) machineInstance.lastTaskId = pendingTask.id + return envelope + } + } + + // If a task is in-flight, wait + if (machineInstance.lastTaskId) return '' + + // Let the state machine continue + const response = await machineInstance.stateMachine.handleResponse(true, { isFirst: false }) + machineInstance.currentState = machineInstance.stateMachine.getCurrentStateName() + + if (machineInstance.stateMachine.isCompleted()) { + this.completeMachine(machineInstance) + return '' + } + + return response + } catch (error) { + logger.error(`Error continuing session ${session}: ${error}`) + this.failMachine(machineInstance, String(error)) + return '' + } + } + + /** + * Handle a CWMP response message + */ + async handleCWMPResponse(session: string, success: boolean, message: any): Promise { + const machineId = this.sessionMachines.get(session) + if (!machineId) return '' + + const machineInstance = this.machines.get(machineId) + if (!machineInstance || machineInstance.status !== MachineStatus.RUNNING) return '' + + try { + const responseTaskId = machineInstance.lastTaskId + const response = await machineInstance.stateMachine.handleResponse(success, message) + + // Execute task callback + if (responseTaskId) { + if (success) { + this.taskQueue.completeTask(responseTaskId, message) + } else { + this.taskQueue.failTask(responseTaskId, JSON.stringify(message)) + } + try { + await this.taskQueue.executeTaskCallback( + responseTaskId, + success ? ResultStatus.Success : ResultStatus.Error, + message + ) + } catch (cbErr) { + logger.error(`Task callback error: ${cbErr}`) + } + if (machineInstance.lastTaskId === responseTaskId) { + machineInstance.lastTaskId = undefined + } + } + + machineInstance.currentState = machineInstance.stateMachine.getCurrentStateName() + + if (machineInstance.stateMachine.isCompleted()) { + this.completeMachine(machineInstance) + return '' + } + + return response + } catch (error) { + logger.error(`Error handling CWMP response: ${error}`) + this.failMachine(machineInstance, String(error)) + return '' + } + } + + hasSession(session: string): boolean { + return this.sessionMachines.has(session) + } + + /** + * Enqueue an immediate task for a device (e.g., after Inform) + */ + async enqueueImmediate(device: Device, type: string, payload: any): Promise { + this.taskQueue.addCWMPTask(device.deviceId, type, payload) + } + + private completeMachine(machine: StateMachineInstance): void { + machine.status = MachineStatus.COMPLETED + machine.completedAt = new Date() + const deviceKey = JSON.stringify(machine.deviceId) + this.deviceSessions.delete(deviceKey) + this.sessionMachines.delete(machine.session) + } + + private failMachine(machine: StateMachineInstance, error: string): void { + machine.status = MachineStatus.FAILED + machine.completedAt = new Date() + const deviceKey = JSON.stringify(machine.deviceId) + this.deviceSessions.delete(deviceKey) + this.sessionMachines.delete(machine.session) + logger.error(`State machine failed: ${error}`) + } + + private normalizeEvent(evt: string): string { + return (evt || '').toString().trim().replace(/\s+/g, ' ').toUpperCase() + } + + cleanupCompletedMachines(olderThanHours: number = 1): void { + const cutoff = new Date(Date.now() - olderThanHours * 60 * 60 * 1000) + for (const [id, machine] of this.machines.entries()) { + if ((machine.status === MachineStatus.COMPLETED || machine.status === MachineStatus.FAILED) && + machine.completedAt && machine.completedAt < cutoff) { + this.machines.delete(id) + } + } + } + + async shutdown(): Promise { + this.stopCleanupTimer() + this.cleanupCompletedMachines(0) + } + + private setupTaskQueueListeners(): void { + this.taskQueue.on('taskSent', (task: any) => { + // Find the machine for this device and set lastTaskId + const deviceKey = JSON.stringify(task.deviceId) + const sessionId = this.deviceSessions.get(deviceKey) + if (sessionId) { + const machineId = this.sessionMachines.get(sessionId) + if (machineId) { + const machine = this.machines.get(machineId) + if (machine) machine.lastTaskId = task.id + } + } + }) + } +} diff --git a/app/services/cwmp/task_queue.ts b/app/services/cwmp/task_queue.ts new file mode 100644 index 0000000..4a8495d --- /dev/null +++ b/app/services/cwmp/task_queue.ts @@ -0,0 +1,250 @@ +import { DeviceId } from '../../domain/device/device_id.js' +import { EventEmitter } from 'node:events' +import { CWMPMessageBuilder } from '../../protocol/common/cwmp_message_builder.js' +import logger from '@adonisjs/core/services/logger' + +export class TaskResult { + status!: ResultStatus + data: any + timestamp!: Date +} + +export enum ResultStatus { + Success = 'success', + Error = 'error', +} + +export interface CWMPTask { + id: string + deviceId: DeviceId + type: string + payload: any + status: TaskStatus + createdAt: Date + completedAt?: Date + result?: any + error?: string +} + +export enum TaskStatus { + PENDING = 'pending', + SENT = 'sent', + COMPLETED = 'completed', + FAILED = 'failed', +} + +/** + * CWMP Task Queue with event-based lifecycle + * Maps tasks to CWMP message builders and manages callbacks + */ +export class CWMPTaskQueue extends EventEmitter { + static task_map: Record string> = { + get_parameter_names: (parameterPath: string) => { + return CWMPMessageBuilder.getParameterNames(undefined, parameterPath, true) + }, + get_parameter_attributes: (parameterNames: string[]) => { + return CWMPMessageBuilder.getParameterAttributes(undefined, parameterNames) + }, + get_parameter_values: (parameterNames: string[]) => { + return CWMPMessageBuilder.getParameterValues(undefined, parameterNames) + }, + set_parameter_values: (props: { parameterValueList: any[]; parameterKey?: string }) => { + const params = (props.parameterValueList || []).map((p: any) => ({ + name: p.Name, + value: String(p.Value?._ ?? ''), + type: p.Value?.$?.Type || 'xsd:string', + })) + return CWMPMessageBuilder.setParameterValues(undefined, params) + }, + set_parameter_attributes: (attrs: any[]) => { + const mapped = attrs.map((a: any) => ({ + name: a.name || a.Name, + notificationChange: !!(a.notificationChange ?? a.NotificationChange), + notification: a.notification ?? a.Notification ?? 0, + accessListChange: !!(a.accessListChange ?? a.AccessListChange), + accessList: a.accessList ?? a.AccessList ?? [], + })) + return CWMPMessageBuilder.setParameterAttributes(undefined, mapped) + }, + schedule_inform: (props: { delayInSeconds: number; commandKey?: string }) => { + return CWMPMessageBuilder.scheduleInform(undefined, props.delayInSeconds || 300, props.commandKey) + }, + download: (props: any) => { + return CWMPMessageBuilder.download(undefined, props) + }, + upload: (props: any) => { + return CWMPMessageBuilder.upload(undefined, props) + }, + factory_reset: () => { + return CWMPMessageBuilder.factoryReset(undefined) + }, + reboot: (props: any) => { + return CWMPMessageBuilder.reboot(undefined, props?.commandKey) + }, + } + + private tasks: Map = new Map() + private deviceQueues: Map = new Map() + private taskCallbacks: Map Promise> = new Map() + + /** + * Adds a task with callback (legacy-compatible API used by DeviceActionExecutor) + * Returns the CWMP XML envelope to send to the device + */ + addTask( + deviceId: DeviceId, + taskType: string, + props: any, + callback: (result: TaskResult) => Promise + ): string { + const cwmpTask = this.addCWMPTask(deviceId, taskType, props) + + if (callback) { + this.taskCallbacks.set(cwmpTask.id, callback) + } + + // Mark as SENT immediately so the manager correlates the response + if (cwmpTask.status === TaskStatus.PENDING) { + cwmpTask.status = TaskStatus.SENT + this.emit('taskSent', cwmpTask) + } + + // Generate CWMP envelope + const builder = CWMPTaskQueue.task_map[taskType] + if (typeof builder !== 'function') { + logger.warn(`No CWMP builder found for task '${taskType}'`) + return '' + } + return builder(props) + } + + /** + * Registers a callback for a task already in the queue (e.g. added via addCWMPTask) + */ + registerCallback(taskId: string, callback: (result: TaskResult) => Promise): void { + this.taskCallbacks.set(taskId, callback) + } + + /** + * Adds a CWMP task (internal) + */ + addCWMPTask(deviceId: DeviceId, type: string, payload: any): CWMPTask { + const taskId = this.generateTaskId(deviceId, type) + const task: CWMPTask = { + id: taskId, + deviceId, + type, + payload, + status: TaskStatus.PENDING, + createdAt: new Date(), + } + + this.tasks.set(taskId, task) + + const deviceKey = JSON.stringify(deviceId) + if (!this.deviceQueues.has(deviceKey)) { + this.deviceQueues.set(deviceKey, []) + } + this.deviceQueues.get(deviceKey)!.push(taskId) + + this.emit('taskAdded', task) + return task + } + + /** + * Retrieve and mark a pending task as SENT for a device + */ + getTaskForDevice(deviceId: DeviceId): { id: string; task: string; props: any } | undefined { + const deviceKey = JSON.stringify(deviceId) + const queue = this.deviceQueues.get(deviceKey) + if (!queue || queue.length === 0) return undefined + + const taskId = queue[0] + const cwmpTask = this.tasks.get(taskId) + if (!cwmpTask) return undefined + + // Dequeue + queue.shift() + + if (cwmpTask.status === TaskStatus.PENDING) { + cwmpTask.status = TaskStatus.SENT + this.emit('taskSent', cwmpTask) + } + + return { id: taskId, task: cwmpTask.type, props: cwmpTask.payload } + } + + completeTask(taskId: string, result: any): void { + const task = this.tasks.get(taskId) + if (!task) return + + task.status = TaskStatus.COMPLETED + task.result = result + task.completedAt = new Date() + + this.removeTaskFromQueue(task.deviceId, taskId) + this.emit('taskCompleted', task) + } + + failTask(taskId: string, error: string): void { + const task = this.tasks.get(taskId) + if (!task) return + + task.status = TaskStatus.FAILED + task.error = error + task.completedAt = new Date() + + this.removeTaskFromQueue(task.deviceId, taskId) + this.emit('taskFailed', task) + } + + async executeTaskCallback(taskId: string, status: ResultStatus, data: any): Promise { + const callback = this.taskCallbacks.get(taskId) + if (callback) { + const result = new TaskResult() + result.status = status + result.data = data + result.timestamp = new Date() + + try { + const response = await callback(result) + this.taskCallbacks.delete(taskId) + return response + } catch (error) { + logger.error(`Error executing task callback: ${error}`) + this.taskCallbacks.delete(taskId) + return '' + } + } + return '' + } + + clearPendingTasksForDevice(deviceId: DeviceId): void { + const deviceKey = JSON.stringify(deviceId) + const queue = this.deviceQueues.get(deviceKey) || [] + const keep: string[] = [] + for (const taskId of queue) { + const t = this.tasks.get(taskId) + if (t && t.status === TaskStatus.SENT) { + keep.push(taskId) + continue + } + this.tasks.delete(taskId) + this.taskCallbacks.delete(taskId) + } + this.deviceQueues.set(deviceKey, keep) + } + + private generateTaskId(deviceId: DeviceId, type: string): string { + return `${JSON.stringify(deviceId)}_${type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + } + + private removeTaskFromQueue(deviceId: DeviceId, taskId: string): void { + const deviceKey = JSON.stringify(deviceId) + const queue = this.deviceQueues.get(deviceKey) + if (queue) { + const index = queue.indexOf(taskId) + if (index > -1) queue.splice(index, 1) + } + } +} diff --git a/app/services/device_service.ts b/app/services/device_service.ts new file mode 100644 index 0000000..c376aa5 --- /dev/null +++ b/app/services/device_service.ts @@ -0,0 +1,197 @@ +import DeviceModel from '#models/device' +import CommandModel from '#models/command' +import { DeviceRecord } from '../domain/device/device_record.js' +import { CommandStatus, CommandType, Priority } from '../domain/commands/command.js' +import { ModeType } from '../domain/shared/mode.js' +import logger from '@adonisjs/core/services/logger' + +export interface PaginationData { + first?: { offset: number; limit: number } + previous?: { offset: number; limit: number } + next?: { offset: number; limit: number } + last?: { offset: number; limit: number } +} + +export default class DeviceService { + async getDevices(offset: number = 0, limit: number = 100) { + const { data, total } = await DeviceModel.paginateDevices(offset, limit) + const pager = this.calculatePagination(offset, limit, total) + return { data, nbDocuments: total, pager } + } + + async getDeviceByIdentifier(identifier: string): Promise { + return DeviceModel.findByIdentifier(identifier) + } + + async deleteByIdentifier(identifier: string): Promise { + return DeviceModel.deleteOne({ identifier }) + } + + /** + * Execute a command on a device. Mode I triggers a Connection Request. + */ + async executeCommand(identifier: string, commandPayload: { + action: CommandType + parameters?: Record + mode?: string + priority?: number + }): Promise { + const device = await DeviceModel.findByIdentifier(identifier) + if (!device) { + throw new Error(`Device not found: ${identifier}`) + } + + const mode = (commandPayload.mode || 'NI').toUpperCase() === 'I' ? ModeType.I : ModeType.NI + const priority = commandPayload.priority ?? Priority.NORMAL + + // Check for duplicate pending command with same mode + priority + action + const collection = CommandModel.getCollection() + const existingDup = await collection.findOne({ + deviceIdentifier: identifier, + mode, + status: CommandStatus.PENDING, + action: commandPayload.action, + priority, + }) + if (existingDup) { + throw new Error(`Duplicate pending command: ${commandPayload.action} in mode ${mode}`) + } + + // Create the command + await CommandModel.createCommand({ + deviceIdentifier: identifier, + action: commandPayload.action, + parameters: commandPayload.parameters || {}, + mode, + status: CommandStatus.PENDING, + priority, + createdAt: new Date(), + updatedAt: new Date(), + }) + + // For mode I, trigger Connection Request to wake the device + if (mode === ModeType.I) { + await this.applyImmediateCommandForDevice(device, identifier) + } + } + + /** + * Refresh a device by sending a Connection Request for REFRESH command + */ + async refreshDevice(identifier: string): Promise { + const device = await DeviceModel.findByIdentifier(identifier) + if (!device) { + throw new Error(`Device not found: ${identifier}`) + } + + await CommandModel.createCommand({ + deviceIdentifier: identifier, + action: CommandType.REFRESH, + parameters: {}, + mode: ModeType.I, + status: CommandStatus.PENDING, + priority: Priority.NORMAL, + createdAt: new Date(), + updatedAt: new Date(), + }) + + await this.applyImmediateCommandForDevice(device, identifier) + } + + /** + * Send Connection Request to CPE to wake it up for immediate command execution + */ + private async applyImmediateCommandForDevice(device: DeviceRecord, identifier: string): Promise { + const connectionRequestParam = (device.properties || []).find((p: any) => + p.name?.includes('ConnectionRequestURL') + ) + + if (!connectionRequestParam || !connectionRequestParam.value) { + logger.error({ identifier, availableProps: (device.properties || []).map((p: any) => p.name) }, 'ConnectionRequestURL not found in device properties') + return + } + + let url: URL + try { + url = new URL(connectionRequestParam.value as string) + } catch { + logger.error(`Invalid ConnectionRequestURL: ${connectionRequestParam.value}`) + return + } + + const wakeSuccess = await this.sendWakeOnWlan(url, device) + + // Mark pending I commands as WAITING (or FAILED) + const collection = CommandModel.getCollection() + const pendingCmds = await collection.find({ + deviceIdentifier: identifier, + mode: ModeType.I, + status: CommandStatus.PENDING, + }).toArray() + + for (const cmd of pendingCmds) { + await collection.updateOne( + { _id: cmd._id }, + { $set: { status: wakeSuccess ? CommandStatus.WAITING : CommandStatus.PENDING, updatedAt: new Date() } } + ) + } + } + + private async sendWakeOnWlan(connectionRequestURL: URL, device: DeviceRecord): Promise { + try { + // Use ConnectionRequest credentials from device properties/parameters + // Fallback chain: ConnectionRequestUsername > ManagementServer.Username > OUI-ProductClass-Serial + const allParams = [...(device.properties || []), ...(device.parameters || [])] + + const connReqUsername = allParams.find((p: any) => p.name?.includes('ConnectionRequestUsername')) + const connReqPassword = allParams.find((p: any) => p.name?.includes('ConnectionRequestPassword')) + const mgmtUsername = allParams.find((p: any) => p.name?.endsWith('ManagementServer.Username')) + const mgmtPassword = allParams.find((p: any) => p.name?.endsWith('ManagementServer.Password')) + + // genieacs-sim uses ManagementServer.Username/Password for CR auth (non-standard) + // Real CPEs use ConnectionRequestUsername/Password + const username = mgmtUsername?.value || connReqUsername?.value || `${device.deviceId.oui}-${device.deviceId.productClass}-${device.deviceId.serialNumber}` + const password = mgmtPassword?.value ?? connReqPassword?.value ?? '' + + logger.info({ url: connectionRequestURL.toString(), username }, 'Sending Connection Request') + + const basicAuth = Buffer.from(`${username}:${password}`).toString('base64') + const response = await fetch(connectionRequestURL.toString(), { + method: 'GET', + headers: { + Authorization: `Basic ${basicAuth}`, + }, + }) + + if (!response.ok) { + logger.error({ status: response.status, statusText: response.statusText, url: connectionRequestURL.toString() }, 'Connection Request failed with non-OK status') + return false + } + logger.info({ status: response.status, url: connectionRequestURL.toString() }, 'Connection Request successful') + return true + } catch (error: any) { + logger.error({ error: error?.message, code: error?.cause?.code, url: connectionRequestURL.toString() }, 'Connection Request network error') + return false + } + } + + calculatePagination(offset: number, limit: number, total: number): PaginationData { + const pagination: PaginationData = { first: { offset: 0, limit } } + + if (total >= limit) { + pagination.last = { offset: total - limit, limit } + } else { + pagination.last = { offset: 0, limit } + } + + if (offset > 0) { + pagination.previous = { offset: Math.max(0, offset - limit), limit } + } + + if (offset + limit < total) { + pagination.next = { offset: offset + limit, limit } + } + + return pagination + } +} diff --git a/app/services/firmware_catalog_service.ts b/app/services/firmware_catalog_service.ts new file mode 100644 index 0000000..101b2fc --- /dev/null +++ b/app/services/firmware_catalog_service.ts @@ -0,0 +1,225 @@ +import FirmwareEntryModel from '#models/firmware_entry' +import MigrationRouteModel from '#models/migration_route' +import { FirmwareEntry, MigrationRoute } from '../domain/firmware/firmware_catalog.js' +import { MigrationStep } from '../domain/firmware/migration_step.js' +import { matchGlob } from '../utils/glob_match.js' +import { BadRequestException } from '../exceptions/index.js' + +export interface CreateFirmwareEntryDto { + identifier: string + productClass: string + version: string + downloadUrl: string + checksum: string + checksumType: 'md5' | 'sha256' + fileSize: number +} + +export interface CreateMigrationRouteDto { + identifier: string + productClass: string + sourceVersionPattern: string + targetVersion: string + downloadUrl: string + priority: number +} + +export default class FirmwareCatalogService { + /** + * Validate that a string is a valid URL. + */ + private validateUrl(url: string, fieldName: string): void { + try { + new URL(url) + } catch { + throw new BadRequestException(`${fieldName} must be a valid URL`) + } + } + + /** + * Validate that a number is a positive integer (> 0). + */ + private validatePositiveInteger(value: number, fieldName: string): void { + if (!Number.isInteger(value) || value <= 0) { + throw new BadRequestException(`${fieldName} must be a positive integer`) + } + } + + /** + * Validate that a string is non-empty. + */ + private validateNonEmptyString(value: string, fieldName: string): void { + if (!value || value.trim().length === 0) { + throw new BadRequestException(`${fieldName} must be a non-empty string`) + } + } + + /** + * Validate checksumType is 'md5' or 'sha256'. + */ + private validateChecksumType(value: string, fieldName: string): void { + if (value !== 'md5' && value !== 'sha256') { + throw new BadRequestException(`${fieldName} must be 'md5' or 'sha256'`) + } + } + + /** + * Create a new firmware entry after validation. + */ + async createEntry(data: CreateFirmwareEntryDto): Promise { + this.validateNonEmptyString(data.identifier, 'identifier') + this.validateUrl(data.downloadUrl, 'downloadUrl') + this.validatePositiveInteger(data.fileSize, 'fileSize') + this.validateChecksumType(data.checksumType, 'checksumType') + + return FirmwareEntryModel.createIfNotExists({ + identifier: data.identifier, + productClass: data.productClass, + version: data.version, + downloadUrl: data.downloadUrl, + checksum: data.checksum, + checksumType: data.checksumType, + fileSize: data.fileSize, + }) + } + + /** + * Find firmware entries, optionally filtered by productClass. + */ + async findEntries(productClass?: string): Promise { + if (productClass) { + return FirmwareEntryModel.findByProductClass(productClass) + } + return FirmwareEntryModel.find({}) as unknown as Promise + } + + /** + * Delete a firmware entry by identifier. + */ + async deleteEntry(identifier: string): Promise { + const deleted = await FirmwareEntryModel.deleteByIdentifier(identifier) + if (!deleted) { + throw new BadRequestException(`Firmware entry '${identifier}' not found`) + } + } + + /** + * Create a new migration route after validation. + */ + async createMigrationRoute(data: CreateMigrationRouteDto): Promise { + this.validateNonEmptyString(data.identifier, 'identifier') + this.validateUrl(data.downloadUrl, 'downloadUrl') + this.validatePositiveInteger(data.priority, 'priority') + + return MigrationRouteModel.createIfNotExists({ + identifier: data.identifier, + productClass: data.productClass, + sourceVersionPattern: data.sourceVersionPattern, + targetVersion: data.targetVersion, + downloadUrl: data.downloadUrl, + priority: data.priority, + }) + } + + async findAllRoutes(): Promise { + return MigrationRouteModel.find({}) as unknown as Promise + } + + async updateEntry(identifier: string, data: Partial): Promise { + const collection = FirmwareEntryModel.getCollection() + const result = await collection.findOneAndUpdate( + { identifier }, + { $set: data }, + { returnDocument: 'after' } + ) + if (!result) throw new BadRequestException(`Firmware entry '${identifier}' not found`) + return result as unknown as FirmwareEntry + } + + /** + * Find migration routes for a productClass, sorted by priority descending. + */ + async findMigrationRoutes(productClass: string): Promise { + return MigrationRouteModel.findByProductClass(productClass, { priority: -1 }) + } + + /** + * Delete a migration route by identifier. + */ + async deleteMigrationRoute(identifier: string): Promise { + const deleted = await MigrationRouteModel.deleteByIdentifier(identifier) + if (!deleted) { + throw new BadRequestException(`Migration route '${identifier}' not found`) + } + } + + /** + * Resolve the firmware upgrade path from currentVersion to targetVersion. + * Uses a greedy best-first algorithm based on route priority with cycle detection. + * + * @returns MigrationStep[] ordered from first to last, or [] if no path exists + */ + async resolveUpgradePath( + productClass: string, + currentVersion: string, + targetVersion: string + ): Promise { + // Trivial case: already at target version + if (currentVersion === targetVersion) return [] + + // Load all routes for this productClass + const routes = await MigrationRouteModel.findByProductClass(productClass) + + const path: MigrationStep[] = [] + const visited = new Set() + let version = currentVersion + + visited.add(version) + + while (version !== targetVersion) { + // Find all applicable routes from the current version + const applicableRoutes = routes + .filter((r) => matchGlob(r.sourceVersionPattern, version)) + .sort((a, b) => b.priority - a.priority) // Highest priority first + + // Take the best route (highest priority) + const bestRoute = applicableRoutes[0] + if (!bestRoute) { + // No route found → no path exists + return [] + } + + // Cycle detection + if (visited.has(bestRoute.targetVersion)) { + return [] + } + + // Add step to path + path.push({ + fromVersion: version, + toVersion: bestRoute.targetVersion, + downloadUrl: bestRoute.downloadUrl, + fileSize: 0, + checksum: '', + }) + + // Advance + visited.add(bestRoute.targetVersion) + version = bestRoute.targetVersion + } + + // Enrich each step with fileSize and checksum from firmware_entries + for (const step of path) { + const entry = await FirmwareEntryModel.findByProductClassAndVersion( + productClass, + step.toVersion + ) + if (entry) { + step.fileSize = entry.fileSize + step.checksum = entry.checksum + } + } + + return path + } +} diff --git a/app/services/migration_service.ts b/app/services/migration_service.ts new file mode 100644 index 0000000..332f88f --- /dev/null +++ b/app/services/migration_service.ts @@ -0,0 +1,297 @@ +import ConfigurationService from './configuration_service.js' +import FirmwareCatalogService from './firmware_catalog_service.js' +import type { CreateConfigurationDto } from './configuration_service.js' +import type { CreateFirmwareEntryDto } from './firmware_catalog_service.js' +import { Db } from 'mongodb' + +interface MigrationReport { + discoveryMigrated: number + discoverySkipped: number + provisioningMigrated: number + provisioningSkipped: number + firmwareEntriesCreated: number + errors: string[] +} + +interface RollbackReport { + discoveryRolledBack: number + provisioningRolledBack: number + errors: string[] +} + +export default class MigrationService { + constructor( + private db: Db, + private configService = new ConfigurationService(), + private firmwareCatalogService = new FirmwareCatalogService(), + ) {} + + /** + * Migrate all legacy Discovery and Provisioning configs to unified Configuration format + */ + async migrateAll(): Promise { + const report: MigrationReport = { + discoveryMigrated: 0, + discoverySkipped: 0, + provisioningMigrated: 0, + provisioningSkipped: 0, + firmwareEntriesCreated: 0, + errors: [], + } + + try { + // Migrate Discovery configs (inventory + maintain policy) + const discoveryCollection = this.db.collection('discovery_configs') + const discoveryConfigs = await discoveryCollection.find({}).toArray() + + for (const legacy of discoveryConfigs) { + try { + const migrated = await this.migrateDiscoveryConfig(legacy) + if (migrated) { + report.discoveryMigrated++ + } else { + report.discoverySkipped++ + } + } catch (error) { + report.discoverySkipped++ + report.errors.push(`Discovery ${legacy.identifier}: ${(error as Error).message}`) + } + } + + // Migrate Provisioning configs (desired parameters + once/maintain + firmware) + const provisioningCollection = this.db.collection('provisioning_configs') + const provisioningConfigs = await provisioningCollection.find({}).toArray() + + for (const legacy of provisioningConfigs) { + try { + const migrated = await this.migrateProvisioningConfig(legacy) + if (migrated) { + report.provisioningMigrated++ + } else { + report.provisioningSkipped++ + } + + // Extract firmware Downloads into FirmwareCatalog + if (legacy.downloadActions && legacy.downloadActions.length > 0) { + for (const action of legacy.downloadActions) { + try { + await this.extractFirmwareEntry(action, legacy) + report.firmwareEntriesCreated++ + } catch (error) { + report.errors.push(`Firmware extraction for ${legacy.identifier}: ${(error as Error).message}`) + } + } + } + } catch (error) { + report.provisioningSkipped++ + report.errors.push(`Provisioning ${legacy.identifier}: ${(error as Error).message}`) + } + } + } catch (error) { + report.errors.push(`Migration error: ${(error as Error).message}`) + } + + return report + } + + /** + * Migrate a single Discovery config to Configuration + * Discovery = inventory paths + "maintain" policy + */ + private async migrateDiscoveryConfig(legacy: any): Promise { + const criteria = this.buildCriteriaFromLegacy(legacy) + + const dto: CreateConfigurationDto = { + identifier: `legacy-disc-${legacy.identifier}`, + name: `[Migrated] ${legacy.name || legacy.identifier}`, + version: '1.0.0', + priority: 100, + policy: 'maintain', + enabled: true, + tags: ['legacy', 'discovery'], + criteria, + inventory: (legacy.modelPaths || []).map((p: string) => ({ path: p })), + desiredParameters: [], + } + + // Check if already exists (skip if yes) + const existing = await this.configService.findById(dto.identifier) + if (existing) { + return false + } + + await this.configService.create(dto) + return true + } + + /** + * Migrate a single Provisioning config to Configuration + * Provisioning = desired parameters + (once OR maintain) policy + firmware migration + */ + private async migrateProvisioningConfig(legacy: any): Promise { + const criteria = this.buildCriteriaFromLegacy(legacy) + const policy = legacy.downloadActions?.length > 0 ? 'once' : 'maintain' + const desiredParameters = this.buildDesiredParametersFromLegacy(legacy) + + // Build inventory paths from desired parameters (auto-suggest) + const inventoryPaths = new Set() + for (const param of desiredParameters) { + const parts = param.name.split('.') + parts.pop() + inventoryPaths.add(parts.join('.') + '.') + } + + const dto: CreateConfigurationDto = { + identifier: `legacy-prov-${legacy.identifier}`, + name: `[Migrated] ${legacy.name || legacy.identifier}`, + version: '1.0.0', + priority: 100, + policy, + enabled: true, + tags: ['legacy', 'provisioning'], + criteria, + inventory: [...inventoryPaths].map((p) => ({ path: p })), + desiredParameters, + } + + const existing = await this.configService.findById(dto.identifier) + if (existing) { + return false + } + + await this.configService.create(dto) + return true + } + + /** + * Build criteria object from legacy config device filters + */ + private buildCriteriaFromLegacy(legacy: any): Record { + const criteria: Record = {} + + if (legacy.manufacturer) criteria.manufacturer = legacy.manufacturer + if (legacy.oui) criteria.oui = legacy.oui + if (legacy.productClass) criteria.productClass = legacy.productClass + if (legacy.serialNumber) criteria.serialNumber = legacy.serialNumber + + return criteria + } + + /** + * Build desired parameters from legacy Set Parameter actions + */ + private buildDesiredParametersFromLegacy(legacy: any): Array<{ name: string; value: string | number | boolean; type: 'string' | 'int' | 'unsignedInt' | 'boolean' | 'dateTime' }> { + const params: Array<{ name: string; value: string | number | boolean; type: 'string' | 'int' | 'unsignedInt' | 'boolean' | 'dateTime' }> = [] + + if (!legacy.actions) return params + + for (const action of legacy.actions) { + if (action.type === 'SET_PARAMETER' && action.parameter && action.value !== undefined) { + params.push({ + name: action.parameter, + value: action.value, + type: typeof action.value === 'boolean' ? 'boolean' : typeof action.value === 'number' ? 'int' : 'string', + }) + } + } + + return params + } + + /** + * Extract firmware Download action into FirmwareCatalog entry + */ + private async extractFirmwareEntry(action: any, legacy: any): Promise { + const entry: CreateFirmwareEntryDto = { + identifier: `legacy-fw-${legacy.productClass || 'device'}-${action.version || '1.0.0'}`, + productClass: legacy.productClass || 'Device', + version: action.version || '1.0.0', + downloadUrl: action.url || action.firmwareUrl || '', + checksum: action.checksum || '', + checksumType: 'md5', + fileSize: action.fileSize || 0, + } + + try { + await this.firmwareCatalogService.createEntry(entry) + } catch { + // Entry may already exist — skip silently + } + } + + /** + * Rollback migration: reconstruct legacy collections from prefixed configurations + */ + async rollback(): Promise { + const report: RollbackReport = { + discoveryRolledBack: 0, + provisioningRolledBack: 0, + errors: [], + } + + try { + const result = await this.configService.findAll({ perPage: 1000 }) + const allConfigs = result.data + + const discoveryCollection = this.db.collection('discovery_configs') + const provisioningCollection = this.db.collection('provisioning_configs') + + for (const config of allConfigs) { + try { + if (config.identifier.startsWith('legacy-disc-')) { + const originalId = config.identifier.replace('legacy-disc-', '') + const legacy = this.buildLegacyDiscoveryFromConfig(config, originalId) + await discoveryCollection.insertOne(legacy) + report.discoveryRolledBack++ + } else if (config.identifier.startsWith('legacy-prov-')) { + const originalId = config.identifier.replace('legacy-prov-', '') + const legacy = this.buildLegacyProvisioningFromConfig(config, originalId) + await provisioningCollection.insertOne(legacy) + report.provisioningRolledBack++ + } + } catch (error) { + report.errors.push(`Rollback ${config.identifier}: ${(error as Error).message}`) + } + } + } catch (error) { + report.errors.push(`Rollback error: ${(error as Error).message}`) + } + + return report + } + + private buildLegacyDiscoveryFromConfig(config: any, originalId: string): any { + return { + identifier: originalId, + name: config.name.replace('[Migrated] ', ''), + manufacturer: config.criteria?.manufacturer, + oui: config.criteria?.oui, + productClass: config.criteria?.productClass, + serialNumber: config.criteria?.serialNumber, + modelPaths: (config.inventory || []).map((inv: any) => inv.path), + createdAt: config.createdAt, + updatedAt: config.updatedAt, + } + } + + private buildLegacyProvisioningFromConfig(config: any, originalId: string): any { + const actions = config.desiredParameters?.map((param: any) => ({ + type: 'SET_PARAMETER', + parameter: param.name, + value: param.value, + })) || [] + + return { + identifier: originalId, + name: config.name.replace('[Migrated] ', ''), + manufacturer: config.criteria?.manufacturer, + oui: config.criteria?.oui, + productClass: config.criteria?.productClass, + serialNumber: config.criteria?.serialNumber, + actions, + downloadActions: [], + createdAt: config.createdAt, + updatedAt: config.updatedAt, + } + } +} diff --git a/app/services/telemetry.ts b/app/services/telemetry.ts new file mode 100644 index 0000000..a70e031 --- /dev/null +++ b/app/services/telemetry.ts @@ -0,0 +1,26 @@ +import env from '#start/env' + +let initialized = false + +export function initTelemetry() { + if (initialized) return + initialized = true + + const exporterUri = env.get('OTEL_EXPORTER_URI') + if (!exporterUri) return + + // OpenTelemetry setup is optional - only initialize when available + import('@opentelemetry/sdk-node') + .then(({ NodeSDK }) => { + return import('@opentelemetry/auto-instrumentations-node').then(({ getNodeAutoInstrumentations }) => { + const sdk = new NodeSDK({ + serviceName: env.get('OTEL_SERVICE_NAME', 'papaye-v2'), + instrumentations: [getNodeAutoInstrumentations()], + }) + sdk.start() + }) + }) + .catch(() => { + // OpenTelemetry not available, skip + }) +} diff --git a/app/types.ts b/app/types.ts new file mode 100644 index 0000000..b636264 --- /dev/null +++ b/app/types.ts @@ -0,0 +1 @@ +import '@adonisjs/inertia/types' diff --git a/app/utils/device_utils.ts b/app/utils/device_utils.ts new file mode 100644 index 0000000..8e1399b --- /dev/null +++ b/app/utils/device_utils.ts @@ -0,0 +1,15 @@ +import { createHash } from 'node:crypto' + +/** + * Generates a deterministic hex identifier for a device (URL-safe, 24 chars) + */ +export function getDeviceHashIdentifier(device: { + oui: string + productClass: string + serialNumber: string + manufacturer?: string +}): string { + const manufacturer = device.manufacturer ?? '' + const canonical = `${manufacturer}-${device.oui}-${device.productClass}-${device.serialNumber}` + return createHash('sha256').update(canonical).digest('hex').substring(0, 24) +} diff --git a/app/utils/glob_match.ts b/app/utils/glob_match.ts new file mode 100644 index 0000000..a762904 --- /dev/null +++ b/app/utils/glob_match.ts @@ -0,0 +1,18 @@ +/** + * Simple glob matching for Configuration criteria. + * Supports: + * - `*` matches any sequence of characters (including empty) + * - `?` matches exactly one character + * Matching is case-sensitive. + */ +export function matchGlob(pattern: string, value: string): boolean { + const regex = new RegExp( + '^' + + pattern + .replace(/[.+^${}()|[\]\\]/g, '\\$&') + .replace(/\*/g, '.*') + .replace(/\?/g, '.') + + '$' + ) + return regex.test(value) +} diff --git a/app/utils/inventory_coverage.ts b/app/utils/inventory_coverage.ts new file mode 100644 index 0000000..cc5f7b4 --- /dev/null +++ b/app/utils/inventory_coverage.ts @@ -0,0 +1,32 @@ +import type { InventoryPath } from '../domain/configuration/configuration.js' + +/** + * Checks whether a desired parameter is covered by at least one inventory path. + * An inventory path ending with "." covers all parameters starting with that prefix (subtree match). + * An inventory path without trailing "." requires an exact match. + */ +export function isParameterCoveredByInventory( + paramName: string, + inventoryPaths: InventoryPath[] +): boolean { + return inventoryPaths.some((inv) => { + const path = inv.path + if (!path.endsWith('.')) { + return paramName === path + } + // Subtree match: param starts with the inventory path + return paramName.startsWith(path) + }) +} + +/** + * Suggests the inventory path to add to cover a given parameter. + * Removes the leaf name and adds a trailing dot. + * Example: "InternetGatewayDevice.ManagementServer.PeriodicInformInterval" + * → "InternetGatewayDevice.ManagementServer." + */ +export function suggestInventoryPath(paramName: string): string { + const parts = paramName.split('.') + parts.pop() // Remove leaf name + return parts.join('.') + '.' +} diff --git a/app/utils/provisioning_code.ts b/app/utils/provisioning_code.ts new file mode 100644 index 0000000..a1afa15 --- /dev/null +++ b/app/utils/provisioning_code.ts @@ -0,0 +1,38 @@ +import { createHash } from 'node:crypto' +import type { Configuration } from '../domain/configuration/configuration.js' + +/** + * Computes a 12-character hex content hash of a Configuration's desired state. + * The hash is based on the sorted desiredParameters and firmwareTarget. + */ +export function computeContentHash(config: Configuration): string { + const payload = JSON.stringify({ + desiredParameters: config.desiredParameters + .slice() + .sort((a, b) => a.name.localeCompare(b.name)), + firmwareTarget: config.firmwareTarget || null, + }) + return createHash('sha256').update(payload).digest('hex').substring(0, 12) +} + +/** + * Builds the ProvisioningCode string for a Configuration. + * Format: `(v)-` + * Example: `BM632w-standard(v1.0.1)-a1b2c3d4e5f6` + */ +export function buildProvisioningCode(config: Configuration): string { + const hash = computeContentHash(config) + return `${config.identifier}(v${config.version})-${hash}` +} + +/** + * Parses an existing ProvisioningCode string. + * Returns null if the format is invalid. + */ +export function parseProvisioningCode( + code: string +): { identifier: string; version: string; hash: string } | null { + const match = code.match(/^(.+)\(v(.+)\)-([a-f0-9]{12})$/) + if (!match) return null + return { identifier: match[1], version: match[2], hash: match[3] } +} diff --git a/app/utils/schedule_window.ts b/app/utils/schedule_window.ts new file mode 100644 index 0000000..6e34a70 --- /dev/null +++ b/app/utils/schedule_window.ts @@ -0,0 +1,87 @@ +import type { ScheduleWindow } from '../domain/configuration/configuration.js' + +/** + * Checks if the current time (or provided time) falls within the schedule window. + * Returns true if no schedule is defined (no restriction). + * + * Handles: + * - IANA timezone conversion (uses the timezone to get local hour and day) + * - Midnight-spanning windows (startHour > endHour means spans midnight) + * - Day-of-week filtering (daysOfWeek array, 0=Sunday) + */ +export function isWithinSchedule(schedule: ScheduleWindow | undefined, now?: Date): boolean { + if (!schedule) { + return true + } + + const currentTime = now ?? new Date() + + // Get the local hour and day in the schedule's timezone + const localParts = getLocalTimeParts(currentTime, schedule.timezone) + const localHour = localParts.hour + const localDay = localParts.dayOfWeek // 0=Sunday + + // Check day-of-week filter + if (schedule.daysOfWeek.length > 0 && !schedule.daysOfWeek.includes(localDay)) { + return false + } + + // Check time window + const { startHour, endHour } = schedule + + if (startHour <= endHour) { + // Normal window (e.g., 2-6 means 02:00 to 06:00) + return localHour >= startHour && localHour < endHour + } else { + // Midnight-spanning window (e.g., 22-6 means 22:00 to 06:00 next day) + return localHour >= startHour || localHour < endHour + } +} + +/** + * Gets the local hour and day-of-week for a given Date in a specific IANA timezone. + */ +function getLocalTimeParts( + date: Date, + timezone: string +): { hour: number; dayOfWeek: number } { + const formatter = new Intl.DateTimeFormat('en-US', { + timeZone: timezone, + hour: 'numeric', + weekday: 'short', + hour12: false, + }) + + const parts = formatter.formatToParts(date) + + let hour = 0 + let dayOfWeek = 0 + + for (const part of parts) { + if (part.type === 'hour') { + // Intl may return "24" for midnight in some locales, normalize to 0 + hour = Number.parseInt(part.value, 10) % 24 + } + if (part.type === 'weekday') { + dayOfWeek = weekdayToNumber(part.value) + } + } + + return { hour, dayOfWeek } +} + +/** + * Converts a short weekday name (from Intl) to a number (0=Sunday). + */ +function weekdayToNumber(weekday: string): number { + const map: Record = { + Sun: 0, + Mon: 1, + Tue: 2, + Wed: 3, + Thu: 4, + Fri: 5, + Sat: 6, + } + return map[weekday] ?? 0 +} diff --git a/app/validators/command.ts b/app/validators/command.ts new file mode 100644 index 0000000..a4d84e8 --- /dev/null +++ b/app/validators/command.ts @@ -0,0 +1,23 @@ +import vine from '@vinejs/vine' + +export const createCommandValidator = vine.compile( + vine.object({ + deviceIdentifier: vine.string().trim(), + action: vine.enum([ + 'SET_PARAMETER_VALUES', + 'GET_PARAMETER_VALUES', + 'GET_PARAMETER_NAMES', + 'SET_PARAMETER_ATTRIBUTES', + 'GET_PARAMETER_ATTRIBUTES', + 'DOWNLOAD', + 'UPLOAD', + 'REBOOT', + 'FACTORY_RESET', + 'SCHEDULE_INFORM', + 'REFRESH', + ]), + parameters: vine.object({}).allowUnknownProperties(), + mode: vine.enum(['NI', 'I']).optional(), + priority: vine.number().min(0).max(3).optional(), + }) +) diff --git a/app/validators/configuration.ts b/app/validators/configuration.ts new file mode 100644 index 0000000..496a075 --- /dev/null +++ b/app/validators/configuration.ts @@ -0,0 +1,69 @@ +import vine from '@vinejs/vine' + +const parameterCriteriaItemSchema = vine.object({ + name: vine.string().trim().minLength(1), + value: vine.any(), +}) + +const criteriaSchema = vine.object({ + manufacturer: vine.string().optional(), + oui: vine.string().optional(), + productClass: vine.string().optional(), + serialNumber: vine.string().optional(), + parameterCriteria: vine.array(parameterCriteriaItemSchema).optional(), +}) + +const inventoryPathSchema = vine.object({ + path: vine.string().trim().minLength(1), +}) + +const desiredParameterSchema = vine.object({ + name: vine.string().trim().minLength(1), + value: vine.any(), + type: vine.enum(['string', 'int', 'unsignedInt', 'boolean', 'dateTime']), +}) + +const firmwareTargetSchema = vine.object({ + productClass: vine.string().trim().minLength(1), + targetVersion: vine.string().trim().minLength(1), +}) + +const scheduleWindowSchema = vine.object({ + daysOfWeek: vine.array(vine.number().min(0).max(6)).minLength(1), + startHour: vine.number().min(0).max(23), + endHour: vine.number().min(0).max(23), + timezone: vine.string().trim().minLength(1), +}) + +export const createConfigurationValidator = vine.compile( + vine.object({ + identifier: vine.string().trim().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/), + name: vine.string().trim().minLength(1), + version: vine.string().trim().regex(/^\d+\.\d+\.\d+$/), + priority: vine.number().positive(), + policy: vine.enum(['once', 'maintain']), + enabled: vine.boolean().optional(), + tags: vine.array(vine.string().trim()).optional(), + criteria: criteriaSchema.optional(), + inventory: vine.array(inventoryPathSchema).optional(), + desiredParameters: vine.array(desiredParameterSchema).optional(), + firmwareTarget: firmwareTargetSchema.optional(), + schedule: scheduleWindowSchema.optional(), + }) +) + +export const updateConfigurationValidator = vine.compile( + vine.object({ + name: vine.string().trim().minLength(1).optional(), + version: vine.string().trim().regex(/^\d+\.\d+\.\d+$/).optional(), + priority: vine.number().positive().optional(), + policy: vine.enum(['once', 'maintain']).optional(), + enabled: vine.boolean().optional(), + tags: vine.array(vine.string().trim()).optional(), + criteria: criteriaSchema.optional(), + inventory: vine.array(inventoryPathSchema).optional(), + desiredParameters: vine.array(desiredParameterSchema).optional(), + firmwareTarget: firmwareTargetSchema.optional(), + schedule: scheduleWindowSchema.optional(), + }) +) diff --git a/app/validators/device.ts b/app/validators/device.ts new file mode 100644 index 0000000..33fcd07 --- /dev/null +++ b/app/validators/device.ts @@ -0,0 +1,12 @@ +import vine from '@vinejs/vine' + +export const createDeviceValidator = vine.compile( + vine.object({ + deviceId: vine.object({ + manufacturer: vine.string().trim().minLength(1), + oui: vine.string().trim(), + productClass: vine.string().trim(), + serialNumber: vine.string().trim().minLength(1), + }), + }) +) diff --git a/app/validators/firmware_catalog.ts b/app/validators/firmware_catalog.ts new file mode 100644 index 0000000..c17bdf6 --- /dev/null +++ b/app/validators/firmware_catalog.ts @@ -0,0 +1,45 @@ +import vine from '@vinejs/vine' + +export const createFirmwareEntryValidator = vine.compile( + vine.object({ + identifier: vine.string().trim().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/), + productClass: vine.string().trim().minLength(1), + version: vine.string().trim().minLength(1), + downloadUrl: vine.string().trim().url(), + checksum: vine.string().trim().minLength(1), + checksumType: vine.enum(['md5', 'sha256']), + fileSize: vine.number().min(0), + }) +) + +export const updateFirmwareEntryValidator = vine.compile( + vine.object({ + productClass: vine.string().trim().minLength(1).optional(), + version: vine.string().trim().minLength(1).optional(), + downloadUrl: vine.string().trim().url().optional(), + checksum: vine.string().trim().minLength(1).optional(), + checksumType: vine.enum(['md5', 'sha256']).optional(), + fileSize: vine.number().min(0).optional(), + }) +) + +export const createMigrationRouteValidator = vine.compile( + vine.object({ + identifier: vine.string().trim().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/), + productClass: vine.string().trim().minLength(1), + sourceVersionPattern: vine.string().trim().minLength(1), + targetVersion: vine.string().trim().minLength(1), + downloadUrl: vine.string().trim().url(), + priority: vine.number().min(0), + }) +) + +export const updateMigrationRouteValidator = vine.compile( + vine.object({ + productClass: vine.string().trim().minLength(1).optional(), + sourceVersionPattern: vine.string().trim().minLength(1).optional(), + targetVersion: vine.string().trim().minLength(1).optional(), + downloadUrl: vine.string().trim().url().optional(), + priority: vine.number().min(0).optional(), + }) +) diff --git a/bin/console.ts b/bin/console.ts new file mode 100644 index 0000000..86bf4d2 --- /dev/null +++ b/bin/console.ts @@ -0,0 +1,47 @@ +/* +|-------------------------------------------------------------------------- +| Ace entry point +|-------------------------------------------------------------------------- +| +| The "console.ts" file is the entrypoint for booting the AdonisJS +| command-line framework and executing commands. +| +| Commands do not boot the application, unless the currently running command +| has "options.startApp" flag set to true. +| +*/ + +await import('reflect-metadata') +const { Ignitor, prettyPrintError } = await import('@adonisjs/core') + +/** + * URL to the application root. AdonisJS need it to resolve + * paths to file and directories for scaffolding commands + */ +const APP_ROOT = new URL('../', import.meta.url) + +/** + * The importer is used to import files in context of the + * application. + */ +const IMPORTER = (filePath: string) => { + if (filePath.startsWith('./') || filePath.startsWith('../')) { + return import(new URL(filePath, APP_ROOT).href) + } + return import(filePath) +} + +new Ignitor(APP_ROOT, { importer: IMPORTER }) + .tap((app) => { + app.booting(async () => { + await import('#start/env') + }) + app.listen('SIGTERM', () => app.terminate()) + app.listenIf(app.managedByPm2, 'SIGINT', () => app.terminate()) + }) + .ace() + .handle(process.argv.splice(2)) + .catch((error) => { + process.exitCode = 1 + prettyPrintError(error) + }) diff --git a/bin/server.ts b/bin/server.ts new file mode 100644 index 0000000..297b371 --- /dev/null +++ b/bin/server.ts @@ -0,0 +1,45 @@ +/* +|-------------------------------------------------------------------------- +| HTTP server entrypoint +|-------------------------------------------------------------------------- +| +| The "server.ts" file is the entrypoint for starting the AdonisJS HTTP +| server. Either you can run this file directly or use the "serve" +| command to run this file and monitor file changes +| +*/ + +await import('reflect-metadata') +const { Ignitor, prettyPrintError } = await import('@adonisjs/core') + +/** + * URL to the application root. AdonisJS need it to resolve + * paths to file and directories for scaffolding commands + */ +const APP_ROOT = new URL('../', import.meta.url) + +/** + * The importer is used to import files in context of the + * application. + */ +const IMPORTER = (filePath: string) => { + if (filePath.startsWith('./') || filePath.startsWith('../')) { + return import(new URL(filePath, APP_ROOT).href) + } + return import(filePath) +} + +new Ignitor(APP_ROOT, { importer: IMPORTER }) + .tap((app) => { + app.booting(async () => { + await import('#start/env') + }) + app.listen('SIGTERM', () => app.terminate()) + app.listenIf(app.managedByPm2, 'SIGINT', () => app.terminate()) + }) + .httpServer() + .start() + .catch((error) => { + process.exitCode = 1 + prettyPrintError(error) + }) diff --git a/bin/test.ts b/bin/test.ts new file mode 100644 index 0000000..d759efe --- /dev/null +++ b/bin/test.ts @@ -0,0 +1,62 @@ +/* +|-------------------------------------------------------------------------- +| Test runner entrypoint +|-------------------------------------------------------------------------- +| +| The "test.ts" file is the entrypoint for running tests using Japa. +| +| Either you can run this file directly or use the "test" +| command to run this file and monitor file changes. +| +*/ + +process.env.NODE_ENV = 'test' + +import 'reflect-metadata' +import { Ignitor, prettyPrintError } from '@adonisjs/core' +import { configure, processCLIArgs, run } from '@japa/runner' + +/** + * URL to the application root. AdonisJS need it to resolve + * paths to file and directories for scaffolding commands + */ +const APP_ROOT = new URL('../', import.meta.url) + +/** + * The importer is used to import files in context of the + * application. + */ +const IMPORTER = (filePath: string) => { + if (filePath.startsWith('./') || filePath.startsWith('../')) { + return import(new URL(filePath, APP_ROOT).href) + } + return import(filePath) +} + +new Ignitor(APP_ROOT, { importer: IMPORTER }) + .tap((app) => { + app.booting(async () => { + await import('#start/env') + }) + app.listen('SIGTERM', () => app.terminate()) + app.listenIf(app.managedByPm2, 'SIGINT', () => app.terminate()) + }) + .testRunner() + .configure(async (app) => { + const { runnerHooks, ...config } = await import('../tests/bootstrap.js') + + processCLIArgs(process.argv.splice(2)) + configure({ + ...app.rcFile.tests, + ...config, + ...{ + setup: runnerHooks.setup, + teardown: runnerHooks.teardown.concat([() => app.terminate()]), + }, + }) + }) + .run(() => run()) + .catch((error) => { + process.exitCode = 1 + prettyPrintError(error) + }) diff --git a/config/app.ts b/config/app.ts new file mode 100644 index 0000000..c03915e --- /dev/null +++ b/config/app.ts @@ -0,0 +1,84 @@ +import env from '#start/env' +import app from '@adonisjs/core/services/app' +import { defineConfig } from '@adonisjs/core/http' + +/** + * The app URL can be used in various places where you want to create absolute + * URLs to your application. For example, when sending emails, images should + * use absolute URLs. + */ +export const appUrl = env.get('APP_URL') + +/** + * The configuration settings used by the HTTP server + */ +export const http = defineConfig({ + /** + * Generate a unique request id for each incoming request. + * Useful to correlate logs and debug a request flow. + */ + generateRequestId: true, + + /** + * Allow HTTP method spoofing via the "_method" form/query parameter. + * This lets HTML forms target PUT/PATCH/DELETE routes while still + * submitting with POST. + */ + allowMethodSpoofing: false, + + /** + * Enabling async local storage will let you access HTTP context + * from anywhere inside your application. + */ + useAsyncLocalStorage: false, + + /** + * Redirect configuration controls the behavior of + * response.redirect().back() and query string forwarding. + */ + redirect: { + /** + * When enabled, all redirects automatically carry over the current + * request's query string parameters to the redirect destination. + * Use withQs(false) to opt out for a specific redirect. + */ + forwardQueryString: true, + }, + + /** + * Manage cookies configuration. The settings for the session id cookie are + * defined inside the "config/session.ts" file. + */ + cookie: { + /** + * Restrict the cookie to a specific domain. + * Keep empty to use the current host. + */ + domain: '', + + /** + * Restrict the cookie to a URL path. '/' means all routes. + */ + path: '/', + + /** + * Default lifetime for cookies managed by the HTTP layer. + */ + maxAge: '2h', + + /** + * Prevent JavaScript access to the cookie in the browser. + */ + httpOnly: true, + + /** + * Send cookies only over HTTPS in production. + */ + secure: app.inProduction, + + /** + * Cross-site policy for cookie sending. + */ + sameSite: 'lax', + }, +}) diff --git a/config/bodyparser.ts b/config/bodyparser.ts new file mode 100644 index 0000000..7e836e6 --- /dev/null +++ b/config/bodyparser.ts @@ -0,0 +1,78 @@ +import { defineConfig } from '@adonisjs/core/bodyparser' + +const bodyParserConfig = defineConfig({ + /** + * Parse request bodies for these HTTP methods. + * Keep this aligned with methods that receive payloads in your routes. + */ + allowedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'], + + /** + * Config for the "application/x-www-form-urlencoded" + * content-type parser. + */ + form: { + /** + * Normalize empty string values to null. + */ + convertEmptyStringsToNull: true, + + /** + * Content types handled by the form parser. + */ + types: ['application/x-www-form-urlencoded'], + }, + + /** + * Config for the JSON parser. + */ + json: { + /** + * Normalize empty string values to null. + */ + convertEmptyStringsToNull: true, + + /** + * Content types handled by the JSON parser. + */ + types: [ + 'application/json', + 'application/json-patch+json', + 'application/vnd.api+json', + 'application/csp-report', + ], + }, + + /** + * Config for the "multipart/form-data" content-type parser. + * File uploads are handled by the multipart parser. + */ + multipart: { + /** + * Automatically process uploaded files into the system tmp directory. + */ + autoProcess: true, + + /** + * Normalize empty string values to null. + */ + convertEmptyStringsToNull: true, + + /** + * Routes where multipart processing is handled manually. + */ + processManually: [], + + /** + * Maximum accepted payload size for multipart requests. + */ + limit: '20mb', + + /** + * Content types handled by the multipart parser. + */ + types: ['multipart/form-data'], + }, +}) + +export default bodyParserConfig diff --git a/config/cors.ts b/config/cors.ts new file mode 100644 index 0000000..dd79007 --- /dev/null +++ b/config/cors.ts @@ -0,0 +1,19 @@ +import { defineConfig } from '@adonisjs/cors' + +/** + * Configuration options to tweak the CORS policy. The following + * options are documented on the official documentation website. + * + * https://docs.adonisjs.com/guides/security/cors + */ +const corsConfig = defineConfig({ + enabled: true, + origin: [], + methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'], + headers: true, + exposeHeaders: [], + credentials: true, + maxAge: 90, +}) + +export default corsConfig diff --git a/config/database.ts b/config/database.ts new file mode 100644 index 0000000..a97cce4 --- /dev/null +++ b/config/database.ts @@ -0,0 +1,8 @@ +import env from '#start/env' + +const databaseConfig = { + uri: env.get('MONGODB_URI'), + dbName: env.get('MONGODB_DB_NAME', 'adam-acs'), +} + +export default databaseConfig diff --git a/config/encryption.ts b/config/encryption.ts new file mode 100644 index 0000000..1bb3dc5 --- /dev/null +++ b/config/encryption.ts @@ -0,0 +1,34 @@ +import env from '#start/env' +import { defineConfig, drivers } from '@adonisjs/core/encryption' + +const encryptionConfig = defineConfig({ + /** + * Default encryption driver used by the application. + */ + default: 'gcm', + + list: { + gcm: drivers.aes256gcm({ + /** + * Keys used for encryption/decryption. + * First key encrypts, all keys are tried for decryption. + */ + keys: [env.get('APP_KEY')], + + /** + * Stable identifier for this driver. + */ + id: 'gcm', + }), + }, +}) + +export default encryptionConfig + +/** + * Inferring types for the list of encryptors you have configured + * in your application. + */ +declare module '@adonisjs/core/types' { + export interface EncryptorsList extends InferEncryptors {} +} diff --git a/config/hash.ts b/config/hash.ts new file mode 100644 index 0000000..a54c086 --- /dev/null +++ b/config/hash.ts @@ -0,0 +1,75 @@ +import { defineConfig, drivers } from '@adonisjs/core/hash' + +/** + * Hashing configuration. + * + * This starter uses Node.js scrypt under the hood. + * Node.js reference: https://nodejs.org/api/crypto.html#cryptoscryptpassword-salt-keylen-options-callback + */ +const hashConfig = defineConfig({ + /** + * Default hasher used by the application. + */ + default: 'scrypt', + + list: { + /** + * Scrypt is memory-hard, which makes brute-force attacks more expensive. + */ + scrypt: drivers.scrypt({ + /** + * Work factor (Node alias: N / cost). + * Higher values increase security and CPU+memory usage. + * + * Tuning guideline: + * - Start with 16384. + * - Increase gradually (for example 32768) and benchmark login/signup latency. + * - Keep values practical for your slowest production machine. + * + * Node constraint: value must be a power of two greater than 1. + */ + cost: 16384, + + /** + * Block size (Node alias: r / blockSize). + * Increases memory and CPU linearly. + * + * Tuning guideline: + * - Keep 8 unless you have a measured reason to change it. + * - Raise only with benchmark data, because memory usage grows quickly. + */ + blockSize: 8, + + /** + * Parallelization (Node alias: p / parallelization). + * Controls how many independent computations are performed. + * + * Tuning guideline: + * - Keep 1 for most applications. + * - Increase only after load testing if your infrastructure benefits from it. + */ + parallelization: 1, + + /** + * Maximum memory limit in bytes (Node alias: maxmem / maxMemory). + * Hashing throws if the estimated memory usage is above this limit. + * Node documents the check as approximately: 128 * N * r > maxmem. + * + * Tuning guideline: + * - Keep this aligned with your cost/blockSize choices. + * - Increase carefully on memory-constrained environments. + */ + maxMemory: 33554432, + }), + }, +}) + +export default hashConfig + +/** + * Inferring types for the list of hashers you have configured + * in your application. + */ +declare module '@adonisjs/core/types' { + export interface HashersList extends InferHashers {} +} diff --git a/config/inertia.ts b/config/inertia.ts new file mode 100644 index 0000000..251fa07 --- /dev/null +++ b/config/inertia.ts @@ -0,0 +1,8 @@ +import { defineConfig } from '@adonisjs/inertia' + +export default defineConfig({ + rootView: 'inertia_layout', + ssr: { + enabled: false, + }, +}) diff --git a/config/logger.ts b/config/logger.ts new file mode 100644 index 0000000..b961300 --- /dev/null +++ b/config/logger.ts @@ -0,0 +1,35 @@ +import env from '#start/env' +import app from '@adonisjs/core/services/app' +import { defineConfig, targets } from '@adonisjs/core/logger' + +const loggerConfig = defineConfig({ + default: 'app', + + /** + * The loggers object can be used to define multiple loggers. + * By default, we configure only one logger (named "app"). + */ + loggers: { + app: { + enabled: true, + name: env.get('APP_NAME'), + level: env.get('LOG_LEVEL'), + transport: { + targets: targets() + .pushIf(!app.inProduction, targets.pretty()) + .pushIf(app.inProduction, targets.file({ destination: 1 })) + .toArray(), + }, + }, + }, +}) + +export default loggerConfig + +/** + * Inferring types for the list of loggers you have configured + * in your application. + */ +declare module '@adonisjs/core/types' { + export interface LoggersList extends InferLoggers {} +} diff --git a/config/session.ts b/config/session.ts new file mode 100644 index 0000000..6a143aa --- /dev/null +++ b/config/session.ts @@ -0,0 +1,48 @@ +import env from '#start/env' +import app from '@adonisjs/core/services/app' +import { defineConfig, stores } from '@adonisjs/session' + +const sessionConfig = defineConfig({ + enabled: true, + cookieName: 'adonis-session', + + /** + * When set to true, the session id cookie will be deleted + * once the user closes the browser. + */ + clearWithBrowser: false, + + /** + * Define how long to keep the session data alive without + * any activity. + */ + age: '2h', + + /** + * Configuration for session cookie and the + * cookie store + */ + cookie: { + path: '/', + httpOnly: true, + secure: app.inProduction, + sameSite: 'lax', + }, + + /** + * The store to use. Make sure to validate the environment + * variable in order to infer the store name without any + * errors. + */ + store: env.get('SESSION_DRIVER'), + + /** + * List of configured stores. Refer documentation to see + * list of available stores and their config. + */ + stores: { + cookie: stores.cookie(), + }, +}) + +export default sessionConfig diff --git a/config/shield.ts b/config/shield.ts new file mode 100644 index 0000000..28f927d --- /dev/null +++ b/config/shield.ts @@ -0,0 +1,54 @@ +import { defineConfig } from '@adonisjs/shield' + +const shieldConfig = defineConfig({ + /** + * Configure CSP policies for your app. Refer documentation + * to learn more + */ + csp: { + enabled: false, + directives: {}, + reportOnly: false, + }, + + /** + * Configure CSRF protection options. Refer documentation + * to learn more + */ + csrf: { + enabled: true, + exceptRoutes: (ctx) => { + const url = ctx.request.url() + return url === '/cwmp10' || url.startsWith('/api/v1/') + }, + enableXsrfCookie: true, + methods: ['POST', 'PUT', 'PATCH', 'DELETE'], + }, + + /** + * Control how your website should be embedded inside + * iFrames + */ + xFrame: { + enabled: true, + action: 'DENY', + }, + + /** + * Force browser to always use HTTPS + */ + hsts: { + enabled: true, + maxAge: '180 days', + }, + + /** + * Disable browsers from sniffing the content type of a + * response and always rely on the "content-type" header. + */ + contentTypeSniffing: { + enabled: true, + }, +}) + +export default shieldConfig diff --git a/config/static.ts b/config/static.ts new file mode 100644 index 0000000..2e20fc0 --- /dev/null +++ b/config/static.ts @@ -0,0 +1,17 @@ +import { defineConfig } from '@adonisjs/static' + +/** + * Configuration options to tweak the static files middleware. + * The complete set of options are documented on the + * official documentation website. + * + * https://docs.adonisjs.com/guides/static-assets + */ +const staticServerConfig = defineConfig({ + enabled: true, + etag: true, + lastModified: true, + dotFiles: 'ignore', +}) + +export default staticServerConfig diff --git a/config/vite.ts b/config/vite.ts new file mode 100644 index 0000000..e7d65bf --- /dev/null +++ b/config/vite.ts @@ -0,0 +1,28 @@ +import { defineConfig } from '@adonisjs/vite' + +const viteBackendConfig = defineConfig({ + /** + * The output of vite will be written inside this + * directory. The path should be relative from + * the application root. + */ + buildDirectory: 'public/assets', + + /** + * The path to the manifest file generated by the + * "vite build" command. + */ + manifestFile: 'public/assets/.vite/manifest.json', + + /** + * Feel free to change the value of the "assetsUrl" to + * point to a CDN in production. + */ + assetsUrl: '/assets', + + scriptAttributes: { + defer: true, + }, +}) + +export default viteBackendConfig diff --git a/database/base_model.ts b/database/base_model.ts new file mode 100644 index 0000000..72f95d3 --- /dev/null +++ b/database/base_model.ts @@ -0,0 +1,136 @@ +import { Db, Collection, ObjectId, Filter, UpdateFilter, Sort } from 'mongodb' +import { getMongoDb } from '#providers/mongodb_provider' + +export abstract class BaseModel { + static collectionName: string + + _id?: ObjectId + createdAt?: Date + updatedAt?: Date + + static getDb(): Db { + return getMongoDb() + } + + static getCollection(this: { collectionName: string; getDb(): Db }): Collection { + return this.getDb().collection(this.collectionName) + } + + static async find(this: { collectionName: string; getDb(): Db }, filter: Filter = {}): Promise { + const collection = this.getDb().collection(this.collectionName) + return collection.find(filter).toArray() + } + + static async findById(this: { collectionName: string; getDb(): Db }, id: string | ObjectId): Promise { + const collection = this.getDb().collection(this.collectionName) + const objectId = typeof id === 'string' ? new ObjectId(id) : id + return collection.findOne({ _id: objectId }) + } + + static async findOne(this: { collectionName: string; getDb(): Db }, filter: Filter): Promise { + const collection = this.getDb().collection(this.collectionName) + return collection.findOne(filter) + } + + static async create(this: { collectionName: string; getDb(): Db }, data: Partial): Promise { + const collection = this.getDb().collection(this.collectionName) + const now = new Date() + const doc = { ...data, createdAt: now, updatedAt: now } + const result = await collection.insertOne(doc) + return { ...doc, _id: result.insertedId } + } + + static async createMany(this: { collectionName: string; getDb(): Db }, data: Partial[]): Promise { + const collection = this.getDb().collection(this.collectionName) + const now = new Date() + const docs = data.map((d) => ({ ...d, createdAt: now, updatedAt: now })) + const result = await collection.insertMany(docs) + return docs.map((doc, i) => ({ ...doc, _id: result.insertedIds[i] })) + } + + static async updateOne( + this: { collectionName: string; getDb(): Db }, + filter: Filter, + update: UpdateFilter | Partial + ): Promise { + const collection = this.getDb().collection(this.collectionName) + const updateDoc = '$set' in update ? update : { $set: { ...update, updatedAt: new Date() } } + return collection.updateOne(filter, updateDoc) + } + + static async updateMany( + this: { collectionName: string; getDb(): Db }, + filter: Filter, + update: UpdateFilter | Partial + ): Promise { + const collection = this.getDb().collection(this.collectionName) + const updateDoc = '$set' in update ? update : { $set: { ...update, updatedAt: new Date() } } + return collection.updateMany(filter, updateDoc) + } + + static async deleteOne(this: { collectionName: string; getDb(): Db }, filter: Filter): Promise { + const collection = this.getDb().collection(this.collectionName) + const result = await collection.deleteOne(filter) + return result.deletedCount === 1 + } + + static async deleteMany(this: { collectionName: string; getDb(): Db }, filter: Filter): Promise { + const collection = this.getDb().collection(this.collectionName) + const result = await collection.deleteMany(filter) + return result.deletedCount + } + + static async paginate( + this: { collectionName: string; getDb(): Db }, + page: number, + perPage: number, + filter: Filter = {}, + sort: Sort = {} + ): Promise<{ data: any[]; total: number; page: number; perPage: number; lastPage: number }> { + const collection = this.getDb().collection(this.collectionName) + const total = await collection.countDocuments(filter) + const lastPage = Math.max(1, Math.ceil(total / perPage)) + const skip = (page - 1) * perPage + const data = await collection.find(filter).sort(sort).skip(skip).limit(perPage).toArray() + return { data, total, page, perPage, lastPage } + } + + static query(this: { collectionName: string; getDb(): Db }) { + const collection = this.getDb().collection(this.collectionName) + return collection + } + + async save(): Promise { + const constructor = this.constructor as typeof BaseModel + const collection = constructor.getDb().collection(constructor.collectionName) + this.updatedAt = new Date() + if (!this.createdAt) { + this.createdAt = new Date() + } + const { _id, ...data } = this as any + if (_id) { + await collection.updateOne({ _id }, { $set: data }) + } else { + const result = await collection.insertOne(data) + this._id = result.insertedId + } + } + + async delete(): Promise { + const constructor = this.constructor as typeof BaseModel + const collection = constructor.getDb().collection(constructor.collectionName) + if (!this._id) return false + const result = await collection.deleteOne({ _id: this._id }) + return result.deletedCount === 1 + } + + async refresh(): Promise { + const constructor = this.constructor as typeof BaseModel + const collection = constructor.getDb().collection(constructor.collectionName) + if (!this._id) return + const doc = await collection.findOne({ _id: this._id }) + if (doc) { + Object.assign(this, doc) + } + } +} diff --git a/database/migrations/1761885935168_create_users_table.ts b/database/migrations/1761885935168_create_users_table.ts new file mode 100644 index 0000000..dbca083 --- /dev/null +++ b/database/migrations/1761885935168_create_users_table.ts @@ -0,0 +1,21 @@ +import { BaseSchema } from '@adonisjs/lucid/schema' + +export default class extends BaseSchema { + protected tableName = 'users' + + async up() { + this.schema.createTable(this.tableName, (table) => { + table.increments('id').notNullable() + table.string('full_name').nullable() + table.string('email', 254).notNullable().unique() + table.string('password').notNullable() + + table.timestamp('created_at').notNullable() + table.timestamp('updated_at').nullable() + }) + } + + async down() { + this.schema.dropTable(this.tableName) + } +} diff --git a/database/migrations/1768620764696_create_access_tokens_table.ts b/database/migrations/1768620764696_create_access_tokens_table.ts new file mode 100644 index 0000000..a3ce197 --- /dev/null +++ b/database/migrations/1768620764696_create_access_tokens_table.ts @@ -0,0 +1,31 @@ +import { BaseSchema } from '@adonisjs/lucid/schema' + +export default class extends BaseSchema { + protected tableName = 'auth_access_tokens' + + async up() { + this.schema.createTable(this.tableName, (table) => { + table.increments('id') + table + .integer('tokenable_id') + .notNullable() + .unsigned() + .references('id') + .inTable('users') + .onDelete('CASCADE') + + table.string('type').notNullable() + table.string('name').nullable() + table.string('hash').notNullable() + table.text('abilities').notNullable() + table.timestamp('created_at') + table.timestamp('updated_at') + table.timestamp('last_used_at').nullable() + table.timestamp('expires_at').nullable() + }) + } + + async down() { + this.schema.dropTable(this.tableName) + } +} diff --git a/database/mongo_migrations/001_create_devices_collection.ts b/database/mongo_migrations/001_create_devices_collection.ts new file mode 100644 index 0000000..fb01726 --- /dev/null +++ b/database/mongo_migrations/001_create_devices_collection.ts @@ -0,0 +1,27 @@ +import { MongoMigration } from '../mongo_migrator.js' + +export default class CreateDevicesCollection extends MongoMigration { + async up(): Promise { + const collection = this.db.collection('devices') + + await collection.createIndex( + { + 'deviceId.manufacturer': 1, + 'deviceId.oui': 1, + 'deviceId.productClass': 1, + 'deviceId.serialNumber': 1, + }, + { unique: true, name: 'idx_deviceId_compound' } + ) + + await collection.createIndex({ identifier: 1 }, { unique: true, name: 'idx_identifier' }) + await collection.createIndex({ 'deviceId.manufacturer': 1 }, { name: 'idx_manufacturer' }) + await collection.createIndex({ 'deviceId.oui': 1 }, { name: 'idx_oui' }) + await collection.createIndex({ 'deviceId.productClass': 1 }, { name: 'idx_productClass' }) + await collection.createIndex({ 'deviceId.serialNumber': 1 }, { name: 'idx_serialNumber' }) + } + + async down(): Promise { + await this.db.dropCollection('devices').catch(() => {}) + } +} diff --git a/database/mongo_migrations/002_create_commands_collection.ts b/database/mongo_migrations/002_create_commands_collection.ts new file mode 100644 index 0000000..32b4522 --- /dev/null +++ b/database/mongo_migrations/002_create_commands_collection.ts @@ -0,0 +1,20 @@ +import { MongoMigration } from '../mongo_migrator.js' + +export default class CreateCommandsCollection extends MongoMigration { + async up(): Promise { + const collection = this.db.collection('commands') + + await collection.createIndex( + { deviceIdentifier: 1 }, + { background: true, name: 'idx_deviceIdentifier' } + ) + await collection.createIndex( + { status: 1, priority: -1 }, + { background: true, name: 'idx_status_priority' } + ) + } + + async down(): Promise { + await this.db.dropCollection('commands').catch(() => {}) + } +} diff --git a/database/mongo_migrations/003_create_discovery_configs_collection.ts b/database/mongo_migrations/003_create_discovery_configs_collection.ts new file mode 100644 index 0000000..603918f --- /dev/null +++ b/database/mongo_migrations/003_create_discovery_configs_collection.ts @@ -0,0 +1,12 @@ +import { MongoMigration } from '../mongo_migrator.js' + +export default class CreateDiscoveryConfigsCollection extends MongoMigration { + async up(): Promise { + const collection = this.db.collection('discovery_configs') + await collection.createIndex({ identifier: 1 }, { unique: true, name: 'idx_identifier' }) + } + + async down(): Promise { + await this.db.dropCollection('discovery_configs').catch(() => {}) + } +} diff --git a/database/mongo_migrations/004_create_provisioning_configs_collection.ts b/database/mongo_migrations/004_create_provisioning_configs_collection.ts new file mode 100644 index 0000000..33f5d9f --- /dev/null +++ b/database/mongo_migrations/004_create_provisioning_configs_collection.ts @@ -0,0 +1,12 @@ +import { MongoMigration } from '../mongo_migrator.js' + +export default class CreateProvisioningConfigsCollection extends MongoMigration { + async up(): Promise { + const collection = this.db.collection('provisioning_configs') + await collection.createIndex({ identifier: 1 }, { unique: true, name: 'idx_identifier' }) + } + + async down(): Promise { + await this.db.dropCollection('provisioning_configs').catch(() => {}) + } +} diff --git a/database/mongo_migrations/005_create_configurations_collection.ts b/database/mongo_migrations/005_create_configurations_collection.ts new file mode 100644 index 0000000..6e40cc1 --- /dev/null +++ b/database/mongo_migrations/005_create_configurations_collection.ts @@ -0,0 +1,19 @@ +import { MongoMigration } from '../mongo_migrator.js' + +export default class CreateConfigurationsCollection extends MongoMigration { + async up(): Promise { + const collection = this.db.collection('configurations') + await collection.createIndex({ identifier: 1 }, { unique: true, name: 'idx_identifier' }) + await collection.createIndex({ enabled: 1, priority: -1 }, { name: 'idx_enabled_priority' }) + await collection.createIndex({ tags: 1 }, { name: 'idx_tags' }) + await collection.createIndex({ 'criteria.productClass': 1 }, { name: 'idx_criteria_product_class' }) + await collection.createIndex( + { 'criteria.manufacturer': 1, 'criteria.productClass': 1 }, + { name: 'idx_criteria_manufacturer_product_class' } + ) + } + + async down(): Promise { + await this.db.dropCollection('configurations').catch(() => {}) + } +} diff --git a/database/mongo_migrations/006_create_configuration_versions_collection.ts b/database/mongo_migrations/006_create_configuration_versions_collection.ts new file mode 100644 index 0000000..dcb1f72 --- /dev/null +++ b/database/mongo_migrations/006_create_configuration_versions_collection.ts @@ -0,0 +1,19 @@ +import { MongoMigration } from '../mongo_migrator.js' + +export default class CreateConfigurationVersionsCollection extends MongoMigration { + async up(): Promise { + const collection = this.db.collection('configuration_versions') + await collection.createIndex( + { configurationIdentifier: 1, versionNumber: -1 }, + { name: 'idx_configuration_identifier_version_number' } + ) + await collection.createIndex( + { configurationIdentifier: 1 }, + { name: 'idx_configuration_identifier' } + ) + } + + async down(): Promise { + await this.db.dropCollection('configuration_versions').catch(() => {}) + } +} diff --git a/database/mongo_migrations/007_create_firmware_entries_collection.ts b/database/mongo_migrations/007_create_firmware_entries_collection.ts new file mode 100644 index 0000000..84e1734 --- /dev/null +++ b/database/mongo_migrations/007_create_firmware_entries_collection.ts @@ -0,0 +1,13 @@ +import { MongoMigration } from '../mongo_migrator.js' + +export default class CreateFirmwareEntriesCollection extends MongoMigration { + async up(): Promise { + const collection = this.db.collection('firmware_entries') + await collection.createIndex({ identifier: 1 }, { unique: true, name: 'idx_identifier' }) + await collection.createIndex({ productClass: 1 }, { name: 'idx_product_class' }) + } + + async down(): Promise { + await this.db.dropCollection('firmware_entries').catch(() => {}) + } +} diff --git a/database/mongo_migrations/008_create_migration_routes_collection.ts b/database/mongo_migrations/008_create_migration_routes_collection.ts new file mode 100644 index 0000000..90c9d9c --- /dev/null +++ b/database/mongo_migrations/008_create_migration_routes_collection.ts @@ -0,0 +1,17 @@ +import { MongoMigration } from '../mongo_migrator.js' + +export default class CreateMigrationRoutesCollection extends MongoMigration { + async up(): Promise { + const collection = this.db.collection('migration_routes') + await collection.createIndex({ identifier: 1 }, { unique: true, name: 'idx_identifier' }) + await collection.createIndex({ productClass: 1 }, { name: 'idx_product_class' }) + await collection.createIndex( + { productClass: 1, sourceVersionPattern: 1 }, + { name: 'idx_product_class_source_version' } + ) + } + + async down(): Promise { + await this.db.dropCollection('migration_routes').catch(() => {}) + } +} diff --git a/database/mongo_migrations/009_add_compliance_to_devices.ts b/database/mongo_migrations/009_add_compliance_to_devices.ts new file mode 100644 index 0000000..e2e3017 --- /dev/null +++ b/database/mongo_migrations/009_add_compliance_to_devices.ts @@ -0,0 +1,18 @@ +import { MongoMigration } from '../mongo_migrator.js' + +export default class AddComplianceToDevices extends MongoMigration { + async up(): Promise { + const collection = this.db.collection('devices') + await collection.createIndex({ 'compliance.status': 1 }, { name: 'idx_compliance_status' }) + await collection.createIndex( + { 'compliance.configurationIdentifier': 1 }, + { name: 'idx_compliance_configuration_identifier' } + ) + } + + async down(): Promise { + const collection = this.db.collection('devices') + await collection.dropIndex('idx_compliance_status').catch(() => {}) + await collection.dropIndex('idx_compliance_configuration_identifier').catch(() => {}) + } +} diff --git a/database/mongo_migrations/010_drop_legacy_collections.ts b/database/mongo_migrations/010_drop_legacy_collections.ts new file mode 100644 index 0000000..307743b --- /dev/null +++ b/database/mongo_migrations/010_drop_legacy_collections.ts @@ -0,0 +1,20 @@ +import { MongoMigration } from '../mongo_migrator.js' + +export default class DropLegacyCollections extends MongoMigration { + async up(): Promise { + // Drop legacy Discovery collection + await this.db.dropCollection('discovery_configs').catch(() => {}) + + // Drop legacy Provisioning collection + await this.db.dropCollection('provisioning_configs').catch(() => {}) + } + + async down(): Promise { + // Recreate legacy collections (empty — data must be restored from migration rollback) + const discoveryCollection = this.db.collection('discovery_configs') + await discoveryCollection.createIndex({ identifier: 1 }, { unique: true, name: 'idx_identifier' }) + + const provisioningCollection = this.db.collection('provisioning_configs') + await provisioningCollection.createIndex({ identifier: 1 }, { unique: true, name: 'idx_identifier' }) + } +} diff --git a/database/mongo_migrator.ts b/database/mongo_migrator.ts new file mode 100644 index 0000000..3c36d46 --- /dev/null +++ b/database/mongo_migrator.ts @@ -0,0 +1,119 @@ +import { Db } from 'mongodb' +import { readdir } from 'node:fs/promises' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' + +export abstract class MongoMigration { + protected db: Db + + constructor(db: Db) { + this.db = db + } + + abstract up(): Promise + abstract down(): Promise +} + +export class MigratorRunner { + private db: Db + private migrationsPath: string + + constructor(db: Db, migrationsPath: string) { + this.db = db + this.migrationsPath = migrationsPath + } + + private get migrationsCollection() { + return this.db.collection('_migrations') + } + + async run(): Promise { + const files = await this.getMigrationFiles() + const executed = await this.getExecutedMigrations() + const pending = files.filter((f) => !executed.includes(f)) + + if (pending.length === 0) { + return [] + } + + const batch = await this.getNextBatch() + const ran: string[] = [] + + for (const file of pending) { + const migration = await this.loadMigration(file) + await migration.up() + await this.migrationsCollection.insertOne({ + name: file, + batch, + executed_at: new Date(), + }) + ran.push(file) + } + + return ran + } + + async rollback(): Promise { + const lastBatch = await this.getLastBatch() + if (lastBatch === 0) return [] + + const migrations = await this.migrationsCollection + .find({ batch: lastBatch }) + .sort({ executed_at: -1 }) + .toArray() + + const rolledBack: string[] = [] + for (const record of migrations) { + const migration = await this.loadMigration(record.name) + await migration.down() + await this.migrationsCollection.deleteOne({ _id: record._id }) + rolledBack.push(record.name) + } + + return rolledBack + } + + async status(): Promise<{ name: string; batch: number | null; ran: boolean }[]> { + const files = await this.getMigrationFiles() + const executed = await this.migrationsCollection.find({}).toArray() + const executedMap = new Map(executed.map((e) => [e.name, e.batch])) + + return files.map((file) => ({ + name: file, + batch: executedMap.get(file) ?? null, + ran: executedMap.has(file), + })) + } + + private async getMigrationFiles(): Promise { + try { + const files = await readdir(this.migrationsPath) + return files.filter((f) => f.endsWith('.ts') || f.endsWith('.js')).sort() + } catch { + return [] + } + } + + private async getExecutedMigrations(): Promise { + const docs = await this.migrationsCollection.find({}).toArray() + return docs.map((d) => d.name) + } + + private async getNextBatch(): Promise { + const last = await this.getLastBatch() + return last + 1 + } + + private async getLastBatch(): Promise { + const doc = await this.migrationsCollection.find({}).sort({ batch: -1 }).limit(1).toArray() + return doc.length > 0 ? doc[0].batch : 0 + } + + private async loadMigration(file: string): Promise { + const filePath = join(this.migrationsPath, file) + const fileUrl = pathToFileURL(filePath).href + const module = await import(fileUrl) + const MigrationClass = module.default + return new MigrationClass(this.db) + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8864982 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,51 @@ +services: + mongodb: + image: mongo:7 + ports: + - "27017:27017" + environment: + MONGO_INITDB_ROOT_USERNAME: adam + MONGO_INITDB_ROOT_PASSWORD: adam + MONGO_INITDB_DATABASE: adam-acs + volumes: + - mongo_data:/data/db + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + + app: + build: . + ports: + - "8080:8080" + env_file: .env + environment: + MONGODB_URI: mongodb://adam:adam@mongodb:27017/adam-acs?authSource=admin + NODE_ENV: production + LOG_LEVEL: debug + PORT: 8080 + HOST: 0.0.0.0 + depends_on: + mongodb: + condition: service_healthy + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://localhost:8080/api/v1/checklife').then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))"] + interval: 5s + timeout: 3s + retries: 10 + start_period: 10s + + genieacs-sim: + build: + context: . + dockerfile: Dockerfile.genieacs-sim + depends_on: + app: + condition: service_healthy + entrypoint: ["sh", "-c", "exec ./genieacs-sim --acs-url \"http://app:8080/cwmp10\""] + profiles: + - test + +volumes: + mongo_data: diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..9be1be3 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,2 @@ +import { configApp } from '@adonisjs/eslint-config' +export default configApp() diff --git a/inertia/app/app.css b/inertia/app/app.css new file mode 100644 index 0000000..6493d76 --- /dev/null +++ b/inertia/app/app.css @@ -0,0 +1,33 @@ +/* Boosted is imported in app.ts via JS */ + +/* Layout adjustments */ +main { + padding-bottom: 60px; +} + +/* Navbar tweaks */ +#main-menu { + display: flex; + align-items: center; + flex-wrap: wrap; +} + +#main-menu .navbar-brand { + margin-right: 1rem; +} + +/* Table clickable rows */ +.clickable-row { + cursor: pointer; +} + +.clickable-row:hover { + background-color: rgba(0, 0, 0, 0.04); +} + +/* Multi-line table cells */ +td.multi-line { + white-space: normal; + word-wrap: break-word; + hyphens: auto; +} diff --git a/inertia/app/app.ts b/inertia/app/app.ts new file mode 100644 index 0000000..8d7dc1a --- /dev/null +++ b/inertia/app/app.ts @@ -0,0 +1,35 @@ +import { createApp, h } from 'vue' +import type { DefineComponent } from 'vue' +import { createInertiaApp } from '@inertiajs/vue3' +import DefaultLayout from '../layouts/MainLayout.vue' +import { createI18n } from 'vue-i18n' +import fr from '../locales/fr.json' +import en from '../locales/en.json' +import 'boosted/dist/css/boosted.min.css' +import './app.css' + +const i18n = createI18n({ + legacy: false, + locale: 'fr', + fallbackLocale: 'en', + messages: { fr, en }, +}) + +createInertiaApp({ + title: (title) => `${title} - ADAM ACS`, + resolve: (name) => { + const pages = import.meta.glob('../pages/**/*.vue', { eager: true }) as Record + const page = pages[`../pages/${name}.vue`] + if (!page) { + throw new Error(`Page not found: ${name}`) + } + page.default.layout = page.default.layout || DefaultLayout + return page + }, + setup({ el, App, props, plugin }) { + createApp({ render: () => h(App, props) }) + .use(plugin) + .use(i18n) + .mount(el) + }, +}) diff --git a/inertia/components/JsonEditor.vue b/inertia/components/JsonEditor.vue new file mode 100644 index 0000000..52931c3 --- /dev/null +++ b/inertia/components/JsonEditor.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/inertia/components/TitleBar.vue b/inertia/components/TitleBar.vue new file mode 100644 index 0000000..a32bcbc --- /dev/null +++ b/inertia/components/TitleBar.vue @@ -0,0 +1,31 @@ + + + diff --git a/inertia/components/configuration/ComplianceBadge.vue b/inertia/components/configuration/ComplianceBadge.vue new file mode 100644 index 0000000..e7e6836 --- /dev/null +++ b/inertia/components/configuration/ComplianceBadge.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/inertia/components/configuration/CriteriaEditor.vue b/inertia/components/configuration/CriteriaEditor.vue new file mode 100644 index 0000000..5732b4f --- /dev/null +++ b/inertia/components/configuration/CriteriaEditor.vue @@ -0,0 +1,177 @@ + + + + diff --git a/inertia/components/configuration/PolicyBadge.vue b/inertia/components/configuration/PolicyBadge.vue new file mode 100644 index 0000000..453f58f --- /dev/null +++ b/inertia/components/configuration/PolicyBadge.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/inertia/components/configuration/TagsInput.vue b/inertia/components/configuration/TagsInput.vue new file mode 100644 index 0000000..34a93d6 --- /dev/null +++ b/inertia/components/configuration/TagsInput.vue @@ -0,0 +1,42 @@ + + + diff --git a/inertia/css/app.css b/inertia/css/app.css new file mode 100644 index 0000000..002ca57 --- /dev/null +++ b/inertia/css/app.css @@ -0,0 +1 @@ +/* App styles */ diff --git a/inertia/layouts/MainLayout.vue b/inertia/layouts/MainLayout.vue new file mode 100644 index 0000000..5ed94a5 --- /dev/null +++ b/inertia/layouts/MainLayout.vue @@ -0,0 +1,232 @@ + + + + + diff --git a/inertia/locales/en.json b/inertia/locales/en.json new file mode 100644 index 0000000..a4f0b08 --- /dev/null +++ b/inertia/locales/en.json @@ -0,0 +1,125 @@ +{ + "header": { + "title": "ADAM ACS", + "devices": "Devices", + "configurations": "Configurations", + "firmware": "Firmware", + "commands": "Commands" + }, + "titles": { + "devices": "Devices", + "device": "Device", + "deviceDetail": "Device Detail", + "configurations": "Configurations", + "commands": "Commands", + "commandsFleet": "Fleet Commands", + "discoveryCreate": "Create Discovery Configuration", + "provisioningCreate": "Create Provisioning Configuration", + "firmware": "Firmware Catalog" + }, + "labels": { + "manufacturer": "Manufacturer", + "productClass": "Product Class", + "serialNumber": "Serial Number", + "externalIP": "External IP", + "softwareVersion": "Software Version", + "identifier": "Identifier", + "creationDate": "Creation Date", + "createdAt": "Created At", + "lastSeen": "Last Seen", + "status": "Status", + "action": "Action", + "mode": "Mode", + "priority": "Priority", + "parameters": "Parameters", + "properties": "Properties", + "name": "Name", + "value": "Value", + "type": "Type", + "writable": "Writable", + "discovery": "Discovery", + "provisioning": "Provisioning", + "criteria": "Criteria", + "modelPaths": "Model Paths", + "actions": "Actions", + "actionType": "Action Type", + "modelPath": "Model Path", + "deviceIdentifier": "Device Identifier", + "version": "Version", + "policy": "Policy", + "enabled": "Enabled", + "disabled": "Disabled", + "tags": "Tags", + "compliance": "Compliance", + "updatedAt": "Updated", + "search": "Search", + "general": "General", + "matchingCriteria": "Matching Criteria", + "inventoryPaths": "Inventory Paths", + "desiredParameters": "Desired Parameters", + "newConfiguration": "New Configuration", + "results": "result(s)", + "loading": "Loading…", + "noResults": "No configurations found.", + "parameterName": "Parameter name", + "once": "Once", + "maintain": "Maintain", + "firmwareEntries": "Firmware Entries", + "migrationRoutes": "Migration Routes", + "firmwareDetails": "Firmware Details", + "noFirmwareEntries": "No firmware entries found.", + "noMigrationRoutes": "No migration routes defined.", + "newFirmwareEntry": "New Entry", + "editFirmwareEntry": "Edit Entry", + "addRoute": "Add Route", + "fileSize": "File Size (bytes)", + "checksumType": "Checksum Type", + "firmwareUrl": "Firmware URL", + "checksum": "Checksum", + "filterByProductClass": "Filter by product class…", + "saveChanges": "Save changes" + }, + "button": { + "refresh": "Refresh", + "getModel": "Get Model", + "delete": "Delete", + "edit": "Edit", + "create": "Create", + "send": "Send", + "cancel": "Cancel", + "confirm": "Confirm", + "fleetManagement": "Fleet Management ({count})", + "addCriteria": "Add Criteria", + "addModelPath": "Add Path", + "addAction": "Add Action", + "addParameter": "Add Parameter", + "previous": "Previous", + "next": "Next" + }, + "checkbox": { + "selectAll": "Select All", + "selectRow": "Select row {index}" + }, + "placeholder": { + "search": "Search...", + "modelPath": "e.g. InternetGatewayDevice.DeviceInfo.", + "value": "Value" + }, + "options": { + "NI": "Non-Immediate (NI)", + "I": "Immediate (I)" + }, + "modal": { + "confirmDelete": "Confirm Deletion", + "confirmDeleteMessage": "Are you sure you want to delete this item?", + "getModel": "Get Model" + }, + "footer": { + "legal": "Legal Notice", + "privacy": "Privacy Policy" + }, + "language": { + "fr": "Français", + "en": "English" + } +} diff --git a/inertia/locales/fr.json b/inertia/locales/fr.json new file mode 100644 index 0000000..4909aea --- /dev/null +++ b/inertia/locales/fr.json @@ -0,0 +1,125 @@ +{ + "header": { + "title": "ADAM ACS", + "devices": "Équipements", + "configurations": "Configurations", + "firmware": "Firmware", + "commands": "Commandes" + }, + "titles": { + "devices": "Équipements", + "device": "Équipement", + "deviceDetail": "Détail de l'équipement", + "configurations": "Configurations", + "commands": "Commandes", + "commandsFleet": "Commandes flotte", + "discoveryCreate": "Créer une configuration de découverte", + "provisioningCreate": "Créer une configuration de provisionnement", + "firmware": "Catalogue Firmware" + }, + "labels": { + "manufacturer": "Fabricant", + "productClass": "Classe produit", + "serialNumber": "Numéro de série", + "externalIP": "IP externe", + "softwareVersion": "Version logicielle", + "identifier": "Identifiant", + "creationDate": "Date de création", + "createdAt": "Créé le", + "lastSeen": "Dernière connexion", + "status": "Statut", + "action": "Action", + "mode": "Mode", + "priority": "Priorité", + "parameters": "Paramètres", + "properties": "Propriétés", + "name": "Nom", + "value": "Valeur", + "type": "Type", + "writable": "Modifiable", + "discovery": "Découverte", + "provisioning": "Provisionnement", + "criteria": "Critères", + "modelPaths": "Chemins du modèle", + "actions": "Actions", + "actionType": "Type d'action", + "modelPath": "Chemin du modèle", + "deviceIdentifier": "Identifiant équipement", + "version": "Version", + "policy": "Politique", + "enabled": "Activé", + "disabled": "Désactivé", + "tags": "Tags", + "compliance": "Conformité", + "updatedAt": "Modifié", + "search": "Rechercher", + "general": "Général", + "matchingCriteria": "Critères de correspondance", + "inventoryPaths": "Chemins d'inventaire", + "desiredParameters": "Paramètres souhaités", + "newConfiguration": "Nouvelle configuration", + "results": "résultat(s)", + "loading": "Chargement…", + "noResults": "Aucune configuration trouvée.", + "parameterName": "Nom du paramètre", + "once": "Une fois", + "maintain": "Maintenir", + "firmwareEntries": "Entrées firmware", + "migrationRoutes": "Parcours de migration", + "firmwareDetails": "Détails du firmware", + "noFirmwareEntries": "Aucune entrée firmware trouvée.", + "noMigrationRoutes": "Aucun parcours de migration défini.", + "newFirmwareEntry": "Nouvelle entrée", + "editFirmwareEntry": "Modifier l'entrée", + "addRoute": "Ajouter un parcours", + "fileSize": "Taille du fichier (octets)", + "checksumType": "Type de checksum", + "firmwareUrl": "URL du firmware", + "checksum": "Checksum", + "filterByProductClass": "Filtrer par classe produit…", + "saveChanges": "Enregistrer" + }, + "button": { + "refresh": "Rafraîchir", + "getModel": "Récupérer le modèle", + "delete": "Supprimer", + "edit": "Éditer", + "create": "Créer", + "send": "Envoyer", + "cancel": "Annuler", + "confirm": "Confirmer", + "fleetManagement": "Gestion de flotte ({count})", + "addCriteria": "Ajouter un critère", + "addModelPath": "Ajouter un chemin", + "addAction": "Ajouter une action", + "addParameter": "Ajouter un paramètre", + "previous": "Précédent", + "next": "Suivant" + }, + "checkbox": { + "selectAll": "Tout sélectionner", + "selectRow": "Sélectionner la ligne {index}" + }, + "placeholder": { + "search": "Rechercher...", + "modelPath": "Ex: InternetGatewayDevice.DeviceInfo.", + "value": "Valeur" + }, + "options": { + "NI": "Non-immédiat (NI)", + "I": "Immédiat (I)" + }, + "modal": { + "confirmDelete": "Confirmer la suppression", + "confirmDeleteMessage": "Êtes-vous sûr de vouloir supprimer cet élément ?", + "getModel": "Récupérer le modèle" + }, + "footer": { + "legal": "Mentions légales", + "privacy": "Politique de confidentialité" + }, + "language": { + "fr": "Français", + "en": "English" + } +} diff --git a/inertia/pages/Commands.vue b/inertia/pages/Commands.vue new file mode 100644 index 0000000..3a57ceb --- /dev/null +++ b/inertia/pages/Commands.vue @@ -0,0 +1,162 @@ + + + diff --git a/inertia/pages/CommandsFleet.vue b/inertia/pages/CommandsFleet.vue new file mode 100644 index 0000000..321f511 --- /dev/null +++ b/inertia/pages/CommandsFleet.vue @@ -0,0 +1,118 @@ + + + diff --git a/inertia/pages/Configurations.vue b/inertia/pages/Configurations.vue new file mode 100644 index 0000000..5b44990 --- /dev/null +++ b/inertia/pages/Configurations.vue @@ -0,0 +1,99 @@ + + + diff --git a/inertia/pages/DeviceDetail.vue b/inertia/pages/DeviceDetail.vue new file mode 100644 index 0000000..3d36053 --- /dev/null +++ b/inertia/pages/DeviceDetail.vue @@ -0,0 +1,390 @@ + + + + + diff --git a/inertia/pages/Home.vue b/inertia/pages/Home.vue new file mode 100644 index 0000000..b944ab8 --- /dev/null +++ b/inertia/pages/Home.vue @@ -0,0 +1,144 @@ + + + diff --git a/inertia/pages/configurations/ConfigurationCreate.vue b/inertia/pages/configurations/ConfigurationCreate.vue new file mode 100644 index 0000000..08f52dc --- /dev/null +++ b/inertia/pages/configurations/ConfigurationCreate.vue @@ -0,0 +1,182 @@ + + + diff --git a/inertia/pages/configurations/ConfigurationDetail.vue b/inertia/pages/configurations/ConfigurationDetail.vue new file mode 100644 index 0000000..a66a6ec --- /dev/null +++ b/inertia/pages/configurations/ConfigurationDetail.vue @@ -0,0 +1,227 @@ + + + + diff --git a/inertia/pages/configurations/ConfigurationEdit.vue b/inertia/pages/configurations/ConfigurationEdit.vue new file mode 100644 index 0000000..34a2e66 --- /dev/null +++ b/inertia/pages/configurations/ConfigurationEdit.vue @@ -0,0 +1,172 @@ + + + diff --git a/inertia/pages/configurations/ConfigurationsList.vue b/inertia/pages/configurations/ConfigurationsList.vue new file mode 100644 index 0000000..5372d55 --- /dev/null +++ b/inertia/pages/configurations/ConfigurationsList.vue @@ -0,0 +1,242 @@ + + + diff --git a/inertia/pages/firmware_catalog/FirmwareCatalog.vue b/inertia/pages/firmware_catalog/FirmwareCatalog.vue new file mode 100644 index 0000000..22417d0 --- /dev/null +++ b/inertia/pages/firmware_catalog/FirmwareCatalog.vue @@ -0,0 +1,141 @@ + + + diff --git a/inertia/pages/firmware_catalog/FirmwareCatalogCreate.vue b/inertia/pages/firmware_catalog/FirmwareCatalogCreate.vue new file mode 100644 index 0000000..6a05b6c --- /dev/null +++ b/inertia/pages/firmware_catalog/FirmwareCatalogCreate.vue @@ -0,0 +1,95 @@ + + + diff --git a/inertia/pages/firmware_catalog/FirmwareCatalogEdit.vue b/inertia/pages/firmware_catalog/FirmwareCatalogEdit.vue new file mode 100644 index 0000000..9bab00f --- /dev/null +++ b/inertia/pages/firmware_catalog/FirmwareCatalogEdit.vue @@ -0,0 +1,104 @@ + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..913f29c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,12303 @@ +{ + "name": "adam-acs", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "adam-acs", + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "@adonisjs/auth": "^10.1.0", + "@adonisjs/core": "^7.3.3", + "@adonisjs/cors": "^3.0.0", + "@adonisjs/inertia": "^4.2.0", + "@adonisjs/lucid": "^22.4.2", + "@adonisjs/session": "^8.1.0", + "@adonisjs/shield": "^9.0.0", + "@adonisjs/static": "^2.0.1", + "@adonisjs/vite": "^5.1.1", + "@inertiajs/vue3": "^2.3.8", + "@japa/api-client": "^3.2.1", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/auto-instrumentations-node": "^0.76.0", + "@opentelemetry/sdk-node": "^0.218.0", + "@popperjs/core": "^2.11.8", + "@tuyau/core": "^1.2.2", + "@vinejs/vine": "^4.4.0", + "boosted": "^5.3.8", + "edge.js": "^6.4.0", + "fast-xml-parser": "^5.8.0", + "mongodb": "^7.2.0", + "reflect-metadata": "^0.2.2", + "vanilla-jsoneditor": "^3.12.0", + "vue": "^3.5.35", + "vue-i18n": "^11.4.4", + "xml-formatter": "^3.7.0", + "xml2js": "^0.6.2" + }, + "devDependencies": { + "@adonisjs/assembler": "^8.4.0", + "@adonisjs/eslint-config": "^3.1.0", + "@adonisjs/prettier-config": "^1.5.0", + "@adonisjs/tsconfig": "^2.0.0", + "@japa/assert": "^4.2.0", + "@japa/plugin-adonisjs": "^5.2.0", + "@japa/runner": "^5.3.0", + "@poppinss/ts-exec": "^1.4.4", + "@types/node": "~25.9.1", + "@types/xml2js": "^0.4.14", + "@vitejs/plugin-vue": "^6.0.7", + "eslint": "^10.4.0", + "fast-check": "^4.8.0", + "hot-hook": "^1.0.0", + "pino-pretty": "^13.1.3", + "prettier": "^3.8.3", + "sass": "^1.100.0", + "typescript": "~6.0.3", + "vitest": "^4.1.7", + "youch": "^4.1.1" + } + }, + "node_modules/@adonisjs/ace": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@adonisjs/ace/-/ace-14.1.0.tgz", + "integrity": "sha512-8N8z1YKePBiXz7wLxHFz/HSqjCRSL/9Vzs4XQt8gk8G17u4PXwNncWt0vSgYEcDrvPAt+QOavY1vMeKOOWe29w==", + "license": "MIT", + "dependencies": { + "@poppinss/cliui": "^6.8.0", + "@poppinss/hooks": "^7.3.0", + "@poppinss/macroable": "^1.1.2", + "@poppinss/prompts": "^3.1.6", + "@poppinss/utils": "^7.0.1", + "fastest-levenshtein": "^1.0.16", + "jsonschema": "^1.5.0", + "string-width": "^8.2.0", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "youch": "^4.1.0-beta.11 || ^4.1.0" + } + }, + "node_modules/@adonisjs/application": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@adonisjs/application/-/application-9.0.1.tgz", + "integrity": "sha512-bSUC8vcECEr9qvvHeMAhjvH73ngAm5c6kOnEJYNCczP6rgG7AdH03MGtrY2ewKYDyS/QepNpDKj4oVE2XtxVtw==", + "license": "MIT", + "dependencies": { + "@poppinss/hooks": "^7.3.0", + "@poppinss/macroable": "^1.1.2", + "@poppinss/utils": "^7.0.1", + "glob-parent": "^6.0.2", + "tempura": "^0.4.1" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@adonisjs/assembler": "^8.0.0-next.23 || ^8.0.0", + "@adonisjs/config": "^6.1.0-next.0 || ^6.0.0", + "@adonisjs/fold": "^11.0.0-next.3 || ^11.0.0" + }, + "peerDependenciesMeta": { + "@adonisjs/assembler": { + "optional": true + } + } + }, + "node_modules/@adonisjs/assembler": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@adonisjs/assembler/-/assembler-8.4.0.tgz", + "integrity": "sha512-Nxi6UU2fd/Wq8iLb+FwicK+7ePyvZcmtbJaT25RhGbgSZ2tgbxzaI7YF4TbaLKDIsF48DOcTP0dTfUIcgjBchw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adonisjs/env": "^7.0.0", + "@antfu/install-pkg": "^1.1.0", + "@ast-grep/napi": "^0.42.0", + "@poppinss/cliui": "^6.8.1", + "@poppinss/hooks": "^7.3.0", + "@poppinss/utils": "^7.0.1", + "chokidar": "^5.0.0", + "dedent": "^1.7.2", + "execa": "^9.6.1", + "fast-glob": "^3.3.3", + "fdir": "^6.5.0", + "get-port": "^7.2.0", + "get-tsconfig": "^4.13.7", + "import-meta-resolve": "^4.2.0", + "junk": "^4.0.1", + "open": "^11.0.0", + "parse-imports": "^3.0.0", + "picomatch": "^4.0.4", + "pretty-hrtime": "^1.0.3", + "tmp-cache": "^1.1.0", + "ts-morph": "^27.0.2" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "typescript": "^5.0.0 || ^6.0.0" + } + }, + "node_modules/@adonisjs/auth": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@adonisjs/auth/-/auth-10.1.0.tgz", + "integrity": "sha512-H92C4HLWBpvxtQrgnsdWdWfoGu2bM/j+nyk/tHmpxH9ETkNOH0z3dRfEebLJI63vtTDpRp5cwiI0bzn32gUQiA==", + "license": "MIT", + "dependencies": { + "@adonisjs/presets": "^3.0.0", + "basic-auth": "^2.0.1" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@adonisjs/assembler": "^8.0.0-next.26 || ^8.0.0", + "@adonisjs/core": "^7.0.0-next.16 || ^7.0.0", + "@adonisjs/i18n": "^3.0.0-next.2 || ^3.0.0", + "@adonisjs/lucid": "^22.0.0-next.1 || ^22.0.0", + "@adonisjs/session": "^8.0.0-next.1 || ^8.0.0", + "@japa/api-client": "^3.1.1", + "@japa/browser-client": "^2.2.0", + "@japa/plugin-adonisjs": "^5.1.0-next.0 || ^5.1.0" + }, + "peerDependenciesMeta": { + "@adonisjs/assembler": { + "optional": true + }, + "@adonisjs/i18n": { + "optional": true + }, + "@adonisjs/lucid": { + "optional": true + }, + "@adonisjs/session": { + "optional": true + }, + "@japa/api-client": { + "optional": true + }, + "@japa/browser-client": { + "optional": true + }, + "@japa/plugin-adonisjs": { + "optional": true + } + } + }, + "node_modules/@adonisjs/bodyparser": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@adonisjs/bodyparser/-/bodyparser-11.0.4.tgz", + "integrity": "sha512-amqMPVFbzxkOoCMbkFs5sC+rTu7cER2rSv+1v8dsGKszolk5lnGuyNjkha1Lqksvw1qykIIbC1fXr37B9WJTdg==", + "license": "MIT", + "dependencies": { + "@poppinss/macroable": "^1.1.2", + "@poppinss/middleware": "^3.2.7", + "@poppinss/multiparty": "^3.0.0", + "@poppinss/qs": "^6.15.0", + "@poppinss/utils": "^7.0.1", + "file-type": "^22.0.1", + "inflation": "^2.1.0", + "media-typer": "^1.1.0", + "raw-body": "^3.0.2" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@adonisjs/http-server": "^8.0.0-next.17 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/@adonisjs/config": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@adonisjs/config/-/config-6.1.0.tgz", + "integrity": "sha512-YVDRL8xHCtM6iMnAefOBaz6iXVpojwBPDQWPKxnVSucycYeNGrGitJiLy+cGaeAU7Gjm8al9SJRJt3rRPr5PKg==", + "license": "MIT", + "dependencies": { + "@poppinss/utils": "^7.0.0" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/@adonisjs/core": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@adonisjs/core/-/core-7.3.3.tgz", + "integrity": "sha512-FSrfhuj8rQM96/L3aiVIH2Um9CSHjfJukopaQAyllGGUs/ct1bQZb3pursMRunYXnpEJzbX+YC/oCP4QcMEXog==", + "license": "MIT", + "dependencies": { + "@adonisjs/ace": "^14.1.0", + "@adonisjs/application": "^9.0.0", + "@adonisjs/bodyparser": "^11.0.2", + "@adonisjs/config": "^6.1.0", + "@adonisjs/env": "^7.0.0", + "@adonisjs/events": "^10.2.0", + "@adonisjs/fold": "^11.0.0", + "@adonisjs/hash": "^10.1.0", + "@adonisjs/health": "^3.1.0", + "@adonisjs/http-server": "^9.0.0", + "@adonisjs/http-transformers": "^2.3.1", + "@adonisjs/logger": "^7.1.1", + "@adonisjs/repl": "^5.0.0", + "@boringnode/encryption": "^1.0.0", + "@poppinss/colors": "^4.1.6", + "@poppinss/dumper": "^0.7.0", + "@poppinss/macroable": "^1.1.2", + "@poppinss/utils": "^7.0.1", + "@sindresorhus/is": "^8.1.0", + "@types/he": "^1.2.3", + "error-stack-parser-es": "^1.0.5", + "he": "^1.2.0", + "pretty-hrtime": "^1.0.3", + "string-width": "^8.2.1" + }, + "bin": { + "adonis-kit": "build/toolkit/main.js" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@adonisjs/assembler": "^8.0.0-next.23 || ^8.0.0", + "@vinejs/vine": "^4.0.0", + "argon2": "^0.44.0", + "bcrypt": "^6.0.0", + "edge.js": "^6.2.0", + "pino-pretty": "^13.1.3", + "youch": "^4.1.0-beta.13 || ^4.1.0" + }, + "peerDependenciesMeta": { + "@adonisjs/assembler": { + "optional": true + }, + "@vinejs/vine": { + "optional": true + }, + "argon2": { + "optional": true + }, + "bcrypt": { + "optional": true + }, + "edge.js": { + "optional": true + }, + "pino-pretty": { + "optional": true + }, + "youch": { + "optional": true + } + } + }, + "node_modules/@adonisjs/cors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@adonisjs/cors/-/cors-3.0.0.tgz", + "integrity": "sha512-S3LS+zZ2SziwVvilnkKg4DjmU+df1xqdZRDSQjUKJr5hYEOVAlnci0kHTiz2szxB+QiseQXr/XfN/D5ooMMlYQ==", + "license": "MIT", + "engines": { + "node": ">=18.16.0" + }, + "peerDependencies": { + "@adonisjs/assembler": "^8.0.0-next.7 || ^8.0.0", + "@adonisjs/core": "^7.0.0-next.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@adonisjs/assembler": { + "optional": true + } + } + }, + "node_modules/@adonisjs/env": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@adonisjs/env/-/env-7.0.0.tgz", + "integrity": "sha512-9lSGONI4B1E7LxyVZiUd1yCH9BOri4Ybp4b9x3ojT9AkKfYwqvj4S2USIvFAlkE7eHUC2WMvPgMLX17342Y3ww==", + "license": "MIT", + "dependencies": { + "@poppinss/utils": "^7.0.0", + "@poppinss/validator-lite": "^2.1.2", + "split-lines": "^3.0.0" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/@adonisjs/eslint-config": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@adonisjs/eslint-config/-/eslint-config-3.1.0.tgz", + "integrity": "sha512-vybyTN/doUTh8mmbontXG1Qi2n32VHb2rqRQ72T40cWym+qMwJhKTwi64TBJTuo2Lg1lnjXrouc9e9VDDrIcPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adonisjs/eslint-plugin": "^2.2.2", + "@stylistic/eslint-plugin": "^5.10.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.5", + "eslint-plugin-unicorn": "^64.0.0", + "typescript-eslint": "^8.60.0" + }, + "peerDependencies": { + "eslint": "^9.9.0 || ^10.0.0", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-vue": "^10.9.1", + "prettier": "^3.8.1" + }, + "peerDependenciesMeta": { + "eslint-plugin-react": { + "optional": true + }, + "eslint-plugin-react-hooks": { + "optional": true + }, + "eslint-plugin-vue": { + "optional": true + } + } + }, + "node_modules/@adonisjs/eslint-plugin": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@adonisjs/eslint-plugin/-/eslint-plugin-2.2.2.tgz", + "integrity": "sha512-OAIrljEpbhyikG+BQ8r7195GoRDPmMEBUfSfr6ajM6IPtQMPAb/oKzeXF8XTy2CxupUoGhMd2n8+sx/pgL1m4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.56.0", + "micromatch": "^4.0.8", + "read-package-up": "^12.0.0" + }, + "engines": { + "node": ">=20.6.0" + }, + "peerDependencies": { + "eslint": "^9.9.1 || ^10.0.0" + } + }, + "node_modules/@adonisjs/events": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@adonisjs/events/-/events-10.2.0.tgz", + "integrity": "sha512-SzwzbmTLsybSZd47zZMZ3df7puwhY7D8vZ5Uy79SiHjLKbr2eVzUuKjjoYB6/0pZu6IwK9Qx06dI43sl+tLoDw==", + "license": "MIT", + "dependencies": { + "@poppinss/utils": "^7.0.0", + "@sindresorhus/is": "^7.2.0", + "emittery": "^1.2.0" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@adonisjs/application": "^9.0.0-next.14 || ^9.0.0", + "@adonisjs/fold": "^11.0.0-next.4 || ^11.0.0" + } + }, + "node_modules/@adonisjs/events/node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@adonisjs/fold": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@adonisjs/fold/-/fold-11.0.0.tgz", + "integrity": "sha512-RnmDPWz2imVp/B74xitxCPqTdoP07bZvfJe1bh9CD9Rmia4jjDvehZF67KFyGNMZ24MuKasqs3jOcM1vGJp0GA==", + "license": "MIT", + "dependencies": { + "@poppinss/utils": "^7.0.0", + "parse-imports": "^3.0.0" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/@adonisjs/hash": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@adonisjs/hash/-/hash-10.1.0.tgz", + "integrity": "sha512-lW0lGwpscu9PUo3Sb3PF580jYU29I9wsPn3dIKzZbTVedqkpaNpBK3YKedwZYkDpYmiyYXi08eTTNhdWjiGLQw==", + "license": "MIT", + "dependencies": { + "@phc/format": "^1.0.0", + "@poppinss/utils": "^7.0.0" + }, + "engines": { + "node": ">=20.6.0" + }, + "peerDependencies": { + "argon2": "^0.31.2 || ^0.41.0 || ^0.43.0 || ^0.44.0", + "bcrypt": "^5.1.1 || ^6.0.0" + }, + "peerDependenciesMeta": { + "argon2": { + "optional": true + }, + "bcrypt": { + "optional": true + } + } + }, + "node_modules/@adonisjs/health": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@adonisjs/health/-/health-3.1.0.tgz", + "integrity": "sha512-m3doBnSwi/l9v1DO79xmjAoSPl9b2XCp+crGwF8QUlhe5CgWgtfnL0SeFNEiZGliD3c4gYdihKz4Pnydctva8A==", + "license": "MIT", + "dependencies": { + "@poppinss/utils": "^7.0.0", + "check-disk-space": "^3.4.0" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/@adonisjs/http-server": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@adonisjs/http-server/-/http-server-9.0.2.tgz", + "integrity": "sha512-x+kfbX8wzy/p2TgbhhnQ5E93/eIioIII0cgOH9l0nqlYRktJkFR0PvH+RmvvX05OVhkt+TZ0jz2t+2EgreDkaw==", + "license": "MIT", + "dependencies": { + "@poppinss/macroable": "^1.1.2", + "@poppinss/matchit": "^3.2.0", + "@poppinss/middleware": "^3.2.7", + "@poppinss/qs": "^6.15.0", + "@poppinss/utils": "^7.0.1", + "@sindresorhus/is": "^8.1.0", + "content-disposition": "^1.1.0", + "cookie-es": "^3.1.1", + "destroy": "^1.2.0", + "encodeurl": "^2.0.0", + "etag": "^1.8.1", + "fresh": "^0.5.2", + "mime-types": "^3.0.2", + "on-finished": "^2.4.1", + "proxy-addr": "^2.0.7", + "tmp-cache": "^1.1.0", + "type-is": "^2.1.0", + "vary": "^1.1.2" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@adonisjs/application": "^9.0.0-next.14 || ^9.0.0", + "@adonisjs/events": "^10.1.0-next.4 || ^10.1.0", + "@adonisjs/fold": "^11.0.0-next.4 || ^11.0.0", + "@adonisjs/logger": "^7.1.0-next.3 || ^7.1.0", + "@boringnode/encryption": "^0.2.0 || ^1.0.0", + "youch": "^4.1.0-beta.13 || ^4.1.0" + }, + "peerDependenciesMeta": { + "youch": { + "optional": true + } + } + }, + "node_modules/@adonisjs/http-transformers": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@adonisjs/http-transformers/-/http-transformers-2.3.1.tgz", + "integrity": "sha512-N3gBcKyoPHDeVvVIedTzc+ARSUURwNGTPid/e3iLdM4v/myoSnXd76FL/GGzMmjfqqxegpjI2IEMibG7ylrKSQ==", + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.3", + "@poppinss/types": "^1.2.1" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@adonisjs/fold": "^11.0.0-next.2" + } + }, + "node_modules/@adonisjs/inertia": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@adonisjs/inertia/-/inertia-4.2.0.tgz", + "integrity": "sha512-AL5IWsoA/qf34AMHf3H+YQydm9v7pj3daOeWLuRn6FxwSkald1zEo5lS7aBD4Nbsn3fJKPKvBlO9BSeIAw1FYQ==", + "license": "MIT", + "dependencies": { + "@poppinss/utils": "^7.0.0", + "edge-error": "^4.0.2", + "html-entities": "^2.6.0" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@adonisjs/assembler": "^8.0.0-next.29 || ^8.0.0", + "@adonisjs/core": "^7.0.0-next.23 || ^7.0.0", + "@adonisjs/session": "^8.0.0-next.2 || ^8.0.0", + "@adonisjs/vite": "^5.1.0-next.2 || ^5.1.0", + "@inertiajs/react": "^2.3.8", + "@inertiajs/vue3": "^2.3.8", + "@japa/api-client": "^3.1.1", + "@japa/plugin-adonisjs": "^5.1.0-next.1 || ^5.1.0", + "@tuyau/core": "^1.0.0-beta.10 || ^1.0.0", + "edge.js": "^6.4.0", + "react": "^19.2.3", + "vue": "^3.5.17" + }, + "peerDependenciesMeta": { + "@adonisjs/assembler": { + "optional": true + }, + "@inertiajs/react": { + "optional": true + }, + "@inertiajs/vue3": { + "optional": true + }, + "@japa/api-client": { + "optional": true + }, + "@japa/plugin-adonisjs": { + "optional": true + }, + "@tuyau/core": { + "optional": true + }, + "react": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@adonisjs/logger": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@adonisjs/logger/-/logger-7.1.1.tgz", + "integrity": "sha512-MmUlp8xBMT6zZy0+vnQcQjHIlNfU4pUJARlINr7Bqha9BvhIn03QZgJL5QJ+kJe1tl6ZpNAryoRTJUiOk/wINQ==", + "license": "MIT", + "dependencies": { + "@poppinss/utils": "^7.0.0", + "abstract-logging": "^2.0.1", + "pino": "^10.3.1" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "pino-pretty": "^13.1.1" + }, + "peerDependenciesMeta": { + "pino-pretty": { + "optional": true + } + } + }, + "node_modules/@adonisjs/lucid": { + "version": "22.4.2", + "resolved": "https://registry.npmjs.org/@adonisjs/lucid/-/lucid-22.4.2.tgz", + "integrity": "sha512-3ztENqBQIFSGAWREHKCGbL1g4ehtsxxsuFX72Uf5bq+oSc0D6UcumjzYpU8TPyBxhmgS6HuRSfz2rG42iuz7AA==", + "license": "MIT", + "dependencies": { + "@adonisjs/presets": "^3.0.0", + "@faker-js/faker": "^10.4.0", + "@poppinss/hooks": "^7.3.0", + "@poppinss/macroable": "^1.1.2", + "@poppinss/qs": "^6.15.0", + "@poppinss/utils": "^7.0.1", + "deepmerge": "^4.3.1", + "fast-deep-equal": "^3.1.3", + "igniculus": "^1.5.0", + "kleur": "^4.1.5", + "knex": "^3.2.8", + "knex-dynamic-connection": "^5.0.1", + "pretty-hrtime": "^1.0.3", + "slash": "^5.1.0", + "tarn": "^3.0.2" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@adonisjs/assembler": "^8.0.0-next.29 || ^8.0.0", + "@adonisjs/core": "^7.0.0-next.21 || ^7.0.0", + "@vinejs/vine": "^3.0.0 || ^4.0.0", + "luxon": "^3.4.4" + }, + "peerDependenciesMeta": { + "@adonisjs/assembler": { + "optional": true + }, + "@vinejs/vine": { + "optional": true + }, + "luxon": { + "optional": true + } + } + }, + "node_modules/@adonisjs/presets": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@adonisjs/presets/-/presets-3.0.0.tgz", + "integrity": "sha512-+gVIvyEiM7jiN5Gb93200TAC8ed3vZIPfxFFo0pIVgX8k40BleuYhWxFhI6TPocVXXIIpw2JuMFV2Pqjk36W2A==", + "license": "MIT", + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@adonisjs/assembler": "^8.0.0-next.9 || ^8.0.0", + "@adonisjs/core": "^7.0.0-next.1 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@adonisjs/assembler": { + "optional": true + } + } + }, + "node_modules/@adonisjs/prettier-config": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@adonisjs/prettier-config/-/prettier-config-1.5.0.tgz", + "integrity": "sha512-im5V503HAaWSqmDWXV3k40KMCXvgurGNExkjGjvEkl2N7Wp+RcBN3pC6PAWUA1j4iukp4azzuuuR2tPFgnIkbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-edge": "^1.0.0" + } + }, + "node_modules/@adonisjs/repl": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@adonisjs/repl/-/repl-5.0.0.tgz", + "integrity": "sha512-cPp0w5svYUA3M1gdxQBO2Mx5g+SZfPweqKCAxk7C1Os/Qu67JKgWqXqNnl1q0Tzv/l0L19Ms1C7ivzQxeBNajg==", + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.6", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/@adonisjs/session": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@adonisjs/session/-/session-8.1.0.tgz", + "integrity": "sha512-hTd5m49Ro8XhxVQN0yyCeDOihDhj+mFIVltzeWPXkCxK19JHAtA++TAygxSUEhvcm56IVS43XWNE1lNIOPjMJA==", + "license": "MIT", + "dependencies": { + "@poppinss/macroable": "^1.1.2", + "@poppinss/utils": "^7.0.1" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@adonisjs/assembler": "^8.0.0-next.26 || ^8.0.0", + "@adonisjs/core": "^7.0.0-next.16 || ^7.0.0", + "@adonisjs/lucid": "^22.0.0-next.0 || ^22.0.0", + "@adonisjs/redis": "^10.0.0-next.2 || ^10.0.0", + "@aws-sdk/client-dynamodb": "^3.955.0", + "@aws-sdk/util-dynamodb": "^3.955.0", + "@japa/api-client": "^3.1.0", + "@japa/browser-client": "^2.0.3", + "@japa/plugin-adonisjs": "^5.1.0-next.0 || ^5.1.0", + "edge.js": "^6.4.0" + }, + "peerDependenciesMeta": { + "@adonisjs/assembler": { + "optional": true + }, + "@adonisjs/lucid": { + "optional": true + }, + "@adonisjs/redis": { + "optional": true + }, + "@aws-sdk/client-dynamodb": { + "optional": true + }, + "@aws-sdk/util-dynamodb": { + "optional": true + }, + "@japa/api-client": { + "optional": true + }, + "@japa/browser-client": { + "optional": true + }, + "@japa/plugin-adonisjs": { + "optional": true + }, + "edge.js": { + "optional": true + } + } + }, + "node_modules/@adonisjs/shield": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@adonisjs/shield/-/shield-9.0.0.tgz", + "integrity": "sha512-RmupWRspYGyzS/fAC0QspL7+Uhp6IaidvxE4CYbuqO43PBPCxhC47VjXML720B8hcps7ClK9RWjdMiJkUrn7fA==", + "license": "MIT", + "dependencies": { + "csrf": "^3.1.0" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@adonisjs/assembler": "^8.0.0-next.26 || ^8.0.0", + "@adonisjs/core": "^7.0.0-next.16 || ^7.0.0", + "@adonisjs/i18n": "^3.0.0-next.2 || ^3.0.0", + "@adonisjs/session": "^8.0.0-next.1 || ^8.0.0", + "@japa/api-client": "^3.1.1", + "@japa/plugin-adonisjs": "^5.1.0-next.0 || ^5.1.0", + "edge.js": "^6.4.0" + }, + "peerDependenciesMeta": { + "@adonisjs/assembler": { + "optional": true + }, + "@adonisjs/i18n": { + "optional": true + }, + "@japa/api-client": { + "optional": true + }, + "@japa/plugin-adonisjs": { + "optional": true + }, + "edge.js": { + "optional": true + } + } + }, + "node_modules/@adonisjs/static": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@adonisjs/static/-/static-2.0.1.tgz", + "integrity": "sha512-pXSyQgha1yfyQDXPp0ywv7hJqk4JgCCkzWLkku2PW3Y6i/9GUTyzzNTCpz29loZzSF0b/EpiHXQE/sqBhW+p7w==", + "license": "MIT", + "dependencies": { + "serve-static": "^2.2.1" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@adonisjs/assembler": "^8.0.0-next.7 || ^8.0.0", + "@adonisjs/core": "^7.0.0-next.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@adonisjs/assembler": { + "optional": true + } + } + }, + "node_modules/@adonisjs/tsconfig": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@adonisjs/tsconfig/-/tsconfig-2.0.0.tgz", + "integrity": "sha512-Uz8qvB6KR9otCh9zei2VEj7tPwdsrT7R+voYoN4tUfEijWRdTNgJ8d1CtyLKHaggCCOwZIwZLVklV/h2FDHgNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@adonisjs/vite": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@adonisjs/vite/-/vite-5.1.1.tgz", + "integrity": "sha512-UeDHMTWPLhWcisnNRcTfo4ib17QXlGepnvN+peei5Vm6USx/9KP50fZPfyf/8+Bqu68ur/6wjTEUpf62/SqlzQ==", + "license": "MIT", + "dependencies": { + "@poppinss/utils": "^7.0.0", + "edge-error": "^4.0.2", + "vite-plugin-restart": "^2.0.0" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@adonisjs/assembler": "^8.0.0-next.10 || ^8.0.0", + "@adonisjs/core": "^7.0.0-next.3 || ^7.0.0", + "@adonisjs/shield": "^9.0.0-next.0 || ^9.0.0", + "edge.js": "^6.0.1", + "vite": "^7.0.0" + }, + "peerDependenciesMeta": { + "@adonisjs/assembler": { + "optional": true + }, + "@adonisjs/shield": { + "optional": true + }, + "edge.js": { + "optional": true + } + } + }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@arr/every": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@arr/every/-/every-1.0.1.tgz", + "integrity": "sha512-UQFQ6SgyJ6LX42W8rHCs8KVc0JS0tzVL9ct4XYedJukskYVWTo49tNiMEK9C2HTyarbNiT/RVIRSY82vH+6sTg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@ast-grep/napi": { + "version": "0.42.3", + "resolved": "https://registry.npmjs.org/@ast-grep/napi/-/napi-0.42.3.tgz", + "integrity": "sha512-iWuH84nptrXpfrpYAsc93CD6JRnyRLYWFRzP9j1KRMK2hspVMBCbmfRyEoMd2B572dd2sSF3eXIQvY/CqIxYmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@ast-grep/napi-darwin-arm64": "0.42.3", + "@ast-grep/napi-darwin-x64": "0.42.3", + "@ast-grep/napi-linux-arm64-gnu": "0.42.3", + "@ast-grep/napi-linux-arm64-musl": "0.42.3", + "@ast-grep/napi-linux-x64-gnu": "0.42.3", + "@ast-grep/napi-linux-x64-musl": "0.42.3", + "@ast-grep/napi-win32-arm64-msvc": "0.42.3", + "@ast-grep/napi-win32-ia32-msvc": "0.42.3", + "@ast-grep/napi-win32-x64-msvc": "0.42.3" + } + }, + "node_modules/@ast-grep/napi-darwin-arm64": { + "version": "0.42.3", + "resolved": "https://registry.npmjs.org/@ast-grep/napi-darwin-arm64/-/napi-darwin-arm64-0.42.3.tgz", + "integrity": "sha512-pcTBI8UiNQ9xVOYfu3Ww5KXcrrHV/jJqECQ6efpJpzTGwW+mOpzgihfrq5GKjbMONhr0OaRARCMwEsvKgBXCpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ast-grep/napi-darwin-x64": { + "version": "0.42.3", + "resolved": "https://registry.npmjs.org/@ast-grep/napi-darwin-x64/-/napi-darwin-x64-0.42.3.tgz", + "integrity": "sha512-mQeDYeMWUruf0oiYJejzRzG9BEXjopbhPML8rJombviX6ePmulNjh6SDX/Mxe+vqNGt0JHXgdYtsAtDu9d0LQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ast-grep/napi-linux-arm64-gnu": { + "version": "0.42.3", + "resolved": "https://registry.npmjs.org/@ast-grep/napi-linux-arm64-gnu/-/napi-linux-arm64-gnu-0.42.3.tgz", + "integrity": "sha512-T2/8eEvFTKSEXd9Dql34/bsyWogFYh7IGsuOoqYNJOcYwT+TPCLTWPZdcpGqrdYdTYT92qX+4H0dUmaZ+TuOPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ast-grep/napi-linux-arm64-musl": { + "version": "0.42.3", + "resolved": "https://registry.npmjs.org/@ast-grep/napi-linux-arm64-musl/-/napi-linux-arm64-musl-0.42.3.tgz", + "integrity": "sha512-O/K4p/GB5MiK2NE8s7pCBDpZ0EOUrMkMF0HbZKi/HIXH9HQdkoYYO+NqKumk92YNr3gUo8fkGkIG6v4wowANvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ast-grep/napi-linux-x64-gnu": { + "version": "0.42.3", + "resolved": "https://registry.npmjs.org/@ast-grep/napi-linux-x64-gnu/-/napi-linux-x64-gnu-0.42.3.tgz", + "integrity": "sha512-HnljxyLZmt7t7al46j/j/fLj7e9qG8Nlk6/vrj+AD6nRqoeu7pW4YHcQA9NXst79GBz2CU0nba1WEtjUfkNgGg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ast-grep/napi-linux-x64-musl": { + "version": "0.42.3", + "resolved": "https://registry.npmjs.org/@ast-grep/napi-linux-x64-musl/-/napi-linux-x64-musl-0.42.3.tgz", + "integrity": "sha512-6WLvV3ErwWXQcpMS4BEVjD7QiTGO2Hr3LuDouQoUUBSMu9gI0bBAZGQaqGfWidI+aXYZ0eL5KmCCDB4X6Oyskg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ast-grep/napi-win32-arm64-msvc": { + "version": "0.42.3", + "resolved": "https://registry.npmjs.org/@ast-grep/napi-win32-arm64-msvc/-/napi-win32-arm64-msvc-0.42.3.tgz", + "integrity": "sha512-LiDOhkvBEeXi+yJomrfUYgnLvCntrWianruKXMzIK1QpxWmFSVC2uwWacMbq2v/DTk0MlQ09225yD6sAuL5pEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ast-grep/napi-win32-ia32-msvc": { + "version": "0.42.3", + "resolved": "https://registry.npmjs.org/@ast-grep/napi-win32-ia32-msvc/-/napi-win32-ia32-msvc-0.42.3.tgz", + "integrity": "sha512-DOAzKQPWQF42a9jgvCz4ajitARcMm9j2xKzcSNwkgZjrconAsRphqCTG3qkfEXX2x7KaVHAqoUp4XgPZXZ5vlA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ast-grep/napi-win32-x64-msvc": { + "version": "0.42.3", + "resolved": "https://registry.npmjs.org/@ast-grep/napi-win32-x64-msvc/-/napi-win32-x64-msvc-0.42.3.tgz", + "integrity": "sha512-1bl5eAbRGI5VRlKzXVPiq+sFJezh60O3ur6G+w8elA20uEZ7ik+WLQZu9T5tScFxOfCQQcjV9v4fQiaGNgHJsA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@boringnode/encryption": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@boringnode/encryption/-/encryption-1.0.0.tgz", + "integrity": "sha512-wGGOE7ywA4W6KAVoVC7s1P4ULzFLIQA/JvthGAa41EA0CaH7kGGawkBB5t5tvWopgBNMhOpIg3uxvULxqf2rQw==", + "license": "MIT", + "dependencies": { + "@poppinss/utils": "^7.0.0-next.7" + }, + "engines": { + "node": ">=20.6" + } + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.2", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.2.tgz", + "integrity": "sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz", + "integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.6.tgz", + "integrity": "sha512-6Kp7r6XfCi/D/5sdXieMfg9pJU1bUEx96WITuLU6ESaKizCz0QHFMjY/TaFSbigDdEAIgi93itLBIUETP4oK+A==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.42.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.0.tgz", + "integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.43.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.0.tgz", + "integrity": "sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.6.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@faker-js/faker": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.4.0.tgz", + "integrity": "sha512-sDBWI3yLy8EcDzgobvJTWq1MJYzAkQdpjXuPukga9wXonhpMRvd1Izuo2Qgwey2OiEoRIBr35RMU9HJRoOHzpw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.2.0.tgz", + "integrity": "sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-7.2.0.tgz", + "integrity": "sha512-iycmlN51EULlQ4D/UU9WZnHiN0CvjJ2TuuCrAh+1MVdzD+4ViKYH2deNAll4XAAYlZa8WAefHR5taSK8hYmSMw==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.2.0.tgz", + "integrity": "sha512-YTVITFGN0/24PxzXrwqCgnyd7njDuzp5ZvaCx5nq/jg55kUYd94Nj8UTchBdBofi/L0nwRfjGOg0E41d2u9T1w==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inertiajs/core": { + "version": "2.3.24", + "resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-2.3.24.tgz", + "integrity": "sha512-xAlUl5+RKtdbutEgsmdWa6HmnvjIGcWTrvfLj/3Icy3/7bSH3aiI+kuYPs17LBq/SMaXnqBZXXo094rEXUv2aA==", + "license": "MIT", + "dependencies": { + "@types/lodash-es": "^4.17.12", + "axios": "^1.13.5", + "laravel-precognition": "^1.0.2", + "lodash-es": "^4.18.1", + "qs": "^6.15.0" + } + }, + "node_modules/@inertiajs/vue3": { + "version": "2.3.24", + "resolved": "https://registry.npmjs.org/@inertiajs/vue3/-/vue3-2.3.24.tgz", + "integrity": "sha512-TokM+JU88YTHClh/LcKk31qiIAZFq3RQ4BBf1dxvk6MV45KWYemJMpLS6WFJ5NaSv6rZFlZrRc92N0ZdyOC/HA==", + "license": "MIT", + "dependencies": { + "@inertiajs/core": "2.3.24", + "@types/lodash-es": "^4.17.12", + "laravel-precognition": "^1.0.2", + "lodash-es": "^4.18.1" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@intlify/core-base": { + "version": "11.4.4", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.4.tgz", + "integrity": "sha512-w/vItlylrAmhebkIbVl5YY8XMCtj8Mb2g70ttxktMYuf5AuRahgEHL2iLgLIsZBIbTSgs4hkUo7ucCL0uTJvOg==", + "license": "MIT", + "dependencies": { + "@intlify/devtools-types": "11.4.4", + "@intlify/message-compiler": "11.4.4", + "@intlify/shared": "11.4.4" + }, + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/devtools-types": { + "version": "11.4.4", + "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.4.tgz", + "integrity": "sha512-PcBLmGmDQsTSVV911P8upzpcLJO1CNVYi/IH6bGnLR2nA+0L963+kXN1ZrisTEnbtw2ewN6HMMSldqzjronA0Q==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "11.4.4", + "@intlify/shared": "11.4.4" + }, + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "11.4.4", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.4.tgz", + "integrity": "sha512-vn0OAV9pYkJlPPmgnsSm5eAG3mL0+9C/oaded2JY9jmxBbhmUXT3TcAUY8WRgLY9Hte7lkUJKpXrVlYjMXBD2w==", + "license": "MIT", + "dependencies": { + "@intlify/shared": "11.4.4", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "11.4.4", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.4.tgz", + "integrity": "sha512-QRUCHqda1U6aR14FR0vvXD4+4gj6+fm0AhAozvSuRCw0fCvrmCugWpgiR4xH2NI6s8am6N9p5OhirplsX8ZS3g==", + "license": "MIT", + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@japa/api-client": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@japa/api-client/-/api-client-3.2.1.tgz", + "integrity": "sha512-dICbeEkgGRpjkm3CviOpvPfYMBZddaoW2w20pgNMm3CfvD2bixWOFn6FBRZRq5L+fHYe/O/xfSNGMCQBFy7Zlw==", + "license": "MIT", + "dependencies": { + "@poppinss/hooks": "^7.3.0", + "@poppinss/macroable": "^1.1.0", + "@poppinss/qs": "^6.15.0", + "@types/superagent": "^8.1.9", + "cookie-es": "^2.0.0", + "set-cookie-parser": "^2.7.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=18.16.0" + }, + "peerDependencies": { + "@japa/assert": "^2.0.0 || ^3.0.0 || ^4.0.0", + "@japa/openapi-assertions": "^0.1.1", + "@japa/runner": "^3.1.2 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "@japa/assert": { + "optional": true + }, + "@japa/openapi-assertions": { + "optional": true + } + } + }, + "node_modules/@japa/api-client/node_modules/cookie-es": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.1.tgz", + "integrity": "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA==", + "license": "MIT" + }, + "node_modules/@japa/assert": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@japa/assert/-/assert-4.2.0.tgz", + "integrity": "sha512-Krgrcee01BN1StlVwK5JQP6LL5t3DE3uFNbfFoDTfW7kQuHB0xh6yfaV0hrgcoiEjsqmm2OOsVWeju9aXK4vIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/macroable": "^1.1.0", + "@types/chai": "^5.2.3", + "assertion-error": "^2.0.1", + "chai": "^6.2.1" + }, + "engines": { + "node": ">=18.16.0" + }, + "peerDependencies": { + "@japa/runner": "^3.1.2 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/@japa/core": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@japa/core/-/core-10.4.0.tgz", + "integrity": "sha512-1zvKL29i7r/4jqTNBsw91hk1tp6wbqFXvyV2p+Og4axDRhIXjSAfauRvBL9QuB80bOa+pDIMQ5kCTaCplSm0Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/hooks": "^7.3.0", + "@poppinss/macroable": "^1.1.0", + "@poppinss/string": "^1.7.1", + "async-retry": "^1.3.3", + "emittery": "^1.2.0", + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/@japa/errors-printer": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@japa/errors-printer/-/errors-printer-4.1.4.tgz", + "integrity": "sha512-ogPT87QLaugKyPVcq+ZypcetVeJpXZN7RfVRlRDIrSHsHBw6ILCtNXghVxD9POjqxtUM7RON3sUkgZzLnVQA5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.6", + "jest-diff": "^30.2.0", + "supports-color": "^10.2.2", + "youch": "^4.1.0-beta.13" + }, + "engines": { + "node": ">=18.16.0" + } + }, + "node_modules/@japa/plugin-adonisjs": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@japa/plugin-adonisjs/-/plugin-adonisjs-5.2.0.tgz", + "integrity": "sha512-Knk3X/UeZiEvkXmTj9+dAIOdJHMqRUJFsUMw9y8at+AR1VbPJR9T+aU5SaOn6OtejEFnUMILwJYC3ohg23+vhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.16.0" + }, + "peerDependencies": { + "@adonisjs/core": "^7.0.0-next.21 || ^7.0.0", + "@japa/api-client": "^3.1.0", + "@japa/browser-client": "^2.2.0", + "@japa/runner": "^4.0.0 || ^5.0.0", + "playwright": "^1.57.0" + }, + "peerDependenciesMeta": { + "@japa/api-client": { + "optional": true + }, + "@japa/browser-client": { + "optional": true + }, + "playwright": { + "optional": true + } + } + }, + "node_modules/@japa/runner": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@japa/runner/-/runner-5.3.0.tgz", + "integrity": "sha512-WCnTd1q2EpbKKa96NzL16kVxJXVLRj1VqbswNAn17hYSuMlNKKhPGNbAosB32QZVFcoe9fv4Ebh1HtjyAT/viw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@japa/core": "^10.4.0", + "@japa/errors-printer": "^4.1.4", + "@poppinss/colors": "^4.1.6", + "@poppinss/hooks": "^7.3.0", + "@poppinss/string": "^1.7.1", + "@poppinss/utils": "^7.0.0-next.6", + "error-stack-parser-es": "^1.0.5", + "find-cache-directory": "^6.0.0", + "getopts": "^2.3.0", + "supports-color": "^10.2.2", + "timekeeper": "^2.3.1" + }, + "engines": { + "node": ">=18.16.0" + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", + "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsonquerylang/jsonquery": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@jsonquerylang/jsonquery/-/jsonquery-5.1.1.tgz", + "integrity": "sha512-Fj4SoA6Ku09EF+t7OEI8QLipA2A+fJCdEOwnDWG84o5jXMRjkcN5NCMH7kFZb5fP62xz914XV5LBOiDdiUXObg==", + "license": "ISC", + "bin": { + "jsonquery": "bin/cli.js" + } + }, + "node_modules/@lezer/common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", + "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.11.tgz", + "integrity": "sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodable/entities": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz", + "integrity": "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", + "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/auto-instrumentations-node": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.76.0.tgz", + "integrity": "sha512-44KWgqsMuqfV4UhOcwwnDeK8CpB5LT1MmpZj6sKXFXu2q6rjKo622pWgOgn5Ntp5Qal9q1uBX2VS8mvTpsMeyw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/instrumentation-amqplib": "^0.65.0", + "@opentelemetry/instrumentation-aws-lambda": "^0.70.0", + "@opentelemetry/instrumentation-aws-sdk": "^0.73.0", + "@opentelemetry/instrumentation-bunyan": "^0.63.0", + "@opentelemetry/instrumentation-cassandra-driver": "^0.63.0", + "@opentelemetry/instrumentation-connect": "^0.61.0", + "@opentelemetry/instrumentation-cucumber": "^0.34.0", + "@opentelemetry/instrumentation-dataloader": "^0.35.0", + "@opentelemetry/instrumentation-dns": "^0.61.0", + "@opentelemetry/instrumentation-express": "^0.66.0", + "@opentelemetry/instrumentation-fs": "^0.37.0", + "@opentelemetry/instrumentation-generic-pool": "^0.61.0", + "@opentelemetry/instrumentation-graphql": "^0.66.0", + "@opentelemetry/instrumentation-grpc": "^0.218.0", + "@opentelemetry/instrumentation-hapi": "^0.64.0", + "@opentelemetry/instrumentation-http": "^0.218.0", + "@opentelemetry/instrumentation-ioredis": "^0.66.0", + "@opentelemetry/instrumentation-kafkajs": "^0.27.0", + "@opentelemetry/instrumentation-knex": "^0.62.0", + "@opentelemetry/instrumentation-koa": "^0.66.0", + "@opentelemetry/instrumentation-lru-memoizer": "^0.62.0", + "@opentelemetry/instrumentation-memcached": "^0.61.0", + "@opentelemetry/instrumentation-mongodb": "^0.71.0", + "@opentelemetry/instrumentation-mongoose": "^0.64.0", + "@opentelemetry/instrumentation-mysql": "^0.64.0", + "@opentelemetry/instrumentation-mysql2": "^0.64.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.64.0", + "@opentelemetry/instrumentation-net": "^0.62.0", + "@opentelemetry/instrumentation-openai": "^0.16.0", + "@opentelemetry/instrumentation-oracledb": "^0.43.0", + "@opentelemetry/instrumentation-pg": "^0.70.0", + "@opentelemetry/instrumentation-pino": "^0.64.0", + "@opentelemetry/instrumentation-redis": "^0.66.0", + "@opentelemetry/instrumentation-restify": "^0.63.0", + "@opentelemetry/instrumentation-router": "^0.62.0", + "@opentelemetry/instrumentation-runtime-node": "^0.31.0", + "@opentelemetry/instrumentation-socket.io": "^0.65.0", + "@opentelemetry/instrumentation-tedious": "^0.37.0", + "@opentelemetry/instrumentation-undici": "^0.28.0", + "@opentelemetry/instrumentation-winston": "^0.62.0", + "@opentelemetry/resource-detector-alibaba-cloud": "^0.33.8", + "@opentelemetry/resource-detector-aws": "^2.18.0", + "@opentelemetry/resource-detector-azure": "^0.26.0", + "@opentelemetry/resource-detector-container": "^0.8.9", + "@opentelemetry/resource-detector-gcp": "^0.53.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/sdk-node": "^0.218.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^2.0.0" + } + }, + "node_modules/@opentelemetry/configuration": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/configuration/-/configuration-0.218.0.tgz", + "integrity": "sha512-W8wIz7H2R1pufR5jfjb3gU2XkMpm2x/7b1RJcsuzvd70Il/rWWE+g5/Od7hQKrxRTSrTrOWlru101PWXz5I1EQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "yaml": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.7.1.tgz", + "integrity": "sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", + "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.218.0.tgz", + "integrity": "sha512-hoxrNH1l/Xy6F9WTJ5IK+6j1r9nQFlPOmrnTlhYHTySdunfXLmUCPv3bQtKYntxag9h3wLYBZQ2HI6FOx+BT2g==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.14.3", + "@opentelemetry/core": "2.7.1", + "@opentelemetry/otlp-exporter-base": "0.218.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.218.0", + "@opentelemetry/otlp-transformer": "0.218.0", + "@opentelemetry/sdk-logs": "0.218.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.218.0.tgz", + "integrity": "sha512-Qx+4rpVHzgg89dawcWRHyt+XRXeLnhFz/qBtvggmjkcgPUdr+NAB0/u/eIPA8yAeJV0J80Vz43JZCh/XFvZFGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.218.0", + "@opentelemetry/core": "2.7.1", + "@opentelemetry/otlp-exporter-base": "0.218.0", + "@opentelemetry/otlp-transformer": "0.218.0", + "@opentelemetry/sdk-logs": "0.218.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.218.0.tgz", + "integrity": "sha512-1/noQNsp9gXD75HPzgjBrcF1+XTtry7pFAUfxVEJgg7mPv2AawKQuYkhMmJ8qjxz4Ubc3Y8bwvfxevXsKTq4cg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.218.0", + "@opentelemetry/core": "2.7.1", + "@opentelemetry/otlp-exporter-base": "0.218.0", + "@opentelemetry/otlp-transformer": "0.218.0", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/sdk-logs": "0.218.0", + "@opentelemetry/sdk-trace-base": "2.7.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.218.0.tgz", + "integrity": "sha512-YapQ9vNMX0NSZF6LK5pWAFfjpJleV2O9uYWfYGeb/5F1Kb9rPGK8tZDMJFa/sOksgdFuflDvYuA0B4qjDB4fjQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.14.3", + "@opentelemetry/core": "2.7.1", + "@opentelemetry/exporter-metrics-otlp-http": "0.218.0", + "@opentelemetry/otlp-exporter-base": "0.218.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.218.0", + "@opentelemetry/otlp-transformer": "0.218.0", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/sdk-metrics": "2.7.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.218.0.tgz", + "integrity": "sha512-bV7d2OuMpZu2+gAaxUAhzfZ0h3WVZk8ETQUEE3DNSntbTaMpuITjtm8I0rNyHFdm7Ax57K6ty7SgFXlBmOLIvQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/otlp-exporter-base": "0.218.0", + "@opentelemetry/otlp-transformer": "0.218.0", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/sdk-metrics": "2.7.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.218.0.tgz", + "integrity": "sha512-ubLddKjWULhla9YZRCj/rTBeppjJYE4e9w0icx5mTu3eFhWjQzbV75NYjXuIlEG+NJsBl6d+sTFw5Qu+oej4oQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/exporter-metrics-otlp-http": "0.218.0", + "@opentelemetry/otlp-exporter-base": "0.218.0", + "@opentelemetry/otlp-transformer": "0.218.0", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/sdk-metrics": "2.7.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.218.0.tgz", + "integrity": "sha512-RT5oEyu1kddZJ1vt7/BUo5wV+P7hpNAESsR3dUd3+8deHuX7gWNoCOZn+SfDT+hJHlIJ5h/AxiCLXIrutswDJg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/sdk-metrics": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.218.0.tgz", + "integrity": "sha512-3fXxVQEj9TNAFaCi79JeFKfeLd0sDtInaR3gaZDVlzNSPHtz8PZuCV34JKWjD4XXzT20IdMe8IpX6mRVNDA4Tw==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.14.3", + "@opentelemetry/core": "2.7.1", + "@opentelemetry/otlp-exporter-base": "0.218.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.218.0", + "@opentelemetry/otlp-transformer": "0.218.0", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/sdk-trace-base": "2.7.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.218.0.tgz", + "integrity": "sha512-8dqezsmPhtKitIK/eTipZhYl9EX2/gNQ5zUMhaz3uxEURwfkNf8IPvo6yNfrzbxdtpAOybS/+h7wmIWYqFSpiw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/otlp-exporter-base": "0.218.0", + "@opentelemetry/otlp-transformer": "0.218.0", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/sdk-trace-base": "2.7.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.218.0.tgz", + "integrity": "sha512-r1Msf8SNLRmwh9J6XQ5uh82D7CdDWMNHnPB7LAVHjzut0TkSeKc5KcIvr4SvHvfk/xwN5gxC+VLKQ1k0o8PSPw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/otlp-exporter-base": "0.218.0", + "@opentelemetry/otlp-transformer": "0.218.0", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/sdk-trace-base": "2.7.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.7.1.tgz", + "integrity": "sha512-mfsD9bKAxcKrh5+y08TPodvClBO0CznBE3p79YAGnO81WI4LrdsGA65T53e4iTSbCalW4WaUpkbeJcbpyIUHfg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/sdk-trace-base": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", + "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.218.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-amqplib": { + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.65.0.tgz", + "integrity": "sha512-fF7fNHA59n3y23ROfst2EbSxmP+L3E+snZO6aMU4w4xD84mfejAivspIAsqa9arX5HZlBK6dslHz5dWGNp5D0A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-aws-lambda": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.70.0.tgz", + "integrity": "sha512-HT74cQxi/iiVEz5dRdNdfGCFzPFbkxSiwHfFPHDwkRcr1JKQqI6hm8qeXEvEiJ+36xIU1KkQMDfeThJ1ifnUiA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/aws-lambda": "^8.10.155" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-aws-sdk": { + "version": "0.73.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.73.0.tgz", + "integrity": "sha512-0INPkHbR6o4J3psE+ncwWaE7qtDpb2p+i+qfV82cfwYLCXavYCGosBZ/S4pOErDVJYIyQVIsNAHhaUgaL313SQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.34.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-bunyan": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.63.0.tgz", + "integrity": "sha512-z0xPSZ62d3I7sG2sUTyQ5/ES1RdESP2eOETiMLY9gPSp+HZwbsAyj7T/2sdZKYD+O2ajRHZEil+DBoUolf1ocQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.218.0", + "@opentelemetry/instrumentation": "^0.218.0", + "@types/bunyan": "1.8.11" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-cassandra-driver": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.63.0.tgz", + "integrity": "sha512-jnVTOr3h/46UDalEwJ4ITux8UWwHmnsOik5WFs3JB/UrUj8Wad5eI+KpOEBuOUeOfPB9sce11qgVw3WXU2r+hg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.37.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-connect": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.61.0.tgz", + "integrity": "sha512-ZTQ0W3Lb7GJsOd+72cG8FJQKA5DqYfELJGLmChrJIezRSLfJIfofwKEGLX5rMtFJmwckpichQkBZWjid5dvnVQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/connect": "3.4.38" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-cucumber": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cucumber/-/instrumentation-cucumber-0.34.0.tgz", + "integrity": "sha512-VK63Cm8osAdsSZpULPk+qnNktQUJzmnIOv2wuh79fV41WuTM38uOFC3s978/24pDkSljhN4EYCbPRLrAhXfKSA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-dataloader": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.35.0.tgz", + "integrity": "sha512-6x6UPP0tLzrdj15PIEN3qgp/WCcESCavHJfkIKoyLmy4UjGLF1KgEUMyD74xhbKGo426uvMbhvCgZC0ye8nO/A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-dns": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dns/-/instrumentation-dns-0.61.0.tgz", + "integrity": "sha512-5D8xFaw9GXq9ZIOAvG7NPDivFfZWFAekLGFn1B7ppyhuAYBVHGybFpx4Q9BV1Uup3yzCdiD78KhyH7c3dKOYSw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-express": { + "version": "0.66.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.66.0.tgz", + "integrity": "sha512-G1xTh5M5shklMgIyUXWDjU2BakulKtcISaM4U5TyanvO7R4xbB3iC7YQ8QKegLXaOs81Ku8RlcIcbYRrz/82wQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fs": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.37.0.tgz", + "integrity": "sha512-5mxhFuwAK0FFvisUdvuywaZ9ySMZ15HfbN6IpLn0gwRh9s1/QBcpLznQ/A15cZs1QFtBJ+JXIHdwY7WOD0c4Eg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.218.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-generic-pool": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.61.0.tgz", + "integrity": "sha512-tvp5PWnGRPHY/kz9Kg1IRLBL0qUAxMSNG623f+ZGEsvnCVEjr3tFyw1JGQzM+B3eZKkO+Dp/LYrtOSfb69D5lA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-graphql": { + "version": "0.66.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.66.0.tgz", + "integrity": "sha512-D4PN1tStj6rnOdofnt2xINJjtT1k2ockzaODrn76VEBZeqJ3QsEvKFfunB0EFAohO4xswVp14VAVmKNnGzA1Dw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-grpc": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.218.0.tgz", + "integrity": "sha512-kcDCNrC7IWNXEKQriGrwuh5jjbMFU5exOQzU9ufEY9UkACNcgYIdOd7XpX3IqZ3UPSnZyZtlwgfsbC5SNlEDbA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "0.218.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-hapi": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.64.0.tgz", + "integrity": "sha512-PCHgCICCDz7p9BgCU9gQz2smbqu4V4P8QtWJ7DLjL3bmzSdrgy6EGvecDg1YuhjBsoN08SR+y36hgdHkqCgrzQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.218.0.tgz", + "integrity": "sha512-x9djaqdzpT8WAboep1H9nCAQ1E+MMsm08TNfA02TqM3bNNddZeiim+E3KMWVQFaX6JpUy7V0nm/wfN/K2Em+Zw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/instrumentation": "0.218.0", + "@opentelemetry/semantic-conventions": "^1.29.0", + "forwarded-parse": "2.1.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-ioredis": { + "version": "0.66.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.66.0.tgz", + "integrity": "sha512-UfTAcaBKCzLUZ9opvfOLV4bH46XiNFqUsKykfPCIefDIxJ1iUYtMOucNaiZ+/kjQdPy5i6Ef5tk2IAjxol4X1w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/redis-common": "^0.38.3", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-kafkajs": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.27.0.tgz", + "integrity": "sha512-kl/C2AU4KZGHlMZD12nMFXcMjxSHvu5Q0UPSQ6IJeBfCadYuWgW+sWIa2JZVK/A0qRYm2cncekJyeBHQDyfUUg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.30.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-knex": { + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.62.0.tgz", + "integrity": "sha512-XgfhCAWwSqA0YnwaEKdpvQMavc90D3R65frhLCO9JNl867EulNps9tm6pjGIg+GiYuewn00gEzW4HQ5btgYxGQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.33.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-koa": { + "version": "0.66.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.66.0.tgz", + "integrity": "sha512-04x/z21WTMEfy3lUSr4aTj8WsTN3OZF901hJ+ciOwdwf7AK8UJTpZCXw6KQ3G4Vag56q1HoMihCONeWZLeld1g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.36.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + } + }, + "node_modules/@opentelemetry/instrumentation-lru-memoizer": { + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.62.0.tgz", + "integrity": "sha512-AlGKIdk6ZT7WmIozfUb2LjOcI3AhQrvAXKX0zi1cVcnw2QlRbVYyV5GTa2Th9ebuczVfWPaoPrmZw61zCp/czw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-memcached": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-memcached/-/instrumentation-memcached-0.61.0.tgz", + "integrity": "sha512-qiCR9Wovf5AHzn6g+LXhvwMmv2I6zhHz2I2tEHZMmBuD8c18bkJzGFxHoSBlxdApRT+SW13r9472dDMm4BRjgQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@types/memcached": "^2.2.6" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongodb": { + "version": "0.71.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.71.0.tgz", + "integrity": "sha512-6rwfVjAUY69CKkyGqzL+F5X7Nzw0+Ke9pOxk9xUPJpy8vracZxuQYF7rWu02sV1xOgi4u52449SuVhD+zaSiIA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongoose": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.64.0.tgz", + "integrity": "sha512-iCIqeUaERN8Uc5Rrtg4zvQ6d7z5JQ5iUmbnr/JHYPxAidDowmRc8/wDMJeMKRfLPTj336Zu0ec7rH/ak/4N9vw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.64.0.tgz", + "integrity": "sha512-W1w76AJkP7i0uzzAe7nsCMWq4+EMSA550f1lAmxDPdQC5FnreNbRIm/tod2OS9gVrYvRrQXNkFmZJKGo4kzCnw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@types/mysql": "2.15.27" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql2": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.64.0.tgz", + "integrity": "sha512-yTu0mYh/qJPSE86VmNLQww5uugDyvCS2KJIPfPtIk2ufoEUoHPsV6Iynnvmz588Moq04aBLxfTa/EtE4A2ykWA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@opentelemetry/sql-common": "^0.41.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-nestjs-core": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.64.0.tgz", + "integrity": "sha512-PW1ArxryMwF8/IXq1nzlQs7tmr/fWd1tf71AHevZT3Fm0hW7jRX9JEfYgIAcKDvmbqcJEr5K1224NEimrRPbuQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.30.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-net": { + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-net/-/instrumentation-net-0.62.0.tgz", + "integrity": "sha512-Gt2kzpACpmIad+q3LQqe8UNHuoVvdLuFpB6SN/A6xLPKNllb+ksPUYQhj1kXdZOpcFZNGKDXHyN+TUCVCk1TRw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-openai": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-openai/-/instrumentation-openai-0.16.0.tgz", + "integrity": "sha512-I0KKybyqqFOxSBgYKQNdR/EF3LvzSaAUT7Y75xkjbgscY+V8UWDpUbY68POLhUC3SKMlGvZmrTSxcQ+Y0vRhNw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.218.0", + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.36.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-oracledb": { + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-oracledb/-/instrumentation-oracledb-0.43.0.tgz", + "integrity": "sha512-7Z4kOOdnrHX4S5gCeWhnnpWQwEd7weRjDhJA1nSrwTYtAcVWNjk5wsMKHBCTDCN0uJtA9T6PouZ+AKRYiS1Rrg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@types/oracledb": "6.5.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pg": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.70.0.tgz", + "integrity": "sha512-g8WXwwOUXfjiEmATwjB/33QKE2AkIpNe4KIuJJh4djtXgCL0Wne+AzAfjuDIAspGvO1txQp8ibKsLd3SBmcvJA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@opentelemetry/sql-common": "^0.41.2", + "@types/pg": "8.15.6", + "@types/pg-pool": "2.0.7" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pino": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pino/-/instrumentation-pino-0.64.0.tgz", + "integrity": "sha512-+vDL7tZMZjkp8BpYMx/cL2/HWGsNUqKcRmAIIEaQu/6F44oM6xGDMCSqMKHdKCsH1+WW52EYdHbWkVGTF0KVsQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.218.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.218.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-redis": { + "version": "0.66.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.66.0.tgz", + "integrity": "sha512-bVShkag6vP2VQO0cpA8CHjOohWbKNYLyjiwGkOnSAwou1TPc6pf9DssFUxwqN2XF1J4oqP0LVSvN9kZUzMecfA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/redis-common": "^0.38.3", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-restify": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-restify/-/instrumentation-restify-0.63.0.tgz", + "integrity": "sha512-Z73YxZpt0Y56uRu2pRWOjO5wXHvZqF46K4czoKRTGlUifzzFmUZxyOeAAECACuMRSLZmZ394WJin0MDgU9iW9w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-router": { + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-router/-/instrumentation-router-0.62.0.tgz", + "integrity": "sha512-0w8ok7GbXtYvX7TtLp72qQJKNyI7lD72Fy2NsNKIcQAv6TqGox5javFyXrIrCAtZHCONePxeAwAYj1Qd9si9OQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-runtime-node": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-runtime-node/-/instrumentation-runtime-node-0.31.0.tgz", + "integrity": "sha512-HkLsuEfUDahFiL/xFtEqJDMp7sp8ynOtA045bJi9nAH8CrPvljPW5SgJQb2mQqEYJQopbWYZ2lPqQEfj7bYgJg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.218.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.218.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-socket.io": { + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.65.0.tgz", + "integrity": "sha512-dNvIbD40h0z69stQ9cIeAWRyy5WyQM1a1XnFthekc/oi/ipX4E6oYJBM4X2xKBxjZMTjdV5VshLoNeYMSBsnjw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-tedious": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.37.0.tgz", + "integrity": "sha512-cGLF46UsgeI1334atJxLO36yQlV7WXKg35Mp+e2NXo2vOTfIZTVqoKOzExVOTOwT4AQjfGVEDxyq5wXybUYXIA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@types/tedious": "^4.0.14" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-undici": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.28.0.tgz", + "integrity": "sha512-7nh4Gw7PhYtQm82FIJtWUhx6iZQJj0bdkKe2RQb3XNIyxu0o9rM1J5Xt083SsG2tCbQZpX9/mlDxhTrK1Z/lVQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.24.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.7.0" + } + }, + "node_modules/@opentelemetry/instrumentation-winston": { + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.62.0.tgz", + "integrity": "sha512-pr1U9ZV4RRy23qMVrRzebfxwDWjp44xA7sC0PAdeW9v4HDcfOr0ejdTJmIsBGvhkNHPBajfieaIF9b6/9wjErA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.218.0", + "@opentelemetry/instrumentation": "^0.218.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.218.0.tgz", + "integrity": "sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/otlp-transformer": "0.218.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.218.0.tgz", + "integrity": "sha512-H/lCGJ536N98VpYJOaWTQOkv4Dx6TnmStK6Rqfu1W7KkFbPAx04hjdYEMZF/YbnHzPUSIK4kM6OE2GKGBTpV9A==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.14.3", + "@opentelemetry/core": "2.7.1", + "@opentelemetry/otlp-exporter-base": "0.218.0", + "@opentelemetry/otlp-transformer": "0.218.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.218.0.tgz", + "integrity": "sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.218.0", + "@opentelemetry/core": "2.7.1", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/sdk-logs": "0.218.0", + "@opentelemetry/sdk-metrics": "2.7.1", + "@opentelemetry/sdk-trace-base": "2.7.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/propagator-b3": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.7.1.tgz", + "integrity": "sha512-RJid6E2CKyeGfKBzXKF21ejabGMHypFkPAh3qZ+NvI+SGjuIye79t3PmiqcDgtRzdKH6ynXzbfslQ8DfpRUg2A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.7.1.tgz", + "integrity": "sha512-KMjVBHzP4N60bOzxja76M1F1hZZ43lGPga5ix+mkv9+kk1nx9SbkxSvJsMbuVUxdPQmsPTqGShmhN8ulrMOg6Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/redis-common": { + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.38.3.tgz", + "integrity": "sha512-VCghU1JYs/4gP6Gqf/xro9MEsZ7LrMv2uONVsaESKL38ZOB9BqnI98FfS23wjMnHlpuE+TTaWSoAVNpTwYXzjw==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + } + }, + "node_modules/@opentelemetry/resource-detector-alibaba-cloud": { + "version": "0.33.8", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.33.8.tgz", + "integrity": "sha512-RnSB/uxkElny0/WBFEtIG2HRG0cpSNTRdE+YSB7Poa+uljK+ddCacEZYz/PMgZh+cs586XstJQxdyjz0jtcAug==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resource-detector-aws": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-aws/-/resource-detector-aws-2.18.0.tgz", + "integrity": "sha512-wyMM4UoRuHvI2KjqnTzvyW8Yv7MKRGA+I78Xti6gTEw7hBhqXU1SRo+f9KrsQfeeiOn+TkDuvxavuaAQbD3i6g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resource-detector-azure": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.26.0.tgz", + "integrity": "sha512-7KxF7mlwI2nKja/iEdwPqOaS0QAJbhT9ye4DeYZnXdOS/4phfonk5nSmyGDBYhBL7J30MPL91oZNuGYRKXZAXA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.37.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resource-detector-container": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.8.9.tgz", + "integrity": "sha512-Xd2C4HjW9hl75iqZT7tQNy2yRBUqNucq2O9+e0FJRNkbiItInYVMzc0S0KDXcx/vZBwNmlrKS3R0uLCU9ULsGA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resource-detector-gcp": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.53.0.tgz", + "integrity": "sha512-RCV31v23ZwZfYR3LPkuORHTHIOvfm3hZBT7hAzSO0+oAIrG/Dm0ld5tV4lYNO05GjI7sHQdRcbSqzEYAvQcQuw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0", + "gcp-metadata": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", + "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz", + "integrity": "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.218.0", + "@opentelemetry/core": "2.7.1", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", + "integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/resources": "2.7.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.218.0.tgz", + "integrity": "sha512-tPMjHrLV5gsfNdYqoRHjeGbCAZBXXD9c1Qo/2ut7VwnUABDNh76xNxrT0SEhkIIJuCN45bbN1vZnYL1gY0IkOg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.218.0", + "@opentelemetry/configuration": "0.218.0", + "@opentelemetry/context-async-hooks": "2.7.1", + "@opentelemetry/core": "2.7.1", + "@opentelemetry/exporter-logs-otlp-grpc": "0.218.0", + "@opentelemetry/exporter-logs-otlp-http": "0.218.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.218.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.218.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.218.0", + "@opentelemetry/exporter-metrics-otlp-proto": "0.218.0", + "@opentelemetry/exporter-prometheus": "0.218.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.218.0", + "@opentelemetry/exporter-trace-otlp-http": "0.218.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.218.0", + "@opentelemetry/exporter-zipkin": "2.7.1", + "@opentelemetry/instrumentation": "0.218.0", + "@opentelemetry/otlp-exporter-base": "0.218.0", + "@opentelemetry/propagator-b3": "2.7.1", + "@opentelemetry/propagator-jaeger": "2.7.1", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/sdk-logs": "0.218.0", + "@opentelemetry/sdk-metrics": "2.7.1", + "@opentelemetry/sdk-trace-base": "2.7.1", + "@opentelemetry/sdk-trace-node": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", + "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.7.1.tgz", + "integrity": "sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "2.7.1", + "@opentelemetry/core": "2.7.1", + "@opentelemetry/sdk-trace-base": "2.7.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sql-common": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.41.2.tgz", + "integrity": "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", + "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@phc/format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@poppinss/cliui": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@poppinss/cliui/-/cliui-6.8.1.tgz", + "integrity": "sha512-o/ssbwr+r6woG65rk9eFHnn9dVUphZr/Rk+4+05ENVMBWYpYhTJGdE9RobTG5JLFubvO4gWIyFeNlC+I4EM6eA==", + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.6", + "cli-boxes": "^4.0.1", + "cli-table3": "^0.6.5", + "cli-truncate": "^5.2.0", + "log-update": "^7.2.0", + "pretty-hrtime": "^1.0.3", + "string-width": "^8.2.0", + "supports-color": "^10.2.2", + "terminal-size": "^4.0.1" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.7.0.tgz", + "integrity": "sha512-0UTYalzk2t6S4rA2uHOz5bSSW2CHdv4vggJI6Alg90yvl0UgXs6XSXpH96OH+bRkX4J/06djv29pqXJ0lq5Kag==", + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/dumper/node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "license": "MIT" + }, + "node_modules/@poppinss/hooks": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@poppinss/hooks/-/hooks-7.3.0.tgz", + "integrity": "sha512-/H35z/bWqHg7085QOxWUDYMidx6Kl6b8kIyzIXlRYzWvsk1xm9hQOlXWdWEYch+Gmn8eL7tThx59MBj8BLxDrQ==", + "license": "MIT" + }, + "node_modules/@poppinss/inspect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@poppinss/inspect/-/inspect-1.0.1.tgz", + "integrity": "sha512-kLeEaBSGhlleyYvKc7c9s3uE6xv7cwyulE0EgHf4jU/CL96h0yC4mkdw1wvC1l1PYYQozCGy46FwMBAAMOobCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@poppinss/macroable": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@poppinss/macroable/-/macroable-1.1.2.tgz", + "integrity": "sha512-FAVBRzzWhYP5mA3lCwLH1A0fKBqq5anyjGet90Z81aRK5c/+LTGUE1zJhZrErjaenBSOOI9BVUs3WVmotneFQA==", + "license": "MIT" + }, + "node_modules/@poppinss/matchit": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@poppinss/matchit/-/matchit-3.2.0.tgz", + "integrity": "sha512-9SoMICN+LMO7ZtMj2ja8N7RHlC4mmuv5WwIBXWjabMd2SyXE1dIydh29exlgm+dGMP84PjwvfJH1TmWL4qz1og==", + "license": "MIT", + "dependencies": { + "@arr/every": "^1.0.0" + } + }, + "node_modules/@poppinss/middleware": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@poppinss/middleware/-/middleware-3.2.7.tgz", + "integrity": "sha512-MZC0Z97ozSz+PpfyxUPUy/ImuthpqvBbY7qku7f4Q2maHz+2uXfchfO8OggXLS6zEJ078l+jpAHZ2rDIRdjeVg==", + "license": "MIT" + }, + "node_modules/@poppinss/multiparty": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@poppinss/multiparty/-/multiparty-3.0.0.tgz", + "integrity": "sha512-z9jchUzsv7E+7sa4tWHb0+95Byx7w0ydlPGxg3nzyb7h3QlRdeW8/QkU9SexUY4lsT12do93AfNBAhSuOoVqjA==", + "license": "MIT", + "dependencies": { + "http-errors": "^2.0.0" + } + }, + "node_modules/@poppinss/object-builder": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@poppinss/object-builder/-/object-builder-1.1.0.tgz", + "integrity": "sha512-FOrOq52l7u8goR5yncX14+k+Ewi5djnrt1JwXeS/FvnwAPOiveFhiczCDuvXdssAwamtrV2hp5Rw9v+n2T7hQg==", + "license": "MIT", + "engines": { + "node": ">=20.6.0" + } + }, + "node_modules/@poppinss/prompts": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/prompts/-/prompts-3.1.6.tgz", + "integrity": "sha512-cKHfkID6b3wl1kbHJJRC/pznQ3KnRVydyk7CE38NfTV3VS45BDYCxeZZ7bfDin71qMzITh18lKnu8iuLxBngHA==", + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.6", + "@poppinss/exception": "^1.2.2", + "@poppinss/object-builder": "^1.1.0", + "enquirer": "^2.4.1" + } + }, + "node_modules/@poppinss/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@poppinss/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-QzfMhxrRB5EPeGz0l8hTwKZ5dFX6ed0aETGbuD369StCO8Ad3SW4wWBYamOK5IKeM/dfOeKaCwUZPTnGcj+jKg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@poppinss/string": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@poppinss/string/-/string-1.7.2.tgz", + "integrity": "sha512-A182GLDfi36iDCbhDrHB0xzrPM1fO3GHnhCDIdadf8C6eycgct4m7zusbLwEh6GPaj2Pz5BVos7XK16w7tZ7wQ==", + "license": "MIT", + "dependencies": { + "@types/pluralize": "^0.0.33", + "case-anything": "^3.1.2", + "pluralize": "^8.0.0", + "slugify": "^1.6.9" + } + }, + "node_modules/@poppinss/ts-exec": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@poppinss/ts-exec/-/ts-exec-1.4.4.tgz", + "integrity": "sha512-jQLbeQG3n9B+hpygIAQpNaNd3y9+7sLn0Jioo9qEo84Vd3XeRMKr3Qql/u2bixzONO2+RsBbzEJ3AWb2iCPARw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@swc/core": "~1.15.11", + "get-tsconfig": "^4.13.0" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/@poppinss/types": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@poppinss/types/-/types-1.2.1.tgz", + "integrity": "sha512-qUYnzl0m9HJTWsXtr8Xo7CwDx6wcjrvo14bOVbIMIlKJCzKrm3LX55dRTDr1/x4PpSvKVgmxvC6Ly2YiqXKOvQ==", + "license": "MIT" + }, + "node_modules/@poppinss/utils": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@poppinss/utils/-/utils-7.0.1.tgz", + "integrity": "sha512-mveSvLI2YPC114mK5HCuSYfUtjpClf1wHG1VCqZJCp4U2ypPhIt62Iku5urh0kPAFvnvCVHx2bXBSH14qMTOlQ==", + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.3", + "@poppinss/object-builder": "^1.1.0", + "@poppinss/string": "^1.7.1", + "@poppinss/types": "^1.2.1", + "flattie": "^1.1.1" + } + }, + "node_modules/@poppinss/validator-lite": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@poppinss/validator-lite/-/validator-lite-2.1.2.tgz", + "integrity": "sha512-UhSG1ouT6r67VbEFHK/8ax3EMZYHioew9PqGmEZjV41G15aPZi6cyhXtBVvF9xqkHMflA5V680k7bQzV0kfD5w==", + "license": "MIT" + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, + "node_modules/@replit/codemirror-indentation-markers": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/@replit/codemirror-indentation-markers/-/codemirror-indentation-markers-6.5.3.tgz", + "integrity": "sha512-hL5Sfvw3C1vgg7GolLe/uxX5T3tmgOA3ZzqlMv47zjU1ON51pzNWiVbS22oh6crYhtVhv8b3gdXwoYp++2ilHw==", + "license": "MIT", + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", + "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", + "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", + "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", + "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", + "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", + "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", + "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", + "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", + "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", + "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", + "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", + "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", + "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", + "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "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.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-8.1.0.tgz", + "integrity": "sha512-2SX/1jW6CIMAiebvVv5ZInoCEuWQmMyBoJXXGC6Vjakjp/fpxP5eHs7/V6WKuPEIbuK06+VpjH+vjLQhr98rDQ==", + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "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": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.15.tgz", + "integrity": "sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@sphinxxxx/color-conversion": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@sphinxxxx/color-conversion/-/color-conversion-2.2.2.tgz", + "integrity": "sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw==", + "license": "ISC" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@stylistic/eslint-plugin": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz", + "integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/types": "^8.56.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.0.0 || ^10.0.0" + } + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.10.tgz", + "integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@swc/core": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.40.tgz", + "integrity": "sha512-2kwzJikRvgtNAG7MwVZY2vEzZjTxKIq5jXOihuSV/8U+Hej8Va22t65aKnJZs3P+NwojZvR8Mf8kyM7O+V8sQg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.26" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.40", + "@swc/core-darwin-x64": "1.15.40", + "@swc/core-linux-arm-gnueabihf": "1.15.40", + "@swc/core-linux-arm64-gnu": "1.15.40", + "@swc/core-linux-arm64-musl": "1.15.40", + "@swc/core-linux-ppc64-gnu": "1.15.40", + "@swc/core-linux-s390x-gnu": "1.15.40", + "@swc/core-linux-x64-gnu": "1.15.40", + "@swc/core-linux-x64-musl": "1.15.40", + "@swc/core-win32-arm64-msvc": "1.15.40", + "@swc/core-win32-ia32-msvc": "1.15.40", + "@swc/core-win32-x64-msvc": "1.15.40" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.40.tgz", + "integrity": "sha512-PaYyclfmQ++77D8ityYvmmVzHv9aG8ROwt2GfG6/ccloy4Hgf80qtOnzb9VYvPsUT7Ty1uhuDRhv3XYpf62qhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.40.tgz", + "integrity": "sha512-HbbPzvfLBUXjIB1Ezks+//lNUjmLjfyd63XSwprJgrZaXYdm70kohXPJUWdqKZozolFxbPaO+xtBaiUp6BoueA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.40.tgz", + "integrity": "sha512-SlRZsCjOCPR2LvFs0Ri/Xrx/5o5TCt8vl4gW6mX1hEZOG0a625RxzRHpHdAQNGykmAN/7IeaFAJG+QnNmxlHcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.40.tgz", + "integrity": "sha512-Q8byxJt2fh8CR3EUX6snBpy47AoBVm+In/+Z3rjDHMjC38ZvR9/gtUUNCT0tfrn4EdVsO8/QPi59nxrxvqxvBQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.40.tgz", + "integrity": "sha512-4z0MgHU+7M0pZDqBN1El7mFXDI1SBwinfcUkAyA4v8QrhOIUOZltySt2aStQLZGrdXVXM4Y4ylfiTC04ED+MoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.40.tgz", + "integrity": "sha512-fLI4iUgeSZu0eRWUXwe6YzPFx9gHbFiPkl8Rp3mJfP8OpNR3nTQCGPvHdDh9xniW7mVvgMY4ni7A4VzqI1KrpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.40.tgz", + "integrity": "sha512-YqeKMAb7d4nQSGMJQ454IlaCENpzcDqhvBE9+CPfdnYpnUXxd+BSrB6Xk0YjW8UyoEhUj4p6quATCxbsp6J3jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.40.tgz", + "integrity": "sha512-7HOuS1iGcme/j/TuL1TfmmLGiMQrjv/GmjyZeydl00FKPtpGXEldwqfI56xgd1YzrzoB2svWjxbGGyQ0TEASxg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.40.tgz", + "integrity": "sha512-h4kZYHc7dpc9P9u4brRJaS8Pl7tPVHAeiLSzw7T5RfIJgAoSdaCMKzI/2Uay9gFhaw8uyCDl0L5q37r0EpAfIA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.40.tgz", + "integrity": "sha512-+mQgKZXSj6mV38Zh05QaxSjUDmGP/R2JWlXZTDLSPkDzHU6p3GxN9eeSf5dfyDVU86946fmCvSzyl/ucImx8+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.40.tgz", + "integrity": "sha512-yvwdPLGd25mcj/mNatjNQ0lZujtQD6psH3v9PNmMb+fSzjbNG8KIDxjFWrcV+fsFVLOkyOmdJsFmX7NAFjVyPw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.40.tgz", + "integrity": "sha512-OXtKsLU1bVtInzzDEAY2sYiF/rl4tvAnLLLpuMp3HzAOQZ5A+i69AKDhA1YLQTaMAqO3vzyYNVAYVRMPtSYD4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz", + "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@ts-morph/common": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.28.1.tgz", + "integrity": "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^10.0.1", + "path-browserify": "^1.0.1", + "tinyglobby": "^0.2.14" + } + }, + "node_modules/@tuyau/core": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@tuyau/core/-/core-1.2.2.tgz", + "integrity": "sha512-kFE+gxuWZ4F6KjMB6wXNEP44oVdBpYyuAsOxjHpGplC5d+ErSVjynh1vN0rkv9poQJI3z+r81JKqhpmJZs886Q==", + "license": "MIT", + "dependencies": { + "ky": "^1.14.3", + "object-to-formdata": "^4.5.1" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@adonisjs/assembler": "^8.0.0-next.27", + "@adonisjs/core": "^7.0.0-next.16" + }, + "peerDependenciesMeta": { + "@adonisjs/assembler": { + "optional": true + }, + "@adonisjs/core": { + "optional": true + } + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aws-lambda": { + "version": "8.10.161", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.161.tgz", + "integrity": "sha512-rUYdp+MQwSFocxIOcSsYSF3YYYC/uUpMbCY/mbO21vGqfrEYvNSoPyKYDj6RhXXpPfS0KstW9RwG3qXh9sL7FQ==", + "license": "MIT" + }, + "node_modules/@types/bunyan": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.11.tgz", + "integrity": "sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "license": "MIT" + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/he": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/he/-/he-1.2.3.tgz", + "integrity": "sha512-q67/qwlxblDzEDvzHhVkwc1gzVWxaNxeyHUBF4xElrvjL11O+Ytze+1fGpBHlr/H9myiBUaUXNnNPmBHxxfAcA==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/memcached": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/@types/memcached/-/memcached-2.2.10.tgz", + "integrity": "sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "license": "MIT" + }, + "node_modules/@types/mysql": { + "version": "2.15.27", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.27.tgz", + "integrity": "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/oracledb": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@types/oracledb/-/oracledb-6.5.2.tgz", + "integrity": "sha512-kK1eBS/Adeyis+3OlBDMeQQuasIDLUYXsi2T15ccNJ0iyUpQ4xDF7svFu3+bGVrI0CMBUclPciz+lsQR3JX3TQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pg": { + "version": "8.15.6", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz", + "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/pg-pool": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.7.tgz", + "integrity": "sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==", + "license": "MIT", + "dependencies": { + "@types/pg": "*" + } + }, + "node_modules/@types/pluralize": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/pluralize/-/pluralize-0.0.33.tgz", + "integrity": "sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==", + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.10", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.10.tgz", + "integrity": "sha512-nbt4IWXABhW0jGmmpRzCFNlbmwCTzZ2gTUsNIr+X+ItdqPms+PAJZbWsNzpS2USqXjcoNLQcO6nXo60zcPQiIg==", + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/tedious": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", + "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz", + "integrity": "sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.60.0", + "@typescript-eslint/type-utils": "8.60.0", + "@typescript-eslint/utils": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.60.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.0.tgz", + "integrity": "sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.60.0", + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.0.tgz", + "integrity": "sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.60.0", + "@typescript-eslint/types": "^8.60.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz", + "integrity": "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.0.tgz", + "integrity": "sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.0.tgz", + "integrity": "sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0", + "@typescript-eslint/utils": "8.60.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.0.tgz", + "integrity": "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.0.tgz", + "integrity": "sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.60.0", + "@typescript-eslint/tsconfig-utils": "8.60.0", + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.0.tgz", + "integrity": "sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.60.0", + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz", + "integrity": "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vinejs/compiler": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@vinejs/compiler/-/compiler-4.1.3.tgz", + "integrity": "sha512-UyH7Zn8dkTMLeU+PF2WjCnWkFb2qYaOxAcvp/uXW0njtKNcJOnVJaPsnWYwqewkTcHN47yvOdzosj3kj3RAP5w==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@vinejs/vine": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@vinejs/vine/-/vine-4.4.0.tgz", + "integrity": "sha512-cfnNXjs9+f+22d3Eb8koyg2qlCbaT394XBlL4AeEr1WM+NH1omFcXwe1zrkW4hHwPDSfUd4lFUdNO6E+YnTeWQ==", + "license": "MIT", + "dependencies": { + "@poppinss/macroable": "^1.1.0", + "@poppinss/types": "^1.2.1", + "@standard-schema/spec": "^1.1.0", + "@types/validator": "^13.15.10", + "@vinejs/compiler": "^4.1.3", + "camelcase": "^9.0.0", + "dayjs": "^1.11.19", + "dlv": "^1.1.3", + "normalize-url": "^8.1.1", + "validator": "^13.15.26" + }, + "engines": { + "node": ">=18.16.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", + "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz", + "integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz", + "integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/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/@vitest/pretty-format": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz", + "integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz", + "integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.7", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz", + "integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.7", + "@vitest/utils": "4.1.7", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz", + "integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz", + "integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.7", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.35.tgz", + "integrity": "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.35", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.35.tgz", + "integrity": "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.35", + "@vue/shared": "3.5.35" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.35.tgz", + "integrity": "sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/compiler-core": "3.5.35", + "@vue/compiler-dom": "3.5.35", + "@vue/compiler-ssr": "3.5.35", + "@vue/shared": "3.5.35", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.35.tgz", + "integrity": "sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.35", + "@vue/shared": "3.5.35" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.35.tgz", + "integrity": "sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.35" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.35.tgz", + "integrity": "sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.35", + "@vue/shared": "3.5.35" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.35.tgz", + "integrity": "sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.35", + "@vue/runtime-core": "3.5.35", + "@vue/shared": "3.5.35", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.35.tgz", + "integrity": "sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.35", + "@vue/shared": "3.5.35" + }, + "peerDependencies": { + "vue": "3.5.35" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.35.tgz", + "integrity": "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==", + "license": "MIT" + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/angular-html-parser": { + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/angular-html-parser/-/angular-html-parser-10.6.1.tgz", + "integrity": "sha512-d49rV6cIh8yEYhEs2+YYDOiDLzlRYTKwbBxzitNo21nksKmi0sdU2pypsYi5i6bzV/sL743XrrV/jZZA0VUVIw==", + "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", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, + "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/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/axios": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", + "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/boosted": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/boosted/-/boosted-5.3.8.tgz", + "integrity": "sha512-khvSYjW/iP9ciMWk/YbA2FpzH9OY7lpuOIVzBowdX1vaUvBWl5GW4juY1Zy4uHiHSFln3dhVKjzOQJQaxegL9Q==", + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bson": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz", + "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/builtin-modules": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.2.0.tgz", + "integrity": "sha512-02yxLeyxF4dNl6SlY6/5HfRSrSdZ/sCPoxy2kZNP5dZZX8LSAD9aE2gtJIUgWrsQTiMPl3mxESyrobSwvRGisQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-9.0.0.tgz", + "integrity": "sha512-TO9xmyXTZ9HUHI8M1OnvExxYB0eYVS/1e5s7IDMTAoIcwUd+aNcFODs6Xk83mobk0velyHFQgA1yIrvYc6wclw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/case-anything": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-3.1.2.tgz", + "integrity": "sha512-wljhAjDDIv/hM2FzgJnYQg90AWmZMNtESCjTeLH680qTzdo0nErlCxOmgzgX4ZsZAtIvqHyD87ES8QyriXB+BQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/check-disk-space": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/check-disk-space/-/check-disk-space-3.4.0.tgz", + "integrity": "sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "license": "MIT" + }, + "node_modules/clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clean-regexp/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/cli-boxes": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-4.0.1.tgz", + "integrity": "sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw==", + "license": "MIT", + "engines": { + "node": ">=18.20 <19 || >=20.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-table3/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/code-block-writer": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "dev": true, + "license": "MIT" + }, + "node_modules/codemirror-wrapped-line-indent": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/codemirror-wrapped-line-indent/-/codemirror-wrapped-line-indent-1.0.9.tgz", + "integrity": "sha512-oc976hHLt35u6Ojbhub+IWOxEpapZSqYieLEdGhsgFZ4rtYQtdb5KjxzgjCCyVe3t0yk+a6hmaIOEsjU/tZRxQ==", + "license": "MIT", + "peerDependencies": { + "@codemirror/language": "^6.9.0", + "@codemirror/state": "^6.2.1", + "@codemirror/view": "^6.17.1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true, + "license": "ISC" + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", + "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csrf": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", + "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", + "license": "MIT", + "dependencies": { + "rndm": "1.2.0", + "tsscmp": "1.0.6", + "uid-safe": "2.1.5" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "license": "MIT" + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/edge-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/edge-error/-/edge-error-4.0.2.tgz", + "integrity": "sha512-jB76VYn8wapDHKHSOmP3vbKLoa77RJYsTLNmfl8+cuCD69uxZtP3h+kqV+Prw/YkYmN7yHyp4IApE15pDByk0A==", + "license": "MIT", + "engines": { + "node": ">=18.16.0" + } + }, + "node_modules/edge-lexer": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/edge-lexer/-/edge-lexer-6.0.5.tgz", + "integrity": "sha512-paSprHn8GRzOUWTVLapgacqDBSOpJgOY60/V5QQ8bbNLHRKUMSxp8Z2oOO0WKtcKmb5+sAlmvG3izhbFulp19A==", + "license": "MIT", + "dependencies": { + "edge-error": "^4.0.2" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/edge-parser": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/edge-parser/-/edge-parser-9.1.1.tgz", + "integrity": "sha512-kem/vInJgTzHCABdFe060WGjyGva8j23QwVbZkNFXkNAQhRz3mga5kNBuPFYQ3t73nPT2vZ8WOGV4OCS0P16Cw==", + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "astring": "^1.9.0", + "edge-error": "^4.0.2", + "edge-lexer": "^6.0.5", + "js-stringify": "^1.0.2" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/edge.js": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/edge.js/-/edge.js-6.5.1.tgz", + "integrity": "sha512-+NgWA8KunEdhVB247GWdmqUZ3MSIExeUCAbWNgNvA0ONH43UL+a/l6AE8GsevT4beqVsbp4Q6USwSpfbnMqBkQ==", + "license": "MIT", + "dependencies": { + "@poppinss/inspect": "^1.0.1", + "@poppinss/macroable": "^1.1.2", + "@poppinss/utils": "^7.0.1", + "edge-error": "^4.0.2", + "edge-lexer": "^6.0.5", + "edge-parser": "^9.1.1", + "he": "^1.2.0", + "property-information": "^7.1.0", + "stringify-attributes": "^4.0.0" + }, + "engines": { + "node": ">=18.16.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.364", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz", + "integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-1.2.1.tgz", + "integrity": "sha512-sFz64DCRjirhwHLxofFqxYQm6DCp6o0Ix7jwKQvuCHPn4GMRZNuBZyLPu9Ccmk/QSCAMZt6FOUqA8JZCQvA9fw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "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==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", + "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", + "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz", + "integrity": "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.13" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-unicorn": { + "version": "64.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-64.0.0.tgz", + "integrity": "sha512-rNZwalHh8i0UfPlhNwg5BTUO1CMdKNmjqe+TgzOTZnpKoi8VBgsW7u9qCHIdpxEzZ1uwrJrPF0uRb7l//K38gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "@eslint-community/eslint-utils": "^4.9.1", + "change-case": "^5.4.4", + "ci-info": "^4.4.0", + "clean-regexp": "^1.0.0", + "core-js-compat": "^3.49.0", + "find-up-simple": "^1.0.1", + "globals": "^17.4.0", + "indent-string": "^5.0.0", + "is-builtin-module": "^5.0.0", + "jsesc": "^3.1.0", + "pluralize": "^8.0.0", + "regexp-tree": "^0.1.27", + "regjsparser": "^0.13.0", + "semver": "^7.7.4", + "strip-indent": "^4.1.1" + }, + "engines": { + "node": "^20.10.0 || >=21.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" + }, + "peerDependencies": { + "eslint": ">=9.38.0" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrap": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.9.tgz", + "integrity": "sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "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/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.8.0.tgz", + "integrity": "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "node_modules/fast-copy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.3.tgz", + "integrity": "sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.8.0.tgz", + "integrity": "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.2.0", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.3.0", + "xml-naming": "^0.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "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", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-type": { + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-22.0.1.tgz", + "integrity": "sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.5", + "token-types": "^6.1.2", + "uint8array-extras": "^1.5.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-directory": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/find-cache-directory/-/find-cache-directory-6.0.0.tgz", + "integrity": "sha512-CvFd5ivA6HcSHbD+59P7CyzINHXzwhuQK8RY7CxJZtgDSAtRlHiCaQpZQ2lMR/WRyUIEmzUvL6G2AGurMfegZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^8.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/flattie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", + "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", + "license": "MIT" + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-port": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.2.0.tgz", + "integrity": "sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/getopts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", + "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==", + "license": "MIT" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/hot-hook": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hot-hook/-/hot-hook-1.0.0.tgz", + "integrity": "sha512-OkZm5tTE4ej8ur8VlcQwMm8G9sFxu4D+shM+ol/h4mrUhuZvFkjk5n/nWKmLq3COmy6epLN7XIIQJ75tnqCGIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "fast-glob": "^3.3.3", + "parse-imports": "^3.0.0", + "picomatch": "^4.0.3", + "read-package-up": "^12.0.0" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "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/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/igniculus": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/igniculus/-/igniculus-1.5.0.tgz", + "integrity": "sha512-vhj2J/cSzNg2G5tcK4Z1KZdeYmQa5keoxFULUYAxctK/zHJb1oraO7noCqnJxKe1b2eZdiiaSL1IHPOFAI8UYQ==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.6.tgz", + "integrity": "sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/immutable-json-patch": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/immutable-json-patch/-/immutable-json-patch-6.0.2.tgz", + "integrity": "sha512-KwCA5DXJiyldda8SPha1zB+6+vbEi5/jRRcYii/6yFXlyu9ZjiSH/wPq8Ri2Hk8iGjjTMcHW3Z21S4MOpl7sOw==", + "license": "ISC" + }, + "node_modules/import-in-the-middle": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.0.1.tgz", + "integrity": "sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.15.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflation": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.1.0.tgz", + "integrity": "sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-builtin-module": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz", + "integrity": "sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-modules": "^5.0.0" + }, + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "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-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "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/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-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jest-diff": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", + "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.4.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsep": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/json-source-map/-/json-source-map-0.6.1.tgz", + "integrity": "sha512-1QoztHPsMQqhDq0hlXY5ZqcEdUzxQEIxgFkKl4WUp2pgShObl+9ovi4kRh2TfvAfxAoHOJ9vIMEqk3k4iex7tg==", + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonpath-plus": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.4.0.tgz", + "integrity": "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==", + "license": "MIT", + "dependencies": { + "@jsep-plugin/assignment": "^1.3.0", + "@jsep-plugin/regex": "^1.0.4", + "jsep": "^1.4.0" + }, + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/jsonrepair": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.14.0.tgz", + "integrity": "sha512-tWPGKMZf/8UPim+fcW2EfcQ/d/7aKUrP6IECz9G3Tu6Q5dX0orSleqJ9z6sSw7qrQkjF8/Edo4DvsWBZ8H+HNg==", + "license": "ISC", + "bin": { + "jsonrepair": "bin/cli.js" + } + }, + "node_modules/jsonschema": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz", + "integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/junk": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/junk/-/junk-4.0.1.tgz", + "integrity": "sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/knex": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/knex/-/knex-3.2.10.tgz", + "integrity": "sha512-oypTHfrc9i72iyxaUQBKHOxhcr0xM65MPf6FpN02nimsftXwzXprIkLjfXdubvhbu4PMWLp023q8o8CYvHSuZw==", + "license": "MIT", + "dependencies": { + "colorette": "2.0.19", + "commander": "^10.0.0", + "debug": "4.3.4", + "escalade": "^3.1.1", + "esm": "^3.2.25", + "get-package-type": "^0.1.0", + "getopts": "2.3.0", + "interpret": "^2.2.0", + "lodash": "^4.18.1", + "pg-connection-string": "2.6.2", + "rechoir": "^0.8.0", + "resolve-from": "^5.0.0", + "tarn": "^3.0.2", + "tildify": "2.0.0" + }, + "bin": { + "knex": "bin/cli.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "pg-query-stream": "^4.14.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "mysql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/knex-dynamic-connection": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/knex-dynamic-connection/-/knex-dynamic-connection-5.1.0.tgz", + "integrity": "sha512-kv41rybMmUKOEAxkGytKRITAthuHFzt0l5vRT1SuTOcve4a9JMc1t/ElVOTRlBEkBexWX8A5WcO71cufrqAxkw==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "knex": "3.2.7" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/knex-dynamic-connection/node_modules/knex": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/knex/-/knex-3.2.7.tgz", + "integrity": "sha512-VxdDE72x7Tc08E5yCu8HqYoeOm0HOjAraOtYiGSAUJTYkydwfSGBOpQqYHrzM5vjLNzw2JDL2vDH8m7DjIjtgA==", + "license": "MIT", + "dependencies": { + "colorette": "2.0.19", + "commander": "^10.0.0", + "debug": "4.3.4", + "escalade": "^3.1.1", + "esm": "^3.2.25", + "get-package-type": "^0.1.0", + "getopts": "2.3.0", + "interpret": "^2.2.0", + "lodash": "^4.17.21", + "pg-connection-string": "2.6.2", + "rechoir": "^0.8.0", + "resolve-from": "^5.0.0", + "tarn": "^3.0.2", + "tildify": "2.0.0" + }, + "bin": { + "knex": "bin/cli.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "pg-query-stream": "^4.14.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "mysql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/knex-dynamic-connection/node_modules/knex/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/knex-dynamic-connection/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/knex/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/knex/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/ky": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/ky/-/ky-1.14.3.tgz", + "integrity": "sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, + "node_modules/laravel-precognition": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/laravel-precognition/-/laravel-precognition-1.0.2.tgz", + "integrity": "sha512-0H08JDdMWONrL/N314fvsO3FATJwGGlFKGkMF3nNmizVFJaWs17816iM+sX7Rp8d5hUjYCx6WLfsehSKfaTxjg==", + "license": "MIT", + "dependencies": { + "axios": "^1.4.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/log-update": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-7.2.0.tgz", + "integrity": "sha512-iLs7dGSyjZiUgvrUvuD3FndAxVJk+TywBkkkwUSm9HdYoskJalWg5qVsEiXeufPvRVPbCUmNQewg798rx+sPXg==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.3.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^8.0.0", + "strip-ansi": "^7.2.0", + "wrap-ansi": "^10.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, + "node_modules/mongodb": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.2.0.tgz", + "integrity": "sha512-F/2+BMZtLVhY30ioZp0dAmZ+IRZMBqI+nrv6t5+9/1AIwCa8sMRC3jBf81lpxMhnZgqq8CoUD503Z1oZWq1/sw==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^7.2.0", + "mongodb-connection-string-url": "^7.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.806.0", + "@mongodb-js/zstd": "^7.0.0", + "gcp-metadata": "^7.0.1", + "kerberos": "^7.0.0", + "mongodb-client-encryption": ">=7.0.0 <7.1.0", + "snappy": "^7.3.2", + "socks": "^2.8.6" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz", + "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-releases": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", + "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-package-data": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-8.0.0.tgz", + "integrity": "sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^9.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/normalize-url": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/npm-run-path/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/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-to-formdata": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/object-to-formdata/-/object-to-formdata-4.5.1.tgz", + "integrity": "sha512-QiM9D0NiU5jV6J6tjE1g7b4Z2tcUnKs1OPUi4iMb2zH+7jwlcUrASghgkFk9GtzqNNq8rTQJtT8AzjBAvLoNMw==", + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", + "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.4.0", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.1.0", + "wsl-utils": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/parse-imports/-/parse-imports-3.0.0.tgz", + "integrity": "sha512-IwiqoJANa4O6M76LBWEvoS2iPIUqBOnKG1lV3/J0oVM6V2XjED+mYAXedEMX5xUglVjfGpZOfaEyuOUjBuUE4g==", + "license": "Apache-2.0 AND MIT", + "dependencies": { + "es-module-lexer": "^1.7.0", + "slashes": "^3.0.12" + }, + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/TomerAberbach" + } + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "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/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz", + "integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "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/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^4.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/pkg-dir": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-8.0.0.tgz", + "integrity": "sha512-4peoBq4Wks0riS0z8741NVv+/8IiTvqnZAr8QGgtdifrtpdXbNw/FxRS1l6NFqm4EMzuS0EDqNNx4XGaz8cuyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up-simple": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-edge": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-edge/-/prettier-edge-1.0.0.tgz", + "integrity": "sha512-D8DwcGGPVWhMUrrE0+R242laEezGmvOpdCdvwVZiySpebZ1H5o7xwmDYhRw//ckOz1EcLOtQyJt/eQ2UC3eXOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "angular-html-parser": "^10.6.1", + "edge-lexer": "^6.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "prettier": "^3.0.0" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.4.1", + "ansi-styles": "^5.2.0", + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/protobufjs": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.1.tgz", + "integrity": "sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz", + "integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react-is-18": { + "name": "react-is", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-is-19": { + "name": "react-is", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", + "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/read-package-up": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-12.0.0.tgz", + "integrity": "sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up-simple": "^1.0.1", + "read-pkg": "^10.0.0", + "type-fest": "^5.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-10.1.0.tgz", + "integrity": "sha512-I8g2lArQiP78ll51UeMZojewtYgIRCKCWqZEgOO8c/uefTI+XDXvCSXu3+YNUaTNvZzobrL5+SqHjBrByRRTdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.4", + "normalize-package-data": "^8.0.0", + "parse-json": "^8.3.0", + "type-fest": "^5.4.4", + "unicorn-magic": "^0.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/unicorn-magic": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", + "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/regjsparser": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz", + "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rndm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", + "integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", + "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.132.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.2", + "@rolldown/binding-darwin-arm64": "1.0.2", + "@rolldown/binding-darwin-x64": "1.0.2", + "@rolldown/binding-freebsd-x64": "1.0.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", + "@rolldown/binding-linux-arm64-gnu": "1.0.2", + "@rolldown/binding-linux-arm64-musl": "1.0.2", + "@rolldown/binding-linux-ppc64-gnu": "1.0.2", + "@rolldown/binding-linux-s390x-gnu": "1.0.2", + "@rolldown/binding-linux-x64-gnu": "1.0.2", + "@rolldown/binding-linux-x64-musl": "1.0.2", + "@rolldown/binding-openharmony-arm64": "1.0.2", + "@rolldown/binding-wasm32-wasi": "1.0.2", + "@rolldown/binding-win32-arm64-msvc": "1.0.2", + "@rolldown/binding-win32-x64-msvc": "1.0.2" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.100.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.100.0.tgz", + "integrity": "sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/send/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slashes": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/slashes/-/slashes-3.0.12.tgz", + "integrity": "sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==", + "license": "ISC" + }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slugify": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.9.tgz", + "integrity": "sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "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==", + "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", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/split-lines": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/split-lines/-/split-lines-3.0.0.tgz", + "integrity": "sha512-d0TpRBL/VfKDXsk8JxPF7zgF5pCUDdBMSlEL36xBgVeaX448t+yGXcJaikUyzkoKOJ0l6KpMfygzJU9naIuivw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "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/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/stringify-attributes": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/stringify-attributes/-/stringify-attributes-4.0.0.tgz", + "integrity": "sha512-6Hq3K153wTTfhEHb4V/viuqmb0DRn08JCrRnmqc4Q/tmoNuvd4DEyqkiiJXtvVz8ZSUhlCQr7zCpCVTgrelesg==", + "license": "MIT", + "dependencies": { + "escape-goat": "^4.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/strip-indent": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", + "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "5.55.10", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.10.tgz", + "integrity": "sha512-v9mFVBY1USosyIWdXE7Cg4AN0ywyKCMcAhONvli8doMowEhFhMdNLKD1j7O/UnsrdVTHaUOk/jv8hD/HClVy+g==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.10", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.8.1", + "esm-env": "^1.2.1", + "esrap": "^2.2.9", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.3.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/tempura": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/tempura/-/tempura-0.4.1.tgz", + "integrity": "sha512-NQ4Cs23jM6UUp3CcS5vjmyjTC6dtA5EsflBG2cyG0wZvP65AV26tJ920MGvTRYIImCY13RBpOhc7q4/pu+FG5A==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/terminal-size": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/terminal-size/-/terminal-size-4.0.1.tgz", + "integrity": "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, + "node_modules/tildify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", + "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/timekeeper": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-2.3.1.tgz", + "integrity": "sha512-LeQRS7/4JcC0PgdSFnfUiStQEdiuySlCj/5SJ18D+T1n9BoY7PxKFfCwLulpHXoLUFr67HxBddQdEX47lDGx1g==", + "dev": true, + "license": "MIT" + }, + "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": "1.2.3", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.3.tgz", + "integrity": "sha512-g62dB+w1/OEFnPvmX0yd/HnetYITOL+1nJW7kitOycOeAvmbWC/nu0fwmmQ/kupNojqExzyC/T++pST/jRJ2mQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tmp-cache": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tmp-cache/-/tmp-cache-1.1.0.tgz", + "integrity": "sha512-j040fkL/x+XAZQ9K3bKGEPwgYhOZNBQLa3NXEADUiuno9C+3N2JJA4bVPDREixp604G3/vTXWA3DIPpA9lu1RQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-morph": { + "version": "27.0.2", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-27.0.2.tgz", + "integrity": "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.28.1", + "code-block-writer": "^13.0.3" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz", + "integrity": "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.0.tgz", + "integrity": "sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.60.0", + "@typescript-eslint/parser": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0", + "@typescript-eslint/utils": "8.60.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "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/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vanilla-jsoneditor": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/vanilla-jsoneditor/-/vanilla-jsoneditor-3.12.0.tgz", + "integrity": "sha512-3cLH1jdr2t1+t9XnPkF9EiR394ty8hcVNX/GTj83RjEmkUMZyL/HvQ3e1PvQ3Be8rfH3AKcgySZYLKFfpVnjqQ==", + "license": "ISC", + "dependencies": { + "@codemirror/autocomplete": "^6.18.1", + "@codemirror/commands": "^6.7.1", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/language": "^6.10.3", + "@codemirror/lint": "^6.8.2", + "@codemirror/search": "^6.5.6", + "@codemirror/state": "^6.4.1", + "@codemirror/view": "^6.34.1", + "@fortawesome/free-regular-svg-icons": "^6.6.0 || ^7.0.1", + "@fortawesome/free-solid-svg-icons": "^6.6.0 || ^7.0.1", + "@jsonquerylang/jsonquery": "^3.1.1 || ^4.0.0 || ^5.0.0", + "@lezer/highlight": "^1.2.1", + "@replit/codemirror-indentation-markers": "^6.5.3", + "ajv": "^8.17.1", + "codemirror-wrapped-line-indent": "^1.0.8", + "diff-sequences": "^29.6.3", + "immutable-json-patch": "^6.0.1", + "jmespath": "^0.16.0", + "json-source-map": "^0.6.1", + "jsonpath-plus": "^10.3.0", + "jsonrepair": "^3.0.0", + "lodash-es": "^4.17.23", + "memoize-one": "^6.0.0", + "natural-compare-lite": "^1.4.0", + "svelte": "^5.0.0", + "vanilla-picker": "^2.12.3" + } + }, + "node_modules/vanilla-jsoneditor/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "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/vanilla-jsoneditor/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/vanilla-picker": { + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/vanilla-picker/-/vanilla-picker-2.12.3.tgz", + "integrity": "sha512-qVkT1E7yMbUsB2mmJNFmaXMWE2hF8ffqzMMwe9zdAikd8u2VfnsVY2HQcOUi2F38bgbxzlJBEdS1UUhOXdF9GQ==", + "license": "ISC", + "dependencies": { + "@sphinxxxx/color-conversion": "^2.2.2" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", + "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.2", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "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/vite-plugin-restart": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vite-plugin-restart/-/vite-plugin-restart-2.0.0.tgz", + "integrity": "sha512-OYsD89msjtd72HHpXnidZmQ+14ztJR74IxQq9aPa48LUx3IeukS+NmnVtk+/VaNoYQJLnTFWG3Sbq/AEwaAyeQ==", + "license": "MIT", + "dependencies": { + "micromatch": "^4.0.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.9.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/vitest": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz", + "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.7", + "@vitest/mocker": "4.1.7", + "@vitest/pretty-format": "4.1.7", + "@vitest/runner": "4.1.7", + "@vitest/snapshot": "4.1.7", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.7", + "@vitest/browser-preview": "4.1.7", + "@vitest/browser-webdriverio": "4.1.7", + "@vitest/coverage-istanbul": "4.1.7", + "@vitest/coverage-v8": "4.1.7", + "@vitest/ui": "4.1.7", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.35.tgz", + "integrity": "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.35", + "@vue/compiler-sfc": "3.5.35", + "@vue/runtime-dom": "3.5.35", + "@vue/server-renderer": "3.5.35", + "@vue/shared": "3.5.35" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-i18n": { + "version": "11.4.4", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.4.tgz", + "integrity": "sha512-gIbXVSFQV4jcSJxfwdZ5zSZmZ+12CnX0K3vBkRSd6Zn+HSzCp+QwUgPwpD/uN0oKNKI9RzlUXPKVedEuMgNG0A==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "11.4.4", + "@intlify/devtools-types": "11.4.4", + "@intlify/shared": "11.4.4", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "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==", + "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==", + "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/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", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", + "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "string-width": "^8.2.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/wsl-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", + "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml-formatter": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/xml-formatter/-/xml-formatter-3.7.0.tgz", + "integrity": "sha512-+8qTc3zv2UcJ1v9IsSIce37Dl4MQG14Cp7tWrwmy202UaI1wqRukw5QMX1JHsV+DX64yw77EgGsj2s5wGvuMbQ==", + "license": "MIT", + "dependencies": { + "xml-parser-xo": "^4.1.5" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/xml-parser-xo": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/xml-parser-xo/-/xml-parser-xo-4.1.6.tgz", + "integrity": "sha512-mXbSNSM+rIwndoxSwmFa83bOdeP8G/cOGr7XvxA67X7ebCzoAuVez9JYDCDub3zRDNBZxIbfjuXLSsamr7NfwQ==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "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/youch": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.1.tgz", + "integrity": "sha512-mxW3qiSnl+GRxXsaUMzv2Mbada1Y8CDltET9UxejDQe6DBYlSekghl5U5K0ReAikcHDi0G1vKZEmmo/NWAGKLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.6", + "@poppinss/dumper": "^0.7.0", + "@speed-highlight/core": "^1.2.14", + "cookie-es": "^3.0.1", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d20635f --- /dev/null +++ b/package.json @@ -0,0 +1,98 @@ +{ + "name": "adam-acs", + "version": "0.0.0", + "private": true, + "type": "module", + "license": "MIT", + "exports": { + "./data": "./.adonisjs/client/data.d.ts", + "./registry": "./.adonisjs/client/registry/index.ts" + }, + "scripts": { + "start": "node bin/server.js", + "build": "node ace build", + "dev": "node ace serve --hmr", + "test": "node ace test", + "lint": "eslint .", + "format": "prettier --write .", + "typecheck": "tsc --noEmit" + }, + "imports": { + "#controllers/*": "./app/controllers/*.js", + "#exceptions/*": "./app/exceptions/*.js", + "#models/*": "./app/models/*.js", + "#mails/*": "./app/mails/*.js", + "#services/*": "./app/services/*.js", + "#listeners/*": "./app/listeners/*.js", + "#events/*": "./app/events/*.js", + "#generated/*": "./.adonisjs/server/*.js", + "#middleware/*": "./app/middleware/*.js", + "#transformers/*": "./app/transformers/*.js", + "#validators/*": "./app/validators/*.js", + "#providers/*": "./providers/*.js", + "#policies/*": "./app/policies/*.js", + "#abilities/*": "./app/abilities/*.js", + "#database/*": "./database/*.js", + "#tests/*": "./tests/*.js", + "#start/*": "./start/*.js", + "#config/*": "./config/*.js" + }, + "devDependencies": { + "@adonisjs/assembler": "^8.4.0", + "@adonisjs/eslint-config": "^3.1.0", + "@adonisjs/prettier-config": "^1.5.0", + "@adonisjs/tsconfig": "^2.0.0", + "@japa/assert": "^4.2.0", + "@japa/plugin-adonisjs": "^5.2.0", + "@japa/runner": "^5.3.0", + "@poppinss/ts-exec": "^1.4.4", + "@types/node": "~25.9.1", + "@types/xml2js": "^0.4.14", + "@vitejs/plugin-vue": "^6.0.7", + "eslint": "^10.4.0", + "fast-check": "^4.8.0", + "hot-hook": "^1.0.0", + "pino-pretty": "^13.1.3", + "prettier": "^3.8.3", + "sass": "^1.100.0", + "typescript": "~6.0.3", + "vitest": "^4.1.7", + "youch": "^4.1.1" + }, + "dependencies": { + "@adonisjs/auth": "^10.1.0", + "@adonisjs/core": "^7.3.3", + "@adonisjs/cors": "^3.0.0", + "@adonisjs/inertia": "^4.2.0", + "@adonisjs/lucid": "^22.4.2", + "@adonisjs/session": "^8.1.0", + "@adonisjs/shield": "^9.0.0", + "@adonisjs/static": "^2.0.1", + "@adonisjs/vite": "^5.1.1", + "@inertiajs/vue3": "^2.3.8", + "@japa/api-client": "^3.2.1", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/auto-instrumentations-node": "^0.76.0", + "@opentelemetry/sdk-node": "^0.218.0", + "@popperjs/core": "^2.11.8", + "@tuyau/core": "^1.2.2", + "@vinejs/vine": "^4.4.0", + "boosted": "^5.3.8", + "edge.js": "^6.4.0", + "fast-xml-parser": "^5.8.0", + "mongodb": "^7.2.0", + "reflect-metadata": "^0.2.2", + "vanilla-jsoneditor": "^3.12.0", + "vue": "^3.5.35", + "vue-i18n": "^11.4.4", + "xml-formatter": "^3.7.0", + "xml2js": "^0.6.2" + }, + "hotHook": { + "boundaries": [ + "./app/controllers/**/*.ts", + "./app/middleware/*.ts" + ] + }, + "prettier": "@adonisjs/prettier-config" +} diff --git a/providers/mongodb_provider.ts b/providers/mongodb_provider.ts new file mode 100644 index 0000000..4193132 --- /dev/null +++ b/providers/mongodb_provider.ts @@ -0,0 +1,55 @@ +import type { ApplicationService } from '@adonisjs/core/types' +import { Db, MongoClient } from 'mongodb' + +declare module '@adonisjs/core/types' { + interface ContainerBindings { + mongodb: Db + } +} + +/** + * Singleton reference to the MongoDB Db instance. + * Set during provider boot, used synchronously by BaseModel. + */ +let dbInstance: Db | null = null + +export function getMongoDb(): Db { + if (!dbInstance) { + throw new Error('MongoDB is not connected. Ensure MongodbProvider has booted.') + } + return dbInstance +} + +export default class MongodbProvider { + private client: MongoClient | null = null + + constructor(protected app: ApplicationService) {} + + async boot() { + const config = await import('#config/database') + const { uri, dbName } = config.default + + this.client = new MongoClient(uri, { + monitorCommands: true, + serverSelectionTimeoutMS: 5000, + }) + + await this.client.connect() + const db = this.client.db(dbName) + + dbInstance = db + this.app.container.bindValue('mongodb', db) + } + + async shutdown() { + if (this.client) { + try { + await this.client.close() + } catch { + // Connection may already be closed + } + this.client = null + dbInstance = null + } + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..67991aa --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,5 @@ + + + ACS + diff --git a/resources/views/inertia_layout.edge b/resources/views/inertia_layout.edge new file mode 100644 index 0000000..7e4182f --- /dev/null +++ b/resources/views/inertia_layout.edge @@ -0,0 +1,14 @@ + + + + + + ADAM ACS + + @vite(['inertia/app/app.ts']) + @inertiaHead() + + + @inertia() + + diff --git a/start/env.ts b/start/env.ts new file mode 100644 index 0000000..ebdf021 --- /dev/null +++ b/start/env.ts @@ -0,0 +1,45 @@ +/* +|-------------------------------------------------------------------------- +| Environment variables service +|-------------------------------------------------------------------------- +| +| The `Env.create` method creates an instance of the Env service. The +| service validates the environment variables and also cast values +| to JavaScript data types. +| +*/ + +import { Env } from '@adonisjs/core/env' + +export default await Env.create(new URL('../', import.meta.url), { + // Node + NODE_ENV: Env.schema.enum(['development', 'production', 'test'] as const), + PORT: Env.schema.number(), + HOST: Env.schema.string({ format: 'host' }), + LOG_LEVEL: Env.schema.string(), + + // App + APP_KEY: Env.schema.secret(), + APP_URL: Env.schema.string({ format: 'url', tld: false }), + + // Session + SESSION_DRIVER: Env.schema.enum(['cookie', 'memory'] as const), + + // MongoDB + MONGODB_URI: Env.schema.string(), + MONGODB_DB_NAME: Env.schema.string.optional(), + + // Observability + OTEL_EXPORTER_URI: Env.schema.string.optional(), + OTEL_SAMPLER_MODE: Env.schema.string.optional(), + OTEL_SAMPLER_RATIO: Env.schema.string.optional(), + OTEL_SERVICE_NAME: Env.schema.string.optional(), + + // Remote logging + LOG_HOST: Env.schema.string.optional(), + LOG_PORT: Env.schema.number.optional(), + LOG_PROTOCOL: Env.schema.string.optional(), + + // Security + HASH_KEY: Env.schema.string.optional(), +}) diff --git a/start/kernel.ts b/start/kernel.ts new file mode 100644 index 0000000..895c408 --- /dev/null +++ b/start/kernel.ts @@ -0,0 +1,23 @@ +import router from '@adonisjs/core/services/router' +import server from '@adonisjs/core/services/server' + +server.errorHandler(() => import('#exceptions/handler')) + +server.use([ + () => import('#middleware/container_bindings_middleware'), + () => import('@adonisjs/static/static_middleware'), + () => import('@adonisjs/cors/cors_middleware'), + () => import('@adonisjs/vite/vite_middleware'), +]) + +router.use([ + () => import('@adonisjs/core/bodyparser_middleware'), + () => import('@adonisjs/session/session_middleware'), + () => import('@adonisjs/shield/shield_middleware'), + () => import('#middleware/inertia_middleware'), +]) + +export const middleware = router.named({ + cwmpLogger: () => import('#middleware/cwmp_logger_middleware'), + cwmp: () => import('#middleware/cwmp_middleware'), +}) diff --git a/start/routes.ts b/start/routes.ts new file mode 100644 index 0000000..31ee0dc --- /dev/null +++ b/start/routes.ts @@ -0,0 +1,76 @@ +import router from '@adonisjs/core/services/router' + +import { middleware } from '#start/kernel' + +const DevicesController = () => import('#controllers/devices_controller') +const CommandsController = () => import('#controllers/commands_controller') +const ConfigurationsV1Controller = () => import('#controllers/configurations_v1_controller') +const FirmwareCatalogController = () => import('#controllers/firmware_catalog_controller') +const HealthController = () => import('#controllers/health_controller') +const PagesController = () => import('#controllers/pages_controller') +const CwmpController = () => import('#controllers/cwmp_controller') + +// CWMP/TR-069 endpoint (SOAP/XML) - uses dedicated middleware for XML parsing +router.post('/cwmp10', [CwmpController, 'handle']).use([middleware.cwmpLogger(), middleware.cwmp()]) + +// Health checks +router.get('/api/v1/checklife', [HealthController, 'checklife']) +router.get('/api/v1/checkservice', [HealthController, 'checkservice']) + +// REST API routes +router.group(() => { + // Devices + router.get('/devices', [DevicesController, 'index']) + router.get('/devices/criteria-values', [DevicesController, 'criteriaValues']) + router.get('/devices/:identifier', [DevicesController, 'show']) + router.delete('/devices/:identifier', [DevicesController, 'destroy']) + router.post('/devices/:identifier/command', [DevicesController, 'executeCommand']) + router.post('/devices/:identifier/refresh', [DevicesController, 'refresh']) + + // Commands + router.get('/commands', [CommandsController, 'index']) + router.get('/commands/:id', [CommandsController, 'show']) + router.post('/commands', [CommandsController, 'store']) + + // Unified Configuration API (v1) + router.get('/configurations', [ConfigurationsV1Controller, 'index']) + router.post('/configurations', [ConfigurationsV1Controller, 'store']) + router.get('/configurations/:id', [ConfigurationsV1Controller, 'show']) + router.put('/configurations/:id', [ConfigurationsV1Controller, 'update']) + router.delete('/configurations/:id', [ConfigurationsV1Controller, 'destroy']) + router.patch('/configurations/:id/toggle', [ConfigurationsV1Controller, 'toggleEnabled']) + router.patch('/configurations/:id/tags', [ConfigurationsV1Controller, 'manageTags']) + router.get('/configurations/:id/history', [ConfigurationsV1Controller, 'history']) + router.post('/configurations/:id/dry-run', [ConfigurationsV1Controller, 'dryRun']) + + // Firmware Catalog API (v1) + router.get('/firmware-catalog', [FirmwareCatalogController, 'index']) + router.get('/firmware-catalog/entries', [FirmwareCatalogController, 'listEntries']) + router.get('/firmware-catalog/entries/:id', [FirmwareCatalogController, 'showEntry']) + router.get('/firmware-catalog/routes', [FirmwareCatalogController, 'listRoutes']) + router.post('/firmware-catalog/entries', [FirmwareCatalogController, 'createEntry']) + router.put('/firmware-catalog/entries/:id', [FirmwareCatalogController, 'updateEntry']) + router.delete('/firmware-catalog/entries/:id', [FirmwareCatalogController, 'deleteEntry']) + router.post('/firmware-catalog/routes', [FirmwareCatalogController, 'createRoute']) + router.put('/firmware-catalog/routes/:id', [FirmwareCatalogController, 'updateRoute']) + router.delete('/firmware-catalog/routes/:id', [FirmwareCatalogController, 'deleteRoute']) +}).prefix('/api/v1') + +// Inertia page routes +router.get('/', [PagesController, 'home']) +router.get('/devices/:identifier', [PagesController, 'deviceDetail']) +router.post('/devices/:identifier/refresh', [PagesController, 'refreshDevice']) +router.post('/devices/:identifier/get-model', [PagesController, 'getModel']) +router.get('/commands', [PagesController, 'commands']).as('commands.page') +router.get('/commands/fleet', [PagesController, 'commandsFleet']) +router.post('/commands/fleet', [PagesController, 'storeFleetDevices']) +router.post('/commands/fleet/command', [PagesController, 'storeFleetCommand']) + +// Unified Configuration System Inertia routes +router.get('/configurations', [PagesController, 'configurationsList']) +router.get('/configurations/create', [PagesController, 'configurationCreate']) +router.get('/configurations/:id/edit', [PagesController, 'configurationEdit']) +router.get('/configurations/:id/detail', [PagesController, 'configurationDetail']) +router.get('/firmware-catalog', [PagesController, 'firmwareCatalog']) +router.get('/firmware-catalog/create', [PagesController, 'firmwareCatalogCreate']) +router.get('/firmware-catalog/:id/edit', [PagesController, 'firmwareCatalogEdit']) diff --git a/tests/bootstrap.ts b/tests/bootstrap.ts new file mode 100644 index 0000000..e9105d3 --- /dev/null +++ b/tests/bootstrap.ts @@ -0,0 +1,37 @@ +import { assert } from '@japa/assert' +import app from '@adonisjs/core/services/app' +import type { Config } from '@japa/runner/types' +import { pluginAdonisJS } from '@japa/plugin-adonisjs' +import testUtils from '@adonisjs/core/services/test_utils' + +/** + * This file is imported by the "bin/test.ts" entrypoint file + */ + +/** + * Configure Japa plugins in the plugins array. + * Learn more - https://japa.dev/docs/runner-config#plugins-optional + */ +export const plugins: Config['plugins'] = [assert(), pluginAdonisJS(app)] + +/** + * Configure lifecycle function to run before and after all the + * tests. + * + * The setup functions are executed before all the tests + * The teardown functions are executed after all the tests + */ +export const runnerHooks: Required> = { + setup: [], + teardown: [], +} + +/** + * Configure suites by tapping into the test suite instance. + * Learn more - https://japa.dev/docs/test-suites#lifecycle-hooks + */ +export const configureSuite: Config['configureSuite'] = (suite) => { + if (['browser', 'functional', 'e2e'].includes(suite.name)) { + return suite.setup(() => testUtils.httpServer().start()) + } +} diff --git a/tests/property/configuration_criteria.property.spec.ts b/tests/property/configuration_criteria.property.spec.ts new file mode 100644 index 0000000..032bd71 --- /dev/null +++ b/tests/property/configuration_criteria.property.spec.ts @@ -0,0 +1,168 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import fc from 'fast-check' +import ConfigurationService, { + ValidationError, +} from '../../app/services/configuration_service.js' +import type { CreateConfigurationDto } from '../../app/services/configuration_service.js' +import { PolicyType } from '../../app/domain/configuration/configuration.js' + +// Mock the models +vi.mock('#models/configuration', () => { + return { + default: { + createIfNotExists: vi.fn(), + findByIdentifier: vi.fn(), + findAllEnabled: vi.fn(), + paginateConfigurations: vi.fn(), + deleteByIdentifier: vi.fn(), + updateOne: vi.fn(), + }, + } +}) + +vi.mock('#models/configuration_version', () => { + return { + default: { + deleteByConfigurationIdentifier: vi.fn(), + getLatestVersionNumber: vi.fn(), + create: vi.fn(), + paginateVersions: vi.fn(), + }, + } +}) + +import ConfigurationModel from '#models/configuration' +import ConfigurationVersionModel from '#models/configuration_version' + +/** + * Property 4: Criteria restricted to DeviceId fields + * Validates: Requirements 2.1, 2.2 + * + * Property: Any criteria field not in {manufacturer, oui, productClass, serialNumber} + * is rejected with a ValidationError. + * + * For field names NOT in the allowed set, verify that ConfigurationService.create + * throws a ValidationError. For field names IN the allowed set, verify it does NOT + * throw on criteria validation. + */ +describe('Property 4: Criteria restricted to DeviceId fields', () => { + const ALLOWED_FIELDS = ['manufacturer', 'oui', 'productClass', 'serialNumber'] as const + + let service: ConfigurationService + + beforeEach(() => { + service = new ConfigurationService() + vi.clearAllMocks() + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([]) + vi.mocked(ConfigurationModel.createIfNotExists).mockResolvedValue({} as any) + vi.mocked(ConfigurationVersionModel.getLatestVersionNumber).mockResolvedValue(0) + vi.mocked(ConfigurationVersionModel.create).mockResolvedValue({} as any) + }) + + /** + * Builds a valid DTO with the given criteria, using minimal valid fields + * for all other properties so that only criteria validation is tested. + */ + function buildDtoWithCriteria(criteria: Record): CreateConfigurationDto { + return { + identifier: 'test-config', + name: 'Test Configuration', + version: '1.0.0', + priority: 1, + policy: PolicyType.MAINTAIN, + enabled: true, + tags: [], + criteria: criteria as any, + inventory: [], + desiredParameters: [], + } + } + + /** + * Arbitrary that generates field names guaranteed NOT to be in the allowed set. + * Uses alphanumeric strings to avoid edge cases with special characters as object keys. + */ + const invalidFieldName = fc + .stringMatching(/^[a-zA-Z][a-zA-Z0-9_]{0,30}$/) + .filter((s) => !ALLOWED_FIELDS.includes(s as any)) + + /** + * Arbitrary that picks one of the allowed field names. + */ + const validFieldName = fc.constantFrom(...ALLOWED_FIELDS) + + it('rejects any criteria field not in {manufacturer, oui, productClass, serialNumber}', async () => { + await fc.assert( + fc.asyncProperty(invalidFieldName, async (fieldName) => { + const dto = buildDtoWithCriteria({ [fieldName]: 'some-value' }) + + await expect(service.create(dto)).rejects.toThrow(ValidationError) + }) + ) + }) + + it('rejected criteria error mentions only DeviceId fields are allowed', async () => { + await fc.assert( + fc.asyncProperty(invalidFieldName, async (fieldName) => { + const dto = buildDtoWithCriteria({ [fieldName]: 'some-value' }) + + try { + await service.create(dto) + expect.fail('Expected ValidationError to be thrown') + } catch (error: any) { + expect(error).toBeInstanceOf(ValidationError) + expect(error.message).toContain('Only DeviceId fields are allowed') + expect(error.field).toBe('criteria') + } + }) + ) + }) + + it('accepts any single allowed field without throwing a criteria ValidationError', async () => { + await fc.assert( + fc.asyncProperty(validFieldName, async (fieldName) => { + const dto = buildDtoWithCriteria({ [fieldName]: 'some-value' }) + + // Should not throw — the create call should succeed (mocked DB) + await expect(service.create(dto)).resolves.not.toThrow() + }) + ) + }) + + it('accepts any subset of allowed fields without throwing a criteria ValidationError', async () => { + await fc.assert( + fc.asyncProperty( + fc.subarray([...ALLOWED_FIELDS], { minLength: 0, maxLength: 4 }), + async (fields) => { + const criteria: Record = {} + for (const field of fields) { + criteria[field] = 'test-value' + } + + const dto = buildDtoWithCriteria(criteria) + + await expect(service.create(dto)).resolves.not.toThrow() + } + ) + ) + }) + + it('rejects when any one field in criteria is invalid, even if others are valid', async () => { + await fc.assert( + fc.asyncProperty( + validFieldName, + invalidFieldName, + async (validField, invalidField) => { + const criteria: Record = { + [validField]: 'valid-value', + [invalidField]: 'invalid-value', + } + + const dto = buildDtoWithCriteria(criteria) + + await expect(service.create(dto)).rejects.toThrow(ValidationError) + } + ) + ) + }) +}) diff --git a/tests/property/configuration_crud.property.spec.ts b/tests/property/configuration_crud.property.spec.ts new file mode 100644 index 0000000..bf5c533 --- /dev/null +++ b/tests/property/configuration_crud.property.spec.ts @@ -0,0 +1,195 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import fc from 'fast-check' +import ConfigurationService from '../../app/services/configuration_service.js' +import type { CreateConfigurationDto } from '../../app/services/configuration_service.js' +import { PolicyType } from '../../app/domain/configuration/configuration.js' +import type { Configuration } from '../../app/domain/configuration/configuration.js' + +// Mock the models (same pattern as existing unit tests) +vi.mock('#models/configuration', () => { + return { + default: { + createIfNotExists: vi.fn(), + findByIdentifier: vi.fn(), + findAllEnabled: vi.fn(), + paginateConfigurations: vi.fn(), + deleteByIdentifier: vi.fn(), + updateOne: vi.fn(), + }, + } +}) + +vi.mock('#models/configuration_version', () => { + return { + default: { + deleteByConfigurationIdentifier: vi.fn(), + getLatestVersionNumber: vi.fn(), + create: vi.fn(), + paginateVersions: vi.fn(), + }, + } +}) + +import ConfigurationModel from '#models/configuration' + +/** + * Property 1: Configuration CRUD round-trip + * Validates: Requirements 1.1, 1.3 + * + * For any valid Configuration, create + findById returns identical fields. + */ +describe('Property 1: Configuration CRUD round-trip', () => { + let service: ConfigurationService + + beforeEach(() => { + service = new ConfigurationService() + vi.clearAllMocks() + // No overlap by default + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([]) + }) + + // ─── Arbitraries ───────────────────────────────────────────────────────────── + + /** + * Generate a valid identifier: starts with alphanumeric, followed by alphanumeric/hyphens/underscores + */ + const arbIdentifier = fc.stringMatching(/^[A-Za-z0-9][A-Za-z0-9_-]{0,20}$/) + + /** + * Generate a valid semver version string (e.g. "1.0.0") + */ + const arbVersion = fc.stringMatching(/^[0-9]+\.[0-9]+\.[0-9]+$/) + + /** + * Generate a positive integer priority + */ + const arbPriority = fc.integer({ min: 1, max: 10000 }) + + /** + * Generate a valid policy type + */ + const arbPolicy = fc.constantFrom(PolicyType.ONCE, PolicyType.MAINTAIN) + + /** + * Generate a valid TR-069 inventory path (ending with ".") + */ + const arbInventoryPath = fc.stringMatching(/^[A-Za-z][A-Za-z0-9]{1,10}\.[A-Za-z][A-Za-z0-9]{1,10}\.$/) + .map((path) => ({ path })) + + /** + * Generate valid criteria restricted to DeviceId fields + */ + const arbCriteria = fc.record( + { + manufacturer: fc.option(fc.string({ minLength: 1, maxLength: 15 }), { nil: undefined }), + productClass: fc.option(fc.string({ minLength: 1, maxLength: 15 }), { nil: undefined }), + }, + { requiredKeys: [] } + ) + + /** + * Generate tags (array of simple strings without whitespace) + */ + const arbTags = fc.array( + fc.string({ minLength: 1, maxLength: 15 }).map((s) => s.replace(/\s/g, 'x')), + { minLength: 0, maxLength: 5 } + ) + + /** + * Generate a valid name + */ + const arbName = fc.string({ minLength: 1, maxLength: 50 }) + + /** + * Generate a complete valid CreateConfigurationDto. + * Desired parameters are generated to be covered by the inventory path. + */ + const arbCreateConfigurationDto: fc.Arbitrary = arbInventoryPath.chain( + (inventoryPath) => { + // Generate desired parameters that are covered by the inventory path + const arbDesiredParam = fc + .stringMatching(/^[A-Za-z][A-Za-z0-9]{1,15}$/) + .map((leafName) => ({ + name: inventoryPath.path + leafName, + value: 3600 as string | number | boolean, + type: 'unsignedInt' as const, + })) + + return fc.record({ + identifier: arbIdentifier, + name: arbName, + version: arbVersion, + priority: arbPriority, + policy: arbPolicy, + enabled: fc.boolean(), + tags: arbTags, + criteria: arbCriteria, + inventory: fc.constant([inventoryPath]), + desiredParameters: fc.array(arbDesiredParam, { minLength: 0, maxLength: 3 }), + }) + } + ) + + // ─── Property Test ─────────────────────────────────────────────────────────── + + it('create + findById returns identical fields for any valid Configuration', () => { + return fc.assert( + fc.asyncProperty(arbCreateConfigurationDto, async (dto) => { + vi.clearAllMocks() + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([]) + + const now = new Date() + + // Simulate what the model would persist and return + const persisted: Configuration = { + identifier: dto.identifier, + name: dto.name, + version: dto.version, + priority: dto.priority, + policy: dto.policy as PolicyType, + enabled: dto.enabled ?? true, + tags: dto.tags ?? [], + criteria: dto.criteria, + inventory: dto.inventory, + desiredParameters: dto.desiredParameters, + firmwareTarget: dto.firmwareTarget, + schedule: dto.schedule, + createdAt: now, + updatedAt: now, + } + + // Mock createIfNotExists to return the persisted configuration + vi.mocked(ConfigurationModel.createIfNotExists).mockResolvedValue(persisted) + // Mock findByIdentifier to return the same persisted configuration + vi.mocked(ConfigurationModel.findByIdentifier).mockResolvedValue(persisted) + + // Act: create the configuration + const created = await service.create(dto) + + // Act: retrieve by identifier + const found = await service.findById(dto.identifier) + + // Assert: findById was called with the correct identifier + expect(ConfigurationModel.findByIdentifier).toHaveBeenCalledWith(dto.identifier) + + // Assert: found configuration matches created configuration on all fields + expect(found).not.toBeNull() + expect(found!.identifier).toBe(created.identifier) + expect(found!.name).toBe(created.name) + expect(found!.version).toBe(created.version) + expect(found!.priority).toBe(created.priority) + expect(found!.policy).toBe(created.policy) + expect(found!.enabled).toBe(created.enabled) + expect(found!.tags).toEqual(created.tags) + expect(found!.criteria).toEqual(created.criteria) + expect(found!.inventory).toEqual(created.inventory) + expect(found!.desiredParameters).toEqual(created.desiredParameters) + expect(found!.firmwareTarget).toEqual(created.firmwareTarget) + expect(found!.schedule).toEqual(created.schedule) + expect(found!.createdAt).toEqual(created.createdAt) + expect(found!.updatedAt).toEqual(created.updatedAt) + }), + { numRuns: 100 } + ) + }) +}) diff --git a/tests/property/configuration_delete.property.spec.ts b/tests/property/configuration_delete.property.spec.ts new file mode 100644 index 0000000..417e8e6 --- /dev/null +++ b/tests/property/configuration_delete.property.spec.ts @@ -0,0 +1,122 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import * as fc from 'fast-check' +import ConfigurationService from '../../app/services/configuration_service.js' +import ConfigurationModel from '../../app/models/configuration.js' +import ConfigurationVersionModel from '../../app/models/configuration_version.js' + +/** + * **Validates: Requirements 1.5** + * + * Property 2: Delete removes Configuration completely + * After deleting a Configuration, findById returns null and all versions are also deleted. + */ + +vi.mock('../../app/models/configuration.js', () => ({ + default: { + deleteByIdentifier: vi.fn(), + findByIdentifier: vi.fn(), + }, +})) + +vi.mock('../../app/models/configuration_version.js', () => ({ + default: { + deleteByConfigurationIdentifier: vi.fn(), + }, +})) + +// Arbitrary: generates a valid configuration identifier (alphanumeric + hyphens/underscores, starts with alphanumeric) +const identifierArb = fc + .tuple( + fc.constantFrom( + ...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.split('') + ), + fc.array( + fc.constantFrom( + ...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'.split('') + ), + { minLength: 0, maxLength: 29 } + ) + ) + .map(([first, rest]) => first + rest.join('')) + +describe('Property 2: Delete removes Configuration completely', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('delete calls deleteByIdentifier on ConfigurationModel for any identifier', async () => { + await fc.assert( + fc.asyncProperty(identifierArb, async (identifier) => { + vi.mocked(ConfigurationModel.deleteByIdentifier).mockResolvedValue(true) + vi.mocked(ConfigurationVersionModel.deleteByConfigurationIdentifier).mockResolvedValue(0) + + const service = new ConfigurationService() + const result = await service.delete(identifier) + + expect(result).toBe(true) + expect(ConfigurationModel.deleteByIdentifier).toHaveBeenCalledWith(identifier) + + vi.clearAllMocks() + }), + { numRuns: 100 } + ) + }) + + it('delete calls deleteByConfigurationIdentifier on ConfigurationVersionModel when configuration exists', async () => { + await fc.assert( + fc.asyncProperty(identifierArb, async (identifier) => { + vi.mocked(ConfigurationModel.deleteByIdentifier).mockResolvedValue(true) + vi.mocked(ConfigurationVersionModel.deleteByConfigurationIdentifier).mockResolvedValue(3) + + const service = new ConfigurationService() + await service.delete(identifier) + + expect(ConfigurationVersionModel.deleteByConfigurationIdentifier).toHaveBeenCalledWith( + identifier + ) + + vi.clearAllMocks() + }), + { numRuns: 100 } + ) + }) + + it('delete does NOT call deleteByConfigurationIdentifier when configuration does not exist', async () => { + await fc.assert( + fc.asyncProperty(identifierArb, async (identifier) => { + vi.mocked(ConfigurationModel.deleteByIdentifier).mockResolvedValue(false) + + const service = new ConfigurationService() + const result = await service.delete(identifier) + + expect(result).toBe(false) + expect( + ConfigurationVersionModel.deleteByConfigurationIdentifier + ).not.toHaveBeenCalled() + + vi.clearAllMocks() + }), + { numRuns: 100 } + ) + }) + + it('after delete returns true, findById returns null for any identifier', async () => { + await fc.assert( + fc.asyncProperty(identifierArb, async (identifier) => { + vi.mocked(ConfigurationModel.deleteByIdentifier).mockResolvedValue(true) + vi.mocked(ConfigurationVersionModel.deleteByConfigurationIdentifier).mockResolvedValue(0) + vi.mocked(ConfigurationModel.findByIdentifier).mockResolvedValue(null) + + const service = new ConfigurationService() + await service.delete(identifier) + + const found = await service.findById(identifier) + expect(found).toBeNull() + expect(ConfigurationModel.findByIdentifier).toHaveBeenCalledWith(identifier) + + vi.clearAllMocks() + }), + { numRuns: 100 } + ) + }) +}) diff --git a/tests/property/configuration_engine.property.spec.ts b/tests/property/configuration_engine.property.spec.ts new file mode 100644 index 0000000..2a4f49c --- /dev/null +++ b/tests/property/configuration_engine.property.spec.ts @@ -0,0 +1,480 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import fc from 'fast-check' +import ConfigurationEngine from '../../app/services/configuration_engine.js' +import type { Configuration, ConfigurationCriteria } from '../../app/domain/configuration/configuration.js' +import { PolicyType } from '../../app/domain/configuration/configuration.js' +import type { Device } from '../../app/domain/device/device.js' +import type { DeviceId } from '../../app/domain/device/device_id.js' +import { buildProvisioningCode } from '../../app/utils/provisioning_code.js' + +// Mock the models and services +vi.mock('#models/configuration', () => { + return { + default: { + find: vi.fn(), + findOne: vi.fn(), + }, + } +}) + +vi.mock('#models/device', () => { + return { + default: { + findOne: vi.fn(), + }, + } +}) + +vi.mock('../../app/services/firmware_catalog_service.js', () => { + return { + default: class FirmwareCatalogService { + async resolveUpgradePath() { + return [] + } + }, + } +}) + +import ConfigurationModel from '#models/configuration' +import DeviceModel from '#models/device' + +/** + * Property 6: Highest priority Configuration wins + * Validates: Requirements 3.2 + * + * Property: When multiple configurations match a device, the one with highest + * priority is selected. If no configurations match, result is compliant. + */ +describe('Property 6: Highest priority Configuration wins', () => { + let engine: ConfigurationEngine + + beforeEach(() => { + engine = new ConfigurationEngine() + vi.clearAllMocks() + }) + + // Arbitrary to generate positive priority integers + const priority = fc.integer({ min: 1, max: 1000 }) + + // Arbitrary to generate a valid device + function deviceArb(): fc.Arbitrary { + return fc.record({ + deviceId: fc.record({ + id: fc.uuid(), + manufacturer: fc.string(), + oui: fc.string(), + productClass: fc.string(), + serialNumber: fc.string(), + }) as unknown as fc.Arbitrary, + createdAt: fc.date(), + }) as unknown as fc.Arbitrary + } + + // Arbitrary to generate a valid configuration + function configurationArb(overrides?: Partial): fc.Arbitrary { + const baseArb = fc.record({ + identifier: fc.string().filter((s) => s.length > 0), + name: fc.string(), + version: fc.stringMatching(/\d+\.\d+\.\d+/), + priority: priority, + policy: fc.constantFrom(PolicyType.ONCE, PolicyType.MAINTAIN), + enabled: fc.constant(true), + tags: fc.array(fc.string()), + criteria: fc.record({ + manufacturer: fc.option(fc.string(), { nil: undefined }), + oui: fc.option(fc.string(), { nil: undefined }), + productClass: fc.option(fc.string(), { nil: undefined }), + serialNumber: fc.option(fc.string(), { nil: undefined }), + }) as fc.Arbitrary, + inventory: fc.array( + fc.record({ + path: fc.string(), + }) + ) as fc.Arbitrary, + desiredParameters: fc.array( + fc.record({ + name: fc.string(), + value: fc.oneof(fc.string(), fc.integer(), fc.boolean()), + type: fc.constantFrom('string', 'int', 'boolean'), + }) + ) as fc.Arbitrary, + firmwareTarget: fc.option( + fc.record({ + productClass: fc.string(), + targetVersion: fc.stringMatching(/\d+\.\d+\.\d+/), + }), + { nil: undefined } + ) as fc.Arbitrary, + schedule: fc.constant(undefined) as fc.Arbitrary, + createdAt: fc.date() as fc.Arbitrary, + updatedAt: fc.date() as fc.Arbitrary, + }) + + if (overrides) { + return baseArb.map((val) => ({ + ...val, + ...overrides, + })) as fc.Arbitrary + } + + return baseArb as fc.Arbitrary + } + + it('selects configuration with highest priority when multiple match', async () => { + await fc.assert( + fc.asyncProperty(deviceArb(), async (device) => { + // Generate configurations with arbitrary priorities + const numConfigs = fc.sample(fc.integer({ min: 2, max: 5 }), 1)[0] + const configsArray = fc.sample(configurationArb(), numConfigs) + + // Ensure all have matching criteria (empty) + const configurations = configsArray.map((c) => ({ + ...c, + criteria: {} as ConfigurationCriteria, + })) + + vi.mocked(ConfigurationModel.find).mockResolvedValue(configurations as any) + vi.mocked(DeviceModel.findOne).mockResolvedValue(device as any) + + const result = await engine.evaluate(device) + + // Find expected highest priority config + const highest = configurations.reduce((max, c) => + c.priority > max.priority ? c : max + ) + + expect(result.configuration?.priority).toBe(highest.priority) + expect(result.configuration?.identifier).toBe(highest.identifier) + }) + ) + }) + + it('returns compliant result when no configurations match', async () => { + await fc.assert( + fc.asyncProperty(deviceArb(), async (device) => { + vi.mocked(ConfigurationModel.find).mockResolvedValue([]) + + const result = await engine.evaluate(device) + + expect(result.configuration).toBeNull() + expect(result.isAlreadyCompliant).toBe(true) + expect(result.parameterDiffs).toEqual([]) + expect(result.firmwareMigrationPath).toEqual([]) + }) + ) + }) +}) + +/** + * Property 8: Diff engine computes only differing parameters + * Validates: Requirements 5.1, 5.2, 5.3, 6.5 + * + * Property: The diff computation returns only parameters where currentValue !== targetValue. + */ +describe('Property 8: Diff engine computes only differing parameters', () => { + let engine: ConfigurationEngine + + beforeEach(() => { + engine = new ConfigurationEngine() + vi.clearAllMocks() + }) + + it('returns empty diffs when all desired parameters match current values', async () => { + await fc.assert( + fc.asyncProperty( + fc.array( + fc.record({ + name: fc.string().filter((s) => s.length > 0), + value: fc.oneof(fc.string(), fc.integer(), fc.boolean()), + type: fc.constantFrom('string', 'int', 'boolean'), + }), + { minLength: 1, maxLength: 10 } + ).map((params) => { + // Deduplicate by name to avoid conflicting values for the same parameter + const seen = new Set() + return params.filter((p) => { + if (seen.has(p.name)) return false + seen.add(p.name) + return true + }) + }).filter((params) => params.length > 0), + async (desiredParams) => { + const device = { + deviceId: { id: 'test', manufacturer: 'Test' }, + createdAt: new Date(), + getParameter: (name: string) => ({ + value: desiredParams.find((p) => p.name === name)?.value, + }), + } as any + + const config = { + identifier: 'test', + priority: 1, + enabled: true, + policy: PolicyType.MAINTAIN, + criteria: {}, + inventory: [], + desiredParameters: desiredParams, + createdAt: new Date(), + updatedAt: new Date(), + } as any + + vi.mocked(ConfigurationModel.find).mockResolvedValue([config]) + vi.mocked(DeviceModel.findOne).mockResolvedValue(device) + + const result = await engine.evaluate(device) + + expect(result.parameterDiffs).toEqual([]) + } + ) + ) + }) + + it('returns diffs only for parameters that differ', async () => { + const desiredParams = [ + { name: 'param1', value: 'value1', type: 'string' }, + { name: 'param2', value: 'value2', type: 'string' }, + ] + + const device = { + deviceId: { id: 'test', manufacturer: 'Test' }, + createdAt: new Date(), + getParameter: (name: string) => { + if (name === 'param1') return { value: 'value1' } // Matches + if (name === 'param2') return { value: 'different' } // Differs + return { value: undefined } + }, + } as any + + const config = { + identifier: 'test', + priority: 1, + enabled: true, + policy: PolicyType.MAINTAIN, + criteria: {}, + inventory: [], + desiredParameters: desiredParams, + createdAt: new Date(), + updatedAt: new Date(), + } as any + + vi.mocked(ConfigurationModel.find).mockResolvedValue([config]) + + const engine = new ConfigurationEngine() + const result = await engine.evaluate(device) + + expect(result.parameterDiffs).toHaveLength(1) + expect(result.parameterDiffs[0].name).toBe('param2') + expect(result.parameterDiffs[0].currentValue).toBe('different') + expect(result.parameterDiffs[0].targetValue).toBe('value2') + }) +}) + +/** + * Property 10: Policy "once" applies based on content hash comparison + * Validates: Requirements 6.1, 6.3 + * + * Property: A configuration with policy "once" applies only if the device + * doesn't have a matching provisioning code with the same content hash. + */ +describe('Property 10: Policy "once" applies based on content hash comparison', () => { + let engine: ConfigurationEngine + + beforeEach(() => { + engine = new ConfigurationEngine() + vi.clearAllMocks() + }) + + it('applies "once" policy if device has no provisioning code', async () => { + const config = { + identifier: 'config1', + priority: 1, + enabled: true, + policy: PolicyType.ONCE, + criteria: {}, + inventory: [], + desiredParameters: [{ name: 'param1', value: 'value1', type: 'string' }], + createdAt: new Date(), + updatedAt: new Date(), + } as any + + const device = { + deviceId: { id: 'test', manufacturer: 'Test' }, + createdAt: new Date(), + getParameter: (name: string) => ({ + value: name === 'param1' ? 'different' : undefined, + }), + // No provisioningCode property + } as any + + vi.mocked(ConfigurationModel.find).mockResolvedValue([config]) + + const result = await engine.evaluate(device) + + expect(result.parameterDiffs).toHaveLength(1) + }) + + it('skips "once" policy if provisioning code matches content hash', async () => { + const config = { + identifier: 'config1', + priority: 1, + enabled: true, + policy: PolicyType.ONCE, + criteria: {}, + inventory: [], + desiredParameters: [{ name: 'param1', value: 'value1', type: 'string' }], + createdAt: new Date(), + updatedAt: new Date(), + } as any + + const provCode = buildProvisioningCode(config) + + const device = { + deviceId: { id: 'test', manufacturer: 'Test' }, + createdAt: new Date(), + getParameter: (name: string) => ({ + value: name === 'param1' ? 'value1' : undefined, + }), + provisioningCode: provCode, + } as any + + vi.mocked(ConfigurationModel.find).mockResolvedValue([config]) + + const result = await engine.evaluate(device) + + expect(result.isAlreadyCompliant).toBe(true) + expect(result.parameterDiffs).toEqual([]) + }) + + it('reapplies "once" policy if content hash changes', async () => { + const configV1 = { + identifier: 'config1', + priority: 1, + enabled: true, + policy: PolicyType.ONCE, + criteria: {}, + inventory: [], + desiredParameters: [{ name: 'param1', value: 'value1', type: 'string' }], + createdAt: new Date(), + updatedAt: new Date(), + } as any + + const provCodeV1 = buildProvisioningCode(configV1) + + // New config version with different content + const configV2 = { + ...configV1, + desiredParameters: [{ name: 'param1', value: 'value2', type: 'string' }], + } as any + + const device = { + deviceId: { id: 'test', manufacturer: 'Test' }, + createdAt: new Date(), + getParameter: (name: string) => ({ + value: name === 'param1' ? 'value1' : undefined, + }), + provisioningCode: provCodeV1, // Has old provisioning code + } as any + + vi.mocked(ConfigurationModel.find).mockResolvedValue([configV2]) + + const result = await engine.evaluate(device) + + // Should reapply because content hash changed + expect(result.parameterDiffs).toHaveLength(1) + expect(result.parameterDiffs[0].targetValue).toBe('value2') + }) +}) + +/** + * Property 16: Dry-run is side-effect-free + * Validates: Requirements 16.1, 16.2 + * + * Property: Calling dryRun() should not modify the device or database state. + */ +describe('Property 16: Dry-run is side-effect-free', () => { + let engine: ConfigurationEngine + + beforeEach(() => { + engine = new ConfigurationEngine() + vi.clearAllMocks() + }) + + it('dryRun returns EngineResult without modifying device', async () => { + const config = { + identifier: 'config1', + priority: 1, + enabled: true, + policy: PolicyType.MAINTAIN, + criteria: {}, + inventory: [], + desiredParameters: [{ name: 'param1', value: 'value1', type: 'string' }], + createdAt: new Date(), + updatedAt: new Date(), + } as any + + const device = { + deviceId: { id: 'test', manufacturer: 'Test' }, + createdAt: new Date(), + getParameter: (name: string) => ({ + value: name === 'param1' ? 'different' : undefined, + }), + } as any + + // Make a deep copy to check it hasn't changed + const deviceSnapshot = JSON.parse(JSON.stringify(device)) + + vi.mocked(ConfigurationModel.findOne).mockResolvedValue(config) + vi.mocked(DeviceModel.findOne).mockResolvedValue(device) + + const result = await engine.dryRun('config1', 'test') + + // Verify device snapshot hasn't changed + expect(JSON.stringify(device)).toBe(JSON.stringify(deviceSnapshot)) + + // Verify result is valid + expect(result).toHaveProperty('configurationId', 'config1') + expect(result).toHaveProperty('deviceIdentifier', 'test') + expect(result.evaluation).toHaveProperty('parameterDiffs') + }) + + it('dryRun does not persist any changes', async () => { + const config = { + identifier: 'config1', + priority: 1, + enabled: true, + policy: PolicyType.ONCE, + criteria: {}, + inventory: [], + desiredParameters: [{ name: 'param1', value: 'value1', type: 'string' }], + createdAt: new Date(), + updatedAt: new Date(), + } as any + + const device = { + deviceId: { id: 'test', manufacturer: 'Test' }, + createdAt: new Date(), + getParameter: (name: string) => ({ + value: name === 'param1' ? 'different' : undefined, + }), + } as any + + vi.mocked(ConfigurationModel.findOne).mockResolvedValue(config) + vi.mocked(DeviceModel.findOne).mockResolvedValue(device) + + const result = await engine.dryRun('config1', 'test') + + // Verify result is a DryRunResult with expected properties + expect(result).toBeDefined() + expect(result.configurationId).toBe('config1') + expect(result.deviceIdentifier).toBe('test') + + // Verify no save/update calls were made (mocks should not have been called) + // Since we're mocking, any DB mutations would show up in mock call counts + expect(vi.mocked(ConfigurationModel.findOne)).toHaveBeenCalledTimes(1) // Only read call + expect(vi.mocked(DeviceModel.findOne)).toHaveBeenCalledTimes(1) // Only read call + + // No mutation methods should have been called + // (we didn't mock save/update, so they shouldn't exist on our mocked objects) + }) +}) diff --git a/tests/property/configuration_overlap.property.spec.ts b/tests/property/configuration_overlap.property.spec.ts new file mode 100644 index 0000000..3fdb84d --- /dev/null +++ b/tests/property/configuration_overlap.property.spec.ts @@ -0,0 +1,185 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import * as fc from 'fast-check' +import ConfigurationService, { ValidationError } from '../../app/services/configuration_service.js' +import ConfigurationModel from '../../app/models/configuration.js' +import type { Configuration, ConfigurationCriteria } from '../../app/domain/configuration/configuration.js' +import { PolicyType } from '../../app/domain/configuration/configuration.js' + +/** + * **Validates: Requirements 3.4** + * + * Property 7: Same-priority overlap rejected + * Two configurations with the same priority and overlapping criteria are always rejected with 409. + */ + +vi.mock('../../app/models/configuration.js', () => ({ + default: { + findAllEnabled: vi.fn(), + }, +})) + +// Arbitrary: generates a positive integer priority +const priorityArb = fc.integer({ min: 1, max: 10000 }) + +// Arbitrary: generates a non-empty string suitable for criteria fields (no glob chars for exact overlap) +const criteriaValueArb = fc + .array( + fc.constantFrom( + ...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.split('') + ), + { minLength: 1, maxLength: 20 } + ) + .map((chars) => chars.join('')) + +// Arbitrary: generates a valid configuration identifier +const identifierArb = fc + .array( + fc.constantFrom( + ...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'.split('') + ), + { minLength: 1, maxLength: 20 } + ) + .map((chars) => { + const first = chars[0].replace(/[-_]/, 'A') + return first + chars.slice(1).join('') + }) + +// Arbitrary: generates overlapping criteria (same productClass and same manufacturer) +const overlappingCriteriaArb = fc.record({ + manufacturer: criteriaValueArb, + productClass: criteriaValueArb, +}) + +// Build a minimal Configuration object for mocking findAllEnabled results +function buildExistingConfig( + identifier: string, + priority: number, + criteria: ConfigurationCriteria +): Configuration { + return { + identifier, + name: `Config ${identifier}`, + version: '1.0.0', + priority, + policy: PolicyType.MAINTAIN, + enabled: true, + tags: [], + criteria, + inventory: [], + desiredParameters: [], + createdAt: new Date(), + updatedAt: new Date(), + } +} + +describe('Property 7: Same-priority overlap rejected', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('detectOverlap throws ValidationError with statusCode 409 when same priority and overlapping criteria', async () => { + await fc.assert( + fc.asyncProperty( + priorityArb, + overlappingCriteriaArb, + identifierArb, + async (priority, criteria, existingId) => { + vi.clearAllMocks() + // Mock findAllEnabled to return an existing config with the same priority and overlapping criteria + const existingConfig = buildExistingConfig(existingId, priority, criteria) + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([existingConfig]) + + const service = new ConfigurationService() + + // Call detectOverlap with the same criteria and same priority + try { + await service.detectOverlap(criteria, priority) + // If no error thrown, the property is violated + expect.fail('Expected ValidationError to be thrown') + } catch (error) { + expect(error).toBeInstanceOf(ValidationError) + expect((error as ValidationError).statusCode).toBe(409) + } + } + ), + { numRuns: 100 } + ) + }) + + it('detectOverlap throws 409 with conflicting identifier listed in the error message', async () => { + await fc.assert( + fc.asyncProperty( + priorityArb, + overlappingCriteriaArb, + identifierArb, + async (priority, criteria, existingId) => { + vi.clearAllMocks() + const existingConfig = buildExistingConfig(existingId, priority, criteria) + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([existingConfig]) + + const service = new ConfigurationService() + + try { + await service.detectOverlap(criteria, priority) + expect.fail('Expected ValidationError to be thrown') + } catch (error) { + expect(error).toBeInstanceOf(ValidationError) + const validationError = error as ValidationError + expect(validationError.statusCode).toBe(409) + expect(validationError.message).toContain(existingId) + } + } + ), + { numRuns: 100 } + ) + }) + + it('detectOverlap does NOT throw 409 when same criteria but different priority', async () => { + await fc.assert( + fc.asyncProperty( + priorityArb, + overlappingCriteriaArb, + identifierArb, + fc.integer({ min: 1, max: 5000 }), + async (priority, criteria, existingId, offset) => { + vi.clearAllMocks() + // Ensure different priority by adding an offset + const differentPriority = priority + offset + 1 + const existingConfig = buildExistingConfig(existingId, differentPriority, criteria) + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([existingConfig]) + + const service = new ConfigurationService() + + // Should NOT throw — returns a warning instead + const result = await service.detectOverlap(criteria, priority) + expect(result).not.toBeNull() + expect(result!.type).toBe('overlap_different_priority') + } + ), + { numRuns: 100 } + ) + }) + + it('detectOverlap excludes the configuration being updated (excludeId)', async () => { + await fc.assert( + fc.asyncProperty( + priorityArb, + overlappingCriteriaArb, + identifierArb, + async (priority, criteria, configId) => { + vi.clearAllMocks() + // The existing config has the same id as the one being updated + const existingConfig = buildExistingConfig(configId, priority, criteria) + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([existingConfig]) + + const service = new ConfigurationService() + + // Should NOT throw because we exclude the config being updated + const result = await service.detectOverlap(criteria, priority, configId) + expect(result).toBeNull() + } + ), + { numRuns: 100 } + ) + }) +}) diff --git a/tests/property/configuration_pagination.property.spec.ts b/tests/property/configuration_pagination.property.spec.ts new file mode 100644 index 0000000..f3d507a --- /dev/null +++ b/tests/property/configuration_pagination.property.spec.ts @@ -0,0 +1,206 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import fc from 'fast-check' + +/** + * Property 18: Pagination invariants + * Validates: Requirements 11.3 + * + * Verifies that: + * 1. Page number is always >= 1 (defaults to 1 if not provided) + * 2. PerPage is always >= 1 (defaults to 25 if not provided) + * 3. The correct page and perPage values are passed to the model's paginateConfigurations method + */ + +// Mock the ConfigurationModel +const mockPaginateConfigurations = vi.fn() + +vi.mock('#models/configuration', () => ({ + default: { + paginateConfigurations: (...args: any[]) => mockPaginateConfigurations(...args), + findAllEnabled: vi.fn().mockResolvedValue([]), + }, +})) + +vi.mock('#models/configuration_version', () => ({ + default: { + getLatestVersionNumber: vi.fn().mockResolvedValue(0), + create: vi.fn().mockResolvedValue({}), + deleteByConfigurationIdentifier: vi.fn().mockResolvedValue(0), + paginateVersions: vi.fn().mockResolvedValue({ data: [], total: 0, page: 1, perPage: 25, lastPage: 1 }), + }, +})) + +// Import the service once after mocks are set up +import ConfigurationService from '../../app/services/configuration_service.js' + +describe('Property 18: Pagination invariants', () => { + beforeEach(() => { + mockPaginateConfigurations.mockReset() + mockPaginateConfigurations.mockResolvedValue({ + data: [], + total: 0, + page: 1, + perPage: 25, + lastPage: 1, + }) + }) + + it('page defaults to 1 when not provided', async () => { + const service = new ConfigurationService() + + await service.findAll({}) + + expect(mockPaginateConfigurations).toHaveBeenCalledWith(1, 25, {}) + }) + + it('perPage defaults to 25 when not provided', async () => { + const service = new ConfigurationService() + + await service.findAll({ page: 3 }) + + expect(mockPaginateConfigurations).toHaveBeenCalledWith(3, 25, {}) + }) + + it('page passed to paginateConfigurations is always >= 1 for any positive integer input', async () => { + await fc.assert( + fc.asyncProperty( + fc.integer({ min: 1, max: 10000 }), + fc.integer({ min: 1, max: 1000 }), + async (page, perPage) => { + mockPaginateConfigurations.mockClear() + mockPaginateConfigurations.mockResolvedValue({ + data: [], + total: 0, + page, + perPage, + lastPage: 1, + }) + + const service = new ConfigurationService() + await service.findAll({ page, perPage }) + + expect(mockPaginateConfigurations).toHaveBeenCalledTimes(1) + const [calledPage] = mockPaginateConfigurations.mock.calls[0] + expect(calledPage).toBeGreaterThanOrEqual(1) + } + ) + ) + }) + + it('perPage passed to paginateConfigurations is always >= 1 for any positive integer input', async () => { + await fc.assert( + fc.asyncProperty( + fc.integer({ min: 1, max: 10000 }), + fc.integer({ min: 1, max: 1000 }), + async (page, perPage) => { + mockPaginateConfigurations.mockClear() + mockPaginateConfigurations.mockResolvedValue({ + data: [], + total: 0, + page, + perPage, + lastPage: 1, + }) + + const service = new ConfigurationService() + await service.findAll({ page, perPage }) + + expect(mockPaginateConfigurations).toHaveBeenCalledTimes(1) + const [, calledPerPage] = mockPaginateConfigurations.mock.calls[0] + expect(calledPerPage).toBeGreaterThanOrEqual(1) + } + ) + ) + }) + + it('correct page and perPage values are passed to paginateConfigurations for random inputs', async () => { + await fc.assert( + fc.asyncProperty( + fc.integer({ min: 1, max: 10000 }), + fc.integer({ min: 1, max: 1000 }), + async (page, perPage) => { + mockPaginateConfigurations.mockClear() + mockPaginateConfigurations.mockResolvedValue({ + data: [], + total: 0, + page, + perPage, + lastPage: 1, + }) + + const service = new ConfigurationService() + await service.findAll({ page, perPage }) + + expect(mockPaginateConfigurations).toHaveBeenCalledTimes(1) + const [calledPage, calledPerPage, calledFilter] = + mockPaginateConfigurations.mock.calls[0] + expect(calledPage).toBe(page) + expect(calledPerPage).toBe(perPage) + expect(calledFilter).toEqual({}) + } + ) + ) + }) + + it('page defaults to 1 and perPage defaults to 25 when both are undefined', async () => { + const service = new ConfigurationService() + await service.findAll({}) + + expect(mockPaginateConfigurations).toHaveBeenCalledTimes(1) + const [calledPage, calledPerPage] = mockPaginateConfigurations.mock.calls[0] + expect(calledPage).toBe(1) + expect(calledPerPage).toBe(25) + }) + + it('filters are correctly passed alongside pagination parameters', async () => { + await fc.assert( + fc.asyncProperty( + fc.integer({ min: 1, max: 100 }), + fc.integer({ min: 1, max: 100 }), + fc.option(fc.string({ minLength: 1, maxLength: 20 }).map((s) => s.replace(/\s/g, 'x')), { nil: undefined }), + fc.option(fc.boolean(), { nil: undefined }), + fc.option(fc.string({ minLength: 1, maxLength: 20 }).map((s) => s.replace(/\s/g, 'x')), { nil: undefined }), + async (page, perPage, tag, enabled, productClass) => { + mockPaginateConfigurations.mockClear() + mockPaginateConfigurations.mockResolvedValue({ + data: [], + total: 0, + page, + perPage, + lastPage: 1, + }) + + const service = new ConfigurationService() + await service.findAll({ page, perPage, tag, enabled, productClass }) + + expect(mockPaginateConfigurations).toHaveBeenCalledTimes(1) + const [calledPage, calledPerPage, calledFilter] = + mockPaginateConfigurations.mock.calls[0] + + // Pagination params are passed correctly + expect(calledPage).toBe(page) + expect(calledPerPage).toBe(perPage) + + // Filters are built correctly + if (tag) { + expect(calledFilter.tags).toBe(tag) + } else { + expect(calledFilter.tags).toBeUndefined() + } + + if (enabled !== undefined) { + expect(calledFilter.enabled).toBe(enabled) + } else { + expect(calledFilter.enabled).toBeUndefined() + } + + if (productClass) { + expect(calledFilter['criteria.productClass']).toBe(productClass) + } else { + expect(calledFilter['criteria.productClass']).toBeUndefined() + } + } + ) + ) + }) +}) diff --git a/tests/property/configuration_partial_update.property.spec.ts b/tests/property/configuration_partial_update.property.spec.ts new file mode 100644 index 0000000..8931f9f --- /dev/null +++ b/tests/property/configuration_partial_update.property.spec.ts @@ -0,0 +1,253 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import fc from 'fast-check' +import { PolicyType } from '../../app/domain/configuration/configuration.js' +import type { Configuration } from '../../app/domain/configuration/configuration.js' + +/** + * Property 3: Partial updates preserve unmodified fields + * Validates: Requirements 1.6, 10.3 + * + * When updating only a subset of fields (toggle enabled, add/remove tags), + * all other fields remain unchanged. Verified by inspecting the updateOne + * call payload to ensure only the targeted field is included. + */ + +// Mock ConfigurationModel and ConfigurationVersionModel +vi.mock('../../app/models/configuration.js', () => { + return { + default: { + findByIdentifier: vi.fn(), + updateOne: vi.fn(), + findAllEnabled: vi.fn(), + }, + } +}) + +vi.mock('../../app/models/configuration_version.js', () => { + return { + default: { + getLatestVersionNumber: vi.fn(), + create: vi.fn(), + }, + } +}) + +import ConfigurationModel from '../../app/models/configuration.js' +import ConfigurationService from '../../app/services/configuration_service.js' + +describe('Property 3: Partial updates preserve unmodified fields', () => { + let service: ConfigurationService + + beforeEach(() => { + vi.clearAllMocks() + service = new ConfigurationService() + }) + + /** + * Arbitrary for generating a valid Configuration object. + */ + const configurationArb: fc.Arbitrary = fc + .record({ + identifier: fc + .string({ minLength: 1, maxLength: 20 }) + .map((s) => s.replace(/[^A-Za-z0-9_-]/g, 'x') || 'cfg'), + name: fc.string({ minLength: 1, maxLength: 50 }), + version: fc + .tuple(fc.nat({ max: 99 }), fc.nat({ max: 99 }), fc.nat({ max: 99 })) + .map(([a, b, c]) => `${a}.${b}.${c}`), + priority: fc.integer({ min: 1, max: 1000 }), + policy: fc.constantFrom(PolicyType.ONCE, PolicyType.MAINTAIN), + enabled: fc.boolean(), + tags: fc.array( + fc.string({ minLength: 1, maxLength: 15 }).map((s) => s.replace(/\s/g, 'a')), + { minLength: 0, maxLength: 5 } + ), + criteria: fc.record({ + manufacturer: fc.option(fc.string({ minLength: 1, maxLength: 20 }), { nil: undefined }), + oui: fc.option(fc.string({ minLength: 1, maxLength: 10 }), { nil: undefined }), + productClass: fc.option(fc.string({ minLength: 1, maxLength: 20 }), { nil: undefined }), + serialNumber: fc.option(fc.string({ minLength: 1, maxLength: 20 }), { nil: undefined }), + }), + inventory: fc.array( + fc.record({ + path: fc + .array(fc.string({ minLength: 1, maxLength: 10 }).map((s) => s.replace(/\./g, 'x')), { + minLength: 1, + maxLength: 4, + }) + .map((parts) => parts.join('.') + '.'), + }), + { minLength: 0, maxLength: 3 } + ), + desiredParameters: fc.constant([]), + firmwareTarget: fc.option( + fc.record({ + productClass: fc.string({ minLength: 1, maxLength: 20 }), + targetVersion: fc.string({ minLength: 1, maxLength: 10 }), + }), + { nil: undefined } + ), + schedule: fc.option( + fc.record({ + daysOfWeek: fc.array(fc.integer({ min: 0, max: 6 }), { minLength: 1, maxLength: 7 }), + startHour: fc.integer({ min: 0, max: 23 }), + endHour: fc.integer({ min: 0, max: 23 }), + timezone: fc.constantFrom('Europe/Paris', 'America/New_York', 'UTC'), + }), + { nil: undefined } + ), + createdAt: fc.date(), + updatedAt: fc.date(), + }) + .map((rec) => rec as unknown as Configuration) + + it('toggleEnabled only changes the enabled field, all other fields remain unchanged', async () => { + await fc.assert( + fc.asyncProperty(configurationArb, fc.boolean(), async (existingConfig, newEnabled) => { + vi.clearAllMocks() + + // Track what updateOne receives + let capturedUpdate: Record | null = null + + vi.mocked(ConfigurationModel.findByIdentifier) + .mockResolvedValueOnce(existingConfig as any) // first call: check existence + .mockResolvedValueOnce({ ...existingConfig, enabled: newEnabled } as any) // second call: return updated + + vi.mocked(ConfigurationModel.updateOne).mockImplementation(async (_filter, update) => { + capturedUpdate = update as Record + return { acknowledged: true, modifiedCount: 1 } + }) + + await service.toggleEnabled(existingConfig.identifier, newEnabled) + + // The updateOne call should only contain the enabled field + expect(capturedUpdate).not.toBeNull() + expect(capturedUpdate!.enabled).toBe(newEnabled) + + // Verify no other configuration fields are in the update payload + const updateKeys = Object.keys(capturedUpdate!) + const forbiddenKeys = [ + 'identifier', + 'name', + 'version', + 'priority', + 'policy', + 'tags', + 'criteria', + 'inventory', + 'desiredParameters', + 'firmwareTarget', + 'schedule', + 'createdAt', + ] + for (const key of forbiddenKeys) { + expect(updateKeys).not.toContain(key) + } + }), + { numRuns: 100 } + ) + }) + + it('updateTags only changes the tags field, all other fields remain unchanged', async () => { + await fc.assert( + fc.asyncProperty( + configurationArb, + fc.array(fc.string({ minLength: 1, maxLength: 10 }).map((s) => s.replace(/\s/g, 'a')), { + minLength: 0, + maxLength: 3, + }), + fc.array(fc.string({ minLength: 1, maxLength: 10 }).map((s) => s.replace(/\s/g, 'a')), { + minLength: 0, + maxLength: 3, + }), + async (existingConfig, tagsToAdd, tagsToRemove) => { + vi.clearAllMocks() + + let capturedUpdate: Record | null = null + + vi.mocked(ConfigurationModel.findByIdentifier) + .mockResolvedValueOnce(existingConfig as any) // first call: check existence + .mockResolvedValueOnce(existingConfig as any) // second call: return after update + + vi.mocked(ConfigurationModel.updateOne).mockImplementation(async (_filter, update) => { + capturedUpdate = update as Record + return { acknowledged: true, modifiedCount: 1 } + }) + + await service.updateTags(existingConfig.identifier, tagsToAdd, tagsToRemove) + + // The updateOne call should only contain the tags field + expect(capturedUpdate).not.toBeNull() + expect(capturedUpdate!).toHaveProperty('tags') + + // Verify no other configuration fields are in the update payload + const updateKeys = Object.keys(capturedUpdate!) + const forbiddenKeys = [ + 'identifier', + 'name', + 'version', + 'priority', + 'policy', + 'enabled', + 'criteria', + 'inventory', + 'desiredParameters', + 'firmwareTarget', + 'schedule', + 'createdAt', + ] + for (const key of forbiddenKeys) { + expect(updateKeys).not.toContain(key) + } + } + ), + { numRuns: 100 } + ) + }) + + it('updateTags preserves existing tags not targeted by remove operations', async () => { + await fc.assert( + fc.asyncProperty( + configurationArb.map((config) => ({ + ...config, + tags: ['existing-tag-1', 'existing-tag-2', 'existing-tag-3'], + })), + fc.array( + fc.string({ minLength: 1, maxLength: 10 }).map((s) => 'new-' + s.replace(/\s/g, '')), + { minLength: 1, maxLength: 2 } + ), + async (existingConfig, tagsToAdd) => { + vi.clearAllMocks() + + let capturedUpdate: Record | null = null + + vi.mocked(ConfigurationModel.findByIdentifier) + .mockResolvedValueOnce(existingConfig as any) + .mockResolvedValueOnce(existingConfig as any) + + vi.mocked(ConfigurationModel.updateOne).mockImplementation(async (_filter, update) => { + capturedUpdate = update as Record + return { acknowledged: true, modifiedCount: 1 } + }) + + // Only add tags, don't remove any + await service.updateTags(existingConfig.identifier, tagsToAdd, []) + + expect(capturedUpdate).not.toBeNull() + const updatedTags = capturedUpdate!.tags as string[] + + // All original tags should still be present + for (const originalTag of existingConfig.tags) { + expect(updatedTags).toContain(originalTag) + } + + // All added tags should be present + for (const addedTag of tagsToAdd) { + expect(updatedTags).toContain(addedTag) + } + } + ), + { numRuns: 100 } + ) + }) +}) diff --git a/tests/property/configuration_tag_filtering.property.spec.ts b/tests/property/configuration_tag_filtering.property.spec.ts new file mode 100644 index 0000000..b3e6c07 --- /dev/null +++ b/tests/property/configuration_tag_filtering.property.spec.ts @@ -0,0 +1,110 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import * as fc from 'fast-check' +import ConfigurationService from '../../app/services/configuration_service.js' +import ConfigurationModel from '../../app/models/configuration.js' + +/** + * **Validates: Requirements 10.2** + * + * Property 17: Tag filtering correctness + * When filtering by tag, only configurations containing that tag are returned. + * Verifies that findAll({ tag: someTag }) passes the correct MongoDB filter + * `{ tags: someTag }` to paginateConfigurations. + */ + +vi.mock('../../app/models/configuration.js', () => ({ + default: { + paginateConfigurations: vi.fn(), + }, +})) + +/** + * Arbitrary: generates a non-empty tag string (alphanumeric, hyphens, underscores). + * Tags are typically short labels used for organization. + */ +const tagArb = fc + .array( + fc.constantFrom( + ...'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_'.split('') + ), + { minLength: 1, maxLength: 30 } + ) + .map((chars) => chars.join('')) + +describe('Property 17: Tag filtering correctness', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('findAll with a tag filter passes { tags: tag } to paginateConfigurations', async () => { + await fc.assert( + fc.asyncProperty(tagArb, async (tag) => { + vi.mocked(ConfigurationModel.paginateConfigurations).mockReset() + vi.mocked(ConfigurationModel.paginateConfigurations).mockResolvedValue({ + data: [], + total: 0, + page: 1, + perPage: 25, + lastPage: 1, + }) + + const service = new ConfigurationService() + await service.findAll({ tag }) + + expect(ConfigurationModel.paginateConfigurations).toHaveBeenCalledWith( + 1, + 25, + expect.objectContaining({ tags: tag }) + ) + }), + { numRuns: 100 } + ) + }) + + it('findAll without a tag filter does not include tags in the MongoDB filter', async () => { + await fc.assert( + fc.asyncProperty(fc.integer({ min: 1, max: 100 }), async (page) => { + vi.mocked(ConfigurationModel.paginateConfigurations).mockReset() + vi.mocked(ConfigurationModel.paginateConfigurations).mockResolvedValue({ + data: [], + total: 0, + page, + perPage: 25, + lastPage: 1, + }) + + const service = new ConfigurationService() + await service.findAll({ page }) + + const calledFilter = vi.mocked(ConfigurationModel.paginateConfigurations).mock.calls[0][2] + expect(calledFilter).not.toHaveProperty('tags') + }), + { numRuns: 100 } + ) + }) + + it('findAll with tag filter combined with other filters includes all filters correctly', async () => { + await fc.assert( + fc.asyncProperty(tagArb, fc.boolean(), async (tag, enabled) => { + vi.mocked(ConfigurationModel.paginateConfigurations).mockReset() + vi.mocked(ConfigurationModel.paginateConfigurations).mockResolvedValue({ + data: [], + total: 0, + page: 1, + perPage: 25, + lastPage: 1, + }) + + const service = new ConfigurationService() + await service.findAll({ tag, enabled }) + + expect(ConfigurationModel.paginateConfigurations).toHaveBeenCalledWith( + 1, + 25, + expect.objectContaining({ tags: tag, enabled }) + ) + }), + { numRuns: 100 } + ) + }) +}) diff --git a/tests/property/configuration_version_history.property.spec.ts b/tests/property/configuration_version_history.property.spec.ts new file mode 100644 index 0000000..67bf876 --- /dev/null +++ b/tests/property/configuration_version_history.property.spec.ts @@ -0,0 +1,297 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import * as fc from 'fast-check' +import { PolicyType } from '../../app/domain/configuration/configuration.js' +import type { + Configuration, + ConfigurationCriteria, + DesiredParameter, + InventoryPath, +} from '../../app/domain/configuration/configuration.js' +import type { ConfigurationVersion } from '../../app/domain/configuration/configuration_version.js' + +/** + * **Validates: Requirements 15.1, 15.2, 15.5** + * + * Property 15: Version history invariants + * + * Properties verified: + * 1. Each update creates exactly one new version snapshot + * 2. Version numbers are strictly increasing (monotonic) + * 3. The snapshot contains the state BEFORE the modification + * 4. Deleting a configuration removes all its versions + */ + +// --- Mutable container that won't be affected by hoisting --- +const store = { + configurations: new Map(), + versions: [] as ConfigurationVersion[], +} + +// --- Mock setup --- + +vi.mock('../../app/models/configuration.js', () => ({ + default: { + findByIdentifier: vi.fn((identifier: string) => { + const found = store.configurations.get(identifier) + return Promise.resolve(found ? { ...found } : null) + }), + findAllEnabled: vi.fn(() => { + return Promise.resolve( + [...store.configurations.values()].filter((c) => c.enabled) + ) + }), + createIfNotExists: vi.fn((data: Partial) => { + if (store.configurations.has(data.identifier!)) { + return Promise.reject( + new Error(`Configuration with identifier '${data.identifier}' already exists`) + ) + } + const now = new Date() + const config = { ...data, createdAt: now, updatedAt: now } as Configuration + store.configurations.set(data.identifier!, config) + return Promise.resolve(config) + }), + updateOne: vi.fn((filter: { identifier: string }, update: Partial) => { + const existing = store.configurations.get(filter.identifier) + if (existing) { + const updated = { ...existing, ...update, updatedAt: new Date() } + store.configurations.set(filter.identifier, updated) + } + return Promise.resolve() + }), + deleteByIdentifier: vi.fn((identifier: string) => { + return Promise.resolve(store.configurations.delete(identifier)) + }), + }, +})) + +vi.mock('../../app/models/configuration_version.js', () => ({ + default: { + create: vi.fn((data: Omit) => { + const version = { ...data } as ConfigurationVersion + store.versions.push(version) + return Promise.resolve(version) + }), + getLatestVersionNumber: vi.fn((configurationIdentifier: string) => { + const versions = store.versions.filter( + (v) => v.configurationIdentifier === configurationIdentifier + ) + if (versions.length === 0) return Promise.resolve(0) + return Promise.resolve(Math.max(...versions.map((v) => v.versionNumber))) + }), + findByConfigurationIdentifier: vi.fn((configurationIdentifier: string) => { + return Promise.resolve( + store.versions + .filter((v) => v.configurationIdentifier === configurationIdentifier) + .sort((a, b) => b.versionNumber - a.versionNumber) + ) + }), + deleteByConfigurationIdentifier: vi.fn((configurationIdentifier: string) => { + const before = store.versions.length + store.versions = store.versions.filter( + (v) => v.configurationIdentifier !== configurationIdentifier + ) + return Promise.resolve(before - store.versions.length) + }), + paginateVersions: vi.fn( + (configurationIdentifier: string, page: number, perPage: number) => { + const all = store.versions + .filter((v) => v.configurationIdentifier === configurationIdentifier) + .sort((a, b) => b.versionNumber - a.versionNumber) + const total = all.length + const start = (page - 1) * perPage + const data = all.slice(start, start + perPage) + return Promise.resolve({ + data, + total, + page, + perPage, + lastPage: Math.max(1, Math.ceil(total / perPage)), + }) + } + ), + }, +})) + +// Import the service AFTER mocking +import ConfigurationService from '../../app/services/configuration_service.js' + +// --- Arbitraries --- + +/** + * Generates a valid Configuration with inventory paths that cover the desired parameters. + * Configurations are disabled to avoid overlap detection triggering on findAllEnabled. + */ +function validConfigurationArb(): fc.Arbitrary { + return fc + .record({ + identifier: fc.stringMatching(/^[A-Za-z][A-Za-z0-9]{2,10}$/), + name: fc.string({ minLength: 1, maxLength: 30 }), + version: fc.stringMatching(/^[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}$/), + priority: fc.integer({ min: 1, max: 1000 }), + policy: fc.constantFrom(PolicyType.ONCE, PolicyType.MAINTAIN), + enabled: fc.constant(false), // disabled to avoid overlap detection issues + tags: fc.array(fc.string({ minLength: 1, maxLength: 10 }), { maxLength: 3 }), + criteria: fc.constant({} as ConfigurationCriteria), + createdAt: fc.constant(new Date('2024-01-01')), + updatedAt: fc.constant(new Date('2024-01-01')), + }) + .map((base) => ({ + ...base, + inventory: [{ path: 'Device.Config.' }] as InventoryPath[], + desiredParameters: [] as DesiredParameter[], + firmwareTarget: undefined, + schedule: undefined, + })) as fc.Arbitrary +} + +function resetStore() { + store.configurations = new Map() + store.versions = [] +} + +describe('Property 15: Version history invariants', () => { + let service: ConfigurationService + + beforeEach(() => { + resetStore() + service = new ConfigurationService() + }) + + it('each update creates exactly one new version snapshot', async () => { + await fc.assert( + fc.asyncProperty(validConfigurationArb(), async (config) => { + // Reset stores for each property run + resetStore() + + // Seed the configuration in the store + store.configurations.set(config.identifier, { ...config }) + + const versionCountBefore = store.versions.length + + // Perform update + await service.update(config.identifier, { name: 'updated-name' }) + + const versionCountAfter = store.versions.length + + // Exactly one version snapshot should have been created + expect(versionCountAfter - versionCountBefore).toBe(1) + }), + { numRuns: 100 } + ) + }) + + it('version numbers are strictly increasing (monotonic) across multiple updates', async () => { + await fc.assert( + fc.asyncProperty( + validConfigurationArb(), + fc.integer({ min: 2, max: 5 }), + async (config, updateCount) => { + // Reset stores + resetStore() + + // Seed the configuration + store.configurations.set(config.identifier, { ...config }) + + // Perform multiple updates + for (let i = 0; i < updateCount; i++) { + await service.update(config.identifier, { + name: `${config.name}-v${i + 1}`, + }) + } + + // Get all versions for this configuration + const versions = store.versions + .filter((v) => v.configurationIdentifier === config.identifier) + .sort((a, b) => a.versionNumber - b.versionNumber) + + // Verify strict monotonic increase + expect(versions.length).toBe(updateCount) + for (let i = 1; i < versions.length; i++) { + expect(versions[i].versionNumber).toBeGreaterThan(versions[i - 1].versionNumber) + } + + // Verify version numbers start at 1 and increment by 1 + for (let i = 0; i < versions.length; i++) { + expect(versions[i].versionNumber).toBe(i + 1) + } + } + ), + { numRuns: 50 } + ) + }) + + it('the snapshot contains the state BEFORE the modification', async () => { + await fc.assert( + fc.asyncProperty( + validConfigurationArb(), + fc.string({ minLength: 1, maxLength: 20 }), + async (config, newName) => { + // Reset stores + resetStore() + + // Seed the configuration with a known original name + const originalName = config.name + store.configurations.set(config.identifier, { ...config }) + + // Perform update with a new name + await service.update(config.identifier, { name: newName }) + + // The snapshot should contain the ORIGINAL name (state before modification) + const createdVersion = store.versions.find( + (v) => v.configurationIdentifier === config.identifier + ) + expect(createdVersion).toBeDefined() + expect(createdVersion!.snapshot.name).toBe(originalName) + + // The current configuration should have the NEW name + const current = store.configurations.get(config.identifier) + expect(current!.name).toBe(newName) + } + ), + { numRuns: 100 } + ) + }) + + it('deleting a configuration removes all its versions', async () => { + await fc.assert( + fc.asyncProperty( + validConfigurationArb(), + fc.integer({ min: 1, max: 5 }), + async (config, updateCount) => { + // Reset stores + resetStore() + + // Seed the configuration + store.configurations.set(config.identifier, { ...config }) + + // Perform multiple updates to create versions + for (let i = 0; i < updateCount; i++) { + await service.update(config.identifier, { + name: `${config.name}-v${i + 1}`, + }) + } + + // Verify versions exist + const versionsBeforeDelete = store.versions.filter( + (v) => v.configurationIdentifier === config.identifier + ) + expect(versionsBeforeDelete.length).toBe(updateCount) + + // Delete the configuration + await service.delete(config.identifier) + + // Verify all versions are removed + const versionsAfterDelete = store.versions.filter( + (v) => v.configurationIdentifier === config.identifier + ) + expect(versionsAfterDelete.length).toBe(0) + + // Verify the configuration itself is removed + expect(store.configurations.has(config.identifier)).toBe(false) + } + ), + { numRuns: 50 } + ) + }) +}) diff --git a/tests/property/firmware_migration_path.property.spec.ts b/tests/property/firmware_migration_path.property.spec.ts new file mode 100644 index 0000000..37f9a2e --- /dev/null +++ b/tests/property/firmware_migration_path.property.spec.ts @@ -0,0 +1,259 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import fc from 'fast-check' +import { MigrationRoute } from '../../app/domain/firmware/firmware_catalog.js' + +/** + * Property 13: Firmware migration path resolution + * Validates: Requirements 7.3 + * + * Verifies that: + * 1. If currentVersion === targetVersion, result is always empty array + * 2. If a direct route exists from current to target, path has exactly 1 step + * 3. Path steps are contiguous: step[i].toVersion === step[i+1].fromVersion + * 4. No version appears twice in the path (no cycles) + * 5. First step starts from currentVersion, last step ends at targetVersion (when path exists) + */ + +vi.mock('../../app/models/migration_route.js', () => ({ + default: { + findByProductClass: vi.fn(), + }, +})) + +vi.mock('../../app/models/firmware_entry.js', () => ({ + default: { + findByProductClassAndVersion: vi.fn(), + }, +})) + +import MigrationRouteModel from '../../app/models/migration_route.js' +import FirmwareEntryModel from '../../app/models/firmware_entry.js' +import FirmwareCatalogService from '../../app/services/firmware_catalog_service.js' + +describe('Property 13: Firmware migration path resolution', () => { + let service: FirmwareCatalogService + + beforeEach(() => { + service = new FirmwareCatalogService() + vi.mocked(MigrationRouteModel.findByProductClass).mockReset() + vi.mocked(FirmwareEntryModel.findByProductClassAndVersion).mockReset() + vi.mocked(FirmwareEntryModel.findByProductClassAndVersion).mockResolvedValue(null) + }) + + /** + * Arbitrary for version strings (e.g., "1.0.0", "2.3.1") + */ + const versionArb = fc + .tuple( + fc.integer({ min: 0, max: 99 }), + fc.integer({ min: 0, max: 99 }), + fc.integer({ min: 0, max: 99 }) + ) + .map(([major, minor, patch]) => `${major}.${minor}.${patch}`) + + /** + * Arbitrary for productClass strings + */ + const productClassArb = fc.stringMatching(/^[A-Za-z][A-Za-z0-9]{2,10}$/) + + /** + * Helper to create a MigrationRoute object + */ + function makeRoute( + productClass: string, + sourceVersionPattern: string, + targetVersion: string, + priority: number = 10 + ): MigrationRoute { + return { + identifier: `${productClass}-${sourceVersionPattern}-to-${targetVersion}`, + productClass, + sourceVersionPattern, + targetVersion, + downloadUrl: `http://firmware.local/${productClass}/${targetVersion}.bin`, + priority, + createdAt: new Date(), + } + } + + /** + * Helper to set up mocks for a given set of routes + */ + function setupMocks(routes: MigrationRoute[]) { + vi.mocked(MigrationRouteModel.findByProductClass).mockImplementation( + async (pc: string) => routes.filter((r) => r.productClass === pc) + ) + } + + it('1. If currentVersion === targetVersion, result is always empty array', async () => { + await fc.assert( + fc.asyncProperty(productClassArb, versionArb, async (productClass, version) => { + setupMocks([]) + const result = await service.resolveUpgradePath(productClass, version, version) + expect(result).toEqual([]) + }) + ) + }) + + it('2. If a direct route exists from current to target, path has exactly 1 step', async () => { + await fc.assert( + fc.asyncProperty( + productClassArb, + versionArb, + versionArb, + fc.integer({ min: 1, max: 100 }), + async (productClass, currentVersion, targetVersion, priority) => { + fc.pre(currentVersion !== targetVersion) + + const route = makeRoute(productClass, currentVersion, targetVersion, priority) + setupMocks([route]) + + const result = await service.resolveUpgradePath( + productClass, + currentVersion, + targetVersion + ) + expect(result).toHaveLength(1) + expect(result[0].fromVersion).toBe(currentVersion) + expect(result[0].toVersion).toBe(targetVersion) + } + ) + ) + }) + + it('3. Path steps are contiguous: step[i].toVersion === step[i+1].fromVersion', async () => { + /** + * Generate a chain of distinct versions and create routes forming a linear path. + */ + const distinctVersionsArb = fc + .array(versionArb, { minLength: 3, maxLength: 6 }) + .map((versions) => [...new Set(versions)]) + .filter((unique) => unique.length >= 3) + + await fc.assert( + fc.asyncProperty(productClassArb, distinctVersionsArb, async (productClass, versionList) => { + // Create a chain of routes: v0→v1, v1→v2, ..., v(n-2)→v(n-1) + const routes: MigrationRoute[] = [] + for (let i = 0; i < versionList.length - 1; i++) { + routes.push(makeRoute(productClass, versionList[i], versionList[i + 1], 10)) + } + + setupMocks(routes) + + const currentVersion = versionList[0] + const targetVersion = versionList[versionList.length - 1] + + const result = await service.resolveUpgradePath( + productClass, + currentVersion, + targetVersion + ) + + // Path should be found and contiguous + expect(result.length).toBeGreaterThan(0) + for (let i = 0; i < result.length - 1; i++) { + expect(result[i].toVersion).toBe(result[i + 1].fromVersion) + } + }) + ) + }) + + it('4. No version appears twice in the path (no cycles)', async () => { + const distinctVersionsArb = fc + .array(versionArb, { minLength: 2, maxLength: 6 }) + .map((versions) => [...new Set(versions)]) + .filter((unique) => unique.length >= 2) + + await fc.assert( + fc.asyncProperty(productClassArb, distinctVersionsArb, async (productClass, versionList) => { + // Create routes forming a chain + const routes: MigrationRoute[] = [] + for (let i = 0; i < versionList.length - 1; i++) { + routes.push(makeRoute(productClass, versionList[i], versionList[i + 1], 10)) + } + + setupMocks(routes) + + const currentVersion = versionList[0] + const targetVersion = versionList[versionList.length - 1] + + const result = await service.resolveUpgradePath( + productClass, + currentVersion, + targetVersion + ) + + // Collect all versions in the path + if (result.length > 0) { + const allVersions: string[] = [result[0].fromVersion] + for (const step of result) { + allVersions.push(step.toVersion) + } + // No duplicates + const uniqueVersions = new Set(allVersions) + expect(uniqueVersions.size).toBe(allVersions.length) + } + }) + ) + }) + + it('5. First step starts from currentVersion, last step ends at targetVersion (when path exists)', async () => { + const distinctVersionsArb = fc + .array(versionArb, { minLength: 2, maxLength: 6 }) + .map((versions) => [...new Set(versions)]) + .filter((unique) => unique.length >= 2) + + await fc.assert( + fc.asyncProperty(productClassArb, distinctVersionsArb, async (productClass, versionList) => { + // Create routes forming a chain + const routes: MigrationRoute[] = [] + for (let i = 0; i < versionList.length - 1; i++) { + routes.push(makeRoute(productClass, versionList[i], versionList[i + 1], 10)) + } + + setupMocks(routes) + + const currentVersion = versionList[0] + const targetVersion = versionList[versionList.length - 1] + + const result = await service.resolveUpgradePath( + productClass, + currentVersion, + targetVersion + ) + + if (result.length > 0) { + expect(result[0].fromVersion).toBe(currentVersion) + expect(result[result.length - 1].toVersion).toBe(targetVersion) + } + }) + ) + }) + + it('cycle detection: routes forming a cycle return empty path', async () => { + await fc.assert( + fc.asyncProperty( + productClassArb, + versionArb, + versionArb, + versionArb, + async (productClass, v1, v2, target) => { + // Ensure all three are distinct + fc.pre(v1 !== v2 && v2 !== target && v1 !== target) + + // Create a cycle: v1→v2, v2→v1 (cycle), no route to target + const routes: MigrationRoute[] = [ + makeRoute(productClass, v1, v2, 10), + makeRoute(productClass, v2, v1, 10), + ] + + setupMocks(routes) + + const result = await service.resolveUpgradePath(productClass, v1, target) + // Should return empty (either no path to target, or cycle detected) + expect(result).toEqual([]) + } + ) + ) + }) +}) diff --git a/tests/property/glob_match.property.spec.ts b/tests/property/glob_match.property.spec.ts new file mode 100644 index 0000000..f47b42b --- /dev/null +++ b/tests/property/glob_match.property.spec.ts @@ -0,0 +1,131 @@ +import { describe, it, expect } from 'vitest' +import fc from 'fast-check' +import { matchGlob } from '../../app/utils/glob_match.js' + +/** + * Property 5: Glob matching correctness + * Validates: Requirements 2.3, 2.4 + * + * Verifies that: + * - `*` matches any sequence of characters + * - `?` matches exactly one character + * - Exact strings always match themselves (when no glob chars present) + * - Matching is case-sensitive + * - Prefix glob patterns work correctly + * - A pattern of N `?` characters matches any string of length N + */ +describe('Property 5: Glob matching correctness', () => { + /** + * Arbitrary that generates strings without glob metacharacters (* and ?) + * to use as literal patterns/values. + */ + const nonGlobString = fc.string({ minLength: 0, maxLength: 50 }).map((s) => + s.replace(/[*?]/g, 'x') + ) + + const nonEmptyNonGlobString = fc.string({ minLength: 1, maxLength: 50 }).map((s) => + s.replace(/[*?]/g, 'x') + ) + + it('* matches any string', () => { + fc.assert( + fc.property(fc.string({ minLength: 0, maxLength: 100 }), (value) => { + expect(matchGlob('*', value)).toBe(true) + }) + ) + }) + + it('? matches exactly one character (single char → true)', () => { + fc.assert( + fc.property( + fc.string({ minLength: 1, maxLength: 1 }), + (singleChar) => { + expect(matchGlob('?', singleChar)).toBe(true) + } + ) + ) + }) + + it('? does not match strings of length != 1', () => { + fc.assert( + fc.property( + fc.string({ minLength: 2, maxLength: 50 }), + (multipleChars) => { + expect(matchGlob('?', multipleChars)).toBe(false) + } + ) + ) + // Also verify empty string + expect(matchGlob('?', '')).toBe(false) + }) + + it('exact match: matchGlob(str, str) is always true for non-glob strings', () => { + fc.assert( + fc.property(nonGlobString, (str) => { + expect(matchGlob(str, str)).toBe(true) + }) + ) + }) + + it('case sensitivity: different cases do not match', () => { + fc.assert( + fc.property( + nonEmptyNonGlobString.filter((s) => s.toLowerCase() !== s.toUpperCase() && s !== s.toUpperCase()), + (str) => { + // If str has at least one letter that differs in case, uppercasing should break the match + const upper = str.toUpperCase() + if (upper !== str) { + expect(matchGlob(str, upper)).toBe(false) + } + } + ) + ) + }) + + it('prefix glob: matchGlob(prefix + "*", prefix + anySuffix) is true', () => { + fc.assert( + fc.property( + nonEmptyNonGlobString, + nonGlobString, + (prefix, suffix) => { + expect(matchGlob(prefix + '*', prefix + suffix)).toBe(true) + } + ) + ) + }) + + it('pattern of N ? characters matches any string of length N', () => { + fc.assert( + fc.property( + fc.integer({ min: 1, max: 20 }), + (n) => { + const pattern = '?'.repeat(n) + // Generate a random string of exactly length n + const value = 'a'.repeat(n) + expect(matchGlob(pattern, value)).toBe(true) + } + ) + ) + }) + + it('pattern of N ? characters does not match strings of different length', () => { + fc.assert( + fc.property( + fc.integer({ min: 1, max: 20 }), + fc.integer({ min: 1, max: 10 }), + (n, offset) => { + const pattern = '?'.repeat(n) + // String longer than pattern + const longerValue = 'a'.repeat(n + offset) + expect(matchGlob(pattern, longerValue)).toBe(false) + + // String shorter than pattern (if n > offset) + if (n > offset) { + const shorterValue = 'a'.repeat(n - offset) + expect(matchGlob(pattern, shorterValue)).toBe(false) + } + } + ) + ) + }) +}) diff --git a/tests/property/inventory_coverage.property.spec.ts b/tests/property/inventory_coverage.property.spec.ts new file mode 100644 index 0000000..1a335b8 --- /dev/null +++ b/tests/property/inventory_coverage.property.spec.ts @@ -0,0 +1,118 @@ +import { describe, it, expect } from 'vitest' +import * as fc from 'fast-check' +import { + isParameterCoveredByInventory, + suggestInventoryPath, +} from '../../app/utils/inventory_coverage.js' + +/** + * **Validates: Requirements 5.5** + * + * Property 9: Desired parameters require inventory coverage + * Verifies subtree prefix matching logic using random TR-069-like paths. + */ + +// Arbitrary: generates a single TR-069 path segment (alphanumeric, 1-10 chars) +const segmentArb = fc.array( + fc.constantFrom(...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.split('')), + { minLength: 1, maxLength: 10 } +).map((chars) => chars.join('')) + +// Arbitrary: generates a TR-069-like dot-separated path with 2-5 segments (e.g. "Device.Info.Param") +const tr069PathArb = fc.array(segmentArb, { minLength: 2, maxLength: 5 }).map((segments) => segments.join('.')) + +// Arbitrary: generates a subtree path (ends with dot) from a TR-069 path by removing the last segment +const subtreePathFromParamArb = tr069PathArb.map((paramPath) => { + const parts = paramPath.split('.') + parts.pop() + return { paramPath, subtreePath: parts.join('.') + '.' } +}) + +describe('Property 9: Inventory coverage validation', () => { + it('a parameter is always covered by its own subtree path', () => { + fc.assert( + fc.property(subtreePathFromParamArb, ({ paramPath, subtreePath }) => { + const result = isParameterCoveredByInventory(paramPath, [{ path: subtreePath }]) + expect(result).toBe(true) + }), + { numRuns: 200 } + ) + }) + + it('suggestInventoryPath always produces a path that covers the original parameter', () => { + fc.assert( + fc.property(tr069PathArb, (paramPath) => { + const suggested = suggestInventoryPath(paramPath) + const result = isParameterCoveredByInventory(paramPath, [{ path: suggested }]) + expect(result).toBe(true) + }), + { numRuns: 200 } + ) + }) + + it('a subtree path covers all parameters that start with it', () => { + fc.assert( + fc.property( + fc.array(segmentArb, { minLength: 1, maxLength: 3 }), + fc.array(segmentArb, { minLength: 1, maxLength: 3 }), + (prefixSegments, suffixSegments) => { + const subtreePath = prefixSegments.join('.') + '.' + const paramPath = prefixSegments.join('.') + '.' + suffixSegments.join('.') + const result = isParameterCoveredByInventory(paramPath, [{ path: subtreePath }]) + expect(result).toBe(true) + } + ), + { numRuns: 200 } + ) + }) + + it('a subtree path does NOT cover parameters that do not start with it', () => { + fc.assert( + fc.property( + fc.array(segmentArb, { minLength: 1, maxLength: 3 }), + fc.array(segmentArb, { minLength: 1, maxLength: 3 }), + segmentArb, + (prefixSegments, suffixSegments, differentFirst) => { + const subtreePath = prefixSegments.join('.') + '.' + // Build a param that does NOT start with the subtree path + // by using a different first segment + const altFirst = differentFirst + 'X' // ensure it differs + const paramPath = altFirst + '.' + suffixSegments.join('.') + + // Only assert when the param truly doesn't start with the subtree + fc.pre(!paramPath.startsWith(subtreePath)) + + const result = isParameterCoveredByInventory(paramPath, [{ path: subtreePath }]) + expect(result).toBe(false) + } + ), + { numRuns: 200 } + ) + }) + + it('an exact path (no trailing dot) only matches the exact same string', () => { + fc.assert( + fc.property(tr069PathArb, tr069PathArb, (exactPath, otherPath) => { + // exactPath has no trailing dot, so it's an exact match inventory path + fc.pre(!exactPath.endsWith('.')) + const result = isParameterCoveredByInventory(otherPath, [{ path: exactPath }]) + if (otherPath === exactPath) { + expect(result).toBe(true) + } else { + expect(result).toBe(false) + } + }), + { numRuns: 200 } + ) + }) + + it('empty inventory paths array means never covered', () => { + fc.assert( + fc.property(tr069PathArb, (paramPath) => { + const result = isParameterCoveredByInventory(paramPath, []) + expect(result).toBe(false) + }), + { numRuns: 200 } + ) + }) +}) diff --git a/tests/property/migration_service.property.spec.ts b/tests/property/migration_service.property.spec.ts new file mode 100644 index 0000000..61fdf7b --- /dev/null +++ b/tests/property/migration_service.property.spec.ts @@ -0,0 +1,431 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import fc from 'fast-check' +import MigrationService from '../../app/services/migration_service.js' +import type { CreateConfigurationDto } from '../../app/services/configuration_service.js' + +// Mock services +vi.mock('../../app/services/configuration_service.js', () => { + return { + default: class ConfigurationService { + private configurations: Map = new Map() + + async findById(id: string) { + return this.configurations.get(id) || null + } + + async findAll() { + return { + data: Array.from(this.configurations.values()), + } + } + + async create(dto: CreateConfigurationDto) { + this.configurations.set(dto.identifier, { + ...dto, + id: dto.identifier, + createdAt: new Date(), + updatedAt: new Date(), + }) + return this.configurations.get(dto.identifier) + } + }, + } +}) + +vi.mock('../../app/services/firmware_catalog_service.js', () => { + return { + default: class FirmwareCatalogService { + async createEntry() { + return {} + } + }, + } +}) + +/** + * Property 14: Migration preserves semantics + * + * For any legacy Discovery configuration with modelPaths M and criteria C, + * the migration SHALL produce a Configuration with inventory paths derived + * from M, criteria derived from C's DEVICE_ID entries, policy "maintain", + * and empty desiredParameters. For any legacy Provisioning configuration + * with SET_PARAMETER_VALUES actions, the migration SHALL produce a + * Configuration with desiredParameters derived from those actions and + * policy "once". In both cases, the original identifier (prefixed) and + * creationDate SHALL be preserved. + * + * Validates: Requirements 14.1, 14.2, 14.3, 14.4 + */ +describe('Property 14: Migration preserves semantics', () => { + let mockDb: any + + beforeEach(() => { + mockDb = { + collection: vi.fn((_name: string) => { + return { + find: vi.fn(() => ({ + toArray: vi.fn(async () => []), + })), + insertOne: vi.fn(async () => ({})), + } + }), + } + + new MigrationService(mockDb) + }) + + /** + * Property 14.1: Discovery migration preserves semantics + * + * For any legacy Discovery config with: + * - identifier + * - modelPaths (array of strings) + * - criteria (manufacturer, oui, productClass, serialNumber) + * - creationDate + * + * The migrated Configuration SHALL have: + * - identifier prefixed with "legacy-disc-" + * - inventory paths derived from modelPaths + * - criteria matching the legacy criteria + * - policy "maintain" + * - empty desiredParameters + */ + it('Property 14.1: Discovery migration preserves inventory paths, criteria, and policy', async () => { + await fc.assert( + fc.asyncProperty( + fc.record({ + identifier: fc.string({ minLength: 1 }), + name: fc.string(), + modelPaths: fc.array(fc.string({ minLength: 1 }), { minLength: 1 }), + manufacturer: fc.option(fc.string()), + oui: fc.option(fc.string()), + productClass: fc.option(fc.string()), + serialNumber: fc.option(fc.string()), + createdAt: fc.date(), + }), + async (legacyConfig) => { + // Create a mock discovery config + const mockCollection = { + find: vi.fn(() => ({ + toArray: vi.fn(async () => [legacyConfig]), + })), + } + const mockDb = { + collection: vi.fn((name: string) => { + if (name === 'discovery_configs') return mockCollection + return { + find: vi.fn(() => ({ + toArray: vi.fn(async () => []), + })), + } + }), + } + + // Create a custom config service that captures created configs + const createdConfigs: CreateConfigurationDto[] = [] + const mockConfigService = { + async findById() { + return null + }, + async create(dto: CreateConfigurationDto) { + createdConfigs.push(dto) + return { ...dto, id: dto.identifier } + }, + async findAll() { + return { data: [] } + }, + } + + const service = new MigrationService(mockDb as any, mockConfigService as any) + const report = await service.migrateAll() + + // Should have migrated exactly one discovery config + expect(report.discoveryMigrated).toBe(1) + expect(report.discoverySkipped).toBe(0) + expect(createdConfigs).toHaveLength(1) + + const migrated = createdConfigs[0] + + // Verify identifier is prefixed + expect(migrated.identifier).toBe(`legacy-disc-${legacyConfig.identifier}`) + + // Verify inventory paths are derived from modelPaths + expect(migrated.inventory).toHaveLength(legacyConfig.modelPaths.length) + for (let i = 0; i < legacyConfig.modelPaths.length; i++) { + expect(migrated.inventory[i]).toEqual({ path: legacyConfig.modelPaths[i] }) + } + + // Verify criteria match legacy values (only truthy values) + ;(['manufacturer', 'oui', 'productClass', 'serialNumber'] as const).forEach((key) => { + const legacyValue = (legacyConfig as any)[key] + if (legacyValue) { + expect(migrated.criteria[key]).toBe(legacyValue) + } else { + expect(migrated.criteria[key]).toBeUndefined() + } + }) + expect(migrated.policy).toBe('maintain') + + // Verify desiredParameters is empty + expect(migrated.desiredParameters).toEqual([]) + + // Verify tags include "legacy" and "discovery" + expect(migrated.tags).toContain('legacy') + expect(migrated.tags).toContain('discovery') + } + ), + { numRuns: 50 } + ) + }) + + /** + * Property 14.2: Provisioning migration preserves semantics + * + * For any legacy Provisioning config with: + * - identifier + * - actions (array of SET_PARAMETER actions) + * - criteria (manufacturer, oui, productClass, serialNumber) + * - downloadActions (optional, determines policy) + * - creationDate + * + * The migrated Configuration SHALL have: + * - identifier prefixed with "legacy-prov-" + * - desiredParameters derived from SET_PARAMETER actions + * - policy "once" if downloadActions exist, "maintain" otherwise + * - criteria matching the legacy criteria + * - tags including "legacy" and "provisioning" + */ + it('Property 14.2: Provisioning migration preserves parameters, policy, and criteria', async () => { + await fc.assert( + fc.asyncProperty( + fc.record({ + identifier: fc.string({ minLength: 1 }), + name: fc.string(), + manufacturer: fc.option(fc.string()), + oui: fc.option(fc.string()), + productClass: fc.option(fc.string()), + serialNumber: fc.option(fc.string()), + actions: fc.array( + fc.record({ + type: fc.constant('SET_PARAMETER'), + parameter: fc.string({ minLength: 1 }), + value: fc.oneof(fc.string(), fc.integer(), fc.boolean()), + }), + { minLength: 1 } + ), + downloadActions: fc.array(fc.record({ version: fc.string() }), { + minLength: 0, + maxLength: 2, + }), + createdAt: fc.date(), + }), + async (legacyConfig) => { + // Create a mock provisioning config + const mockCollection = { + find: vi.fn(() => ({ + toArray: vi.fn(async () => [legacyConfig]), + })), + } + const mockDb = { + collection: vi.fn((name: string) => { + if (name === 'provisioning_configs') return mockCollection + return { + find: vi.fn(() => ({ + toArray: vi.fn(async () => []), + })), + } + }), + } + + // Create a custom config service that captures created configs + const createdConfigs: CreateConfigurationDto[] = [] + const mockConfigService = { + async findById() { + return null + }, + async create(dto: CreateConfigurationDto) { + createdConfigs.push(dto) + return { ...dto, id: dto.identifier } + }, + async findAll() { + return { data: [] } + }, + } + + const service = new MigrationService(mockDb as any, mockConfigService as any) + const report = await service.migrateAll() + + // Should have migrated exactly one provisioning config + expect(report.provisioningMigrated).toBe(1) + expect(report.provisioningSkipped).toBe(0) + expect(createdConfigs).toHaveLength(1) + + const migrated = createdConfigs[0] + + // Verify identifier is prefixed + expect(migrated.identifier).toBe(`legacy-prov-${legacyConfig.identifier}`) + + // Verify desiredParameters are derived from SET_PARAMETER actions + expect(migrated.desiredParameters).toHaveLength(legacyConfig.actions.length) + for (let i = 0; i < legacyConfig.actions.length; i++) { + const param = migrated.desiredParameters[i] + const action = legacyConfig.actions[i] + expect(param.name).toBe(action.parameter) + expect(param.value).toBe(action.value) + } + + // Verify policy based on downloadActions + const expectedPolicy = legacyConfig.downloadActions.length > 0 ? 'once' : 'maintain' + expect(migrated.policy).toBe(expectedPolicy) + + // Verify criteria match legacy values (only truthy values) + ;(['manufacturer', 'oui', 'productClass', 'serialNumber'] as const).forEach((key) => { + const legacyValue = (legacyConfig as any)[key] + if (legacyValue) { + expect(migrated.criteria[key]).toBe(legacyValue) + } else { + expect(migrated.criteria[key]).toBeUndefined() + } + }) + expect(migrated.tags).toContain('legacy') + expect(migrated.tags).toContain('provisioning') + } + ), + { numRuns: 50 } + ) + }) + + /** + * Property 14.3: Migration preserves identifier uniqueness + * + * If two different legacy configs have the same identifier, the second + * migration should be skipped (not overwrite the first). + */ + it('Property 14.3: Migration skips duplicate identifiers', async () => { + await fc.assert( + fc.asyncProperty( + fc.record({ + identifier: fc.string({ minLength: 1 }), + modelPaths: fc.array(fc.string({ minLength: 1 }), { minLength: 1 }), + }), + async (legacyConfig) => { + // Create a config service that returns the first config as already existing + const mockDb = { + collection: vi.fn((name: string) => ({ + find: vi.fn(() => ({ + toArray: vi.fn(async () => { + if (name === 'discovery_configs') { + // Return the same config twice to test duplicate handling + return [legacyConfig, legacyConfig] + } + return [] + }), + })), + })), + } + + const createdConfigs: CreateConfigurationDto[] = [] + let callCount = 0 + const mockConfigService = { + async findById(id: string) { + // First call returns null (not exists), second call returns the config + if (callCount > 0) { + return { identifier: id } + } + return null + }, + async create(dto: CreateConfigurationDto) { + callCount++ + createdConfigs.push(dto) + return { ...dto, id: dto.identifier } + }, + async findAll() { + return { data: [] } + }, + } + + const service = new MigrationService(mockDb as any, mockConfigService as any) + const report = await service.migrateAll() + + // First config created, second skipped + expect(report.discoveryMigrated).toBe(1) + expect(report.discoverySkipped).toBe(1) + expect(createdConfigs).toHaveLength(1) + } + ), + { numRuns: 30 } + ) + }) + + /** + * Property 14.4: Criteria coverage + * + * When legacy config has partial criteria (e.g., only manufacturer), + * the migrated Configuration should preserve exactly those fields + * (not add missing fields with defaults). + */ + it('Property 14.4: Partial criteria preserved exactly', async () => { + await fc.assert( + fc.asyncProperty( + fc.record({ + identifier: fc.string({ minLength: 1 }), + modelPaths: fc.array(fc.string({ minLength: 1 }), { minLength: 1 }), + manufacturer: fc.option(fc.string()), + oui: fc.option(fc.string()), + productClass: fc.option(fc.string()), + serialNumber: fc.option(fc.string()), + }), + async (legacyConfig) => { + const mockDb = { + collection: vi.fn((name: string) => ({ + find: vi.fn(() => ({ + toArray: vi.fn(async () => { + if (name === 'discovery_configs') return [legacyConfig] + return [] + }), + })), + })), + } + + const createdConfigs: CreateConfigurationDto[] = [] + const mockConfigService = { + async findById() { + return null + }, + async create(dto: CreateConfigurationDto) { + createdConfigs.push(dto) + return { ...dto, id: dto.identifier } + }, + async findAll() { + return { data: [] } + }, + } + + const service = new MigrationService(mockDb as any, mockConfigService as any) + await service.migrateAll() + + const migrated = createdConfigs[0] + + // Verify criteria only contain truthy values from legacy + Object.entries(migrated.criteria).forEach(([key]) => { + // Each criterion in migrated must come from a truthy value in legacy + expect((legacyConfig as any)[key]).toBeTruthy() + }) + + // Verify all truthy legacy criteria are in migrated + ;(['manufacturer', 'oui', 'productClass', 'serialNumber'] as const).forEach((key) => { + const legacyValue = (legacyConfig as any)[key] + if (legacyValue) { + // Truthy values must be in criteria + expect(migrated.criteria[key]).toBe(legacyValue) + } else { + // Falsy values must NOT be in criteria + expect(migrated.criteria[key]).toBeUndefined() + } + }) + } + ), + { numRuns: 50 } + ) + }) +}) diff --git a/tests/property/provisioning_code.property.spec.ts b/tests/property/provisioning_code.property.spec.ts new file mode 100644 index 0000000..d34a59f --- /dev/null +++ b/tests/property/provisioning_code.property.spec.ts @@ -0,0 +1,152 @@ +import { describe, it, expect } from 'vitest' +import * as fc from 'fast-check' +import { + computeContentHash, + buildProvisioningCode, + parseProvisioningCode, +} from '../../app/utils/provisioning_code.js' +import { PolicyType } from '../../app/domain/configuration/configuration.js' +import type { Configuration, DesiredParameter } from '../../app/domain/configuration/configuration.js' + +/** + * **Validates: Requirements 6.2** + * + * Property 11: ProvisioningCode format correctness + */ + +// --- Arbitraries --- + +const parameterTypeArb = fc.constantFrom('string', 'int', 'unsignedInt', 'boolean', 'dateTime') as fc.Arbitrary + +const desiredParameterArb: fc.Arbitrary = fc.record({ + name: fc.stringMatching(/^[A-Za-z][A-Za-z0-9.]{1,50}$/), + value: fc.oneof( + fc.string({ minLength: 0, maxLength: 20 }), + fc.integer(), + fc.boolean() + ), + type: parameterTypeArb, +}) + +const firmwareTargetArb = fc.record({ + productClass: fc.string({ minLength: 1, maxLength: 20 }), + targetVersion: fc.stringMatching(/^[0-9]+\.[0-9]+\.[0-9]+$/), +}) + +/** Generates a valid Configuration with random identifier, version, and desired state. */ +function configurationArb(): fc.Arbitrary { + return fc.record({ + identifier: fc.stringMatching(/^[A-Za-z0-9][A-Za-z0-9_-]{0,30}$/), + name: fc.string({ minLength: 1, maxLength: 50 }), + version: fc.stringMatching(/^[0-9]+\.[0-9]+\.[0-9]+$/), + priority: fc.integer({ min: 1, max: 1000 }), + policy: fc.constantFrom(PolicyType.ONCE, PolicyType.MAINTAIN), + enabled: fc.boolean(), + tags: fc.array(fc.string({ minLength: 1, maxLength: 10 }), { maxLength: 5 }), + criteria: fc.record({ + manufacturer: fc.option(fc.string({ minLength: 1, maxLength: 20 }), { nil: undefined }), + productClass: fc.option(fc.string({ minLength: 1, maxLength: 20 }), { nil: undefined }), + }), + inventory: fc.array( + fc.record({ path: fc.stringMatching(/^[A-Za-z][A-Za-z0-9.]{1,30}\.$/) }), + { maxLength: 5 } + ), + desiredParameters: fc.array(desiredParameterArb, { maxLength: 10 }), + firmwareTarget: fc.option(firmwareTargetArb, { nil: undefined }), + schedule: fc.constant(undefined), + createdAt: fc.date(), + updatedAt: fc.date(), + }) as fc.Arbitrary +} + +describe('Property 11: ProvisioningCode format correctness', () => { + it('buildProvisioningCode output always matches the expected regex', () => { + fc.assert( + fc.property(configurationArb(), (config) => { + const code = buildProvisioningCode(config) + expect(code).toMatch(/^(.+)\(v(.+)\)-([a-f0-9]{12})$/) + }), + { numRuns: 200 } + ) + }) + + it('parseProvisioningCode(buildProvisioningCode(config)) round-trips correctly', () => { + fc.assert( + fc.property(configurationArb(), (config) => { + const code = buildProvisioningCode(config) + const parsed = parseProvisioningCode(code) + + expect(parsed).not.toBeNull() + expect(parsed!.identifier).toBe(config.identifier) + expect(parsed!.version).toBe(config.version) + expect(parsed!.hash).toBe(computeContentHash(config)) + }), + { numRuns: 200 } + ) + }) + + it('computeContentHash is deterministic (same config → same hash)', () => { + fc.assert( + fc.property(configurationArb(), (config) => { + const hash1 = computeContentHash(config) + const hash2 = computeContentHash(config) + expect(hash1).toBe(hash2) + }), + { numRuns: 200 } + ) + }) + + it('computeContentHash produces exactly 12 hex characters', () => { + fc.assert( + fc.property(configurationArb(), (config) => { + const hash = computeContentHash(config) + expect(hash).toMatch(/^[a-f0-9]{12}$/) + }), + { numRuns: 200 } + ) + }) + + it('changing desiredParameters changes the hash', () => { + fc.assert( + fc.property( + configurationArb(), + desiredParameterArb, + (config, extraParam) => { + // Ensure the extra param has a different name from all existing ones + const existingNames = new Set(config.desiredParameters.map((p) => p.name)) + if (existingNames.has(extraParam.name)) return // skip this case + + const hash1 = computeContentHash(config) + const modified = { + ...config, + desiredParameters: [...config.desiredParameters, extraParam], + } + const hash2 = computeContentHash(modified) + expect(hash1).not.toBe(hash2) + } + ), + { numRuns: 200 } + ) + }) + + it('parseProvisioningCode returns null for invalid strings', () => { + const invalidStringsArb = fc.oneof( + fc.constant(''), + fc.constant('no-parens-here'), + fc.constant('id(v1.0)-tooshort'), + fc.constant('id(1.0)-a1b2c3d4e5f6'), // missing 'v' prefix + fc.constant('id(v1.0)-ZZZZZZZZZZZZ'), // uppercase not hex + // Random strings that don't match the pattern + fc.string({ minLength: 0, maxLength: 50 }).filter( + (s) => !/^(.+)\(v(.+)\)-([a-f0-9]{12})$/.test(s) + ) + ) + + fc.assert( + fc.property(invalidStringsArb, (input) => { + expect(parseProvisioningCode(input)).toBeNull() + }), + { numRuns: 200 } + ) + }) +}) diff --git a/tests/property/schedule_window.property.spec.ts b/tests/property/schedule_window.property.spec.ts new file mode 100644 index 0000000..cbcf9d9 --- /dev/null +++ b/tests/property/schedule_window.property.spec.ts @@ -0,0 +1,192 @@ +import { describe, it, expect } from 'vitest' +import fc from 'fast-check' +import { isWithinSchedule } from '../../app/utils/schedule_window.js' +import type { ScheduleWindow } from '../../app/domain/configuration/configuration.js' + +/** + * Property 12: Schedule window evaluation + * Validates: Requirements 8.1, 8.3 + * + * Verifies midnight-spanning logic and day-of-week filtering using + * randomly generated schedules and timestamps. + */ +describe('Property 12: Schedule window evaluation', () => { + // --- Arbitraries --- + + /** Generate a valid hour (0-23) */ + const arbHour = fc.integer({ min: 0, max: 23 }) + + /** Generate a valid day-of-week subset (0=Sunday ... 6=Saturday) */ + const arbDaysOfWeek = fc.subarray([0, 1, 2, 3, 4, 5, 6], { minLength: 1 }) + + /** Generate a Date within a reasonable range */ + const arbDate = fc.date({ + min: new Date('2020-01-01T00:00:00Z'), + max: new Date('2030-12-31T23:59:59Z'), + }) + + /** Generate a non-midnight-spanning schedule (startHour < endHour) */ + const arbNormalSchedule: fc.Arbitrary = fc + .tuple(arbHour, arbHour, arbDaysOfWeek) + .filter(([s, e]) => s < e) + .map(([startHour, endHour, daysOfWeek]) => ({ + startHour, + endHour, + daysOfWeek, + timezone: 'UTC', + })) + + /** Generate a midnight-spanning schedule (startHour > endHour) */ + const arbMidnightSchedule: fc.Arbitrary = fc + .tuple(arbHour, arbHour, arbDaysOfWeek) + .filter(([s, e]) => s > e) + .map(([startHour, endHour, daysOfWeek]) => ({ + startHour, + endHour, + daysOfWeek, + timezone: 'UTC', + })) + + /** Generate any valid schedule (startHour !== endHour) */ + const arbSchedule: fc.Arbitrary = fc + .tuple(arbHour, arbHour, arbDaysOfWeek) + .filter(([s, e]) => s !== e) + .map(([startHour, endHour, daysOfWeek]) => ({ + startHour, + endHour, + daysOfWeek, + timezone: 'UTC', + })) + + /** + * Helper: create a UTC Date at a specific hour on a specific day-of-week. + * Uses a known reference week (2025-01-05 is Sunday). + */ + function makeDateAtHourAndDay(hour: number, dayOfWeek: number): Date { + // 2025-01-05 is a Sunday (dayOfWeek=0) + const baseDate = new Date('2025-01-05T00:00:00Z') + baseDate.setUTCDate(baseDate.getUTCDate() + dayOfWeek) + baseDate.setUTCHours(hour, 0, 0, 0) + return baseDate + } + + // --- Property 1: undefined schedule always returns true --- + + it('isWithinSchedule(undefined, anyDate) always returns true', () => { + fc.assert( + fc.property(arbDate, (date) => { + expect(isWithinSchedule(undefined, date)).toBe(true) + }) + ) + }) + + // --- Property 2: Non-midnight-spanning window boundary behavior --- + + /** + * **Validates: Requirements 8.1, 8.3** + * + * For a non-midnight-spanning window (startHour < endHour): + * - A time at startHour is within the window + * - A time at endHour is NOT within the window (exclusive end) + */ + it('non-midnight-spanning: startHour is within, endHour is NOT within', () => { + fc.assert( + fc.property(arbNormalSchedule, (schedule) => { + // Pick a day that IS in daysOfWeek + const day = schedule.daysOfWeek[0] + + const atStart = makeDateAtHourAndDay(schedule.startHour, day) + const atEnd = makeDateAtHourAndDay(schedule.endHour, day) + + expect(isWithinSchedule(schedule, atStart)).toBe(true) + expect(isWithinSchedule(schedule, atEnd)).toBe(false) + }) + ) + }) + + // --- Property 3: Midnight-spanning window behavior --- + + /** + * **Validates: Requirements 8.1, 8.3** + * + * For a midnight-spanning window (startHour > endHour): + * - A time at startHour is within the window + * - A time between endHour and startHour (exclusive) is NOT within the window + */ + it('midnight-spanning: startHour is within, hour between endHour and startHour is NOT within', () => { + fc.assert( + fc.property( + arbMidnightSchedule, + fc.integer({ min: 0, max: 23 }), + (schedule, randomHour) => { + // Pick a day that IS in daysOfWeek + const day = schedule.daysOfWeek[0] + + // startHour should be within + const atStart = makeDateAtHourAndDay(schedule.startHour, day) + expect(isWithinSchedule(schedule, atStart)).toBe(true) + + // An hour strictly between endHour and startHour should be outside + // For midnight-spanning: outside range is [endHour, startHour) + const outsideHour = schedule.endHour + (randomHour % (schedule.startHour - schedule.endHour)) + if (outsideHour >= schedule.endHour && outsideHour < schedule.startHour) { + const outsideDate = makeDateAtHourAndDay(outsideHour, day) + expect(isWithinSchedule(schedule, outsideDate)).toBe(false) + } + } + ) + ) + }) + + // --- Property 4: Day-of-week filtering --- + + /** + * **Validates: Requirements 8.1, 8.3** + * + * If the day is NOT in daysOfWeek, returns false regardless of hour. + */ + it('day-of-week filtering: excluded day always returns false', () => { + fc.assert( + fc.property(arbSchedule, arbHour, (schedule, hour) => { + const allDays = [0, 1, 2, 3, 4, 5, 6] + const excludedDays = allDays.filter((d) => !schedule.daysOfWeek.includes(d)) + + // Only test if there are excluded days + if (excludedDays.length > 0) { + const excludedDay = excludedDays[0] + const date = makeDateAtHourAndDay(hour, excludedDay) + expect(isWithinSchedule(schedule, date)).toBe(false) + } + }) + ) + }) + + // --- Property 5: Included day + within hour range returns true --- + + /** + * **Validates: Requirements 8.1, 8.3** + * + * If daysOfWeek includes the current day and hour is within range, returns true. + */ + it('included day + hour within range returns true', () => { + fc.assert( + fc.property(arbSchedule, (schedule) => { + const day = schedule.daysOfWeek[0] + const { startHour, endHour } = schedule + + // Pick an hour that is definitely within the window + let withinHour: number + if (startHour < endHour) { + // Normal window: any hour in [startHour, endHour) + withinHour = startHour + } else { + // Midnight-spanning: startHour is always within + withinHour = startHour + } + + const date = makeDateAtHourAndDay(withinHour, day) + expect(isWithinSchedule(schedule, date)).toBe(true) + }) + ) + }) +}) diff --git a/tests/unit/services/configuration_service.spec.ts b/tests/unit/services/configuration_service.spec.ts new file mode 100644 index 0000000..3139546 --- /dev/null +++ b/tests/unit/services/configuration_service.spec.ts @@ -0,0 +1,1258 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import ConfigurationService, { + ValidationError, +} from '../../../app/services/configuration_service.js' +import type { CreateConfigurationDto } from '../../../app/services/configuration_service.js' +import { PolicyType } from '../../../app/domain/configuration/configuration.js' + +// Mock the models +vi.mock('#models/configuration', () => { + return { + default: { + createIfNotExists: vi.fn(), + findByIdentifier: vi.fn(), + findAllEnabled: vi.fn(), + paginateConfigurations: vi.fn(), + deleteByIdentifier: vi.fn(), + updateOne: vi.fn(), + }, + } +}) + +vi.mock('#models/configuration_version', () => { + return { + default: { + deleteByConfigurationIdentifier: vi.fn(), + getLatestVersionNumber: vi.fn(), + create: vi.fn(), + paginateVersions: vi.fn(), + }, + } +}) + +import ConfigurationModel from '#models/configuration' +import ConfigurationVersionModel from '#models/configuration_version' + +function buildValidDto(overrides: Partial = {}): CreateConfigurationDto { + return { + identifier: 'BM632w-standard', + name: 'Configuration standard BM632w', + version: '1.0.0', + priority: 100, + policy: PolicyType.MAINTAIN, + enabled: true, + tags: ['production'], + criteria: { manufacturer: 'Sagemcom', productClass: 'BM632w' }, + inventory: [{ path: 'InternetGatewayDevice.ManagementServer.' }], + desiredParameters: [ + { + name: 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', + value: 3600, + type: 'unsignedInt', + }, + ], + ...overrides, + } +} + +describe('ConfigurationService', () => { + let service: ConfigurationService + + beforeEach(() => { + service = new ConfigurationService() + vi.clearAllMocks() + // Default mock returns for version model methods used in update + vi.mocked(ConfigurationVersionModel.getLatestVersionNumber).mockResolvedValue(0) + vi.mocked(ConfigurationVersionModel.create).mockResolvedValue({} as any) + // Default: no existing configurations (no overlap) + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([]) + }) + + describe('create', () => { + it('creates a valid configuration', async () => { + const dto = buildValidDto() + const expected = { ...dto, createdAt: new Date(), updatedAt: new Date() } + vi.mocked(ConfigurationModel.createIfNotExists).mockResolvedValue(expected as any) + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([]) + + const result = await service.create(dto) + + expect(ConfigurationModel.createIfNotExists).toHaveBeenCalledWith( + expect.objectContaining({ + identifier: 'BM632w-standard', + name: 'Configuration standard BM632w', + version: '1.0.0', + priority: 100, + policy: PolicyType.MAINTAIN, + enabled: true, + }) + ) + expect(result).toEqual(expected) + }) + + it('defaults enabled to true when not provided', async () => { + const dto = buildValidDto({ enabled: undefined }) + vi.mocked(ConfigurationModel.createIfNotExists).mockResolvedValue({} as any) + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([]) + + await service.create(dto) + + expect(ConfigurationModel.createIfNotExists).toHaveBeenCalledWith( + expect.objectContaining({ enabled: true }) + ) + }) + + it('defaults tags to empty array when not provided', async () => { + const dto = buildValidDto({ tags: undefined }) + vi.mocked(ConfigurationModel.createIfNotExists).mockResolvedValue({} as any) + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([]) + + await service.create(dto) + + expect(ConfigurationModel.createIfNotExists).toHaveBeenCalledWith( + expect.objectContaining({ tags: [] }) + ) + }) + }) + + describe('validation - identifier', () => { + it('rejects empty identifier', async () => { + const dto = buildValidDto({ identifier: '' }) + await expect(service.create(dto)).rejects.toThrow(ValidationError) + }) + + it('rejects identifier starting with hyphen', async () => { + const dto = buildValidDto({ identifier: '-invalid' }) + await expect(service.create(dto)).rejects.toThrow(ValidationError) + }) + + it('rejects identifier starting with underscore', async () => { + const dto = buildValidDto({ identifier: '_invalid' }) + await expect(service.create(dto)).rejects.toThrow(ValidationError) + }) + + it('rejects identifier with spaces', async () => { + const dto = buildValidDto({ identifier: 'has space' }) + await expect(service.create(dto)).rejects.toThrow(ValidationError) + }) + + it('rejects identifier with special characters', async () => { + const dto = buildValidDto({ identifier: 'invalid@char' }) + await expect(service.create(dto)).rejects.toThrow(ValidationError) + }) + + it('accepts identifier with hyphens and underscores', async () => { + const dto = buildValidDto({ identifier: 'valid-id_123' }) + vi.mocked(ConfigurationModel.createIfNotExists).mockResolvedValue({} as any) + await expect(service.create(dto)).resolves.not.toThrow() + }) + + it('accepts identifier starting with number', async () => { + const dto = buildValidDto({ identifier: '123-config' }) + vi.mocked(ConfigurationModel.createIfNotExists).mockResolvedValue({} as any) + await expect(service.create(dto)).resolves.not.toThrow() + }) + }) + + describe('validation - version (semver)', () => { + it('rejects non-semver version', async () => { + const dto = buildValidDto({ version: '1.0' }) + await expect(service.create(dto)).rejects.toThrow(ValidationError) + }) + + it('rejects version with prefix', async () => { + const dto = buildValidDto({ version: 'v1.0.0' }) + await expect(service.create(dto)).rejects.toThrow(ValidationError) + }) + + it('rejects empty version', async () => { + const dto = buildValidDto({ version: '' }) + await expect(service.create(dto)).rejects.toThrow(ValidationError) + }) + + it('accepts valid semver', async () => { + const dto = buildValidDto({ version: '2.3.4' }) + vi.mocked(ConfigurationModel.createIfNotExists).mockResolvedValue({} as any) + await expect(service.create(dto)).resolves.not.toThrow() + }) + }) + + describe('validation - priority', () => { + it('rejects zero priority', async () => { + const dto = buildValidDto({ priority: 0 }) + await expect(service.create(dto)).rejects.toThrow(ValidationError) + }) + + it('rejects negative priority', async () => { + const dto = buildValidDto({ priority: -5 }) + await expect(service.create(dto)).rejects.toThrow(ValidationError) + }) + + it('rejects non-integer priority', async () => { + const dto = buildValidDto({ priority: 1.5 }) + await expect(service.create(dto)).rejects.toThrow(ValidationError) + }) + + it('accepts positive integer priority', async () => { + const dto = buildValidDto({ priority: 1 }) + vi.mocked(ConfigurationModel.createIfNotExists).mockResolvedValue({} as any) + await expect(service.create(dto)).resolves.not.toThrow() + }) + }) + + describe('validation - policy', () => { + it('rejects invalid policy', async () => { + const dto = buildValidDto({ policy: 'invalid' as any }) + await expect(service.create(dto)).rejects.toThrow(ValidationError) + }) + + it('accepts "once" policy', async () => { + const dto = buildValidDto({ policy: PolicyType.ONCE }) + vi.mocked(ConfigurationModel.createIfNotExists).mockResolvedValue({} as any) + await expect(service.create(dto)).resolves.not.toThrow() + }) + + it('accepts "maintain" policy', async () => { + const dto = buildValidDto({ policy: PolicyType.MAINTAIN }) + vi.mocked(ConfigurationModel.createIfNotExists).mockResolvedValue({} as any) + await expect(service.create(dto)).resolves.not.toThrow() + }) + }) + + describe('validation - criteria', () => { + it('rejects criteria with non-DeviceId field', async () => { + const dto = buildValidDto({ + criteria: { manufacturer: 'Sagemcom', invalidField: 'value' } as any, + }) + await expect(service.create(dto)).rejects.toThrow(ValidationError) + }) + + it('rejects criteria with writable parameter field', async () => { + const dto = buildValidDto({ + criteria: { firmwareVersion: '1.0.0' } as any, + }) + await expect(service.create(dto)).rejects.toThrow(ValidationError) + }) + + it('accepts all valid DeviceId fields', async () => { + const dto = buildValidDto({ + criteria: { + manufacturer: 'Sagemcom', + oui: '001122', + productClass: 'BM632w', + serialNumber: 'SN123', + }, + }) + vi.mocked(ConfigurationModel.createIfNotExists).mockResolvedValue({} as any) + await expect(service.create(dto)).resolves.not.toThrow() + }) + + it('accepts empty criteria object', async () => { + const dto = buildValidDto({ criteria: {} }) + vi.mocked(ConfigurationModel.createIfNotExists).mockResolvedValue({} as any) + await expect(service.create(dto)).resolves.not.toThrow() + }) + }) + + describe('validation - inventory coverage', () => { + it('rejects desired parameter not covered by inventory', async () => { + const dto = buildValidDto({ + inventory: [{ path: 'InternetGatewayDevice.DeviceInfo.' }], + desiredParameters: [ + { + name: 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', + value: 3600, + type: 'unsignedInt', + }, + ], + }) + await expect(service.create(dto)).rejects.toThrow(ValidationError) + }) + + it('includes suggestion in error message', async () => { + const dto = buildValidDto({ + inventory: [{ path: 'InternetGatewayDevice.DeviceInfo.' }], + desiredParameters: [ + { + name: 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', + value: 3600, + type: 'unsignedInt', + }, + ], + }) + await expect(service.create(dto)).rejects.toThrow( + /suggest adding "InternetGatewayDevice\.ManagementServer\."/ + ) + }) + + it('accepts when all desired parameters are covered', async () => { + const dto = buildValidDto({ + inventory: [{ path: 'InternetGatewayDevice.ManagementServer.' }], + desiredParameters: [ + { + name: 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', + value: 3600, + type: 'unsignedInt', + }, + ], + }) + vi.mocked(ConfigurationModel.createIfNotExists).mockResolvedValue({} as any) + await expect(service.create(dto)).resolves.not.toThrow() + }) + + it('accepts empty desired parameters without inventory', async () => { + const dto = buildValidDto({ + inventory: [], + desiredParameters: [], + }) + vi.mocked(ConfigurationModel.createIfNotExists).mockResolvedValue({} as any) + await expect(service.create(dto)).resolves.not.toThrow() + }) + }) + + describe('findAll', () => { + it('calls paginateConfigurations with default page and perPage', async () => { + vi.mocked(ConfigurationModel.paginateConfigurations).mockResolvedValue({ + data: [], + total: 0, + page: 1, + perPage: 25, + lastPage: 1, + }) + + await service.findAll() + + expect(ConfigurationModel.paginateConfigurations).toHaveBeenCalledWith(1, 25, {}) + }) + + it('applies tag filter', async () => { + vi.mocked(ConfigurationModel.paginateConfigurations).mockResolvedValue({ + data: [], + total: 0, + page: 1, + perPage: 25, + lastPage: 1, + }) + + await service.findAll({ tag: 'production' }) + + expect(ConfigurationModel.paginateConfigurations).toHaveBeenCalledWith(1, 25, { + tags: 'production', + }) + }) + + it('applies enabled filter', async () => { + vi.mocked(ConfigurationModel.paginateConfigurations).mockResolvedValue({ + data: [], + total: 0, + page: 1, + perPage: 25, + lastPage: 1, + }) + + await service.findAll({ enabled: true }) + + expect(ConfigurationModel.paginateConfigurations).toHaveBeenCalledWith(1, 25, { + enabled: true, + }) + }) + + it('applies productClass filter', async () => { + vi.mocked(ConfigurationModel.paginateConfigurations).mockResolvedValue({ + data: [], + total: 0, + page: 1, + perPage: 25, + lastPage: 1, + }) + + await service.findAll({ productClass: 'BM632w' }) + + expect(ConfigurationModel.paginateConfigurations).toHaveBeenCalledWith(1, 25, { + 'criteria.productClass': 'BM632w', + }) + }) + + it('applies multiple filters together', async () => { + vi.mocked(ConfigurationModel.paginateConfigurations).mockResolvedValue({ + data: [], + total: 0, + page: 1, + perPage: 25, + lastPage: 1, + }) + + await service.findAll({ tag: 'prod', enabled: true, productClass: 'BM632w' }) + + expect(ConfigurationModel.paginateConfigurations).toHaveBeenCalledWith(1, 25, { + tags: 'prod', + enabled: true, + 'criteria.productClass': 'BM632w', + }) + }) + + it('uses custom page and perPage', async () => { + vi.mocked(ConfigurationModel.paginateConfigurations).mockResolvedValue({ + data: [], + total: 0, + page: 3, + perPage: 10, + lastPage: 1, + }) + + await service.findAll({ page: 3, perPage: 10 }) + + expect(ConfigurationModel.paginateConfigurations).toHaveBeenCalledWith(3, 10, {}) + }) + }) + + describe('findById', () => { + it('returns configuration when found', async () => { + const config = buildValidDto() + vi.mocked(ConfigurationModel.findByIdentifier).mockResolvedValue(config as any) + + const result = await service.findById('BM632w-standard') + + expect(ConfigurationModel.findByIdentifier).toHaveBeenCalledWith('BM632w-standard') + expect(result).toEqual(config) + }) + + it('returns null when not found', async () => { + vi.mocked(ConfigurationModel.findByIdentifier).mockResolvedValue(null) + + const result = await service.findById('nonexistent') + + expect(result).toBeNull() + }) + }) + + describe('update', () => { + it('throws when configuration not found', async () => { + vi.mocked(ConfigurationModel.findByIdentifier).mockResolvedValue(null) + + await expect(service.update('nonexistent', { name: 'New Name' })).rejects.toThrow( + ValidationError + ) + }) + + it('updates only provided fields', async () => { + const existing = buildValidDto() + vi.mocked(ConfigurationModel.findByIdentifier) + .mockResolvedValueOnce(existing as any) + .mockResolvedValueOnce({ ...existing, name: 'Updated Name' } as any) + vi.mocked(ConfigurationModel.updateOne).mockResolvedValue({} as any) + + const result = await service.update('BM632w-standard', { name: 'Updated Name' }) + + expect(ConfigurationModel.updateOne).toHaveBeenCalledWith( + { identifier: 'BM632w-standard' }, + { name: 'Updated Name' } + ) + expect(result.name).toBe('Updated Name') + }) + + it('validates version on update', async () => { + const existing = buildValidDto() + vi.mocked(ConfigurationModel.findByIdentifier).mockResolvedValue(existing as any) + + await expect(service.update('BM632w-standard', { version: 'invalid' })).rejects.toThrow( + ValidationError + ) + }) + + it('validates priority on update', async () => { + const existing = buildValidDto() + vi.mocked(ConfigurationModel.findByIdentifier).mockResolvedValue(existing as any) + + await expect(service.update('BM632w-standard', { priority: -1 })).rejects.toThrow( + ValidationError + ) + }) + + it('validates criteria on update', async () => { + const existing = buildValidDto() + vi.mocked(ConfigurationModel.findByIdentifier).mockResolvedValue(existing as any) + + await expect( + service.update('BM632w-standard', { criteria: { badField: 'x' } as any }) + ).rejects.toThrow(ValidationError) + }) + + it('validates inventory coverage with merged data on update', async () => { + const existing = buildValidDto({ + inventory: [{ path: 'InternetGatewayDevice.ManagementServer.' }], + desiredParameters: [ + { + name: 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', + value: 3600, + type: 'unsignedInt', + }, + ], + }) + vi.mocked(ConfigurationModel.findByIdentifier).mockResolvedValue(existing as any) + + // Changing inventory to one that doesn't cover existing desired params + await expect( + service.update('BM632w-standard', { + inventory: [{ path: 'InternetGatewayDevice.DeviceInfo.' }], + }) + ).rejects.toThrow(ValidationError) + }) + }) + + describe('delete', () => { + it('deletes configuration and its versions', async () => { + vi.mocked(ConfigurationModel.deleteByIdentifier).mockResolvedValue(true) + vi.mocked(ConfigurationVersionModel.deleteByConfigurationIdentifier).mockResolvedValue(3) + + const result = await service.delete('BM632w-standard') + + expect(result).toBe(true) + expect(ConfigurationModel.deleteByIdentifier).toHaveBeenCalledWith('BM632w-standard') + expect(ConfigurationVersionModel.deleteByConfigurationIdentifier).toHaveBeenCalledWith( + 'BM632w-standard' + ) + }) + + it('does not delete versions when configuration not found', async () => { + vi.mocked(ConfigurationModel.deleteByIdentifier).mockResolvedValue(false) + + const result = await service.delete('nonexistent') + + expect(result).toBe(false) + expect(ConfigurationVersionModel.deleteByConfigurationIdentifier).not.toHaveBeenCalled() + }) + }) + + describe('toggleEnabled', () => { + it('toggles enabled to true', async () => { + const existing = buildValidDto({ enabled: false }) + vi.mocked(ConfigurationModel.findByIdentifier) + .mockResolvedValueOnce(existing as any) + .mockResolvedValueOnce({ ...existing, enabled: true } as any) + vi.mocked(ConfigurationModel.updateOne).mockResolvedValue({} as any) + + const result = await service.toggleEnabled('BM632w-standard', true) + + expect(ConfigurationModel.updateOne).toHaveBeenCalledWith( + { identifier: 'BM632w-standard' }, + { enabled: true } + ) + expect(result.enabled).toBe(true) + }) + + it('toggles enabled to false', async () => { + const existing = buildValidDto({ enabled: true }) + vi.mocked(ConfigurationModel.findByIdentifier) + .mockResolvedValueOnce(existing as any) + .mockResolvedValueOnce({ ...existing, enabled: false } as any) + vi.mocked(ConfigurationModel.updateOne).mockResolvedValue({} as any) + + const result = await service.toggleEnabled('BM632w-standard', false) + + expect(ConfigurationModel.updateOne).toHaveBeenCalledWith( + { identifier: 'BM632w-standard' }, + { enabled: false } + ) + expect(result.enabled).toBe(false) + }) + + it('throws ValidationError when configuration not found', async () => { + vi.mocked(ConfigurationModel.findByIdentifier).mockResolvedValue(null) + + await expect(service.toggleEnabled('nonexistent', true)).rejects.toThrow(ValidationError) + }) + + it('does not modify other fields', async () => { + const existing = buildValidDto({ enabled: false, tags: ['production'], priority: 100 }) + const updated = { ...existing, enabled: true } + vi.mocked(ConfigurationModel.findByIdentifier) + .mockResolvedValueOnce(existing as any) + .mockResolvedValueOnce(updated as any) + vi.mocked(ConfigurationModel.updateOne).mockResolvedValue({} as any) + + const result = await service.toggleEnabled('BM632w-standard', true) + + // Only enabled field should be in the update payload + expect(ConfigurationModel.updateOne).toHaveBeenCalledWith( + { identifier: 'BM632w-standard' }, + { enabled: true } + ) + expect(result.tags).toEqual(['production']) + expect(result.priority).toBe(100) + }) + }) + + describe('updateTags', () => { + it('adds tags to a configuration', async () => { + const existing = buildValidDto({ tags: ['production'] }) + const updated = { ...existing, tags: ['production', 'livebox'] } + vi.mocked(ConfigurationModel.findByIdentifier) + .mockResolvedValueOnce(existing as any) + .mockResolvedValueOnce(updated as any) + vi.mocked(ConfigurationModel.updateOne).mockResolvedValue({} as any) + + const result = await service.updateTags('BM632w-standard', ['livebox']) + + expect(ConfigurationModel.updateOne).toHaveBeenCalledWith( + { identifier: 'BM632w-standard' }, + { tags: ['production', 'livebox'] } + ) + expect(result.tags).toEqual(['production', 'livebox']) + }) + + it('removes tags from a configuration', async () => { + const existing = buildValidDto({ tags: ['production', 'livebox', 'test'] }) + const updated = { ...existing, tags: ['production'] } + vi.mocked(ConfigurationModel.findByIdentifier) + .mockResolvedValueOnce(existing as any) + .mockResolvedValueOnce(updated as any) + vi.mocked(ConfigurationModel.updateOne).mockResolvedValue({} as any) + + const result = await service.updateTags('BM632w-standard', undefined, ['livebox', 'test']) + + expect(ConfigurationModel.updateOne).toHaveBeenCalledWith( + { identifier: 'BM632w-standard' }, + { tags: ['production'] } + ) + expect(result.tags).toEqual(['production']) + }) + + it('adds and removes tags simultaneously', async () => { + const existing = buildValidDto({ tags: ['production', 'old-tag'] }) + const updated = { ...existing, tags: ['production', 'new-tag'] } + vi.mocked(ConfigurationModel.findByIdentifier) + .mockResolvedValueOnce(existing as any) + .mockResolvedValueOnce(updated as any) + vi.mocked(ConfigurationModel.updateOne).mockResolvedValue({} as any) + + const result = await service.updateTags('BM632w-standard', ['new-tag'], ['old-tag']) + + expect(ConfigurationModel.updateOne).toHaveBeenCalledWith( + { identifier: 'BM632w-standard' }, + { tags: ['production', 'new-tag'] } + ) + expect(result.tags).toEqual(['production', 'new-tag']) + }) + + it('avoids duplicate tags when adding', async () => { + const existing = buildValidDto({ tags: ['production', 'livebox'] }) + const updated = { ...existing, tags: ['production', 'livebox'] } + vi.mocked(ConfigurationModel.findByIdentifier) + .mockResolvedValueOnce(existing as any) + .mockResolvedValueOnce(updated as any) + vi.mocked(ConfigurationModel.updateOne).mockResolvedValue({} as any) + + const result = await service.updateTags('BM632w-standard', ['production', 'livebox']) + + expect(ConfigurationModel.updateOne).toHaveBeenCalledWith( + { identifier: 'BM632w-standard' }, + { tags: ['production', 'livebox'] } + ) + expect(result.tags).toEqual(['production', 'livebox']) + }) + + it('handles removing non-existent tags gracefully', async () => { + const existing = buildValidDto({ tags: ['production'] }) + const updated = { ...existing, tags: ['production'] } + vi.mocked(ConfigurationModel.findByIdentifier) + .mockResolvedValueOnce(existing as any) + .mockResolvedValueOnce(updated as any) + vi.mocked(ConfigurationModel.updateOne).mockResolvedValue({} as any) + + const result = await service.updateTags('BM632w-standard', undefined, ['nonexistent']) + + expect(ConfigurationModel.updateOne).toHaveBeenCalledWith( + { identifier: 'BM632w-standard' }, + { tags: ['production'] } + ) + expect(result.tags).toEqual(['production']) + }) + + it('throws ValidationError when configuration not found', async () => { + vi.mocked(ConfigurationModel.findByIdentifier).mockResolvedValue(null) + + await expect( + service.updateTags('nonexistent', ['tag1']) + ).rejects.toThrow(ValidationError) + }) + + it('does not modify other fields', async () => { + const existing = buildValidDto({ tags: ['production'], enabled: true, priority: 100 }) + const updated = { ...existing, tags: ['production', 'new-tag'] } + vi.mocked(ConfigurationModel.findByIdentifier) + .mockResolvedValueOnce(existing as any) + .mockResolvedValueOnce(updated as any) + vi.mocked(ConfigurationModel.updateOne).mockResolvedValue({} as any) + + const result = await service.updateTags('BM632w-standard', ['new-tag']) + + // Only tags field should be in the update payload + expect(ConfigurationModel.updateOne).toHaveBeenCalledWith( + { identifier: 'BM632w-standard' }, + { tags: ['production', 'new-tag'] } + ) + expect(result.enabled).toBe(true) + expect(result.priority).toBe(100) + }) + + it('handles empty add and remove arrays', async () => { + const existing = buildValidDto({ tags: ['production'] }) + const updated = { ...existing, tags: ['production'] } + vi.mocked(ConfigurationModel.findByIdentifier) + .mockResolvedValueOnce(existing as any) + .mockResolvedValueOnce(updated as any) + vi.mocked(ConfigurationModel.updateOne).mockResolvedValue({} as any) + + const result = await service.updateTags('BM632w-standard', [], []) + + expect(ConfigurationModel.updateOne).toHaveBeenCalledWith( + { identifier: 'BM632w-standard' }, + { tags: ['production'] } + ) + expect(result.tags).toEqual(['production']) + }) + }) + + describe('version history', () => { + describe('update creates version snapshot', () => { + it('creates a version snapshot before applying update', async () => { + const existing = buildValidDto() + vi.mocked(ConfigurationModel.findByIdentifier) + .mockResolvedValueOnce(existing as any) + .mockResolvedValueOnce({ ...existing, name: 'Updated Name' } as any) + vi.mocked(ConfigurationModel.updateOne).mockResolvedValue({} as any) + vi.mocked(ConfigurationVersionModel.getLatestVersionNumber).mockResolvedValue(0) + vi.mocked(ConfigurationVersionModel.create).mockResolvedValue({} as any) + + await service.update('BM632w-standard', { name: 'Updated Name' }) + + expect(ConfigurationVersionModel.getLatestVersionNumber).toHaveBeenCalledWith( + 'BM632w-standard' + ) + expect(ConfigurationVersionModel.create).toHaveBeenCalledWith( + expect.objectContaining({ + configurationIdentifier: 'BM632w-standard', + versionNumber: 1, + snapshot: existing, + modifiedAt: expect.any(Date), + }) + ) + }) + + it('increments version number based on latest existing version', async () => { + const existing = buildValidDto() + vi.mocked(ConfigurationModel.findByIdentifier) + .mockResolvedValueOnce(existing as any) + .mockResolvedValueOnce({ ...existing, name: 'Updated' } as any) + vi.mocked(ConfigurationModel.updateOne).mockResolvedValue({} as any) + vi.mocked(ConfigurationVersionModel.getLatestVersionNumber).mockResolvedValue(5) + vi.mocked(ConfigurationVersionModel.create).mockResolvedValue({} as any) + + await service.update('BM632w-standard', { name: 'Updated' }) + + expect(ConfigurationVersionModel.create).toHaveBeenCalledWith( + expect.objectContaining({ + versionNumber: 6, + }) + ) + }) + + it('stores the full current state as snapshot before modification', async () => { + const existing = buildValidDto({ + name: 'Original Name', + priority: 100, + tags: ['production'], + }) + vi.mocked(ConfigurationModel.findByIdentifier) + .mockResolvedValueOnce(existing as any) + .mockResolvedValueOnce({ ...existing, name: 'New Name', priority: 200 } as any) + vi.mocked(ConfigurationModel.updateOne).mockResolvedValue({} as any) + vi.mocked(ConfigurationVersionModel.getLatestVersionNumber).mockResolvedValue(2) + vi.mocked(ConfigurationVersionModel.create).mockResolvedValue({} as any) + + await service.update('BM632w-standard', { name: 'New Name', priority: 200 }) + + expect(ConfigurationVersionModel.create).toHaveBeenCalledWith( + expect.objectContaining({ + snapshot: expect.objectContaining({ + name: 'Original Name', + priority: 100, + tags: ['production'], + }), + }) + ) + }) + + it('creates snapshot before the update is applied', async () => { + const existing = buildValidDto() + const callOrder: string[] = [] + + vi.mocked(ConfigurationModel.findByIdentifier) + .mockResolvedValueOnce(existing as any) + .mockResolvedValueOnce({ ...existing, name: 'Updated' } as any) + vi.mocked(ConfigurationVersionModel.getLatestVersionNumber).mockResolvedValue(0) + vi.mocked(ConfigurationVersionModel.create).mockImplementation(async () => { + callOrder.push('createVersion') + return {} as any + }) + vi.mocked(ConfigurationModel.updateOne).mockImplementation(async () => { + callOrder.push('updateConfig') + return {} as any + }) + + await service.update('BM632w-standard', { name: 'Updated' }) + + expect(callOrder).toEqual(['createVersion', 'updateConfig']) + }) + }) + + describe('getHistory', () => { + it('returns paginated version history', async () => { + const existing = buildValidDto() + vi.mocked(ConfigurationModel.findByIdentifier).mockResolvedValue(existing as any) + + const paginatedResult = { + data: [ + { + configurationIdentifier: 'BM632w-standard', + versionNumber: 2, + snapshot: existing, + modifiedAt: new Date('2024-01-02'), + }, + { + configurationIdentifier: 'BM632w-standard', + versionNumber: 1, + snapshot: existing, + modifiedAt: new Date('2024-01-01'), + }, + ], + total: 2, + page: 1, + perPage: 25, + lastPage: 1, + } + vi.mocked(ConfigurationVersionModel.paginateVersions).mockResolvedValue(paginatedResult as any) + + const result = await service.getHistory('BM632w-standard', 1, 25) + + expect(ConfigurationVersionModel.paginateVersions).toHaveBeenCalledWith( + 'BM632w-standard', + 1, + 25 + ) + expect(result.data).toHaveLength(2) + expect(result.data[0].versionNumber).toBe(2) + expect(result.data[1].versionNumber).toBe(1) + expect(result.total).toBe(2) + }) + + it('throws ValidationError when configuration not found', async () => { + vi.mocked(ConfigurationModel.findByIdentifier).mockResolvedValue(null) + + await expect(service.getHistory('nonexistent', 1, 25)).rejects.toThrow(ValidationError) + }) + + it('uses default pagination values', async () => { + const existing = buildValidDto() + vi.mocked(ConfigurationModel.findByIdentifier).mockResolvedValue(existing as any) + vi.mocked(ConfigurationVersionModel.paginateVersions).mockResolvedValue({ + data: [], + total: 0, + page: 1, + perPage: 25, + lastPage: 1, + }) + + await service.getHistory('BM632w-standard') + + expect(ConfigurationVersionModel.paginateVersions).toHaveBeenCalledWith( + 'BM632w-standard', + 1, + 25 + ) + }) + + it('supports custom page and limit', async () => { + const existing = buildValidDto() + vi.mocked(ConfigurationModel.findByIdentifier).mockResolvedValue(existing as any) + vi.mocked(ConfigurationVersionModel.paginateVersions).mockResolvedValue({ + data: [], + total: 0, + page: 3, + perPage: 10, + lastPage: 1, + }) + + await service.getHistory('BM632w-standard', 3, 10) + + expect(ConfigurationVersionModel.paginateVersions).toHaveBeenCalledWith( + 'BM632w-standard', + 3, + 10 + ) + }) + }) + + describe('delete removes all versions', () => { + it('deletes all associated versions when configuration is deleted', async () => { + vi.mocked(ConfigurationModel.deleteByIdentifier).mockResolvedValue(true) + vi.mocked(ConfigurationVersionModel.deleteByConfigurationIdentifier).mockResolvedValue(5) + + await service.delete('BM632w-standard') + + expect(ConfigurationVersionModel.deleteByConfigurationIdentifier).toHaveBeenCalledWith( + 'BM632w-standard' + ) + }) + + it('does not attempt to delete versions when configuration does not exist', async () => { + vi.mocked(ConfigurationModel.deleteByIdentifier).mockResolvedValue(false) + + await service.delete('nonexistent') + + expect(ConfigurationVersionModel.deleteByConfigurationIdentifier).not.toHaveBeenCalled() + }) + }) + }) + + describe('detectOverlap', () => { + it('returns null when no existing configurations', async () => { + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([]) + + const result = await service.detectOverlap( + { manufacturer: 'Sagemcom', productClass: 'BM632w' }, + 100 + ) + + expect(result).toBeNull() + }) + + it('returns null when no overlap with existing configurations', async () => { + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([ + { + identifier: 'other-config', + priority: 100, + enabled: true, + criteria: { manufacturer: 'Huawei', productClass: 'HG8245' }, + } as any, + ]) + + const result = await service.detectOverlap( + { manufacturer: 'Sagemcom', productClass: 'BM632w' }, + 100 + ) + + expect(result).toBeNull() + }) + + it('throws ValidationError with 409 for same-priority overlap', async () => { + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([ + { + identifier: 'existing-config', + priority: 100, + enabled: true, + criteria: { manufacturer: 'Sagemcom', productClass: 'BM632w' }, + } as any, + ]) + + await expect( + service.detectOverlap( + { manufacturer: 'Sagemcom', productClass: 'BM632w' }, + 100 + ) + ).rejects.toThrow(ValidationError) + + try { + await service.detectOverlap( + { manufacturer: 'Sagemcom', productClass: 'BM632w' }, + 100 + ) + } catch (error: any) { + expect(error.statusCode).toBe(409) + expect(error.message).toContain('existing-config') + } + }) + + it('returns warning for different-priority overlap', async () => { + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([ + { + identifier: 'existing-config', + priority: 50, + enabled: true, + criteria: { manufacturer: 'Sagemcom', productClass: 'BM632w' }, + } as any, + ]) + + const result = await service.detectOverlap( + { manufacturer: 'Sagemcom', productClass: 'BM632w' }, + 100 + ) + + expect(result).toEqual({ + type: 'overlap_different_priority', + overlappingConfigurations: [{ identifier: 'existing-config', priority: 50 }], + }) + }) + + it('excludes the specified configuration by excludeId', async () => { + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([ + { + identifier: 'self-config', + priority: 100, + enabled: true, + criteria: { manufacturer: 'Sagemcom', productClass: 'BM632w' }, + } as any, + ]) + + const result = await service.detectOverlap( + { manufacturer: 'Sagemcom', productClass: 'BM632w' }, + 100, + 'self-config' + ) + + expect(result).toBeNull() + }) + + it('detects overlap when one criteria has no field (matches all)', async () => { + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([ + { + identifier: 'broad-config', + priority: 50, + enabled: true, + criteria: { manufacturer: 'Sagemcom' }, // no productClass → matches all + } as any, + ]) + + const result = await service.detectOverlap( + { manufacturer: 'Sagemcom', productClass: 'BM632w' }, + 100 + ) + + expect(result).toEqual({ + type: 'overlap_different_priority', + overlappingConfigurations: [{ identifier: 'broad-config', priority: 50 }], + }) + }) + + it('detects overlap with glob patterns', async () => { + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([ + { + identifier: 'glob-config', + priority: 50, + enabled: true, + criteria: { manufacturer: 'Sagemcom', productClass: 'BM*' }, + } as any, + ]) + + const result = await service.detectOverlap( + { manufacturer: 'Sagemcom', productClass: 'BM632w' }, + 100 + ) + + expect(result).toEqual({ + type: 'overlap_different_priority', + overlappingConfigurations: [{ identifier: 'glob-config', priority: 50 }], + }) + }) + + it('does not detect overlap when patterns are clearly disjoint', async () => { + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([ + { + identifier: 'other-config', + priority: 100, + enabled: true, + criteria: { manufacturer: 'Sagemcom', productClass: 'HG*' }, + } as any, + ]) + + const result = await service.detectOverlap( + { manufacturer: 'Sagemcom', productClass: 'BM632w' }, + 100 + ) + + expect(result).toBeNull() + }) + + it('reports multiple overlapping configurations', async () => { + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([ + { + identifier: 'config-a', + priority: 50, + enabled: true, + criteria: { manufacturer: 'Sagemcom', productClass: 'BM632w' }, + } as any, + { + identifier: 'config-b', + priority: 200, + enabled: true, + criteria: { manufacturer: 'Sagemcom', productClass: 'BM*' }, + } as any, + ]) + + const result = await service.detectOverlap( + { manufacturer: 'Sagemcom', productClass: 'BM632w' }, + 100 + ) + + expect(result).toEqual({ + type: 'overlap_different_priority', + overlappingConfigurations: expect.arrayContaining([ + { identifier: 'config-a', priority: 50 }, + { identifier: 'config-b', priority: 200 }, + ]), + }) + expect(result!.overlappingConfigurations).toHaveLength(2) + }) + + it('prioritizes same-priority rejection over different-priority warning', async () => { + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([ + { + identifier: 'same-priority-config', + priority: 100, + enabled: true, + criteria: { manufacturer: 'Sagemcom', productClass: 'BM632w' }, + } as any, + { + identifier: 'diff-priority-config', + priority: 50, + enabled: true, + criteria: { manufacturer: 'Sagemcom', productClass: 'BM632w' }, + } as any, + ]) + + await expect( + service.detectOverlap( + { manufacturer: 'Sagemcom', productClass: 'BM632w' }, + 100 + ) + ).rejects.toThrow(ValidationError) + }) + + it('detects overlap when both criteria are empty (match all devices)', async () => { + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([ + { + identifier: 'catch-all', + priority: 1, + enabled: true, + criteria: {}, + } as any, + ]) + + const result = await service.detectOverlap({}, 100) + + expect(result).toEqual({ + type: 'overlap_different_priority', + overlappingConfigurations: [{ identifier: 'catch-all', priority: 1 }], + }) + }) + }) + + describe('create with overlap detection', () => { + it('includes warning in result for different-priority overlap', async () => { + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([ + { + identifier: 'existing-config', + priority: 50, + enabled: true, + criteria: { manufacturer: 'Sagemcom', productClass: 'BM632w' }, + } as any, + ]) + const dto = buildValidDto() + vi.mocked(ConfigurationModel.createIfNotExists).mockResolvedValue({ + ...dto, + createdAt: new Date(), + updatedAt: new Date(), + } as any) + + const result = await service.create(dto) + + expect((result as any).warning).toEqual({ + type: 'overlap_different_priority', + overlappingConfigurations: [{ identifier: 'existing-config', priority: 50 }], + }) + }) + + it('rejects creation with 409 for same-priority overlap', async () => { + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([ + { + identifier: 'existing-config', + priority: 100, + enabled: true, + criteria: { manufacturer: 'Sagemcom', productClass: 'BM632w' }, + } as any, + ]) + const dto = buildValidDto() + + try { + await service.create(dto) + expect.fail('Should have thrown') + } catch (error: any) { + expect(error).toBeInstanceOf(ValidationError) + expect(error.statusCode).toBe(409) + expect(error.message).toContain('existing-config') + } + }) + }) + + describe('update with overlap detection', () => { + it('includes warning when criteria change causes different-priority overlap', async () => { + const existing = buildValidDto({ priority: 100 }) + vi.mocked(ConfigurationModel.findByIdentifier) + .mockResolvedValueOnce(existing as any) + .mockResolvedValueOnce({ ...existing, criteria: { manufacturer: 'Sagemcom', productClass: 'HG*' } } as any) + vi.mocked(ConfigurationModel.updateOne).mockResolvedValue({} as any) + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([ + { + identifier: 'other-config', + priority: 50, + enabled: true, + criteria: { manufacturer: 'Sagemcom', productClass: 'HG8245' }, + } as any, + ]) + + const result = await service.update('BM632w-standard', { + criteria: { manufacturer: 'Sagemcom', productClass: 'HG*' }, + }) + + expect((result as any).warning).toEqual({ + type: 'overlap_different_priority', + overlappingConfigurations: [{ identifier: 'other-config', priority: 50 }], + }) + }) + + it('rejects update with 409 when priority change causes same-priority overlap', async () => { + const existing = buildValidDto({ priority: 100 }) + vi.mocked(ConfigurationModel.findByIdentifier).mockResolvedValue(existing as any) + vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([ + { + identifier: 'other-config', + priority: 50, + enabled: true, + criteria: { manufacturer: 'Sagemcom', productClass: 'BM632w' }, + } as any, + ]) + + try { + await service.update('BM632w-standard', { priority: 50 }) + expect.fail('Should have thrown') + } catch (error: any) { + expect(error).toBeInstanceOf(ValidationError) + expect(error.statusCode).toBe(409) + expect(error.message).toContain('other-config') + } + }) + + it('does not check overlap when neither criteria nor priority changed', async () => { + const existing = buildValidDto() + vi.mocked(ConfigurationModel.findByIdentifier) + .mockResolvedValueOnce(existing as any) + .mockResolvedValueOnce({ ...existing, name: 'Updated Name' } as any) + vi.mocked(ConfigurationModel.updateOne).mockResolvedValue({} as any) + + await service.update('BM632w-standard', { name: 'Updated Name' }) + + expect(ConfigurationModel.findAllEnabled).not.toHaveBeenCalled() + }) + }) +}) diff --git a/tests/unit/services/firmware_catalog_service.spec.ts b/tests/unit/services/firmware_catalog_service.spec.ts new file mode 100644 index 0000000..50b9dad --- /dev/null +++ b/tests/unit/services/firmware_catalog_service.spec.ts @@ -0,0 +1,505 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import FirmwareCatalogService from '../../../app/services/firmware_catalog_service.js' +import type { CreateFirmwareEntryDto, CreateMigrationRouteDto } from '../../../app/services/firmware_catalog_service.js' + +// Mock the models +vi.mock('#models/firmware_entry', () => ({ + default: { + createIfNotExists: vi.fn(), + findByProductClass: vi.fn(), + findByProductClassAndVersion: vi.fn(), + find: vi.fn(), + deleteByIdentifier: vi.fn(), + }, +})) + +vi.mock('#models/migration_route', () => ({ + default: { + createIfNotExists: vi.fn(), + findByProductClass: vi.fn(), + deleteByIdentifier: vi.fn(), + }, +})) + +import FirmwareEntryModel from '#models/firmware_entry' +import MigrationRouteModel from '#models/migration_route' + +describe('FirmwareCatalogService', () => { + let service: FirmwareCatalogService + + beforeEach(() => { + service = new FirmwareCatalogService() + vi.clearAllMocks() + }) + + describe('createEntry', () => { + const validEntry: CreateFirmwareEntryDto = { + identifier: 'BM632w-fw-2.1.0', + productClass: 'BM632w', + version: '2.1.0', + downloadUrl: 'http://firmware.local/BM632w/2.1.0.bin', + checksum: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4', + checksumType: 'md5', + fileSize: 15728640, + } + + it('creates a firmware entry with valid data', async () => { + const expected = { ...validEntry, createdAt: new Date() } + vi.mocked(FirmwareEntryModel.createIfNotExists).mockResolvedValue(expected as any) + + const result = await service.createEntry(validEntry) + + expect(FirmwareEntryModel.createIfNotExists).toHaveBeenCalledWith({ + identifier: validEntry.identifier, + productClass: validEntry.productClass, + version: validEntry.version, + downloadUrl: validEntry.downloadUrl, + checksum: validEntry.checksum, + checksumType: validEntry.checksumType, + fileSize: validEntry.fileSize, + }) + expect(result).toEqual(expected) + }) + + it('rejects invalid downloadUrl', async () => { + const invalidEntry = { ...validEntry, downloadUrl: 'not-a-url' } + + await expect(service.createEntry(invalidEntry)).rejects.toThrow( + 'downloadUrl must be a valid URL' + ) + }) + + it('rejects non-positive fileSize', async () => { + const invalidEntry = { ...validEntry, fileSize: 0 } + + await expect(service.createEntry(invalidEntry)).rejects.toThrow( + 'fileSize must be a positive integer' + ) + }) + + it('rejects negative fileSize', async () => { + const invalidEntry = { ...validEntry, fileSize: -1 } + + await expect(service.createEntry(invalidEntry)).rejects.toThrow( + 'fileSize must be a positive integer' + ) + }) + + it('rejects non-integer fileSize', async () => { + const invalidEntry = { ...validEntry, fileSize: 1.5 } + + await expect(service.createEntry(invalidEntry)).rejects.toThrow( + 'fileSize must be a positive integer' + ) + }) + + it('rejects invalid checksumType', async () => { + const invalidEntry = { ...validEntry, checksumType: 'sha512' as any } + + await expect(service.createEntry(invalidEntry)).rejects.toThrow( + "checksumType must be 'md5' or 'sha256'" + ) + }) + + it('rejects empty identifier', async () => { + const invalidEntry = { ...validEntry, identifier: '' } + + await expect(service.createEntry(invalidEntry)).rejects.toThrow( + 'identifier must be a non-empty string' + ) + }) + + it('rejects whitespace-only identifier', async () => { + const invalidEntry = { ...validEntry, identifier: ' ' } + + await expect(service.createEntry(invalidEntry)).rejects.toThrow( + 'identifier must be a non-empty string' + ) + }) + }) + + describe('findEntries', () => { + it('returns entries filtered by productClass', async () => { + const entries = [ + { identifier: 'fw-1', productClass: 'BM632w', version: '1.0.0' }, + ] + vi.mocked(FirmwareEntryModel.findByProductClass).mockResolvedValue(entries as any) + + const result = await service.findEntries('BM632w') + + expect(FirmwareEntryModel.findByProductClass).toHaveBeenCalledWith('BM632w') + expect(result).toEqual(entries) + }) + + it('returns all entries when no productClass is specified', async () => { + const entries = [ + { identifier: 'fw-1', productClass: 'BM632w' }, + { identifier: 'fw-2', productClass: 'Livebox5' }, + ] + vi.mocked(FirmwareEntryModel.find).mockResolvedValue(entries as any) + + const result = await service.findEntries() + + expect(FirmwareEntryModel.find).toHaveBeenCalledWith({}) + expect(result).toEqual(entries) + }) + }) + + describe('deleteEntry', () => { + it('deletes an existing entry', async () => { + vi.mocked(FirmwareEntryModel.deleteByIdentifier).mockResolvedValue(true) + + await expect(service.deleteEntry('fw-1')).resolves.toBeUndefined() + expect(FirmwareEntryModel.deleteByIdentifier).toHaveBeenCalledWith('fw-1') + }) + + it('throws when entry not found', async () => { + vi.mocked(FirmwareEntryModel.deleteByIdentifier).mockResolvedValue(false) + + await expect(service.deleteEntry('nonexistent')).rejects.toThrow( + "Firmware entry 'nonexistent' not found" + ) + }) + }) + + describe('createMigrationRoute', () => { + const validRoute: CreateMigrationRouteDto = { + identifier: 'BM632w-1.x-to-2.0', + productClass: 'BM632w', + sourceVersionPattern: '1.*', + targetVersion: '2.0.0', + downloadUrl: 'http://firmware.local/BM632w/upgrade-1.x-to-2.0.bin', + priority: 10, + } + + it('creates a migration route with valid data', async () => { + const expected = { ...validRoute, createdAt: new Date() } + vi.mocked(MigrationRouteModel.createIfNotExists).mockResolvedValue(expected as any) + + const result = await service.createMigrationRoute(validRoute) + + expect(MigrationRouteModel.createIfNotExists).toHaveBeenCalledWith({ + identifier: validRoute.identifier, + productClass: validRoute.productClass, + sourceVersionPattern: validRoute.sourceVersionPattern, + targetVersion: validRoute.targetVersion, + downloadUrl: validRoute.downloadUrl, + priority: validRoute.priority, + }) + expect(result).toEqual(expected) + }) + + it('rejects invalid downloadUrl', async () => { + const invalidRoute = { ...validRoute, downloadUrl: 'invalid' } + + await expect(service.createMigrationRoute(invalidRoute)).rejects.toThrow( + 'downloadUrl must be a valid URL' + ) + }) + + it('rejects non-positive priority', async () => { + const invalidRoute = { ...validRoute, priority: 0 } + + await expect(service.createMigrationRoute(invalidRoute)).rejects.toThrow( + 'priority must be a positive integer' + ) + }) + + it('rejects negative priority', async () => { + const invalidRoute = { ...validRoute, priority: -5 } + + await expect(service.createMigrationRoute(invalidRoute)).rejects.toThrow( + 'priority must be a positive integer' + ) + }) + + it('rejects non-integer priority', async () => { + const invalidRoute = { ...validRoute, priority: 2.5 } + + await expect(service.createMigrationRoute(invalidRoute)).rejects.toThrow( + 'priority must be a positive integer' + ) + }) + + it('rejects empty identifier', async () => { + const invalidRoute = { ...validRoute, identifier: '' } + + await expect(service.createMigrationRoute(invalidRoute)).rejects.toThrow( + 'identifier must be a non-empty string' + ) + }) + }) + + describe('findMigrationRoutes', () => { + it('returns routes sorted by priority descending', async () => { + const routes = [ + { identifier: 'route-1', priority: 20 }, + { identifier: 'route-2', priority: 10 }, + ] + vi.mocked(MigrationRouteModel.findByProductClass).mockResolvedValue(routes as any) + + const result = await service.findMigrationRoutes('BM632w') + + expect(MigrationRouteModel.findByProductClass).toHaveBeenCalledWith('BM632w', { priority: -1 }) + expect(result).toEqual(routes) + }) + }) + + describe('deleteMigrationRoute', () => { + it('deletes an existing route', async () => { + vi.mocked(MigrationRouteModel.deleteByIdentifier).mockResolvedValue(true) + + await expect(service.deleteMigrationRoute('route-1')).resolves.toBeUndefined() + expect(MigrationRouteModel.deleteByIdentifier).toHaveBeenCalledWith('route-1') + }) + + it('throws when route not found', async () => { + vi.mocked(MigrationRouteModel.deleteByIdentifier).mockResolvedValue(false) + + await expect(service.deleteMigrationRoute('nonexistent')).rejects.toThrow( + "Migration route 'nonexistent' not found" + ) + }) + }) + + describe('resolveUpgradePath', () => { + it('returns empty array when already at target version', async () => { + const result = await service.resolveUpgradePath('BM632w', '2.0.0', '2.0.0') + + expect(result).toEqual([]) + // Should not even query the database + expect(MigrationRouteModel.findByProductClass).not.toHaveBeenCalled() + }) + + it('resolves a direct single-step path', async () => { + vi.mocked(MigrationRouteModel.findByProductClass).mockResolvedValue([ + { + identifier: 'route-1', + productClass: 'BM632w', + sourceVersionPattern: '1.0.0', + targetVersion: '2.0.0', + downloadUrl: 'http://firmware.local/BM632w/upgrade-1.0-to-2.0.bin', + priority: 10, + createdAt: new Date(), + }, + ]) + vi.mocked(FirmwareEntryModel.findByProductClassAndVersion).mockResolvedValue({ + identifier: 'BM632w-fw-2.0.0', + productClass: 'BM632w', + version: '2.0.0', + downloadUrl: 'http://firmware.local/BM632w/2.0.0.bin', + checksum: 'abc123', + checksumType: 'md5', + fileSize: 1024000, + createdAt: new Date(), + }) + + const result = await service.resolveUpgradePath('BM632w', '1.0.0', '2.0.0') + + expect(result).toEqual([ + { + fromVersion: '1.0.0', + toVersion: '2.0.0', + downloadUrl: 'http://firmware.local/BM632w/upgrade-1.0-to-2.0.bin', + fileSize: 1024000, + checksum: 'abc123', + }, + ]) + }) + + it('resolves a multi-step path', async () => { + vi.mocked(MigrationRouteModel.findByProductClass).mockResolvedValue([ + { + identifier: 'route-1', + productClass: 'BM632w', + sourceVersionPattern: '1.0.*', + targetVersion: '1.5.0', + downloadUrl: 'http://firmware.local/BM632w/upgrade-1.0-to-1.5.bin', + priority: 10, + createdAt: new Date(), + }, + { + identifier: 'route-2', + productClass: 'BM632w', + sourceVersionPattern: '1.5.*', + targetVersion: '2.0.0', + downloadUrl: 'http://firmware.local/BM632w/upgrade-1.5-to-2.0.bin', + priority: 10, + createdAt: new Date(), + }, + ]) + vi.mocked(FirmwareEntryModel.findByProductClassAndVersion).mockImplementation( + async (_pc: string, version: string) => { + if (version === '1.5.0') { + return { + identifier: 'BM632w-fw-1.5.0', + productClass: 'BM632w', + version: '1.5.0', + downloadUrl: 'http://firmware.local/BM632w/1.5.0.bin', + checksum: 'hash150', + checksumType: 'sha256' as const, + fileSize: 800000, + createdAt: new Date(), + } + } + if (version === '2.0.0') { + return { + identifier: 'BM632w-fw-2.0.0', + productClass: 'BM632w', + version: '2.0.0', + downloadUrl: 'http://firmware.local/BM632w/2.0.0.bin', + checksum: 'hash200', + checksumType: 'md5' as const, + fileSize: 1200000, + createdAt: new Date(), + } + } + return null + } + ) + + const result = await service.resolveUpgradePath('BM632w', '1.0.3', '2.0.0') + + expect(result).toHaveLength(2) + expect(result[0]).toEqual({ + fromVersion: '1.0.3', + toVersion: '1.5.0', + downloadUrl: 'http://firmware.local/BM632w/upgrade-1.0-to-1.5.bin', + fileSize: 800000, + checksum: 'hash150', + }) + expect(result[1]).toEqual({ + fromVersion: '1.5.0', + toVersion: '2.0.0', + downloadUrl: 'http://firmware.local/BM632w/upgrade-1.5-to-2.0.bin', + fileSize: 1200000, + checksum: 'hash200', + }) + }) + + it('returns empty array when no path exists', async () => { + vi.mocked(MigrationRouteModel.findByProductClass).mockResolvedValue([ + { + identifier: 'route-1', + productClass: 'BM632w', + sourceVersionPattern: '1.0.*', + targetVersion: '1.5.0', + downloadUrl: 'http://firmware.local/BM632w/upgrade-1.0-to-1.5.bin', + priority: 10, + createdAt: new Date(), + }, + ]) + + // Trying to reach 3.0.0 but no route from 1.5.0 to 3.0.0 + const result = await service.resolveUpgradePath('BM632w', '1.0.0', '3.0.0') + + expect(result).toEqual([]) + }) + + it('returns empty array when cycle is detected', async () => { + vi.mocked(MigrationRouteModel.findByProductClass).mockResolvedValue([ + { + identifier: 'route-1', + productClass: 'BM632w', + sourceVersionPattern: '1.0.0', + targetVersion: '1.5.0', + downloadUrl: 'http://firmware.local/BM632w/upgrade-1.0-to-1.5.bin', + priority: 10, + createdAt: new Date(), + }, + { + identifier: 'route-2', + productClass: 'BM632w', + sourceVersionPattern: '1.5.0', + targetVersion: '1.0.0', + downloadUrl: 'http://firmware.local/BM632w/upgrade-1.5-to-1.0.bin', + priority: 10, + createdAt: new Date(), + }, + ]) + + const result = await service.resolveUpgradePath('BM632w', '1.0.0', '2.0.0') + + expect(result).toEqual([]) + }) + + it('selects the highest priority route when multiple apply', async () => { + vi.mocked(MigrationRouteModel.findByProductClass).mockResolvedValue([ + { + identifier: 'route-low', + productClass: 'BM632w', + sourceVersionPattern: '1.0.*', + targetVersion: '1.5.0', + downloadUrl: 'http://firmware.local/BM632w/slow-path.bin', + priority: 5, + createdAt: new Date(), + }, + { + identifier: 'route-high', + productClass: 'BM632w', + sourceVersionPattern: '1.*', + targetVersion: '2.0.0', + downloadUrl: 'http://firmware.local/BM632w/fast-path.bin', + priority: 20, + createdAt: new Date(), + }, + ]) + vi.mocked(FirmwareEntryModel.findByProductClassAndVersion).mockResolvedValue({ + identifier: 'BM632w-fw-2.0.0', + productClass: 'BM632w', + version: '2.0.0', + downloadUrl: 'http://firmware.local/BM632w/2.0.0.bin', + checksum: 'directhash', + checksumType: 'sha256', + fileSize: 2000000, + createdAt: new Date(), + }) + + const result = await service.resolveUpgradePath('BM632w', '1.0.5', '2.0.0') + + expect(result).toHaveLength(1) + expect(result[0]).toEqual({ + fromVersion: '1.0.5', + toVersion: '2.0.0', + downloadUrl: 'http://firmware.local/BM632w/fast-path.bin', + fileSize: 2000000, + checksum: 'directhash', + }) + }) + + it('keeps fileSize 0 and checksum empty when no firmware entry exists', async () => { + vi.mocked(MigrationRouteModel.findByProductClass).mockResolvedValue([ + { + identifier: 'route-1', + productClass: 'BM632w', + sourceVersionPattern: '1.0.0', + targetVersion: '2.0.0', + downloadUrl: 'http://firmware.local/BM632w/upgrade.bin', + priority: 10, + createdAt: new Date(), + }, + ]) + vi.mocked(FirmwareEntryModel.findByProductClassAndVersion).mockResolvedValue(null) + + const result = await service.resolveUpgradePath('BM632w', '1.0.0', '2.0.0') + + expect(result).toEqual([ + { + fromVersion: '1.0.0', + toVersion: '2.0.0', + downloadUrl: 'http://firmware.local/BM632w/upgrade.bin', + fileSize: 0, + checksum: '', + }, + ]) + }) + + it('returns empty array when no routes exist for the productClass', async () => { + vi.mocked(MigrationRouteModel.findByProductClass).mockResolvedValue([]) + + const result = await service.resolveUpgradePath('BM632w', '1.0.0', '2.0.0') + + expect(result).toEqual([]) + }) + }) +}) diff --git a/tests/unit/utils/glob_match.spec.ts b/tests/unit/utils/glob_match.spec.ts new file mode 100644 index 0000000..c61272a --- /dev/null +++ b/tests/unit/utils/glob_match.spec.ts @@ -0,0 +1,42 @@ +import { describe, it, expect } from 'vitest' +import { matchGlob } from '../../../app/utils/glob_match.js' + +describe('matchGlob', () => { + it('matches exact strings', () => { + expect(matchGlob('Sagemcom', 'Sagemcom')).toBe(true) + expect(matchGlob('Sagemcom', 'sagemcom')).toBe(false) + }) + + it('matches * as any sequence of characters', () => { + expect(matchGlob('Livebox*', 'Livebox5')).toBe(true) + expect(matchGlob('Livebox*', 'Livebox')).toBe(true) + expect(matchGlob('Livebox*', 'LiveboxPro')).toBe(true) + expect(matchGlob('*box', 'Livebox')).toBe(true) + expect(matchGlob('*box*', 'Livebox5')).toBe(true) + }) + + it('matches ? as exactly one character', () => { + expect(matchGlob('BM63?w', 'BM632w')).toBe(true) + expect(matchGlob('BM63?w', 'BM63w')).toBe(false) + expect(matchGlob('BM63?w', 'BM6312w')).toBe(false) + }) + + it('is case-sensitive', () => { + expect(matchGlob('Livebox*', 'livebox5')).toBe(false) + expect(matchGlob('livebox*', 'Livebox5')).toBe(false) + }) + + it('escapes regex special characters in pattern', () => { + expect(matchGlob('device.info', 'device.info')).toBe(true) + expect(matchGlob('device.info', 'deviceXinfo')).toBe(false) + expect(matchGlob('a+b', 'a+b')).toBe(true) + expect(matchGlob('a+b', 'aab')).toBe(false) + }) + + it('handles empty pattern and value', () => { + expect(matchGlob('', '')).toBe(true) + expect(matchGlob('*', '')).toBe(true) + expect(matchGlob('?', '')).toBe(false) + expect(matchGlob('', 'something')).toBe(false) + }) +}) diff --git a/tests/unit/utils/inventory_coverage.spec.ts b/tests/unit/utils/inventory_coverage.spec.ts new file mode 100644 index 0000000..71a79f9 --- /dev/null +++ b/tests/unit/utils/inventory_coverage.spec.ts @@ -0,0 +1,89 @@ +import { describe, it, expect } from 'vitest' +import { + isParameterCoveredByInventory, + suggestInventoryPath, +} from '../../../app/utils/inventory_coverage.js' + +describe('isParameterCoveredByInventory', () => { + it('returns true for subtree match (path ending with dot)', () => { + const inventoryPaths = [{ path: 'InternetGatewayDevice.ManagementServer.' }] + expect( + isParameterCoveredByInventory( + 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', + inventoryPaths + ) + ).toBe(true) + }) + + it('returns true for exact match (path without trailing dot)', () => { + const inventoryPaths = [ + { path: 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval' }, + ] + expect( + isParameterCoveredByInventory( + 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', + inventoryPaths + ) + ).toBe(true) + }) + + it('returns false when parameter is not covered', () => { + const inventoryPaths = [{ path: 'InternetGatewayDevice.DeviceInfo.' }] + expect( + isParameterCoveredByInventory( + 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', + inventoryPaths + ) + ).toBe(false) + }) + + it('returns false for empty inventory', () => { + expect( + isParameterCoveredByInventory( + 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', + [] + ) + ).toBe(false) + }) + + it('handles multiple inventory paths', () => { + const inventoryPaths = [ + { path: 'InternetGatewayDevice.DeviceInfo.' }, + { path: 'InternetGatewayDevice.ManagementServer.' }, + ] + expect( + isParameterCoveredByInventory( + 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', + inventoryPaths + ) + ).toBe(true) + }) + + it('does not match partial path names without trailing dot', () => { + const inventoryPaths = [{ path: 'InternetGatewayDevice.Man' }] + expect( + isParameterCoveredByInventory( + 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', + inventoryPaths + ) + ).toBe(false) + }) +}) + +describe('suggestInventoryPath', () => { + it('removes the leaf name and adds trailing dot', () => { + expect( + suggestInventoryPath('InternetGatewayDevice.ManagementServer.PeriodicInformInterval') + ).toBe('InternetGatewayDevice.ManagementServer.') + }) + + it('handles deeply nested paths', () => { + expect( + suggestInventoryPath('InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.Name') + ).toBe('InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.') + }) + + it('handles single-level path', () => { + expect(suggestInventoryPath('ParameterName')).toBe('.') + }) +}) diff --git a/tests/unit/utils/provisioning_code.spec.ts b/tests/unit/utils/provisioning_code.spec.ts new file mode 100644 index 0000000..9af3a36 --- /dev/null +++ b/tests/unit/utils/provisioning_code.spec.ts @@ -0,0 +1,119 @@ +import { describe, it, expect } from 'vitest' +import { + computeContentHash, + buildProvisioningCode, + parseProvisioningCode, +} from '../../../app/utils/provisioning_code.js' +import type { Configuration } from '../../../app/domain/configuration/configuration.js' +import { PolicyType } from '../../../app/domain/configuration/configuration.js' + +function makeConfig(overrides: Partial = {}): Configuration { + return { + identifier: 'BM632w-standard', + name: 'Configuration standard BM632w', + version: '1.0.1', + priority: 100, + policy: PolicyType.ONCE, + enabled: true, + tags: [], + criteria: { productClass: 'BM632w' }, + inventory: [], + desiredParameters: [ + { name: 'Device.Param.B', value: 'hello', type: 'string' }, + { name: 'Device.Param.A', value: 42, type: 'int' }, + ], + firmwareTarget: { productClass: 'BM632w', targetVersion: '2.1.0' }, + createdAt: new Date(), + updatedAt: new Date(), + ...overrides, + } +} + +describe('computeContentHash', () => { + it('returns a 12-character hex string', () => { + const hash = computeContentHash(makeConfig()) + expect(hash).toMatch(/^[a-f0-9]{12}$/) + }) + + it('is deterministic for the same config', () => { + const config = makeConfig() + expect(computeContentHash(config)).toBe(computeContentHash(config)) + }) + + it('sorts desiredParameters by name for consistency', () => { + const config1 = makeConfig({ + desiredParameters: [ + { name: 'B.Param', value: 1, type: 'int' }, + { name: 'A.Param', value: 2, type: 'int' }, + ], + }) + const config2 = makeConfig({ + desiredParameters: [ + { name: 'A.Param', value: 2, type: 'int' }, + { name: 'B.Param', value: 1, type: 'int' }, + ], + }) + expect(computeContentHash(config1)).toBe(computeContentHash(config2)) + }) + + it('changes when desiredParameters change', () => { + const config1 = makeConfig({ + desiredParameters: [{ name: 'A.Param', value: 1, type: 'int' }], + }) + const config2 = makeConfig({ + desiredParameters: [{ name: 'A.Param', value: 2, type: 'int' }], + }) + expect(computeContentHash(config1)).not.toBe(computeContentHash(config2)) + }) + + it('changes when firmwareTarget changes', () => { + const config1 = makeConfig({ + firmwareTarget: { productClass: 'BM632w', targetVersion: '2.0.0' }, + }) + const config2 = makeConfig({ + firmwareTarget: { productClass: 'BM632w', targetVersion: '3.0.0' }, + }) + expect(computeContentHash(config1)).not.toBe(computeContentHash(config2)) + }) +}) + +describe('buildProvisioningCode', () => { + it('returns the correct format', () => { + const config = makeConfig() + const code = buildProvisioningCode(config) + expect(code).toMatch(/^BM632w-standard\(v1\.0\.1\)-[a-f0-9]{12}$/) + }) + + it('matches the regex pattern from the spec', () => { + const config = makeConfig() + const code = buildProvisioningCode(config) + expect(code).toMatch(/^(.+)\(v(.+)\)-([a-f0-9]{12})$/) + }) +}) + +describe('parseProvisioningCode', () => { + it('parses a valid provisioning code', () => { + const result = parseProvisioningCode('BM632w-standard(v1.0.1)-a1b2c3d4e5f6') + expect(result).toEqual({ + identifier: 'BM632w-standard', + version: '1.0.1', + hash: 'a1b2c3d4e5f6', + }) + }) + + it('returns null for invalid format', () => { + expect(parseProvisioningCode('invalid')).toBeNull() + expect(parseProvisioningCode('')).toBeNull() + expect(parseProvisioningCode('id(v1.0)-short')).toBeNull() // hash too short + }) + + it('round-trips with buildProvisioningCode', () => { + const config = makeConfig() + const code = buildProvisioningCode(config) + const parsed = parseProvisioningCode(code) + expect(parsed).not.toBeNull() + expect(parsed!.identifier).toBe('BM632w-standard') + expect(parsed!.version).toBe('1.0.1') + expect(parsed!.hash).toMatch(/^[a-f0-9]{12}$/) + }) +}) diff --git a/tests/unit/utils/schedule_window.spec.ts b/tests/unit/utils/schedule_window.spec.ts new file mode 100644 index 0000000..d90f275 --- /dev/null +++ b/tests/unit/utils/schedule_window.spec.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from 'vitest' +import { isWithinSchedule } from '../../../app/utils/schedule_window.js' +import type { ScheduleWindow } from '../../../app/domain/configuration/configuration.js' + +describe('isWithinSchedule', () => { + it('returns true when schedule is undefined', () => { + expect(isWithinSchedule(undefined)).toBe(true) + }) + + it('returns true when within a normal window (non-midnight-spanning)', () => { + const schedule: ScheduleWindow = { + daysOfWeek: [1, 2, 3, 4, 5], // Mon-Fri + startHour: 2, + endHour: 6, + timezone: 'UTC', + } + // Wednesday at 03:00 UTC + const now = new Date('2025-01-08T03:00:00Z') + expect(isWithinSchedule(schedule, now)).toBe(true) + }) + + it('returns false when outside a normal window', () => { + const schedule: ScheduleWindow = { + daysOfWeek: [1, 2, 3, 4, 5], + startHour: 2, + endHour: 6, + timezone: 'UTC', + } + // Wednesday at 10:00 UTC + const now = new Date('2025-01-08T10:00:00Z') + expect(isWithinSchedule(schedule, now)).toBe(false) + }) + + it('returns false when at the endHour boundary (exclusive)', () => { + const schedule: ScheduleWindow = { + daysOfWeek: [1, 2, 3, 4, 5], + startHour: 2, + endHour: 6, + timezone: 'UTC', + } + // Wednesday at 06:00 UTC (endHour is exclusive) + const now = new Date('2025-01-08T06:00:00Z') + expect(isWithinSchedule(schedule, now)).toBe(false) + }) + + it('handles midnight-spanning window (startHour > endHour)', () => { + const schedule: ScheduleWindow = { + daysOfWeek: [0, 1, 2, 3, 4, 5, 6], + startHour: 22, + endHour: 6, + timezone: 'UTC', + } + // At 23:00 UTC - should be within + expect(isWithinSchedule(schedule, new Date('2025-01-08T23:00:00Z'))).toBe(true) + // At 03:00 UTC - should be within + expect(isWithinSchedule(schedule, new Date('2025-01-08T03:00:00Z'))).toBe(true) + // At 10:00 UTC - should be outside + expect(isWithinSchedule(schedule, new Date('2025-01-08T10:00:00Z'))).toBe(false) + }) + + it('filters by day of week', () => { + const schedule: ScheduleWindow = { + daysOfWeek: [1, 2, 3, 4, 5], // Mon-Fri only + startHour: 0, + endHour: 23, + timezone: 'UTC', + } + // Saturday 2025-01-11 at 10:00 UTC + const saturday = new Date('2025-01-11T10:00:00Z') + expect(isWithinSchedule(schedule, saturday)).toBe(false) + + // Monday 2025-01-06 at 10:00 UTC + const monday = new Date('2025-01-06T10:00:00Z') + expect(isWithinSchedule(schedule, monday)).toBe(true) + }) + + it('handles IANA timezone conversion', () => { + const schedule: ScheduleWindow = { + daysOfWeek: [0, 1, 2, 3, 4, 5, 6], + startHour: 2, + endHour: 6, + timezone: 'Europe/Paris', // UTC+1 in winter + } + // 03:00 UTC = 04:00 Paris time → within [2, 6) + expect(isWithinSchedule(schedule, new Date('2025-01-08T03:00:00Z'))).toBe(true) + // 00:00 UTC = 01:00 Paris time → outside [2, 6) + expect(isWithinSchedule(schedule, new Date('2025-01-08T00:00:00Z'))).toBe(false) + }) + + it('returns true when daysOfWeek is empty (no day restriction)', () => { + const schedule: ScheduleWindow = { + daysOfWeek: [], + startHour: 2, + endHour: 6, + timezone: 'UTC', + } + // Any day at 03:00 UTC + expect(isWithinSchedule(schedule, new Date('2025-01-08T03:00:00Z'))).toBe(true) + }) +}) diff --git a/tmp/.gitkeep b/tmp/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8c9803b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@adonisjs/tsconfig/tsconfig.app.json", + "compilerOptions": { + "rootDir": "./", + "jsx": "react", + "outDir": "./build" + } +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..8143e24 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from 'vite' +import inertia from '@adonisjs/inertia/vite' +import vue from '@vitejs/plugin-vue' +import adonisjs from '@adonisjs/vite/client' + +export default defineConfig({ + plugins: [ + inertia({ ssr: { enabled: false } }), + vue(), + adonisjs({ entrypoints: ['inertia/app/app.ts'], reload: ['resources/views/**/*.edge'] }), + ], + + resolve: { + alias: { + '~/': `${import.meta.dirname}/inertia/`, + }, + }, + + css: { + preprocessorOptions: { + scss: { + silenceDeprecations: ['mixed-decls', 'color-functions', 'global-builtin', 'import'], + }, + }, + }, +})