From d1c65dbf1e999a0299ef6facc08c9e4f44326cfb Mon Sep 17 00:00:00 2001 From: Kevin L'Hereec Date: Fri, 29 May 2026 16:46:37 +0200 Subject: [PATCH] =?UTF-8?q?chore:=20initial=20commit=20=E2=80=94=20AdonisJ?= =?UTF-8?q?S=20v7=20TR-069=20ACS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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> --- .adonisjs/client/data.d.ts | 15 + .adonisjs/client/manifest.d.ts | 9 + .adonisjs/client/registry/index.ts | 47 + .adonisjs/client/registry/schema.d.ts | 58 + .adonisjs/client/registry/tree.d.ts | 21 + .adonisjs/server/controllers.ts | 14 + .adonisjs/server/events.ts | 6 + .adonisjs/server/listeners.ts | 6 + .adonisjs/server/routes.d.ts | 26 + .dockerignore | 5 + .editorconfig | 22 + .env.example | 31 + .env.test | 1 + .gitignore | 26 + .prettierignore | 3 + Dockerfile | 31 + Dockerfile.genieacs-sim | 10 + ace.js | 27 + adonisrc.ts | 129 + app/controllers/commands_controller.ts | 31 + .../configurations_v1_controller.ts | 279 + app/controllers/cwmp_controller.ts | 203 + app/controllers/devices_controller.ts | 94 + .../firmware_catalog_controller.ts | 192 + app/controllers/health_controller.ts | 18 + app/controllers/pages_controller.ts | 132 + app/domain/commands/command.ts | 145 + app/domain/configuration/compliance.ts | 12 + app/domain/configuration/configuration.ts | 58 + .../configuration/configuration_version.ts | 8 + app/domain/device/device.ts | 249 + app/domain/device/device_id.ts | 45 + app/domain/device/device_parameter.ts | 110 + app/domain/device/device_record.ts | 14 + app/domain/firmware/firmware_catalog.ts | 25 + app/domain/firmware/migration_step.ts | 7 + app/domain/shared/mode.ts | 16 + app/domain/shared/types.ts | 7 + app/exceptions/handler.ts | 14 + app/exceptions/index.ts | 21 + .../container_bindings_middleware.ts | 19 + app/middleware/cwmp_logger_middleware.ts | 62 + app/middleware/cwmp_middleware.ts | 88 + app/middleware/inertia_middleware.ts | 17 + app/models/command.ts | 39 + app/models/configuration.ts | 37 + app/models/configuration_version.ts | 40 + app/models/device.ts | 131 + app/models/firmware_entry.ts | 34 + app/models/migration_route.ts | 31 + app/protocol/common/cwmp_message_builder.ts | 150 + app/protocol/soap.ts | 97 + app/services/command_service.ts | 58 + app/services/configuration_engine.ts | 408 + app/services/configuration_service.ts | 541 + app/services/cwmp/action_execution_helper.ts | 62 + .../cwmp/configuration_evaluation_state.ts | 269 + app/services/cwmp/device_action_executor.ts | 377 + .../cwmp/immediate_command_state_machine.ts | 64 + app/services/cwmp/main_state_machine.ts | 366 + app/services/cwmp/state_machine_manager.ts | 383 + app/services/cwmp/task_queue.ts | 250 + app/services/device_service.ts | 197 + app/services/firmware_catalog_service.ts | 225 + app/services/migration_service.ts | 297 + app/services/telemetry.ts | 26 + app/types.ts | 1 + app/utils/device_utils.ts | 15 + app/utils/glob_match.ts | 18 + app/utils/inventory_coverage.ts | 32 + app/utils/provisioning_code.ts | 38 + app/utils/schedule_window.ts | 87 + app/validators/command.ts | 23 + app/validators/configuration.ts | 69 + app/validators/device.ts | 12 + app/validators/firmware_catalog.ts | 45 + bin/console.ts | 47 + bin/server.ts | 45 + bin/test.ts | 62 + config/app.ts | 84 + config/bodyparser.ts | 78 + config/cors.ts | 19 + config/database.ts | 8 + config/encryption.ts | 34 + config/hash.ts | 75 + config/inertia.ts | 8 + config/logger.ts | 35 + config/session.ts | 48 + config/shield.ts | 54 + config/static.ts | 17 + config/vite.ts | 28 + database/base_model.ts | 136 + .../1761885935168_create_users_table.ts | 21 + ...768620764696_create_access_tokens_table.ts | 31 + .../001_create_devices_collection.ts | 27 + .../002_create_commands_collection.ts | 20 + ...003_create_discovery_configs_collection.ts | 12 + ..._create_provisioning_configs_collection.ts | 12 + .../005_create_configurations_collection.ts | 19 + ...reate_configuration_versions_collection.ts | 19 + .../007_create_firmware_entries_collection.ts | 13 + .../008_create_migration_routes_collection.ts | 17 + .../009_add_compliance_to_devices.ts | 18 + .../010_drop_legacy_collections.ts | 20 + database/mongo_migrator.ts | 119 + docker-compose.yml | 51 + eslint.config.js | 2 + inertia/app/app.css | 33 + inertia/app/app.ts | 35 + inertia/components/JsonEditor.vue | 75 + inertia/components/TitleBar.vue | 31 + .../configuration/ComplianceBadge.vue | 64 + .../configuration/CriteriaEditor.vue | 177 + .../components/configuration/PolicyBadge.vue | 55 + .../components/configuration/TagsInput.vue | 42 + inertia/css/app.css | 1 + inertia/layouts/MainLayout.vue | 232 + inertia/locales/en.json | 125 + inertia/locales/fr.json | 125 + inertia/pages/Commands.vue | 162 + inertia/pages/CommandsFleet.vue | 118 + inertia/pages/Configurations.vue | 99 + inertia/pages/DeviceDetail.vue | 390 + inertia/pages/Home.vue | 144 + .../configurations/ConfigurationCreate.vue | 182 + .../configurations/ConfigurationDetail.vue | 227 + .../configurations/ConfigurationEdit.vue | 172 + .../configurations/ConfigurationsList.vue | 242 + .../firmware_catalog/FirmwareCatalog.vue | 141 + .../FirmwareCatalogCreate.vue | 95 + .../firmware_catalog/FirmwareCatalogEdit.vue | 104 + package-lock.json | 12303 ++++++++++++++++ package.json | 98 + providers/mongodb_provider.ts | 55 + public/favicon.svg | 5 + resources/views/inertia_layout.edge | 14 + start/env.ts | 45 + start/kernel.ts | 23 + start/routes.ts | 76 + tests/bootstrap.ts | 37 + .../configuration_criteria.property.spec.ts | 168 + .../configuration_crud.property.spec.ts | 195 + .../configuration_delete.property.spec.ts | 122 + .../configuration_engine.property.spec.ts | 480 + .../configuration_overlap.property.spec.ts | 185 + .../configuration_pagination.property.spec.ts | 206 + ...figuration_partial_update.property.spec.ts | 253 + ...nfiguration_tag_filtering.property.spec.ts | 110 + ...iguration_version_history.property.spec.ts | 297 + .../firmware_migration_path.property.spec.ts | 259 + tests/property/glob_match.property.spec.ts | 131 + .../inventory_coverage.property.spec.ts | 118 + .../migration_service.property.spec.ts | 431 + .../provisioning_code.property.spec.ts | 152 + .../property/schedule_window.property.spec.ts | 192 + .../services/configuration_service.spec.ts | 1258 ++ .../services/firmware_catalog_service.spec.ts | 505 + tests/unit/utils/glob_match.spec.ts | 42 + tests/unit/utils/inventory_coverage.spec.ts | 89 + tests/unit/utils/provisioning_code.spec.ts | 119 + tests/unit/utils/schedule_window.spec.ts | 100 + tmp/.gitkeep | 0 tsconfig.json | 8 + vite.config.ts | 26 + 164 files changed, 29127 insertions(+) create mode 100644 .adonisjs/client/data.d.ts create mode 100644 .adonisjs/client/manifest.d.ts create mode 100644 .adonisjs/client/registry/index.ts create mode 100644 .adonisjs/client/registry/schema.d.ts create mode 100644 .adonisjs/client/registry/tree.d.ts create mode 100644 .adonisjs/server/controllers.ts create mode 100644 .adonisjs/server/events.ts create mode 100644 .adonisjs/server/listeners.ts create mode 100644 .adonisjs/server/routes.d.ts create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .env.test create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 Dockerfile create mode 100644 Dockerfile.genieacs-sim create mode 100644 ace.js create mode 100644 adonisrc.ts create mode 100644 app/controllers/commands_controller.ts create mode 100644 app/controllers/configurations_v1_controller.ts create mode 100644 app/controllers/cwmp_controller.ts create mode 100644 app/controllers/devices_controller.ts create mode 100644 app/controllers/firmware_catalog_controller.ts create mode 100644 app/controllers/health_controller.ts create mode 100644 app/controllers/pages_controller.ts create mode 100644 app/domain/commands/command.ts create mode 100644 app/domain/configuration/compliance.ts create mode 100644 app/domain/configuration/configuration.ts create mode 100644 app/domain/configuration/configuration_version.ts create mode 100644 app/domain/device/device.ts create mode 100644 app/domain/device/device_id.ts create mode 100644 app/domain/device/device_parameter.ts create mode 100644 app/domain/device/device_record.ts create mode 100644 app/domain/firmware/firmware_catalog.ts create mode 100644 app/domain/firmware/migration_step.ts create mode 100644 app/domain/shared/mode.ts create mode 100644 app/domain/shared/types.ts create mode 100644 app/exceptions/handler.ts create mode 100644 app/exceptions/index.ts create mode 100644 app/middleware/container_bindings_middleware.ts create mode 100644 app/middleware/cwmp_logger_middleware.ts create mode 100644 app/middleware/cwmp_middleware.ts create mode 100644 app/middleware/inertia_middleware.ts create mode 100644 app/models/command.ts create mode 100644 app/models/configuration.ts create mode 100644 app/models/configuration_version.ts create mode 100644 app/models/device.ts create mode 100644 app/models/firmware_entry.ts create mode 100644 app/models/migration_route.ts create mode 100644 app/protocol/common/cwmp_message_builder.ts create mode 100644 app/protocol/soap.ts create mode 100644 app/services/command_service.ts create mode 100644 app/services/configuration_engine.ts create mode 100644 app/services/configuration_service.ts create mode 100644 app/services/cwmp/action_execution_helper.ts create mode 100644 app/services/cwmp/configuration_evaluation_state.ts create mode 100644 app/services/cwmp/device_action_executor.ts create mode 100644 app/services/cwmp/immediate_command_state_machine.ts create mode 100644 app/services/cwmp/main_state_machine.ts create mode 100644 app/services/cwmp/state_machine_manager.ts create mode 100644 app/services/cwmp/task_queue.ts create mode 100644 app/services/device_service.ts create mode 100644 app/services/firmware_catalog_service.ts create mode 100644 app/services/migration_service.ts create mode 100644 app/services/telemetry.ts create mode 100644 app/types.ts create mode 100644 app/utils/device_utils.ts create mode 100644 app/utils/glob_match.ts create mode 100644 app/utils/inventory_coverage.ts create mode 100644 app/utils/provisioning_code.ts create mode 100644 app/utils/schedule_window.ts create mode 100644 app/validators/command.ts create mode 100644 app/validators/configuration.ts create mode 100644 app/validators/device.ts create mode 100644 app/validators/firmware_catalog.ts create mode 100644 bin/console.ts create mode 100644 bin/server.ts create mode 100644 bin/test.ts create mode 100644 config/app.ts create mode 100644 config/bodyparser.ts create mode 100644 config/cors.ts create mode 100644 config/database.ts create mode 100644 config/encryption.ts create mode 100644 config/hash.ts create mode 100644 config/inertia.ts create mode 100644 config/logger.ts create mode 100644 config/session.ts create mode 100644 config/shield.ts create mode 100644 config/static.ts create mode 100644 config/vite.ts create mode 100644 database/base_model.ts create mode 100644 database/migrations/1761885935168_create_users_table.ts create mode 100644 database/migrations/1768620764696_create_access_tokens_table.ts create mode 100644 database/mongo_migrations/001_create_devices_collection.ts create mode 100644 database/mongo_migrations/002_create_commands_collection.ts create mode 100644 database/mongo_migrations/003_create_discovery_configs_collection.ts create mode 100644 database/mongo_migrations/004_create_provisioning_configs_collection.ts create mode 100644 database/mongo_migrations/005_create_configurations_collection.ts create mode 100644 database/mongo_migrations/006_create_configuration_versions_collection.ts create mode 100644 database/mongo_migrations/007_create_firmware_entries_collection.ts create mode 100644 database/mongo_migrations/008_create_migration_routes_collection.ts create mode 100644 database/mongo_migrations/009_add_compliance_to_devices.ts create mode 100644 database/mongo_migrations/010_drop_legacy_collections.ts create mode 100644 database/mongo_migrator.ts create mode 100644 docker-compose.yml create mode 100644 eslint.config.js create mode 100644 inertia/app/app.css create mode 100644 inertia/app/app.ts create mode 100644 inertia/components/JsonEditor.vue create mode 100644 inertia/components/TitleBar.vue create mode 100644 inertia/components/configuration/ComplianceBadge.vue create mode 100644 inertia/components/configuration/CriteriaEditor.vue create mode 100644 inertia/components/configuration/PolicyBadge.vue create mode 100644 inertia/components/configuration/TagsInput.vue create mode 100644 inertia/css/app.css create mode 100644 inertia/layouts/MainLayout.vue create mode 100644 inertia/locales/en.json create mode 100644 inertia/locales/fr.json create mode 100644 inertia/pages/Commands.vue create mode 100644 inertia/pages/CommandsFleet.vue create mode 100644 inertia/pages/Configurations.vue create mode 100644 inertia/pages/DeviceDetail.vue create mode 100644 inertia/pages/Home.vue create mode 100644 inertia/pages/configurations/ConfigurationCreate.vue create mode 100644 inertia/pages/configurations/ConfigurationDetail.vue create mode 100644 inertia/pages/configurations/ConfigurationEdit.vue create mode 100644 inertia/pages/configurations/ConfigurationsList.vue create mode 100644 inertia/pages/firmware_catalog/FirmwareCatalog.vue create mode 100644 inertia/pages/firmware_catalog/FirmwareCatalogCreate.vue create mode 100644 inertia/pages/firmware_catalog/FirmwareCatalogEdit.vue create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 providers/mongodb_provider.ts create mode 100644 public/favicon.svg create mode 100644 resources/views/inertia_layout.edge create mode 100644 start/env.ts create mode 100644 start/kernel.ts create mode 100644 start/routes.ts create mode 100644 tests/bootstrap.ts create mode 100644 tests/property/configuration_criteria.property.spec.ts create mode 100644 tests/property/configuration_crud.property.spec.ts create mode 100644 tests/property/configuration_delete.property.spec.ts create mode 100644 tests/property/configuration_engine.property.spec.ts create mode 100644 tests/property/configuration_overlap.property.spec.ts create mode 100644 tests/property/configuration_pagination.property.spec.ts create mode 100644 tests/property/configuration_partial_update.property.spec.ts create mode 100644 tests/property/configuration_tag_filtering.property.spec.ts create mode 100644 tests/property/configuration_version_history.property.spec.ts create mode 100644 tests/property/firmware_migration_path.property.spec.ts create mode 100644 tests/property/glob_match.property.spec.ts create mode 100644 tests/property/inventory_coverage.property.spec.ts create mode 100644 tests/property/migration_service.property.spec.ts create mode 100644 tests/property/provisioning_code.property.spec.ts create mode 100644 tests/property/schedule_window.property.spec.ts create mode 100644 tests/unit/services/configuration_service.spec.ts create mode 100644 tests/unit/services/firmware_catalog_service.spec.ts create mode 100644 tests/unit/utils/glob_match.spec.ts create mode 100644 tests/unit/utils/inventory_coverage.spec.ts create mode 100644 tests/unit/utils/provisioning_code.spec.ts create mode 100644 tests/unit/utils/schedule_window.spec.ts create mode 100644 tmp/.gitkeep create mode 100644 tsconfig.json create mode 100644 vite.config.ts 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'], + }, + }, + }, +})