From df3eb45bbfff57b0c63049ab708c019c6ef68bbc Mon Sep 17 00:00:00 2001 From: Dmitri Date: Wed, 1 Jul 2026 15:45:38 +0200 Subject: [PATCH] migration to typescript and ESM modules --- .dockerignore | 8 + .gitignore | 2 + Dockerfile | 17 +- Dockerfile.dev | 22 +- README.md | 32 +- backend/.dockerignore | 7 + backend/.eslintignore | 4 - backend/.eslintrc.cjs | 16 - backend/.sequelizerc | 7 - backend/Dockerfile | 18 +- backend/README.md | 116 +- backend/eslint.config.js | 99 + backend/package-lock.json | 10244 ++++++++++++++++ backend/package.json | 96 +- backend/scripts/check-esm-boundaries.ts | 90 + ...ng.js => check-public-access-hardening.ts} | 66 +- backend/scripts/copy-runtime-assets.ts | 45 + backend/src/ai/LocalAIApi.js | 512 - backend/src/auth/auth.js | 80 - backend/src/auth/auth.ts | 228 + backend/src/auth/passport-middleware.ts | 56 + backend/src/config.js | 102 - backend/src/config.ts | 96 + backend/src/contracts/entity-options.js | 95 - backend/src/contracts/entity-options.ts | 87 + .../db/api/{access_logs.js => access_logs.ts} | 36 +- .../{asset_variants.js => asset_variants.ts} | 36 +- backend/src/db/api/{assets.js => assets.ts} | 46 +- backend/src/db/api/base.api.js | 517 - backend/src/db/api/base.api.ts | 735 ++ ...e_defaults.js => element_type_defaults.ts} | 167 +- backend/src/db/api/file.js | 73 - backend/src/db/api/file.ts | 109 + .../src/db/api/global_transition_defaults.js | 155 - .../src/db/api/global_transition_defaults.ts | 264 + ...aults.js => global_ui_control_defaults.ts} | 116 +- backend/src/db/api/permissions.js | 53 - backend/src/db/api/permissions.ts | 60 + ..._requests.js => presigned_url_requests.ts} | 38 +- backend/src/db/api/project_audio_tracks.js | 199 - backend/src/db/api/project_audio_tracks.ts | 324 + .../src/db/api/project_element_defaults.js | 390 - .../src/db/api/project_element_defaults.ts | 520 + ..._memberships.js => project_memberships.ts} | 38 +- .../src/db/api/project_transition_settings.js | 277 - .../src/db/api/project_transition_settings.ts | 407 + .../src/db/api/project_ui_control_settings.js | 183 - .../src/db/api/project_ui_control_settings.ts | 312 + backend/src/db/api/projects.js | 237 - backend/src/db/api/projects.ts | 351 + .../{publish_events.js => publish_events.ts} | 38 +- .../db/api/{pwa_caches.js => pwa_caches.ts} | 36 +- backend/src/db/api/roles.js | 71 - backend/src/db/api/roles.ts | 84 + backend/src/db/api/runtime-context.js | 57 - backend/src/db/api/runtime-context.ts | 75 + backend/src/db/api/tour_pages.js | 289 - backend/src/db/api/tour_pages.ts | 363 + backend/src/db/api/users.js | 883 -- backend/src/db/api/users.ts | 979 ++ backend/src/db/db-config.ts | 64 + backend/src/db/db.config.js | 37 - ...move-unused-theme-columns-from-projects.js | 1 - ...403000001-add-background-video-settings.js | 1 - ...00001-add-design-dimensions-to-projects.js | 1 - ...0260413091125-add-reversed-variant-type.js | 1 - .../20260430000001-add-transition-settings.js | 1 - ...605000001-add-background-audio-settings.js | 1 - backend/src/db/migrations/package.json | 3 + .../models/{access_logs.js => access_logs.ts} | 19 +- .../{asset_variants.js => asset_variants.ts} | 34 +- .../src/db/models/{assets.js => assets.ts} | 15 +- ...e_defaults.js => element_type_defaults.ts} | 39 +- backend/src/db/models/{file.js => file.ts} | 11 +- ...aults.js => global_transition_defaults.ts} | 18 +- ...aults.js => global_ui_control_defaults.ts} | 18 +- backend/src/db/models/index.js | 46 - backend/src/db/models/index.ts | 1 + backend/src/db/models/loader.ts | 429 + .../models/{permissions.js => permissions.ts} | 15 +- ..._requests.js => presigned_url_requests.ts} | 38 +- ...s.js => production_presentation_access.ts} | 18 +- ...udio_tracks.js => project_audio_tracks.ts} | 22 +- ...efaults.js => project_element_defaults.ts} | 22 +- ..._memberships.js => project_memberships.ts} | 22 +- ...ings.js => project_transition_settings.ts} | 18 +- ...ings.js => project_ui_control_settings.ts} | 18 +- .../db/models/{projects.js => projects.ts} | 18 +- .../{publish_events.js => publish_events.ts} | 22 +- .../models/{pwa_caches.js => pwa_caches.ts} | 19 +- backend/src/db/models/{roles.js => roles.ts} | 15 +- .../models/{tour_pages.js => tour_pages.ts} | 19 +- backend/src/db/models/{users.js => users.ts} | 102 +- backend/src/db/reset.js | 16 - backend/src/db/reset.ts | 19 + .../db/seeders/20200430130759-admin-user.js | 73 - .../db/seeders/20200430130759-admin-user.ts | 92 + ...-roles.js => 20200430130760-user-roles.ts} | 202 +- ...-data.js => 20231127130745-sample-data.ts} | 76 +- backend/src/db/{sync.js => sync.ts} | 8 +- backend/src/db/umzug.ts | 420 + backend/src/db/utils.js | 45 - backend/src/db/utils.ts | 27 + backend/src/factories/router.factory.js | 222 - backend/src/factories/router.factory.ts | 433 + backend/src/factories/service.factory.js | 180 - backend/src/factories/service.factory.ts | 347 + backend/src/helpers.js | 47 - backend/src/helpers.ts | 86 + backend/src/index.js | 344 - backend/src/index.ts | 441 + backend/src/load-env.ts | 19 + backend/src/middlewares/check-permissions.js | 202 - backend/src/middlewares/check-permissions.ts | 224 + .../project-settings-runtime-auth.ts | 172 + backend/src/middlewares/public-read-auth.ts | 28 + .../{rateLimiter.js => rateLimiter.ts} | 34 +- backend/src/middlewares/runtime-context.js | 34 - backend/src/middlewares/runtime-context.ts | 30 + backend/src/middlewares/runtime-public.js | 159 - backend/src/middlewares/runtime-public.ts | 201 + backend/src/middlewares/upload.js | 9 - backend/src/middlewares/upload.ts | 34 + backend/src/middlewares/validate-request.js | 58 - backend/src/middlewares/validate-request.ts | 101 + .../routes/{access_logs.js => access_logs.ts} | 8 +- .../{asset_variants.js => asset_variants.ts} | 8 +- backend/src/routes/{assets.js => assets.ts} | 8 +- backend/src/routes/{auth.js => auth.ts} | 171 +- backend/src/routes/element_type_defaults.js | 12 - backend/src/routes/element_type_defaults.ts | 12 + backend/src/routes/file.js | 119 - backend/src/routes/file.ts | 151 + ...aults.js => global_transition_defaults.ts} | 96 +- .../src/routes/global_ui_control_defaults.js | 67 - .../src/routes/global_ui_control_defaults.ts | 79 + backend/src/routes/openai.js | 337 - .../routes/{permissions.js => permissions.ts} | 8 +- ..._requests.js => presigned_url_requests.ts} | 8 +- ...udio_tracks.js => project_audio_tracks.ts} | 8 +- ...efaults.js => project_element_defaults.ts} | 43 +- ..._memberships.js => project_memberships.ts} | 8 +- .../src/routes/project_transition_settings.js | 478 - .../src/routes/project_transition_settings.ts | 213 + .../src/routes/project_ui_control_settings.js | 203 - .../src/routes/project_ui_control_settings.ts | 126 + backend/src/routes/projects.js | 41 - backend/src/routes/projects.ts | 47 + backend/src/routes/publish.js | 64 - backend/src/routes/publish.ts | 108 + .../{publish_events.js => publish_events.ts} | 8 +- .../routes/{pwa_caches.js => pwa_caches.ts} | 8 +- backend/src/routes/{roles.js => roles.ts} | 8 +- .../{runtime-access.js => runtime-access.ts} | 23 +- backend/src/routes/runtime-context.js | 11 - backend/src/routes/runtime-context.ts | 17 + backend/src/routes/{search.js => search.ts} | 32 +- backend/src/routes/sql.js | 87 - backend/src/routes/sql.ts | 114 + .../routes/{tour_pages.js => tour_pages.ts} | 108 +- backend/src/routes/users.js | 35 - backend/src/routes/users.ts | 64 + ...policy-audit.js => access-policy-audit.ts} | 88 +- backend/src/services/access-policy.js | 146 - backend/src/services/access-policy.ts | 189 + backend/src/services/access_logs.js | 6 - backend/src/services/access_logs.ts | 6 + backend/src/services/asset_variants.js | 6 - backend/src/services/asset_variants.ts | 6 + backend/src/services/{assets.js => assets.ts} | 221 +- backend/src/services/{auth.js => auth.ts} | 117 +- backend/src/services/element_type_defaults.js | 6 - backend/src/services/element_type_defaults.ts | 6 + backend/src/services/email/index.js | 44 - backend/src/services/email/index.ts | 48 + .../email/list/addressVerification.js | 41 - .../email/list/addressVerification.ts | 51 + backend/src/services/email/list/invitation.js | 41 - backend/src/services/email/list/invitation.ts | 52 + .../src/services/email/list/passwordReset.js | 42 - .../src/services/email/list/passwordReset.ts | 51 + backend/src/services/{file.js => file.ts} | 791 +- .../src/services/file/BaseStorageProvider.js | 88 - .../src/services/file/BaseStorageProvider.ts | 47 + .../src/services/file/LocalStorageProvider.js | 250 - .../src/services/file/LocalStorageProvider.ts | 195 + .../src/services/file/S3StorageProvider.js | 523 - .../src/services/file/S3StorageProvider.ts | 481 + ...sionManager.js => UploadSessionManager.ts} | 188 +- backend/src/services/file/index.js | 27 - backend/src/services/file/index.ts | 23 + .../services/global_transition_defaults.js | 6 - .../services/global_transition_defaults.ts | 6 + .../services/global_ui_control_defaults.js | 6 - .../services/global_ui_control_defaults.ts | 6 + .../notifications/errors/forbidden.js | 16 - .../notifications/errors/forbidden.ts | 18 + .../notifications/errors/validation.js | 17 - .../notifications/errors/validation.ts | 24 + backend/src/services/notifications/helpers.js | 30 - backend/src/services/notifications/helpers.ts | 51 + .../notifications/{list.js => list.ts} | 6 +- backend/src/services/openai.js | 85 - backend/src/services/permissions.js | 6 - backend/src/services/permissions.ts | 6 + .../src/services/presigned_url_requests.js | 6 - .../src/services/presigned_url_requests.ts | 6 + backend/src/services/project_audio_tracks.js | 171 - backend/src/services/project_audio_tracks.ts | 295 + .../src/services/project_element_defaults.js | 34 - .../src/services/project_element_defaults.ts | 34 + backend/src/services/project_memberships.js | 6 - backend/src/services/project_memberships.ts | 6 + .../services/project_transition_settings.js | 209 - .../services/project_transition_settings.ts | 342 + .../services/project_ui_control_settings.js | 73 - .../services/project_ui_control_settings.ts | 115 + backend/src/services/projects.js | 631 - backend/src/services/projects.ts | 811 ++ .../src/services/{publish.js => publish.ts} | 289 +- backend/src/services/publish_events.js | 6 - backend/src/services/publish_events.ts | 6 + backend/src/services/pwa_caches.js | 6 - backend/src/services/pwa_caches.ts | 6 + backend/src/services/roles.js | 393 - backend/src/services/roles.ts | 306 + .../services/runtime-presentation-access.js | 80 - .../services/runtime-presentation-access.ts | 103 + backend/src/services/search.js | 185 - backend/src/services/search.ts | 265 + .../services/{tour_pages.js => tour_pages.ts} | 569 +- backend/src/services/users.js | 316 - backend/src/services/users.ts | 434 + ...{videoProcessing.js => videoProcessing.ts} | 122 +- backend/src/types/access-logs-db.ts | 44 + backend/src/types/access-policy-audit.ts | 87 + backend/src/types/access-policy.ts | 27 + backend/src/types/app.ts | 44 + backend/src/types/assets.ts | 168 + backend/src/types/auth-routes.ts | 40 + backend/src/types/auth-service.ts | 65 + backend/src/types/auth.ts | 38 + backend/src/types/config.ts | 69 + backend/src/types/db-api.ts | 147 + backend/src/types/db-config.ts | 23 + backend/src/types/db-models.ts | 593 + backend/src/types/db-seeders.ts | 72 + backend/src/types/element-type-defaults.ts | 113 + backend/src/types/email.ts | 19 + backend/src/types/entity-router.ts | 72 + backend/src/types/entity-service.ts | 69 + backend/src/types/env.ts | 33 + backend/src/types/errors.ts | 7 + backend/src/types/file.ts | 404 + backend/src/types/global-defaults.ts | 99 + backend/src/types/http.ts | 54 + backend/src/types/index.ts | 622 + backend/src/types/notifications.ts | 3 + backend/src/types/pagination.ts | 24 + backend/src/types/permissions.ts | 23 + .../src/types/presigned-url-requests-db.ts | 51 + backend/src/types/project-audio-tracks.ts | 152 + backend/src/types/project-element-defaults.ts | 204 + backend/src/types/project-memberships-db.ts | 45 + backend/src/types/project-settings.ts | 25 + .../src/types/project-transition-settings.ts | 191 + .../src/types/project-ui-control-settings.ts | 113 + backend/src/types/projects.ts | 251 + backend/src/types/publish-events-db.ts | 62 + backend/src/types/publish.ts | 91 + backend/src/types/pwa-caches-db.ts | 47 + backend/src/types/rate-limit.ts | 20 + backend/src/types/request.ts | 7 + backend/src/types/roles.ts | 81 + .../src/types/runtime-presentation-access.ts | 31 + backend/src/types/runtime.ts | 51 + backend/src/types/search.ts | 14 + backend/src/types/sequelize-models.ts | 37 + backend/src/types/service-options.ts | 45 + backend/src/types/sql.ts | 43 + backend/src/types/tour-pages.ts | 374 + backend/src/types/upload.ts | 6 + backend/src/types/users.ts | 368 + backend/src/types/validation.ts | 18 + backend/src/types/video-processing.ts | 8 + backend/src/utils/env-validation.js | 69 - backend/src/utils/env-validation.ts | 169 + backend/src/utils/{errors.js => errors.ts} | 20 +- backend/src/utils/global-defaults.ts | 62 + backend/src/utils/index.js | 5 - backend/src/utils/index.ts | 10 + backend/src/utils/logger.js | 55 - backend/src/utils/logger.ts | 75 + backend/src/utils/project-settings-query.ts | 114 + .../src/utils/project-transition-settings.ts | 45 + .../src/utils/project-ui-control-settings.ts | 24 + backend/src/utils/request-body.ts | 31 + backend/src/utils/request-context.ts | 102 + backend/src/utils/route-context.ts | 22 + .../{sqlValidator.js => sqlValidator.ts} | 26 +- ...{request-schemas.js => request-schemas.ts} | 385 +- ...s-policy.test.js => access-policy.test.ts} | 37 +- backend/tests/check-permissions.test.js | 77 - backend/tests/check-permissions.test.ts | 24 + .../{helpers.test.js => helpers.test.ts} | 12 +- ...s-policy.test.js => access-policy.test.ts} | 172 +- backend/tests/request-validation.test.js | 129 - backend/tests/request-validation.test.ts | 147 + backend/tests/update-contracts.test.js | 486 - backend/tests/update-contracts.test.ts | 582 + backend/tsconfig.json | 40 + backend/{watcher.js => watcher.ts} | 63 +- backend/yarn.lock | 6038 --------- docker/README.md | 14 +- docker/docker-compose.yml | 14 +- docker/start-backend.sh | 4 +- frontend/.dockerignore | 7 + frontend/Dockerfile | 10 +- frontend/README.md | 20 +- frontend/package.json | 43 +- .../components/SmartWidget/SmartWidget.tsx | 101 - .../SmartWidget/components/AreaChart.tsx | 18 - .../components/AreaChart/ApexAreaChart.tsx | 139 - .../components/AreaChart/ChartJSAreaChart.tsx | 100 - .../SmartWidget/components/BarChart.tsx | 18 - .../components/BarChart/ApexBarChart.tsx | 135 - .../components/BarChart/ChartJSBarChart.tsx | 97 - .../SmartWidget/components/FunnelChart.tsx | 138 - .../SmartWidget/components/LineChart.tsx | 18 - .../components/LineChart/ApexLineChart.tsx | 139 - .../components/LineChart/ChartJSLineChart.tsx | 96 - .../SmartWidget/components/PieChart.tsx | 18 - .../components/PieChart/ApexPieChart.tsx | 114 - .../components/PieChart/ChartJSPieChart.tsx | 85 - .../SmartWidget/models/widget.model.ts | 37 - .../components/SmartWidget/widgetHelpers.tsx | 35 - .../components/WidgetCreator/RoleSelect.tsx | 66 - .../WidgetCreator/WidgetCreator.tsx | 154 - frontend/src/config.ts | 2 +- frontend/src/layouts/Authenticated.tsx | 21 +- frontend/src/lib/authToken.ts | 34 + frontend/src/pages/dashboard.tsx | 85 +- frontend/src/stores/authSlice.ts | 16 +- frontend/src/stores/introSteps.ts | 7 - frontend/src/stores/openAiSlice.ts | 157 - frontend/src/stores/roles/rolesSlice.ts | 77 +- frontend/src/stores/store.ts | 2 - frontend/src/sw.ts | 2 +- frontend/src/types/auth.ts | 4 + frontend/src/types/charts.ts | 12 +- frontend/src/types/index.ts | 1 - frontend/src/types/openai.ts | 73 - frontend/yarn.lock | 4515 ------- package.json | 4 +- 354 files changed, 33834 insertions(+), 26093 deletions(-) create mode 100644 backend/.dockerignore delete mode 100644 backend/.eslintignore delete mode 100644 backend/.eslintrc.cjs delete mode 100644 backend/.sequelizerc create mode 100644 backend/eslint.config.js create mode 100644 backend/package-lock.json create mode 100644 backend/scripts/check-esm-boundaries.ts rename backend/scripts/{check-public-access-hardening.js => check-public-access-hardening.ts} (52%) create mode 100644 backend/scripts/copy-runtime-assets.ts delete mode 100644 backend/src/ai/LocalAIApi.js delete mode 100644 backend/src/auth/auth.js create mode 100644 backend/src/auth/auth.ts create mode 100644 backend/src/auth/passport-middleware.ts delete mode 100644 backend/src/config.js create mode 100644 backend/src/config.ts delete mode 100644 backend/src/contracts/entity-options.js create mode 100644 backend/src/contracts/entity-options.ts rename backend/src/db/api/{access_logs.js => access_logs.ts} (58%) rename backend/src/db/api/{asset_variants.js => asset_variants.ts} (54%) rename backend/src/db/api/{assets.js => assets.ts} (60%) delete mode 100644 backend/src/db/api/base.api.js create mode 100644 backend/src/db/api/base.api.ts rename backend/src/db/api/{element_type_defaults.js => element_type_defaults.ts} (70%) delete mode 100644 backend/src/db/api/file.js create mode 100644 backend/src/db/api/file.ts delete mode 100644 backend/src/db/api/global_transition_defaults.js create mode 100644 backend/src/db/api/global_transition_defaults.ts rename backend/src/db/api/{global_ui_control_defaults.js => global_ui_control_defaults.ts} (50%) delete mode 100644 backend/src/db/api/permissions.js create mode 100644 backend/src/db/api/permissions.ts rename backend/src/db/api/{presigned_url_requests.js => presigned_url_requests.ts} (58%) delete mode 100644 backend/src/db/api/project_audio_tracks.js create mode 100644 backend/src/db/api/project_audio_tracks.ts delete mode 100644 backend/src/db/api/project_element_defaults.js create mode 100644 backend/src/db/api/project_element_defaults.ts rename backend/src/db/api/{project_memberships.js => project_memberships.ts} (56%) delete mode 100644 backend/src/db/api/project_transition_settings.js create mode 100644 backend/src/db/api/project_transition_settings.ts delete mode 100644 backend/src/db/api/project_ui_control_settings.js create mode 100644 backend/src/db/api/project_ui_control_settings.ts delete mode 100644 backend/src/db/api/projects.js create mode 100644 backend/src/db/api/projects.ts rename backend/src/db/api/{publish_events.js => publish_events.ts} (64%) rename backend/src/db/api/{pwa_caches.js => pwa_caches.ts} (54%) delete mode 100644 backend/src/db/api/roles.js create mode 100644 backend/src/db/api/roles.ts delete mode 100644 backend/src/db/api/runtime-context.js create mode 100644 backend/src/db/api/runtime-context.ts delete mode 100644 backend/src/db/api/tour_pages.js create mode 100644 backend/src/db/api/tour_pages.ts delete mode 100644 backend/src/db/api/users.js create mode 100644 backend/src/db/api/users.ts create mode 100644 backend/src/db/db-config.ts delete mode 100644 backend/src/db/db.config.js create mode 100644 backend/src/db/migrations/package.json rename backend/src/db/models/{access_logs.js => access_logs.ts} (85%) rename backend/src/db/models/{asset_variants.js => asset_variants.ts} (75%) rename backend/src/db/models/{assets.js => assets.ts} (92%) rename backend/src/db/models/{element_type_defaults.js => element_type_defaults.ts} (72%) rename backend/src/db/models/{file.js => file.ts} (80%) rename backend/src/db/models/{global_transition_defaults.js => global_transition_defaults.ts} (80%) rename backend/src/db/models/{global_ui_control_defaults.js => global_ui_control_defaults.ts} (58%) delete mode 100644 backend/src/db/models/index.js create mode 100644 backend/src/db/models/index.ts create mode 100644 backend/src/db/models/loader.ts rename backend/src/db/models/{permissions.js => permissions.ts} (77%) rename backend/src/db/models/{presigned_url_requests.js => presigned_url_requests.ts} (75%) rename backend/src/db/models/{production_presentation_access.js => production_presentation_access.ts} (73%) rename backend/src/db/models/{project_audio_tracks.js => project_audio_tracks.ts} (82%) rename backend/src/db/models/{project_element_defaults.js => project_element_defaults.ts} (81%) rename backend/src/db/models/{project_memberships.js => project_memberships.ts} (81%) rename backend/src/db/models/{project_transition_settings.js => project_transition_settings.ts} (84%) rename backend/src/db/models/{project_ui_control_settings.js => project_ui_control_settings.ts} (75%) rename backend/src/db/models/{projects.js => projects.ts} (92%) rename backend/src/db/models/{publish_events.js => publish_events.ts} (89%) rename backend/src/db/models/{pwa_caches.js => pwa_caches.ts} (81%) rename backend/src/db/models/{roles.js => roles.ts} (86%) rename backend/src/db/models/{tour_pages.js => tour_pages.ts} (92%) rename backend/src/db/models/{users.js => users.ts} (70%) delete mode 100644 backend/src/db/reset.js create mode 100644 backend/src/db/reset.ts delete mode 100644 backend/src/db/seeders/20200430130759-admin-user.js create mode 100644 backend/src/db/seeders/20200430130759-admin-user.ts rename backend/src/db/seeders/{20200430130760-user-roles.js => 20200430130760-user-roles.ts} (90%) rename backend/src/db/seeders/{20231127130745-sample-data.js => 20231127130745-sample-data.ts} (92%) rename backend/src/db/{sync.js => sync.ts} (69%) create mode 100644 backend/src/db/umzug.ts delete mode 100644 backend/src/db/utils.js create mode 100644 backend/src/db/utils.ts delete mode 100644 backend/src/factories/router.factory.js create mode 100644 backend/src/factories/router.factory.ts delete mode 100644 backend/src/factories/service.factory.js create mode 100644 backend/src/factories/service.factory.ts delete mode 100644 backend/src/helpers.js create mode 100644 backend/src/helpers.ts delete mode 100644 backend/src/index.js create mode 100644 backend/src/index.ts create mode 100644 backend/src/load-env.ts delete mode 100644 backend/src/middlewares/check-permissions.js create mode 100644 backend/src/middlewares/check-permissions.ts create mode 100644 backend/src/middlewares/project-settings-runtime-auth.ts create mode 100644 backend/src/middlewares/public-read-auth.ts rename backend/src/middlewares/{rateLimiter.js => rateLimiter.ts} (90%) delete mode 100644 backend/src/middlewares/runtime-context.js create mode 100644 backend/src/middlewares/runtime-context.ts delete mode 100644 backend/src/middlewares/runtime-public.js create mode 100644 backend/src/middlewares/runtime-public.ts delete mode 100644 backend/src/middlewares/upload.js create mode 100644 backend/src/middlewares/upload.ts delete mode 100644 backend/src/middlewares/validate-request.js create mode 100644 backend/src/middlewares/validate-request.ts rename backend/src/routes/{access_logs.js => access_logs.ts} (93%) rename backend/src/routes/{asset_variants.js => asset_variants.ts} (93%) rename backend/src/routes/{assets.js => assets.ts} (93%) rename backend/src/routes/{auth.js => auth.ts} (57%) delete mode 100644 backend/src/routes/element_type_defaults.js create mode 100644 backend/src/routes/element_type_defaults.ts delete mode 100644 backend/src/routes/file.js create mode 100644 backend/src/routes/file.ts rename backend/src/routes/{global_transition_defaults.js => global_transition_defaults.ts} (56%) delete mode 100644 backend/src/routes/global_ui_control_defaults.js create mode 100644 backend/src/routes/global_ui_control_defaults.ts delete mode 100644 backend/src/routes/openai.js rename backend/src/routes/{permissions.js => permissions.ts} (95%) rename backend/src/routes/{presigned_url_requests.js => presigned_url_requests.ts} (93%) rename backend/src/routes/{project_audio_tracks.js => project_audio_tracks.ts} (93%) rename backend/src/routes/{project_element_defaults.js => project_element_defaults.ts} (57%) rename backend/src/routes/{project_memberships.js => project_memberships.ts} (93%) delete mode 100644 backend/src/routes/project_transition_settings.js create mode 100644 backend/src/routes/project_transition_settings.ts delete mode 100644 backend/src/routes/project_ui_control_settings.js create mode 100644 backend/src/routes/project_ui_control_settings.ts delete mode 100644 backend/src/routes/projects.js create mode 100644 backend/src/routes/projects.ts delete mode 100644 backend/src/routes/publish.js create mode 100644 backend/src/routes/publish.ts rename backend/src/routes/{publish_events.js => publish_events.ts} (94%) rename backend/src/routes/{pwa_caches.js => pwa_caches.ts} (93%) rename backend/src/routes/{roles.js => roles.ts} (93%) rename backend/src/routes/{runtime-access.js => runtime-access.ts} (70%) delete mode 100644 backend/src/routes/runtime-context.js create mode 100644 backend/src/routes/runtime-context.ts rename backend/src/routes/{search.js => search.ts} (54%) delete mode 100644 backend/src/routes/sql.js create mode 100644 backend/src/routes/sql.ts rename backend/src/routes/{tour_pages.js => tour_pages.ts} (77%) delete mode 100644 backend/src/routes/users.js create mode 100644 backend/src/routes/users.ts rename backend/src/services/{access-policy-audit.js => access-policy-audit.ts} (62%) delete mode 100644 backend/src/services/access-policy.js create mode 100644 backend/src/services/access-policy.ts delete mode 100644 backend/src/services/access_logs.js create mode 100644 backend/src/services/access_logs.ts delete mode 100644 backend/src/services/asset_variants.js create mode 100644 backend/src/services/asset_variants.ts rename backend/src/services/{assets.js => assets.ts} (51%) rename backend/src/services/{auth.js => auth.ts} (55%) delete mode 100644 backend/src/services/element_type_defaults.js create mode 100644 backend/src/services/element_type_defaults.ts delete mode 100644 backend/src/services/email/index.js create mode 100644 backend/src/services/email/index.ts delete mode 100644 backend/src/services/email/list/addressVerification.js create mode 100644 backend/src/services/email/list/addressVerification.ts delete mode 100644 backend/src/services/email/list/invitation.js create mode 100644 backend/src/services/email/list/invitation.ts delete mode 100644 backend/src/services/email/list/passwordReset.js create mode 100644 backend/src/services/email/list/passwordReset.ts rename backend/src/services/{file.js => file.ts} (65%) delete mode 100644 backend/src/services/file/BaseStorageProvider.js create mode 100644 backend/src/services/file/BaseStorageProvider.ts delete mode 100644 backend/src/services/file/LocalStorageProvider.js create mode 100644 backend/src/services/file/LocalStorageProvider.ts delete mode 100644 backend/src/services/file/S3StorageProvider.js create mode 100644 backend/src/services/file/S3StorageProvider.ts rename backend/src/services/file/{UploadSessionManager.js => UploadSessionManager.ts} (55%) delete mode 100644 backend/src/services/file/index.js create mode 100644 backend/src/services/file/index.ts delete mode 100644 backend/src/services/global_transition_defaults.js create mode 100644 backend/src/services/global_transition_defaults.ts delete mode 100644 backend/src/services/global_ui_control_defaults.js create mode 100644 backend/src/services/global_ui_control_defaults.ts delete mode 100644 backend/src/services/notifications/errors/forbidden.js create mode 100644 backend/src/services/notifications/errors/forbidden.ts delete mode 100644 backend/src/services/notifications/errors/validation.js create mode 100644 backend/src/services/notifications/errors/validation.ts delete mode 100644 backend/src/services/notifications/helpers.js create mode 100644 backend/src/services/notifications/helpers.ts rename backend/src/services/notifications/{list.js => list.ts} (95%) delete mode 100644 backend/src/services/openai.js delete mode 100644 backend/src/services/permissions.js create mode 100644 backend/src/services/permissions.ts delete mode 100644 backend/src/services/presigned_url_requests.js create mode 100644 backend/src/services/presigned_url_requests.ts delete mode 100644 backend/src/services/project_audio_tracks.js create mode 100644 backend/src/services/project_audio_tracks.ts delete mode 100644 backend/src/services/project_element_defaults.js create mode 100644 backend/src/services/project_element_defaults.ts delete mode 100644 backend/src/services/project_memberships.js create mode 100644 backend/src/services/project_memberships.ts delete mode 100644 backend/src/services/project_transition_settings.js create mode 100644 backend/src/services/project_transition_settings.ts delete mode 100644 backend/src/services/project_ui_control_settings.js create mode 100644 backend/src/services/project_ui_control_settings.ts delete mode 100644 backend/src/services/projects.js create mode 100644 backend/src/services/projects.ts rename backend/src/services/{publish.js => publish.ts} (54%) delete mode 100644 backend/src/services/publish_events.js create mode 100644 backend/src/services/publish_events.ts delete mode 100644 backend/src/services/pwa_caches.js create mode 100644 backend/src/services/pwa_caches.ts delete mode 100644 backend/src/services/roles.js create mode 100644 backend/src/services/roles.ts delete mode 100644 backend/src/services/runtime-presentation-access.js create mode 100644 backend/src/services/runtime-presentation-access.ts delete mode 100644 backend/src/services/search.js create mode 100644 backend/src/services/search.ts rename backend/src/services/{tour_pages.js => tour_pages.ts} (69%) delete mode 100644 backend/src/services/users.js create mode 100644 backend/src/services/users.ts rename backend/src/services/{videoProcessing.js => videoProcessing.ts} (64%) create mode 100644 backend/src/types/access-logs-db.ts create mode 100644 backend/src/types/access-policy-audit.ts create mode 100644 backend/src/types/access-policy.ts create mode 100644 backend/src/types/app.ts create mode 100644 backend/src/types/assets.ts create mode 100644 backend/src/types/auth-routes.ts create mode 100644 backend/src/types/auth-service.ts create mode 100644 backend/src/types/auth.ts create mode 100644 backend/src/types/config.ts create mode 100644 backend/src/types/db-api.ts create mode 100644 backend/src/types/db-config.ts create mode 100644 backend/src/types/db-models.ts create mode 100644 backend/src/types/db-seeders.ts create mode 100644 backend/src/types/element-type-defaults.ts create mode 100644 backend/src/types/email.ts create mode 100644 backend/src/types/entity-router.ts create mode 100644 backend/src/types/entity-service.ts create mode 100644 backend/src/types/env.ts create mode 100644 backend/src/types/errors.ts create mode 100644 backend/src/types/file.ts create mode 100644 backend/src/types/global-defaults.ts create mode 100644 backend/src/types/http.ts create mode 100644 backend/src/types/index.ts create mode 100644 backend/src/types/notifications.ts create mode 100644 backend/src/types/pagination.ts create mode 100644 backend/src/types/permissions.ts create mode 100644 backend/src/types/presigned-url-requests-db.ts create mode 100644 backend/src/types/project-audio-tracks.ts create mode 100644 backend/src/types/project-element-defaults.ts create mode 100644 backend/src/types/project-memberships-db.ts create mode 100644 backend/src/types/project-settings.ts create mode 100644 backend/src/types/project-transition-settings.ts create mode 100644 backend/src/types/project-ui-control-settings.ts create mode 100644 backend/src/types/projects.ts create mode 100644 backend/src/types/publish-events-db.ts create mode 100644 backend/src/types/publish.ts create mode 100644 backend/src/types/pwa-caches-db.ts create mode 100644 backend/src/types/rate-limit.ts create mode 100644 backend/src/types/request.ts create mode 100644 backend/src/types/roles.ts create mode 100644 backend/src/types/runtime-presentation-access.ts create mode 100644 backend/src/types/runtime.ts create mode 100644 backend/src/types/search.ts create mode 100644 backend/src/types/sequelize-models.ts create mode 100644 backend/src/types/service-options.ts create mode 100644 backend/src/types/sql.ts create mode 100644 backend/src/types/tour-pages.ts create mode 100644 backend/src/types/upload.ts create mode 100644 backend/src/types/users.ts create mode 100644 backend/src/types/validation.ts create mode 100644 backend/src/types/video-processing.ts delete mode 100644 backend/src/utils/env-validation.js create mode 100644 backend/src/utils/env-validation.ts rename backend/src/utils/{errors.js => errors.ts} (75%) create mode 100644 backend/src/utils/global-defaults.ts delete mode 100644 backend/src/utils/index.js create mode 100644 backend/src/utils/index.ts delete mode 100644 backend/src/utils/logger.js create mode 100644 backend/src/utils/logger.ts create mode 100644 backend/src/utils/project-settings-query.ts create mode 100644 backend/src/utils/project-transition-settings.ts create mode 100644 backend/src/utils/project-ui-control-settings.ts create mode 100644 backend/src/utils/request-body.ts create mode 100644 backend/src/utils/request-context.ts create mode 100644 backend/src/utils/route-context.ts rename backend/src/utils/{sqlValidator.js => sqlValidator.ts} (59%) rename backend/src/validators/{request-schemas.js => request-schemas.ts} (50%) rename backend/tests/{access-policy.test.js => access-policy.test.ts} (55%) delete mode 100644 backend/tests/check-permissions.test.js create mode 100644 backend/tests/check-permissions.test.ts rename backend/tests/{helpers.test.js => helpers.test.ts} (59%) rename backend/tests/integration/{access-policy.test.js => access-policy.test.ts} (58%) delete mode 100644 backend/tests/request-validation.test.js create mode 100644 backend/tests/request-validation.test.ts delete mode 100644 backend/tests/update-contracts.test.js create mode 100644 backend/tests/update-contracts.test.ts create mode 100644 backend/tsconfig.json rename backend/{watcher.js => watcher.ts} (52%) delete mode 100644 backend/yarn.lock create mode 100644 frontend/.dockerignore delete mode 100644 frontend/src/components/SmartWidget/SmartWidget.tsx delete mode 100644 frontend/src/components/SmartWidget/components/AreaChart.tsx delete mode 100644 frontend/src/components/SmartWidget/components/AreaChart/ApexAreaChart.tsx delete mode 100644 frontend/src/components/SmartWidget/components/AreaChart/ChartJSAreaChart.tsx delete mode 100644 frontend/src/components/SmartWidget/components/BarChart.tsx delete mode 100644 frontend/src/components/SmartWidget/components/BarChart/ApexBarChart.tsx delete mode 100644 frontend/src/components/SmartWidget/components/BarChart/ChartJSBarChart.tsx delete mode 100644 frontend/src/components/SmartWidget/components/FunnelChart.tsx delete mode 100644 frontend/src/components/SmartWidget/components/LineChart.tsx delete mode 100644 frontend/src/components/SmartWidget/components/LineChart/ApexLineChart.tsx delete mode 100644 frontend/src/components/SmartWidget/components/LineChart/ChartJSLineChart.tsx delete mode 100644 frontend/src/components/SmartWidget/components/PieChart.tsx delete mode 100644 frontend/src/components/SmartWidget/components/PieChart/ApexPieChart.tsx delete mode 100644 frontend/src/components/SmartWidget/components/PieChart/ChartJSPieChart.tsx delete mode 100644 frontend/src/components/SmartWidget/models/widget.model.ts delete mode 100644 frontend/src/components/SmartWidget/widgetHelpers.tsx delete mode 100644 frontend/src/components/WidgetCreator/RoleSelect.tsx delete mode 100644 frontend/src/components/WidgetCreator/WidgetCreator.tsx create mode 100644 frontend/src/lib/authToken.ts delete mode 100644 frontend/src/stores/openAiSlice.ts create mode 100644 frontend/src/types/auth.ts delete mode 100644 frontend/src/types/openai.ts delete mode 100644 frontend/yarn.lock diff --git a/.dockerignore b/.dockerignore index 2c83cc6..ef133d6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,11 @@ +**/.DS_Store +.git +docker/data +backend/.env +backend/dist backend/node_modules +frontend/.env.local +frontend/.next +frontend/tsconfig.tsbuildinfo frontend/node_modules frontend/build diff --git a/.gitignore b/.gitignore index 76b080e..43a45fe 100644 --- a/.gitignore +++ b/.gitignore @@ -4,10 +4,12 @@ node_modules/ */node_modules/ **/node_modules/ */build/ +backend/dist/ frontend/.next/ frontend/out/ frontend/public/sw.js frontend/next-env.d.ts package-lock.json +!backend/package-lock.json AGENTS.md .codex/ diff --git a/Dockerfile b/Dockerfile index 2130300..372dc4b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,21 @@ -FROM node:20.15.1-alpine AS builder +FROM node:24-alpine AS builder RUN apk add --no-cache git WORKDIR /app -COPY frontend/package.json frontend/yarn.lock ./ -RUN yarn install --pure-lockfile +COPY frontend/package*.json ./ +RUN npm ci COPY frontend . -RUN yarn build +RUN npm run build -FROM node:20.15.1-alpine +FROM node:24-alpine # FFmpeg is bundled via npm package ffmpeg-static WORKDIR /app -COPY backend/package.json backend/yarn.lock ./ -RUN yarn install --pure-lockfile +COPY backend/package*.json ./ +RUN npm ci COPY backend . COPY --from=builder /app/build /app/public -CMD ["yarn", "start"] - +CMD ["npm", "run", "start"] diff --git a/Dockerfile.dev b/Dockerfile.dev index 1e4dd1c..b98162c 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,22 +1,21 @@ # Base image for Node.js dependencies -FROM node:20.15.1-alpine AS frontend-deps +FROM node:24-alpine AS frontend-deps RUN apk add --no-cache git WORKDIR /app/frontend -COPY frontend/package.json frontend/yarn.lock ./ -RUN yarn install --pure-lockfile +COPY frontend/package*.json ./ +RUN npm ci -FROM node:20.15.1-alpine AS backend-deps +FROM node:24-alpine AS backend-deps RUN apk add --no-cache git WORKDIR /app/backend -COPY backend/package.json backend/yarn.lock ./ -RUN yarn install --pure-lockfile +COPY backend/package*.json ./ +RUN npm ci # Nginx setup and application build -FROM node:20.15.1-alpine AS build +FROM node:24-alpine AS build RUN apk add --no-cache git nginx curl RUN apk add --no-cache lsof procps # FFmpeg is bundled via npm package ffmpeg-static -RUN yarn global add concurrently RUN apk add --no-cache \ chromium \ @@ -31,9 +30,6 @@ ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser RUN mkdir -p /app/pids -# Make sure to add yarn global bin to PATH -ENV PATH /root/.yarn/bin:/root/.config/yarn/global/node_modules/.bin:$PATH - # Copy dependencies WORKDIR /app COPY --from=frontend-deps /app/frontend /app/frontend @@ -63,8 +59,8 @@ ENV FRONT_PORT=3001 ENV BACKEND_PORT=3000 CMD ["sh", "-c", "\ - yarn --cwd /app/frontend dev & echo $! > /app/pids/frontend.pid && \ - yarn --cwd /app/backend start & echo $! > /app/pids/backend.pid && \ + npm --prefix /app/frontend run dev & echo $! > /app/pids/frontend.pid && \ + npm --prefix /app/backend run start & echo $! > /app/pids/backend.pid && \ sleep 10 && nginx -g 'daemon off;' & \ NGINX_PID=$! && \ echo 'Waiting for backend (port 3000) to be available...' && \ diff --git a/README.md b/README.md index d9a1869..d8dbe87 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,9 @@ A web application for building and managing interactive virtual tours with drag- ### Prerequisites -- Node.js 18+ +- Node.js 24 LTS for backend - PostgreSQL 14+ -- Yarn (backend) / npm (frontend) +- npm for backend; frontend uses its existing package scripts ### Database Setup (First Time) @@ -46,11 +46,11 @@ PGPASSWORD='postgres' psql -U postgres -c "CREATE DATABASE app_39215 OWNER app_3 ```bash cd backend -yarn install +npm install npm run start-dev ``` -Backend runs on **http://localhost:8080** +Backend runs on **http://localhost:3000** ### Start Frontend (Terminal 2) @@ -60,7 +60,7 @@ npm install npm run dev ``` -Frontend runs on **http://localhost:3000** +Frontend runs on **http://localhost:3001** ### Login @@ -155,7 +155,7 @@ Pages have an `environment` field (`dev`, `stage`, `production`) that determines ## API Overview -Base URL: `http://localhost:8080/api` +Base URL: `http://localhost:3000/api` | Endpoint | Description | |----------|-------------| @@ -168,7 +168,7 @@ Base URL: `http://localhost:8080/api` | `GET /assets` | List assets | | `POST /file/presign` | Get S3 presigned URLs for asset download (public) | -Full API documentation: `http://localhost:8080/api-docs` (Swagger) +Full API documentation: `http://localhost:3000/api-docs` (Swagger) ## Docker Setup @@ -224,7 +224,7 @@ EMAIL_PASS=... ### Frontend (`frontend/.env.local`) ```env -NEXT_PUBLIC_BACK_API=http://localhost:8080/api +NEXT_PUBLIC_BACK_API=http://localhost:3000/api ``` ## Common Commands @@ -233,11 +233,13 @@ NEXT_PUBLIC_BACK_API=http://localhost:8080/api ```bash cd backend -yarn start # Start server (migrate + seed + watch) -yarn db:migrate # Run migrations -yarn db:seed # Seed data -yarn db:reset # Drop + create + migrate + seed -yarn lint # ESLint +npm run start # Start server (migrate + seed + watch) +npm run db:migrate # Run migrations +npm run db:seed # Seed data +npm run db:reset # Drop + create + migrate + seed +npm run lint # ESLint +npm run typecheck # Strict TypeScript check for migrated backend scope +npm run build # Compile migrated TypeScript files ``` ### Frontend @@ -255,7 +257,7 @@ npm run format # Prettier ### Connection Refused 1. Ensure PostgreSQL is running -2. Check that port 5432 (db), 8080 (backend), 3000 (frontend) are available +2. Check that port 5432 (db), 3000 (backend), 3001 (frontend) are available 3. Verify database credentials in `.env` ### Database Issues @@ -263,7 +265,7 @@ npm run format # Prettier ```bash # Reset database completely cd backend -yarn db:reset +npm run db:reset ``` ### Permission Denied diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..3e6e749 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +tmp +logs +.env +.DS_Store +npm-debug.log* diff --git a/backend/.eslintignore b/backend/.eslintignore deleted file mode 100644 index 3fabb75..0000000 --- a/backend/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore generated and runtime files -node_modules/ -tmp/ -logs/ diff --git a/backend/.eslintrc.cjs b/backend/.eslintrc.cjs deleted file mode 100644 index dc8e0db..0000000 --- a/backend/.eslintrc.cjs +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = { - env: { - node: true, - es2021: true - }, - extends: [ - 'eslint:recommended' - ], - plugins: [ - 'import' - ], - rules: { - 'import/no-unresolved': 'error', - 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }] - } -}; diff --git a/backend/.sequelizerc b/backend/.sequelizerc deleted file mode 100644 index fe89188..0000000 --- a/backend/.sequelizerc +++ /dev/null @@ -1,7 +0,0 @@ -const path = require('path'); -module.exports = { - "config": path.resolve("src", "db", "db.config.js"), - "models-path": path.resolve("src", "db", "models"), - "seeders-path": path.resolve("src", "db", "seeders"), - "migrations-path": path.resolve("src", "db", "migrations") -}; \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index be94ae1..e3a19cc 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,25 +1,21 @@ -FROM node:20.15.1-alpine +FROM node:24-alpine -# Install bash and FFmpeg for video processing (reversed video generation) -RUN apk update && apk add --no-cache bash ffmpeg +# Bash is required by docker/wait-for-it.sh when this image is used by docker-compose. +# FFmpeg is bundled by ffmpeg-static/ffprobe-static npm packages. +RUN apk add --no-cache bash # Create app directory WORKDIR /usr/src/app # Install app dependencies -# A wildcard is used to ensure both package.json AND package-lock.json are copied -# where available (npm@5+) COPY package*.json ./ -RUN yarn install -# If you are building your code for production -# RUN npm ci --only=production - +RUN npm ci # Bundle app source COPY . . -EXPOSE 8080 +EXPOSE 3000 -CMD [ "yarn", "start" ] +CMD [ "npm", "run", "start" ] diff --git a/backend/README.md b/backend/README.md index 5436c70..0227a86 100644 --- a/backend/README.md +++ b/backend/README.md @@ -4,7 +4,7 @@ Node.js/Express REST API server with Sequelize ORM for the Tour Builder Platform ## Tech Stack -- **Runtime**: Node.js 18+ +- **Runtime**: Node.js 24 LTS - **Framework**: Express 4.x - **Database**: PostgreSQL with Sequelize ORM - **Authentication**: Passport.js (JWT, Google OAuth, Microsoft OAuth) @@ -14,35 +14,39 @@ Node.js/Express REST API server with Sequelize ORM for the Tour Builder Platform ## Prerequisites -- Node.js 18+ +- Node.js 24 LTS - PostgreSQL 14+ -- Yarn package manager +- npm package manager ## Quick Start ```bash # Install dependencies -yarn install +npm install # Create database (first time only) -yarn db:create +npm run db:create # Start server (runs migrations, seeds, and watches for changes) npm run start-dev ``` -The server runs on **port 8080** by default. +The server runs on **port 3000** by default. ## Checks ```bash npm run lint +npm run typecheck +npm run build npm run test npm run test:integration npm run check:public-access ``` - `npm run test` runs fast unit tests without requiring PostgreSQL. +- `npm run typecheck` checks the migrated TypeScript scope with `strict: true`. +- `npm run build` compiles the migrated TypeScript scope into `dist/`. - `npm run test:integration` runs rollback-based PostgreSQL integration tests when a valid database configuration is available; otherwise the tests skip. - `npm run check:public-access` audits stale Public role/user permissions and @@ -88,63 +92,65 @@ MS_CLIENT_SECRET=your-client-secret EMAIL_USER=ses-smtp-user EMAIL_PASS=ses-smtp-password -# OpenAI (optional) -GPT_KEY=your-openai-key ``` ## Project Structure ``` backend/src/ -├── index.js # Express app entry point -├── config.js # Environment configuration -├── helpers.js # Utility functions (wrapAsync) +├── index.ts # Express app entry point +├── config.ts # Environment configuration +├── load-env.ts # Central .env bootstrap for app and DB entrypoints +├── helpers.ts # Utility functions (wrapAsync) +├── types/ # Shared TypeScript contracts │ ├── auth/ # Passport.js authentication strategies -│ └── auth.js # JWT, Google, Microsoft strategies +│ └── auth.ts # JWT, Google, Microsoft strategies │ ├── db/ -│ ├── db.config.js # Database connection config (per environment) -│ ├── models/ # Sequelize model definitions (16 models) -│ ├── api/ # Database access layer (CRUD per model) -│ ├── migrations/ # Database migrations -│ └── seeders/ # Seed data (admin users, permissions, roles) +│ ├── db-config.ts # Typed database connection config +│ ├── umzug.ts # Typed Umzug runner for migrations and seeders +│ ├── models/ # Sequelize model definitions +│ ├── api/ # Database access layer +│ ├── migrations/ # Applied migration history; do not rewrite +│ └── seeders/ # Typed seed data files │ -├── routes/ # Express route handlers (22 routes) -│ ├── auth.js # Authentication endpoints -│ ├── projects.js # Project CRUD -│ ├── tour_pages.js # Tour page management -│ ├── assets.js # Asset management -│ ├── file.js # File upload/download, presigned URLs -│ ├── publish.js # Publishing workflow -│ ├── search.js # Global search +├── routes/ # Express route handlers +│ ├── auth.ts # Authentication endpoints +│ ├── projects.ts # Project CRUD +│ ├── tour_pages.ts # Tour page management +│ ├── assets.ts # Asset management +│ ├── file.ts # File upload/download, presigned URLs +│ ├── publish.ts # Publishing workflow +│ ├── search.ts # Global search │ └── ... # Other entity routes │ -├── services/ # Business logic layer (21 services) -│ ├── auth.js # Auth service (JWT, OAuth) -│ ├── publish.js # Publishing workflow logic -│ ├── file.js # File storage abstraction -│ ├── search.js # Global search service +├── services/ # Business logic layer +│ ├── auth.ts # Auth service (JWT, OAuth) +│ ├── publish.ts # Publishing workflow logic +│ ├── file.ts # File storage abstraction +│ ├── search.ts # Global search service │ ├── email/ # Email templates and sending │ ├── notifications/ # Error classes and i18n messages │ └── ... # Other entity services │ ├── middlewares/ -│ ├── check-permissions.js # RBAC permission checking -│ ├── runtime-context.js # Environment detection from headers -│ ├── runtime-public.js # Public runtime access (no auth) -│ ├── upload.js # File upload handling (multer) -│ └── rateLimiter.js # Rate limiting for API endpoints +│ ├── check-permissions.ts # RBAC permission checking +│ ├── runtime-context.ts # Environment detection from headers +│ ├── runtime-public.ts # Public runtime access (no auth) +│ ├── upload.ts # File upload handling (multer) +│ └── rateLimiter.ts # Rate limiting for API endpoints │ ├── factories/ -│ ├── router.factory.js # Generate CRUD routes -│ └── service.factory.js # Generate service classes +│ ├── router.factory.ts # Generate CRUD routes +│ └── service.factory.ts # Generate service classes │ └── utils/ - ├── env-validation.js # Environment variable validation (Joi) - ├── errors.js # Custom error classes - ├── logger.js # Pino logger configuration - └── index.js # Utils barrel export + ├── env-validation.ts # Environment variable validation (Joi) + ├── errors.ts # Custom error classes + ├── logger.ts # Pino logger configuration + ├── request-context.ts # Request-scoped currentUser/runtime/log storage + └── index.ts # Utils barrel export ``` ## Database Setup @@ -169,22 +175,24 @@ GRANT ALL PRIVILEGES ON DATABASE app_39215 TO app_39215; ### Available Commands ```bash -yarn db:create # Create database -yarn db:drop # Drop database -yarn db:migrate # Run pending migrations -yarn db:migrate:undo # Undo last migration -yarn db:migrate:undo:all # Undo all migrations -yarn db:migrate:status # Show migration status -yarn db:seed # Run all seeders -yarn db:seed:undo # Undo all seeders -yarn db:reset # Drop, create, migrate, and seed -yarn start # Migrate, seed, and start with watch -yarn lint # Run ESLint +npm run db:create # Create database +npm run db:drop # Drop database +npm run db:migrate # Run pending migrations +npm run db:migrate:undo # Undo last migration +npm run db:migrate:undo:all # Undo all migrations +npm run db:migrate:status # Show migration status +npm run db:seed # Run all seeders +npm run db:seed:undo # Undo all seeders +npm run db:reset # Drop, create, migrate, and seed +npm run start # Migrate, seed, and start with watch +npm run lint # Run ESLint +npm run typecheck # Run strict TypeScript check +npm run build # Compile migrated TypeScript files ``` ## API Documentation -Swagger UI available at: `http://localhost:8080/api-docs` +Swagger UI available at: `http://localhost:3000/api-docs` ### Core Endpoints @@ -363,7 +371,7 @@ Separate from server environment, tour pages have a content environment field: | `stage` | Stage preview | Pre-production review | | `production` | Public runtime | Published content | -The `X-Runtime-Environment` header (set by frontend) determines which content environment to query. The `runtime-context.js` middleware resolves this for API requests. +The `X-Runtime-Environment` header (set by frontend) determines which content environment to query. The `runtime-context.ts` middleware resolves this for API requests. ## Docker diff --git a/backend/eslint.config.js b/backend/eslint.config.js new file mode 100644 index 0000000..5a13913 --- /dev/null +++ b/backend/eslint.config.js @@ -0,0 +1,99 @@ +import js from '@eslint/js'; +import tsParser from '@typescript-eslint/parser'; +import tsPlugin from '@typescript-eslint/eslint-plugin'; +import importPlugin from 'eslint-plugin-import'; +import globals from 'globals'; + +const typeCheckedConfigs = tsPlugin.configs['flat/recommended-type-checked']; + +export default [ + { + ignores: ['node_modules/**', 'dist/**', 'tmp/**', 'logs/**'], + }, + js.configs.recommended, + { + files: ['**/*.js'], + languageOptions: { + ecmaVersion: 2024, + sourceType: 'module', + globals: globals.node, + }, + plugins: { + import: importPlugin, + }, + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.ts'], + }, + typescript: { + project: './tsconfig.json', + }, + }, + }, + rules: { + 'import/no-unresolved': 'error', + 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + }, + }, + ...typeCheckedConfigs.map((config) => ({ + ...config, + files: ['**/*.ts'], + })), + { + files: ['**/*.ts'], + languageOptions: { + parser: tsParser, + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: import.meta.dirname, + sourceType: 'module', + }, + globals: globals.node, + }, + plugins: { + '@typescript-eslint': tsPlugin, + import: importPlugin, + }, + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.ts'], + }, + typescript: { + project: './tsconfig.json', + }, + }, + }, + rules: { + 'import/no-unresolved': 'error', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, + ], + '@typescript-eslint/consistent-type-imports': [ + 'error', + { prefer: 'type-imports' }, + ], + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-non-null-assertion': 'error', + '@typescript-eslint/no-require-imports': 'error', + '@typescript-eslint/no-unsafe-type-assertion': 'error', + '@typescript-eslint/no-unnecessary-type-assertion': 'error', + 'no-restricted-syntax': [ + 'error', + { + selector: 'TSAsExpression', + message: + 'Do not use type casts in migrated backend TypeScript; use validation, guards, or typed adapters instead.', + }, + { + selector: 'TSTypeAssertion', + message: + 'Do not use type casts in migrated backend TypeScript; use validation, guards, or typed adapters instead.', + }, + ], + }, + }, +]; diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..011fcf8 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,10244 @@ +{ + "name": "tourbuilderplatform", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tourbuilderplatform", + "dependencies": { + "@aws-sdk/client-s3": "^3.1011.0", + "@aws-sdk/s3-request-presigner": "^3.1016.0", + "@google-cloud/storage": "^7.0.0", + "@smithy/node-http-handler": "^4.9.1", + "@smithy/types": "^4.15.0", + "bcrypt": "^6.0.0", + "body-parser": "^2.3.0", + "chokidar": "^4.0.3", + "cors": "^2.8.6", + "csv-parser": "^3.2.0", + "dotenv": "^16.4.0", + "express": "^4.22.2", + "ffmpeg-static": "^5.2.0", + "ffprobe-static": "^3.1.0", + "fluent-ffmpeg": "^2.1.3", + "helmet": "^8.0.0", + "joi": "^17.13.0", + "json2csv": "^5.0.7", + "jsonwebtoken": "^9.0.0", + "multer": "^2.0.0", + "nodemailer": "^9.0.3", + "passport": "^0.7.0", + "passport-google-oauth2": "^0.2.0", + "passport-jwt": "^4.0.1", + "passport-microsoft": "^2.0.0", + "pg": "^8.20.0", + "pino": "^9.0.0", + "pino-pretty": "^11.0.0", + "sequelize": "^6.37.0", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.0", + "uuid": "^14.0.1", + "validator": "^13.15.35" + }, + "devDependencies": { + "@eslint/js": "^8.57.1", + "@types/bcrypt": "^6.0.0", + "@types/body-parser": "^1.19.6", + "@types/cors": "^2.8.19", + "@types/express": "^4.17.25", + "@types/express-serve-static-core": "^4.19.8", + "@types/ffprobe-static": "^2.0.3", + "@types/fluent-ffmpeg": "^2.1.28", + "@types/json2csv": "^5.0.7", + "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.1.0", + "@types/node": "^24.13.2", + "@types/nodemailer": "^8.0.1", + "@types/passport": "^1.0.17", + "@types/passport-google-oauth2": "^0.1.10", + "@types/passport-jwt": "^4.0.1", + "@types/passport-microsoft": "^2.1.1", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", + "@types/validator": "^13.15.10", + "@typescript-eslint/eslint-plugin": "^8.62.1", + "@typescript-eslint/parser": "^8.62.1", + "eslint": "^8.57.0", + "eslint-import-resolver-typescript": "^4.4.5", + "eslint-plugin-import": "^2.29.1", + "globals": "^15.15.0", + "node-mocks-http": "^1.17.0", + "nodemon": "^3.0.0", + "typescript": "^6.0.3", + "umzug": "^3.8.3" + }, + "engines": { + "node": ">=24 <25" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.1045.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1045.0.tgz", + "integrity": "sha512-fsuO3Y6t+3Ro9Bsg41DKj4Sfy53CGSrhnMldNplWmG8Tx0UbYk+YDa4RD1hVlJpERw4JBmPkl0+J9qlxMh1pcA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-node": "^3.972.39", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.10", + "@aws-sdk/middleware-expect-continue": "^3.972.10", + "@aws-sdk/middleware-flexible-checksums": "^3.974.16", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-location-constraint": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-sdk-s3": "^3.972.37", + "@aws-sdk/middleware-ssec": "^3.972.10", + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/signature-v4-multi-region": "^3.996.25", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.24", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/eventstream-serde-browser": "^4.2.14", + "@smithy/eventstream-serde-config-resolver": "^4.3.14", + "@smithy/eventstream-serde-node": "^4.2.14", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-blob-browser": "^4.2.15", + "@smithy/hash-node": "^4.2.14", + "@smithy/hash-stream-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/md5-js": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.7", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-stream": "^4.5.25", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.974.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.8.tgz", + "integrity": "sha512-njR2qoG6ZuB0kvAS2FyICsFZJ6gmCcf2X/7JcD14sUvGDm26wiZ5BrA6LOiUxKFEF+IVe7kdroxyE00YlkiYsw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.22", + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/crc64-nvme": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.7.tgz", + "integrity": "sha512-QUagVVBbC8gODCF6e1aV0mE2TXWB9Opz4k8EJFdNrujUVQm5R4AjJa1mpOqzwOuROBzqJU9zawzig7M96L8Ejg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.34.tgz", + "integrity": "sha512-XT0jtf8Fw9JE6ppsQeoNnZRiG+jqRixMT1v1ZR17G60UvVdsQmTG8nbEyHuEPfMxDXEhfdARaM/XiEhca4lGHQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.36.tgz", + "integrity": "sha512-DPoGWfy7J7RKxvbf5kOKIGQkD2ek3dbKgzKIGrnLuvZBz5myU+Im/H6pmc14QcnFbqHMqxvtWSgRDSJW3qXLQg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.25", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.38.tgz", + "integrity": "sha512-oDzUBu2MGJFgoar05sPMCwSrhw44ASyccrHzj66vO69OZqi7I6hZZxXfuPLC8OCzW7C+sU+bI73XHij41yekgQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-env": "^3.972.34", + "@aws-sdk/credential-provider-http": "^3.972.36", + "@aws-sdk/credential-provider-login": "^3.972.38", + "@aws-sdk/credential-provider-process": "^3.972.34", + "@aws-sdk/credential-provider-sso": "^3.972.38", + "@aws-sdk/credential-provider-web-identity": "^3.972.38", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.38.tgz", + "integrity": "sha512-g1NosS8qe4OF++G2UFCM5ovSkgipC7YYor5KCWatG0UoMSO5YFj9C8muePlyVmOBV/WTI16Jo3/s1NUo/o1Bww==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.39.tgz", + "integrity": "sha512-HEswDQyxUtadoZ/bJsPPENHg7R0Lzym5LuMksJeHvqhCOpP+rtkDLKI4/ZChH4w3cf5kG8n6bZuI8PzajoiqMg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.34", + "@aws-sdk/credential-provider-http": "^3.972.36", + "@aws-sdk/credential-provider-ini": "^3.972.38", + "@aws-sdk/credential-provider-process": "^3.972.34", + "@aws-sdk/credential-provider-sso": "^3.972.38", + "@aws-sdk/credential-provider-web-identity": "^3.972.38", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.34.tgz", + "integrity": "sha512-T3IFs4EVmVi1dVN5RciFnklCANSzvrQd/VuHY9ThHSQmYkTogjcGkoJEr+oNUPQZnso52183088NqysMPji1/Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.38.tgz", + "integrity": "sha512-5ZxG+t0+3Q3QPh8KEjX6syskhgNf7I0MN7oGioTf6Lm1NTjfP7sIcYGNsthXC2qR8vcD3edNZwCr2ovfSSWuRA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/token-providers": "3.1041.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.38.tgz", + "integrity": "sha512-lYHFF30DGI20jZcYX8cm6Ns0V7f1dDN6g/MBDLTyD/5iw+bXs3yBr2iAiHDkx4RFU5JgsnZvCHYKiRVPRdmOgw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.10.tgz", + "integrity": "sha512-Vbc2frZH7wXlMNd+ZZSXUEs/l1Sv8Jj4zUnIfwrYF5lwaLdXHZ9xx4U3rjUcaye3HRhFVc+E5DbBxpRAbB16BA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.10.tgz", + "integrity": "sha512-2Yn0f1Qiq/DjxYR3wfI3LokXnjOhFM7Ssn4LTdFDIxRMCE6I32MAsVnhPX1cUZsuVA9tiZtwwhlSLAtFGxAZlQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.974.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.16.tgz", + "integrity": "sha512-6ru8doI0/XzszqLIPXf0E/V7HhAw1Pu94010XCKYtBUfD0LxF0BuOzrUf8OQGR6j2o6wgKTHUniOmndQycHwCA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/crc64-nvme": "^3.972.7", + "@aws-sdk/types": "^3.973.8", + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.25", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.10.tgz", + "integrity": "sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.10.tgz", + "integrity": "sha512-rI3NZvJcEvjoD0+0PI0iUAwlPw2IlSlhyvgBK/3WkKJQE/YiKFedd9dMN2lVacdNxPNhxL/jzQaKQdrGtQagjQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz", + "integrity": "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.11.tgz", + "integrity": "sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.37.tgz", + "integrity": "sha512-Km7M+i8DrLArVzrid1gfxeGhYHBd3uxvE77g0s5a52zPSVosxzQBnJ0gwWb6NIp/DOk8gsBMhi7V+cpJG0ndTA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.25", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.10.tgz", + "integrity": "sha512-Gli9A0u8EVVb+5bFDGS/QbSVg28w/wpEidg1ggVcSj65BDTdGR6punsOcVjqdiu1i42WHWo51MCvARPIIz9juw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.38.tgz", + "integrity": "sha512-iz+B29TXcAZsJpwB+AwG/TTGA5l/VnmMZ2UxtiySOZjI6gCdmviXPwdgzcmuazMy16rXoPY4mYCGe7zdNKfx5A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@smithy/core": "^3.23.17", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-retry": "^4.3.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.6.tgz", + "integrity": "sha512-WBDnqatJl+kGObpfmfSxqnXeYTu3Me8wx8WCtvoxX3pfWrrTv8I4WTMSSs7PZqcRcVh8WeUKMgGFjMG+52SR1w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/signature-v4-multi-region": "^3.996.25", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.24", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.7", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.13.tgz", + "integrity": "sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/config-resolver": "^4.4.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.1045.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1045.0.tgz", + "integrity": "sha512-VDRF8GIuUPX+K4DUYrvcODj/h54LOmdJ7DhpLQ0wrYrdxzIiJEpi0n9jZ1bbjT2UxhwTbOorse5EGo+gnOK2aA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/signature-v4-multi-region": "^3.996.25", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-format-url": "^3.972.10", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.25.tgz", + "integrity": "sha512-+CMIt3e1VzlklAECmG+DtP1sV8iKq25FuA0OKpnJ4KA0kxUtd7CgClY7/RU6VzJBQwbN4EJ9Ue6plvqx1qGadw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "^3.972.37", + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1041.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1041.0.tgz", + "integrity": "sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", + "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.8.tgz", + "integrity": "sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-endpoints": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.10.tgz", + "integrity": "sha512-DEKiHNJVtNxdyTeQspzY+15Po/kHm6sF0Cs4HV9Q2+lplB63+DrvdeiSoOSdWEWAoO2RcY1veoXVDz2tWxWCgQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.10.tgz", + "integrity": "sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.24.tgz", + "integrity": "sha512-ZWwlkjcIp7cEL8ZfTpTAPNkwx25p7xol0xlKoWVVf22+nsjwmLcHYtTPjIV1cSpmB/b6DaK4cb1fSkvCXHgRdw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/types": "^3.973.8", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.22.tgz", + "integrity": "sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==", + "license": "Apache-2.0", + "dependencies": { + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@derhuerst/http-basic": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz", + "integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==", + "license": "MIT", + "dependencies": { + "caseless": "^0.12.0", + "concat-stream": "^2.0.0", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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/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/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", + "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", + "license": "Apache-2.0", + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.4.tgz", + "integrity": "sha512-j8yRSSqswWi1QqUGKVEKOG03Q7qOoZP6/h2zN2YO+F5h2+DHU0bSrHCK9Y7lo2DI9fBd8qGAw795sf+3Jva4yA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/storage": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.21.0.tgz", + "integrity": "sha512-l+IFTkd+6Y5LoAuXyYCKNAKtw/Ci+rAMqgdTB1jv4iZiLhw0rtq+0qjIRbBizXkNzEFmXiXUW0H7sZQQvk1ffA==", + "license": "Apache-2.0", + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "<4.1.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^5.3.4", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.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/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "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/@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/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/node-core-library": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-4.0.2.tgz", + "integrity": "sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "z-schema": "~5.0.2" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@rushstack/terminal": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.10.0.tgz", + "integrity": "sha512-UbELbXnUdc7EKwfH2sb8ChqNgapUOdqcCIdQP4NGxBpTZV2sQyeekuK3zmfQSa/MN+/7b4kBogl2wq0vpkpYGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rushstack/node-core-library": "4.0.2", + "supports-color": "~8.1.1" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/terminal/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@rushstack/ts-command-line": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.19.1.tgz", + "integrity": "sha512-J7H768dgcpG60d7skZ5uSSwyCZs/S2HrWP1Ds8d1qYAyaaeJmpmmLr9BVw97RjFzmQPOYnoXcKA4GkqDCkduQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rushstack/terminal": "0.10.0", + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "string-argv": "~0.3.1" + } + }, + "node_modules/@rushstack/ts-command-line/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@rushstack/ts-command-line/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz", + "integrity": "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.3.tgz", + "integrity": "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.17", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.17.tgz", + "integrity": "sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.28.0.tgz", + "integrity": "sha512-N/LoLG8pZ1zv5cIWpdF6vmSjtZtXKK9G0OqT5yYCOZU+CzPq1+nYA95VoKJBGWRScs7YbMugZ7lZx8Fj1vdHoA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.14.tgz", + "integrity": "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.14.tgz", + "integrity": "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.1", + "@smithy/util-hex-encoding": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.14.tgz", + "integrity": "sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.14.tgz", + "integrity": "sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.14.tgz", + "integrity": "sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.14.tgz", + "integrity": "sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.17.tgz", + "integrity": "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.15.tgz", + "integrity": "sha512-0PJ4Al3fg2nM4qKrAIxyNcApgqHAXcBkN8FeizOz69z0rb26uZ6lMESYtxegaTlXB5Hj84JfwMPavMrwDMjucA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.2", + "@smithy/chunked-blob-reader-native": "^4.2.3", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.14.tgz", + "integrity": "sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.14.tgz", + "integrity": "sha512-tw4GANWkZPb6+BdD4Fgucqzey2+r73Z/GRo9zklsCdwrnxxumUV83ZIaBDdudV4Ylazw3EPTiJZhpX42105ruQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.14.tgz", + "integrity": "sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.14.tgz", + "integrity": "sha512-V2v0vx+h0iUSNG1Alt+GNBMSLGCrl9iVsdd+Ap67HPM9PN479x12V8LkuMoKImNZxn3MXeuyUjls+/7ZACZghA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.14.tgz", + "integrity": "sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.32", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.32.tgz", + "integrity": "sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-middleware": "^4.2.14", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.7.tgz", + "integrity": "sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/service-error-classification": "^4.3.1", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.20", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.20.tgz", + "integrity": "sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.14.tgz", + "integrity": "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.14.tgz", + "integrity": "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.1.tgz", + "integrity": "sha512-m/f15di58P6NtLQ7eVEb5N19NdJWn+4c7zfkFHMT/i3JH7U8UtknpPoy8o2tm2R3OdliYvsvQhZHIfACQDqT+Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.28.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.14.tgz", + "integrity": "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.14.tgz", + "integrity": "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.14.tgz", + "integrity": "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "@smithy/util-uri-escape": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.14.tgz", + "integrity": "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.1.tgz", + "integrity": "sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.9.tgz", + "integrity": "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.14.tgz", + "integrity": "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.12.13", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.13.tgz", + "integrity": "sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.25", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.15.0.tgz", + "integrity": "sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.14.tgz", + "integrity": "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.49", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.49.tgz", + "integrity": "sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.54", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.54.tgz", + "integrity": "sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.17", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.2.tgz", + "integrity": "sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.14.tgz", + "integrity": "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.8.tgz", + "integrity": "sha512-LUIxbTBi+OpvXpg91poGA6BdyoleMDLnfXjVDqyi2RvZmTveY5loE/FgYUBCR5LU2BThW2SoZRh8dTIIy38IPw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.3.1", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.25", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.25.tgz", + "integrity": "sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.3.0.tgz", + "integrity": "sha512-JyjYmLAfS+pdxF92o4yLgEoy0zhayKTw73FU1aofLWwLcJw7iSqIY2exGmMTrl/lmZugP5p/zxdFSippJDfKWA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", + "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/argparse": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "license": "MIT" + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/express/node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/ffprobe-static": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/ffprobe-static/-/ffprobe-static-2.0.3.tgz", + "integrity": "sha512-86Q8dIzhjknxA6935ZcVLkVU3t6bP+F08mdriWZk2+3UqjxIbT3QEADjbO5Yq8826cVs/aZBREVZ7jUoSTgHiw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fluent-ffmpeg": { + "version": "2.1.28", + "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.28.tgz", + "integrity": "sha512-5ovxsDwBcPfJ+eYs1I/ZpcYCnkce7pvH9AHSvrZllAp1ZPpTRDZAFjF3TRFbukxSgIYTTNYePbS0rKUmaxVbXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "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==", + "license": "MIT" + }, + "node_modules/@types/json2csv": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/json2csv/-/json2csv-5.0.7.tgz", + "integrity": "sha512-Ma25zw9G9GEBnX8b12R4EYvnFT6dBh8L3jwsN5EUFXa+fl2dqmbLDbNWN0XuQU3rSXdsbBeCYjI9uHU2PUBxhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz", + "integrity": "sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/nodemailer": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.1.tgz", + "integrity": "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/oauth": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.6.tgz", + "integrity": "sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-google-oauth2": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/@types/passport-google-oauth2/-/passport-google-oauth2-0.1.10.tgz", + "integrity": "sha512-Awm0w8qvAOmxvcO+hLLvEzzpQKqtYrzWcCLt4x59YRjCDPNsTIJUYCDWuK6Hvur39/m7UGKflo2Pnnx2KENG2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-microsoft": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/passport-microsoft/-/passport-microsoft-2.1.1.tgz", + "integrity": "sha512-mtxO0nUt8PSAQd1MPD4JZXacYRx9MXYCTFjKyBEarmCJYyJlKHMwBRYltBEi/zpvC6xYX0LgK1AFXp+hQI+DEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/passport-oauth2": "*" + } + }, + "node_modules/@types/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-6//z+4orIOy/g3zx17HyQ71GSRK4bs7Sb+zFasRoc2xzlv7ZCJ+vkDBYFci8U6HY+or6Zy7ajf4mz4rK7nsWJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/oauth": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/request": { + "version": "2.48.13", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", + "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", + "license": "MIT", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.5" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/swagger-jsdoc": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", + "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "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/@typescript-eslint/eslint-plugin": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz", + "integrity": "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/type-utils": "8.62.1", + "@typescript-eslint/utils": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", + "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.62.1", + "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.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz", + "integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", + "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.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz", + "integrity": "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.62.1", + "@typescript-eslint/types": "^8.62.1", + "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.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz", + "integrity": "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1" + }, + "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.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz", + "integrity": "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==", + "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.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz", + "integrity": "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1", + "@typescript-eslint/utils": "8.62.1", + "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.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz", + "integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==", + "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.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz", + "integrity": "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.62.1", + "@typescript-eslint/tsconfig-utils": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", + "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/typescript-estree/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/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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/@typescript-eslint/utils": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.1.tgz", + "integrity": "sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1" + }, + "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.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz", + "integrity": "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.62.1", + "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/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "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": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/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/ajv": { + "version": "6.15.0", + "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/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes/node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes/node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex/node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex/node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat/node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap/node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice/node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "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/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "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/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/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/body-parser/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/body-parser/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/body-parser/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/body-parser/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/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "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": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "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": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-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/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, + "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/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "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", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/cross-spawn/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/csv-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.1.tgz", + "integrity": "sha512-v8RPMSglouR9od735SnwSxLBbCJqEPSbgm1R5qfr8yIiMUCEFjox56kRZid0SvgHJEkxeIEu3+a9QS3YRh7CuA==", + "license": "MIT", + "bin": { + "csv-parser": "bin/csv-parser" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-buffer/node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-length/node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "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/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/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "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/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dottie": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.7.tgz", + "integrity": "sha512-7lAK2A0b3zZr3UC5aE69CPdCFR4RHW1o2Dr74TqFykxkUCBXSRJum/yPc7g8zRHJqWKomPLHwFLLoUnn8PXXRg==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "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/dunder-proto/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/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "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/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract/node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract/node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract/node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-abstract/node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract/node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract/node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract/node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract/node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract/node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract/node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract/node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "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/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-to-primitive/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-to-primitive/node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-import-context": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz", + "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-tsconfig": "^4.10.1", + "stable-hash-x": "^0.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-context" + }, + "peerDependencies": { + "unrs-resolver": "^1.0.0" + }, + "peerDependenciesMeta": { + "unrs-resolver": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "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/eslint-import-resolver-node/node_modules/resolve/node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.5.tgz", + "integrity": "sha512-nbE5XLph6TLtGYcu/U6e6ZVXyKBhbDWK5cLGk76eJ7NdZpwf1P9EFkpt1Z01mNZNrrilsAYWKH6zUkL4reoXbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "debug": "^4.4.1", + "eslint-import-context": "^0.1.8", + "get-tsconfig": "^4.10.1", + "is-bun-module": "^2.0.0", + "stable-hash-x": "^0.2.0", + "tinyglobby": "^0.2.14", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^16.17.0 || >=18.6.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-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/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.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/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/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "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/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "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==", + "dev": true, + "license": "MIT" + }, + "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-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.7.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", + "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.5", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "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/ffmpeg-static": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.3.0.tgz", + "integrity": "sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==", + "hasInstallScript": true, + "license": "GPL-3.0-or-later", + "dependencies": { + "@derhuerst/http-basic": "^8.2.0", + "env-paths": "^2.2.0", + "https-proxy-agent": "^5.0.0", + "progress": "^2.0.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/ffprobe-static": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ffprobe-static/-/ffprobe-static-3.1.0.tgz", + "integrity": "sha512-Dvpa9uhVMOYivhHKWLGDoa512J751qN1WZAIO+Xw4L/mrUSPxS4DApzSUDbCFE/LUq2+xYnznEahTd63AqBSpA==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "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/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "dev": true, + "license": "ISC" + }, + "node_modules/fluent-ffmpeg": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz", + "integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==", + "license": "MIT", + "dependencies": { + "async": "^0.2.9", + "which": "^1.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/form-data": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.6.tgz", + "integrity": "sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "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/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/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "dev": 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/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/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/gaxios/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/gaxios/node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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-intrinsic/node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic/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/get-intrinsic/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/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-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "license": "MIT", + "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-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "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/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "license": "MIT" + }, + "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/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "license": "MIT", + "dependencies": { + "@types/node": "^10.0.3" + } + }, + "node_modules/http-response-object/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "license": "MIT" + }, + "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/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/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/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "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/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex/node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true, + "license": "MIT" + }, + "node_modules/joi": { + "version": "17.13.4", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.4.tgz", + "integrity": "sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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-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/json2csv": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/json2csv/-/json2csv-5.0.7.tgz", + "integrity": "sha512-YRZbUnyaJZLZUJSRi2G/MqahCyRv9n/ds+4oIetjDF3jWQA7AG7iSeKTiZiCNqtMZM7HDyt0e/W6lEnoGEmMGA==", + "license": "MIT", + "dependencies": { + "commander": "^6.1.0", + "jsonparse": "^1.3.1", + "lodash.get": "^4.4.2" + }, + "bin": { + "json2csv": "bin/json2csv.js" + }, + "engines": { + "node": ">= 10", + "npm": ">= 6.13.0" + } + }, + "node_modules/json2csv/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "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/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/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.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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/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/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.45", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz", + "integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "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/multer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz", + "integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", + "integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-mocks-http": { + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/node-mocks-http/-/node-mocks-http-1.17.2.tgz", + "integrity": "sha512-HVxSnjNzE9NzoWMx9T9z4MLqwMpLwVvA0oVZ+L+gXskYXEJ6tFn3Kx4LargoB6ie7ZlCLplv7QbWO6N+MysWGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^1.3.7", + "content-disposition": "^0.5.3", + "depd": "^1.1.0", + "fresh": "^0.5.2", + "merge-descriptors": "^1.0.1", + "methods": "^1.1.2", + "mime": "^1.3.4", + "parseurl": "^1.3.3", + "range-parser": "^1.2.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@types/express": "^4.17.21 || ^5.0.0", + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + }, + "@types/node": { + "optional": true + } + } + }, + "node_modules/node-mocks-http/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemailer": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.3.tgz", + "integrity": "sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/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/nodemon/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/nodemon/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/nodemon/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/nodemon/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/nodemon/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oauth": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz", + "integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.assign/node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.assign/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values/node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, + "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/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==" + }, + "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/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-google-oauth2": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth2/-/passport-google-oauth2-0.2.0.tgz", + "integrity": "sha512-62EdPtbfVdc55nIXi0p1WOa/fFMM8v/M8uQGnbcXA4OexZWCnfsEi3wo2buag+Is5oqpuHzOtI64JpHk0Xi5RQ==", + "license": "MIT", + "dependencies": { + "passport-oauth2": "^1.1.2" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-microsoft": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/passport-microsoft/-/passport-microsoft-2.1.0.tgz", + "integrity": "sha512-7bOcjEmZCHg5qD55iHaMD/mgBxPtXLbqAwmKox5IsqOSEU50WJk5nQKK4lxKdBHLZ0hf+gzrFgDsTybJP18/JA==", + "dependencies": { + "passport-oauth2": "1.8.0" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "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-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.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==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "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-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "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/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.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": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-11.3.0.tgz", + "integrity": "sha512-oXwn7ICywaZPHmu3epHGU2oJX4nPmKvHvB/bwrJHlGcbEWaVcotkpyVHMKLKmiVryWYByNp0jpgAcXpFJDXJzA==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.2", + "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": "^2.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "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/pony-cause": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-2.1.11.tgz", + "integrity": "sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg==", + "dev": true, + "license": "0BSD", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "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/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/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "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/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.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/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "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/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/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.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/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reflect.getprototypeof/node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags/node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags/node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-as-promised": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz", + "integrity": "sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA==", + "license": "MIT" + }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "license": "MIT", + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "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/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/sequelize": { + "version": "6.37.8", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.8.tgz", + "integrity": "sha512-HJ0IQFqcTsTiqbEgiuioYFMSD00TP6Cz7zoTti+zVVBwVe9fEhev9cH6WnM3XU31+ABS356durAb99ZuOthnKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/sequelize/node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "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/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "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/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/stable-hash-x": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz", + "integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "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/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim/node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend/node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "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/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-color/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "license": "MIT", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.17.14", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", + "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==", + "license": "Apache-2.0" + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "license": "Apache-2.0", + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "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/tinyglobby/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/tinyglobby/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/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "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/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", + "license": "MIT" + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "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/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "license": "0BSD" + }, + "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": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-buffer/node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-length/node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-length/node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-length/node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-length/node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset/node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset/node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset/node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset/node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "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/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, + "node_modules/umzug": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/umzug/-/umzug-3.8.3.tgz", + "integrity": "sha512-U9SRJI6LJvV0XwrqGMVPBkE26WHJklHZjtscJ2sEjUp7f+h4NH/25YGjPBernWLroVJvMnTkCAGC0bT0dd63qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rushstack/ts-command-line": "4.19.1", + "emittery": "^0.13.0", + "pony-cause": "^2.1.4", + "tinyglobby": "^0.2.16", + "type-fest": "^4.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/umzug/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/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "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/unrs-resolver": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, + "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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "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/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/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-boxed-primitive/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-boxed-primitive/node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array/node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array/node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array/node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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/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/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/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/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/z-schema": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.6.tgz", + "integrity": "sha512-+XR1GhnWklYdfr8YaZv/iu+vY+ux7V5DS5zH1DQf6bO5ufrt/5cgNhVO5qyhsjFXvsqQb/f08DWE9b6uPscyAg==", + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^10.0.0" + } + } + } +} diff --git a/backend/package.json b/backend/package.json index 96f2f1f..9a93b01 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,76 +1,110 @@ { "name": "tourbuilderplatform", "description": "Tour Builder Platform - template backend", + "type": "module", "scripts": { "start": "npm run db:migrate && npm run db:seed && npm run watch", - "start-dev": "cross-env NODE_ENV=production LOG_PRETTY=true DOTENV_CONFIG_PATH=.env NODE_OPTIONS=\"-r dotenv/config\" npm run start", - "test": "node --test tests/*.test.js", - "test:integration": "node --test tests/integration/*.test.js", - "check:public-access": "node scripts/check-public-access-hardening.js", - "fix:public-access": "node scripts/check-public-access-hardening.js --fix", - "lint": "eslint . --ext .js", - "db:migrate": "sequelize-cli db:migrate", - "db:migrate:undo": "sequelize-cli db:migrate:undo", - "db:migrate:undo:all": "sequelize-cli db:migrate:undo:all", - "db:migrate:status": "sequelize-cli db:migrate:status", - "db:seed": "sequelize-cli db:seed:all", - "db:seed:undo": "sequelize-cli db:seed:undo:all", - "db:drop": "sequelize-cli db:drop", - "db:create": "sequelize-cli db:create", + "start-dev": "LOG_PRETTY=true npm run start", + "typecheck": "tsc -p tsconfig.json --noEmit", + "build": "tsc -p tsconfig.json && node scripts/copy-runtime-assets.ts", + "test": "node --test tests/*.test.ts", + "test:integration": "node --test tests/integration/*.test.ts", + "verify": "npm run typecheck && npm run lint && npm run check:esm-boundaries && npm run test", + "check:esm-boundaries": "node scripts/check-esm-boundaries.ts", + "check:public-access": "node scripts/check-public-access-hardening.ts", + "fix:public-access": "node scripts/check-public-access-hardening.ts --fix", + "lint": "eslint .", + "db:migrate": "node src/db/umzug.ts migrate:up", + "db:migrate:undo": "node src/db/umzug.ts migrate:down", + "db:migrate:undo:all": "node src/db/umzug.ts migrate:down:all", + "db:migrate:status": "node src/db/umzug.ts migrate:status", + "db:seed": "node src/db/umzug.ts seed:up", + "db:seed:undo": "node src/db/umzug.ts seed:down:all", + "db:drop": "node src/db/umzug.ts db:drop", + "db:create": "node src/db/umzug.ts db:create", "db:reset": "npm run db:drop && npm run db:create && npm run db:migrate && npm run db:seed", - "watch": "node watcher.js" + "watch": "node watcher.ts" }, "dependencies": { "@aws-sdk/client-s3": "^3.1011.0", "@aws-sdk/s3-request-presigner": "^3.1016.0", "@google-cloud/storage": "^7.0.0", - "axios": "^1.13.0", + "@smithy/node-http-handler": "^4.9.1", + "@smithy/types": "^4.15.0", "bcrypt": "^6.0.0", + "body-parser": "^2.3.0", "chokidar": "^4.0.3", "cors": "^2.8.6", "csv-parser": "^3.2.0", "dotenv": "^16.4.0", - "express": "4.18.2", - "express-validator": "^7.0.0", + "express": "^4.22.2", "ffmpeg-static": "^5.2.0", "ffprobe-static": "^3.1.0", "fluent-ffmpeg": "^2.1.3", - "formidable": "1.2.2", "helmet": "^8.0.0", "joi": "^17.13.0", "json2csv": "^5.0.7", "jsonwebtoken": "^9.0.0", - "lodash": "^4.17.23", - "moment": "2.30.1", "multer": "^2.0.0", - "mysql2": "2.2.5", - "nodemailer": "6.9.9", + "nodemailer": "^9.0.3", "passport": "^0.7.0", "passport-google-oauth2": "^0.2.0", "passport-jwt": "^4.0.1", "passport-microsoft": "^2.0.0", "pg": "^8.20.0", - "pg-hstore": "2.3.4", "pino": "^9.0.0", "pino-pretty": "^11.0.0", "sequelize": "^6.37.0", - "sequelize-json-schema": "^2.1.1", - "sqlite": "4.0.15", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.0", - "tedious": "^18.6.0" + "uuid": "^14.0.1", + "validator": "^13.15.35" }, "engines": { - "node": ">=18" + "node": ">=24 <25" + }, + "overrides": { + "gaxios": { + "uuid": "^11.1.1" + }, + "sequelize": { + "uuid": "^11.1.1" + }, + "teeny-request": { + "uuid": "^11.1.1" + } }, "private": true, "devDependencies": { - "cross-env": "^7.0.3", + "@eslint/js": "^8.57.1", + "@types/bcrypt": "^6.0.0", + "@types/body-parser": "^1.19.6", + "@types/cors": "^2.8.19", + "@types/express": "^4.17.25", + "@types/express-serve-static-core": "^4.19.8", + "@types/ffprobe-static": "^2.0.3", + "@types/fluent-ffmpeg": "^2.1.28", + "@types/json2csv": "^5.0.7", + "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.1.0", + "@types/node": "^24.13.2", + "@types/nodemailer": "^8.0.1", + "@types/passport": "^1.0.17", + "@types/passport-google-oauth2": "^0.1.10", + "@types/passport-jwt": "^4.0.1", + "@types/passport-microsoft": "^2.1.1", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", + "@types/validator": "^13.15.10", + "@typescript-eslint/eslint-plugin": "^8.62.1", + "@typescript-eslint/parser": "^8.62.1", "eslint": "^8.57.0", + "eslint-import-resolver-typescript": "^4.4.5", "eslint-plugin-import": "^2.29.1", - "mocha": "^10.0.0", + "globals": "^15.15.0", "node-mocks-http": "^1.17.0", "nodemon": "^3.0.0", - "sequelize-cli": "^6.6.5" + "typescript": "^6.0.3", + "umzug": "^3.8.3" } } diff --git a/backend/scripts/check-esm-boundaries.ts b/backend/scripts/check-esm-boundaries.ts new file mode 100644 index 0000000..b0c8261 --- /dev/null +++ b/backend/scripts/check-esm-boundaries.ts @@ -0,0 +1,90 @@ +import { readdir, readFile } from 'node:fs/promises'; +import path from 'node:path'; + +interface BoundaryViolation { + file: string; + reason: string; +} + +const sourceRoots = ['src', 'scripts', 'tests']; +const commonJsPattern = new RegExp( + String.raw`\b(${['require\\s*\\(', 'module\\.exports', 'exports\\.'].join('|')})`, +); + +async function collectFiles(dir: string): Promise { + const entries = await readdir(dir, { withFileTypes: true }); + const nestedFiles = await Promise.all( + entries.map(async (entry) => { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (entry.name === 'node_modules' || entry.name === 'dist') return []; + return collectFiles(fullPath); + } + return [fullPath]; + }), + ); + + return nestedFiles.flat(); +} + +function toProjectPath(filePath: string): string { + return path.relative(process.cwd(), filePath).split(path.sep).join('/'); +} + +function isHistoricalMigration(projectPath: string): boolean { + return projectPath.startsWith('src/db/migrations/') && projectPath.endsWith('.js'); +} + +function checkJsBoundary(projectPath: string): BoundaryViolation | null { + if (isHistoricalMigration(projectPath)) return null; + + return { + file: projectPath, + reason: 'Unexpected JavaScript source. Use TypeScript ESM source.', + }; +} + +function checkTsBoundary(projectPath: string, source: string): BoundaryViolation | null { + if (!commonJsPattern.test(source)) return null; + + return { + file: projectPath, + reason: + 'Unexpected CommonJS syntax in TypeScript source. Use ESM import/export.', + }; +} + +async function checkFile(filePath: string): Promise { + const projectPath = toProjectPath(filePath); + const ext = path.extname(filePath); + if (ext !== '.js' && ext !== '.ts') return null; + + const source = await readFile(filePath, 'utf8'); + + if (ext === '.js') { + return checkJsBoundary(projectPath); + } + + return checkTsBoundary(projectPath, source); +} + +async function main(): Promise { + const files = ( + await Promise.all(sourceRoots.map((root) => collectFiles(path.join(process.cwd(), root)))) + ).flat(); + const checks = await Promise.all(files.map((file) => checkFile(file))); + const violations = checks.filter((violation) => violation !== null); + + if (violations.length > 0) { + console.error('ESM boundary check failed:'); + for (const violation of violations) { + console.error(`- ${violation.file}: ${violation.reason}`); + } + process.exitCode = 1; + return; + } + + console.log('ESM boundary check passed.'); +} + +void main(); diff --git a/backend/scripts/check-public-access-hardening.js b/backend/scripts/check-public-access-hardening.ts similarity index 52% rename from backend/scripts/check-public-access-hardening.js rename to backend/scripts/check-public-access-hardening.ts index 3be174f..bda2cde 100644 --- a/backend/scripts/check-public-access-hardening.js +++ b/backend/scripts/check-public-access-hardening.ts @@ -1,12 +1,18 @@ #!/usr/bin/env node -const db = require('../src/db/models'); -const AccessPolicyAuditService = require('../src/services/access-policy-audit'); +import db from '../src/db/models/index.ts'; +import AccessPolicyAuditService from '../src/services/access-policy-audit.ts'; +import type { + AccessPolicyAuditReport, + PublicAccessHardeningSummary, +} from '../src/types/index.ts'; const shouldFix = process.argv.includes('--fix'); const EXIT_TIMEOUT_MS = 1500; -function summarizeReport(report) { +function summarizeReport( + report: AccessPolicyAuditReport, +): PublicAccessHardeningSummary { return { publicRolePermissions: report.publicRolePermissions.length, publicUsersWithCustomPermissions: @@ -16,45 +22,41 @@ function summarizeReport(report) { }; } -async function main() { +function logJson(value: unknown): void { + console.log(JSON.stringify(value, null, 2)); +} + +function logError(error: unknown): void { + console.error(error); +} + +async function main(): Promise { if (shouldFix) { const result = await db.sequelize.transaction((transaction) => AccessPolicyAuditService.cleanupViolations({ transaction }), ); - console.log( - JSON.stringify( - { - fixed: true, - summary: { - removedPublicRolePermissions: result.removedPublicRolePermissions, - clearedPublicUserCustomPermissions: - result.clearedPublicUserCustomPermissions, - removedNonPublicProductionPresentationGrants: - result.removedNonPublicProductionPresentationGrants, - }, - }, - null, - 2, - ), - ); + logJson({ + fixed: true, + summary: { + removedPublicRolePermissions: result.removedPublicRolePermissions, + clearedPublicUserCustomPermissions: + result.clearedPublicUserCustomPermissions, + removedNonPublicProductionPresentationGrants: + result.removedNonPublicProductionPresentationGrants, + }, + }); return; } const report = await AccessPolicyAuditService.findViolations(); const hasViolations = AccessPolicyAuditService.hasViolations(report); - console.log( - JSON.stringify( - { - ok: !hasViolations, - summary: summarizeReport(report), - report, - }, - null, - 2, - ), - ); + logJson({ + ok: !hasViolations, + summary: summarizeReport(report), + report, + }); if (hasViolations) { process.exitCode = 1; @@ -63,7 +65,7 @@ async function main() { main() .catch((error) => { - console.error(error); + logError(error); process.exitCode = 1; }) .finally(async () => { diff --git a/backend/scripts/copy-runtime-assets.ts b/backend/scripts/copy-runtime-assets.ts new file mode 100644 index 0000000..55447df --- /dev/null +++ b/backend/scripts/copy-runtime-assets.ts @@ -0,0 +1,45 @@ +import { copyFile, mkdir, readdir } from 'node:fs/promises'; +import path from 'node:path'; + +interface RuntimeAssetCopy { + source: string; + target: string; +} + +const runtimeAssetCopies: readonly RuntimeAssetCopy[] = [ + { + source: 'src/db/migrations/package.json', + target: 'dist/src/db/migrations/package.json', + }, +]; + +async function copyRuntimeAsset(copy: RuntimeAssetCopy): Promise { + await mkdir(path.dirname(copy.target), { recursive: true }); + await copyFile(copy.source, copy.target); +} + +async function copyMigrationFiles(): Promise { + const sourceDirectory = 'src/db/migrations'; + const targetDirectory = 'dist/src/db/migrations'; + const entries = await readdir(sourceDirectory, { withFileTypes: true }); + + await mkdir(targetDirectory, { recursive: true }); + + await Promise.all( + entries + .filter((entry) => entry.isFile() && entry.name.endsWith('.js')) + .map((entry) => + copyFile( + path.join(sourceDirectory, entry.name), + path.join(targetDirectory, entry.name), + ), + ), + ); +} + +async function main(): Promise { + await Promise.all(runtimeAssetCopies.map((copy) => copyRuntimeAsset(copy))); + await copyMigrationFiles(); +} + +void main(); diff --git a/backend/src/ai/LocalAIApi.js b/backend/src/ai/LocalAIApi.js deleted file mode 100644 index 31dd270..0000000 --- a/backend/src/ai/LocalAIApi.js +++ /dev/null @@ -1,512 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const http = require('http'); -const https = require('https'); -const { URL } = require('url'); - -let CONFIG_CACHE = null; - -class LocalAIApi { - static createResponse(params, options) { - return createResponse(params, options); - } - - static request(pathValue, payload, options) { - return request(pathValue, payload, options); - } - - static fetchStatus(aiRequestId, options) { - return fetchStatus(aiRequestId, options); - } - - static awaitResponse(aiRequestId, options) { - return awaitResponse(aiRequestId, options); - } - - static extractText(response) { - return extractText(response); - } - - static decodeJsonFromResponse(response) { - return decodeJsonFromResponse(response); - } -} - -async function createResponse(params, options = {}) { - const payload = { ...(params || {}) }; - - if (!Array.isArray(payload.input) || payload.input.length === 0) { - return { - success: false, - error: 'input_missing', - message: 'Parameter "input" is required and must be a non-empty array.', - }; - } - - const cfg = config(); - if (!payload.model) { - payload.model = cfg.defaultModel; - } - - const initial = await request(options.path, payload, options); - if (!initial.success) { - return initial; - } - - const data = initial.data; - if (data && typeof data === 'object' && data.ai_request_id) { - const pollTimeout = Number(options.poll_timeout ?? 300); - const pollInterval = Number(options.poll_interval ?? 5); - return await awaitResponse(data.ai_request_id, { - interval: pollInterval, - timeout: pollTimeout, - headers: options.headers, - timeout_per_call: options.timeout, - verify_tls: options.verify_tls, - }); - } - - return initial; -} - -async function request(pathValue, payload = {}, options = {}) { - const cfg = config(); - const resolvedPath = pathValue || options.path || cfg.responsesPath; - - if (!resolvedPath) { - return { - success: false, - error: 'project_id_missing', - message: 'PROJECT_ID is not defined; cannot resolve AI proxy endpoint.', - }; - } - - if (!cfg.projectUuid) { - return { - success: false, - error: 'project_uuid_missing', - message: 'PROJECT_UUID is not defined; aborting AI request.', - }; - } - - const bodyPayload = { ...(payload || {}) }; - if (!bodyPayload.project_uuid) { - bodyPayload.project_uuid = cfg.projectUuid; - } - - const url = buildUrl(resolvedPath, cfg.baseUrl); - const timeout = resolveTimeout(options.timeout, cfg.timeout); - const verifyTls = resolveVerifyTls(options.verify_tls, cfg.verifyTls); - - const headers = { - Accept: 'application/json', - 'Content-Type': 'application/json', - [cfg.projectHeader]: cfg.projectUuid, - }; - if (Array.isArray(options.headers)) { - for (const header of options.headers) { - if (typeof header === 'string' && header.includes(':')) { - const [name, value] = header.split(':', 2); - headers[name.trim()] = value.trim(); - } - } - } - - const body = JSON.stringify(bodyPayload); - return sendRequest(url, 'POST', body, headers, timeout, verifyTls); -} - -async function fetchStatus(aiRequestId, options = {}) { - const cfg = config(); - if (!cfg.projectUuid) { - return { - success: false, - error: 'project_uuid_missing', - message: 'PROJECT_UUID is not defined; aborting status check.', - }; - } - - const statusPath = resolveStatusPath(aiRequestId, cfg); - const url = buildUrl(statusPath, cfg.baseUrl); - const timeout = resolveTimeout(options.timeout, cfg.timeout); - const verifyTls = resolveVerifyTls(options.verify_tls, cfg.verifyTls); - - const headers = { - Accept: 'application/json', - [cfg.projectHeader]: cfg.projectUuid, - }; - if (Array.isArray(options.headers)) { - for (const header of options.headers) { - if (typeof header === 'string' && header.includes(':')) { - const [name, value] = header.split(':', 2); - headers[name.trim()] = value.trim(); - } - } - } - - return sendRequest(url, 'GET', null, headers, timeout, verifyTls); -} - -async function awaitResponse(aiRequestId, options = {}) { - const timeout = Number(options.timeout ?? 300); - const interval = Math.max(Number(options.interval ?? 5), 1); - const deadline = Date.now() + Math.max(timeout, interval) * 1000; - - let isPending = true; - - while (isPending) { - const statusResp = await fetchStatus(aiRequestId, { - headers: options.headers, - timeout: options.timeout_per_call, - verify_tls: options.verify_tls, - }); - - if (statusResp.success) { - const data = statusResp.data || {}; - if (data && typeof data === 'object') { - if (data.status === 'success') { - isPending = false; - return { - success: true, - status: 200, - data: data.response || data, - }; - } - if (data.status === 'failed') { - isPending = false; - return { - success: false, - status: 500, - error: String(data.error || 'AI request failed'), - data, - }; - } - } - } else { - return statusResp; - } - - if (Date.now() >= deadline) { - return { - success: false, - error: 'timeout', - message: 'Timed out waiting for AI response.', - }; - } - - await sleep(interval * 1000); - } -} - -function extractText(response) { - const payload = - response && typeof response === 'object' ? response.data || response : null; - if (!payload || typeof payload !== 'object') { - return ''; - } - - if (Array.isArray(payload.output)) { - let combined = ''; - for (const item of payload.output) { - if (!item || !Array.isArray(item.content)) { - continue; - } - for (const block of item.content) { - if ( - block && - typeof block === 'object' && - block.type === 'output_text' && - typeof block.text === 'string' && - block.text.length > 0 - ) { - combined += block.text; - } - } - } - if (combined) { - return combined; - } - } - - if ( - payload.choices && - payload.choices[0] && - payload.choices[0].message && - typeof payload.choices[0].message.content === 'string' - ) { - return payload.choices[0].message.content; - } - - return ''; -} - -function decodeJsonFromResponse(response) { - const text = extractText(response); - if (!text) { - throw new Error('No text found in AI response.'); - } - - const parsed = parseJson(text); - if (parsed.ok && parsed.value && typeof parsed.value === 'object') { - return parsed.value; - } - - const stripped = stripJsonFence(text); - if (stripped !== text) { - const parsedStripped = parseJson(stripped); - if ( - parsedStripped.ok && - parsedStripped.value && - typeof parsedStripped.value === 'object' - ) { - return parsedStripped.value; - } - throw new Error( - `JSON parse failed after stripping fences: ${parsedStripped.error}`, - ); - } - - throw new Error(`JSON parse failed: ${parsed.error}`); -} - -function config() { - if (CONFIG_CACHE) { - return CONFIG_CACHE; - } - - ensureEnvLoaded(); - - const baseUrl = process.env.AI_PROXY_BASE_URL || 'https://flatlogic.com'; - const projectId = process.env.PROJECT_ID || null; - let responsesPath = process.env.AI_RESPONSES_PATH || null; - if (!responsesPath && projectId) { - responsesPath = `/projects/${projectId}/ai-request`; - } - - const timeout = resolveTimeout(process.env.AI_TIMEOUT, 30); - const verifyTls = resolveVerifyTls(process.env.AI_VERIFY_TLS, true); - - CONFIG_CACHE = { - baseUrl, - responsesPath, - projectId, - projectUuid: process.env.PROJECT_UUID || null, - projectHeader: process.env.AI_PROJECT_HEADER || 'project-uuid', - defaultModel: process.env.AI_DEFAULT_MODEL || 'gpt-5-mini', - timeout, - verifyTls, - }; - - return CONFIG_CACHE; -} - -function buildUrl(pathValue, baseUrl) { - const trimmed = String(pathValue || '').trim(); - if (trimmed === '') { - return baseUrl; - } - if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { - return trimmed; - } - if (trimmed.startsWith('/')) { - return `${baseUrl}${trimmed}`; - } - return `${baseUrl}/${trimmed}`; -} - -function resolveStatusPath(aiRequestId, cfg) { - const basePath = (cfg.responsesPath || '').replace(/\/+$/, ''); - if (!basePath) { - return `/ai-request/${encodeURIComponent(String(aiRequestId))}/status`; - } - const normalized = basePath.endsWith('/ai-request') - ? basePath - : `${basePath}/ai-request`; - return `${normalized}/${encodeURIComponent(String(aiRequestId))}/status`; -} - -function sendRequest( - urlString, - method, - body, - headers, - timeoutSeconds, - verifyTls, -) { - return new Promise((resolve) => { - let targetUrl; - try { - targetUrl = new URL(urlString); - } catch (err) { - resolve({ - success: false, - error: 'invalid_url', - message: err.message, - }); - return; - } - - const isHttps = targetUrl.protocol === 'https:'; - const requestFn = isHttps ? https.request : http.request; - const options = { - protocol: targetUrl.protocol, - hostname: targetUrl.hostname, - port: targetUrl.port || (isHttps ? 443 : 80), - path: `${targetUrl.pathname}${targetUrl.search}`, - method: method.toUpperCase(), - headers, - timeout: Math.max(Number(timeoutSeconds || 30), 1) * 1000, - }; - if (isHttps) { - options.rejectUnauthorized = Boolean(verifyTls); - } - - const req = requestFn(options, (res) => { - let responseBody = ''; - res.setEncoding('utf8'); - res.on('data', (chunk) => { - responseBody += chunk; - }); - res.on('end', () => { - const status = res.statusCode || 0; - const parsed = parseJson(responseBody); - const payload = parsed.ok ? parsed.value : responseBody; - - if (status >= 200 && status < 300) { - const result = { - success: true, - status, - data: payload, - }; - if (!parsed.ok) { - result.json_error = parsed.error; - } - resolve(result); - return; - } - - const errorMessage = - parsed.ok && payload && typeof payload === 'object' - ? String( - payload.error || payload.message || 'AI proxy request failed', - ) - : String(responseBody || 'AI proxy request failed'); - - resolve({ - success: false, - status, - error: errorMessage, - response: payload, - json_error: parsed.ok ? undefined : parsed.error, - }); - }); - }); - - req.on('timeout', () => { - req.destroy(new Error('request_timeout')); - }); - - req.on('error', (err) => { - resolve({ - success: false, - error: 'request_failed', - message: err.message, - }); - }); - - if (body) { - req.write(body); - } - req.end(); - }); -} - -function parseJson(value) { - if (typeof value !== 'string' || value.trim() === '') { - return { ok: false, error: 'empty_response' }; - } - try { - return { ok: true, value: JSON.parse(value) }; - } catch (err) { - return { ok: false, error: err.message }; - } -} - -function stripJsonFence(text) { - const trimmed = text.trim(); - if (trimmed.startsWith('```json')) { - return trimmed - .replace(/^```json/, '') - .replace(/```$/, '') - .trim(); - } - if (trimmed.startsWith('```')) { - return trimmed.replace(/^```/, '').replace(/```$/, '').trim(); - } - return text; -} - -function resolveTimeout(value, fallback) { - const parsed = Number.parseInt(String(value ?? fallback), 10); - return Number.isNaN(parsed) ? Number(fallback) : parsed; -} - -function resolveVerifyTls(value, fallback) { - if (value === undefined || value === null) { - return Boolean(fallback); - } - return String(value).toLowerCase() !== 'false' && String(value) !== '0'; -} - -function ensureEnvLoaded() { - if (process.env.PROJECT_UUID && process.env.PROJECT_ID) { - return; - } - - const envPath = path.resolve(__dirname, '../../../../.env'); - if (!fs.existsSync(envPath)) { - return; - } - - let content; - try { - content = fs.readFileSync(envPath, 'utf8'); - } catch (err) { - throw new Error(`Failed to read executor .env: ${err.message}`); - } - - for (const line of content.split(/\r?\n/)) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) { - continue; - } - const [rawKey, ...rest] = trimmed.split('='); - const key = rawKey.trim(); - if (!key) { - continue; - } - const value = rest - .join('=') - .trim() - .replace(/^['"]|['"]$/g, ''); - if (!process.env[key]) { - process.env[key] = value; - } - } -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -module.exports = { - LocalAIApi, - createResponse, - request, - fetchStatus, - awaitResponse, - extractText, - decodeJsonFromResponse, -}; diff --git a/backend/src/auth/auth.js b/backend/src/auth/auth.js deleted file mode 100644 index d26df22..0000000 --- a/backend/src/auth/auth.js +++ /dev/null @@ -1,80 +0,0 @@ -const config = require('../config'); -const providers = config.providers; -const helpers = require('../helpers'); -const db = require('../db/models'); - -const passport = require('passport'); -const JWTstrategy = require('passport-jwt').Strategy; -const ExtractJWT = require('passport-jwt').ExtractJwt; -const GoogleStrategy = require('passport-google-oauth2').Strategy; -const MicrosoftStrategy = require('passport-microsoft').Strategy; -const UsersDBApi = require('../db/api/users'); - -passport.use( - new JWTstrategy( - { - passReqToCallback: true, - secretOrKey: config.secret_key, - jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(), - }, - async (req, token, done) => { - try { - // Use lightweight auth query - only loads essential fields + permissions - const user = await UsersDBApi.findByForAuth({ - email: token.user.email, - }); - - if (user && user.disabled) { - return done(new Error(`User '${user.email}' is disabled`)); - } - - req.currentUser = user; - - return done(null, user); - } catch (error) { - done(error); - } - }, - ), -); - -passport.use( - new GoogleStrategy( - { - clientID: config.google.clientId, - clientSecret: config.google.clientSecret, - callbackURL: config.apiUrl + '/auth/signin/google/callback', - passReqToCallback: true, - }, - function (request, accessToken, refreshToken, profile, done) { - socialStrategy(profile.email, profile, providers.GOOGLE, done); - }, - ), -); - -passport.use( - new MicrosoftStrategy( - { - clientID: config.microsoft.clientId, - clientSecret: config.microsoft.clientSecret, - callbackURL: config.apiUrl + '/auth/signin/microsoft/callback', - passReqToCallback: true, - }, - function (request, accessToken, refreshToken, profile, done) { - const email = profile._json.mail || profile._json.userPrincipalName; - socialStrategy(email, profile, providers.MICROSOFT, done); - }, - ), -); - -function socialStrategy(email, profile, provider, done) { - db.users.findOrCreate({ where: { email, provider } }).then(([user]) => { - const body = { - id: user.id, - email: user.email, - name: profile.displayName, - }; - const token = helpers.jwtSign({ user: body }); - return done(null, { token }); - }); -} diff --git a/backend/src/auth/auth.ts b/backend/src/auth/auth.ts new file mode 100644 index 0000000..504361b --- /dev/null +++ b/backend/src/auth/auth.ts @@ -0,0 +1,228 @@ +import type { Request } from 'express'; +import passport from 'passport'; +import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt'; +import type { VerifiedCallback } from 'passport-jwt'; +import { Strategy as GoogleStrategy } from 'passport-google-oauth2'; +import { Strategy as MicrosoftStrategy } from 'passport-microsoft'; +import type { VerifyCallback as OAuthVerifyCallback } from 'passport-google-oauth2'; + +import config from '../config.ts'; +import db from '../db/models/index.ts'; +import UsersDBApi from '../db/api/users.ts'; +import { jwtSign } from '../helpers.ts'; +import { setCurrentUser, setSocialAuthToken } from '../utils/request-context.ts'; +import type { + AuthTokenPayload, + CurrentUser, + SocialAuthUserRecord, + UserRecord, +} from '../types/index.ts'; + +interface JwtPayload { + user?: { + email?: string; + }; +} + +interface SocialProfile { + email?: string; + displayName?: string; + _json?: { + mail?: string; + userPrincipalName?: string; + }; +} + +type SocialDone = OAuthVerifyCallback; + +function isJwtPayload(value: unknown): value is JwtPayload { + return ( + value !== null && + typeof value === 'object' && + 'user' in value && + value.user !== null && + typeof value.user === 'object' + ); +} + +function isSocialProfile(value: unknown): value is SocialProfile { + return value !== null && typeof value === 'object'; +} + +function getJwtEmail(token: unknown): string | undefined { + if (!isJwtPayload(token)) return undefined; + return typeof token.user?.email === 'string' ? token.user.email : undefined; +} + +function getGoogleEmail(profile: unknown): string | undefined { + if (!isSocialProfile(profile)) return undefined; + return typeof profile.email === 'string' ? profile.email : undefined; +} + +function getMicrosoftEmail(profile: unknown): string | undefined { + if (!isSocialProfile(profile)) return undefined; + const json = profile._json; + return json?.mail || json?.userPrincipalName; +} + +function getDisplayName(profile: SocialProfile): string | undefined { + return typeof profile.displayName === 'string' + ? profile.displayName + : undefined; +} + +function toCurrentUser(user: UserRecord): CurrentUser { + const currentUser: CurrentUser = { + id: user.id, + }; + + if (user.email !== null) { + currentUser.email = user.email; + } + if (user.firstName !== null && user.firstName !== undefined) { + currentUser.firstName = user.firstName; + } + if (user.lastName !== null && user.lastName !== undefined) { + currentUser.lastName = user.lastName; + } + if (user.app_role !== undefined) { + currentUser.app_role = user.app_role; + } + if (user.custom_permissions !== undefined) { + currentUser.custom_permissions = user.custom_permissions; + } + if (user.app_role_permissions !== undefined) { + currentUser.app_role_permissions = user.app_role_permissions; + } + + return currentUser; +} + +async function socialStrategy( + req: Request, + email: string | undefined, + profile: unknown, + provider: string, + done: SocialDone, +): Promise { + if (!email || !isSocialProfile(profile)) { + done(new Error('Social profile email is missing')); + return; + } + + try { + const [user]: [SocialAuthUserRecord, boolean] = await db.users.findOrCreate({ + where: { email, provider }, + }); + const body: AuthTokenPayload['user'] = { + id: user.id, + email: user.email, + }; + const token = jwtSign({ + user: { + ...body, + name: getDisplayName(profile), + }, + }); + + setSocialAuthToken(req, token); + done(null, {}); + } catch (error) { + done(error); + } +} + +async function verifyJwt( + req: Request, + token: unknown, + done: VerifiedCallback, +): Promise { + try { + const email = getJwtEmail(token); + const user: UserRecord | null = email + ? await UsersDBApi.findByForAuth({ email }) + : null; + + if (user && user.disabled) { + done(new Error(`User '${user.email}' is disabled`)); + return; + } + + if (user) { + setCurrentUser(req, toCurrentUser(user)); + } + + done(null, user); + } catch (error) { + done(error); + } +} + +passport.use( + new JwtStrategy( + { + passReqToCallback: true, + secretOrKey: config.secret_key, + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + }, + ( + req: Request, + token: unknown, + done: VerifiedCallback, + ) => { + void verifyJwt(req, token, done); + }, + ), +); + +passport.use( + new GoogleStrategy( + { + clientID: config.google.clientId, + clientSecret: config.google.clientSecret, + callbackURL: `${config.apiUrl}/auth/signin/google/callback`, + passReqToCallback: true, + }, + ( + req: Request, + _accessToken: string, + _refreshToken: string, + profile: unknown, + done: SocialDone, + ) => { + void socialStrategy( + req, + getGoogleEmail(profile), + profile, + config.providers.GOOGLE, + done, + ); + }, + ), +); + +passport.use( + new MicrosoftStrategy( + { + clientID: config.microsoft.clientId, + clientSecret: config.microsoft.clientSecret, + callbackURL: `${config.apiUrl}/auth/signin/microsoft/callback`, + passReqToCallback: true, + }, + ( + req: Request, + _accessToken: string, + _refreshToken: string, + profile: unknown, + done: SocialDone, + ) => { + void socialStrategy( + req, + getMicrosoftEmail(profile), + profile, + config.providers.MICROSOFT, + done, + ); + }, + ), +); diff --git a/backend/src/auth/passport-middleware.ts b/backend/src/auth/passport-middleware.ts new file mode 100644 index 0000000..6f54511 --- /dev/null +++ b/backend/src/auth/passport-middleware.ts @@ -0,0 +1,56 @@ +import type { RequestHandler } from 'express'; +import passport from 'passport'; +import type { AuthenticateOptions } from 'passport'; + +import type { CurrentUser } from '../types/index.ts'; + +type JwtAuthenticationUser = CurrentUser | false | null | undefined; +type JwtAuthenticationCallback = ( + error: unknown, + user: JwtAuthenticationUser, +) => void | Promise; + +function isRequestHandler(value: unknown): value is RequestHandler { + return typeof value === 'function'; +} + +function authenticateJwt(): RequestHandler { + const middleware: unknown = passport.authenticate('jwt', { session: false }); + + if (!isRequestHandler(middleware)) { + throw new Error('Passport JWT authentication middleware is unavailable.'); + } + + return middleware; +} + +function authenticateJwtWithCallback( + callback: JwtAuthenticationCallback, +): RequestHandler { + const middleware: unknown = passport.authenticate( + 'jwt', + { session: false }, + callback, + ); + + if (!isRequestHandler(middleware)) { + throw new Error('Passport JWT authentication middleware is unavailable.'); + } + + return middleware; +} + +function authenticatePassport( + strategy: string, + options: AuthenticateOptions, +): RequestHandler { + const middleware: unknown = passport.authenticate(strategy, options); + + if (!isRequestHandler(middleware)) { + throw new Error(`Passport ${strategy} authentication middleware is unavailable.`); + } + + return middleware; +} + +export { authenticateJwt, authenticateJwtWithCallback, authenticatePassport }; diff --git a/backend/src/config.js b/backend/src/config.js deleted file mode 100644 index 20df96b..0000000 --- a/backend/src/config.js +++ /dev/null @@ -1,102 +0,0 @@ -const os = require('os'); -const path = require('path'); - -require('dotenv').config({ path: path.resolve(__dirname, '../.env') }); - -const { validateEnv } = require('./utils/env-validation'); -validateEnv(); - -const config = { - gcloud: { - bucket: 'fldemo-files', - hash: 'afeefb9d49f5b7977577876b99532ac7', - }, - s3: { - bucket: process.env.AWS_S3_BUCKET || '', - region: process.env.AWS_S3_REGION || 'us-east-1', - accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '', - prefix: process.env.AWS_S3_PREFIX || 'afeefb9d49f5b7977577876b99532ac7', - // Timeout configuration (in milliseconds) - connectionTimeout: - parseInt(process.env.AWS_S3_CONNECTION_TIMEOUT, 10) || 5000, - requestTimeout: parseInt(process.env.AWS_S3_REQUEST_TIMEOUT, 10) || 30000, - // Retry configuration - maxAttempts: parseInt(process.env.AWS_S3_MAX_ATTEMPTS, 10) || 3, - // Connection pool configuration - maxSockets: parseInt(process.env.AWS_S3_MAX_SOCKETS, 10) || 50, - keepAlive: process.env.AWS_S3_KEEP_ALIVE !== 'false', - // Presigned URL expiry (in seconds) - presignExpirySeconds: - parseInt(process.env.AWS_S3_PRESIGN_EXPIRY, 10) || 3600, - }, - bcrypt: { - saltRounds: 12, - }, - admin_pass: process.env.ADMIN_PASS || '88dbeaf8', - user_pass: process.env.USER_PASS || 'c3baadeda5c6', - admin_email: process.env.ADMIN_EMAIL || 'admin@flatlogic.com', - providers: { - LOCAL: 'local', - GOOGLE: 'google', - MICROSOFT: 'microsoft', - }, - secret_key: process.env.SECRET_KEY || '88dbeaf8-e906-405e-9e41-c3baadeda5c6', - remote: '', - port: process.env.NODE_ENV === 'production' ? '' : '8080', - hostUI: process.env.NODE_ENV === 'production' ? '' : 'http://localhost', - portUI: process.env.NODE_ENV === 'production' ? '' : '3000', - - portUIProd: process.env.NODE_ENV === 'production' ? '' : ':3000', - - swaggerUI: process.env.NODE_ENV === 'production' ? '' : 'http://localhost', - swaggerPort: process.env.NODE_ENV === 'production' ? '' : ':8080', - google: { - clientId: process.env.GOOGLE_CLIENT_ID || '', - clientSecret: process.env.GOOGLE_CLIENT_SECRET || '', - }, - microsoft: { - clientId: process.env.MS_CLIENT_ID || '', - clientSecret: process.env.MS_CLIENT_SECRET || '', - }, - uploadDir: os.tmpdir(), - // Local cache for S3 proxy downloads (improves performance for repeated requests) - s3CacheDir: process.env.S3_CACHE_DIR || path.join(os.tmpdir(), 's3-cache'), - s3CacheEnabled: process.env.S3_CACHE_ENABLED !== 'false', // Enabled by default - s3CacheMaxAge: parseInt(process.env.S3_CACHE_MAX_AGE, 10) || 86400, // 24 hours - email: { - from: 'Tour Builder Platform ', - host: 'email-smtp.us-east-1.amazonaws.com', - port: 587, - auth: { - user: process.env.EMAIL_USER || '', - pass: process.env.EMAIL_PASS, - }, - tls: { - rejectUnauthorized: process.env.EMAIL_TLS_REJECT_UNAUTHORIZED !== 'false', - }, - }, - roles: { - admin: 'Administrator', - - user: 'Analytics Viewer', - }, - - project_uuid: '88dbeaf8-e906-405e-9e41-c3baadeda5c6', - flHost: - process.env.NODE_ENV === 'production' || - process.env.NODE_ENV === 'dev_stage' - ? 'https://flatlogic.com/projects' - : 'http://localhost:3000/projects', - - gpt_key: process.env.GPT_KEY || '', -}; - -config.host = - process.env.NODE_ENV === 'production' ? config.remote : 'http://localhost'; -config.apiUrl = `${config.host}${config.port ? `:${config.port}` : ``}/api`; -config.swaggerUrl = `${config.swaggerUI}${config.swaggerPort}`; -config.uiUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}/#`; -config.backUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}`; - -module.exports = config; diff --git a/backend/src/config.ts b/backend/src/config.ts new file mode 100644 index 0000000..cf7e016 --- /dev/null +++ b/backend/src/config.ts @@ -0,0 +1,96 @@ +import os from 'node:os'; +import path from 'node:path'; + +import './load-env.ts'; +import { validateEnv } from './utils/env-validation.ts'; +import type { BackendConfig } from './types/index.ts'; + +validateEnv(); + +function envNumber(name: string, fallback: number): number { + const parsed = Number.parseInt(process.env[name] || '', 10); + return Number.isFinite(parsed) ? parsed : fallback; +} + +const isProduction = process.env.NODE_ENV === 'production'; +const remote = ''; +const port = isProduction ? '' : '8080'; +const hostUI = isProduction ? '' : 'http://localhost'; +const portUI = isProduction ? '' : '3000'; +const swaggerUI = isProduction ? '' : 'http://localhost'; +const swaggerPort = isProduction ? '' : ':8080'; +const host = isProduction ? remote : 'http://localhost'; + +const config: BackendConfig = { + gcloud: { + bucket: 'fldemo-files', + hash: 'afeefb9d49f5b7977577876b99532ac7', + }, + s3: { + bucket: process.env.AWS_S3_BUCKET || '', + region: process.env.AWS_S3_REGION || 'us-east-1', + accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '', + prefix: process.env.AWS_S3_PREFIX || 'afeefb9d49f5b7977577876b99532ac7', + connectionTimeout: envNumber('AWS_S3_CONNECTION_TIMEOUT', 5000), + requestTimeout: envNumber('AWS_S3_REQUEST_TIMEOUT', 30000), + maxAttempts: envNumber('AWS_S3_MAX_ATTEMPTS', 3), + maxSockets: envNumber('AWS_S3_MAX_SOCKETS', 50), + keepAlive: process.env.AWS_S3_KEEP_ALIVE !== 'false', + presignExpirySeconds: envNumber('AWS_S3_PRESIGN_EXPIRY', 3600), + }, + bcrypt: { + saltRounds: 12, + }, + admin_pass: process.env.ADMIN_PASS || '88dbeaf8', + user_pass: process.env.USER_PASS || 'c3baadeda5c6', + admin_email: process.env.ADMIN_EMAIL || 'admin@flatlogic.com', + providers: { + LOCAL: 'local', + GOOGLE: 'google', + MICROSOFT: 'microsoft', + }, + secret_key: process.env.SECRET_KEY || '88dbeaf8-e906-405e-9e41-c3baadeda5c6', + remote, + port, + host, + hostUI, + portUI, + portUIProd: isProduction ? '' : ':3000', + swaggerUI, + swaggerPort, + google: { + clientId: process.env.GOOGLE_CLIENT_ID || '', + clientSecret: process.env.GOOGLE_CLIENT_SECRET || '', + }, + microsoft: { + clientId: process.env.MS_CLIENT_ID || '', + clientSecret: process.env.MS_CLIENT_SECRET || '', + }, + uploadDir: os.tmpdir(), + s3CacheDir: process.env.S3_CACHE_DIR || path.join(os.tmpdir(), 's3-cache'), + s3CacheEnabled: process.env.S3_CACHE_ENABLED !== 'false', + s3CacheMaxAge: envNumber('S3_CACHE_MAX_AGE', 86400), + email: { + from: 'Tour Builder Platform ', + host: 'email-smtp.us-east-1.amazonaws.com', + port: 587, + auth: { + user: process.env.EMAIL_USER || '', + pass: process.env.EMAIL_PASS || '', + }, + tls: { + rejectUnauthorized: process.env.EMAIL_TLS_REJECT_UNAUTHORIZED !== 'false', + }, + }, + roles: { + admin: 'Administrator', + user: 'Analytics Viewer', + }, + apiUrl: `${host}${port ? `:${port}` : ''}/api`, + swaggerUrl: `${swaggerUI}${swaggerPort}`, + uiUrl: `${hostUI}${portUI ? `:${portUI}` : ''}/#`, + backUrl: `${hostUI}${portUI ? `:${portUI}` : ''}`, +}; + +export default config; diff --git a/backend/src/contracts/entity-options.js b/backend/src/contracts/entity-options.js deleted file mode 100644 index 2f11d26..0000000 --- a/backend/src/contracts/entity-options.js +++ /dev/null @@ -1,95 +0,0 @@ -/** - * @typedef {Object} ServiceCreateOptions - * @property {Object} data - * @property {Object} [currentUser] - * @property {Object} [transaction] - * @property {Object} [runtimeContext] - * @property {boolean} [sendInvitationEmails] - * @property {string} [host] - */ - -/** - * @typedef {Object} EntityIdOptions - * @property {string} id - * @property {Object} [currentUser] - * @property {Object} [transaction] - * @property {Object} [runtimeContext] - */ - -/** - * @typedef {Object} DeleteByIdsOptions - * @property {string[]} ids - * @property {Object} [currentUser] - * @property {Object} [transaction] - * @property {Object} [runtimeContext] - */ - -/** - * @typedef {Object} AutocompleteOptions - * @property {string} [query] - * @property {number} [limit] - * @property {number} [offset] - */ - -/** - * @typedef {Object} UpdateOptions - * @property {string} id - * @property {Object} data - * @property {Object} [currentUser] - * @property {Object} [transaction] - * @property {Object} [runtimeContext] - */ - -function assertOptionsObject(options, contractName, methodName) { - if (!options || typeof options !== 'object' || Array.isArray(options)) { - throw new TypeError( - `${contractName}.${methodName} expects an options object`, - ); - } -} - -function assertCreateOptions(options, contractName) { - assertOptionsObject(options, contractName, 'create'); - - if (options.data === undefined) { - throw new TypeError(`${contractName}.create requires { data }`); - } -} - -function assertIdOptions(options, contractName, methodName) { - assertOptionsObject(options, contractName, methodName); - - if (!options.id) { - throw new TypeError(`${contractName}.${methodName} requires { id }`); - } -} - -function assertDeleteByIdsOptions(options, contractName) { - assertOptionsObject(options, contractName, 'deleteByIds'); - - if (!Array.isArray(options.ids)) { - throw new TypeError(`${contractName}.deleteByIds requires { ids }`); - } -} - -function assertAutocompleteOptions(options, contractName) { - assertOptionsObject(options, contractName, 'findAllAutocomplete'); -} - -function assertUpdateOptions(options, contractName) { - assertOptionsObject(options, contractName, 'update'); - - if (!options.id || options.data === undefined) { - throw new TypeError( - `${contractName}.update requires { id, data } in the options object`, - ); - } -} - -module.exports = { - assertAutocompleteOptions, - assertCreateOptions, - assertDeleteByIdsOptions, - assertIdOptions, - assertUpdateOptions, -}; diff --git a/backend/src/contracts/entity-options.ts b/backend/src/contracts/entity-options.ts new file mode 100644 index 0000000..d58cb1f --- /dev/null +++ b/backend/src/contracts/entity-options.ts @@ -0,0 +1,87 @@ +import type { + AutocompleteOptions, + CreateOptions, + DeleteByIdsOptions, + EntityIdOptions, + UpdateOptions, +} from '../types/index.ts'; + +interface ContractOptions { + data?: unknown; + id?: unknown; + ids?: unknown; +} + +function assertOptionsObject( + options: unknown, + contractName: string, + methodName: string, +): asserts options is ContractOptions { + if (!options || typeof options !== 'object' || Array.isArray(options)) { + throw new TypeError( + `${contractName}.${methodName} expects an options object`, + ); + } +} + +function assertCreateOptions( + options: unknown, + contractName: string, +): asserts options is CreateOptions { + assertOptionsObject(options, contractName, 'create'); + + if (options.data === undefined) { + throw new TypeError(`${contractName}.create requires { data }`); + } +} + +function assertIdOptions( + options: unknown, + contractName: string, + methodName: string, +): asserts options is EntityIdOptions { + assertOptionsObject(options, contractName, methodName); + + if (!options.id) { + throw new TypeError(`${contractName}.${methodName} requires { id }`); + } +} + +function assertDeleteByIdsOptions( + options: unknown, + contractName: string, +): asserts options is DeleteByIdsOptions { + assertOptionsObject(options, contractName, 'deleteByIds'); + + if (!Array.isArray(options.ids)) { + throw new TypeError(`${contractName}.deleteByIds requires { ids }`); + } +} + +function assertAutocompleteOptions( + options: unknown, + contractName: string, +): asserts options is AutocompleteOptions { + assertOptionsObject(options, contractName, 'findAllAutocomplete'); +} + +function assertUpdateOptions( + options: unknown, + contractName: string, +): asserts options is UpdateOptions { + assertOptionsObject(options, contractName, 'update'); + + if (!options.id || options.data === undefined) { + throw new TypeError( + `${contractName}.update requires { id, data } in the options object`, + ); + } +} + +export { + assertAutocompleteOptions, + assertCreateOptions, + assertDeleteByIdsOptions, + assertIdOptions, + assertUpdateOptions, +}; diff --git a/backend/src/db/api/access_logs.js b/backend/src/db/api/access_logs.ts similarity index 58% rename from backend/src/db/api/access_logs.js rename to backend/src/db/api/access_logs.ts index c97ba25..0223c70 100644 --- a/backend/src/db/api/access_logs.js +++ b/backend/src/db/api/access_logs.ts @@ -1,28 +1,34 @@ -const GenericDBApi = require('./base.api'); -const db = require('../models'); +import GenericDBApi from './base.api.ts'; +import db from '../models/index.ts'; +import type { + AccessLogAssociationConfig, + AccessLogData, + AccessLogFieldMapping, + AccessLogRelationFilterConfig, +} from '../../types/index.ts'; class Access_logsDBApi extends GenericDBApi { - static get MODEL() { + static override get MODEL(): unknown { return db.access_logs; } - static get TABLE_NAME() { + static override get TABLE_NAME(): string { return 'access_logs'; } - static get SEARCHABLE_FIELDS() { + static override get SEARCHABLE_FIELDS(): string[] { return ['path', 'ip_address', 'user_agent']; } - static get RANGE_FIELDS() { + static override get RANGE_FIELDS(): string[] { return ['accessed_at']; } - static get ENUM_FIELDS() { + static override get ENUM_FIELDS(): string[] { return ['environment']; } - static get CSV_FIELDS() { + static override get CSV_FIELDS(): string[] { return [ 'id', 'environment', @@ -34,29 +40,29 @@ class Access_logsDBApi extends GenericDBApi { ]; } - static get AUTOCOMPLETE_FIELD() { + static override get AUTOCOMPLETE_FIELD(): string { return 'path'; } - static get ASSOCIATIONS() { + static override get ASSOCIATIONS(): AccessLogAssociationConfig[] { return [ { field: 'project', setter: 'setProject', isArray: false }, { field: 'user', setter: 'setUser', isArray: false }, ]; } - static get FIND_BY_INCLUDES() { + static override get FIND_BY_INCLUDES(): unknown[] { return [{ association: 'project' }, { association: 'user' }]; } - static get FIND_ALL_INCLUDES() { + static override get FIND_ALL_INCLUDES(): unknown[] { return [ { model: db.projects, as: 'project', required: false }, { model: db.users, as: 'user', required: false }, ]; } - static get RELATION_FILTERS() { + static override get RELATION_FILTERS(): AccessLogRelationFilterConfig[] { return [ { filterKey: 'project', @@ -73,7 +79,7 @@ class Access_logsDBApi extends GenericDBApi { ]; } - static getFieldMapping(data) { + static override getFieldMapping(data: AccessLogData): AccessLogFieldMapping { return { id: data.id || undefined, environment: data.environment || null, @@ -85,4 +91,4 @@ class Access_logsDBApi extends GenericDBApi { } } -module.exports = Access_logsDBApi; +export default Access_logsDBApi; diff --git a/backend/src/db/api/asset_variants.js b/backend/src/db/api/asset_variants.ts similarity index 54% rename from backend/src/db/api/asset_variants.js rename to backend/src/db/api/asset_variants.ts index c268c8a..e12a232 100644 --- a/backend/src/db/api/asset_variants.js +++ b/backend/src/db/api/asset_variants.ts @@ -1,28 +1,34 @@ -const GenericDBApi = require('./base.api'); -const db = require('../models'); +import GenericDBApi from './base.api.ts'; +import db from '../models/index.ts'; +import type { + AssetVariantAssociationConfig, + AssetVariantData, + AssetVariantFieldMapping, + AssetVariantRelationFilterConfig, +} from '../../types/index.ts'; class Asset_variantsDBApi extends GenericDBApi { - static get MODEL() { + static override get MODEL(): unknown { return db.asset_variants; } - static get TABLE_NAME() { + static override get TABLE_NAME(): string { return 'asset_variants'; } - static get SEARCHABLE_FIELDS() { + static override get SEARCHABLE_FIELDS(): string[] { return ['cdn_url']; } - static get RANGE_FIELDS() { + static override get RANGE_FIELDS(): string[] { return ['width_px', 'height_px', 'size_mb']; } - static get ENUM_FIELDS() { + static override get ENUM_FIELDS(): string[] { return ['variant_type']; } - static get CSV_FIELDS() { + static override get CSV_FIELDS(): string[] { return [ 'id', 'variant_type', @@ -34,19 +40,19 @@ class Asset_variantsDBApi extends GenericDBApi { ]; } - static get AUTOCOMPLETE_FIELD() { + static override get AUTOCOMPLETE_FIELD(): string { return 'variant_type'; } - static get ASSOCIATIONS() { + static override get ASSOCIATIONS(): AssetVariantAssociationConfig[] { return [{ field: 'asset', setter: 'setAsset', isArray: false }]; } - static get FIND_BY_INCLUDES() { + static override get FIND_BY_INCLUDES(): unknown[] { return [{ association: 'asset' }]; } - static get FIND_ALL_INCLUDES() { + static override get FIND_ALL_INCLUDES(): unknown[] { return [ { model: db.assets, @@ -56,7 +62,7 @@ class Asset_variantsDBApi extends GenericDBApi { ]; } - static get RELATION_FILTERS() { + static override get RELATION_FILTERS(): AssetVariantRelationFilterConfig[] { return [ { filterKey: 'asset', @@ -67,7 +73,7 @@ class Asset_variantsDBApi extends GenericDBApi { ]; } - static getFieldMapping(data) { + static override getFieldMapping(data: AssetVariantData): AssetVariantFieldMapping { return { id: data.id || undefined, assetId: data.assetId || null, @@ -81,4 +87,4 @@ class Asset_variantsDBApi extends GenericDBApi { } } -module.exports = Asset_variantsDBApi; +export default Asset_variantsDBApi; diff --git a/backend/src/db/api/assets.js b/backend/src/db/api/assets.ts similarity index 60% rename from backend/src/db/api/assets.js rename to backend/src/db/api/assets.ts index 639b2e3..a5ebe51 100644 --- a/backend/src/db/api/assets.js +++ b/backend/src/db/api/assets.ts @@ -1,16 +1,26 @@ -const GenericDBApi = require('./base.api'); -const db = require('../models'); +import GenericDBApi from './base.api.ts'; +import db from '../models/index.ts'; +import type { + AssetData, + AssetFieldMapping, + AssetUsageType, + AssetsDbApi, + DbAssociationConfig, + DbRelationFilterConfig, +} from '../../types/index.ts'; class AssetsDBApi extends GenericDBApi { - static get MODEL() { + declare static findBy: AssetsDbApi['findBy']; + + static override get MODEL(): unknown { return db.assets; } - static get TABLE_NAME() { + static override get TABLE_NAME(): string { return 'assets'; } - static get SEARCHABLE_FIELDS() { + static override get SEARCHABLE_FIELDS(): string[] { return [ 'name', 'cdn_url', @@ -21,19 +31,19 @@ class AssetsDBApi extends GenericDBApi { ]; } - static get RANGE_FIELDS() { + static override get RANGE_FIELDS(): string[] { return ['size_mb', 'width_px', 'height_px', 'duration_sec', 'frame_rate']; } - static get ENUM_FIELDS() { + static override get ENUM_FIELDS(): string[] { return ['asset_type', 'type', 'is_public']; } - static get UUID_FIELDS() { + static override get UUID_FIELDS(): string[] { return ['projectId']; } - static get CSV_FIELDS() { + static override get CSV_FIELDS(): string[] { return [ 'id', 'name', @@ -47,26 +57,26 @@ class AssetsDBApi extends GenericDBApi { ]; } - static get AUTOCOMPLETE_FIELD() { + static override get AUTOCOMPLETE_FIELD(): string { return 'name'; } - static get ASSOCIATIONS() { + static override get ASSOCIATIONS(): DbAssociationConfig[] { return [{ field: 'project', setter: 'setProject', isArray: false }]; } - static get FIND_BY_INCLUDES() { + static override get FIND_BY_INCLUDES(): unknown[] { return [ { association: 'asset_variants_asset' }, { association: 'project' }, ]; } - static get FIND_ALL_INCLUDES() { + static override get FIND_ALL_INCLUDES(): unknown[] { return [{ model: db.projects, as: 'project', required: false }]; } - static get RELATION_FILTERS() { + static override get RELATION_FILTERS(): DbRelationFilterConfig[] { return [ { filterKey: 'project', @@ -77,12 +87,12 @@ class AssetsDBApi extends GenericDBApi { ]; } - static getFieldMapping(data) { + static override getFieldMapping(data: AssetData): AssetFieldMapping { return { id: data.id || undefined, name: data.name || null, asset_type: data.asset_type || null, - type: data.type || 'general', + type: data.type || defaultAssetUsageType, cdn_url: data.cdn_url || null, storage_key: data.storage_key || null, mime_type: data.mime_type || null, @@ -100,4 +110,6 @@ class AssetsDBApi extends GenericDBApi { } } -module.exports = AssetsDBApi; +const defaultAssetUsageType: AssetUsageType = 'general'; + +export default AssetsDBApi; diff --git a/backend/src/db/api/base.api.js b/backend/src/db/api/base.api.js deleted file mode 100644 index e1c0dda..0000000 --- a/backend/src/db/api/base.api.js +++ /dev/null @@ -1,517 +0,0 @@ -const db = require('../models'); -const Utils = require('../utils'); -const { parse } = require('json2csv'); -const { logger } = require('../../utils/logger'); -const { - assertAutocompleteOptions, - assertCreateOptions, - assertDeleteByIdsOptions, - assertIdOptions, - assertUpdateOptions, -} = require('../../contracts/entity-options'); - -const Sequelize = db.Sequelize; -const Op = Sequelize.Op; - -class GenericDBApi { - static get MODEL() { - throw new Error('MODEL must be defined in subclass'); - } - - static get TABLE_NAME() { - return this.MODEL.getTableName(); - } - - static get SEARCHABLE_FIELDS() { - return []; - } - - static get RANGE_FIELDS() { - return []; - } - - static get ENUM_FIELDS() { - return []; - } - - /** - * UUID fields that require validation before querying. - * These are typically foreign key fields like 'projectId'. - * Invalid UUIDs will return empty results instead of causing DB errors. - * Override in subclass to specify fields. - * Example: return ['projectId', 'userId']; - */ - static get UUID_FIELDS() { - return []; - } - - static get RELATION_FILTERS() { - return []; - } - - static get CSV_FIELDS() { - return ['id', 'createdAt']; - } - - static get SORTABLE_FIELDS() { - return Object.keys(this.MODEL.rawAttributes || {}); - } - - static get AUTOCOMPLETE_FIELD() { - return 'name'; - } - - static get ASSOCIATIONS() { - return []; - } - - static get FIND_BY_INCLUDES() { - return []; - } - - static get FIND_ALL_INCLUDES() { - return []; - } - - /** - * Fields that should be automatically JSON-stringified - * Override in subclass to specify fields. - * Example: return ['settings_json', 'metadata_json']; - */ - static get JSON_FIELDS() { - return []; - } - - /** - * Custom field transformers for data mapping. - * Override in subclass to add custom transformations. - * Example: - * return { - * email: (value) => value?.toLowerCase().trim(), - * slug: (value) => value?.toLowerCase().replace(/\s+/g, '-'), - * }; - */ - static get FIELD_TRANSFORMERS() { - return {}; - } - - /** - * Field mapping configuration for declarative field handling. - * Override in subclass to specify how fields should be mapped. - * Example: - * return { - * name: { default: null }, - * sort_order: { default: 0 }, - * is_active: { default: true }, - * }; - */ - static get FIELD_DEFAULTS() { - return {}; - } - - /** - * Transform input data for database operations. - * Template Method Pattern: Uses JSON_FIELDS, FIELD_TRANSFORMERS, and FIELD_DEFAULTS - * to declaratively transform data, reducing boilerplate in subclasses. - * - * Override this method for complex custom transformations that can't be - * expressed declaratively. - * - * @param {Object} data - Input data to transform - * @returns {Object} - Transformed data ready for database - */ - static getFieldMapping(data) { - if (!data) return data; - const mapped = { ...data }; - - // Apply field defaults - for (const [field, config] of Object.entries(this.FIELD_DEFAULTS)) { - if (mapped[field] === undefined) { - mapped[field] = config.default; - } else if (mapped[field] === null && config.nullDefault !== undefined) { - mapped[field] = config.nullDefault; - } - } - - // Auto-stringify JSON fields - for (const field of this.JSON_FIELDS) { - if (mapped[field] !== undefined && mapped[field] !== null) { - if (typeof mapped[field] !== 'string') { - mapped[field] = JSON.stringify(mapped[field]); - } - } - } - - // Apply custom transformers - for (const [field, transformer] of Object.entries( - this.FIELD_TRANSFORMERS, - )) { - if (mapped[field] !== undefined) { - mapped[field] = transformer(mapped[field]); - } - } - - return mapped; - } - - static async create(options) { - assertCreateOptions(options, 'DBApi'); - - const { data, currentUser = { id: null }, transaction } = options; - - const mappedData = this.getFieldMapping(data); - - const record = await this.MODEL.create( - { - ...mappedData, - importHash: data.importHash || null, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { transaction }, - ); - - for (const assoc of this.ASSOCIATIONS) { - if (data[assoc.field] !== undefined) { - await record[assoc.setter]( - data[assoc.field] || (assoc.isArray ? [] : null), - { transaction }, - ); - } - } - - return record; - } - - static async bulkImport(data, options = {}) { - const currentUser = options.currentUser || { id: null }; - const transaction = options.transaction; - - const recordsData = data.map((item, index) => ({ - ...this.getFieldMapping(item), - importHash: item.importHash || null, - createdById: currentUser.id, - updatedById: currentUser.id, - createdAt: new Date(Date.now() + index * 1000), - })); - - return this.MODEL.bulkCreate(recordsData, { transaction }); - } - - /** - * @param {Object} options - * @param {string} options.id - * @param {Object} options.data - * @param {Object} [options.currentUser] - * @param {Object} [options.transaction] - */ - static async update(options) { - assertUpdateOptions(options, 'DBApi'); - - const { id, data, currentUser = { id: null }, transaction } = options; - - const record = await this.MODEL.findByPk(id, { transaction }); - - if (!record) { - throw { status: 404, message: `${this.TABLE_NAME} not found` }; - } - - const updatePayload = { updatedById: currentUser.id }; - const mappedData = this.getFieldMapping(data); - - for (const [key, value] of Object.entries(mappedData)) { - if (value !== undefined) { - updatePayload[key] = value; - } - } - - await record.update(updatePayload, { transaction }); - - for (const assoc of this.ASSOCIATIONS) { - if (data[assoc.field] !== undefined) { - await record[assoc.setter](data[assoc.field], { transaction }); - } - } - - return record; - } - - /** - * Partial update - only updates fields explicitly passed in data. - * Unlike update(), this doesn't go through getFieldMapping which - * converts missing fields to null. - * - * Use this when you need to update specific fields without affecting others. - * - * @param {Object} options - * @param {string} options.id - Record ID - * @param {Object} options.data - Fields to update - * @param {Object} [options.currentUser] - * @param {Object} [options.transaction] - */ - static async partialUpdate(options) { - assertUpdateOptions(options, 'DBApi'); - - const { id, data, currentUser = { id: null }, transaction } = options; - - const record = await this.MODEL.findByPk(id, { transaction }); - - if (!record) { - throw { status: 404, message: `${this.TABLE_NAME} not found` }; - } - - const updatePayload = { updatedById: currentUser.id }; - - // Only include fields that are explicitly in the data object - for (const [key, value] of Object.entries(data)) { - if (value !== undefined) { - updatePayload[key] = value; - } - } - - await record.update(updatePayload, { transaction }); - - return record; - } - - static async deleteByIds(options) { - assertDeleteByIdsOptions(options, 'DBApi'); - - const { ids, currentUser = { id: null }, transaction } = options; - - const records = await this.MODEL.findAll({ - where: { id: { [Op.in]: ids } }, - transaction, - }); - - for (const record of records) { - await record.update({ deletedBy: currentUser.id }, { transaction }); - } - for (const record of records) { - await record.destroy({ transaction }); - } - - return records; - } - - static async remove(options) { - assertIdOptions(options, 'DBApi', 'remove'); - - const { id, currentUser = { id: null }, transaction } = options; - - const record = await this.MODEL.findByPk(id, { transaction }); - - if (!record) { - throw { status: 404, message: `${this.TABLE_NAME} not found` }; - } - - await record.update({ deletedBy: currentUser.id }, { transaction }); - await record.destroy({ transaction }); - - return record; - } - - static async findBy(where, options = {}) { - const transaction = options.transaction; - const include = - options.include !== undefined ? options.include : this.FIND_BY_INCLUDES; - - const record = await this.MODEL.findOne({ - where, - transaction, - include, - }); - - if (!record) { - return null; - } - - return record.get({ plain: true }); - } - - static async findAll(filter = {}, options = {}) { - filter = filter || {}; - const limit = Number(filter.limit) || 0; - const currentPage = Number(filter.page) || 0; - const offset = Math.max(currentPage - 1, 0) * limit; - - let where = {}; - let include = [...this.FIND_ALL_INCLUDES]; - - if (filter.id) { - if (!Utils.isValidUuid(filter.id)) { - return { rows: [], count: 0 }; - } - where.id = filter.id; - } - - for (const field of this.SEARCHABLE_FIELDS) { - if (filter[field]) { - where[Op.and] = Utils.ilike(this.TABLE_NAME, field, filter[field]); - } - } - - for (const field of this.RANGE_FIELDS) { - const rangeKey = `${field}Range`; - if (filter[rangeKey]) { - const [start, end] = filter[rangeKey]; - if (start !== undefined && start !== null && start !== '') { - where[field] = { ...where[field], [Op.gte]: start }; - } - if (end !== undefined && end !== null && end !== '') { - where[field] = { ...where[field], [Op.lte]: end }; - } - } - } - - for (const field of this.ENUM_FIELDS) { - if (filter[field] !== undefined) { - where[field] = filter[field]; - } - } - - // Validate UUID fields - return empty results for invalid UUIDs - for (const field of this.UUID_FIELDS) { - if (filter[field] !== undefined) { - if (!Utils.isValidUuid(filter[field])) { - return { rows: [], count: 0 }; - } - where[field] = filter[field]; - } - } - - if (filter.active !== undefined) { - where.active = filter.active === true || filter.active === 'true'; - } - - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange; - if (start !== undefined && start !== null && start !== '') { - where.createdAt = { ...where.createdAt, [Op.gte]: start }; - } - if (end !== undefined && end !== null && end !== '') { - where.createdAt = { ...where.createdAt, [Op.lte]: end }; - } - } - - for (const rel of this.RELATION_FILTERS) { - if (filter[rel.filterKey]) { - const searchTerms = filter[rel.filterKey].split('|'); - const validUuids = Utils.filterValidUuids(searchTerms); - - // Build OR conditions array - const orConditions = []; - - // Add UUID condition only if there are valid UUIDs - if (validUuids.length > 0) { - orConditions.push({ id: { [Op.in]: validUuids } }); - } - - // Add text search condition if searchField is defined - if (rel.searchField) { - orConditions.push({ - [rel.searchField]: { - [Op.or]: searchTerms.map((term) => ({ - [Op.iLike]: `%${term}%`, - })), - }, - }); - } - - const relInclude = { - model: rel.model, - as: rel.as, - required: orConditions.length > 0, - where: - orConditions.length > 0 ? { [Op.or]: orConditions } : undefined, - }; - include = [relInclude, ...include]; - } - } - - const sortField = this.SORTABLE_FIELDS.includes(filter.field) - ? filter.field - : 'createdAt'; - const sortDirection = - String(filter.sort || 'desc').toUpperCase() === 'ASC' ? 'ASC' : 'DESC'; - - try { - if (options.countOnly) { - const count = await this.MODEL.count({ - where, - include: include.filter((entry) => entry.required || entry.where), - distinct: true, - transaction: options.transaction, - }); - - return { - rows: [], - count, - }; - } - - const queryOptions = { - where, - include, - distinct: true, - order: [[sortField, sortDirection]], - transaction: options.transaction, - limit: limit ? Number(limit) : undefined, - offset: offset ? Number(offset) : undefined, - }; - - const { rows, count } = await this.MODEL.findAndCountAll(queryOptions); - return { - rows, - count, - }; - } catch (error) { - logger.error( - { err: error, table: this.TABLE_NAME }, - 'Error executing query', - ); - throw error; - } - } - - static async findAllAutocomplete(options, queryOptions = {}) { - assertAutocompleteOptions(options, 'DBApi'); - - const { query, limit, offset } = options; - const transaction = queryOptions.transaction; - let where = {}; - - if (query) { - const orConditions = [ - Utils.ilike(this.TABLE_NAME, this.AUTOCOMPLETE_FIELD, query), - ]; - - if (Utils.isValidUuid(query)) { - orConditions.unshift({ id: query }); - } - - where = { [Op.or]: orConditions }; - } - - const records = await this.MODEL.findAll({ - attributes: ['id', this.AUTOCOMPLETE_FIELD], - where, - limit: limit ? Number(limit) : undefined, - offset: offset ? Number(offset) : undefined, - order: [[this.AUTOCOMPLETE_FIELD, 'ASC']], - transaction, - }); - - return records.map((record) => ({ - id: record.id, - label: record[this.AUTOCOMPLETE_FIELD], - })); - } - - static toCSV(rows) { - const opts = { fields: this.CSV_FIELDS }; - return parse(rows, opts); - } -} - -module.exports = GenericDBApi; diff --git a/backend/src/db/api/base.api.ts b/backend/src/db/api/base.api.ts new file mode 100644 index 0000000..c0f9848 --- /dev/null +++ b/backend/src/db/api/base.api.ts @@ -0,0 +1,735 @@ +import type { Transaction } from 'sequelize'; +import { parse } from 'json2csv'; + +import db from '../models/index.ts'; +import Utils from '../utils.ts'; +import { logger } from '../../utils/logger.ts'; +import { + assertAutocompleteOptions, + assertCreateOptions, + assertDeleteByIdsOptions, + assertIdOptions, + assertUpdateOptions, +} from '../../contracts/entity-options.ts'; +import type { + BulkImportOptions, + DbAssociationConfig, + DbData, + DbFindAllOptions, + DbFindByOptions, + DbPrimitive, + DbRelationFilterConfig, + EntityRecord, + GenericDbListFilter, + GenericDbModel, + PaginatedResult, + ServiceOptions, +} from '../../types/index.ts'; + +const { Op } = db.Sequelize; + +interface GenericDbFindAllOptions extends ServiceOptions { + countOnly?: boolean; +} + +interface RelationInclude { + model: unknown; + as: string; + required: boolean; + where?: DbData | undefined; +} + +interface GenericDbFindAndCountOptions { + where: DbData; + include: unknown[]; + distinct: true; + order: string[][]; + transaction?: Transaction | undefined; + limit?: number | undefined; + offset?: number | undefined; +} + +type FieldTransformer = (value: unknown) => unknown; +type RelationSetter = ( + value: unknown, + options: { transaction?: Transaction | undefined }, +) => Promise; + +class DbApiNotFoundError extends Error { + status = 404; + + constructor(message: string) { + super(message); + } +} + +function isRecord(value: unknown): value is DbData { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function isFieldTransformer(value: unknown): value is FieldTransformer { + return typeof value === 'function'; +} + +function isRelationSetter(value: unknown): value is RelationSetter { + return typeof value === 'function'; +} + +function normalizeData(value: unknown): DbData { + return isRecord(value) ? value : {}; +} + +function isGenericDbModel(value: unknown): value is GenericDbModel { + if ( + value === null || + (typeof value !== 'object' && typeof value !== 'function') + ) { + return false; + } + + return ( + 'getTableName' in value && + typeof value.getTableName === 'function' + ); +} + +function buildTransactionOptions( + transaction: Transaction | undefined, +): { transaction?: Transaction | undefined } { + const options: { transaction?: Transaction | undefined } = {}; + if (transaction !== undefined) { + options.transaction = transaction; + } + return options; +} + +function getCurrentUserId(options: ServiceOptions): string | null { + return options.currentUser?.id ?? null; +} + +function isTransaction(value: unknown): value is Transaction { + return Boolean(value) && typeof value === 'object'; +} + +function isRange(value: unknown): value is readonly [DbPrimitive, DbPrimitive] { + return Array.isArray(value) && value.length === 2; +} + +function addRangeBoundary( + where: DbData, + field: string, + operator: symbol, + value: DbPrimitive, +): void { + if (value === undefined || value === null || value === '') return; + + const current = isRecord(where[field]) ? where[field] : {}; + where[field] = { + ...current, + [operator]: value, + }; +} + +function addRangeFilter(where: DbData, field: string, range: unknown): void { + if (!isRange(range)) return; + + const [start, end] = range; + addRangeBoundary(where, field, Op.gte, start); + addRangeBoundary(where, field, Op.lte, end); +} + +function getFilterString(filter: GenericDbListFilter, field: string): string | null { + const value = filter[field]; + return typeof value === 'string' ? value : null; +} + +function isDbFindAllOptions(value: unknown): value is DbFindAllOptions { + return ( + isRecord(value) && + ('filter' in value || + 'offset' in value || + ('limit' in value && typeof value.limit === 'number')) + ); +} + +class GenericDBApi { + static get MODEL(): unknown { + throw new Error('MODEL must be defined in subclass'); + } + + private static getModel(): GenericDbModel { + if (!isGenericDbModel(this.MODEL)) { + throw new Error('MODEL must implement GenericDbModel contract'); + } + return this.MODEL; + } + + static get TABLE_NAME(): string { + return this.getModel().getTableName(); + } + + static get SEARCHABLE_FIELDS(): string[] { + return []; + } + + static get RANGE_FIELDS(): string[] { + return []; + } + + static get ENUM_FIELDS(): string[] { + return []; + } + + /** + * UUID fields that require validation before querying. + * These are typically foreign key fields like 'projectId'. + * Invalid UUIDs will return empty results instead of causing DB errors. + * Override in subclass to specify fields. + * Example: return ['projectId', 'userId']; + */ + static get UUID_FIELDS(): string[] { + return []; + } + + static get RELATION_FILTERS(): DbRelationFilterConfig[] { + return []; + } + + static get CSV_FIELDS(): string[] { + return ['id', 'createdAt']; + } + + static get SORTABLE_FIELDS(): string[] { + return Object.keys(this.getModel().rawAttributes || {}); + } + + static get AUTOCOMPLETE_FIELD(): string { + return 'name'; + } + + static get ASSOCIATIONS(): DbAssociationConfig[] { + return []; + } + + static get FIND_BY_INCLUDES(): unknown[] { + return []; + } + + static get FIND_ALL_INCLUDES(): unknown[] { + return []; + } + + /** + * Fields that should be automatically JSON-stringified + * Override in subclass to specify fields. + * Example: return ['settings_json', 'metadata_json']; + */ + static get JSON_FIELDS(): string[] { + return []; + } + + /** + * Custom field transformers for data mapping. + * Override in subclass to add custom transformations. + * Example: + * return { + * email: (value) => value?.toLowerCase().trim(), + * slug: (value) => value?.toLowerCase().replace(/\s+/g, '-'), + * }; + */ + static get FIELD_TRANSFORMERS(): object { + return {}; + } + + /** + * Field mapping configuration for declarative field handling. + * Override in subclass to specify how fields should be mapped. + * Example: + * return { + * name: { default: null }, + * sort_order: { default: 0 }, + * is_active: { default: true }, + * }; + */ + static get FIELD_DEFAULTS(): object { + return {}; + } + + /** + * Transform input data for database operations. + * Template Method Pattern: Uses JSON_FIELDS, FIELD_TRANSFORMERS, and FIELD_DEFAULTS + * to declaratively transform data, reducing boilerplate in subclasses. + * + * Override this method for complex custom transformations that can't be + * expressed declaratively. + * + * @param {Object} data - Input data to transform + * @returns {Object} - Transformed data ready for database + */ + static getFieldMapping(data: unknown): object { + const mapped = { ...normalizeData(data) }; + + // Apply field defaults + for (const [field, rawConfig] of Object.entries(this.FIELD_DEFAULTS)) { + const config = normalizeData(rawConfig); + if (mapped[field] === undefined) { + mapped[field] = config.default; + } else if (mapped[field] === null && config.nullDefault !== undefined) { + mapped[field] = config.nullDefault; + } + } + + // Auto-stringify JSON fields + for (const field of this.JSON_FIELDS) { + if (mapped[field] !== undefined && mapped[field] !== null) { + if (typeof mapped[field] !== 'string') { + mapped[field] = JSON.stringify(mapped[field]); + } + } + } + + // Apply custom transformers + for (const [field, transformer] of Object.entries( + this.FIELD_TRANSFORMERS, + )) { + if (mapped[field] !== undefined && isFieldTransformer(transformer)) { + mapped[field] = transformer(mapped[field]); + } + } + + return mapped; + } + + static async create(options: unknown): Promise { + assertCreateOptions(options, 'DBApi'); + + const { transaction } = options; + const data = normalizeData(options.data); + + const mappedData = normalizeData(this.getFieldMapping(data)); + + const record = await this.getModel().create( + { + ...mappedData, + importHash: data.importHash || null, + createdById: getCurrentUserId(options), + updatedById: getCurrentUserId(options), + }, + buildTransactionOptions(transaction), + ); + + for (const assoc of this.ASSOCIATIONS) { + if (data[assoc.field] !== undefined) { + const setter = record[assoc.setter]; + if (isRelationSetter(setter)) { + await setter( + data[assoc.field] || (assoc.isArray ? [] : null), + buildTransactionOptions(transaction), + ); + } + } + } + + return record; + } + + static async bulkImport( + data: unknown[], + options: BulkImportOptions, + ): Promise { + const transaction = options.transaction; + + const recordsData = data.map((item, index) => { + const rawItem = normalizeData(item); + return { + ...normalizeData(this.getFieldMapping(rawItem)), + importHash: rawItem.importHash || null, + createdById: getCurrentUserId(options), + updatedById: getCurrentUserId(options), + createdAt: new Date(Date.now() + index * 1000), + }; + }); + + return this.getModel().bulkCreate( + recordsData, + buildTransactionOptions(transaction), + ); + } + + /** + * @param {Object} options + * @param {string} options.id + * @param {Object} options.data + * @param {Object} [options.currentUser] + * @param {Object} [options.transaction] + */ + static async update(options: unknown): Promise { + assertUpdateOptions(options, 'DBApi'); + + const { id, transaction } = options; + const data = normalizeData(options.data); + + const record = await this.getModel().findByPk( + id, + buildTransactionOptions(transaction), + ); + + if (!record) { + throw new DbApiNotFoundError(`${this.TABLE_NAME} not found`); + } + + const updatePayload: DbData = { updatedById: getCurrentUserId(options) }; + const mappedData = normalizeData(this.getFieldMapping(data)); + + for (const [key, value] of Object.entries(mappedData)) { + if (value !== undefined) { + updatePayload[key] = value; + } + } + + await record.update(updatePayload, buildTransactionOptions(transaction)); + + for (const assoc of this.ASSOCIATIONS) { + if (data[assoc.field] !== undefined) { + const setter = record[assoc.setter]; + if (isRelationSetter(setter)) { + await setter(data[assoc.field], buildTransactionOptions(transaction)); + } + } + } + + return record; + } + + /** + * Partial update - only updates fields explicitly passed in data. + * Unlike update(), this doesn't go through getFieldMapping which + * converts missing fields to null. + * + * Use this when you need to update specific fields without affecting others. + * + * @param {Object} options + * @param {string} options.id - Record ID + * @param {Object} options.data - Fields to update + * @param {Object} [options.currentUser] + * @param {Object} [options.transaction] + */ + static async partialUpdate(options: unknown): Promise { + assertUpdateOptions(options, 'DBApi'); + + const { id, transaction } = options; + const data = normalizeData(options.data); + + const record = await this.getModel().findByPk( + id, + buildTransactionOptions(transaction), + ); + + if (!record) { + throw new DbApiNotFoundError(`${this.TABLE_NAME} not found`); + } + + const updatePayload: DbData = { updatedById: getCurrentUserId(options) }; + + // Only include fields that are explicitly in the data object + for (const [key, value] of Object.entries(data)) { + if (value !== undefined) { + updatePayload[key] = value; + } + } + + await record.update(updatePayload, buildTransactionOptions(transaction)); + + return record; + } + + static async deleteByIds(options: unknown): Promise { + assertDeleteByIdsOptions(options, 'DBApi'); + + const { ids, transaction } = options; + + const records = await this.getModel().findAll({ + where: { id: { [Op.in]: ids } }, + ...buildTransactionOptions(transaction), + }); + + for (const record of records) { + await record.update( + { deletedBy: getCurrentUserId(options) }, + buildTransactionOptions(transaction), + ); + } + for (const record of records) { + await record.destroy(buildTransactionOptions(transaction)); + } + + return records; + } + + static async remove(options: unknown): Promise { + assertIdOptions(options, 'DBApi', 'remove'); + + const { id, transaction } = options; + + const record = await this.getModel().findByPk( + id, + buildTransactionOptions(transaction), + ); + + if (!record) { + throw new DbApiNotFoundError(`${this.TABLE_NAME} not found`); + } + + await record.update( + { deletedBy: getCurrentUserId(options) }, + buildTransactionOptions(transaction), + ); + await record.destroy(buildTransactionOptions(transaction)); + + return record; + } + + static async findBy( + options: DbFindByOptions, + ): Promise; + static async findBy( + where: unknown, + options?: ServiceOptions & { include?: unknown[] }, + ): Promise; + static async findBy( + whereOrOptions: unknown, + options: ServiceOptions & { include?: unknown[] } = {}, + ): Promise { + const maybeOptions = normalizeData(whereOrOptions); + const hasWhereOption = isRecord(maybeOptions.where); + const where = hasWhereOption + ? normalizeData(maybeOptions.where) + : normalizeData(whereOrOptions); + const rawTransaction = hasWhereOption + ? maybeOptions.transaction + : options.transaction; + const transaction = isTransaction(rawTransaction) ? rawTransaction : undefined; + const include = + hasWhereOption && Array.isArray(maybeOptions.include) + ? maybeOptions.include + : options.include !== undefined + ? options.include + : this.FIND_BY_INCLUDES; + + const record = await this.getModel().findOne({ + where, + ...buildTransactionOptions(transaction), + include, + }); + + if (!record) { + return null; + } + + return record.get({ plain: true }); + } + + static async findAll( + options: DbFindAllOptions, + ): Promise>; + static async findAll( + filter?: GenericDbListFilter, + options?: GenericDbFindAllOptions, + ): Promise>; + static async findAll( + filter: GenericDbListFilter | DbFindAllOptions = {}, + options: GenericDbFindAllOptions = {}, + ): Promise> { + const normalizedFilter: GenericDbListFilter = isDbFindAllOptions(filter) + ? isRecord(filter.filter) + ? filter.filter + : {} + : filter || {}; + const limit = Number(normalizedFilter.limit) || 0; + const currentPage = Number(normalizedFilter.page) || 0; + const offset = Math.max(currentPage - 1, 0) * limit; + + const where: DbData = {}; + let include = [...this.FIND_ALL_INCLUDES]; + + if (normalizedFilter.id) { + if (!Utils.isValidUuid(normalizedFilter.id)) { + return { rows: [], count: 0 }; + } + where.id = normalizedFilter.id; + } + + for (const field of this.SEARCHABLE_FIELDS) { + if (normalizedFilter[field]) { + const value = getFilterString(normalizedFilter, field); + if (value) { + where[Op.and] = Utils.ilike(this.TABLE_NAME, field, value); + } + } + } + + for (const field of this.RANGE_FIELDS) { + const rangeKey = `${field}Range`; + addRangeFilter(where, field, normalizedFilter[rangeKey]); + } + + for (const field of this.ENUM_FIELDS) { + if (normalizedFilter[field] !== undefined) { + where[field] = normalizedFilter[field]; + } + } + + // Validate UUID fields - return empty results for invalid UUIDs + for (const field of this.UUID_FIELDS) { + const value = normalizedFilter[field]; + if (value !== undefined) { + if (!Utils.isValidUuid(value)) { + return { rows: [], count: 0 }; + } + where[field] = value; + } + } + + if (normalizedFilter.active !== undefined) { + where.active = + normalizedFilter.active === true || normalizedFilter.active === 'true'; + } + + if (normalizedFilter.createdAtRange) { + addRangeFilter(where, 'createdAt', normalizedFilter.createdAtRange); + } + + for (const rel of this.RELATION_FILTERS) { + const relationFilter = getFilterString(normalizedFilter, rel.filterKey); + if (relationFilter) { + const searchTerms = relationFilter.split('|'); + const validUuids = Utils.filterValidUuids(searchTerms); + + // Build OR conditions array + const orConditions: DbData[] = []; + + // Add UUID condition only if there are valid UUIDs + if (validUuids.length > 0) { + orConditions.push({ id: { [Op.in]: validUuids } }); + } + + // Add text search condition if searchField is defined + if (rel.searchField) { + orConditions.push({ + [rel.searchField]: { + [Op.or]: searchTerms.map((term) => ({ + [Op.iLike]: `%${term}%`, + })), + }, + }); + } + + const relInclude: RelationInclude = { + model: rel.model, + as: rel.as, + required: orConditions.length > 0, + where: + orConditions.length > 0 ? { [Op.or]: orConditions } : undefined, + }; + include = [relInclude, ...include]; + } + } + + const sortField = + typeof normalizedFilter.field === 'string' && + this.SORTABLE_FIELDS.includes(normalizedFilter.field) + ? normalizedFilter.field + : 'createdAt'; + const sortDirection = + String(normalizedFilter.sort || 'desc').toUpperCase() === 'ASC' + ? 'ASC' + : 'DESC'; + + try { + if (options.countOnly) { + const count = await this.getModel().count({ + where, + include: include.filter( + (entry): entry is RelationInclude => + isRecord(entry) && Boolean(entry.required || entry.where), + ), + distinct: true, + ...buildTransactionOptions(options.transaction), + }); + + return { + rows: [], + count, + }; + } + + const queryOptions: GenericDbFindAndCountOptions = { + where, + include, + distinct: true, + order: [[sortField, sortDirection]], + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + }; + if (options.transaction !== undefined) { + queryOptions.transaction = options.transaction; + } + + const { rows, count } = await this.getModel().findAndCountAll(queryOptions); + return { + rows, + count, + }; + } catch (error) { + logger.error( + { err: error, table: this.TABLE_NAME }, + 'Error executing query', + ); + throw error; + } + } + + static async findAllAutocomplete( + options: unknown, + queryOptions: ServiceOptions = {}, + ): Promise { + assertAutocompleteOptions(options, 'DBApi'); + + const { query, limit, offset } = options; + const transaction = queryOptions.transaction; + let where: DbData = {}; + + if (query) { + const orConditions: unknown[] = [ + Utils.ilike(this.TABLE_NAME, this.AUTOCOMPLETE_FIELD, query), + ]; + + if (Utils.isValidUuid(query)) { + orConditions.unshift({ id: query }); + } + + where = { [Op.or]: orConditions }; + } + + const records = await this.getModel().findAll({ + attributes: ['id', this.AUTOCOMPLETE_FIELD], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: [[this.AUTOCOMPLETE_FIELD, 'ASC']], + ...buildTransactionOptions(transaction), + }); + + return records.map((record) => ({ + id: record.id, + label: record[this.AUTOCOMPLETE_FIELD], + })); + } + + static toCSV(rows: readonly EntityRecord[]): string { + const opts = { fields: this.CSV_FIELDS }; + return parse(rows, opts); + } +} + +export default GenericDBApi; diff --git a/backend/src/db/api/element_type_defaults.js b/backend/src/db/api/element_type_defaults.ts similarity index 70% rename from backend/src/db/api/element_type_defaults.js rename to backend/src/db/api/element_type_defaults.ts index f8a8d2f..0e3f5d6 100644 --- a/backend/src/db/api/element_type_defaults.js +++ b/backend/src/db/api/element_type_defaults.ts @@ -1,28 +1,74 @@ -const GenericDBApi = require('./base.api'); -const db = require('../models'); +import GenericDBApi from './base.api.ts'; +import db from '../models/index.ts'; +import type { + DbFindAllOptions, + DbFindByOptions, + AutocompleteOptions, + CreateOptions, + DeleteByIdsOptions, + ElementSettingsJson, + ElementTypeDefaultsData, + ElementTypeDefaultsFieldDefaults, + ElementTypeDefaultsFieldMapping, + ElementTypeDefaultsModel, + ElementTypeDefaultsSeedRow, + EntityIdOptions, + EntityRecord, + GenericDbListFilter, + PaginatedResult, + ServiceOptions, + UpdateOptions, +} from '../../types/index.ts'; + +function isMissingTableError(error: unknown): boolean { + if (!error || typeof error !== 'object' || !('original' in error)) { + return false; + } + + const original = error.original; + if (!original || typeof original !== 'object' || !('code' in original)) { + return false; + } + + return original.code === '42P01'; +} + +function stringifySettings(value: ElementSettingsJson | string | null | undefined): string | null { + if (value === undefined || value === null) return null; + if (typeof value === 'string') return value; + return JSON.stringify(value); +} + +function isElementTypeDefaultsListFilter( + value: unknown, +): value is GenericDbListFilter { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} class Element_type_defaultsDBApi extends GenericDBApi { - static get MODEL() { + static initializationPromise: Promise | null = null; + + static override get MODEL(): ElementTypeDefaultsModel { return db.element_type_defaults; } - static get TABLE_NAME() { + static override get TABLE_NAME(): string { return 'element_type_defaults'; } - static get SEARCHABLE_FIELDS() { + static override get SEARCHABLE_FIELDS(): string[] { return ['name', 'element_type']; } - static get RANGE_FIELDS() { + static override get RANGE_FIELDS(): string[] { return ['sort_order']; } - static get ENUM_FIELDS() { + static override get ENUM_FIELDS(): string[] { return []; } - static get CSV_FIELDS() { + static override get CSV_FIELDS(): string[] { return [ 'id', 'element_type', @@ -33,16 +79,16 @@ class Element_type_defaultsDBApi extends GenericDBApi { ]; } - static get AUTOCOMPLETE_FIELD() { + static override get AUTOCOMPLETE_FIELD(): string { return 'name'; } // Declarative field configuration using base class patterns - static get JSON_FIELDS() { + static override get JSON_FIELDS(): string[] { return ['default_settings_json']; } - static get FIELD_DEFAULTS() { + static override get FIELD_DEFAULTS(): ElementTypeDefaultsFieldDefaults { return { element_type: { default: null }, name: { default: null }, @@ -50,20 +96,19 @@ class Element_type_defaultsDBApi extends GenericDBApi { }; } - static getFieldMapping(data) { - // Apply base class transformations (JSON fields, defaults, transformers) - const mapped = super.getFieldMapping(data); - + static override getFieldMapping( + data: ElementTypeDefaultsData, + ): ElementTypeDefaultsFieldMapping { return { - id: mapped.id || undefined, - element_type: mapped.element_type, - name: mapped.name, - sort_order: mapped.sort_order, - default_settings_json: mapped.default_settings_json, + id: data.id || undefined, + element_type: data.element_type ?? null, + name: data.name ?? null, + sort_order: data.sort_order ?? 0, + default_settings_json: stringifySettings(data.default_settings_json), }; } - static get DEFAULT_ROWS() { + static get DEFAULT_ROWS(): ElementTypeDefaultsSeedRow[] { return [ { element_type: 'navigation_next', @@ -328,7 +373,7 @@ class Element_type_defaultsDBApi extends GenericDBApi { ]; } - static async ensureInitialized() { + static async ensureInitialized(): Promise { if (!this.initializationPromise) { this.initializationPromise = (async () => { let count = 0; @@ -336,7 +381,7 @@ class Element_type_defaultsDBApi extends GenericDBApi { try { count = await this.MODEL.count(); } catch (error) { - if (error?.original?.code !== '42P01') { + if (!isMissingTableError(error)) { throw error; } @@ -363,47 +408,91 @@ class Element_type_defaultsDBApi extends GenericDBApi { await this.initializationPromise; } - static async create(options) { + static override async create( + options: CreateOptions, + ): Promise { await this.ensureInitialized(); return super.create(options); } - static async bulkImport(data, options = {}) { + static override async bulkImport( + data: unknown[], + options: ServiceOptions = {}, + ): Promise { await this.ensureInitialized(); - return super.bulkImport(data, options); + await super.bulkImport(data, options); } - static async update({ id, data, currentUser, transaction, runtimeContext }) { + static override async update( + options: UpdateOptions, + ): Promise { await this.ensureInitialized(); - return super.update({ id, data, currentUser, transaction, runtimeContext }); + return super.update(options); } - static async deleteByIds(options) { + static override async deleteByIds( + options: DeleteByIdsOptions, + ): Promise { await this.ensureInitialized(); return super.deleteByIds(options); } - static async remove(options) { + static override async remove(options: EntityIdOptions): Promise { await this.ensureInitialized(); return super.remove(options); } - static async findBy(where, options = {}) { + static override async findBy( + where: { id: string }, + options?: ServiceOptions, + ): Promise; + static override async findBy( + options: DbFindByOptions, + ): Promise; + static override async findBy( + whereOrOptions: { id: string } | DbFindByOptions, + options: ServiceOptions = {}, + ): Promise { await this.ensureInitialized(); - return super.findBy(where, options); + + if ('where' in whereOrOptions) { + return super.findBy(whereOrOptions); + } + + const findOptions: DbFindByOptions = { where: whereOrOptions }; + + if (options.transaction) { + findOptions.transaction = options.transaction; + } + + return super.findBy(findOptions); } - static async findAll(filter = {}, options = {}) { + static override async findAll( + filter?: unknown, + options?: ServiceOptions, + ): Promise>; + static override async findAll( + options: DbFindAllOptions, + ): Promise>; + static override async findAll( + filterOrOptions: unknown = {}, + options: ServiceOptions = {}, + ): Promise> { await this.ensureInitialized(); - return super.findAll(filter, options); + if (isElementTypeDefaultsListFilter(filterOrOptions)) { + return super.findAll(filterOrOptions, options); + } + return super.findAll(options); } - static async findAllAutocomplete(options, queryOptions = {}) { + static override async findAllAutocomplete( + options: AutocompleteOptions, + ): Promise { await this.ensureInitialized(); - return super.findAllAutocomplete(options, queryOptions); + const records = await super.findAllAutocomplete(options); + return records; } } -Element_type_defaultsDBApi.initializationPromise = null; - -module.exports = Element_type_defaultsDBApi; +export default Element_type_defaultsDBApi; diff --git a/backend/src/db/api/file.js b/backend/src/db/api/file.js deleted file mode 100644 index 1efb039..0000000 --- a/backend/src/db/api/file.js +++ /dev/null @@ -1,73 +0,0 @@ -const db = require('../models'); -const assert = require('assert'); -const services = require('../../services/file/'); - -module.exports = class FileDBApi { - static async replaceRelationFiles(relation, rawFiles, options) { - assert(relation.belongsTo, 'belongsTo is required'); - assert(relation.belongsToColumn, 'belongsToColumn is required'); - assert(relation.belongsToId, 'belongsToId is required'); - - let files = []; - - if (Array.isArray(rawFiles)) { - files = rawFiles; - } else { - files = rawFiles ? [rawFiles] : []; - } - - await this._removeLegacyFiles(relation, files, options); - await this._addFiles(relation, files, options); - } - - static async _addFiles(relation, files, options) { - const transaction = (options && options.transaction) || undefined; - const currentUser = (options && options.currentUser) || { id: null }; - - const inexistentFiles = files.filter((file) => !!file.new); - - for (const file of inexistentFiles) { - await db.file.create( - { - belongsTo: relation.belongsTo, - belongsToColumn: relation.belongsToColumn, - belongsToId: relation.belongsToId, - name: file.name, - sizeInBytes: file.sizeInBytes, - privateUrl: file.privateUrl, - publicUrl: file.publicUrl, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { - transaction, - }, - ); - } - } - - static async _removeLegacyFiles(relation, files, options) { - const transaction = (options && options.transaction) || undefined; - - const filesToDelete = await db.file.findAll({ - where: { - belongsTo: relation.belongsTo, - belongsToId: relation.belongsToId, - belongsToColumn: relation.belongsToColumn, - id: { - [db.Sequelize.Op.notIn]: files - .filter((file) => !file.new) - .map((file) => file.id), - }, - }, - transaction, - }); - - for (let file of filesToDelete) { - await services.deleteFile(file.privateUrl); - await file.destroy({ - transaction, - }); - } - } -}; diff --git a/backend/src/db/api/file.ts b/backend/src/db/api/file.ts new file mode 100644 index 0000000..3faf4c6 --- /dev/null +++ b/backend/src/db/api/file.ts @@ -0,0 +1,109 @@ +import assert from 'node:assert'; + +import db from '../models/index.ts'; +import services from '../../services/file/index.ts'; +import type { + FileDbApi, + FileDbOptions, + FileModel, + FileRelationDescriptor, + RelationFileInput, + RelationFileRecord, +} from '../../types/index.ts'; + +function normalizeRelationFiles(rawFiles: RelationFileInput): RelationFileRecord[] { + if (Array.isArray(rawFiles)) return rawFiles; + return rawFiles ? [rawFiles] : []; +} + +function isExistingRelationFile( + file: RelationFileRecord, +): file is RelationFileRecord & { id: string } { + return !file.new && typeof file.id === 'string'; +} + +function getExistingFileIds(files: readonly RelationFileRecord[]): string[] { + return files.filter(isExistingRelationFile).map((file) => file.id); +} + +class FileDBApi { + static get MODEL(): FileModel { + return db.file; + } + + static async replaceRelationFiles( + relation: FileRelationDescriptor, + rawFiles: RelationFileInput, + options: FileDbOptions = {}, + ): Promise { + assert(relation.belongsTo, 'belongsTo is required'); + assert(relation.belongsToColumn, 'belongsToColumn is required'); + assert(relation.belongsToId, 'belongsToId is required'); + + const files = normalizeRelationFiles(rawFiles); + + await this._removeLegacyFiles(relation, files, options); + await this._addFiles(relation, files, options); + } + + private static async _addFiles( + relation: FileRelationDescriptor, + files: readonly RelationFileRecord[], + options: FileDbOptions, + ): Promise { + const transaction = options.transaction; + const currentUser = options.currentUser || { id: null }; + + const inexistentFiles = files.filter((file) => !!file.new); + + for (const file of inexistentFiles) { + await this.MODEL.create( + { + belongsTo: relation.belongsTo, + belongsToColumn: relation.belongsToColumn, + belongsToId: relation.belongsToId, + name: file.name, + sizeInBytes: file.sizeInBytes, + privateUrl: file.privateUrl, + publicUrl: file.publicUrl, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { + transaction, + }, + ); + } + } + + private static async _removeLegacyFiles( + relation: FileRelationDescriptor, + files: readonly RelationFileRecord[], + options: FileDbOptions, + ): Promise { + const transaction = options.transaction; + + const filesToDelete = await this.MODEL.findAll({ + where: { + belongsTo: relation.belongsTo, + belongsToId: relation.belongsToId, + belongsToColumn: relation.belongsToColumn, + id: { + [db.Sequelize.Op.notIn]: getExistingFileIds(files), + }, + }, + transaction, + }); + + for (const file of filesToDelete) { + await services.deleteFile(file.privateUrl); + await file.destroy({ + transaction, + }); + } + } +} + +const fileDBApi: FileDbApi = FileDBApi; + +export default fileDBApi; diff --git a/backend/src/db/api/global_transition_defaults.js b/backend/src/db/api/global_transition_defaults.js deleted file mode 100644 index 65ced69..0000000 --- a/backend/src/db/api/global_transition_defaults.js +++ /dev/null @@ -1,155 +0,0 @@ -const GenericDBApi = require('./base.api'); -const db = require('../models'); - -/** - * Global Transition Defaults API - * - * Single-row table pattern for platform-wide transition settings. - * Auto-seeds default values if the table is empty. - */ -class Global_transition_defaultsDBApi extends GenericDBApi { - static get MODEL() { - return db.global_transition_defaults; - } - - static get TABLE_NAME() { - return 'global_transition_defaults'; - } - - static get SEARCHABLE_FIELDS() { - return []; - } - - static get RANGE_FIELDS() { - return []; - } - - static get ENUM_FIELDS() { - return ['transition_type', 'easing']; - } - - static get CSV_FIELDS() { - return [ - 'id', - 'transition_type', - 'duration_ms', - 'easing', - 'overlay_color', - 'createdAt', - 'updatedAt', - ]; - } - - static get AUTOCOMPLETE_FIELD() { - return 'transition_type'; - } - - static get FIELD_DEFAULTS() { - return { - transition_type: { default: 'fade' }, - duration_ms: { default: 700 }, - easing: { default: 'ease-in-out' }, - overlay_color: { default: '#000000' }, - }; - } - - static get DEFAULT_ROW() { - return { - transition_type: 'fade', - duration_ms: 700, - easing: 'ease-in-out', - overlay_color: '#000000', - }; - } - - static getFieldMapping(data) { - const mapped = super.getFieldMapping(data); - - return { - id: mapped.id || undefined, - transition_type: mapped.transition_type, - duration_ms: mapped.duration_ms, - easing: mapped.easing, - overlay_color: mapped.overlay_color, - }; - } - - /** - * Ensures the singleton row exists. - * Creates the default row if table is empty. - */ - static async ensureInitialized() { - if (!this.initializationPromise) { - this.initializationPromise = (async () => { - let count = 0; - - try { - count = await this.MODEL.count(); - } catch (error) { - // Table doesn't exist yet (happens during initial migration) - if (error?.original?.code !== '42P01') { - throw error; - } - - await this.MODEL.sync(); - count = await this.MODEL.count(); - } - - if (count > 0) return; - - const now = new Date(); - await this.MODEL.create({ - ...this.getFieldMapping(this.DEFAULT_ROW), - createdAt: now, - updatedAt: now, - }); - })().catch((error) => { - this.initializationPromise = null; - throw error; - }); - } - - await this.initializationPromise; - } - - /** - * Get the singleton row. - * Always returns a single object, not an array. - */ - static async findOne(options = {}) { - await this.ensureInitialized(); - - const record = await this.MODEL.findOne({ - transaction: options.transaction, - }); - - if (!record) return null; - return record.get({ plain: true }); - } - - /** - * Alias for findOne to maintain semantic clarity. - */ - static async get(options = {}) { - return this.findOne(options); - } - - static async update({ id, data, currentUser, transaction, runtimeContext }) { - await this.ensureInitialized(); - return super.update({ id, data, currentUser, transaction, runtimeContext }); - } - - static async findBy(where, options = {}) { - await this.ensureInitialized(); - return super.findBy(where, options); - } - - static async findAll(filter = {}, options = {}) { - await this.ensureInitialized(); - return super.findAll(filter, options); - } -} - -Global_transition_defaultsDBApi.initializationPromise = null; - -module.exports = Global_transition_defaultsDBApi; diff --git a/backend/src/db/api/global_transition_defaults.ts b/backend/src/db/api/global_transition_defaults.ts new file mode 100644 index 0000000..db7c63d --- /dev/null +++ b/backend/src/db/api/global_transition_defaults.ts @@ -0,0 +1,264 @@ +import GenericDBApi from './base.api.ts'; +import db from '../models/index.ts'; +import type { + DbFindAllOptions, + DbFindByOptions, + EntityRecord, + GlobalTransitionDefaultsData, + GlobalTransitionDefaultsFieldDefaults, + GlobalTransitionDefaultsFieldMapping, + GlobalTransitionDefaultsModel, + GlobalTransitionDefaultsRecord, + GenericDbListFilter, + PaginatedResult, + ServiceOptions, + UpdateOptions, +} from '../../types/index.ts'; + +function isMissingTableError(error: unknown): boolean { + if (!error || typeof error !== 'object' || !('original' in error)) { + return false; + } + + const original = error.original; + if (!original || typeof original !== 'object' || !('code' in original)) { + return false; + } + + return original.code === '42P01'; +} + +function isGlobalTransitionListFilter( + value: unknown, +): value is GenericDbListFilter { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function isDbFindAllOptions(value: unknown): value is DbFindAllOptions { + return ( + value !== null && + typeof value === 'object' && + !Array.isArray(value) && + 'filter' in value + ); +} + +/** + * Global Transition Defaults API + * + * Single-row table pattern for platform-wide transition settings. + * Auto-seeds default values if the table is empty. + */ +class Global_transition_defaultsDBApi extends GenericDBApi { + static initializationPromise: Promise | null = null; + + static override get MODEL(): GlobalTransitionDefaultsModel { + return db.global_transition_defaults; + } + + static override get TABLE_NAME(): string { + return 'global_transition_defaults'; + } + + static override get SEARCHABLE_FIELDS(): string[] { + return []; + } + + static override get RANGE_FIELDS(): string[] { + return []; + } + + static override get ENUM_FIELDS(): string[] { + return ['transition_type', 'easing']; + } + + static override get CSV_FIELDS(): string[] { + return [ + 'id', + 'transition_type', + 'duration_ms', + 'easing', + 'overlay_color', + 'createdAt', + 'updatedAt', + ]; + } + + static override get AUTOCOMPLETE_FIELD(): string { + return 'transition_type'; + } + + static override get FIELD_DEFAULTS(): GlobalTransitionDefaultsFieldDefaults { + return { + transition_type: { default: 'fade' }, + duration_ms: { default: 700 }, + easing: { default: 'ease-in-out' }, + overlay_color: { default: '#000000' }, + }; + } + + static get DEFAULT_ROW(): GlobalTransitionDefaultsData { + return { + transition_type: 'fade', + duration_ms: 700, + easing: 'ease-in-out', + overlay_color: '#000000', + }; + } + + static override getFieldMapping( + data: GlobalTransitionDefaultsData, + ): GlobalTransitionDefaultsFieldMapping { + return { + id: data.id || undefined, + transition_type: + data.transition_type ?? this.FIELD_DEFAULTS.transition_type.default, + duration_ms: data.duration_ms ?? this.FIELD_DEFAULTS.duration_ms.default, + easing: data.easing ?? this.FIELD_DEFAULTS.easing.default, + overlay_color: + data.overlay_color ?? this.FIELD_DEFAULTS.overlay_color.default, + }; + } + + /** + * Ensures the singleton row exists. + * Creates the default row if table is empty. + */ + static async ensureInitialized(): Promise { + if (!this.initializationPromise) { + this.initializationPromise = (async () => { + let count = 0; + + try { + count = await this.MODEL.count(); + } catch (error) { + // Table doesn't exist yet (happens during initial migration) + if (!isMissingTableError(error)) { + throw error; + } + + await this.MODEL.sync(); + count = await this.MODEL.count(); + } + + if (count > 0) return; + + const now = new Date(); + await this.MODEL.create({ + ...this.getFieldMapping(this.DEFAULT_ROW), + createdAt: now, + updatedAt: now, + }); + })().catch((error) => { + this.initializationPromise = null; + throw error; + }); + } + + await this.initializationPromise; + } + + /** + * Get the singleton row. + * Always returns a single object, not an array. + */ + static async findOne( + options: ServiceOptions = {}, + ): Promise { + await this.ensureInitialized(); + + const record = await this.MODEL.findOne({ + transaction: options.transaction, + }); + + if (!record) return null; + return record.get({ plain: true }); + } + + /** + * Alias for findOne to maintain semantic clarity. + */ + static async get( + options: ServiceOptions = {}, + ): Promise { + return this.findOne(options); + } + + static override async update({ + id, + data, + currentUser, + transaction, + runtimeContext, + }: UpdateOptions): Promise { + await this.ensureInitialized(); + + const updateOptions: UpdateOptions = { + id, + data, + currentUser: currentUser ?? null, + }; + + if (transaction) { + updateOptions.transaction = transaction; + } + + if (runtimeContext) { + updateOptions.runtimeContext = runtimeContext; + } + + return super.update(updateOptions); + } + + static override async findBy( + where: { id: string }, + options?: ServiceOptions, + ): Promise; + static override async findBy( + options: DbFindByOptions, + ): Promise; + static override async findBy( + whereOrOptions: { id: string } | DbFindByOptions, + options: ServiceOptions = {}, + ): Promise { + await this.ensureInitialized(); + + if ('where' in whereOrOptions) { + return super.findBy(whereOrOptions); + } + + const findOptions: DbFindByOptions = { where: whereOrOptions }; + + if (options.transaction) { + findOptions.transaction = options.transaction; + } + + return super.findBy(findOptions); + } + + static override async findAll( + filter?: GenericDbListFilter, + options?: ServiceOptions, + ): Promise>; + static override async findAll( + options?: DbFindAllOptions, + ): Promise>; + static override async findAll( + filterOrOptions: GenericDbListFilter | DbFindAllOptions = {}, + options: ServiceOptions = {}, + ): Promise> { + await this.ensureInitialized(); + if ( + isGlobalTransitionListFilter(filterOrOptions) && + !('filter' in filterOrOptions) + ) { + return super.findAll(filterOrOptions, options); + } + if (isDbFindAllOptions(filterOrOptions)) { + return super.findAll(filterOrOptions); + } + return super.findAll({}); + } +} + +export default Global_transition_defaultsDBApi; diff --git a/backend/src/db/api/global_ui_control_defaults.js b/backend/src/db/api/global_ui_control_defaults.ts similarity index 50% rename from backend/src/db/api/global_ui_control_defaults.js rename to backend/src/db/api/global_ui_control_defaults.ts index 7873d68..3997773 100644 --- a/backend/src/db/api/global_ui_control_defaults.js +++ b/backend/src/db/api/global_ui_control_defaults.ts @@ -1,7 +1,31 @@ -const GenericDBApi = require('./base.api'); -const db = require('../models'); +import GenericDBApi from './base.api.ts'; +import db from '../models/index.ts'; +import type { + DbFindByOptions, + EntityRecord, + GlobalUiControlDefaultsData, + GlobalUiControlDefaultsFieldMapping, + GlobalUiControlDefaultsModel, + GlobalUiControlDefaultsRecord, + GlobalUiControlSettingsJson, + ServiceOptions, + UpdateOptions, +} from '../../types/index.ts'; -const DEFAULT_SETTINGS = { +function isMissingTableError(error: unknown): boolean { + if (!error || typeof error !== 'object' || !('original' in error)) { + return false; + } + + const original = error.original; + if (!original || typeof original !== 'object' || !('code' in original)) { + return false; + } + + return original.code === '42P01'; +} + +const DEFAULT_SETTINGS: GlobalUiControlSettingsJson = { fullscreen: { enabled: true, hidden: false, @@ -71,40 +95,42 @@ const DEFAULT_SETTINGS = { }; class Global_ui_control_defaultsDBApi extends GenericDBApi { - static get MODEL() { + static initializationPromise: Promise | null = null; + + static override get MODEL(): GlobalUiControlDefaultsModel { return db.global_ui_control_defaults; } - static get TABLE_NAME() { + static override get TABLE_NAME(): string { return 'global_ui_control_defaults'; } - static get SEARCHABLE_FIELDS() { + static override get SEARCHABLE_FIELDS(): string[] { return []; } - static get RANGE_FIELDS() { + static override get RANGE_FIELDS(): string[] { return []; } - static get ENUM_FIELDS() { + static override get ENUM_FIELDS(): string[] { return []; } - static get DEFAULT_SETTINGS() { + static get DEFAULT_SETTINGS(): GlobalUiControlSettingsJson { return DEFAULT_SETTINGS; } - static getFieldMapping(data) { - const mapped = super.getFieldMapping(data); + static override getFieldMapping( + data: GlobalUiControlDefaultsData, + ): GlobalUiControlDefaultsFieldMapping { return { - id: mapped.id || undefined, - settings_json: - mapped.settings_json || mapped.settings || DEFAULT_SETTINGS, + id: data.id || undefined, + settings_json: data.settings_json || data.settings || DEFAULT_SETTINGS, }; } - static async ensureInitialized() { + static async ensureInitialized(): Promise { if (!this.initializationPromise) { this.initializationPromise = (async () => { let count = 0; @@ -112,7 +138,7 @@ class Global_ui_control_defaultsDBApi extends GenericDBApi { try { count = await this.MODEL.count(); } catch (error) { - if (error?.original?.code !== '42P01') { + if (!isMissingTableError(error)) { throw error; } @@ -137,7 +163,9 @@ class Global_ui_control_defaultsDBApi extends GenericDBApi { await this.initializationPromise; } - static async findOne(options = {}) { + static async findOne( + options: ServiceOptions = {}, + ): Promise { await this.ensureInitialized(); const record = await this.MODEL.findOne({ @@ -148,17 +176,57 @@ class Global_ui_control_defaultsDBApi extends GenericDBApi { return record.get({ plain: true }); } - static async update({ id, data, currentUser, transaction, runtimeContext }) { + static override async update({ + id, + data, + currentUser, + transaction, + runtimeContext, + }: UpdateOptions): Promise { await this.ensureInitialized(); - return super.update({ id, data, currentUser, transaction, runtimeContext }); + + const updateOptions: UpdateOptions = { + id, + data, + currentUser: currentUser ?? null, + }; + + if (transaction) { + updateOptions.transaction = transaction; + } + + if (runtimeContext) { + updateOptions.runtimeContext = runtimeContext; + } + + return super.update(updateOptions); } - static async findBy(where, options = {}) { + static override async findBy( + where: { id: string }, + options?: ServiceOptions, + ): Promise; + static override async findBy( + options: DbFindByOptions, + ): Promise; + static override async findBy( + whereOrOptions: { id: string } | DbFindByOptions, + options: ServiceOptions = {}, + ): Promise { await this.ensureInitialized(); - return super.findBy(where, options); + + if ('where' in whereOrOptions) { + return super.findBy(whereOrOptions); + } + + const findOptions: DbFindByOptions = { where: whereOrOptions }; + + if (options.transaction) { + findOptions.transaction = options.transaction; + } + + return super.findBy(findOptions); } } -Global_ui_control_defaultsDBApi.initializationPromise = null; - -module.exports = Global_ui_control_defaultsDBApi; +export default Global_ui_control_defaultsDBApi; diff --git a/backend/src/db/api/permissions.js b/backend/src/db/api/permissions.js deleted file mode 100644 index b002a8d..0000000 --- a/backend/src/db/api/permissions.js +++ /dev/null @@ -1,53 +0,0 @@ -const GenericDBApi = require('./base.api'); -const db = require('../models'); - -class PermissionsDBApi extends GenericDBApi { - static get MODEL() { - return db.permissions; - } - - static get TABLE_NAME() { - return 'permissions'; - } - - static get SEARCHABLE_FIELDS() { - return ['name']; - } - - static get RANGE_FIELDS() { - return []; - } - - static get ENUM_FIELDS() { - return []; - } - - static get CSV_FIELDS() { - return ['id', 'name', 'createdAt']; - } - - static get AUTOCOMPLETE_FIELD() { - return 'name'; - } - - static get ASSOCIATIONS() { - return []; - } - - static get FIND_BY_INCLUDES() { - return []; - } - - static get FIND_ALL_INCLUDES() { - return []; - } - - static getFieldMapping(data) { - return { - id: data.id || undefined, - name: data.name || null, - }; - } -} - -module.exports = PermissionsDBApi; diff --git a/backend/src/db/api/permissions.ts b/backend/src/db/api/permissions.ts new file mode 100644 index 0000000..8a679ef --- /dev/null +++ b/backend/src/db/api/permissions.ts @@ -0,0 +1,60 @@ +import GenericDBApi from './base.api.ts'; +import db from '../models/index.ts'; +import type { + DbAssociationConfig, + PermissionData, + PermissionFieldMapping, +} from '../../types/index.ts'; + +class PermissionsDBApi extends GenericDBApi { + static override get MODEL(): unknown { + return db.permissions; + } + + static override get TABLE_NAME(): string { + return 'permissions'; + } + + static override get SEARCHABLE_FIELDS(): string[] { + return ['name']; + } + + static override get RANGE_FIELDS(): string[] { + return []; + } + + static override get ENUM_FIELDS(): string[] { + return []; + } + + static override get CSV_FIELDS(): string[] { + return ['id', 'name', 'createdAt']; + } + + static override get AUTOCOMPLETE_FIELD(): string { + return 'name'; + } + + static override get ASSOCIATIONS(): DbAssociationConfig[] { + return []; + } + + static override get FIND_BY_INCLUDES(): unknown[] { + return []; + } + + static override get FIND_ALL_INCLUDES(): unknown[] { + return []; + } + + static override getFieldMapping( + data: PermissionData, + ): PermissionFieldMapping { + return { + id: data.id || undefined, + name: data.name || null, + }; + } +} + +export default PermissionsDBApi; diff --git a/backend/src/db/api/presigned_url_requests.js b/backend/src/db/api/presigned_url_requests.ts similarity index 58% rename from backend/src/db/api/presigned_url_requests.js rename to backend/src/db/api/presigned_url_requests.ts index d180e42..ac3dba7 100644 --- a/backend/src/db/api/presigned_url_requests.js +++ b/backend/src/db/api/presigned_url_requests.ts @@ -1,28 +1,34 @@ -const GenericDBApi = require('./base.api'); -const db = require('../models'); +import GenericDBApi from './base.api.ts'; +import db from '../models/index.ts'; +import type { + PresignedUrlRequestAssociationConfig, + PresignedUrlRequestData, + PresignedUrlRequestFieldMapping, + PresignedUrlRequestRelationFilterConfig, +} from '../../types/index.ts'; class Presigned_url_requestsDBApi extends GenericDBApi { - static get MODEL() { + static override get MODEL(): unknown { return db.presigned_url_requests; } - static get TABLE_NAME() { + static override get TABLE_NAME(): string { return 'presigned_url_requests'; } - static get SEARCHABLE_FIELDS() { + static override get SEARCHABLE_FIELDS(): string[] { return ['requested_key', 'mime_type', 'status']; } - static get RANGE_FIELDS() { + static override get RANGE_FIELDS(): string[] { return ['requested_size_mb', 'expires_at']; } - static get ENUM_FIELDS() { + static override get ENUM_FIELDS(): string[] { return ['purpose', 'asset_type']; } - static get CSV_FIELDS() { + static override get CSV_FIELDS(): string[] { return [ 'id', 'purpose', @@ -34,29 +40,29 @@ class Presigned_url_requestsDBApi extends GenericDBApi { ]; } - static get AUTOCOMPLETE_FIELD() { + static override get AUTOCOMPLETE_FIELD(): string { return 'requested_key'; } - static get ASSOCIATIONS() { + static override get ASSOCIATIONS(): PresignedUrlRequestAssociationConfig[] { return [ { field: 'project', setter: 'setProject', isArray: false }, { field: 'user', setter: 'setUser', isArray: false }, ]; } - static get FIND_BY_INCLUDES() { + static override get FIND_BY_INCLUDES(): unknown[] { return [{ association: 'project' }, { association: 'user' }]; } - static get FIND_ALL_INCLUDES() { + static override get FIND_ALL_INCLUDES(): unknown[] { return [ { model: db.projects, as: 'project', required: false }, { model: db.users, as: 'user', required: false }, ]; } - static get RELATION_FILTERS() { + static override get RELATION_FILTERS(): PresignedUrlRequestRelationFilterConfig[] { return [ { filterKey: 'project', @@ -73,7 +79,9 @@ class Presigned_url_requestsDBApi extends GenericDBApi { ]; } - static getFieldMapping(data) { + static override getFieldMapping( + data: PresignedUrlRequestData, + ): PresignedUrlRequestFieldMapping { return { id: data.id || undefined, purpose: data.purpose || null, @@ -87,4 +95,4 @@ class Presigned_url_requestsDBApi extends GenericDBApi { } } -module.exports = Presigned_url_requestsDBApi; +export default Presigned_url_requestsDBApi; diff --git a/backend/src/db/api/project_audio_tracks.js b/backend/src/db/api/project_audio_tracks.js deleted file mode 100644 index 4a57c67..0000000 --- a/backend/src/db/api/project_audio_tracks.js +++ /dev/null @@ -1,199 +0,0 @@ -const GenericDBApi = require('./base.api'); -const db = require('../models'); -const Utils = require('../utils'); -const { - applyRuntimeEnvironment, - applyRuntimeProjectFilter, -} = require('./runtime-context'); - -const Sequelize = db.Sequelize; -const Op = Sequelize.Op; - -class Project_audio_tracksDBApi extends GenericDBApi { - static get MODEL() { - return db.project_audio_tracks; - } - - static get TABLE_NAME() { - return 'project_audio_tracks'; - } - - static get SEARCHABLE_FIELDS() { - return ['source_key', 'name', 'slug', 'url']; - } - - static get RANGE_FIELDS() { - return ['volume', 'sort_order']; - } - - static get ENUM_FIELDS() { - return ['environment', 'loop', 'is_enabled']; - } - - static get CSV_FIELDS() { - return [ - 'id', - 'environment', - 'source_key', - 'name', - 'slug', - 'url', - 'loop', - 'volume', - 'createdAt', - ]; - } - - static get AUTOCOMPLETE_FIELD() { - return 'name'; - } - - static get ASSOCIATIONS() { - return [{ field: 'project', setter: 'setProject', isArray: false }]; - } - - static getFieldMapping(data) { - return { - id: data.id || undefined, - environment: data.environment || null, - source_key: data.source_key || null, - name: data.name || null, - slug: data.slug || null, - url: data.url || null, - loop: data.loop || false, - volume: data.volume || null, - sort_order: data.sort_order || null, - is_enabled: data.is_enabled || false, - }; - } - - static async findBy(where, options = {}) { - const transaction = options.transaction; - const queryWhere = applyRuntimeEnvironment({ ...where }, options); - const projectInclude = applyRuntimeProjectFilter( - { model: db.projects, as: 'project' }, - options, - ); - - const record = await this.MODEL.findOne({ - where: queryWhere, - transaction, - include: [projectInclude], - }); - - if (!record) return null; - return record.get({ plain: true }); - } - - static async findAll(filter = {}, options = {}) { - filter = filter || {}; - const limit = filter.limit || 0; - const currentPage = Number(filter.page) || 0; - const offset = Math.max(currentPage - 1, 0) * limit; - - let where = {}; - - const terms = filter.project ? filter.project.split('|') : []; - const validUuids = Utils.filterValidUuids(terms); - - let include = [ - { - model: db.projects, - as: 'project', - where: filter.project - ? { - [Op.or]: [ - ...(validUuids.length > 0 - ? [{ id: { [Op.in]: validUuids } }] - : []), - { - name: { - [Op.or]: terms.map((term) => ({ [Op.iLike]: `%${term}%` })), - }, - }, - ], - } - : {}, - }, - ]; - - include[0] = applyRuntimeProjectFilter(include[0], options); - - if (filter.id) { - if (!Utils.isValidUuid(filter.id)) { - return { rows: [], count: 0 }; - } - where.id = filter.id; - } - - for (const field of this.SEARCHABLE_FIELDS) { - if (filter[field]) { - where[Op.and] = Utils.ilike(this.TABLE_NAME, field, filter[field]); - } - } - - for (const field of this.RANGE_FIELDS) { - const rangeKey = `${field}Range`; - if (filter[rangeKey]) { - const [start, end] = filter[rangeKey]; - if (start !== undefined && start !== null && start !== '') { - where[field] = { ...where[field], [Op.gte]: start }; - } - if (end !== undefined && end !== null && end !== '') { - where[field] = { ...where[field], [Op.lte]: end }; - } - } - } - - for (const field of this.ENUM_FIELDS) { - if (filter[field] !== undefined) { - where[field] = filter[field]; - } - } - - if (filter.active !== undefined) { - where.active = filter.active === true || filter.active === 'true'; - } - - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange; - if (start !== undefined && start !== null && start !== '') { - where.createdAt = { ...where.createdAt, [Op.gte]: start }; - } - if (end !== undefined && end !== null && end !== '') { - where.createdAt = { ...where.createdAt, [Op.lte]: end }; - } - } - - where = applyRuntimeEnvironment(where, options); - - const queryOptions = { - where, - include, - distinct: true, - order: - filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], - transaction: options.transaction, - }; - - if (!options.countOnly) { - queryOptions.limit = limit ? Number(limit) : undefined; - queryOptions.offset = offset ? Number(offset) : undefined; - } - - try { - const { rows, count } = await this.MODEL.findAndCountAll(queryOptions); - return { - rows: options.countOnly ? [] : rows, - count, - }; - } catch (error) { - console.error('Error executing query:', error); - throw error; - } - } -} - -module.exports = Project_audio_tracksDBApi; diff --git a/backend/src/db/api/project_audio_tracks.ts b/backend/src/db/api/project_audio_tracks.ts new file mode 100644 index 0000000..e9366cc --- /dev/null +++ b/backend/src/db/api/project_audio_tracks.ts @@ -0,0 +1,324 @@ +import type { Transaction } from 'sequelize'; + +import GenericDBApi from './base.api.ts'; +import db from '../models/index.ts'; +import Utils from '../utils.ts'; +import { + applyRuntimeEnvironment, + applyRuntimeProjectFilter, +} from './runtime-context.ts'; +import type { + DbAssociationConfig, + DbFindAllOptions, + DbFindByOptions, + EntityRecord, + PaginatedResult, + ProjectAudioTrackData, + ProjectAudioTrackFieldMapping, + ProjectAudioTrackListFilter, + ProjectAudioTrackModel, + ProjectAudioTrackRangeFilter, + ProjectAudioTrackRecord, + ProjectAudioTrackRuntimeOptions, + QueryWhere, + RuntimeProjectInclude, +} from '../../types/index.ts'; + +const { Op } = db.Sequelize; + +function isDbFindAllOptions( + value: ProjectAudioTrackListFilter | DbFindAllOptions, +): value is DbFindAllOptions { + return Boolean(value) && typeof value === 'object' && 'filter' in value; +} + +function isProjectAudioTrackListFilter( + value: unknown, +): value is ProjectAudioTrackListFilter { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function isQueryWhere(value: unknown): value is QueryWhere { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function isRangeFilter(value: unknown): value is ProjectAudioTrackRangeFilter { + return Array.isArray(value) && value.length === 2; +} + +function getFilterString( + filter: ProjectAudioTrackListFilter, + field: string, +): string | null { + const value = filter[field]; + return typeof value === 'string' ? value : null; +} + +function addRangeBoundary( + where: QueryWhere, + field: string, + operator: symbol, + value: ProjectAudioTrackRangeFilter[number], +): void { + if (value === undefined || value === null || value === '') return; + + const current = isQueryWhere(where[field]) ? where[field] : {}; + where[field] = { + ...current, + [operator]: value, + }; +} + +function addRangeFilter( + where: QueryWhere, + field: string, + range: unknown, +): void { + if (!isRangeFilter(range)) return; + + const [start, end] = range; + addRangeBoundary(where, field, Op.gte, start); + addRangeBoundary(where, field, Op.lte, end); +} + +function normalizeBooleanFilter(value: unknown): boolean { + return value === true || value === 'true'; +} + +function normalizeFilter( + filter?: ProjectAudioTrackListFilter | DbFindAllOptions, +): ProjectAudioTrackListFilter { + if (!filter) return {}; + + if (!isDbFindAllOptions(filter)) return filter; + + return isProjectAudioTrackListFilter(filter.filter) ? filter.filter : {}; +} + +class Project_audio_tracksDBApi extends GenericDBApi { + static override get MODEL(): ProjectAudioTrackModel { + return db.project_audio_tracks; + } + + static override get TABLE_NAME(): string { + return 'project_audio_tracks'; + } + + static override get SEARCHABLE_FIELDS(): string[] { + return ['source_key', 'name', 'slug', 'url']; + } + + static override get RANGE_FIELDS(): string[] { + return ['volume', 'sort_order']; + } + + static override get ENUM_FIELDS(): string[] { + return ['environment', 'loop', 'is_enabled']; + } + + static override get CSV_FIELDS(): string[] { + return [ + 'id', + 'environment', + 'source_key', + 'name', + 'slug', + 'url', + 'loop', + 'volume', + 'createdAt', + ]; + } + + static override get AUTOCOMPLETE_FIELD(): string { + return 'name'; + } + + static override get ASSOCIATIONS(): DbAssociationConfig[] { + return [{ field: 'project', setter: 'setProject', isArray: false }]; + } + + static override getFieldMapping( + data: ProjectAudioTrackData, + ): ProjectAudioTrackFieldMapping { + return { + id: data.id || undefined, + environment: data.environment || null, + source_key: data.source_key || null, + name: data.name || null, + slug: data.slug || null, + url: data.url || null, + loop: data.loop || false, + volume: data.volume || null, + sort_order: data.sort_order || null, + is_enabled: data.is_enabled || false, + }; + } + + static override async findBy( + where: { id: string }, + options?: ProjectAudioTrackRuntimeOptions, + ): Promise; + static override async findBy( + options: DbFindByOptions, + ): Promise; + static override async findBy( + whereOrOptions: { id: string } | DbFindByOptions, + options: ProjectAudioTrackRuntimeOptions = {}, + ): Promise { + const sourceWhere = + 'where' in whereOrOptions ? whereOrOptions.where : whereOrOptions; + const where = isQueryWhere(sourceWhere) ? sourceWhere : {}; + const runtimeOptions: ProjectAudioTrackRuntimeOptions = {}; + + if ('where' in whereOrOptions) { + if (whereOrOptions.transaction) { + runtimeOptions.transaction = whereOrOptions.transaction; + } + } else if (options.transaction) { + runtimeOptions.transaction = options.transaction; + } + + if (options.runtimeContext) { + runtimeOptions.runtimeContext = options.runtimeContext; + } + + const queryWhere = applyRuntimeEnvironment({ ...where }, runtimeOptions); + const projectInclude = applyRuntimeProjectFilter( + { model: db.projects, as: 'project' }, + runtimeOptions, + ); + const findOptions: { + where: QueryWhere; + include: RuntimeProjectInclude[]; + transaction?: Transaction; + } = { + where: queryWhere, + include: [projectInclude], + }; + + if (runtimeOptions.transaction) { + findOptions.transaction = runtimeOptions.transaction; + } + + const record = await this.MODEL.findOne(findOptions); + + if (!record) return null; + return record.get({ plain: true }); + } + + static override async findAll( + filter?: ProjectAudioTrackListFilter, + options?: ProjectAudioTrackRuntimeOptions, + ): Promise>; + static override async findAll( + options: DbFindAllOptions, + ): Promise>; + static override async findAll( + filter?: ProjectAudioTrackListFilter | DbFindAllOptions, + options: ProjectAudioTrackRuntimeOptions = {}, + ): Promise> { + const normalizedFilter = normalizeFilter(filter); + const limit = Number(normalizedFilter.limit) || 0; + const currentPage = Number(normalizedFilter.page) || 0; + const offset = Math.max(currentPage - 1, 0) * limit; + let where: QueryWhere = {}; + + const terms = normalizedFilter.project + ? normalizedFilter.project.split('|') + : []; + const validUuids = Utils.filterValidUuids(terms); + const include: RuntimeProjectInclude[] = [ + { + model: db.projects, + as: 'project', + where: normalizedFilter.project + ? { + [Op.or]: [ + ...(validUuids.length > 0 + ? [{ id: { [Op.in]: validUuids } }] + : []), + { + name: { + [Op.or]: terms.map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + include[0] = applyRuntimeProjectFilter(include[0], options); + + if (normalizedFilter.id) { + if (!Utils.isValidUuid(normalizedFilter.id)) { + return { rows: [], count: 0 }; + } + where.id = normalizedFilter.id; + } + + for (const field of this.SEARCHABLE_FIELDS) { + const value = getFilterString(normalizedFilter, field); + if (value) { + where[Op.and] = Utils.ilike(this.TABLE_NAME, field, value); + } + } + + for (const field of this.RANGE_FIELDS) { + addRangeFilter(where, field, normalizedFilter[`${field}Range`]); + } + + for (const field of this.ENUM_FIELDS) { + if (normalizedFilter[field] !== undefined) { + where[field] = normalizedFilter[field]; + } + } + + if (normalizedFilter.active !== undefined) { + where.active = normalizeBooleanFilter(normalizedFilter.active); + } + + addRangeFilter(where, 'createdAt', normalizedFilter.createdAtRange); + where = applyRuntimeEnvironment(where, options); + + const findOptions: { + where: QueryWhere; + include: RuntimeProjectInclude[]; + distinct: true; + order: string[][]; + transaction?: Transaction; + limit?: number; + offset?: number; + } = { + where, + include, + distinct: true, + order: + normalizedFilter.field && normalizedFilter.sort + ? [[normalizedFilter.field, normalizedFilter.sort]] + : [['createdAt', 'desc']], + }; + + if (options.transaction) { + findOptions.transaction = options.transaction; + } + + if (!options.countOnly && limit) { + findOptions.limit = Number(limit); + } + + if (!options.countOnly && offset) { + findOptions.offset = Number(offset); + } + + const { rows, count } = await this.MODEL.findAndCountAll(findOptions); + + return { + rows: options.countOnly ? [] : rows, + count, + }; + } +} + +export default Project_audio_tracksDBApi; diff --git a/backend/src/db/api/project_element_defaults.js b/backend/src/db/api/project_element_defaults.js deleted file mode 100644 index 87fbef0..0000000 --- a/backend/src/db/api/project_element_defaults.js +++ /dev/null @@ -1,390 +0,0 @@ -const GenericDBApi = require('./base.api'); -const db = require('../models'); -const Utils = require('../utils'); - -const Sequelize = db.Sequelize; -const Op = Sequelize.Op; - -class Project_element_defaultsDBApi extends GenericDBApi { - static get MODEL() { - return db.project_element_defaults; - } - - static get TABLE_NAME() { - return 'project_element_defaults'; - } - - static get SEARCHABLE_FIELDS() { - return ['name', 'element_type']; - } - - static get RANGE_FIELDS() { - return ['sort_order', 'snapshot_version']; - } - - static get ENUM_FIELDS() { - return []; - } - - static get ASSOCIATIONS() { - return [{ field: 'project', setter: 'setProject', isArray: false }]; - } - - static get RELATION_FILTERS() { - return [ - { - filterKey: 'project', - model: db.projects, - as: 'project', - searchField: 'name', - }, - ]; - } - - static get FIND_ALL_INCLUDES() { - return [{ association: 'project' }, { association: 'source_element' }]; - } - - static get CSV_FIELDS() { - return [ - 'id', - 'element_type', - 'name', - 'sort_order', - 'projectId', - 'snapshot_version', - 'createdAt', - ]; - } - - static get AUTOCOMPLETE_FIELD() { - return 'name'; - } - - // Declarative field configuration using base class patterns - static get JSON_FIELDS() { - return ['settings_json']; - } - - static get FIELD_DEFAULTS() { - return { - element_type: { default: null }, - name: { default: null }, - sort_order: { default: 0 }, - source_element_id: { default: null }, - snapshot_version: { default: 1 }, - }; - } - - static getFieldMapping(data) { - // Apply base class transformations (JSON fields, defaults, transformers) - const mapped = super.getFieldMapping(data); - - // Custom mapping for projectId field (accepts both projectId and project) - if (mapped.project && !mapped.projectId) { - mapped.projectId = mapped.project; - } - - return { - id: mapped.id || undefined, - element_type: mapped.element_type, - name: mapped.name, - sort_order: mapped.sort_order, - settings_json: mapped.settings_json, - source_element_id: mapped.source_element_id, - snapshot_version: mapped.snapshot_version, - projectId: mapped.projectId, - }; - } - - /** - * Custom findAll with project filtering - * Supports both 'project' and 'projectId' query params for consistency - */ - static async findAll(filter = {}, options = {}) { - filter = filter || {}; - const limit = filter.limit || 0; - const currentPage = Number(filter.page) || 0; - const offset = Math.max(currentPage - 1, 0) * limit; - - let where = {}; - - // Support both 'project' and 'projectId' query params - const projectFilter = filter.project || filter.projectId; - const terms = projectFilter ? projectFilter.split('|') : []; - const validUuids = Utils.filterValidUuids(terms); - - let include = [ - { - model: db.projects, - as: 'project', - where: projectFilter - ? { - [Op.or]: [ - ...(validUuids.length > 0 - ? [{ id: { [Op.in]: validUuids } }] - : []), - { - name: { - [Op.or]: terms.map((term) => ({ [Op.iLike]: `%${term}%` })), - }, - }, - ], - } - : {}, - }, - { - model: db.element_type_defaults, - as: 'source_element', - required: false, - }, - ]; - - if (filter.id) { - if (!Utils.isValidUuid(filter.id)) { - return { rows: [], count: 0 }; - } - where.id = filter.id; - } - - for (const field of this.SEARCHABLE_FIELDS) { - if (filter[field]) { - where[Op.and] = Utils.ilike(this.TABLE_NAME, field, filter[field]); - } - } - - for (const field of this.RANGE_FIELDS) { - const rangeKey = `${field}Range`; - if (filter[rangeKey]) { - const [start, end] = filter[rangeKey]; - if (start !== undefined && start !== null && start !== '') { - where[field] = { ...where[field], [Op.gte]: start }; - } - if (end !== undefined && end !== null && end !== '') { - where[field] = { ...where[field], [Op.lte]: end }; - } - } - } - - for (const field of this.ENUM_FIELDS) { - if (filter[field] !== undefined) { - where[field] = filter[field]; - } - } - - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange; - if (start !== undefined && start !== null && start !== '') { - where.createdAt = { ...where.createdAt, [Op.gte]: start }; - } - if (end !== undefined && end !== null && end !== '') { - where.createdAt = { ...where.createdAt, [Op.lte]: end }; - } - } - - const queryOptions = { - where, - include, - distinct: true, - order: - filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['sort_order', 'asc']], - transaction: options.transaction, - }; - - if (!options.countOnly) { - queryOptions.limit = limit ? Number(limit) : undefined; - queryOptions.offset = offset ? Number(offset) : undefined; - } - - try { - const { rows, count } = await this.MODEL.findAndCountAll(queryOptions); - return { - rows: options.countOnly ? [] : rows, - count, - }; - } catch (error) { - console.error('Error executing query:', error); - throw error; - } - } - - /** - * Find project element default by element type for a specific project - */ - static async findByElementType(projectId, elementType, options = {}) { - return this.MODEL.findOne({ - where: { - projectId, - element_type: elementType, - deletedAt: null, - }, - ...options, - }); - } - - /** - * Snapshot all global element defaults to a project - * Used when creating a new project - */ - static async snapshotGlobalDefaults(projectId, options = {}) { - const Element_type_defaultsDBApi = require('./element_type_defaults'); - - // Get all global defaults - const globalDefaults = await Element_type_defaultsDBApi.findAll({}); - - if (!globalDefaults?.rows?.length) { - return []; - } - - // Dedupe by element_type (keep first occurrence) - // Prevents unique constraint violations if global defaults have duplicates - const seenTypes = new Set(); - const dedupedDefaults = globalDefaults.rows.filter((row) => { - if (seenTypes.has(row.element_type)) { - console.warn( - `Duplicate element_type in global defaults: ${row.element_type} (skipping)`, - ); - return false; - } - seenTypes.add(row.element_type); - return true; - }); - - const now = new Date(); - const currentUserId = options.currentUser?.id || null; - - // Create project defaults from global defaults - const projectDefaults = await this.MODEL.bulkCreate( - dedupedDefaults.map((globalDefault) => ({ - projectId, - element_type: globalDefault.element_type, - name: globalDefault.name, - sort_order: globalDefault.sort_order, - settings_json: globalDefault.default_settings_json, - source_element_id: globalDefault.id, - snapshot_version: 1, - createdById: currentUserId, - updatedById: currentUserId, - createdAt: now, - updatedAt: now, - })), - { - transaction: options.transaction, - returning: true, - }, - ); - - return projectDefaults; - } - - /** - * Reset a project element default to the current global default - */ - static async resetToGlobal(id, options = {}) { - const Element_type_defaultsDBApi = require('./element_type_defaults'); - - // Ensure global defaults are initialized - await Element_type_defaultsDBApi.ensureInitialized(); - - // Find the project default - const projectDefault = await this.MODEL.findByPk(id); - if (!projectDefault) { - throw new Error('Project element default not found'); - } - - // Find the matching global default - const globalDefault = await Element_type_defaultsDBApi.MODEL.findOne({ - where: { - element_type: projectDefault.element_type, - deletedAt: null, - }, - }); - - if (!globalDefault) { - throw new Error( - `No global default found for element type: ${projectDefault.element_type}`, - ); - } - - // Update with global settings and increment version - const now = new Date(); - await projectDefault.update( - { - name: globalDefault.name, - sort_order: globalDefault.sort_order, - settings_json: globalDefault.default_settings_json, - source_element_id: globalDefault.id, - snapshot_version: projectDefault.snapshot_version + 1, - updatedById: options.currentUser?.id || null, - updatedAt: now, - }, - { - transaction: options.transaction, - }, - ); - - return projectDefault.reload(); - } - - /** - * Get diff between project default and current global default - */ - static async getDiffFromGlobal(id) { - const Element_type_defaultsDBApi = require('./element_type_defaults'); - - // Ensure global defaults are initialized - await Element_type_defaultsDBApi.ensureInitialized(); - - // Find the project default - const projectDefault = await this.MODEL.findByPk(id); - if (!projectDefault) { - throw new Error('Project element default not found'); - } - - // Find the matching global default - const globalDefault = await Element_type_defaultsDBApi.MODEL.findOne({ - where: { - element_type: projectDefault.element_type, - deletedAt: null, - }, - }); - - if (!globalDefault) { - return { - projectDefault, - globalDefault: null, - hasGlobalDefault: false, - isDifferent: true, - }; - } - - // Parse JSON settings for comparison - const projectSettings = - typeof projectDefault.settings_json === 'string' - ? JSON.parse(projectDefault.settings_json || '{}') - : projectDefault.settings_json || {}; - - const globalSettings = - typeof globalDefault.default_settings_json === 'string' - ? JSON.parse(globalDefault.default_settings_json || '{}') - : globalDefault.default_settings_json || {}; - - const isDifferent = - JSON.stringify(projectSettings) !== JSON.stringify(globalSettings) || - projectDefault.name !== globalDefault.name || - projectDefault.sort_order !== globalDefault.sort_order; - - return { - projectDefault, - globalDefault, - hasGlobalDefault: true, - isDifferent, - projectSettings, - globalSettings, - }; - } -} - -module.exports = Project_element_defaultsDBApi; diff --git a/backend/src/db/api/project_element_defaults.ts b/backend/src/db/api/project_element_defaults.ts new file mode 100644 index 0000000..d4de173 --- /dev/null +++ b/backend/src/db/api/project_element_defaults.ts @@ -0,0 +1,520 @@ +import GenericDBApi from './base.api.ts'; +import Element_type_defaultsDBApi from './element_type_defaults.ts'; +import db from '../models/index.ts'; +import Utils from '../utils.ts'; +import { logger } from '../../utils/logger.ts'; +import type { + DbAssociationConfig, + DbFindAllOptions, + DbFindByOptions, + DbRelationFilterConfig, + ElementSettingsJson, + EntityRecord, + GlobalElementDefaultRecord, + PaginatedResult, + ProjectElementDefaultRecord, + ProjectElementDefaultsData, + ProjectElementDefaultsDbApi, + ProjectElementDefaultsDiff, + ProjectElementDefaultsFieldMapping, + ProjectElementDefaultsListFilter, + ProjectElementDefaultsModel, + ProjectElementDefaultsModelRecord, + ProjectElementDefaultsRangeFilter, + ProjectElementDefaultsOptions, + QueryWhere, + RuntimeProjectInclude, +} from '../../types/index.ts'; + +const { Op } = db.Sequelize; + +function stringifySettings(value: ElementSettingsJson | string | null | undefined): string | null { + if (value === undefined || value === null) return null; + if (typeof value === 'string') return value; + return JSON.stringify(value); +} + +function parseSettings(value: ElementSettingsJson | string | null | undefined): ElementSettingsJson { + if (!value) return {}; + if (typeof value !== 'string') return value; + + const parsed: unknown = JSON.parse(value || '{}'); + return isElementSettingsJson(parsed) ? parsed : {}; +} + +function isElementSettingsJson(value: unknown): value is ElementSettingsJson { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function isGlobalElementDefaultRecord( + value: EntityRecord, +): value is GlobalElementDefaultRecord { + return ( + typeof value.id === 'string' && + 'element_type' in value && + typeof value.element_type === 'string' && + 'name' in value && + typeof value.name === 'string' && + 'sort_order' in value && + typeof value.sort_order === 'number' + ); +} + +function isDbFindAllOptions( + value: ProjectElementDefaultsListFilter | DbFindAllOptions, +): value is DbFindAllOptions { + return Boolean(value) && typeof value === 'object' && 'filter' in value; +} + +function isProjectElementDefaultsListFilter( + value: unknown, +): value is ProjectElementDefaultsListFilter { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function isQueryWhere(value: unknown): value is QueryWhere { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function isRangeFilter(value: unknown): value is ProjectElementDefaultsRangeFilter { + return Array.isArray(value) && value.length === 2; +} + +function normalizeFilter( + filter?: ProjectElementDefaultsListFilter | DbFindAllOptions, +): ProjectElementDefaultsListFilter { + if (!filter) return {}; + if (!isDbFindAllOptions(filter)) return filter; + return isProjectElementDefaultsListFilter(filter.filter) ? filter.filter : {}; +} + +function addRangeBoundary( + where: QueryWhere, + field: string, + operator: symbol, + value: ProjectElementDefaultsRangeFilter[number], +): void { + if (value === undefined || value === null || value === '') return; + + const current = isQueryWhere(where[field]) ? where[field] : {}; + where[field] = { + ...current, + [operator]: value, + }; +} + +function addRangeFilter( + where: QueryWhere, + field: string, + range: unknown, +): void { + if (!isRangeFilter(range)) return; + + const [start, end] = range; + addRangeBoundary(where, field, Op.gte, start); + addRangeBoundary(where, field, Op.lte, end); +} + +function getFilterString( + filter: ProjectElementDefaultsListFilter, + field: string, +): string | null { + const value = filter[field]; + return typeof value === 'string' ? value : null; +} + +class Project_element_defaultsDBApi extends GenericDBApi { + declare static findAllAutocomplete: ProjectElementDefaultsDbApi['findAllAutocomplete']; + + static override get MODEL(): ProjectElementDefaultsModel { + return db.project_element_defaults; + } + + static override get TABLE_NAME(): string { + return 'project_element_defaults'; + } + + static override get SEARCHABLE_FIELDS(): string[] { + return ['name', 'element_type']; + } + + static override get RANGE_FIELDS(): string[] { + return ['sort_order', 'snapshot_version']; + } + + static override get ENUM_FIELDS(): string[] { + return []; + } + + static override get ASSOCIATIONS(): DbAssociationConfig[] { + return [{ field: 'project', setter: 'setProject', isArray: false }]; + } + + static override get RELATION_FILTERS(): DbRelationFilterConfig[] { + return [ + { + filterKey: 'project', + model: db.projects, + as: 'project', + searchField: 'name', + }, + ]; + } + + static override get FIND_ALL_INCLUDES(): unknown[] { + return [{ association: 'project' }, { association: 'source_element' }]; + } + + static override get CSV_FIELDS(): string[] { + return [ + 'id', + 'element_type', + 'name', + 'sort_order', + 'projectId', + 'snapshot_version', + 'createdAt', + ]; + } + + static override get AUTOCOMPLETE_FIELD(): string { + return 'name'; + } + + static override get JSON_FIELDS(): string[] { + return ['settings_json']; + } + + static override get FIELD_DEFAULTS(): Record { + return { + element_type: { default: null }, + name: { default: null }, + sort_order: { default: 0 }, + source_element_id: { default: null }, + snapshot_version: { default: 1 }, + }; + } + + static override getFieldMapping( + data: ProjectElementDefaultsData, + ): ProjectElementDefaultsFieldMapping { + return { + id: data.id || undefined, + element_type: data.element_type ?? null, + name: data.name ?? null, + sort_order: data.sort_order ?? 0, + settings_json: stringifySettings(data.settings_json), + source_element_id: data.source_element_id ?? null, + snapshot_version: data.snapshot_version ?? 1, + projectId: data.projectId || data.project || null, + }; + } + + static override async findBy( + where: { id: string }, + options?: ProjectElementDefaultsOptions, + ): Promise; + static override async findBy( + options: DbFindByOptions, + ): Promise; + static override async findBy( + whereOrOptions: { id: string } | DbFindByOptions, + options: ProjectElementDefaultsOptions = {}, + ): Promise { + const where = 'where' in whereOrOptions ? whereOrOptions.where : whereOrOptions; + const findOptions: { + where: QueryWhere; + include?: unknown[]; + transaction?: ProjectElementDefaultsOptions['transaction']; + } = { + where: isQueryWhere(where) ? where : {}, + }; + + if ('where' in whereOrOptions) { + if (whereOrOptions.include) { + findOptions.include = whereOrOptions.include; + } + if (whereOrOptions.transaction) { + findOptions.transaction = whereOrOptions.transaction; + } + } else if (options.transaction) { + findOptions.transaction = options.transaction; + } + + const record = await this.MODEL.findOne(findOptions); + + return record ? record.get({ plain: true }) : null; + } + + static override async findAll( + filter?: ProjectElementDefaultsListFilter, + options?: ProjectElementDefaultsOptions, + ): Promise>; + static override async findAll( + options: DbFindAllOptions, + ): Promise>; + static override async findAll( + filter?: ProjectElementDefaultsListFilter | DbFindAllOptions, + options: ProjectElementDefaultsOptions = {}, + ): Promise> { + const normalizedFilter = normalizeFilter(filter); + const limit = Number(normalizedFilter.limit) || 0; + const currentPage = Number(normalizedFilter.page) || 0; + const offset = Math.max(currentPage - 1, 0) * limit; + const where: QueryWhere = {}; + + const projectFilter = normalizedFilter.project || normalizedFilter.projectId; + const terms = projectFilter ? projectFilter.split('|') : []; + const validUuids = Utils.filterValidUuids(terms); + const include: RuntimeProjectInclude[] = [ + { + model: db.projects, + as: 'project', + where: projectFilter + ? { + [Op.or]: [ + ...(validUuids.length > 0 + ? [{ id: { [Op.in]: validUuids } }] + : []), + { + name: { + [Op.or]: terms.map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + { + model: db.element_type_defaults, + as: 'source_element', + required: false, + }, + ]; + + if (normalizedFilter.id) { + if (!Utils.isValidUuid(normalizedFilter.id)) { + return { rows: [], count: 0 }; + } + where.id = normalizedFilter.id; + } + + for (const field of this.SEARCHABLE_FIELDS) { + const value = getFilterString(normalizedFilter, field); + if (value) { + where[Op.and] = Utils.ilike(this.TABLE_NAME, field, value); + } + } + + for (const field of this.RANGE_FIELDS) { + addRangeFilter(where, field, normalizedFilter[`${field}Range`]); + } + + for (const field of this.ENUM_FIELDS) { + if (normalizedFilter[field] !== undefined) { + where[field] = normalizedFilter[field]; + } + } + + addRangeFilter(where, 'createdAt', normalizedFilter.createdAtRange); + + const findOptions: { + where: QueryWhere; + include: RuntimeProjectInclude[]; + distinct: true; + order: string[][]; + transaction?: ProjectElementDefaultsOptions['transaction']; + limit?: number; + offset?: number; + } = { + where, + include, + distinct: true, + order: + normalizedFilter.field && normalizedFilter.sort + ? [[normalizedFilter.field, normalizedFilter.sort]] + : [['sort_order', 'asc']], + }; + + if (options.transaction) { + findOptions.transaction = options.transaction; + } + + if (!options.countOnly && limit) { + findOptions.limit = Number(limit); + } + + if (!options.countOnly && offset) { + findOptions.offset = Number(offset); + } + + const { rows, count } = await this.MODEL.findAndCountAll(findOptions); + + return { + rows: options.countOnly ? [] : rows, + count, + }; + } + + static async findByElementType( + projectId: string, + elementType: string, + options: ProjectElementDefaultsOptions = {}, + ): Promise { + const findOptions: { + where: QueryWhere; + transaction?: ProjectElementDefaultsOptions['transaction']; + } = { + where: { + projectId, + element_type: elementType, + deletedAt: null, + }, + }; + + if (options.transaction) { + findOptions.transaction = options.transaction; + } + + return this.MODEL.findOne(findOptions); + } + + static async snapshotGlobalDefaults( + projectId: string, + options: ProjectElementDefaultsOptions = {}, + ): Promise { + const globalDefaults = await Element_type_defaultsDBApi.findAll({}); + + if (!globalDefaults.rows.length) { + return []; + } + + const seenTypes = new Set(); + const dedupedDefaults = globalDefaults.rows.filter(isGlobalElementDefaultRecord).filter((row) => { + if (seenTypes.has(row.element_type)) { + logger.warn( + { elementType: row.element_type }, + 'Duplicate element_type in global defaults skipped', + ); + return false; + } + seenTypes.add(row.element_type); + return true; + }); + + const now = new Date(); + const currentUserId = options.currentUser?.id || null; + const projectDefaults = await this.MODEL.bulkCreate( + dedupedDefaults.map((globalDefault) => ({ + projectId, + element_type: globalDefault.element_type, + name: globalDefault.name, + sort_order: globalDefault.sort_order, + settings_json: stringifySettings(globalDefault.default_settings_json), + source_element_id: globalDefault.id, + snapshot_version: 1, + createdById: currentUserId, + updatedById: currentUserId, + createdAt: now, + updatedAt: now, + id: undefined, + })), + { + transaction: options.transaction, + returning: true, + }, + ); + + return projectDefaults.map((projectDefault) => + projectDefault.get({ plain: true }), + ); + } + + static async resetToGlobal( + id: string, + options: ProjectElementDefaultsOptions = {}, + ): Promise { + await Element_type_defaultsDBApi.ensureInitialized(); + + const projectDefault = await this.MODEL.findByPk(id); + if (!projectDefault) { + throw new Error('Project element default not found'); + } + + const globalDefault = await Element_type_defaultsDBApi.MODEL.findOne({ + where: { + element_type: projectDefault.element_type, + deletedAt: null, + }, + }); + + if (!globalDefault) { + throw new Error( + `No global default found for element type: ${projectDefault.element_type}`, + ); + } + + await projectDefault.update( + { + name: globalDefault.name, + sort_order: globalDefault.sort_order, + settings_json: stringifySettings(globalDefault.default_settings_json), + source_element_id: globalDefault.id, + snapshot_version: projectDefault.snapshot_version + 1, + updatedById: options.currentUser?.id || null, + updatedAt: new Date(), + }, + { + transaction: options.transaction, + }, + ); + + const reloaded = await projectDefault.reload(); + return reloaded.get({ plain: true }); + } + + static async getDiffFromGlobal( + id: string, + ): Promise { + await Element_type_defaultsDBApi.ensureInitialized(); + + const projectDefault = await this.MODEL.findByPk(id); + if (!projectDefault) { + throw new Error('Project element default not found'); + } + + const globalDefault = await Element_type_defaultsDBApi.MODEL.findOne({ + where: { + element_type: projectDefault.element_type, + deletedAt: null, + }, + }); + + if (!globalDefault) { + return { + projectDefault: projectDefault.get({ plain: true }), + globalDefault: null, + hasGlobalDefault: false, + isDifferent: true, + }; + } + + const projectSettings = parseSettings(projectDefault.settings_json); + const globalSettings = parseSettings(globalDefault.default_settings_json); + const isDifferent = + JSON.stringify(projectSettings) !== JSON.stringify(globalSettings) || + projectDefault.name !== globalDefault.name || + projectDefault.sort_order !== globalDefault.sort_order; + + return { + projectDefault: projectDefault.get({ plain: true }), + globalDefault: globalDefault.get({ plain: true }), + hasGlobalDefault: true, + isDifferent, + projectSettings, + globalSettings, + }; + } +} + +export default Project_element_defaultsDBApi; diff --git a/backend/src/db/api/project_memberships.js b/backend/src/db/api/project_memberships.ts similarity index 56% rename from backend/src/db/api/project_memberships.js rename to backend/src/db/api/project_memberships.ts index 067af1e..aba5e81 100644 --- a/backend/src/db/api/project_memberships.js +++ b/backend/src/db/api/project_memberships.ts @@ -1,28 +1,34 @@ -const GenericDBApi = require('./base.api'); -const db = require('../models'); +import GenericDBApi from './base.api.ts'; +import db from '../models/index.ts'; +import type { + ProjectMembershipAssociationConfig, + ProjectMembershipData, + ProjectMembershipFieldMapping, + ProjectMembershipRelationFilterConfig, +} from '../../types/index.ts'; class Project_membershipsDBApi extends GenericDBApi { - static get MODEL() { + static override get MODEL(): unknown { return db.project_memberships; } - static get TABLE_NAME() { + static override get TABLE_NAME(): string { return 'project_memberships'; } - static get SEARCHABLE_FIELDS() { + static override get SEARCHABLE_FIELDS(): string[] { return []; } - static get RANGE_FIELDS() { + static override get RANGE_FIELDS(): string[] { return ['invited_at', 'accepted_at']; } - static get ENUM_FIELDS() { + static override get ENUM_FIELDS(): string[] { return ['access_level', 'is_active']; } - static get CSV_FIELDS() { + static override get CSV_FIELDS(): string[] { return [ 'id', 'access_level', @@ -33,29 +39,29 @@ class Project_membershipsDBApi extends GenericDBApi { ]; } - static get AUTOCOMPLETE_FIELD() { + static override get AUTOCOMPLETE_FIELD(): string { return 'access_level'; } - static get ASSOCIATIONS() { + static override get ASSOCIATIONS(): ProjectMembershipAssociationConfig[] { return [ { field: 'project', setter: 'setProject', isArray: false }, { field: 'user', setter: 'setUser', isArray: false }, ]; } - static get FIND_BY_INCLUDES() { + static override get FIND_BY_INCLUDES(): unknown[] { return [{ association: 'project' }, { association: 'user' }]; } - static get FIND_ALL_INCLUDES() { + static override get FIND_ALL_INCLUDES(): unknown[] { return [ { model: db.projects, as: 'project', required: false }, { model: db.users, as: 'user', required: false }, ]; } - static get RELATION_FILTERS() { + static override get RELATION_FILTERS(): ProjectMembershipRelationFilterConfig[] { return [ { filterKey: 'project', @@ -72,7 +78,9 @@ class Project_membershipsDBApi extends GenericDBApi { ]; } - static getFieldMapping(data) { + static override getFieldMapping( + data: ProjectMembershipData, + ): ProjectMembershipFieldMapping { return { id: data.id || undefined, access_level: data.access_level || null, @@ -83,4 +91,4 @@ class Project_membershipsDBApi extends GenericDBApi { } } -module.exports = Project_membershipsDBApi; +export default Project_membershipsDBApi; diff --git a/backend/src/db/api/project_transition_settings.js b/backend/src/db/api/project_transition_settings.js deleted file mode 100644 index 6757dd1..0000000 --- a/backend/src/db/api/project_transition_settings.js +++ /dev/null @@ -1,277 +0,0 @@ -const GenericDBApi = require('./base.api'); -const db = require('../models'); -const Utils = require('../utils'); -const { - applyRuntimeEnvironment, - applyRuntimeProjectFilter, -} = require('./runtime-context'); - -const Sequelize = db.Sequelize; -const Op = Sequelize.Op; - -class Project_transition_settingsDBApi extends GenericDBApi { - static get MODEL() { - return db.project_transition_settings; - } - - static get TABLE_NAME() { - return 'project_transition_settings'; - } - - static get SEARCHABLE_FIELDS() { - return ['source_key', 'transition_type', 'easing', 'overlay_color']; - } - - static get RANGE_FIELDS() { - return ['duration_ms']; - } - - static get ENUM_FIELDS() { - return ['environment']; - } - - static get CSV_FIELDS() { - return [ - 'id', - 'environment', - 'source_key', - 'transition_type', - 'duration_ms', - 'easing', - 'overlay_color', - 'createdAt', - ]; - } - - static get AUTOCOMPLETE_FIELD() { - return 'transition_type'; - } - - static get ASSOCIATIONS() { - return [{ field: 'project', setter: 'setProject', isArray: false }]; - } - - static getFieldMapping(data) { - // Note: environment and projectId are NOT included here because they are - // set explicitly in upsertForProject and should never be changed via data - return { - id: data.id || undefined, - source_key: data.source_key || null, - transition_type: data.transition_type || 'fade', - duration_ms: data.duration_ms !== undefined ? data.duration_ms : 700, - easing: data.easing || 'ease-in-out', - overlay_color: data.overlay_color || '#000000', - }; - } - - /** - * Find settings by project ID and environment. - * This is the primary method for fetching transition settings. - * - * @param {string} projectId - Project ID - * @param {string} environment - Environment (dev, stage, production) - * @param {object} options - Query options - * @returns {object|null} Settings record or null - */ - static async findByProjectAndEnvironment( - projectId, - environment, - options = {}, - ) { - const transaction = options.transaction; - - const record = await this.MODEL.findOne({ - where: { - projectId, - environment, - }, - transaction, - include: [ - { - model: db.projects, - as: 'project', - }, - ], - }); - - if (!record) return null; - return record.get({ plain: true }); - } - - /** - * Create or update settings for a project/environment combination. - * Uses upsert semantics - creates if not exists, updates if exists. - * - * @param {string} projectId - Project ID - * @param {string} environment - Environment (dev, stage, production) - * @param {object} data - Settings data - * @param {object} options - Query options - * @returns {object} Created or updated record - */ - static async upsertForProject(projectId, environment, data, options = {}) { - const transaction = options.transaction; - const currentUser = options.currentUser; - - // Check if record exists - const existing = await this.MODEL.findOne({ - where: { projectId, environment }, - transaction, - }); - - if (existing) { - // Update existing record - await existing.update( - { - ...this.getFieldMapping(data), - updatedById: currentUser?.id || null, - }, - { transaction }, - ); - return existing.get({ plain: true }); - } - - // Create new record - const newRecord = await this.MODEL.create( - { - ...this.getFieldMapping(data), - projectId, - environment, - createdById: currentUser?.id || null, - updatedById: currentUser?.id || null, - }, - { transaction }, - ); - - return newRecord.get({ plain: true }); - } - - static async findBy(where, options = {}) { - const transaction = options.transaction; - const queryWhere = applyRuntimeEnvironment({ ...where }, options); - const projectInclude = applyRuntimeProjectFilter( - { model: db.projects, as: 'project' }, - options, - ); - - const record = await this.MODEL.findOne({ - where: queryWhere, - transaction, - include: [projectInclude], - }); - - if (!record) return null; - return record.get({ plain: true }); - } - - static async findAll(filter = {}, options = {}) { - filter = filter || {}; - const limit = filter.limit || 0; - const currentPage = Number(filter.page) || 0; - const offset = Math.max(currentPage - 1, 0) * limit; - - let where = {}; - - const terms = filter.project ? filter.project.split('|') : []; - const validUuids = Utils.filterValidUuids(terms); - - let include = [ - { - model: db.projects, - as: 'project', - where: filter.project - ? { - [Op.or]: [ - ...(validUuids.length > 0 - ? [{ id: { [Op.in]: validUuids } }] - : []), - { - name: { - [Op.or]: terms.map((term) => ({ [Op.iLike]: `%${term}%` })), - }, - }, - ], - } - : {}, - }, - ]; - - include[0] = applyRuntimeProjectFilter(include[0], options); - - if (filter.id) { - if (!Utils.isValidUuid(filter.id)) { - return { rows: [], count: 0 }; - } - where.id = filter.id; - } - - for (const field of this.SEARCHABLE_FIELDS) { - if (filter[field]) { - where[Op.and] = Utils.ilike(this.TABLE_NAME, field, filter[field]); - } - } - - for (const field of this.RANGE_FIELDS) { - const rangeKey = `${field}Range`; - if (filter[rangeKey]) { - const [start, end] = filter[rangeKey]; - if (start !== undefined && start !== null && start !== '') { - where[field] = { ...where[field], [Op.gte]: start }; - } - if (end !== undefined && end !== null && end !== '') { - where[field] = { ...where[field], [Op.lte]: end }; - } - } - } - - for (const field of this.ENUM_FIELDS) { - if (filter[field] !== undefined) { - where[field] = filter[field]; - } - } - - if (filter.active !== undefined) { - where.active = filter.active === true || filter.active === 'true'; - } - - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange; - if (start !== undefined && start !== null && start !== '') { - where.createdAt = { ...where.createdAt, [Op.gte]: start }; - } - if (end !== undefined && end !== null && end !== '') { - where.createdAt = { ...where.createdAt, [Op.lte]: end }; - } - } - - where = applyRuntimeEnvironment(where, options); - - const queryOptions = { - where, - include, - distinct: true, - order: - filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], - transaction: options.transaction, - }; - - if (!options.countOnly) { - queryOptions.limit = limit ? Number(limit) : undefined; - queryOptions.offset = offset ? Number(offset) : undefined; - } - - try { - const { rows, count } = await this.MODEL.findAndCountAll(queryOptions); - return { - rows: options.countOnly ? [] : rows, - count, - }; - } catch (error) { - console.error('Error executing query:', error); - throw error; - } - } -} - -module.exports = Project_transition_settingsDBApi; diff --git a/backend/src/db/api/project_transition_settings.ts b/backend/src/db/api/project_transition_settings.ts new file mode 100644 index 0000000..75c088d --- /dev/null +++ b/backend/src/db/api/project_transition_settings.ts @@ -0,0 +1,407 @@ +import type { Transaction } from 'sequelize'; + +import GenericDBApi from './base.api.ts'; +import db from '../models/index.ts'; +import Utils from '../utils.ts'; +import { + applyRuntimeEnvironment, + applyRuntimeProjectFilter, +} from './runtime-context.ts'; +import type { + DbAssociationConfig, + DbFindAllOptions, + DbFindByOptions, + EntityRecord, + PaginatedResult, + ProjectTransitionEasing, + ProjectTransitionSettingsData, + ProjectTransitionSettingsDbApi, + ProjectTransitionSettingsFieldMapping, + ProjectTransitionSettingsListFilter, + ProjectTransitionSettingsModel, + ProjectTransitionSettingsRangeFilter, + ProjectTransitionSettingsRecord, + ProjectTransitionSettingsRuntimeOptions, + ProjectTransitionType, + QueryWhere, + RuntimeEnvironment, + RuntimeProjectInclude, +} from '../../types/index.ts'; + +const { Op } = db.Sequelize; +const defaultTransitionType: ProjectTransitionType = 'fade'; +const defaultDurationMs = 700; +const defaultEasing: ProjectTransitionEasing = 'ease-in-out'; +const defaultOverlayColor = '#000000'; + +function isDbFindAllOptions( + value: ProjectTransitionSettingsListFilter | DbFindAllOptions, +): value is DbFindAllOptions { + return Boolean(value) && typeof value === 'object' && 'filter' in value; +} + +function isProjectTransitionSettingsListFilter( + value: unknown, +): value is ProjectTransitionSettingsListFilter { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function isQueryWhere(value: unknown): value is QueryWhere { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function isRangeFilter( + value: unknown, +): value is ProjectTransitionSettingsRangeFilter { + return Array.isArray(value) && value.length === 2; +} + +function getFilterString( + filter: ProjectTransitionSettingsListFilter, + field: string, +): string | null { + const value = filter[field]; + return typeof value === 'string' ? value : null; +} + +function addRangeBoundary( + where: QueryWhere, + field: string, + operator: symbol, + value: ProjectTransitionSettingsRangeFilter[number], +): void { + if (value === undefined || value === null || value === '') return; + + const current = isQueryWhere(where[field]) ? where[field] : {}; + where[field] = { + ...current, + [operator]: value, + }; +} + +function addRangeFilter( + where: QueryWhere, + field: string, + range: unknown, +): void { + if (!isRangeFilter(range)) return; + + const [start, end] = range; + addRangeBoundary(where, field, Op.gte, start); + addRangeBoundary(where, field, Op.lte, end); +} + +function normalizeBooleanFilter(value: unknown): boolean { + return value === true || value === 'true'; +} + +function normalizeFilter( + filter?: ProjectTransitionSettingsListFilter | DbFindAllOptions, +): ProjectTransitionSettingsListFilter { + if (!filter) return {}; + + if (!isDbFindAllOptions(filter)) return filter; + + return isProjectTransitionSettingsListFilter(filter.filter) + ? filter.filter + : {}; +} + +class Project_transition_settingsDBApi extends GenericDBApi { + declare static create: ProjectTransitionSettingsDbApi['create']; + declare static update: ProjectTransitionSettingsDbApi['update']; + declare static deleteByIds: ProjectTransitionSettingsDbApi['deleteByIds']; + declare static remove: ProjectTransitionSettingsDbApi['remove']; + + static override get MODEL(): ProjectTransitionSettingsModel { + return db.project_transition_settings; + } + + static override get TABLE_NAME(): string { + return 'project_transition_settings'; + } + + static override get SEARCHABLE_FIELDS(): string[] { + return ['source_key', 'transition_type', 'easing', 'overlay_color']; + } + + static override get RANGE_FIELDS(): string[] { + return ['duration_ms']; + } + + static override get ENUM_FIELDS(): string[] { + return ['environment']; + } + + static override get CSV_FIELDS(): string[] { + return [ + 'id', + 'environment', + 'source_key', + 'transition_type', + 'duration_ms', + 'easing', + 'overlay_color', + 'createdAt', + ]; + } + + static override get AUTOCOMPLETE_FIELD(): string { + return 'transition_type'; + } + + static override get ASSOCIATIONS(): DbAssociationConfig[] { + return [{ field: 'project', setter: 'setProject', isArray: false }]; + } + + static override getFieldMapping( + data: ProjectTransitionSettingsData, + ): ProjectTransitionSettingsFieldMapping { + return { + id: data.id || undefined, + source_key: data.source_key || null, + transition_type: data.transition_type || defaultTransitionType, + duration_ms: + data.duration_ms !== undefined ? data.duration_ms : defaultDurationMs, + easing: data.easing || defaultEasing, + overlay_color: data.overlay_color || defaultOverlayColor, + }; + } + + static async findByProjectAndEnvironment( + projectId: string, + environment: RuntimeEnvironment, + options: ProjectTransitionSettingsRuntimeOptions = {}, + ): Promise { + const findOptions: { + where: QueryWhere; + include: RuntimeProjectInclude[]; + transaction?: Transaction; + } = { + where: { projectId, environment }, + include: [{ model: db.projects, as: 'project' }], + }; + + if (options.transaction) { + findOptions.transaction = options.transaction; + } + + const record = await this.MODEL.findOne(findOptions); + + if (!record) return null; + return record.get({ plain: true }); + } + + static async upsertForProject( + projectId: string, + environment: RuntimeEnvironment, + data: ProjectTransitionSettingsData, + options: ProjectTransitionSettingsRuntimeOptions = {}, + ): Promise { + const transaction = options.transaction; + const currentUser = options.currentUser; + const findOptions: { + where: QueryWhere; + transaction?: Transaction; + } = { + where: { projectId, environment }, + }; + + if (transaction) { + findOptions.transaction = transaction; + } + + const existing = await this.MODEL.findOne(findOptions); + + if (existing) { + await existing.update( + { + ...this.getFieldMapping(data), + updatedById: currentUser?.id || null, + }, + transaction ? { transaction } : {}, + ); + return existing.get({ plain: true }); + } + + const newRecord = await this.MODEL.create( + { + ...this.getFieldMapping(data), + projectId, + environment, + createdById: currentUser?.id || null, + updatedById: currentUser?.id || null, + }, + transaction ? { transaction } : {}, + ); + + return newRecord.get({ plain: true }); + } + + static override async findBy( + where: { id: string }, + options?: ProjectTransitionSettingsRuntimeOptions, + ): Promise; + static override async findBy( + options: DbFindByOptions, + ): Promise; + static override async findBy( + whereOrOptions: { id: string } | DbFindByOptions, + options: ProjectTransitionSettingsRuntimeOptions = {}, + ): Promise { + const sourceWhere = + 'where' in whereOrOptions ? whereOrOptions.where : whereOrOptions; + const where = isQueryWhere(sourceWhere) ? sourceWhere : {}; + const runtimeOptions: ProjectTransitionSettingsRuntimeOptions = {}; + + if ('where' in whereOrOptions) { + if (whereOrOptions.transaction) { + runtimeOptions.transaction = whereOrOptions.transaction; + } + } else if (options.transaction) { + runtimeOptions.transaction = options.transaction; + } + + if (options.runtimeContext) { + runtimeOptions.runtimeContext = options.runtimeContext; + } + + const queryWhere = applyRuntimeEnvironment({ ...where }, runtimeOptions); + const projectInclude = applyRuntimeProjectFilter( + { model: db.projects, as: 'project' }, + runtimeOptions, + ); + const findOptions: { + where: QueryWhere; + include: RuntimeProjectInclude[]; + transaction?: Transaction; + } = { + where: queryWhere, + include: [projectInclude], + }; + + if (runtimeOptions.transaction) { + findOptions.transaction = runtimeOptions.transaction; + } + + const record = await this.MODEL.findOne(findOptions); + + if (!record) return null; + return record.get({ plain: true }); + } + + static override async findAll( + filter?: ProjectTransitionSettingsListFilter, + options?: ProjectTransitionSettingsRuntimeOptions, + ): Promise>; + static override async findAll( + options: DbFindAllOptions, + ): Promise>; + static override async findAll( + filter?: ProjectTransitionSettingsListFilter | DbFindAllOptions, + options: ProjectTransitionSettingsRuntimeOptions = {}, + ): Promise> { + const normalizedFilter = normalizeFilter(filter); + const limit = Number(normalizedFilter.limit) || 0; + const currentPage = Number(normalizedFilter.page) || 0; + const offset = Math.max(currentPage - 1, 0) * limit; + let where: QueryWhere = {}; + + const terms = normalizedFilter.project + ? normalizedFilter.project.split('|') + : []; + const validUuids = Utils.filterValidUuids(terms); + const include: RuntimeProjectInclude[] = [ + { + model: db.projects, + as: 'project', + where: normalizedFilter.project + ? { + [Op.or]: [ + ...(validUuids.length > 0 + ? [{ id: { [Op.in]: validUuids } }] + : []), + { + name: { + [Op.or]: terms.map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + include[0] = applyRuntimeProjectFilter(include[0], options); + + if (normalizedFilter.id) { + if (!Utils.isValidUuid(normalizedFilter.id)) { + return { rows: [], count: 0 }; + } + where.id = normalizedFilter.id; + } + + for (const field of this.SEARCHABLE_FIELDS) { + const value = getFilterString(normalizedFilter, field); + if (value) { + where[Op.and] = Utils.ilike(this.TABLE_NAME, field, value); + } + } + + for (const field of this.RANGE_FIELDS) { + addRangeFilter(where, field, normalizedFilter[`${field}Range`]); + } + + for (const field of this.ENUM_FIELDS) { + if (normalizedFilter[field] !== undefined) { + where[field] = normalizedFilter[field]; + } + } + + if (normalizedFilter.active !== undefined) { + where.active = normalizeBooleanFilter(normalizedFilter.active); + } + + addRangeFilter(where, 'createdAt', normalizedFilter.createdAtRange); + where = applyRuntimeEnvironment(where, options); + + const findOptions: { + where: QueryWhere; + include: RuntimeProjectInclude[]; + distinct: true; + order: string[][]; + transaction?: Transaction; + limit?: number; + offset?: number; + } = { + where, + include, + distinct: true, + order: + normalizedFilter.field && normalizedFilter.sort + ? [[normalizedFilter.field, normalizedFilter.sort]] + : [['createdAt', 'desc']], + }; + + if (options.transaction) { + findOptions.transaction = options.transaction; + } + + if (!options.countOnly && limit) { + findOptions.limit = Number(limit); + } + + if (!options.countOnly && offset) { + findOptions.offset = Number(offset); + } + + const { rows, count } = await this.MODEL.findAndCountAll(findOptions); + + return { + rows: options.countOnly ? [] : rows, + count, + }; + } +} + +export default Project_transition_settingsDBApi; diff --git a/backend/src/db/api/project_ui_control_settings.js b/backend/src/db/api/project_ui_control_settings.js deleted file mode 100644 index 0e7a333..0000000 --- a/backend/src/db/api/project_ui_control_settings.js +++ /dev/null @@ -1,183 +0,0 @@ -const GenericDBApi = require('./base.api'); -const db = require('../models'); -const Utils = require('../utils'); -const { - applyRuntimeEnvironment, - applyRuntimeProjectFilter, -} = require('./runtime-context'); - -const Sequelize = db.Sequelize; -const Op = Sequelize.Op; - -class Project_ui_control_settingsDBApi extends GenericDBApi { - static get MODEL() { - return db.project_ui_control_settings; - } - - static get TABLE_NAME() { - return 'project_ui_control_settings'; - } - - static get SEARCHABLE_FIELDS() { - return ['source_key']; - } - - static get RANGE_FIELDS() { - return []; - } - - static get ENUM_FIELDS() { - return ['environment']; - } - - static get ASSOCIATIONS() { - return [{ field: 'project', setter: 'setProject', isArray: false }]; - } - - static getFieldMapping(data) { - return { - id: data.id || undefined, - source_key: data.source_key || null, - settings_json: data.settings_json || data.settings || {}, - }; - } - - static async findByProjectAndEnvironment( - projectId, - environment, - options = {}, - ) { - const record = await this.MODEL.findOne({ - where: { projectId, environment }, - transaction: options.transaction, - include: [{ model: db.projects, as: 'project' }], - }); - - if (!record) return null; - return record.get({ plain: true }); - } - - static async upsertForProject(projectId, environment, data, options = {}) { - const transaction = options.transaction; - const currentUser = options.currentUser; - const existing = await this.MODEL.findOne({ - where: { projectId, environment }, - transaction, - }); - - if (existing) { - await existing.update( - { - ...this.getFieldMapping(data), - updatedById: currentUser?.id || null, - }, - { transaction }, - ); - return existing.get({ plain: true }); - } - - const newRecord = await this.MODEL.create( - { - ...this.getFieldMapping(data), - projectId, - environment, - createdById: currentUser?.id || null, - updatedById: currentUser?.id || null, - }, - { transaction }, - ); - - return newRecord.get({ plain: true }); - } - - static async findBy(where, options = {}) { - const queryWhere = applyRuntimeEnvironment({ ...where }, options); - const projectInclude = applyRuntimeProjectFilter( - { model: db.projects, as: 'project' }, - options, - ); - - const record = await this.MODEL.findOne({ - where: queryWhere, - transaction: options.transaction, - include: [projectInclude], - }); - - if (!record) return null; - return record.get({ plain: true }); - } - - static async findAll(filter = {}, options = {}) { - filter = filter || {}; - const limit = filter.limit || 0; - const currentPage = Number(filter.page) || 0; - const offset = Math.max(currentPage - 1, 0) * limit; - let where = {}; - - const terms = filter.project ? filter.project.split('|') : []; - const validUuids = Utils.filterValidUuids(terms); - let include = [ - { - model: db.projects, - as: 'project', - where: filter.project - ? { - [Op.or]: [ - ...(validUuids.length > 0 - ? [{ id: { [Op.in]: validUuids } }] - : []), - { - name: { - [Op.or]: terms.map((term) => ({ [Op.iLike]: `%${term}%` })), - }, - }, - ], - } - : {}, - }, - ]; - - include[0] = applyRuntimeProjectFilter(include[0], options); - - if (filter.id) { - if (!Utils.isValidUuid(filter.id)) { - return { rows: [], count: 0 }; - } - where.id = filter.id; - } - - for (const field of this.SEARCHABLE_FIELDS) { - if (filter[field]) { - where[Op.and] = Utils.ilike(this.TABLE_NAME, field, filter[field]); - } - } - - for (const field of this.ENUM_FIELDS) { - if (filter[field] !== undefined) { - where[field] = filter[field]; - } - } - - where = applyRuntimeEnvironment(where, options); - - const { rows, count } = await this.MODEL.findAndCountAll({ - where, - include, - distinct: true, - order: - filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], - transaction: options.transaction, - limit: !options.countOnly && limit ? Number(limit) : undefined, - offset: !options.countOnly && offset ? Number(offset) : undefined, - }); - - return { - rows: options.countOnly ? [] : rows, - count, - }; - } -} - -module.exports = Project_ui_control_settingsDBApi; diff --git a/backend/src/db/api/project_ui_control_settings.ts b/backend/src/db/api/project_ui_control_settings.ts new file mode 100644 index 0000000..a3c91af --- /dev/null +++ b/backend/src/db/api/project_ui_control_settings.ts @@ -0,0 +1,312 @@ +import GenericDBApi from './base.api.ts'; +import db from '../models/index.ts'; +import Utils from '../utils.ts'; +import { + applyRuntimeEnvironment, + applyRuntimeProjectFilter, +} from './runtime-context.ts'; +import type { + DbAssociationConfig, + DbFindAllOptions, + DbFindByOptions, + EntityRecord, + PaginatedResult, + ProjectSettingsListFilter, + ProjectUiControlSettingsData, + ProjectUiControlSettingsFieldMapping, + ProjectUiControlSettingsModel, + ProjectUiControlSettingsRecord, + ProjectUiControlSettingsRuntimeOptions, + ProjectUiControlSettingsUpsertOptions, + QueryWhere, + RuntimeEnvironment, + RuntimeProjectInclude, +} from '../../types/index.ts'; +import type { Transaction } from 'sequelize'; + +const { Op } = db.Sequelize; + +function isDbFindAllOptions( + value: ProjectSettingsListFilter | DbFindAllOptions, +): value is DbFindAllOptions { + return Boolean(value) && typeof value === 'object' && 'filter' in value; +} + +function isProjectSettingsListFilter( + value: unknown, +): value is ProjectSettingsListFilter { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function isQueryWhere(value: unknown): value is QueryWhere { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +class Project_ui_control_settingsDBApi extends GenericDBApi { + static override get MODEL(): ProjectUiControlSettingsModel { + return db.project_ui_control_settings; + } + + static override get TABLE_NAME(): string { + return 'project_ui_control_settings'; + } + + static override get SEARCHABLE_FIELDS(): string[] { + return ['source_key']; + } + + static override get RANGE_FIELDS(): string[] { + return []; + } + + static override get ENUM_FIELDS(): string[] { + return ['environment']; + } + + static override get ASSOCIATIONS(): DbAssociationConfig[] { + return [{ field: 'project', setter: 'setProject', isArray: false }]; + } + + static override getFieldMapping( + data: ProjectUiControlSettingsData, + ): ProjectUiControlSettingsFieldMapping { + return { + id: data.id || undefined, + source_key: data.source_key || null, + settings_json: data.settings_json || data.settings || {}, + }; + } + + static async findByProjectAndEnvironment( + projectId: string, + environment: RuntimeEnvironment, + options: ProjectUiControlSettingsRuntimeOptions = {}, + ): Promise { + const findOptions: { + where: QueryWhere; + transaction?: Transaction; + include: RuntimeProjectInclude[]; + } = { + where: { projectId, environment }, + include: [{ model: db.projects, as: 'project' }], + }; + + if (options.transaction) { + findOptions.transaction = options.transaction; + } + + const record = await this.MODEL.findOne(findOptions); + + if (!record) return null; + return record.get({ plain: true }); + } + + static async upsertForProject( + projectId: string, + environment: RuntimeEnvironment, + data: ProjectUiControlSettingsData, + options: ProjectUiControlSettingsUpsertOptions = {}, + ): Promise { + const transaction = options.transaction; + const currentUser = options.currentUser; + const findOptions: { + where: QueryWhere; + transaction?: Transaction; + } = { + where: { projectId, environment }, + }; + + if (transaction) { + findOptions.transaction = transaction; + } + + const existing = await this.MODEL.findOne(findOptions); + + if (existing) { + await existing.update( + { + ...this.getFieldMapping(data), + updatedById: currentUser?.id || null, + }, + transaction ? { transaction } : {}, + ); + return existing.get({ plain: true }); + } + + const newRecord = await this.MODEL.create( + { + ...this.getFieldMapping(data), + projectId, + environment, + createdById: currentUser?.id || null, + updatedById: currentUser?.id || null, + }, + transaction ? { transaction } : {}, + ); + + return newRecord.get({ plain: true }); + } + + static override async findBy( + where: { id: string }, + options?: ProjectUiControlSettingsRuntimeOptions, + ): Promise; + static override async findBy( + options: DbFindByOptions, + ): Promise; + static override async findBy( + whereOrOptions: { id: string } | DbFindByOptions, + options: ProjectUiControlSettingsRuntimeOptions = {}, + ): Promise { + const sourceWhere = + 'where' in whereOrOptions ? whereOrOptions.where : whereOrOptions; + const where = isQueryWhere(sourceWhere) ? sourceWhere : {}; + let runtimeOptions: ProjectUiControlSettingsRuntimeOptions = options; + + if ('where' in whereOrOptions) { + runtimeOptions = {}; + + if (whereOrOptions.transaction) { + runtimeOptions.transaction = whereOrOptions.transaction; + } + + if (options.runtimeContext) { + runtimeOptions.runtimeContext = options.runtimeContext; + } + } + const queryWhere = applyRuntimeEnvironment({ ...where }, runtimeOptions); + const projectInclude = applyRuntimeProjectFilter( + { model: db.projects, as: 'project' }, + runtimeOptions, + ); + + const findOptions: { + where: QueryWhere; + transaction?: Transaction; + include: RuntimeProjectInclude[]; + } = { + where: queryWhere, + include: [projectInclude], + }; + + if (runtimeOptions.transaction) { + findOptions.transaction = runtimeOptions.transaction; + } + + const record = await this.MODEL.findOne(findOptions); + + if (!record) return null; + return record.get({ plain: true }); + } + + static override async findAll( + filter?: ProjectSettingsListFilter, + options?: ProjectUiControlSettingsRuntimeOptions, + ): Promise>; + static override async findAll( + options: DbFindAllOptions, + ): Promise>; + static override async findAll( + filter?: ProjectSettingsListFilter | DbFindAllOptions, + options: ProjectUiControlSettingsRuntimeOptions = {}, + ): Promise> { + const normalizedFilter = filter + ? isDbFindAllOptions(filter) + ? isProjectSettingsListFilter(filter.filter) + ? filter.filter + : {} + : filter + : {}; + const limit = Number(normalizedFilter.limit) || 0; + const currentPage = Number(normalizedFilter.page) || 0; + const offset = Math.max(currentPage - 1, 0) * limit; + let where: QueryWhere = {}; + + const terms = normalizedFilter.project + ? normalizedFilter.project.split('|') + : []; + const validUuids = Utils.filterValidUuids(terms); + const include: RuntimeProjectInclude[] = [ + { + model: db.projects, + as: 'project', + where: normalizedFilter.project + ? { + [Op.or]: [ + ...(validUuids.length > 0 + ? [{ id: { [Op.in]: validUuids } }] + : []), + { + name: { + [Op.or]: terms.map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + include[0] = applyRuntimeProjectFilter(include[0], options); + + if (normalizedFilter.id) { + if (!Utils.isValidUuid(normalizedFilter.id)) { + return { rows: [], count: 0 }; + } + where.id = normalizedFilter.id; + } + + if (normalizedFilter.source_key) { + where[Op.and] = Utils.ilike( + this.TABLE_NAME, + 'source_key', + normalizedFilter.source_key, + ); + } + + if (normalizedFilter.environment !== undefined) { + where.environment = normalizedFilter.environment; + } + + where = applyRuntimeEnvironment(where, options); + + const findOptions: { + where: QueryWhere; + include: RuntimeProjectInclude[]; + distinct: true; + order: string[][]; + transaction?: Transaction; + limit?: number; + offset?: number; + } = { + where, + include, + distinct: true, + order: + normalizedFilter.field && normalizedFilter.sort + ? [[normalizedFilter.field, normalizedFilter.sort]] + : [['createdAt', 'desc']], + }; + + if (options.transaction) { + findOptions.transaction = options.transaction; + } + + if (!options.countOnly && limit) { + findOptions.limit = limit; + } + + if (!options.countOnly && offset) { + findOptions.offset = offset; + } + + const { rows, count } = await this.MODEL.findAndCountAll(findOptions); + + return { + rows: options.countOnly ? [] : rows, + count, + }; + } +} + +export default Project_ui_control_settingsDBApi; diff --git a/backend/src/db/api/projects.js b/backend/src/db/api/projects.js deleted file mode 100644 index c9eb18c..0000000 --- a/backend/src/db/api/projects.js +++ /dev/null @@ -1,237 +0,0 @@ -const GenericDBApi = require('./base.api'); -const db = require('../models'); -const Utils = require('../utils'); -const { getRuntimeProjectSlug } = require('./runtime-context'); - -const Sequelize = db.Sequelize; -const Op = Sequelize.Op; - -class ProjectsDBApi extends GenericDBApi { - static get MODEL() { - return db.projects; - } - - static get TABLE_NAME() { - return 'projects'; - } - - static get SEARCHABLE_FIELDS() { - return [ - 'name', - 'slug', - 'description', - 'logo_url', - 'favicon_url', - 'og_image_url', - 'production_presentation_visibility', - ]; - } - - static get RANGE_FIELDS() { - return []; - } - - static get ENUM_FIELDS() { - return []; - } - - static get CSV_FIELDS() { - return ['id', 'name', 'slug', 'description', 'logo_url', 'createdAt']; - } - - static get AUTOCOMPLETE_FIELD() { - return 'name'; - } - - static get ASSOCIATIONS() { - return []; - } - - static getFieldMapping(data) { - // Use undefined for missing fields so they're skipped during update - // Only include fields that are explicitly provided in data - // Note: transition_settings moved to project_transition_settings table - return { - id: data.id || undefined, - name: 'name' in data ? data.name || null : undefined, - slug: 'slug' in data ? data.slug || null : undefined, - description: 'description' in data ? data.description || null : undefined, - logo_url: 'logo_url' in data ? data.logo_url || null : undefined, - favicon_url: 'favicon_url' in data ? data.favicon_url || null : undefined, - og_image_url: - 'og_image_url' in data ? data.og_image_url || null : undefined, - design_width: 'design_width' in data ? data.design_width : undefined, - design_height: 'design_height' in data ? data.design_height : undefined, - production_presentation_visibility: - 'production_presentation_visibility' in data - ? data.production_presentation_visibility || 'public' - : undefined, - }; - } - - static get DEFAULT_INCLUDES() { - return []; - } - - static get ALL_INCLUDES() { - return [ - { association: 'project_memberships_project' }, - { association: 'production_presentation_access_project' }, - { association: 'assets_project' }, - { association: 'presigned_url_requests_project' }, - { association: 'tour_pages_project' }, - { association: 'project_audio_tracks_project' }, - { association: 'publish_events_project' }, - { association: 'pwa_caches_project' }, - { association: 'access_logs_project' }, - { association: 'project_ui_control_settings_project' }, - ]; - } - - static async findBy(where, options = {}) { - const transaction = options.transaction; - const runtimeProjectSlug = getRuntimeProjectSlug(options); - const queryWhere = { ...where }; - - // Runtime access: filter by project slug - // Skip if finding by ID (unambiguous lookup) - if (runtimeProjectSlug && !where.id) { - queryWhere.slug = runtimeProjectSlug; - } - - const include = - options.include !== undefined ? options.include : this.DEFAULT_INCLUDES; - - const record = await this.MODEL.findOne({ - where: queryWhere, - transaction, - include, - }); - - if (!record) return null; - return record.get({ plain: true }); - } - - /** - * Create a new project and auto-snapshot global element defaults - */ - static async create(options) { - const { transaction } = options; - - // Create the project using parent's create - const project = await super.create(options); - - // Auto-snapshot global element defaults to the new project - // Errors propagate to service layer → transaction rollback → proper error to client - const Project_element_defaultsDBApi = require('./project_element_defaults'); - await Project_element_defaultsDBApi.snapshotGlobalDefaults(project.id, { - ...options, - transaction, - }); - - return project; - } - - static async findAll(filter = {}, options = {}) { - filter = filter || {}; - const limit = filter.limit || 0; - const currentPage = Number(filter.page) || 0; - const offset = Math.max(currentPage - 1, 0) * limit; - - let where = {}; - let include = []; - - if (filter.id) { - if (!Utils.isValidUuid(filter.id)) { - return { rows: [], count: 0 }; - } - where.id = filter.id; - } - - for (const field of this.SEARCHABLE_FIELDS) { - if (filter[field]) { - where[Op.and] = Utils.ilike(this.TABLE_NAME, field, filter[field]); - } - } - - for (const field of this.RANGE_FIELDS) { - const rangeKey = `${field}Range`; - if (filter[rangeKey]) { - const [start, end] = filter[rangeKey]; - if (start !== undefined && start !== null && start !== '') { - where[field] = { ...where[field], [Op.gte]: start }; - } - if (end !== undefined && end !== null && end !== '') { - where[field] = { ...where[field], [Op.lte]: end }; - } - } - } - - for (const field of this.ENUM_FIELDS) { - if (filter[field] !== undefined) { - where[field] = filter[field]; - } - } - - if (filter.active !== undefined) { - where.active = filter.active === true || filter.active === 'true'; - } - - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange; - if (start !== undefined && start !== null && start !== '') { - where.createdAt = { ...where.createdAt, [Op.gte]: start }; - } - if (end !== undefined && end !== null && end !== '') { - where.createdAt = { ...where.createdAt, [Op.lte]: end }; - } - } - - // Runtime access: filter by project slug - const runtimeProjectSlug = getRuntimeProjectSlug(options); - - if (runtimeProjectSlug) { - where.slug = runtimeProjectSlug; - } - - try { - if (options.countOnly) { - const count = await this.MODEL.count({ - where, - include, - distinct: true, - transaction: options.transaction, - }); - - return { - rows: [], - count, - }; - } - - const queryOptions = { - where, - include, - distinct: true, - order: - filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], - transaction: options.transaction, - limit: limit ? Number(limit) : undefined, - offset: offset ? Number(offset) : undefined, - }; - - const { rows, count } = await this.MODEL.findAndCountAll(queryOptions); - return { - rows, - count, - }; - } catch (error) { - console.error('Error executing query:', error); - throw error; - } - } -} - -module.exports = ProjectsDBApi; diff --git a/backend/src/db/api/projects.ts b/backend/src/db/api/projects.ts new file mode 100644 index 0000000..162a465 --- /dev/null +++ b/backend/src/db/api/projects.ts @@ -0,0 +1,351 @@ +import GenericDBApi from './base.api.ts'; +import Project_element_defaultsDBApi from './project_element_defaults.ts'; +import { getRuntimeProjectSlug } from './runtime-context.ts'; +import db from '../models/index.ts'; +import Utils from '../utils.ts'; +import type { + CreateOptions, + DbAssociationConfig, + DbFindAllOptions, + DbFindByOptions, + PaginatedResult, + ProjectData, + ProjectFieldMapping, + ProjectFindAllOptions, + ProjectListFilter, + ProjectModelApi, + ProjectElementDefaultsOptions, + ProjectRangeFilter, + ProjectRecord, + ProjectsDbApi, + QueryWhere, +} from '../../types/index.ts'; + +const { Op } = db.Sequelize; + +function isDbFindAllOptions( + value: ProjectListFilter | DbFindAllOptions, +): value is DbFindAllOptions { + return Boolean(value) && typeof value === 'object' && 'filter' in value; +} + +function isProjectListFilter(value: unknown): value is ProjectListFilter { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function isQueryWhere(value: unknown): value is QueryWhere { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function isRangeFilter(value: unknown): value is ProjectRangeFilter { + return Array.isArray(value) && value.length === 2; +} + +function normalizeFilter( + filter?: ProjectListFilter | DbFindAllOptions, +): ProjectListFilter { + if (!filter) return {}; + if (!isDbFindAllOptions(filter)) return filter; + return isProjectListFilter(filter.filter) ? filter.filter : {}; +} + +function getFilterString(filter: ProjectListFilter, field: string): string | null { + const value = filter[field]; + return typeof value === 'string' ? value : null; +} + +function addRangeBoundary( + where: QueryWhere, + field: string, + operator: symbol, + value: ProjectRangeFilter[number], +): void { + if (value === undefined || value === null || value === '') return; + + const current = isQueryWhere(where[field]) ? where[field] : {}; + where[field] = { + ...current, + [operator]: value, + }; +} + +function addRangeFilter(where: QueryWhere, field: string, range: unknown): void { + if (!isRangeFilter(range)) return; + + const [start, end] = range; + addRangeBoundary(where, field, Op.gte, start); + addRangeBoundary(where, field, Op.lte, end); +} + +function getDefinedProjectFields(data: ProjectData): ProjectFieldMapping { + return { + id: data.id || undefined, + name: 'name' in data ? data.name || null : undefined, + slug: 'slug' in data ? data.slug || null : undefined, + description: 'description' in data ? data.description || null : undefined, + logo_url: 'logo_url' in data ? data.logo_url || null : undefined, + favicon_url: 'favicon_url' in data ? data.favicon_url || null : undefined, + og_image_url: 'og_image_url' in data ? data.og_image_url || null : undefined, + design_width: 'design_width' in data ? data.design_width : undefined, + design_height: 'design_height' in data ? data.design_height : undefined, + production_presentation_visibility: + 'production_presentation_visibility' in data + ? data.production_presentation_visibility || 'public' + : undefined, + }; +} + +class ProjectsDBApi extends GenericDBApi { + declare static update: ProjectsDbApi['update']; + declare static partialUpdate: ProjectsDbApi['partialUpdate']; + declare static deleteByIds: ProjectsDbApi['deleteByIds']; + declare static remove: ProjectsDbApi['remove']; + declare static findAllAutocomplete: ProjectsDbApi['findAllAutocomplete']; + + static override get MODEL(): ProjectModelApi { + return db.projects; + } + + static override get TABLE_NAME(): string { + return 'projects'; + } + + static override get SEARCHABLE_FIELDS(): string[] { + return [ + 'name', + 'slug', + 'description', + 'logo_url', + 'favicon_url', + 'og_image_url', + 'production_presentation_visibility', + ]; + } + + static override get RANGE_FIELDS(): string[] { + return []; + } + + static override get ENUM_FIELDS(): string[] { + return []; + } + + static override get CSV_FIELDS(): string[] { + return ['id', 'name', 'slug', 'description', 'logo_url', 'createdAt']; + } + + static override get AUTOCOMPLETE_FIELD(): string { + return 'name'; + } + + static override get ASSOCIATIONS(): DbAssociationConfig[] { + return []; + } + + static override getFieldMapping(data: ProjectData): ProjectFieldMapping { + return getDefinedProjectFields(data); + } + + static get DEFAULT_INCLUDES(): unknown[] { + return []; + } + + static get ALL_INCLUDES(): unknown[] { + return [ + { association: 'project_memberships_project' }, + { association: 'production_presentation_access_project' }, + { association: 'assets_project' }, + { association: 'presigned_url_requests_project' }, + { association: 'tour_pages_project' }, + { association: 'project_audio_tracks_project' }, + { association: 'publish_events_project' }, + { association: 'pwa_caches_project' }, + { association: 'access_logs_project' }, + { association: 'project_ui_control_settings_project' }, + ]; + } + + static override async findBy( + where: { id: string }, + options?: ProjectFindAllOptions, + ): Promise; + static override async findBy( + options: DbFindByOptions, + ): Promise; + static override async findBy( + whereOrOptions: { id: string } | DbFindByOptions, + options: ProjectFindAllOptions = {}, + ): Promise { + const sourceWhere = + 'where' in whereOrOptions ? whereOrOptions.where : whereOrOptions; + const queryWhere: QueryWhere = isQueryWhere(sourceWhere) + ? { ...sourceWhere } + : {}; + const runtimeProjectSlug = getRuntimeProjectSlug(options); + + if (runtimeProjectSlug && !queryWhere.id) { + queryWhere.slug = runtimeProjectSlug; + } + + const include = + 'where' in whereOrOptions && whereOrOptions.include !== undefined + ? whereOrOptions.include + : options.include !== undefined + ? options.include + : this.DEFAULT_INCLUDES; + const findOptions: { + where: QueryWhere; + transaction?: ProjectFindAllOptions['transaction']; + include: unknown[]; + } = { + where: queryWhere, + include, + }; + + if ('where' in whereOrOptions && whereOrOptions.transaction) { + findOptions.transaction = whereOrOptions.transaction; + } else if (options.transaction) { + findOptions.transaction = options.transaction; + } + + const record = await this.MODEL.findOne(findOptions); + return record ? record.get({ plain: true }) : null; + } + + static override async create( + options: CreateOptions, + ): Promise { + const { data, currentUser = { id: null }, transaction } = options; + const record = await this.MODEL.create( + { + ...this.getFieldMapping(data), + importHash: null, + createdById: currentUser?.id ?? null, + updatedById: currentUser?.id ?? null, + }, + { transaction }, + ); + const project = record.get({ plain: true }); + + const snapshotOptions: ProjectElementDefaultsOptions = {}; + if (options.currentUser !== undefined) { + snapshotOptions.currentUser = options.currentUser; + } + if (options.runtimeContext !== undefined) { + snapshotOptions.runtimeContext = options.runtimeContext; + } + if (transaction) { + snapshotOptions.transaction = transaction; + } + + await Project_element_defaultsDBApi.snapshotGlobalDefaults( + project.id, + snapshotOptions, + ); + + return project; + } + + static override async findAll( + filter?: ProjectListFilter, + options?: ProjectFindAllOptions, + ): Promise>; + static override async findAll( + options: DbFindAllOptions, + ): Promise>; + static override async findAll( + filter?: ProjectListFilter | DbFindAllOptions, + options: ProjectFindAllOptions = {}, + ): Promise> { + const normalizedFilter = normalizeFilter(filter); + const limit = Number(normalizedFilter.limit) || 0; + const currentPage = Number(normalizedFilter.page) || 0; + const offset = Math.max(currentPage - 1, 0) * limit; + const where: QueryWhere = {}; + const include: unknown[] = []; + + const id = getFilterString(normalizedFilter, 'id'); + if (id) { + if (!Utils.isValidUuid(id)) { + return { rows: [], count: 0 }; + } + where.id = id; + } + + for (const field of this.SEARCHABLE_FIELDS) { + const value = getFilterString(normalizedFilter, field); + if (value) { + where[Op.and] = Utils.ilike(this.TABLE_NAME, field, value); + } + } + + for (const field of this.RANGE_FIELDS) { + addRangeFilter(where, field, normalizedFilter[`${field}Range`]); + } + + for (const field of this.ENUM_FIELDS) { + if (normalizedFilter[field] !== undefined) { + where[field] = normalizedFilter[field]; + } + } + + const active = normalizedFilter.active; + if (active !== undefined) { + where.active = active === 'true'; + } + + addRangeFilter(where, 'createdAt', normalizedFilter.createdAtRange); + + const runtimeProjectSlug = getRuntimeProjectSlug(options); + if (runtimeProjectSlug) { + where.slug = runtimeProjectSlug; + } + + if (options.countOnly) { + const count = await this.MODEL.count({ + where, + include, + distinct: true, + transaction: options.transaction, + }); + return { + rows: [], + count, + }; + } + + const queryOptions: { + where: QueryWhere; + include: unknown[]; + distinct: true; + order: string[][]; + transaction?: ProjectFindAllOptions['transaction']; + limit?: number; + offset?: number; + } = { + where, + include, + distinct: true, + order: + normalizedFilter.field && normalizedFilter.sort + ? [[normalizedFilter.field, normalizedFilter.sort]] + : [['createdAt', 'desc']], + }; + + if (options.transaction) { + queryOptions.transaction = options.transaction; + } + + if (limit) { + queryOptions.limit = limit; + } + + if (offset) { + queryOptions.offset = offset; + } + + return this.MODEL.findAndCountAll(queryOptions); + } +} + +export default ProjectsDBApi; diff --git a/backend/src/db/api/publish_events.js b/backend/src/db/api/publish_events.ts similarity index 64% rename from backend/src/db/api/publish_events.js rename to backend/src/db/api/publish_events.ts index f01e4ea..12e0602 100644 --- a/backend/src/db/api/publish_events.js +++ b/backend/src/db/api/publish_events.ts @@ -1,20 +1,26 @@ -const GenericDBApi = require('./base.api'); -const db = require('../models'); +import GenericDBApi from './base.api.ts'; +import db from '../models/index.ts'; +import type { + PublishEventAssociationConfig, + PublishEventData, + PublishEventFieldMapping, + PublishEventRelationFilterConfig, +} from '../../types/index.ts'; class Publish_eventsDBApi extends GenericDBApi { - static get MODEL() { + static override get MODEL(): unknown { return db.publish_events; } - static get TABLE_NAME() { + static override get TABLE_NAME(): string { return 'publish_events'; } - static get SEARCHABLE_FIELDS() { + static override get SEARCHABLE_FIELDS(): string[] { return ['title', 'description', 'error_message']; } - static get RANGE_FIELDS() { + static override get RANGE_FIELDS(): string[] { return [ 'started_at', 'finished_at', @@ -24,15 +30,15 @@ class Publish_eventsDBApi extends GenericDBApi { ]; } - static get ENUM_FIELDS() { + static override get ENUM_FIELDS(): string[] { return ['from_environment', 'to_environment', 'status']; } - static get UUID_FIELDS() { + static override get UUID_FIELDS(): string[] { return ['projectId']; } - static get CSV_FIELDS() { + static override get CSV_FIELDS(): string[] { return [ 'id', 'title', @@ -45,29 +51,29 @@ class Publish_eventsDBApi extends GenericDBApi { ]; } - static get AUTOCOMPLETE_FIELD() { + static override get AUTOCOMPLETE_FIELD(): string { return 'status'; } - static get ASSOCIATIONS() { + static override get ASSOCIATIONS(): PublishEventAssociationConfig[] { return [ { field: 'project', setter: 'setProject', isArray: false }, { field: 'user', setter: 'setUser', isArray: false }, ]; } - static get FIND_BY_INCLUDES() { + static override get FIND_BY_INCLUDES(): unknown[] { return [{ association: 'project' }, { association: 'user' }]; } - static get FIND_ALL_INCLUDES() { + static override get FIND_ALL_INCLUDES(): unknown[] { return [ { model: db.projects, as: 'project', required: false }, { model: db.users, as: 'user', required: false }, ]; } - static get RELATION_FILTERS() { + static override get RELATION_FILTERS(): PublishEventRelationFilterConfig[] { return [ { filterKey: 'project', @@ -84,7 +90,7 @@ class Publish_eventsDBApi extends GenericDBApi { ]; } - static getFieldMapping(data) { + static override getFieldMapping(data: PublishEventData): PublishEventFieldMapping { return { id: data.id || undefined, title: data.title || null, @@ -102,4 +108,4 @@ class Publish_eventsDBApi extends GenericDBApi { } } -module.exports = Publish_eventsDBApi; +export default Publish_eventsDBApi; diff --git a/backend/src/db/api/pwa_caches.js b/backend/src/db/api/pwa_caches.ts similarity index 54% rename from backend/src/db/api/pwa_caches.js rename to backend/src/db/api/pwa_caches.ts index c953c73..437782a 100644 --- a/backend/src/db/api/pwa_caches.js +++ b/backend/src/db/api/pwa_caches.ts @@ -1,28 +1,34 @@ -const GenericDBApi = require('./base.api'); -const db = require('../models'); +import GenericDBApi from './base.api.ts'; +import db from '../models/index.ts'; +import type { + PwaCacheAssociationConfig, + PwaCacheData, + PwaCacheFieldMapping, + PwaCacheRelationFilterConfig, +} from '../../types/index.ts'; class Pwa_cachesDBApi extends GenericDBApi { - static get MODEL() { + static override get MODEL(): unknown { return db.pwa_caches; } - static get TABLE_NAME() { + static override get TABLE_NAME(): string { return 'pwa_caches'; } - static get SEARCHABLE_FIELDS() { + static override get SEARCHABLE_FIELDS(): string[] { return ['cache_version', 'manifest_json', 'asset_list_json']; } - static get RANGE_FIELDS() { + static override get RANGE_FIELDS(): string[] { return ['generated_at']; } - static get ENUM_FIELDS() { + static override get ENUM_FIELDS(): string[] { return ['environment', 'is_active']; } - static get CSV_FIELDS() { + static override get CSV_FIELDS(): string[] { return [ 'id', 'environment', @@ -33,23 +39,23 @@ class Pwa_cachesDBApi extends GenericDBApi { ]; } - static get AUTOCOMPLETE_FIELD() { + static override get AUTOCOMPLETE_FIELD(): string { return 'cache_version'; } - static get ASSOCIATIONS() { + static override get ASSOCIATIONS(): PwaCacheAssociationConfig[] { return [{ field: 'project', setter: 'setProject', isArray: false }]; } - static get FIND_BY_INCLUDES() { + static override get FIND_BY_INCLUDES(): unknown[] { return [{ association: 'project' }]; } - static get FIND_ALL_INCLUDES() { + static override get FIND_ALL_INCLUDES(): unknown[] { return [{ model: db.projects, as: 'project', required: false }]; } - static get RELATION_FILTERS() { + static override get RELATION_FILTERS(): PwaCacheRelationFilterConfig[] { return [ { filterKey: 'project', @@ -60,7 +66,7 @@ class Pwa_cachesDBApi extends GenericDBApi { ]; } - static getFieldMapping(data) { + static override getFieldMapping(data: PwaCacheData): PwaCacheFieldMapping { return { id: data.id || undefined, environment: data.environment || null, @@ -73,4 +79,4 @@ class Pwa_cachesDBApi extends GenericDBApi { } } -module.exports = Pwa_cachesDBApi; +export default Pwa_cachesDBApi; diff --git a/backend/src/db/api/roles.js b/backend/src/db/api/roles.js deleted file mode 100644 index 6b99d16..0000000 --- a/backend/src/db/api/roles.js +++ /dev/null @@ -1,71 +0,0 @@ -const GenericDBApi = require('./base.api'); -const db = require('../models'); - -class RolesDBApi extends GenericDBApi { - static get MODEL() { - return db.roles; - } - - static get TABLE_NAME() { - return 'roles'; - } - - static get SEARCHABLE_FIELDS() { - return ['name', 'role_customization']; - } - - static get RANGE_FIELDS() { - return []; - } - - static get ENUM_FIELDS() { - return []; - } - - static get CSV_FIELDS() { - return ['id', 'name', 'role_customization', 'createdAt']; - } - - static get AUTOCOMPLETE_FIELD() { - return 'name'; - } - - static get ASSOCIATIONS() { - return [{ field: 'permissions', setter: 'setPermissions', isArray: true }]; - } - - static get FIND_BY_INCLUDES() { - return [{ association: 'users_app_role' }, { association: 'permissions' }]; - } - - static get FIND_ALL_INCLUDES() { - return [ - { - model: db.permissions, - as: 'permissions', - required: false, - }, - ]; - } - - static get RELATION_FILTERS() { - return [ - { - filterKey: 'permissions', - model: db.permissions, - as: 'permissions_filter', - searchField: 'name', - }, - ]; - } - - static getFieldMapping(data) { - return { - id: data.id || undefined, - name: data.name || null, - role_customization: data.role_customization || null, - }; - } -} - -module.exports = RolesDBApi; diff --git a/backend/src/db/api/roles.ts b/backend/src/db/api/roles.ts new file mode 100644 index 0000000..c2ebfae --- /dev/null +++ b/backend/src/db/api/roles.ts @@ -0,0 +1,84 @@ +import GenericDBApi from './base.api.ts'; +import db from '../models/index.ts'; +import type { + RoleAssociationConfig, + RoleData, + RoleFieldMapping, + RoleRelationFilterConfig, + RolesDbApi, +} from '../../types/index.ts'; + +class RolesDBApi extends GenericDBApi { + declare static create: RolesDbApi['create']; + declare static findBy: RolesDbApi['findBy']; + declare static update: RolesDbApi['update']; + declare static deleteByIds: RolesDbApi['deleteByIds']; + declare static remove: RolesDbApi['remove']; + + static override get MODEL(): unknown { + return db.roles; + } + + static override get TABLE_NAME(): string { + return 'roles'; + } + + static override get SEARCHABLE_FIELDS(): string[] { + return ['name', 'role_customization']; + } + + static override get RANGE_FIELDS(): string[] { + return []; + } + + static override get ENUM_FIELDS(): string[] { + return []; + } + + static override get CSV_FIELDS(): string[] { + return ['id', 'name', 'role_customization', 'createdAt']; + } + + static override get AUTOCOMPLETE_FIELD(): string { + return 'name'; + } + + static override get ASSOCIATIONS(): RoleAssociationConfig[] { + return [{ field: 'permissions', setter: 'setPermissions', isArray: true }]; + } + + static override get FIND_BY_INCLUDES(): unknown[] { + return [{ association: 'users_app_role' }, { association: 'permissions' }]; + } + + static override get FIND_ALL_INCLUDES(): unknown[] { + return [ + { + model: db.permissions, + as: 'permissions', + required: false, + }, + ]; + } + + static override get RELATION_FILTERS(): RoleRelationFilterConfig[] { + return [ + { + filterKey: 'permissions', + model: db.permissions, + as: 'permissions_filter', + searchField: 'name', + }, + ]; + } + + static override getFieldMapping(data: RoleData): RoleFieldMapping { + return { + id: data.id || undefined, + name: data.name || null, + role_customization: data.role_customization || null, + }; + } +} + +export default RolesDBApi; diff --git a/backend/src/db/api/runtime-context.js b/backend/src/db/api/runtime-context.js deleted file mode 100644 index 38e24a6..0000000 --- a/backend/src/db/api/runtime-context.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Runtime Context Helpers - * For route-based environment access via X-Runtime-Environment header - */ - -function getRuntimeContext(options = {}) { - return (options || {}).runtimeContext || null; -} - -function getRuntimeEnvironment(options = {}) { - const runtimeContext = getRuntimeContext(options); - if (!runtimeContext) return null; - - // Read from header (route-based mode) - // SECURITY: Only allow 'production' and 'stage' from header - // to prevent unauthorized access to dev data - if (runtimeContext.headerEnvironment === 'production') return 'production'; - if (runtimeContext.headerEnvironment === 'stage') return 'stage'; - - return null; -} - -function getRuntimeProjectSlug(options = {}) { - const runtimeContext = getRuntimeContext(options); - return runtimeContext?.headerProjectSlug || null; -} - -function applyRuntimeEnvironment(where = {}, options = {}) { - const environment = getRuntimeEnvironment(options); - if (!environment) return where; - - return { - ...where, - environment, - }; -} - -function applyRuntimeProjectFilter(projectInclude = {}, options = {}) { - const projectSlug = getRuntimeProjectSlug(options); - if (!projectSlug) return projectInclude; - - return { - ...projectInclude, - required: true, - where: { - ...(projectInclude.where || {}), - slug: projectSlug, - }, - }; -} - -module.exports = { - applyRuntimeEnvironment, - applyRuntimeProjectFilter, - getRuntimeEnvironment, - getRuntimeProjectSlug, -}; diff --git a/backend/src/db/api/runtime-context.ts b/backend/src/db/api/runtime-context.ts new file mode 100644 index 0000000..e39424f --- /dev/null +++ b/backend/src/db/api/runtime-context.ts @@ -0,0 +1,75 @@ +import type { + QueryWhere, + RuntimeContext, + RuntimeFilterOptions, + RuntimeProjectInclude, + RuntimeReadableEnvironment, +} from '../../types/index.ts'; + +function isQueryWhere(value: unknown): value is QueryWhere { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function getRuntimeContext( + options: RuntimeFilterOptions = {}, +): RuntimeContext | null { + return options.runtimeContext ?? null; +} + +function getRuntimeEnvironment( + options: RuntimeFilterOptions = {}, +): RuntimeReadableEnvironment | null { + const runtimeContext = getRuntimeContext(options); + if (!runtimeContext) return null; + + if (runtimeContext.headerEnvironment === 'production') return 'production'; + if (runtimeContext.headerEnvironment === 'stage') return 'stage'; + + return null; +} + +function getRuntimeProjectSlug(options: RuntimeFilterOptions = {}): string | null { + const runtimeContext = getRuntimeContext(options); + return runtimeContext?.headerProjectSlug ?? null; +} + +function applyRuntimeEnvironment( + where: QueryWhere = {}, + options: RuntimeFilterOptions = {}, +): QueryWhere { + const environment = getRuntimeEnvironment(options); + if (!environment) return where; + + return { + ...where, + environment, + }; +} + +function applyRuntimeProjectFilter( + projectInclude: RuntimeProjectInclude = {}, + options: RuntimeFilterOptions = {}, +): RuntimeProjectInclude { + const projectSlug = getRuntimeProjectSlug(options); + if (!projectSlug) return projectInclude; + + const existingWhere = isQueryWhere(projectInclude.where) + ? projectInclude.where + : {}; + + return { + ...projectInclude, + required: true, + where: { + ...existingWhere, + slug: projectSlug, + }, + }; +} + +export { + applyRuntimeEnvironment, + applyRuntimeProjectFilter, + getRuntimeEnvironment, + getRuntimeProjectSlug, +}; diff --git a/backend/src/db/api/tour_pages.js b/backend/src/db/api/tour_pages.js deleted file mode 100644 index e7824d8..0000000 --- a/backend/src/db/api/tour_pages.js +++ /dev/null @@ -1,289 +0,0 @@ -const GenericDBApi = require('./base.api'); -const db = require('../models'); -const Utils = require('../utils'); -const { - applyRuntimeEnvironment, - applyRuntimeProjectFilter, -} = require('./runtime-context'); - -const Sequelize = db.Sequelize; -const Op = Sequelize.Op; - -class Tour_pagesDBApi extends GenericDBApi { - static get MODEL() { - return db.tour_pages; - } - - static get TABLE_NAME() { - return 'tour_pages'; - } - - static get SEARCHABLE_FIELDS() { - return [ - 'source_key', - 'name', - 'slug', - 'background_image_url', - 'background_video_url', - 'background_embed_url', - 'background_audio_url', - 'ui_schema_json', - ]; - } - - static get RANGE_FIELDS() { - return ['sort_order']; - } - - static get ENUM_FIELDS() { - return ['environment', 'background_loop', 'requires_auth']; - } - - static get UUID_FIELDS() { - return ['projectId']; - } - - static get CSV_FIELDS() { - return [ - 'id', - 'environment', - 'source_key', - 'name', - 'slug', - 'sort_order', - 'createdAt', - ]; - } - - static get AUTOCOMPLETE_FIELD() { - return 'name'; - } - - static get ASSOCIATIONS() { - return [{ field: 'project', setter: 'setProject', isArray: false }]; - } - - static getFieldMapping(data) { - return { - id: data.id || undefined, - environment: data.environment || null, - source_key: data.source_key || null, - name: data.name || null, - slug: data.slug || null, - sort_order: data.sort_order || null, - background_image_url: data.background_image_url || null, - background_video_url: data.background_video_url || null, - background_embed_url: data.background_embed_url || null, - background_audio_url: data.background_audio_url || null, - background_audio_autoplay: - data.background_audio_autoplay !== undefined - ? data.background_audio_autoplay - : true, - background_audio_loop: - data.background_audio_loop !== undefined - ? data.background_audio_loop - : true, - background_audio_start_time: - data.background_audio_start_time !== undefined - ? data.background_audio_start_time - : null, - background_audio_end_time: - data.background_audio_end_time !== undefined - ? data.background_audio_end_time - : null, - background_loop: data.background_loop || false, - background_video_autoplay: - data.background_video_autoplay !== undefined - ? data.background_video_autoplay - : true, - background_video_loop: - data.background_video_loop !== undefined - ? data.background_video_loop - : true, - background_video_muted: - data.background_video_muted !== undefined - ? data.background_video_muted - : true, - background_video_start_time: - data.background_video_start_time !== undefined - ? data.background_video_start_time - : null, - background_video_end_time: - data.background_video_end_time !== undefined - ? data.background_video_end_time - : null, - design_width: data.design_width !== undefined ? data.design_width : null, - design_height: - data.design_height !== undefined ? data.design_height : null, - requires_auth: data.requires_auth || false, - ui_schema_json: data.ui_schema_json || null, - global_ui_controls_settings_json: - data.global_ui_controls_settings_json !== undefined - ? data.global_ui_controls_settings_json - : null, - }; - } - - static async create(options) { - const { data, currentUser = { id: null }, transaction } = options; - const projectId = data.project || data.projectId || null; - - const record = await this.MODEL.create( - { - ...this.getFieldMapping(data), - projectId, - importHash: data.importHash || null, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { transaction }, - ); - - await record.setProject(projectId, { transaction }); - - return record; - } - - static async findBy(where, options = {}) { - const transaction = options.transaction; - const queryWhere = applyRuntimeEnvironment({ ...where }, options); - const projectInclude = applyRuntimeProjectFilter( - { - model: db.projects, - as: 'project', - }, - options, - ); - - const record = await this.MODEL.findOne({ - where: queryWhere, - transaction, - include: [projectInclude], - }); - - if (!record) return null; - return record.get({ plain: true }); - } - - static async findAll(filter = {}, options = {}) { - filter = filter || {}; - const limit = filter.limit || 0; - const currentPage = Number(filter.page) || 0; - const offset = Math.max(currentPage - 1, 0) * limit; - - let where = {}; - - const terms = filter.project ? filter.project.split('|') : []; - const validUuids = Utils.filterValidUuids(terms); - - let include = [ - { - model: db.projects, - as: 'project', - where: filter.project - ? { - [Op.or]: [ - ...(validUuids.length > 0 - ? [{ id: { [Op.in]: validUuids } }] - : []), - { - name: { - [Op.or]: terms.map((term) => ({ [Op.iLike]: `%${term}%` })), - }, - }, - ], - } - : {}, - }, - ]; - - include[0] = applyRuntimeProjectFilter(include[0], options); - - if (filter.id) { - if (!Utils.isValidUuid(filter.id)) { - return { rows: [], count: 0 }; - } - where.id = filter.id; - } - - for (const field of this.SEARCHABLE_FIELDS) { - if (filter[field]) { - where[Op.and] = Utils.ilike(this.TABLE_NAME, field, filter[field]); - } - } - - for (const field of this.RANGE_FIELDS) { - const rangeKey = `${field}Range`; - if (filter[rangeKey]) { - const [start, end] = filter[rangeKey]; - if (start !== undefined && start !== null && start !== '') { - where[field] = { ...where[field], [Op.gte]: start }; - } - if (end !== undefined && end !== null && end !== '') { - where[field] = { ...where[field], [Op.lte]: end }; - } - } - } - - for (const field of this.ENUM_FIELDS) { - if (filter[field] !== undefined) { - where[field] = filter[field]; - } - } - - // Validate and filter by UUID fields (e.g., projectId) - for (const field of this.UUID_FIELDS) { - if (filter[field] !== undefined) { - if (!Utils.isValidUuid(filter[field])) { - return { rows: [], count: 0 }; - } - where[field] = filter[field]; - } - } - - if (filter.active !== undefined) { - where.active = filter.active === true || filter.active === 'true'; - } - - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange; - if (start !== undefined && start !== null && start !== '') { - where.createdAt = { ...where.createdAt, [Op.gte]: start }; - } - if (end !== undefined && end !== null && end !== '') { - where.createdAt = { ...where.createdAt, [Op.lte]: end }; - } - } - - where = applyRuntimeEnvironment(where, options); - - const queryOptions = { - where, - include, - distinct: true, - order: - filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], - transaction: options.transaction, - }; - - if (!options.countOnly) { - queryOptions.limit = limit ? Number(limit) : undefined; - queryOptions.offset = offset ? Number(offset) : undefined; - } - - try { - const { rows, count } = await this.MODEL.findAndCountAll(queryOptions); - return { - rows: options.countOnly ? [] : rows, - count, - }; - } catch (error) { - console.error('Error executing query:', error); - throw error; - } - } -} - -module.exports = Tour_pagesDBApi; diff --git a/backend/src/db/api/tour_pages.ts b/backend/src/db/api/tour_pages.ts new file mode 100644 index 0000000..9a803aa --- /dev/null +++ b/backend/src/db/api/tour_pages.ts @@ -0,0 +1,363 @@ +import GenericDBApi from './base.api.ts'; +import { + applyRuntimeEnvironment, + applyRuntimeProjectFilter, +} from './runtime-context.ts'; +import db from '../models/index.ts'; +import Utils from '../utils.ts'; +import type { + CreateOptions, + DbAssociationConfig, + DbFindAllOptions, + DbFindByOptions, + EntityRecord, + PaginatedResult, + QueryWhere, + RuntimeProjectInclude, + TourPageData, + TourPageFieldMapping, + TourPageFindAllOptions, + TourPageListQuery, + TourPageModelApi, + TourPageRangeFilter, + TourPageRecord, + TourPagesDbApi, +} from '../../types/index.ts'; + +const { Op } = db.Sequelize; + +function isDbFindAllOptions( + value: TourPageListQuery | DbFindAllOptions, +): value is DbFindAllOptions { + return Boolean(value) && typeof value === 'object' && 'filter' in value; +} + +function isTourPageListQuery(value: unknown): value is TourPageListQuery { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function isQueryWhere(value: unknown): value is QueryWhere { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function isRangeFilter(value: unknown): value is TourPageRangeFilter { + return Array.isArray(value) && value.length === 2; +} + +function normalizeFilter( + filter?: TourPageListQuery | DbFindAllOptions, +): TourPageListQuery { + if (!filter) return {}; + if (!isDbFindAllOptions(filter)) return filter; + return isTourPageListQuery(filter.filter) ? filter.filter : {}; +} + +function getFilterString(filter: TourPageListQuery, field: string): string | null { + const value = filter[field]; + return typeof value === 'string' ? value : null; +} + +function addRangeBoundary( + where: QueryWhere, + field: string, + operator: symbol, + value: TourPageRangeFilter[number], +): void { + if (value === undefined || value === null || value === '') return; + + const current = isQueryWhere(where[field]) ? where[field] : {}; + where[field] = { + ...current, + [operator]: value, + }; +} + +function addRangeFilter(where: QueryWhere, field: string, range: unknown): void { + if (!isRangeFilter(range)) return; + + const [start, end] = range; + addRangeBoundary(where, field, Op.gte, start); + addRangeBoundary(where, field, Op.lte, end); +} + +function getProjectId(data: TourPageData): string | null { + if (typeof data.projectId === 'string') return data.projectId; + if (typeof data.project === 'string') return data.project; + if (data.project && typeof data.project.id === 'string') return data.project.id; + return null; +} + +class Tour_pagesDBApi extends GenericDBApi { + declare static update: TourPagesDbApi['update']; + declare static deleteByIds: TourPagesDbApi['deleteByIds']; + declare static remove: TourPagesDbApi['remove']; + declare static findAllAutocomplete: TourPagesDbApi['findAllAutocomplete']; + + static override get MODEL(): TourPageModelApi { + return db.tour_pages; + } + + static override get TABLE_NAME(): string { + return 'tour_pages'; + } + + static override get SEARCHABLE_FIELDS(): string[] { + return [ + 'source_key', + 'name', + 'slug', + 'background_image_url', + 'background_video_url', + 'background_embed_url', + 'background_audio_url', + 'ui_schema_json', + ]; + } + + static override get RANGE_FIELDS(): string[] { + return ['sort_order']; + } + + static override get ENUM_FIELDS(): string[] { + return ['environment', 'background_loop', 'requires_auth']; + } + + static override get UUID_FIELDS(): string[] { + return ['projectId']; + } + + static override get CSV_FIELDS(): string[] { + return [ + 'id', + 'environment', + 'source_key', + 'name', + 'slug', + 'sort_order', + 'createdAt', + ]; + } + + static override get AUTOCOMPLETE_FIELD(): string { + return 'name'; + } + + static override get ASSOCIATIONS(): DbAssociationConfig[] { + return [{ field: 'project', setter: 'setProject', isArray: false }]; + } + + static override getFieldMapping(data: TourPageData): TourPageFieldMapping { + return { + id: data.id || undefined, + environment: data.environment || null, + source_key: data.source_key || null, + name: data.name || null, + slug: data.slug || null, + sort_order: data.sort_order || null, + background_image_url: data.background_image_url || null, + background_video_url: data.background_video_url || null, + background_embed_url: data.background_embed_url || null, + background_audio_url: data.background_audio_url || null, + background_audio_autoplay: data.background_audio_autoplay ?? true, + background_audio_loop: data.background_audio_loop ?? true, + background_audio_start_time: data.background_audio_start_time ?? null, + background_audio_end_time: data.background_audio_end_time ?? null, + background_loop: data.background_loop || false, + background_video_autoplay: data.background_video_autoplay ?? true, + background_video_loop: data.background_video_loop ?? true, + background_video_muted: data.background_video_muted ?? true, + background_video_start_time: data.background_video_start_time ?? null, + background_video_end_time: data.background_video_end_time ?? null, + design_width: data.design_width ?? null, + design_height: data.design_height ?? null, + requires_auth: data.requires_auth || false, + ui_schema_json: data.ui_schema_json || null, + global_ui_controls_settings_json: + data.global_ui_controls_settings_json ?? null, + }; + } + + static override async create( + options: CreateOptions, + ): Promise { + const { data, currentUser = { id: null }, transaction } = options; + const projectId = getProjectId(data); + const record = await this.MODEL.create( + { + ...this.getFieldMapping(data), + projectId, + importHash: data.importHash || null, + createdById: currentUser?.id ?? null, + updatedById: currentUser?.id ?? null, + }, + { transaction }, + ); + + await record.setProject(projectId, { transaction }); + + return record.get({ plain: true }); + } + + static override async findBy( + where: { id: string }, + options?: TourPageFindAllOptions, + ): Promise; + static override async findBy( + options: DbFindByOptions, + ): Promise; + static override async findBy( + whereOrOptions: { id: string } | DbFindByOptions, + options: TourPageFindAllOptions = {}, + ): Promise { + const sourceWhere = + 'where' in whereOrOptions ? whereOrOptions.where : whereOrOptions; + const where = isQueryWhere(sourceWhere) ? sourceWhere : {}; + const queryWhere = applyRuntimeEnvironment({ ...where }, options); + const projectInclude = applyRuntimeProjectFilter( + { model: db.projects, as: 'project' }, + options, + ); + const findOptions: { + where: QueryWhere; + transaction?: TourPageFindAllOptions['transaction']; + include: RuntimeProjectInclude[]; + } = { + where: queryWhere, + include: [projectInclude], + }; + + if ('where' in whereOrOptions && whereOrOptions.transaction) { + findOptions.transaction = whereOrOptions.transaction; + } else if (options.transaction) { + findOptions.transaction = options.transaction; + } + + const record = await this.MODEL.findOne(findOptions); + return record ? record.get({ plain: true }) : null; + } + + static override async findAll( + filter?: TourPageListQuery, + options?: TourPageFindAllOptions, + ): Promise>; + static override async findAll( + options: DbFindAllOptions, + ): Promise>; + static override async findAll( + filter?: TourPageListQuery | DbFindAllOptions, + options: TourPageFindAllOptions = {}, + ): Promise> { + const normalizedFilter = normalizeFilter(filter); + const limit = Number(normalizedFilter.limit) || 0; + const currentPage = Number(normalizedFilter.page) || 0; + const offset = Math.max(currentPage - 1, 0) * limit; + let where: QueryWhere = {}; + + const project = getFilterString(normalizedFilter, 'project'); + const terms = project ? project.split('|') : []; + const validUuids = Utils.filterValidUuids(terms); + const include: RuntimeProjectInclude[] = [ + { + model: db.projects, + as: 'project', + where: project + ? { + [Op.or]: [ + ...(validUuids.length > 0 + ? [{ id: { [Op.in]: validUuids } }] + : []), + { + name: { + [Op.or]: terms.map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + include[0] = applyRuntimeProjectFilter(include[0], options); + + const id = getFilterString(normalizedFilter, 'id'); + if (id) { + if (!Utils.isValidUuid(id)) { + return { rows: [], count: 0 }; + } + where.id = id; + } + + for (const field of this.SEARCHABLE_FIELDS) { + const value = getFilterString(normalizedFilter, field); + if (value) { + where[Op.and] = Utils.ilike(this.TABLE_NAME, field, value); + } + } + + for (const field of this.RANGE_FIELDS) { + addRangeFilter(where, field, normalizedFilter[`${field}Range`]); + } + + for (const field of this.ENUM_FIELDS) { + if (normalizedFilter[field] !== undefined) { + where[field] = normalizedFilter[field]; + } + } + + for (const field of this.UUID_FIELDS) { + const value = getFilterString(normalizedFilter, field); + if (value) { + if (!Utils.isValidUuid(value)) { + return { rows: [], count: 0 }; + } + where[field] = value; + } + } + + const active = normalizedFilter.active; + if (active !== undefined) { + where.active = active === 'true'; + } + + addRangeFilter(where, 'createdAt', normalizedFilter.createdAtRange); + where = applyRuntimeEnvironment(where, options); + + const queryOptions: { + where: QueryWhere; + include: RuntimeProjectInclude[]; + distinct: true; + order: string[][]; + transaction?: TourPageFindAllOptions['transaction']; + limit?: number; + offset?: number; + } = { + where, + include, + distinct: true, + order: + normalizedFilter.field && normalizedFilter.sort + ? [[normalizedFilter.field, normalizedFilter.sort]] + : [['createdAt', 'desc']], + }; + + if (options.transaction) { + queryOptions.transaction = options.transaction; + } + + if (!options.countOnly && limit) { + queryOptions.limit = limit; + } + + if (!options.countOnly && offset) { + queryOptions.offset = offset; + } + + const { rows, count } = await this.MODEL.findAndCountAll(queryOptions); + return { + rows: options.countOnly ? [] : rows, + count, + }; + } +} + +export default Tour_pagesDBApi; diff --git a/backend/src/db/api/users.js b/backend/src/db/api/users.js deleted file mode 100644 index 3c3c961..0000000 --- a/backend/src/db/api/users.js +++ /dev/null @@ -1,883 +0,0 @@ -const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); -const Utils = require('../utils'); - -const bcrypt = require('bcrypt'); -const config = require('../../config'); -const { logger } = require('../../utils/logger'); -const { - assertAutocompleteOptions, - assertCreateOptions, - assertDeleteByIdsOptions, - assertIdOptions, - assertUpdateOptions, -} = require('../../contracts/entity-options'); - -const Sequelize = db.Sequelize; -const Op = Sequelize.Op; - -module.exports = class UsersDBApi { - static get MODEL() { - return db.users; - } - - static get SORTABLE_FIELDS() { - return Object.keys(db.users.rawAttributes || {}); - } - - /** - * Default includes for findBy() - minimal set for single user lookup - * Only loads avatar and app_role with permissions (needed for RBAC) - */ - static get FIND_BY_INCLUDES() { - return [ - { association: 'avatar' }, - { - association: 'app_role', - include: [{ association: 'permissions' }], - }, - ]; - } - - /** - * Minimal includes for findAll() - only app_role for list display - * Excludes avatar, custom_permissions (rarely needed in list views) - */ - static get FIND_ALL_INCLUDES() { - return [ - { - model: db.roles, - as: 'app_role', - required: false, - }, - ]; - } - - /** - * Sensitive fields that should be excluded from query results - */ - static get SENSITIVE_FIELDS() { - return [ - 'password', - 'emailVerificationToken', - 'emailVerificationTokenExpiresAt', - 'passwordResetToken', - 'passwordResetTokenExpiresAt', - ]; - } - - static async create(options) { - assertCreateOptions(options, 'DBApi'); - - const { data, currentUser = { id: null }, transaction } = options; - const userData = data.data || data; - const password = - userData.password || crypto.randomBytes(20).toString('hex'); - - const users = await db.users.create( - { - id: userData.id || undefined, - - firstName: userData.firstName || null, - lastName: userData.lastName || null, - phoneNumber: userData.phoneNumber || null, - email: userData.email || null, - disabled: userData.disabled || false, - - password: bcrypt.hashSync(password, config.bcrypt.saltRounds), - emailVerified: userData.emailVerified || true, - - emailVerificationToken: userData.emailVerificationToken || null, - emailVerificationTokenExpiresAt: - userData.emailVerificationTokenExpiresAt || null, - passwordResetToken: userData.passwordResetToken || null, - passwordResetTokenExpiresAt: - userData.passwordResetTokenExpiresAt || null, - provider: userData.provider || config.providers.LOCAL, - importHash: userData.importHash || null, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { transaction }, - ); - - if (!userData.app_role) { - const role = await db.roles.findOne({ - where: { name: 'User' }, - }); - if (role) { - await users.setApp_role(role, { - transaction, - }); - } - } else { - await users.setApp_role(userData.app_role || null, { - transaction, - }); - } - - await users.setCustom_permissions(userData.custom_permissions || [], { - transaction, - }); - - await FileDBApi.replaceRelationFiles( - { - belongsTo: db.users.getTableName(), - belongsToColumn: 'avatar', - belongsToId: users.id, - }, - userData.avatar, - options, - ); - - return users; - } - - static async bulkImport(data, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - // Prepare data - wrapping individual data transformations in a map() method - const usersData = data.map((item, index) => ({ - id: item.id || undefined, - - firstName: item.firstName || null, - lastName: item.lastName || null, - phoneNumber: item.phoneNumber || null, - email: item.email || null, - disabled: item.disabled || false, - - password: bcrypt.hashSync( - item.password || crypto.randomBytes(20).toString('hex'), - config.bcrypt.saltRounds, - ), - emailVerified: item.emailVerified || false, - - emailVerificationToken: item.emailVerificationToken || null, - emailVerificationTokenExpiresAt: - item.emailVerificationTokenExpiresAt || null, - passwordResetToken: item.passwordResetToken || null, - passwordResetTokenExpiresAt: item.passwordResetTokenExpiresAt || null, - provider: item.provider || config.providers.LOCAL, - importHash: item.importHash || null, - createdById: currentUser.id, - updatedById: currentUser.id, - createdAt: new Date(Date.now() + index * 1000), - })); - - // Bulk create items - const users = await db.users.bulkCreate(usersData, { transaction }); - - // For each item created, replace relation files - - for (let i = 0; i < users.length; i++) { - await FileDBApi.replaceRelationFiles( - { - belongsTo: db.users.getTableName(), - belongsToColumn: 'avatar', - belongsToId: users[i].id, - }, - data[i].avatar, - options, - ); - } - - return users; - } - - static async update(updateOptions) { - assertUpdateOptions(updateOptions, 'DBApi'); - const { - id, - data, - currentUser = { id: null }, - transaction, - runtimeContext, - } = updateOptions; - const dbOptions = { currentUser, transaction, runtimeContext }; - - const users = await db.users.findByPk(id, { transaction }); - - if (data?.app_role && typeof data.app_role === 'object') { - data.app_role = data.app_role.id || data.app_role.value || null; - } - - if (Array.isArray(data?.custom_permissions)) { - data.custom_permissions = data.custom_permissions - .map((item) => { - if (typeof item === 'string') return item; - if (item && typeof item === 'object') return item.id || item.value; - return null; - }) - .filter(Boolean); - } - - if (!data?.app_role) { - data.app_role = users?.app_role?.id; - } - if (!data?.custom_permissions) { - data.custom_permissions = users?.custom_permissions?.map( - (item) => item.id, - ); - } - - if (data.password) { - data.password = bcrypt.hashSync(data.password, config.bcrypt.saltRounds); - } else { - data.password = users.password; - } - - const updatePayload = {}; - - if (data.firstName !== undefined) updatePayload.firstName = data.firstName; - - if (data.lastName !== undefined) updatePayload.lastName = data.lastName; - - if (data.phoneNumber !== undefined) - updatePayload.phoneNumber = data.phoneNumber; - - if (data.email !== undefined) updatePayload.email = data.email; - - if (data.disabled !== undefined) updatePayload.disabled = data.disabled; - - if (data.password !== undefined) updatePayload.password = data.password; - - if (data.emailVerified !== undefined) - updatePayload.emailVerified = data.emailVerified; - else updatePayload.emailVerified = true; - - if (data.emailVerificationToken !== undefined) - updatePayload.emailVerificationToken = data.emailVerificationToken; - - if (data.emailVerificationTokenExpiresAt !== undefined) - updatePayload.emailVerificationTokenExpiresAt = - data.emailVerificationTokenExpiresAt; - - if (data.passwordResetToken !== undefined) - updatePayload.passwordResetToken = data.passwordResetToken; - - if (data.passwordResetTokenExpiresAt !== undefined) - updatePayload.passwordResetTokenExpiresAt = - data.passwordResetTokenExpiresAt; - - if (data.provider !== undefined) updatePayload.provider = data.provider; - - updatePayload.updatedById = currentUser.id; - - await users.update(updatePayload, { transaction }); - - if (data.app_role !== undefined) { - await users.setApp_role( - data.app_role, - - { transaction }, - ); - } - - if (data.custom_permissions !== undefined) { - await users.setCustom_permissions(data.custom_permissions, { - transaction, - }); - } - - await FileDBApi.replaceRelationFiles( - { - belongsTo: db.users.getTableName(), - belongsToColumn: 'avatar', - belongsToId: users.id, - }, - data.avatar, - dbOptions, - ); - - return users; - } - - static async deleteByIds(options) { - assertDeleteByIdsOptions(options, 'DBApi'); - const { ids, currentUser = { id: null }, transaction } = options; - - const users = await db.users.findAll({ - where: { - id: { - [Op.in]: ids, - }, - }, - transaction, - }); - - for (const record of users) { - await record.update({ deletedBy: currentUser.id }, { transaction }); - } - for (const record of users) { - await record.destroy({ transaction }); - } - - return users; - } - - static async remove(options) { - assertIdOptions(options, 'DBApi', 'remove'); - const { id, currentUser = { id: null }, transaction } = options; - - const users = await db.users.findByPk(id, { transaction }); - - await users.update( - { - deletedBy: currentUser.id, - }, - { - transaction, - }, - ); - - await users.destroy({ - transaction, - }); - - return users; - } - - /** - * Find a single user by criteria - * Uses minimal includes by default (avatar + app_role with permissions) - * @param {Object} where - Query conditions - * @param {Object} options - Options including transaction and custom includes - * @param {Array} options.include - Override default includes if needed - */ - static async findBy(where, options) { - const transaction = (options && options.transaction) || undefined; - const include = options?.include ?? this.FIND_BY_INCLUDES; - - const users = await db.users.findOne({ - where, - transaction, - include, - }); - - if (!users) { - return users; - } - - const output = users.get({ plain: true }); - - // Map nested permissions from app_role for backward compatibility - if (output.app_role) { - output.app_role_permissions = output.app_role.permissions || []; - } - - const productionPresentationAccess = - await db.production_presentation_access.findAll({ - where: { userId: output.id }, - include: [ - { - association: 'project', - attributes: ['id', 'name', 'slug'], - where: { production_presentation_visibility: 'private' }, - required: true, - }, - ], - transaction, - }); - - output.allowed_private_production_project_ids = productionPresentationAccess - .map((row) => { - const plain = - typeof row.get === 'function' ? row.get({ plain: true }) : row; - const project = plain.project; - if (!project?.id) return null; - - return { - id: project.id, - label: `${project.name} (${project.slug})`, - name: project.name, - slug: project.slug, - }; - }) - .filter(Boolean); - - return output; - } - - /** - * Lightweight user lookup for JWT authentication - * Only loads essential fields and app_role with permissions for RBAC - * Optimized for the auth flow that runs on every authenticated request - */ - static async findByForAuth(where, options) { - const transaction = (options && options.transaction) || undefined; - - const user = await db.users.findOne({ - where, - transaction, - attributes: [ - 'id', - 'email', - 'disabled', - 'firstName', - 'lastName', - 'app_roleId', - ], - include: [ - { - association: 'app_role', - include: [{ association: 'permissions' }], - }, - { association: 'custom_permissions' }, - ], - }); - - if (!user) { - return user; - } - - const output = user.get({ plain: true }); - - // Map nested permissions from app_role for backward compatibility - if (output.app_role) { - output.app_role_permissions = output.app_role.permissions || []; - } - - return output; - } - - static async findAll(filter, options) { - const limit = filter.limit || 0; - let offset = 0; - let where = {}; - const currentPage = Number(filter.page) || 1; - - offset = Math.max(currentPage - 1, 0) * limit; - - const appRoleTerms = filter.app_role ? filter.app_role.split('|') : []; - const appRoleValidUuids = Utils.filterValidUuids(appRoleTerms); - - // Use lightweight includes for list view (only app_role, no custom_permissions or avatar) - let include = [ - { - model: db.roles, - as: 'app_role', - required: false, - where: filter.app_role - ? { - [Op.or]: [ - ...(appRoleValidUuids.length > 0 - ? [{ id: { [Op.in]: appRoleValidUuids } }] - : []), - { - name: { - [Op.or]: appRoleTerms.map((term) => ({ - [Op.iLike]: `%${term}%`, - })), - }, - }, - ], - } - : {}, - }, - ]; - - if (filter) { - if (filter.id) { - if (!Utils.isValidUuid(filter.id)) { - return { rows: [], count: 0 }; - } - where = { ...where, id: filter.id }; - } - - if (filter.firstName) { - where = { - ...where, - [Op.and]: Utils.ilike('users', 'firstName', filter.firstName), - }; - } - - if (filter.lastName) { - where = { - ...where, - [Op.and]: Utils.ilike('users', 'lastName', filter.lastName), - }; - } - - if (filter.phoneNumber) { - where = { - ...where, - [Op.and]: Utils.ilike('users', 'phoneNumber', filter.phoneNumber), - }; - } - - if (filter.email) { - where = { - ...where, - [Op.and]: Utils.ilike('users', 'email', filter.email), - }; - } - - if (filter.password) { - where = { - ...where, - [Op.and]: Utils.ilike('users', 'password', filter.password), - }; - } - - if (filter.emailVerificationToken) { - where = { - ...where, - [Op.and]: Utils.ilike( - 'users', - 'emailVerificationToken', - filter.emailVerificationToken, - ), - }; - } - - if (filter.passwordResetToken) { - where = { - ...where, - [Op.and]: Utils.ilike( - 'users', - 'passwordResetToken', - filter.passwordResetToken, - ), - }; - } - - if (filter.provider) { - where = { - ...where, - [Op.and]: Utils.ilike('users', 'provider', filter.provider), - }; - } - - if (filter.emailVerificationTokenExpiresAtRange) { - const [start, end] = filter.emailVerificationTokenExpiresAtRange; - - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - emailVerificationTokenExpiresAt: { - ...where.emailVerificationTokenExpiresAt, - [Op.gte]: start, - }, - }; - } - - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - emailVerificationTokenExpiresAt: { - ...where.emailVerificationTokenExpiresAt, - [Op.lte]: end, - }, - }; - } - } - - if (filter.passwordResetTokenExpiresAtRange) { - const [start, end] = filter.passwordResetTokenExpiresAtRange; - - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - passwordResetTokenExpiresAt: { - ...where.passwordResetTokenExpiresAt, - [Op.gte]: start, - }, - }; - } - - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - passwordResetTokenExpiresAt: { - ...where.passwordResetTokenExpiresAt, - [Op.lte]: end, - }, - }; - } - } - - if (filter.active !== undefined) { - where = { - ...where, - active: filter.active === true || filter.active === 'true', - }; - } - - if (filter.disabled) { - where = { - ...where, - disabled: filter.disabled, - }; - } - - if (filter.emailVerified) { - where = { - ...where, - emailVerified: filter.emailVerified, - }; - } - - if (filter.custom_permissions) { - const searchTerms = filter.custom_permissions.split('|'); - const permissionValidUuids = Utils.filterValidUuids(searchTerms); - - include = [ - { - model: db.permissions, - as: 'custom_permissions_filter', - required: searchTerms.length > 0, - where: - searchTerms.length > 0 - ? { - [Op.or]: [ - ...(permissionValidUuids.length > 0 - ? [{ id: { [Op.in]: permissionValidUuids } }] - : []), - { - name: { - [Op.or]: searchTerms.map((term) => ({ - [Op.iLike]: `%${term}%`, - })), - }, - }, - ], - } - : undefined, - }, - ...include, - ]; - } - - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange; - - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - ['createdAt']: { - ...where.createdAt, - [Op.gte]: start, - }, - }; - } - - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - ['createdAt']: { - ...where.createdAt, - [Op.lte]: end, - }, - }; - } - } - } - - const sortField = this.SORTABLE_FIELDS.includes(filter.field) - ? filter.field - : 'createdAt'; - const sortDirection = - String(filter.sort || 'desc').toUpperCase() === 'ASC' ? 'ASC' : 'DESC'; - - const queryOptions = { - attributes: { exclude: this.SENSITIVE_FIELDS }, - where, - include, - distinct: true, - order: [[sortField, sortDirection]], - transaction: options?.transaction, - }; - - if (!options?.countOnly) { - queryOptions.limit = limit ? Number(limit) : undefined; - queryOptions.offset = offset ? Number(offset) : undefined; - } - - try { - const { rows, count } = await db.users.findAndCountAll(queryOptions); - - return { - rows: options?.countOnly ? [] : rows, - count: count, - }; - } catch (error) { - logger.error({ err: error, table: 'users' }, 'Error executing query'); - throw error; - } - } - - static async findAllAutocomplete(options, queryOptions = {}) { - assertAutocompleteOptions(options, 'DBApi'); - const { query, limit, offset } = options; - const transaction = queryOptions.transaction; - let where = {}; - - if (query) { - const orConditions = [Utils.ilike('users', 'firstName', query)]; - - if (Utils.isValidUuid(query)) { - orConditions.unshift({ id: query }); - } - - where = { [Op.or]: orConditions }; - } - - const records = await db.users.findAll({ - attributes: ['id', 'firstName'], - where, - limit: limit ? Number(limit) : undefined, - offset: offset ? Number(offset) : undefined, - orderBy: [['firstName', 'ASC']], - transaction, - }); - - return records.map((record) => ({ - id: record.id, - label: record.firstName, - })); - } - - static async createFromAuth(data, options) { - const transaction = (options && options.transaction) || undefined; - const users = await db.users.create( - { - email: data.email, - firstName: data.firstName, - authenticationUid: data.authenticationUid, - password: data.password, - }, - { transaction }, - ); - - const app_role = await db.roles.findOne({ - where: { name: config.roles?.user || 'User' }, - }); - if (app_role?.id) { - await users.setApp_role(app_role?.id || null, { - transaction, - }); - } - - await users.update( - { - authenticationUid: users.id, - }, - { transaction }, - ); - - delete users.password; - return users; - } - - static async updatePassword(id, password, options) { - const currentUser = (options && options.currentUser) || { id: null }; - - const transaction = (options && options.transaction) || undefined; - - const users = await db.users.findByPk(id, { - transaction, - }); - - await users.update( - { - password, - authenticationUid: id, - updatedById: currentUser.id, - }, - { transaction }, - ); - - return users; - } - - static async generateEmailVerificationToken(email, options) { - return this._generateToken( - ['emailVerificationToken', 'emailVerificationTokenExpiresAt'], - email, - options, - ); - } - - static async generatePasswordResetToken(email, options) { - return this._generateToken( - ['passwordResetToken', 'passwordResetTokenExpiresAt'], - email, - options, - ); - } - - static async findByPasswordResetToken(token, options) { - const transaction = (options && options.transaction) || undefined; - - return db.users.findOne({ - where: { - passwordResetToken: token, - passwordResetTokenExpiresAt: { - [db.Sequelize.Op.gt]: Date.now(), - }, - }, - transaction, - }); - } - - static async findByEmailVerificationToken(token, options) { - const transaction = (options && options.transaction) || undefined; - return db.users.findOne({ - where: { - emailVerificationToken: token, - emailVerificationTokenExpiresAt: { - [db.Sequelize.Op.gt]: Date.now(), - }, - }, - transaction, - }); - } - - static async markEmailVerified(id, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - const users = await db.users.findByPk(id, { - transaction, - }); - - await users.update( - { - emailVerified: true, - updatedById: currentUser.id, - }, - { transaction }, - ); - - return true; - } - - static async _generateToken(keyNames, email, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - const users = await db.users.findOne({ - where: { email: email.toLowerCase() }, - transaction, - }); - - const token = crypto.randomBytes(20).toString('hex'); - // Token expires in 24 hours (was 6 minutes - too short for email verification flows) - const TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours - const tokenExpiresAt = Date.now() + TOKEN_EXPIRY_MS; - - if (users) { - await users.update( - { - [keyNames[0]]: token, - [keyNames[1]]: tokenExpiresAt, - updatedById: currentUser.id, - }, - { transaction }, - ); - } - - return token; - } -}; diff --git a/backend/src/db/api/users.ts b/backend/src/db/api/users.ts new file mode 100644 index 0000000..c4b2fd8 --- /dev/null +++ b/backend/src/db/api/users.ts @@ -0,0 +1,979 @@ +import crypto from 'node:crypto'; +import bcrypt from 'bcrypt'; +import type { Transaction } from 'sequelize'; + +import db from '../models/index.ts'; +import FileDBApi from './file.ts'; +import Utils from '../utils.ts'; +import config from '../../config.ts'; +import { logger } from '../../utils/logger.ts'; +import { + assertAutocompleteOptions, + assertCreateOptions, + assertDeleteByIdsOptions, + assertIdOptions, + assertUpdateOptions, +} from '../../contracts/entity-options.ts'; +import type { + AutocompleteOptions, + DbFindAllOptions, + DbFindByOptions, + PaginatedResult, + QueryWhere, + RoleRecord, + ServiceOptions, + UserAuthCreatePayload, + UserAutocompleteOption, + UserCreatePayload, + UserData, + UserFindAllOptions, + UserFindByWhere, + UserListFilter, + UserModelApi, + UserProductionPresentationAccessRecord, + UserRangeFilter, + UserRecord, + UsersDbApi, + UserTokenFields, + UserUpdatePayload, +} from '../../types/index.ts'; + +const { Op } = db.Sequelize; + +function isDbFindAllOptions( + value: UserListFilter | DbFindAllOptions | undefined, +): value is DbFindAllOptions { + return Boolean(value) && typeof value === 'object' && 'filter' in value; +} + +function isDbFindByOptionsInput( + value: UserFindByWhere | DbFindByOptions, +): value is DbFindByOptions { + return 'where' in value && isQueryWhere(value.where); +} + +function isUserListFilter(value: unknown): value is UserListFilter { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function isQueryWhere(value: unknown): value is QueryWhere { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function isRangeFilter(value: unknown): value is UserRangeFilter { + return Array.isArray(value) && value.length === 2; +} + +function normalizeFilter( + filter?: UserListFilter | DbFindAllOptions, +): UserListFilter { + if (!filter) return {}; + if (!isDbFindAllOptions(filter)) return filter; + return isUserListFilter(filter.filter) ? filter.filter : {}; +} + +function hasObjectId(value: unknown): value is { id?: string; value?: string } { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function normalizeRoleId( + role: UserData['app_role'], +): string | RoleRecord | null | undefined { + if (!hasObjectId(role)) return role; + return role.id || role.value || null; +} + +function normalizePermissionIds( + permissions: UserData['custom_permissions'], +): string[] | undefined { + if (!Array.isArray(permissions)) return undefined; + + return permissions.flatMap((item) => { + if (typeof item === 'string') return [item]; + if (!hasObjectId(item)) return []; + const id = item.id || item.value; + return id ? [id] : []; + }); +} + +function addTextFilter(where: QueryWhere, field: string, value: unknown): void { + if (typeof value !== 'string' || !value) return; + where[Op.and] = Utils.ilike('users', field, value); +} + +function addRangeBoundary( + where: QueryWhere, + field: string, + operator: symbol, + value: UserRangeFilter[number], +): void { + if (value === undefined || value === null || value === '') return; + + const current = isQueryWhere(where[field]) ? where[field] : {}; + where[field] = { + ...current, + [operator]: value, + }; +} + +function addRangeFilter(where: QueryWhere, field: string, range: unknown): void { + if (!isRangeFilter(range)) return; + + const [start, end] = range; + addRangeBoundary(where, field, Op.gte, start); + addRangeBoundary(where, field, Op.lte, end); +} + +function getCurrentUserId(options?: ServiceOptions): string | null { + return options?.currentUser?.id ?? null; +} + +function buildTransactionOptions( + transaction: Transaction | undefined, +): { transaction?: Transaction | undefined } { + const options: { transaction?: Transaction | undefined } = {}; + if (transaction !== undefined) { + options.transaction = transaction; + } + return options; +} + +function buildUserCreatePayload( + userData: UserData, + currentUserId: string | null, +): UserCreatePayload { + const password = userData.password || crypto.randomBytes(20).toString('hex'); + + return { + id: userData.id || undefined, + firstName: userData.firstName || null, + lastName: userData.lastName || null, + phoneNumber: userData.phoneNumber || null, + email: userData.email || null, + disabled: userData.disabled || false, + password: bcrypt.hashSync(password, config.bcrypt.saltRounds), + emailVerified: userData.emailVerified || true, + emailVerificationToken: userData.emailVerificationToken || null, + emailVerificationTokenExpiresAt: + userData.emailVerificationTokenExpiresAt || null, + passwordResetToken: userData.passwordResetToken || null, + passwordResetTokenExpiresAt: userData.passwordResetTokenExpiresAt || null, + provider: userData.provider || config.providers.LOCAL, + importHash: userData.importHash || null, + createdById: currentUserId, + updatedById: currentUserId, + }; +} + +function buildUserUpdatePayload( + data: UserData, + currentUserId: string | null, +): UserUpdatePayload { + const updatePayload: UserUpdatePayload = {}; + + if (data.firstName !== undefined) updatePayload.firstName = data.firstName; + if (data.lastName !== undefined) updatePayload.lastName = data.lastName; + if (data.phoneNumber !== undefined) updatePayload.phoneNumber = data.phoneNumber; + if (data.email !== undefined) updatePayload.email = data.email; + if (data.disabled !== undefined) updatePayload.disabled = data.disabled; + if (data.password !== undefined && data.password !== null) { + updatePayload.password = data.password; + } + if (data.emailVerified !== undefined) { + updatePayload.emailVerified = data.emailVerified; + } else { + updatePayload.emailVerified = true; + } + if (data.emailVerificationToken !== undefined) { + updatePayload.emailVerificationToken = data.emailVerificationToken; + } + if (data.emailVerificationTokenExpiresAt !== undefined) { + updatePayload.emailVerificationTokenExpiresAt = + data.emailVerificationTokenExpiresAt; + } + if (data.passwordResetToken !== undefined) { + updatePayload.passwordResetToken = data.passwordResetToken; + } + if (data.passwordResetTokenExpiresAt !== undefined) { + updatePayload.passwordResetTokenExpiresAt = data.passwordResetTokenExpiresAt; + } + if (data.provider !== undefined) updatePayload.provider = data.provider; + + updatePayload.updatedById = currentUserId; + return updatePayload; +} + +class UsersDBApi { + declare static partialUpdate: UsersDbApi['partialUpdate']; + + static get MODEL(): UserModelApi { + return db.users; + } + + static get SORTABLE_FIELDS(): string[] { + return Object.keys(db.users.rawAttributes || {}); + } + + /** + * Default includes for findBy() - minimal set for single user lookup + * Only loads avatar and app_role with permissions (needed for RBAC) + */ + static get FIND_BY_INCLUDES(): unknown[] { + return [ + { association: 'avatar' }, + { + association: 'app_role', + include: [{ association: 'permissions' }], + }, + ]; + } + + /** + * Minimal includes for findAll() - only app_role for list display + * Excludes avatar, custom_permissions (rarely needed in list views) + */ + static get FIND_ALL_INCLUDES(): unknown[] { + return [ + { + model: db.roles, + as: 'app_role', + required: false, + }, + ]; + } + + /** + * Sensitive fields that should be excluded from query results + */ + static get SENSITIVE_FIELDS(): string[] { + return [ + 'password', + 'emailVerificationToken', + 'emailVerificationTokenExpiresAt', + 'passwordResetToken', + 'passwordResetTokenExpiresAt', + ]; + } + + static async create(options: Parameters[0]): Promise { + assertCreateOptions(options, 'DBApi'); + + const { data: userData, transaction } = options; + + const users = await db.users.create( + buildUserCreatePayload(userData, getCurrentUserId(options)), + buildTransactionOptions(transaction), + ); + + if (!userData.app_role) { + const role = await db.roles.findOne({ + where: { name: 'User' }, + }); + if (role) { + await users.setApp_role(role, { + transaction, + }); + } + } else { + await users.setApp_role(normalizeRoleId(userData.app_role) || null, { + transaction, + }); + } + + await users.setCustom_permissions( + normalizePermissionIds(userData.custom_permissions) || [], + { + transaction, + }, + ); + + await FileDBApi.replaceRelationFiles( + { + belongsTo: db.users.getTableName(), + belongsToColumn: 'avatar', + belongsToId: users.id, + }, + userData.avatar, + options, + ); + + return users; + } + + static async bulkImport( + data: UserData[], + options: ServiceOptions = {}, + ): Promise { + const currentUserId = getCurrentUserId(options); + const transaction = options.transaction; + + // Prepare data - wrapping individual data transformations in a map() method + const usersData = data.map((item, index) => ({ + ...buildUserCreatePayload(item, currentUserId), + emailVerified: item.emailVerified || false, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const users = await db.users.bulkCreate(usersData, { transaction }); + + // For each item created, replace relation files + + for (let i = 0; i < users.length; i++) { + const user = users[i]; + const item = data[i]; + if (!user || !item) continue; + + await FileDBApi.replaceRelationFiles( + { + belongsTo: db.users.getTableName(), + belongsToColumn: 'avatar', + belongsToId: user.id, + }, + item.avatar, + options, + ); + } + + return users; + } + + static async update( + updateOptions: Parameters[0], + ): Promise { + assertUpdateOptions(updateOptions, 'DBApi'); + const { + id, + data, + transaction, + runtimeContext, + } = updateOptions; + const dbOptions: ServiceOptions = {}; + if (transaction !== undefined) { + dbOptions.transaction = transaction; + } + if (updateOptions.currentUser !== undefined) { + dbOptions.currentUser = updateOptions.currentUser; + } + if (runtimeContext !== undefined) { + dbOptions.runtimeContext = runtimeContext; + } + + const users = await db.users.findByPk(id, buildTransactionOptions(transaction)); + if (!users) { + throw new Error('UsersNotFound'); + } + + const normalizedRole = normalizeRoleId(data.app_role) || null; + data.app_role = normalizedRole; + + const customPermissionIds = normalizePermissionIds(data.custom_permissions); + if (customPermissionIds) { + data.custom_permissions = customPermissionIds; + } + + if (!data?.app_role) { + const existingRoleId = users.app_role?.id; + if (existingRoleId) { + data.app_role = existingRoleId; + } + } + if (!data?.custom_permissions) { + const existingPermissionIds = users.custom_permissions?.flatMap((item) => + item.id ? [item.id] : [], + ) || []; + if (existingPermissionIds.length) { + data.custom_permissions = existingPermissionIds; + } + } + + if (data.password) { + data.password = bcrypt.hashSync(data.password, config.bcrypt.saltRounds); + } else { + data.password = users.password || null; + } + + const updatePayload = buildUserUpdatePayload( + data, + getCurrentUserId(updateOptions), + ); + + await users.update(updatePayload, { transaction }); + + if (data.app_role !== undefined) { + const roleForRelation = normalizeRoleId(data.app_role) || null; + await users.setApp_role( + roleForRelation, + + { transaction }, + ); + } + + if (data.custom_permissions !== undefined) { + await users.setCustom_permissions(normalizePermissionIds(data.custom_permissions) || [], { + transaction, + }); + } + + await FileDBApi.replaceRelationFiles( + { + belongsTo: db.users.getTableName(), + belongsToColumn: 'avatar', + belongsToId: users.id, + }, + data.avatar, + dbOptions, + ); + + return users; + } + + static async deleteByIds( + options: Parameters[0], + ): Promise { + assertDeleteByIdsOptions(options, 'DBApi'); + const { ids, transaction } = options; + const currentUserId = getCurrentUserId(options); + + const users = await db.users.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + for (const record of users) { + await record.update({ deletedBy: currentUserId }, { transaction }); + } + for (const record of users) { + await record.destroy({ transaction }); + } + + return users; + } + + static async remove( + options: Parameters[0], + ): Promise { + assertIdOptions(options, 'DBApi', 'remove'); + const { id, transaction } = options; + const currentUserId = getCurrentUserId(options); + + const users = await db.users.findByPk(id, buildTransactionOptions(transaction)); + if (!users) { + throw new Error('UsersNotFound'); + } + + await users.update( + { + deletedBy: currentUserId, + }, + { + transaction, + }, + ); + + await users.destroy({ + transaction, + }); + + return users; + } + + /** + * Find a single user by criteria + * Uses minimal includes by default (avatar + app_role with permissions) + * @param {Object} where - Query conditions + * @param {Object} options - Options including transaction and custom includes + * @param {Array} options.include - Override default includes if needed + */ + static async findBy(options: DbFindByOptions): Promise; + static async findBy( + where: UserFindByWhere, + options?: ServiceOptions, + ): Promise; + static async findBy( + whereOrOptions: UserFindByWhere | DbFindByOptions, + options: ServiceOptions = {}, + ): Promise { + const isOptionsInput = isDbFindByOptionsInput(whereOrOptions); + const where = isOptionsInput ? whereOrOptions.where : whereOrOptions; + const transaction = isOptionsInput + ? whereOrOptions.transaction + : options.transaction; + const include = + isOptionsInput && whereOrOptions.include !== undefined + ? whereOrOptions.include + : this.FIND_BY_INCLUDES; + + const users = await db.users.findOne({ + where: isQueryWhere(where) ? where : {}, + ...buildTransactionOptions(transaction), + include, + }); + + if (!users) { + return users; + } + + const output = users.get({ plain: true }); + + // Map nested permissions from app_role for backward compatibility + if (output.app_role) { + output.app_role_permissions = output.app_role.permissions || []; + } + + const productionPresentationAccess = + await db.production_presentation_access.findAll({ + where: { userId: output.id }, + include: [ + { + association: 'project', + attributes: ['id', 'name', 'slug'], + where: { production_presentation_visibility: 'private' }, + required: true, + }, + ], + ...buildTransactionOptions(transaction), + }); + + output.allowed_private_production_project_ids = productionPresentationAccess + .flatMap((row: UserProductionPresentationAccessRecord) => { + const plain = row.get({ plain: true }); + const project = plain.project; + if (!project?.id || !project.name || !project.slug) return []; + + return { + id: project.id, + label: `${project.name} (${project.slug})`, + name: project.name, + slug: project.slug, + }; + }); + + return output; + } + + /** + * Lightweight user lookup for JWT authentication + * Only loads essential fields and app_role with permissions for RBAC + * Optimized for the auth flow that runs on every authenticated request + */ + static async findByForAuth( + where: UserFindByWhere, + options: ServiceOptions = {}, + ): Promise { + const transaction = options.transaction; + + const user = await db.users.findOne({ + where, + ...buildTransactionOptions(transaction), + attributes: [ + 'id', + 'email', + 'disabled', + 'firstName', + 'lastName', + 'app_roleId', + ], + include: [ + { + association: 'app_role', + include: [{ association: 'permissions' }], + }, + { association: 'custom_permissions' }, + ], + }); + + if (!user) { + return user; + } + + const output = user.get({ plain: true }); + + // Map nested permissions from app_role for backward compatibility + if (output.app_role) { + output.app_role_permissions = output.app_role.permissions || []; + } + + return output; + } + + static async findAll( + filter?: UserListFilter, + options?: UserFindAllOptions, + ): Promise>; + static async findAll( + options: DbFindAllOptions, + ): Promise>; + static async findAll( + filter?: UserListFilter | DbFindAllOptions, + options: UserFindAllOptions = {}, + ): Promise> { + const normalizedFilter = normalizeFilter(filter); + const limit = Number(normalizedFilter.limit) || 0; + let offset = 0; + const where: QueryWhere = {}; + const currentPage = Number(normalizedFilter.page) || 1; + + offset = Math.max(currentPage - 1, 0) * limit; + + const appRoleTerms = + typeof normalizedFilter.app_role === 'string' + ? normalizedFilter.app_role.split('|') + : []; + const appRoleValidUuids = Utils.filterValidUuids(appRoleTerms); + + // Use lightweight includes for list view (only app_role, no custom_permissions or avatar) + let include: unknown[] = [ + { + model: db.roles, + as: 'app_role', + required: false, + where: normalizedFilter.app_role + ? { + [Op.or]: [ + ...(appRoleValidUuids.length > 0 + ? [{ id: { [Op.in]: appRoleValidUuids } }] + : []), + { + name: { + [Op.or]: appRoleTerms.map((term) => ({ + [Op.iLike]: `%${term}%`, + })), + }, + }, + ], + } + : {}, + }, + ]; + + if (normalizedFilter) { + if (normalizedFilter.id) { + if (!Utils.isValidUuid(normalizedFilter.id)) { + return { rows: [], count: 0 }; + } + where.id = normalizedFilter.id; + } + + addTextFilter(where, 'firstName', normalizedFilter.firstName); + addTextFilter(where, 'lastName', normalizedFilter.lastName); + addTextFilter(where, 'phoneNumber', normalizedFilter.phoneNumber); + addTextFilter(where, 'email', normalizedFilter.email); + addTextFilter(where, 'password', normalizedFilter.password); + addTextFilter( + where, + 'emailVerificationToken', + normalizedFilter.emailVerificationToken, + ); + addTextFilter(where, 'passwordResetToken', normalizedFilter.passwordResetToken); + addTextFilter(where, 'provider', normalizedFilter.provider); + addRangeFilter( + where, + 'emailVerificationTokenExpiresAt', + normalizedFilter.emailVerificationTokenExpiresAtRange, + ); + addRangeFilter( + where, + 'passwordResetTokenExpiresAt', + normalizedFilter.passwordResetTokenExpiresAtRange, + ); + + if (normalizedFilter.active !== undefined) { + where.active = + normalizedFilter.active === true || normalizedFilter.active === 'true'; + } + + if (normalizedFilter.disabled) { + where.disabled = normalizedFilter.disabled; + } + + if (normalizedFilter.emailVerified) { + where.emailVerified = normalizedFilter.emailVerified; + } + + if (typeof normalizedFilter.custom_permissions === 'string') { + const searchTerms = normalizedFilter.custom_permissions.split('|'); + const permissionValidUuids = Utils.filterValidUuids(searchTerms); + + include = [ + { + model: db.permissions, + as: 'custom_permissions_filter', + required: searchTerms.length > 0, + where: + searchTerms.length > 0 + ? { + [Op.or]: [ + ...(permissionValidUuids.length > 0 + ? [{ id: { [Op.in]: permissionValidUuids } }] + : []), + { + name: { + [Op.or]: searchTerms.map((term) => ({ + [Op.iLike]: `%${term}%`, + })), + }, + }, + ], + } + : undefined, + }, + ...include, + ]; + } + + addRangeFilter(where, 'createdAt', normalizedFilter.createdAtRange); + } + + const sortField = + typeof normalizedFilter.field === 'string' && + this.SORTABLE_FIELDS.includes(normalizedFilter.field) + ? normalizedFilter.field + : 'createdAt'; + const sortDirection = + String(normalizedFilter.sort || 'desc').toUpperCase() === 'ASC' + ? 'ASC' + : 'DESC'; + + const queryOptions: { + attributes: { exclude: string[] }; + where: QueryWhere; + include: unknown[]; + distinct: true; + order: string[][]; + transaction?: Transaction | undefined; + limit?: number | undefined; + offset?: number | undefined; + } = { + attributes: { exclude: this.SENSITIVE_FIELDS }, + where, + include, + distinct: true, + order: [[sortField, sortDirection]], + }; + if (options.transaction !== undefined) { + queryOptions.transaction = options.transaction; + } + + if (!options.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.users.findAndCountAll(queryOptions); + + return { + rows: options.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + logger.error({ err: error, table: 'users' }, 'Error executing query'); + throw error; + } + } + + static async findAllAutocomplete( + options: AutocompleteOptions, + queryOptions: ServiceOptions = {}, + ): Promise { + assertAutocompleteOptions(options, 'DBApi'); + const { query, limit, offset } = options; + const transaction = queryOptions.transaction; + const where: QueryWhere = {}; + + if (query) { + const orConditions: unknown[] = [Utils.ilike('users', 'firstName', query)]; + + if (Utils.isValidUuid(query)) { + orConditions.unshift({ id: query }); + } + + where[Op.or] = orConditions; + } + + const records = await db.users.findAll({ + attributes: ['id', 'firstName'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['firstName', 'ASC']], + transaction, + }); + + return records.map((record) => ({ + id: record.id, + label: record.firstName, + })); + } + + static async createFromAuth( + data: UserAuthCreatePayload, + options: ServiceOptions = {}, + ): Promise { + const transaction = options.transaction; + const users = await db.users.create( + data, + buildTransactionOptions(transaction), + ); + + const app_role = await db.roles.findOne({ + where: { name: config.roles?.user || 'User' }, + }); + if (app_role?.id) { + await users.setApp_role(app_role?.id || null, { + transaction, + }); + } + + await users.update( + { + authenticationUid: users.id, + }, + { transaction }, + ); + + delete users.password; + return users; + } + + static async updatePassword( + id: string, + password: string, + options: ServiceOptions = {}, + ): Promise { + const currentUserId = getCurrentUserId(options); + const transaction = options.transaction; + + const users = await db.users.findByPk(id, buildTransactionOptions(transaction)); + if (!users) { + throw new Error('UsersNotFound'); + } + + await users.update( + { + password, + authenticationUid: id, + updatedById: currentUserId, + }, + { transaction }, + ); + + return users; + } + + static async generateEmailVerificationToken( + email: string, + options?: ServiceOptions, + ): Promise { + return this._generateToken( + ['emailVerificationToken', 'emailVerificationTokenExpiresAt'], + email, + options, + ); + } + + static async generatePasswordResetToken( + email: string, + options?: ServiceOptions, + ): Promise { + return this._generateToken( + ['passwordResetToken', 'passwordResetTokenExpiresAt'], + email, + options, + ); + } + + static async findByPasswordResetToken( + token: string, + options: ServiceOptions = {}, + ): Promise { + const transaction = options.transaction; + + return db.users.findOne({ + where: { + passwordResetToken: token, + passwordResetTokenExpiresAt: { + [db.Sequelize.Op.gt]: Date.now(), + }, + }, + ...buildTransactionOptions(transaction), + }); + } + + static async findByEmailVerificationToken( + token: string, + options: ServiceOptions = {}, + ): Promise { + const transaction = options.transaction; + return db.users.findOne({ + where: { + emailVerificationToken: token, + emailVerificationTokenExpiresAt: { + [db.Sequelize.Op.gt]: Date.now(), + }, + }, + ...buildTransactionOptions(transaction), + }); + } + + static async markEmailVerified( + id: string, + options: ServiceOptions = {}, + ): Promise { + const currentUserId = getCurrentUserId(options); + const transaction = options.transaction; + + const users = await db.users.findByPk(id, buildTransactionOptions(transaction)); + if (!users) { + return false; + } + + await users.update( + { + emailVerified: true, + updatedById: currentUserId, + }, + { transaction }, + ); + + return true; + } + + private static async _generateToken( + keyNames: readonly [keyof UserTokenFields, keyof UserTokenFields], + email: string, + options: ServiceOptions = {}, + ): Promise { + const currentUserId = getCurrentUserId(options); + const transaction = options.transaction; + const users = await db.users.findOne({ + where: { email: email.toLowerCase() }, + ...buildTransactionOptions(transaction), + }); + + const token = crypto.randomBytes(20).toString('hex'); + // Token expires in 24 hours (was 6 minutes - too short for email verification flows) + const TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours + const tokenExpiresAt = Date.now() + TOKEN_EXPIRY_MS; + + if (users) { + const updatePayload: UserTokenFields & { updatedById: string | null } = { + [keyNames[0]]: token, + [keyNames[1]]: tokenExpiresAt, + updatedById: currentUserId, + }; + await users.update(updatePayload, { transaction }); + } + + return token; + } +} + +const usersDBApi: UsersDbApi = UsersDBApi; + +export default usersDBApi; diff --git a/backend/src/db/db-config.ts b/backend/src/db/db-config.ts new file mode 100644 index 0000000..a0e00e6 --- /dev/null +++ b/backend/src/db/db-config.ts @@ -0,0 +1,64 @@ +import '../load-env.ts'; +import type { DatabaseConfigMap } from '../types/index.ts'; + +const sequelizeStorage = 'sequelize'; +const migrationStorageTableName = 'SequelizeMeta'; +type DatabaseLogging = NonNullable; + +function readEnvPort(name: string): number | undefined { + const value = process.env[name]; + if (!value) return undefined; + + const parsed = Number.parseInt(value, 10); + return Number.isFinite(parsed) ? parsed : undefined; +} + +function createEnvDatabaseConfig( + logging: DatabaseLogging, +): DatabaseConfigMap['production'] { + const config: DatabaseConfigMap['production'] = { + dialect: 'postgres', + logging, + seederStorage: sequelizeStorage, + migrationStorage: sequelizeStorage, + migrationStorageTableName, + }; + + if (process.env.DB_USER !== undefined) { + config.username = process.env.DB_USER; + } + if (process.env.DB_PASS !== undefined) { + config.password = process.env.DB_PASS; + } + if (process.env.DB_NAME !== undefined) { + config.database = process.env.DB_NAME; + } + if (process.env.DB_HOST !== undefined) { + config.host = process.env.DB_HOST; + } + + const port = readEnvPort('DB_PORT'); + if (port !== undefined) { + config.port = port; + } + + return config; +} + +const dbConfig: DatabaseConfigMap = { + production: createEnvDatabaseConfig(false), + development: { + username: 'postgres', + dialect: 'postgres', + password: '', + database: 'db_tour_builder_platform', + host: process.env.DB_HOST || 'localhost', + logging: console.log, + seederStorage: sequelizeStorage, + migrationStorage: sequelizeStorage, + migrationStorageTableName, + }, + dev_stage: createEnvDatabaseConfig(console.log), +}; + +export default dbConfig; diff --git a/backend/src/db/db.config.js b/backend/src/db/db.config.js deleted file mode 100644 index 81e77e5..0000000 --- a/backend/src/db/db.config.js +++ /dev/null @@ -1,37 +0,0 @@ -module.exports = { - production: { - dialect: 'postgres', - username: process.env.DB_USER, - password: process.env.DB_PASS, - database: process.env.DB_NAME, - host: process.env.DB_HOST, - port: process.env.DB_PORT, - logging: false, - seederStorage: 'sequelize', - migrationStorage: 'sequelize', - migrationStorageTableName: 'SequelizeMeta', - }, - development: { - username: 'postgres', - dialect: 'postgres', - password: '', - database: 'db_tour_builder_platform', - host: process.env.DB_HOST || 'localhost', - logging: console.log, - seederStorage: 'sequelize', - migrationStorage: 'sequelize', - migrationStorageTableName: 'SequelizeMeta', - }, - dev_stage: { - dialect: 'postgres', - username: process.env.DB_USER, - password: process.env.DB_PASS, - database: process.env.DB_NAME, - host: process.env.DB_HOST, - port: process.env.DB_PORT, - logging: console.log, - seederStorage: 'sequelize', - migrationStorage: 'sequelize', - migrationStorageTableName: 'SequelizeMeta', - }, -}; diff --git a/backend/src/db/migrations/20260331024423-remove-unused-theme-columns-from-projects.js b/backend/src/db/migrations/20260331024423-remove-unused-theme-columns-from-projects.js index 9926310..700cc3c 100644 --- a/backend/src/db/migrations/20260331024423-remove-unused-theme-columns-from-projects.js +++ b/backend/src/db/migrations/20260331024423-remove-unused-theme-columns-from-projects.js @@ -1,6 +1,5 @@ 'use strict'; -/** @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, _Sequelize) { await queryInterface.removeColumn('projects', 'theme_config_json'); diff --git a/backend/src/db/migrations/20260403000001-add-background-video-settings.js b/backend/src/db/migrations/20260403000001-add-background-video-settings.js index 0e7cb56..c5abb1a 100644 --- a/backend/src/db/migrations/20260403000001-add-background-video-settings.js +++ b/backend/src/db/migrations/20260403000001-add-background-video-settings.js @@ -1,6 +1,5 @@ 'use strict'; -/** @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, Sequelize) { await queryInterface.addColumn('tour_pages', 'background_video_autoplay', { diff --git a/backend/src/db/migrations/20260409000001-add-design-dimensions-to-projects.js b/backend/src/db/migrations/20260409000001-add-design-dimensions-to-projects.js index 1c60fe1..8b512e7 100644 --- a/backend/src/db/migrations/20260409000001-add-design-dimensions-to-projects.js +++ b/backend/src/db/migrations/20260409000001-add-design-dimensions-to-projects.js @@ -6,7 +6,6 @@ * Adds design_width and design_height columns to support * responsive canvas scaling with project-specific aspect ratios. * - * @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, Sequelize) { diff --git a/backend/src/db/migrations/20260413091125-add-reversed-variant-type.js b/backend/src/db/migrations/20260413091125-add-reversed-variant-type.js index 067d5a8..daa3671 100644 --- a/backend/src/db/migrations/20260413091125-add-reversed-variant-type.js +++ b/backend/src/db/migrations/20260413091125-add-reversed-variant-type.js @@ -7,7 +7,6 @@ * Also adds storage_key column to track the S3/local storage path. */ -/** @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, Sequelize) { // Add 'reversed' to the enum_asset_variants_variant_type enum diff --git a/backend/src/db/migrations/20260430000001-add-transition-settings.js b/backend/src/db/migrations/20260430000001-add-transition-settings.js index e94da7b..5f63a91 100644 --- a/backend/src/db/migrations/20260430000001-add-transition-settings.js +++ b/backend/src/db/migrations/20260430000001-add-transition-settings.js @@ -10,7 +10,6 @@ const { v4: uuidv4 } = require('uuid'); * * Cascade: Element → Project → Global (fallback) * - * @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, Sequelize) { diff --git a/backend/src/db/migrations/20260605000001-add-background-audio-settings.js b/backend/src/db/migrations/20260605000001-add-background-audio-settings.js index 33680f8..a47902e 100644 --- a/backend/src/db/migrations/20260605000001-add-background-audio-settings.js +++ b/backend/src/db/migrations/20260605000001-add-background-audio-settings.js @@ -1,6 +1,5 @@ 'use strict'; -/** @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, Sequelize) { await queryInterface.addColumn('tour_pages', 'background_audio_autoplay', { diff --git a/backend/src/db/migrations/package.json b/backend/src/db/migrations/package.json new file mode 100644 index 0000000..5bbefff --- /dev/null +++ b/backend/src/db/migrations/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/backend/src/db/models/access_logs.js b/backend/src/db/models/access_logs.ts similarity index 85% rename from backend/src/db/models/access_logs.js rename to backend/src/db/models/access_logs.ts index f4c6366..2467e82 100644 --- a/backend/src/db/models/access_logs.js +++ b/backend/src/db/models/access_logs.ts @@ -1,5 +1,10 @@ -module.exports = function (sequelize, DataTypes) { - const access_logs = sequelize.define( +import type { + SequelizeModel, + SequelizeModelFactory, +} from '../../types/index.ts'; + +const defineAccessLogsModel: SequelizeModelFactory = (sequelize, DataTypes) => { + const accessLogs: SequelizeModel = sequelize.define( 'access_logs', { id: { @@ -67,11 +72,7 @@ module.exports = function (sequelize, DataTypes) { }, ); - access_logs.associate = (db) => { - /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - //end loop - + accessLogs.associate = (db) => { db.access_logs.belongsTo(db.projects, { as: 'project', foreignKey: { @@ -101,5 +102,7 @@ module.exports = function (sequelize, DataTypes) { }); }; - return access_logs; + return accessLogs; }; + +export default defineAccessLogsModel; diff --git a/backend/src/db/models/asset_variants.js b/backend/src/db/models/asset_variants.ts similarity index 75% rename from backend/src/db/models/asset_variants.js rename to backend/src/db/models/asset_variants.ts index b665bf6..cfaa674 100644 --- a/backend/src/db/models/asset_variants.js +++ b/backend/src/db/models/asset_variants.ts @@ -1,5 +1,19 @@ -module.exports = function (sequelize, DataTypes) { - const asset_variants = sequelize.define( +import type { + SequelizeModel, + SequelizeModelFactory, +} from '../../types/index.ts'; + +function validateUrlOrEmpty(value: string | null | undefined): void { + if (value && value.length > 0 && !/^https?:\/\/.+/.test(value)) { + throw new Error('CDN URL must be a valid URL'); + } +} + +const defineAssetVariantsModel: SequelizeModelFactory = ( + sequelize, + DataTypes, +) => { + const assetVariants: SequelizeModel = sequelize.define( 'asset_variants', { id: { @@ -40,11 +54,7 @@ module.exports = function (sequelize, DataTypes) { args: [0, 2048], msg: 'CDN URL must be at most 2048 characters', }, - isUrlOrEmpty(value) { - if (value && value.length > 0 && !/^https?:\/\/.+/.test(value)) { - throw new Error('CDN URL must be a valid URL'); - } - }, + isUrlOrEmpty: validateUrlOrEmpty, }, }, @@ -82,11 +92,7 @@ module.exports = function (sequelize, DataTypes) { }, ); - asset_variants.associate = (db) => { - /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - //end loop - + assetVariants.associate = (db) => { db.asset_variants.belongsTo(db.assets, { as: 'asset', foreignKey: { @@ -106,5 +112,7 @@ module.exports = function (sequelize, DataTypes) { }); }; - return asset_variants; + return assetVariants; }; + +export default defineAssetVariantsModel; diff --git a/backend/src/db/models/assets.js b/backend/src/db/models/assets.ts similarity index 92% rename from backend/src/db/models/assets.js rename to backend/src/db/models/assets.ts index 08ded09..dafb588 100644 --- a/backend/src/db/models/assets.js +++ b/backend/src/db/models/assets.ts @@ -1,5 +1,10 @@ -module.exports = function (sequelize, DataTypes) { - const assets = sequelize.define( +import type { + SequelizeModel, + SequelizeModelFactory, +} from '../../types/index.ts'; + +const defineAssetsModel: SequelizeModelFactory = (sequelize, DataTypes) => { + const assets: SequelizeModel = sequelize.define( 'assets', { id: { @@ -137,8 +142,6 @@ module.exports = function (sequelize, DataTypes) { ); assets.associate = (db) => { - /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - db.assets.hasMany(db.asset_variants, { as: 'asset_variants_asset', foreignKey: { @@ -149,8 +152,6 @@ module.exports = function (sequelize, DataTypes) { onUpdate: 'CASCADE', }); - //end loop - db.assets.belongsTo(db.projects, { as: 'project', foreignKey: { @@ -172,3 +173,5 @@ module.exports = function (sequelize, DataTypes) { return assets; }; + +export default defineAssetsModel; diff --git a/backend/src/db/models/element_type_defaults.js b/backend/src/db/models/element_type_defaults.ts similarity index 72% rename from backend/src/db/models/element_type_defaults.js rename to backend/src/db/models/element_type_defaults.ts index 70b3e21..8a0a99e 100644 --- a/backend/src/db/models/element_type_defaults.js +++ b/backend/src/db/models/element_type_defaults.ts @@ -1,5 +1,13 @@ -module.exports = function (sequelize, DataTypes) { - const element_type_defaults = sequelize.define( +import type { + SequelizeModel, + SequelizeModelFactory, +} from '../../types/index.ts'; + +const defineElementTypeDefaultsModel: SequelizeModelFactory = ( + sequelize, + DataTypes, +) => { + const elementTypeDefaults: SequelizeModel = sequelize.define( 'element_type_defaults', { id: { @@ -64,7 +72,7 @@ module.exports = function (sequelize, DataTypes) { }, ); - element_type_defaults.associate = (db) => { + elementTypeDefaults.associate = (db) => { db.element_type_defaults.belongsTo(db.users, { as: 'createdBy', }); @@ -73,19 +81,18 @@ module.exports = function (sequelize, DataTypes) { as: 'updatedBy', }); - // Add hasMany relationship to project_element_defaults - if (db.project_element_defaults) { - db.element_type_defaults.hasMany(db.project_element_defaults, { - as: 'project_defaults', - foreignKey: { - name: 'source_element_id', - }, - constraints: true, - onDelete: 'SET NULL', - onUpdate: 'CASCADE', - }); - } + db.element_type_defaults.hasMany(db.project_element_defaults, { + as: 'project_defaults', + foreignKey: { + name: 'source_element_id', + }, + constraints: true, + onDelete: 'SET NULL', + onUpdate: 'CASCADE', + }); }; - return element_type_defaults; + return elementTypeDefaults; }; + +export default defineElementTypeDefaultsModel; diff --git a/backend/src/db/models/file.js b/backend/src/db/models/file.ts similarity index 80% rename from backend/src/db/models/file.js rename to backend/src/db/models/file.ts index 84ee670..7cb79bc 100644 --- a/backend/src/db/models/file.js +++ b/backend/src/db/models/file.ts @@ -1,5 +1,10 @@ -module.exports = function (sequelize, DataTypes) { - const file = sequelize.define( +import type { + SequelizeModel, + SequelizeModelFactory, +} from '../../types/index.ts'; + +const defineFileModel: SequelizeModelFactory = (sequelize, DataTypes) => { + const file: SequelizeModel = sequelize.define( 'file', { id: { @@ -51,3 +56,5 @@ module.exports = function (sequelize, DataTypes) { return file; }; + +export default defineFileModel; diff --git a/backend/src/db/models/global_transition_defaults.js b/backend/src/db/models/global_transition_defaults.ts similarity index 80% rename from backend/src/db/models/global_transition_defaults.js rename to backend/src/db/models/global_transition_defaults.ts index c6a67e8..dc43f42 100644 --- a/backend/src/db/models/global_transition_defaults.js +++ b/backend/src/db/models/global_transition_defaults.ts @@ -1,5 +1,13 @@ -module.exports = function (sequelize, DataTypes) { - const global_transition_defaults = sequelize.define( +import type { + SequelizeModel, + SequelizeModelFactory, +} from '../../types/index.ts'; + +const defineGlobalTransitionDefaultsModel: SequelizeModelFactory = ( + sequelize, + DataTypes, +) => { + const globalTransitionDefaults: SequelizeModel = sequelize.define( 'global_transition_defaults', { id: { @@ -59,7 +67,7 @@ module.exports = function (sequelize, DataTypes) { }, ); - global_transition_defaults.associate = (db) => { + globalTransitionDefaults.associate = (db) => { db.global_transition_defaults.belongsTo(db.users, { as: 'createdBy', }); @@ -69,5 +77,7 @@ module.exports = function (sequelize, DataTypes) { }); }; - return global_transition_defaults; + return globalTransitionDefaults; }; + +export default defineGlobalTransitionDefaultsModel; diff --git a/backend/src/db/models/global_ui_control_defaults.js b/backend/src/db/models/global_ui_control_defaults.ts similarity index 58% rename from backend/src/db/models/global_ui_control_defaults.js rename to backend/src/db/models/global_ui_control_defaults.ts index 3144170..32d8250 100644 --- a/backend/src/db/models/global_ui_control_defaults.js +++ b/backend/src/db/models/global_ui_control_defaults.ts @@ -1,5 +1,13 @@ -module.exports = function (sequelize, DataTypes) { - const global_ui_control_defaults = sequelize.define( +import type { + SequelizeModel, + SequelizeModelFactory, +} from '../../types/index.ts'; + +const defineGlobalUiControlDefaultsModel: SequelizeModelFactory = ( + sequelize, + DataTypes, +) => { + const globalUiControlDefaults: SequelizeModel = sequelize.define( 'global_ui_control_defaults', { id: { @@ -20,7 +28,7 @@ module.exports = function (sequelize, DataTypes) { }, ); - global_ui_control_defaults.associate = (db) => { + globalUiControlDefaults.associate = (db) => { db.global_ui_control_defaults.belongsTo(db.users, { as: 'createdBy', }); @@ -30,5 +38,7 @@ module.exports = function (sequelize, DataTypes) { }); }; - return global_ui_control_defaults; + return globalUiControlDefaults; }; + +export default defineGlobalUiControlDefaultsModel; diff --git a/backend/src/db/models/index.js b/backend/src/db/models/index.js deleted file mode 100644 index 116aeac..0000000 --- a/backend/src/db/models/index.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const Sequelize = require('sequelize'); -const basename = path.basename(__filename); -const env = process.env.NODE_ENV || 'development'; -const config = require('../db.config')[env]; -const db = {}; - -let sequelize; -if (config.use_env_variable) { - sequelize = new Sequelize(process.env[config.use_env_variable], config); -} else { - sequelize = new Sequelize( - config.database, - config.username, - config.password, - config, - ); -} - -fs.readdirSync(__dirname) - .filter((file) => { - return ( - file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js' - ); - }) - .forEach((file) => { - const model = require(path.join(__dirname, file))( - sequelize, - Sequelize.DataTypes, - ); - db[model.name] = model; - }); - -Object.keys(db).forEach((modelName) => { - if (db[modelName].associate) { - db[modelName].associate(db); - } -}); - -db.sequelize = sequelize; -db.Sequelize = Sequelize; - -module.exports = db; diff --git a/backend/src/db/models/index.ts b/backend/src/db/models/index.ts new file mode 100644 index 0000000..cfe1331 --- /dev/null +++ b/backend/src/db/models/index.ts @@ -0,0 +1 @@ +export { default } from './loader.ts'; diff --git a/backend/src/db/models/loader.ts b/backend/src/db/models/loader.ts new file mode 100644 index 0000000..a9d7596 --- /dev/null +++ b/backend/src/db/models/loader.ts @@ -0,0 +1,429 @@ +import * as SequelizeModule from 'sequelize'; +import { DataTypes, Sequelize } from 'sequelize'; + +import '../../load-env.ts'; +import dbConfig from '../db-config.ts'; +import defineAccessLogsModel from './access_logs.ts'; +import defineAssetVariantsModel from './asset_variants.ts'; +import defineAssetsModel from './assets.ts'; +import defineElementTypeDefaultsModel from './element_type_defaults.ts'; +import defineFileModel from './file.ts'; +import defineGlobalTransitionDefaultsModel from './global_transition_defaults.ts'; +import defineGlobalUiControlDefaultsModel from './global_ui_control_defaults.ts'; +import definePermissionsModel from './permissions.ts'; +import definePresignedUrlRequestsModel from './presigned_url_requests.ts'; +import defineProductionPresentationAccessModel from './production_presentation_access.ts'; +import defineProjectAudioTracksModel from './project_audio_tracks.ts'; +import defineProjectElementDefaultsModel from './project_element_defaults.ts'; +import defineProjectMembershipsModel from './project_memberships.ts'; +import defineProjectTransitionSettingsModel from './project_transition_settings.ts'; +import defineProjectUiControlSettingsModel from './project_ui_control_settings.ts'; +import defineProjectsModel from './projects.ts'; +import definePublishEventsModel from './publish_events.ts'; +import definePwaCachesModel from './pwa_caches.ts'; +import defineRolesModel from './roles.ts'; +import defineTourPagesModel from './tour_pages.ts'; +import defineUsersModel from './users.ts'; +import type { + DatabaseEnvironmentConfig, + DbModels, + ElementTypeDefaultsModel, + FileModel, + GlobalTransitionDefaultsModel, + GlobalUiControlDefaultsModel, + PermissionModel, + ProductionPresentationAccessModel, + ProjectElementDefaultsModel, + ProjectModel, + ProjectTransitionSettingsModel, + ProjectUiControlSettingsModel, + RoleModel, + SampleDataModel, + SequelizeModelRegistry, + TourPageModel, + UserModel, +} from '../../types/index.ts'; + +type FunctionPropertyName = + | 'bulkCreate' + | 'cast' + | 'close' + | 'col' + | 'count' + | 'create' + | 'destroy' + | 'findAll' + | 'findAndCountAll' + | 'findByPk' + | 'findOne' + | 'findOrCreate' + | 'max' + | 'query' + | 'sync' + | 'transaction' + | 'where'; + +function isDatabaseConfigEnvironment( + value: string, +): value is keyof typeof dbConfig { + return value === 'development' || value === 'production' || value === 'dev_stage'; +} + +function getDatabaseConfig(): DatabaseEnvironmentConfig { + const env = process.env.NODE_ENV || 'development'; + if (!isDatabaseConfigEnvironment(env)) { + throw new Error(`Unsupported database environment '${env}'.`); + } + + return dbConfig[env]; +} + +function createSequelize(config: DatabaseEnvironmentConfig): Sequelize { + if (config.use_env_variable) { + const connectionString = process.env[config.use_env_variable]; + if (!connectionString) { + throw new Error( + `Database environment variable '${config.use_env_variable}' is not set.`, + ); + } + return new Sequelize(connectionString, config); + } + + return new Sequelize( + config.database || '', + config.username || '', + config.password || '', + config, + ); +} + +function hasFunctionProperty( + value: object, + property: FunctionPropertyName, +): boolean { + return typeof Reflect.get(value, property) === 'function'; +} + +function hasFunctionProperties( + value: object, + properties: readonly FunctionPropertyName[], +): boolean { + return properties.every((property) => hasFunctionProperty(value, property)); +} + +function isSampleDataModel(value: object): value is SampleDataModel { + return hasFunctionProperties(value, ['bulkCreate', 'count', 'findOne']); +} + +function isProjectModel(value: object): value is ProjectModel & SampleDataModel { + return hasFunctionProperties(value, [ + 'bulkCreate', + 'count', + 'create', + 'destroy', + 'findAll', + 'findByPk', + 'findOne', + ]); +} + +function isProjectCloneAssetModel( + value: object, +): value is DbModels['assets'] { + return hasFunctionProperties(value, [ + 'bulkCreate', + 'count', + 'create', + 'findOne', + ]); +} + +function isProjectCloneVariantModel( + value: object, +): value is DbModels['asset_variants'] { + return hasFunctionProperties(value, [ + 'bulkCreate', + 'count', + 'create', + 'findOne', + ]); +} + +function isPublishEventModel(value: object): value is DbModels['publish_events'] { + return hasFunctionProperties(value, [ + 'bulkCreate', + 'count', + 'create', + 'findOne', + ]); +} + +function isTourPageModel(value: object): value is TourPageModel & SampleDataModel { + return hasFunctionProperties(value, [ + 'bulkCreate', + 'count', + 'create', + 'destroy', + 'findAll', + 'findAndCountAll', + 'findOne', + 'max', + ]); +} + +function isProjectAudioTrackModel( + value: object, +): value is DbModels['project_audio_tracks'] { + return hasFunctionProperties(value, [ + 'bulkCreate', + 'count', + 'create', + 'destroy', + 'findAll', + 'findOne', + ]); +} + +function isProjectElementDefaultsModel( + value: object, +): value is ProjectElementDefaultsModel & DbModels['project_element_defaults'] { + return hasFunctionProperties(value, [ + 'create', + 'destroy', + 'findAll', + 'findOne', + ]); +} + +function isProjectTransitionSettingsModel( + value: object, +): value is ProjectTransitionSettingsModel & + DbModels['project_transition_settings'] { + return hasFunctionProperties(value, ['create', 'destroy', 'findOne']); +} + +function isProjectUiControlSettingsModel( + value: object, +): value is ProjectUiControlSettingsModel & + DbModels['project_ui_control_settings'] { + return hasFunctionProperties(value, ['create', 'destroy', 'findOne']); +} + +function isElementTypeDefaultsModel( + value: object, +): value is ElementTypeDefaultsModel { + return hasFunctionProperties(value, ['bulkCreate', 'findAll']); +} + +function isGlobalTransitionDefaultsModel( + value: object, +): value is GlobalTransitionDefaultsModel { + return hasFunctionProperties(value, ['bulkCreate', 'destroy']); +} + +function isGlobalUiControlDefaultsModel( + value: object, +): value is GlobalUiControlDefaultsModel { + return hasFunctionProperties(value, ['bulkCreate', 'destroy']); +} + +function isFileModel(value: object): value is FileModel { + return hasFunctionProperties(value, ['create']); +} + +function isProductionPresentationAccessModel( + value: object, +): value is ProductionPresentationAccessModel { + return hasFunctionProperties(value, [ + 'bulkCreate', + 'create', + 'destroy', + 'findAll', + 'findOne', + ]); +} + +function isRoleModel(value: object): value is RoleModel { + return hasFunctionProperties(value, ['create', 'findAll', 'findByPk', 'findOne']); +} + +function isPermissionModel(value: object): value is PermissionModel { + return hasFunctionProperties(value, ['create']); +} + +function isUserModel(value: object): value is UserModel & SampleDataModel { + return hasFunctionProperties(value, [ + 'bulkCreate', + 'count', + 'findAll', + 'findByPk', + 'findOne', + 'findOrCreate', + ]); +} + +function requireSampleDataModel(name: string, value: object): SampleDataModel { + if (isSampleDataModel(value)) return value; + throw new Error(`Model '${name}' does not satisfy SampleDataModel contract.`); +} + +function requireModel( + name: string, + value: object, + guard: (model: object) => model is T, +): T { + if (guard(value)) return value; + throw new Error(`Model '${name}' does not satisfy its typed DB contract.`); +} + +const sequelize = createSequelize(getDatabaseConfig()); + +const models: SequelizeModelRegistry = { + access_logs: defineAccessLogsModel(sequelize, DataTypes), + asset_variants: defineAssetVariantsModel(sequelize, DataTypes), + assets: defineAssetsModel(sequelize, DataTypes), + element_type_defaults: defineElementTypeDefaultsModel(sequelize, DataTypes), + file: defineFileModel(sequelize, DataTypes), + global_transition_defaults: defineGlobalTransitionDefaultsModel( + sequelize, + DataTypes, + ), + global_ui_control_defaults: defineGlobalUiControlDefaultsModel( + sequelize, + DataTypes, + ), + permissions: definePermissionsModel(sequelize, DataTypes), + presigned_url_requests: definePresignedUrlRequestsModel(sequelize, DataTypes), + production_presentation_access: defineProductionPresentationAccessModel( + sequelize, + DataTypes, + ), + project_audio_tracks: defineProjectAudioTracksModel(sequelize, DataTypes), + project_element_defaults: defineProjectElementDefaultsModel( + sequelize, + DataTypes, + ), + project_memberships: defineProjectMembershipsModel(sequelize, DataTypes), + project_transition_settings: defineProjectTransitionSettingsModel( + sequelize, + DataTypes, + ), + project_ui_control_settings: defineProjectUiControlSettingsModel( + sequelize, + DataTypes, + ), + projects: defineProjectsModel(sequelize, DataTypes), + publish_events: definePublishEventsModel(sequelize, DataTypes), + pwa_caches: definePwaCachesModel(sequelize, DataTypes), + roles: defineRolesModel(sequelize, DataTypes), + tour_pages: defineTourPagesModel(sequelize, DataTypes), + users: defineUsersModel(sequelize, DataTypes), +}; + +const modelList = [ + models.access_logs, + models.asset_variants, + models.assets, + models.element_type_defaults, + models.file, + models.global_transition_defaults, + models.global_ui_control_defaults, + models.permissions, + models.presigned_url_requests, + models.production_presentation_access, + models.project_audio_tracks, + models.project_element_defaults, + models.project_memberships, + models.project_transition_settings, + models.project_ui_control_settings, + models.projects, + models.publish_events, + models.pwa_caches, + models.roles, + models.tour_pages, + models.users, +]; + +for (const model of modelList) { + if (model.associate) { + model.associate(models); + } +} + +const db: DbModels = { + sequelize, + Sequelize: SequelizeModule, + access_logs: requireSampleDataModel('access_logs', models.access_logs), + asset_variants: requireModel( + 'asset_variants', + models.asset_variants, + isProjectCloneVariantModel, + ), + assets: requireModel('assets', models.assets, isProjectCloneAssetModel), + element_type_defaults: requireModel( + 'element_type_defaults', + models.element_type_defaults, + isElementTypeDefaultsModel, + ), + file: requireModel('file', models.file, isFileModel), + global_transition_defaults: requireModel( + 'global_transition_defaults', + models.global_transition_defaults, + isGlobalTransitionDefaultsModel, + ), + global_ui_control_defaults: requireModel( + 'global_ui_control_defaults', + models.global_ui_control_defaults, + isGlobalUiControlDefaultsModel, + ), + permissions: requireModel( + 'permissions', + models.permissions, + isPermissionModel, + ), + presigned_url_requests: requireSampleDataModel( + 'presigned_url_requests', + models.presigned_url_requests, + ), + production_presentation_access: requireModel( + 'production_presentation_access', + models.production_presentation_access, + isProductionPresentationAccessModel, + ), + project_audio_tracks: requireModel( + 'project_audio_tracks', + models.project_audio_tracks, + isProjectAudioTrackModel, + ), + project_element_defaults: requireModel( + 'project_element_defaults', + models.project_element_defaults, + isProjectElementDefaultsModel, + ), + project_memberships: requireSampleDataModel( + 'project_memberships', + models.project_memberships, + ), + project_transition_settings: requireModel( + 'project_transition_settings', + models.project_transition_settings, + isProjectTransitionSettingsModel, + ), + project_ui_control_settings: requireModel( + 'project_ui_control_settings', + models.project_ui_control_settings, + isProjectUiControlSettingsModel, + ), + projects: requireModel('projects', models.projects, isProjectModel), + publish_events: requireModel( + 'publish_events', + models.publish_events, + isPublishEventModel, + ), + pwa_caches: requireSampleDataModel('pwa_caches', models.pwa_caches), + roles: requireModel('roles', models.roles, isRoleModel), + tour_pages: requireModel('tour_pages', models.tour_pages, isTourPageModel), + users: requireModel('users', models.users, isUserModel), +}; + +export default db; diff --git a/backend/src/db/models/permissions.js b/backend/src/db/models/permissions.ts similarity index 77% rename from backend/src/db/models/permissions.js rename to backend/src/db/models/permissions.ts index b0c6ef0..d275f13 100644 --- a/backend/src/db/models/permissions.js +++ b/backend/src/db/models/permissions.ts @@ -1,5 +1,10 @@ -module.exports = function (sequelize, DataTypes) { - const permissions = sequelize.define( +import type { + SequelizeModel, + SequelizeModelFactory, +} from '../../types/index.ts'; + +const definePermissionsModel: SequelizeModelFactory = (sequelize, DataTypes) => { + const permissions: SequelizeModel = sequelize.define( 'permissions', { id: { @@ -35,10 +40,6 @@ module.exports = function (sequelize, DataTypes) { ); permissions.associate = (db) => { - /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - //end loop - db.permissions.belongsTo(db.users, { as: 'createdBy', }); @@ -50,3 +51,5 @@ module.exports = function (sequelize, DataTypes) { return permissions; }; + +export default definePermissionsModel; diff --git a/backend/src/db/models/presigned_url_requests.js b/backend/src/db/models/presigned_url_requests.ts similarity index 75% rename from backend/src/db/models/presigned_url_requests.js rename to backend/src/db/models/presigned_url_requests.ts index 46baf02..de2b9a7 100644 --- a/backend/src/db/models/presigned_url_requests.js +++ b/backend/src/db/models/presigned_url_requests.ts @@ -1,5 +1,19 @@ -module.exports = function (sequelize, DataTypes) { - const presigned_url_requests = sequelize.define( +import type { + SequelizeModel, + SequelizeModelFactory, +} from '../../types/index.ts'; + +function validateMimeTypeOrEmpty(value: string | null | undefined): void { + if (value && value.length > 0 && !/^[\w.-]+\/[\w.+-]+$/.test(value)) { + throw new Error('MIME type must be in format type/subtype'); + } +} + +const definePresignedUrlRequestsModel: SequelizeModelFactory = ( + sequelize, + DataTypes, +) => { + const presignedUrlRequests: SequelizeModel = sequelize.define( 'presigned_url_requests', { id: { @@ -37,15 +51,7 @@ module.exports = function (sequelize, DataTypes) { args: [0, 255], msg: 'MIME type must be at most 255 characters', }, - isMimeTypeOrEmpty(value) { - if ( - value && - value.length > 0 && - !/^[\w.-]+\/[\w.+-]+$/.test(value) - ) { - throw new Error('MIME type must be in format type/subtype'); - } - }, + isMimeTypeOrEmpty: validateMimeTypeOrEmpty, }, }, @@ -80,11 +86,7 @@ module.exports = function (sequelize, DataTypes) { }, ); - presigned_url_requests.associate = (db) => { - /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - //end loop - + presignedUrlRequests.associate = (db) => { db.presigned_url_requests.belongsTo(db.projects, { as: 'project', foreignKey: { @@ -114,5 +116,7 @@ module.exports = function (sequelize, DataTypes) { }); }; - return presigned_url_requests; + return presignedUrlRequests; }; + +export default definePresignedUrlRequestsModel; diff --git a/backend/src/db/models/production_presentation_access.js b/backend/src/db/models/production_presentation_access.ts similarity index 73% rename from backend/src/db/models/production_presentation_access.js rename to backend/src/db/models/production_presentation_access.ts index 6933fda..ba1a0b8 100644 --- a/backend/src/db/models/production_presentation_access.js +++ b/backend/src/db/models/production_presentation_access.ts @@ -1,5 +1,13 @@ -module.exports = function (sequelize, DataTypes) { - const production_presentation_access = sequelize.define( +import type { + SequelizeModel, + SequelizeModelFactory, +} from '../../types/index.ts'; + +const defineProductionPresentationAccessModel: SequelizeModelFactory = ( + sequelize, + DataTypes, +) => { + const productionPresentationAccess: SequelizeModel = sequelize.define( 'production_presentation_access', { id: { @@ -26,7 +34,7 @@ module.exports = function (sequelize, DataTypes) { }, ); - production_presentation_access.associate = (db) => { + productionPresentationAccess.associate = (db) => { db.production_presentation_access.belongsTo(db.projects, { as: 'project', foreignKey: { @@ -56,5 +64,7 @@ module.exports = function (sequelize, DataTypes) { }); }; - return production_presentation_access; + return productionPresentationAccess; }; + +export default defineProductionPresentationAccessModel; diff --git a/backend/src/db/models/project_audio_tracks.js b/backend/src/db/models/project_audio_tracks.ts similarity index 82% rename from backend/src/db/models/project_audio_tracks.js rename to backend/src/db/models/project_audio_tracks.ts index f38fd55..039e2d8 100644 --- a/backend/src/db/models/project_audio_tracks.js +++ b/backend/src/db/models/project_audio_tracks.ts @@ -1,5 +1,13 @@ -module.exports = function (sequelize, DataTypes) { - const project_audio_tracks = sequelize.define( +import type { + SequelizeModel, + SequelizeModelFactory, +} from '../../types/index.ts'; + +const defineProjectAudioTracksModel: SequelizeModelFactory = ( + sequelize, + DataTypes, +) => { + const projectAudioTracks: SequelizeModel = sequelize.define( 'project_audio_tracks', { id: { @@ -75,11 +83,7 @@ module.exports = function (sequelize, DataTypes) { }, ); - project_audio_tracks.associate = (db) => { - /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - //end loop - + projectAudioTracks.associate = (db) => { db.project_audio_tracks.belongsTo(db.projects, { as: 'project', foreignKey: { @@ -99,5 +103,7 @@ module.exports = function (sequelize, DataTypes) { }); }; - return project_audio_tracks; + return projectAudioTracks; }; + +export default defineProjectAudioTracksModel; diff --git a/backend/src/db/models/project_element_defaults.js b/backend/src/db/models/project_element_defaults.ts similarity index 81% rename from backend/src/db/models/project_element_defaults.js rename to backend/src/db/models/project_element_defaults.ts index 4d981d4..52bccc7 100644 --- a/backend/src/db/models/project_element_defaults.js +++ b/backend/src/db/models/project_element_defaults.ts @@ -1,5 +1,13 @@ -module.exports = function (sequelize, DataTypes) { - const project_element_defaults = sequelize.define( +import type { + SequelizeModel, + SequelizeModelFactory, +} from '../../types/index.ts'; + +const defineProjectElementDefaultsModel: SequelizeModelFactory = ( + sequelize, + DataTypes, +) => { + const projectElementDefaults: SequelizeModel = sequelize.define( 'project_element_defaults', { id: { @@ -8,7 +16,6 @@ module.exports = function (sequelize, DataTypes) { primaryKey: true, }, element_type: { - // TEXT for flexibility - matches element_type_defaults and page_elements type: DataTypes.TEXT, allowNull: false, validate: { @@ -35,13 +42,10 @@ module.exports = function (sequelize, DataTypes) { allowNull: true, }, source_element_id: { - // Optional FK - tracks which global default this was snapshotted from - // SET NULL on global delete to preserve project overrides type: DataTypes.UUID, allowNull: true, }, snapshot_version: { - // Increments when resetting from global - enables "check for updates" feature type: DataTypes.INTEGER, allowNull: false, defaultValue: 1, @@ -66,7 +70,7 @@ module.exports = function (sequelize, DataTypes) { }, ); - project_element_defaults.associate = (db) => { + projectElementDefaults.associate = (db) => { db.project_element_defaults.belongsTo(db.projects, { as: 'project', foreignKey: { @@ -97,5 +101,7 @@ module.exports = function (sequelize, DataTypes) { }); }; - return project_element_defaults; + return projectElementDefaults; }; + +export default defineProjectElementDefaultsModel; diff --git a/backend/src/db/models/project_memberships.js b/backend/src/db/models/project_memberships.ts similarity index 81% rename from backend/src/db/models/project_memberships.js rename to backend/src/db/models/project_memberships.ts index 2ef0572..0de8429 100644 --- a/backend/src/db/models/project_memberships.js +++ b/backend/src/db/models/project_memberships.ts @@ -1,5 +1,13 @@ -module.exports = function (sequelize, DataTypes) { - const project_memberships = sequelize.define( +import type { + SequelizeModel, + SequelizeModelFactory, +} from '../../types/index.ts'; + +const defineProjectMembershipsModel: SequelizeModelFactory = ( + sequelize, + DataTypes, +) => { + const projectMemberships: SequelizeModel = sequelize.define( 'project_memberships', { id: { @@ -51,11 +59,7 @@ module.exports = function (sequelize, DataTypes) { }, ); - project_memberships.associate = (db) => { - /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - //end loop - + projectMemberships.associate = (db) => { db.project_memberships.belongsTo(db.projects, { as: 'project', foreignKey: { @@ -85,5 +89,7 @@ module.exports = function (sequelize, DataTypes) { }); }; - return project_memberships; + return projectMemberships; }; + +export default defineProjectMembershipsModel; diff --git a/backend/src/db/models/project_transition_settings.js b/backend/src/db/models/project_transition_settings.ts similarity index 84% rename from backend/src/db/models/project_transition_settings.js rename to backend/src/db/models/project_transition_settings.ts index 40bb980..c8efd6d 100644 --- a/backend/src/db/models/project_transition_settings.js +++ b/backend/src/db/models/project_transition_settings.ts @@ -1,5 +1,13 @@ -module.exports = function (sequelize, DataTypes) { - const project_transition_settings = sequelize.define( +import type { + SequelizeModel, + SequelizeModelFactory, +} from '../../types/index.ts'; + +const defineProjectTransitionSettingsModel: SequelizeModelFactory = ( + sequelize, + DataTypes, +) => { + const projectTransitionSettings: SequelizeModel = sequelize.define( 'project_transition_settings', { id: { @@ -79,7 +87,7 @@ module.exports = function (sequelize, DataTypes) { }, ); - project_transition_settings.associate = (db) => { + projectTransitionSettings.associate = (db) => { db.project_transition_settings.belongsTo(db.projects, { as: 'project', foreignKey: { @@ -99,5 +107,7 @@ module.exports = function (sequelize, DataTypes) { }); }; - return project_transition_settings; + return projectTransitionSettings; }; + +export default defineProjectTransitionSettingsModel; diff --git a/backend/src/db/models/project_ui_control_settings.js b/backend/src/db/models/project_ui_control_settings.ts similarity index 75% rename from backend/src/db/models/project_ui_control_settings.js rename to backend/src/db/models/project_ui_control_settings.ts index 915b6aa..4e53bdd 100644 --- a/backend/src/db/models/project_ui_control_settings.js +++ b/backend/src/db/models/project_ui_control_settings.ts @@ -1,5 +1,13 @@ -module.exports = function (sequelize, DataTypes) { - const project_ui_control_settings = sequelize.define( +import type { + SequelizeModel, + SequelizeModelFactory, +} from '../../types/index.ts'; + +const defineProjectUiControlSettingsModel: SequelizeModelFactory = ( + sequelize, + DataTypes, +) => { + const projectUiControlSettings: SequelizeModel = sequelize.define( 'project_ui_control_settings', { id: { @@ -38,7 +46,7 @@ module.exports = function (sequelize, DataTypes) { }, ); - project_ui_control_settings.associate = (db) => { + projectUiControlSettings.associate = (db) => { db.project_ui_control_settings.belongsTo(db.projects, { as: 'project', foreignKey: { @@ -58,5 +66,7 @@ module.exports = function (sequelize, DataTypes) { }); }; - return project_ui_control_settings; + return projectUiControlSettings; }; + +export default defineProjectUiControlSettingsModel; diff --git a/backend/src/db/models/projects.js b/backend/src/db/models/projects.ts similarity index 92% rename from backend/src/db/models/projects.js rename to backend/src/db/models/projects.ts index cbf95cc..6696a1e 100644 --- a/backend/src/db/models/projects.js +++ b/backend/src/db/models/projects.ts @@ -1,5 +1,10 @@ -module.exports = function (sequelize, DataTypes) { - const projects = sequelize.define( +import type { + SequelizeModel, + SequelizeModelFactory, +} from '../../types/index.ts'; + +const defineProjectsModel: SequelizeModelFactory = (sequelize, DataTypes) => { + const projects: SequelizeModel = sequelize.define( 'projects', { id: { @@ -71,9 +76,6 @@ module.exports = function (sequelize, DataTypes) { defaultValue: 'public', }, - // Note: transition_settings moved to project_transition_settings table - // for environment-aware storage (dev, stage, production) - importHash: { type: DataTypes.STRING(255), allowNull: true, @@ -89,8 +91,6 @@ module.exports = function (sequelize, DataTypes) { ); projects.associate = (db) => { - /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - db.projects.hasMany(db.project_memberships, { as: 'project_memberships_project', foreignKey: { @@ -211,8 +211,6 @@ module.exports = function (sequelize, DataTypes) { onUpdate: 'CASCADE', }); - //end loop - db.projects.belongsTo(db.users, { as: 'createdBy', }); @@ -224,3 +222,5 @@ module.exports = function (sequelize, DataTypes) { return projects; }; + +export default defineProjectsModel; diff --git a/backend/src/db/models/publish_events.js b/backend/src/db/models/publish_events.ts similarity index 89% rename from backend/src/db/models/publish_events.js rename to backend/src/db/models/publish_events.ts index 7c09129..7ea4077 100644 --- a/backend/src/db/models/publish_events.js +++ b/backend/src/db/models/publish_events.ts @@ -1,5 +1,13 @@ -module.exports = function (sequelize, DataTypes) { - const publish_events = sequelize.define( +import type { + SequelizeModel, + SequelizeModelFactory, +} from '../../types/index.ts'; + +const definePublishEventsModel: SequelizeModelFactory = ( + sequelize, + DataTypes, +) => { + const publishEvents: SequelizeModel = sequelize.define( 'publish_events', { id: { @@ -110,11 +118,7 @@ module.exports = function (sequelize, DataTypes) { }, ); - publish_events.associate = (db) => { - /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - //end loop - + publishEvents.associate = (db) => { db.publish_events.belongsTo(db.projects, { as: 'project', foreignKey: { @@ -144,5 +148,7 @@ module.exports = function (sequelize, DataTypes) { }); }; - return publish_events; + return publishEvents; }; + +export default definePublishEventsModel; diff --git a/backend/src/db/models/pwa_caches.js b/backend/src/db/models/pwa_caches.ts similarity index 81% rename from backend/src/db/models/pwa_caches.js rename to backend/src/db/models/pwa_caches.ts index 7f84fc4..c34cf09 100644 --- a/backend/src/db/models/pwa_caches.js +++ b/backend/src/db/models/pwa_caches.ts @@ -1,5 +1,10 @@ -module.exports = function (sequelize, DataTypes) { - const pwa_caches = sequelize.define( +import type { + SequelizeModel, + SequelizeModelFactory, +} from '../../types/index.ts'; + +const definePwaCachesModel: SequelizeModelFactory = (sequelize, DataTypes) => { + const pwaCaches: SequelizeModel = sequelize.define( 'pwa_caches', { id: { @@ -56,11 +61,7 @@ module.exports = function (sequelize, DataTypes) { }, ); - pwa_caches.associate = (db) => { - /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - //end loop - + pwaCaches.associate = (db) => { db.pwa_caches.belongsTo(db.projects, { as: 'project', foreignKey: { @@ -80,5 +81,7 @@ module.exports = function (sequelize, DataTypes) { }); }; - return pwa_caches; + return pwaCaches; }; + +export default definePwaCachesModel; diff --git a/backend/src/db/models/roles.js b/backend/src/db/models/roles.ts similarity index 86% rename from backend/src/db/models/roles.js rename to backend/src/db/models/roles.ts index 6765d3e..9d8c2b4 100644 --- a/backend/src/db/models/roles.js +++ b/backend/src/db/models/roles.ts @@ -1,5 +1,10 @@ -module.exports = function (sequelize, DataTypes) { - const roles = sequelize.define( +import type { + SequelizeModel, + SequelizeModelFactory, +} from '../../types/index.ts'; + +const defineRolesModel: SequelizeModelFactory = (sequelize, DataTypes) => { + const roles: SequelizeModel = sequelize.define( 'roles', { id: { @@ -58,8 +63,6 @@ module.exports = function (sequelize, DataTypes) { through: 'rolesPermissionsPermissions', }); - /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - db.roles.hasMany(db.users, { as: 'users_app_role', foreignKey: { @@ -70,8 +73,6 @@ module.exports = function (sequelize, DataTypes) { onUpdate: 'CASCADE', }); - //end loop - db.roles.belongsTo(db.users, { as: 'createdBy', }); @@ -83,3 +84,5 @@ module.exports = function (sequelize, DataTypes) { return roles; }; + +export default defineRolesModel; diff --git a/backend/src/db/models/tour_pages.js b/backend/src/db/models/tour_pages.ts similarity index 92% rename from backend/src/db/models/tour_pages.js rename to backend/src/db/models/tour_pages.ts index 3924b0b..d62a0e7 100644 --- a/backend/src/db/models/tour_pages.js +++ b/backend/src/db/models/tour_pages.ts @@ -1,5 +1,10 @@ -module.exports = function (sequelize, DataTypes) { - const tour_pages = sequelize.define( +import type { + SequelizeModel, + SequelizeModelFactory, +} from '../../types/index.ts'; + +const defineTourPagesModel: SequelizeModelFactory = (sequelize, DataTypes) => { + const tourPages: SequelizeModel = sequelize.define( 'tour_pages', { id: { @@ -177,11 +182,7 @@ module.exports = function (sequelize, DataTypes) { }, ); - tour_pages.associate = (db) => { - /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - //end loop - + tourPages.associate = (db) => { db.tour_pages.belongsTo(db.projects, { as: 'project', foreignKey: { @@ -201,5 +202,7 @@ module.exports = function (sequelize, DataTypes) { }); }; - return tour_pages; + return tourPages; }; + +export default defineTourPagesModel; diff --git a/backend/src/db/models/users.js b/backend/src/db/models/users.ts similarity index 70% rename from backend/src/db/models/users.js rename to backend/src/db/models/users.ts index e03d8f1..acc19a5 100644 --- a/backend/src/db/models/users.js +++ b/backend/src/db/models/users.ts @@ -1,10 +1,59 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); +import crypto from 'node:crypto'; -module.exports = function (sequelize, DataTypes) { - const users = sequelize.define( +import bcrypt from 'bcrypt'; +import type { Model, ModelStatic } from 'sequelize'; + +import config from '../../config.ts'; +import type { + SequelizeModelFactory, + SequelizeModelRegistry, +} from '../../types/index.ts'; + +const providers = config.providers; + +interface UserModelInstance extends Model { + email: string; + firstName: string | null; + lastName: string | null; + password: string; + provider: string; + emailVerified: boolean; +} + +interface UsersSequelizeModel extends ModelStatic { + associate?: (db: SequelizeModelRegistry) => void; +} + +function isKnownExternalProvider(provider: string): boolean { + return provider !== providers.LOCAL && Object.values(providers).includes(provider); +} + +function trimStringFields(user: UserModelInstance): UserModelInstance { + user.email = user.email.trim(); + + user.firstName = user.firstName ? user.firstName.trim() : null; + + user.lastName = user.lastName ? user.lastName.trim() : null; + + return user; +} + +function ensureExternalProviderPassword(user: UserModelInstance): void { + if (!isKnownExternalProvider(user.provider)) return; + + user.emailVerified = true; + + if (!user.password) { + const password = crypto.randomBytes(20).toString('hex'); + + const hashedPassword = bcrypt.hashSync(password, config.bcrypt.saltRounds); + + user.password = hashedPassword; + } +} + +const defineUsersModel: SequelizeModelFactory = (sequelize, DataTypes) => { + const users: UsersSequelizeModel = sequelize.define( 'users', { id: { @@ -115,8 +164,6 @@ module.exports = function (sequelize, DataTypes) { through: 'usersCustom_permissionsPermissions', }); - /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - db.users.hasMany(db.project_memberships, { as: 'project_memberships_user', foreignKey: { @@ -167,8 +214,6 @@ module.exports = function (sequelize, DataTypes) { onUpdate: 'CASCADE', }); - //end loop - db.users.belongsTo(db.roles, { as: 'app_role', foreignKey: { @@ -200,41 +245,16 @@ module.exports = function (sequelize, DataTypes) { }); }; - users.beforeCreate((users) => { - users = trimStringFields(users); - - if ( - users.provider !== providers.LOCAL && - Object.values(providers).indexOf(users.provider) > -1 - ) { - users.emailVerified = true; - - if (!users.password) { - const password = crypto.randomBytes(20).toString('hex'); - - const hashedPassword = bcrypt.hashSync( - password, - config.bcrypt.saltRounds, - ); - - users.password = hashedPassword; - } - } + users.beforeCreate((user: UserModelInstance) => { + trimStringFields(user); + ensureExternalProviderPassword(user); }); - users.beforeUpdate((users) => { - trimStringFields(users); + users.beforeUpdate((user: UserModelInstance) => { + trimStringFields(user); }); return users; }; -function trimStringFields(users) { - users.email = users.email.trim(); - - users.firstName = users.firstName ? users.firstName.trim() : null; - - users.lastName = users.lastName ? users.lastName.trim() : null; - - return users; -} +export default defineUsersModel; diff --git a/backend/src/db/reset.js b/backend/src/db/reset.js deleted file mode 100644 index bc0b5f9..0000000 --- a/backend/src/db/reset.js +++ /dev/null @@ -1,16 +0,0 @@ -const db = require('./models'); -const { execSync } = require('child_process'); - -console.log('Resetting Database'); - -db.sequelize - .sync({ force: true }) - .then(() => { - execSync('sequelize db:seed:all'); - console.log('OK'); - process.exit(); - }) - .catch((error) => { - console.error(error); - process.exit(1); - }); diff --git a/backend/src/db/reset.ts b/backend/src/db/reset.ts new file mode 100644 index 0000000..3c88cd1 --- /dev/null +++ b/backend/src/db/reset.ts @@ -0,0 +1,19 @@ +import { execSync } from 'node:child_process'; + +import db from './models/index.ts'; + +async function resetDatabase(): Promise { + console.log('Resetting Database'); + + try { + await db.sequelize.sync({ force: true }); + execSync('sequelize db:seed:all'); + console.log('OK'); + process.exit(0); + } catch (error) { + console.error(error); + process.exit(1); + } +} + +void resetDatabase(); diff --git a/backend/src/db/seeders/20200430130759-admin-user.js b/backend/src/db/seeders/20200430130759-admin-user.js deleted file mode 100644 index bb6ec0b..0000000 --- a/backend/src/db/seeders/20200430130759-admin-user.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict'; -const bcrypt = require('bcrypt'); -const config = require('../../config'); - -const ids = [ - '193bf4b5-9f07-4bd5-9a43-e7e41f3e96af', - 'af5a87be-8f9c-4630-902a-37a60b7005ba', - '5bc531ab-611f-41f3-9373-b7cc5d09c93d', -]; - -module.exports = { - up: async (queryInterface) => { - let admin_hash = bcrypt.hashSync( - config.admin_pass, - config.bcrypt.saltRounds, - ); - let user_hash = bcrypt.hashSync(config.user_pass, config.bcrypt.saltRounds); - - try { - await queryInterface.bulkInsert('users', [ - { - id: ids[0], - firstName: 'Admin', - email: config.admin_email, - emailVerified: true, - provider: config.providers.LOCAL, - password: admin_hash, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: ids[1], - firstName: 'John', - email: 'john@doe.com', - emailVerified: true, - provider: config.providers.LOCAL, - password: user_hash, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: ids[2], - firstName: 'Client', - email: 'client@hello.com', - emailVerified: true, - provider: config.providers.LOCAL, - password: user_hash, - createdAt: new Date(), - updatedAt: new Date(), - }, - ]); - } catch (error) { - console.error('Error during bulkInsert:', error); - throw error; - } - }, - down: async (queryInterface, Sequelize) => { - try { - await queryInterface.bulkDelete( - 'users', - { - id: { - [Sequelize.Op.in]: ids, - }, - }, - {}, - ); - } catch (error) { - console.error('Error during bulkDelete:', error); - throw error; - } - }, -}; diff --git a/backend/src/db/seeders/20200430130759-admin-user.ts b/backend/src/db/seeders/20200430130759-admin-user.ts new file mode 100644 index 0000000..4111b9c --- /dev/null +++ b/backend/src/db/seeders/20200430130759-admin-user.ts @@ -0,0 +1,92 @@ +import bcrypt from 'bcrypt'; +import { Op, QueryTypes } from 'sequelize'; + +import config from '../../config.ts'; +import type { + AdminUserSeedExistingIdRow, + AdminUserSeedRow, + SequelizeSeeder, +} from '../../types/index.ts'; + +const seedUserIds: readonly [string, string, string] = [ + '193bf4b5-9f07-4bd5-9a43-e7e41f3e96af', + 'af5a87be-8f9c-4630-902a-37a60b7005ba', + '5bc531ab-611f-41f3-9373-b7cc5d09c93d', +]; + +function createAdminUserRows(): AdminUserSeedRow[] { + const adminHash = bcrypt.hashSync( + config.admin_pass, + config.bcrypt.saltRounds, + ); + const userHash = bcrypt.hashSync(config.user_pass, config.bcrypt.saltRounds); + const now = new Date(); + + return [ + { + id: seedUserIds[0], + firstName: 'Admin', + email: config.admin_email, + emailVerified: true, + provider: config.providers.LOCAL, + password: adminHash, + createdAt: now, + updatedAt: now, + }, + { + id: seedUserIds[1], + firstName: 'John', + email: 'john@doe.com', + emailVerified: true, + provider: config.providers.LOCAL, + password: userHash, + createdAt: now, + updatedAt: now, + }, + { + id: seedUserIds[2], + firstName: 'Client', + email: 'client@hello.com', + emailVerified: true, + provider: config.providers.LOCAL, + password: userHash, + createdAt: now, + updatedAt: now, + }, + ]; +} + +const adminUserSeeder: SequelizeSeeder = { + async up(queryInterface) { + const existingRows = + await queryInterface.sequelize.query( + 'SELECT "id" FROM "users" WHERE "id" IN (:ids)', + { + replacements: { ids: seedUserIds }, + type: QueryTypes.SELECT, + }, + ); + const existingIds = new Set(existingRows.map((row) => row.id)); + const rowsToInsert = createAdminUserRows().filter( + (row) => !existingIds.has(row.id), + ); + + if (rowsToInsert.length > 0) { + await queryInterface.bulkInsert('users', rowsToInsert); + } + }, + + async down(queryInterface) { + await queryInterface.bulkDelete( + 'users', + { + id: { + [Op.in]: seedUserIds, + }, + }, + {}, + ); + }, +}; + +export default adminUserSeeder; diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.ts similarity index 90% rename from backend/src/db/seeders/20200430130760-user-roles.js rename to backend/src/db/seeders/20200430130760-user-roles.ts index dc7cfd9..bdc7dc7 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.ts @@ -1,80 +1,91 @@ -const { v4: uuid } = require('uuid'); +import type { QueryInterface } from 'sequelize'; +import { QueryTypes } from 'sequelize'; +import { v4 as uuid } from 'uuid'; -module.exports = { - /** - * @param{import("sequelize").QueryInterface} queryInterface - * @return {Promise} - */ - async up(queryInterface) { +import type { + RbacSeedExistingNamedIdRow, + RbacSeedExistingRolePermissionRow, + RbacSeedPermissionRow, + RbacSeedRoleDefinition, + RbacSeedRolePermissionRow, + RbacSeedRoleRow, + SequelizeSeeder, +} from '../../types/index.ts'; + +function toRolePermissionKey(row: { + roles_permissionsId: string; + permissionId: string; +}): string { + return `${row.roles_permissionsId}:${row.permissionId}`; +} + +const userRolesSeeder: SequelizeSeeder = { + async up(queryInterface: QueryInterface) { const createdAt = new Date(); const updatedAt = new Date(); - /** @type {Map} */ - const idMap = new Map(); + const idMap = new Map(); - /** - * @param {string} key - * @return {string} - */ - function getId(key) { - if (idMap.has(key)) { - return idMap.get(key); + function getId(key: string): string { + const existingId = idMap.get(key); + if (existingId) { + return existingId; } + const id = uuid(); idMap.set(key, id); return id; } - await queryInterface.bulkInsert('roles', [ - { - id: getId('Administrator'), - name: 'Administrator', - createdAt, - updatedAt, - }, + const roleDefinitions: RbacSeedRoleDefinition[] = [ + { key: 'Administrator', name: 'Administrator' }, + { key: 'PlatformOwner', name: 'Platform Owner' }, + { key: 'AccountManager', name: 'Account Manager' }, + { key: 'TourDesigner', name: 'Tour Designer' }, + { key: 'ContentReviewer', name: 'Content Reviewer' }, + { key: 'AnalyticsViewer', name: 'Analytics Viewer' }, + { key: 'Public', name: 'Public' }, + ]; + const roleKeyByName = new Map( + roleDefinitions.map((role) => [role.name, role.key]), + ); + const existingRoles = + await queryInterface.sequelize.query( + `SELECT DISTINCT ON ("name") "id", "name" + FROM "roles" + WHERE "name" IN (:names) + ORDER BY "name", "createdAt" ASC`, + { + replacements: { + names: roleDefinitions.map((role) => role.name), + }, + type: QueryTypes.SELECT, + }, + ); - { - id: getId('PlatformOwner'), - name: 'Platform Owner', - createdAt, - updatedAt, - }, + for (const role of existingRoles) { + const key = roleKeyByName.get(role.name); + if (key) { + idMap.set(key, role.id); + } + } - { - id: getId('AccountManager'), - name: 'Account Manager', - createdAt, - updatedAt, - }, + const existingRoleNames = new Set(existingRoles.map((role) => role.name)); + const roleRows: RbacSeedRoleRow[] = roleDefinitions.map((role) => ({ + id: getId(role.key), + name: role.name, + createdAt, + updatedAt, + })); + const rolesToInsert = roleRows.filter( + (role) => !existingRoleNames.has(role.name), + ); - { - id: getId('TourDesigner'), - name: 'Tour Designer', - createdAt, - updatedAt, - }, + if (rolesToInsert.length > 0) { + await queryInterface.bulkInsert('roles', rolesToInsert); + } - { - id: getId('ContentReviewer'), - name: 'Content Reviewer', - createdAt, - updatedAt, - }, - - { - id: getId('AnalyticsViewer'), - name: 'Analytics Viewer', - createdAt, - updatedAt, - }, - - { id: getId('Public'), name: 'Public', createdAt, updatedAt }, - ]); - - /** - * @param {string} name - */ - function createPermissions(name) { + function createPermissions(name: string): RbacSeedPermissionRow[] { return [ { id: getId(`CREATE_${name.toUpperCase()}`), @@ -118,26 +129,49 @@ module.exports = { 'pwa_caches', 'access_logs', ]; - await queryInterface.bulkInsert( - 'permissions', - entities.flatMap(createPermissions), - ); - await queryInterface.bulkInsert('permissions', [ + const permissionRows: RbacSeedPermissionRow[] = [ + ...entities.flatMap(createPermissions), { id: getId(`READ_API_DOCS`), createdAt, updatedAt, name: `READ_API_DOCS`, }, - ]); - await queryInterface.bulkInsert('permissions', [ { id: getId(`CREATE_SEARCH`), createdAt, updatedAt, name: `CREATE_SEARCH`, }, - ]); + ]; + const existingPermissions = + await queryInterface.sequelize.query( + `SELECT DISTINCT ON ("name") "id", "name" + FROM "permissions" + WHERE "name" IN (:names) + ORDER BY "name", "createdAt" ASC`, + { + replacements: { + names: permissionRows.map((permission) => permission.name), + }, + type: QueryTypes.SELECT, + }, + ); + + for (const permission of existingPermissions) { + idMap.set(permission.name, permission.id); + } + + const existingPermissionNames = new Set( + existingPermissions.map((permission) => permission.name), + ); + const permissionsToInsert = permissionRows.filter( + (permission) => !existingPermissionNames.has(permission.name), + ); + + if (permissionsToInsert.length > 0) { + await queryInterface.bulkInsert('permissions', permissionsToInsert); + } await queryInterface.sequelize .query(`CREATE TABLE IF NOT EXISTS "rolesPermissionsPermissions" @@ -159,7 +193,7 @@ constraint "rolesPermissionsPermissions_permission_fk" 'CREATE INDEX IF NOT EXISTS "rolesPermissionsPermissions_permission_idx" ON "rolesPermissionsPermissions" ("permissionId");', ); - await queryInterface.bulkInsert('rolesPermissionsPermissions', [ + const rolePermissionRows: RbacSeedRolePermissionRow[] = [ { createdAt, updatedAt, @@ -1748,7 +1782,29 @@ constraint "rolesPermissionsPermissions_permission_fk" roles_permissionsId: getId('Administrator'), permissionId: getId('CREATE_SEARCH'), }, - ]); + ]; + + const existingRolePermissions = + await queryInterface.sequelize.query( + `SELECT "roles_permissionsId", "permissionId" + FROM "rolesPermissionsPermissions"`, + { + type: QueryTypes.SELECT, + }, + ); + const existingRolePermissionKeys = new Set( + existingRolePermissions.map(toRolePermissionKey), + ); + const rolePermissionsToInsert = rolePermissionRows.filter( + (row) => !existingRolePermissionKeys.has(toRolePermissionKey(row)), + ); + + if (rolePermissionsToInsert.length > 0) { + await queryInterface.bulkInsert( + 'rolesPermissionsPermissions', + rolePermissionsToInsert, + ); + } await queryInterface.sequelize.query( `UPDATE "users" SET "app_roleId"='${getId('SuperAdmin')}' WHERE "email"='super_admin@flatlogic.com'`, @@ -1765,3 +1821,5 @@ constraint "rolesPermissionsPermissions_permission_fk" ); }, }; + +export default userRolesSeeder; diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.ts similarity index 92% rename from backend/src/db/seeders/20231127130745-sample-data.js rename to backend/src/db/seeders/20231127130745-sample-data.ts index 4144aa8..c1e597d 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.ts @@ -1,25 +1,27 @@ -const db = require('../models'); -const Users = db.users; +import db from '../models/index.ts'; +import type { SampleDataModel, SequelizeSeeder } from '../../types/index.ts'; -const Projects = db.projects; +const Users: SampleDataModel = db.users; -const ProjectMemberships = db.project_memberships; +const Projects: SampleDataModel = db.projects; -const Assets = db.assets; +const ProjectMemberships: SampleDataModel = db.project_memberships; -const AssetVariants = db.asset_variants; +const Assets: SampleDataModel = db.assets; -const PresignedUrlRequests = db.presigned_url_requests; +const AssetVariants: SampleDataModel = db.asset_variants; -const TourPages = db.tour_pages; +const PresignedUrlRequests: SampleDataModel = db.presigned_url_requests; -const ProjectAudioTracks = db.project_audio_tracks; +const TourPages: SampleDataModel = db.tour_pages; -const PublishEvents = db.publish_events; +const ProjectAudioTracks: SampleDataModel = db.project_audio_tracks; -const PwaCaches = db.pwa_caches; +const PublishEvents: SampleDataModel = db.publish_events; -const AccessLogs = db.access_logs; +const PwaCaches: SampleDataModel = db.pwa_caches; + +const AccessLogs: SampleDataModel = db.access_logs; const ProjectsData = [ { @@ -1084,7 +1086,7 @@ async function associateAccessLogWithUser() { } } -module.exports = { +const sampleDataSeeder: SequelizeSeeder = { up: async () => { // Keep production-like schema strict; skip auto sample payload inserts by default. if (process.env.ENABLE_SAMPLE_DATA !== 'true') { @@ -1114,31 +1116,31 @@ module.exports = { await Promise.all([ // Similar logic for "relation_many" - await associateProjectMembershipWithProject(), + associateProjectMembershipWithProject(), - await associateProjectMembershipWithUser(), + associateProjectMembershipWithUser(), - await associateAssetWithProject(), + associateAssetWithProject(), - await associateAssetVariantWithAsset(), + associateAssetVariantWithAsset(), - await associatePresignedUrlRequestWithProject(), + associatePresignedUrlRequestWithProject(), - await associatePresignedUrlRequestWithUser(), + associatePresignedUrlRequestWithUser(), - await associateTourPageWithProject(), + associateTourPageWithProject(), - await associateProjectAudioTrackWithProject(), + associateProjectAudioTrackWithProject(), - await associatePublishEventWithProject(), + associatePublishEventWithProject(), - await associatePublishEventWithUser(), + associatePublishEventWithUser(), - await associatePwaCacheWithProject(), + associatePwaCacheWithProject(), - await associateAccessLogWithProject(), + associateAccessLogWithProject(), - await associateAccessLogWithUser(), + associateAccessLogWithUser(), ]); }, @@ -1147,24 +1149,26 @@ module.exports = { return; } - await queryInterface.bulkDelete('projects', null, {}); + await queryInterface.bulkDelete('projects', {}, {}); - await queryInterface.bulkDelete('project_memberships', null, {}); + await queryInterface.bulkDelete('project_memberships', {}, {}); - await queryInterface.bulkDelete('assets', null, {}); + await queryInterface.bulkDelete('assets', {}, {}); - await queryInterface.bulkDelete('asset_variants', null, {}); + await queryInterface.bulkDelete('asset_variants', {}, {}); - await queryInterface.bulkDelete('presigned_url_requests', null, {}); + await queryInterface.bulkDelete('presigned_url_requests', {}, {}); - await queryInterface.bulkDelete('tour_pages', null, {}); + await queryInterface.bulkDelete('tour_pages', {}, {}); - await queryInterface.bulkDelete('project_audio_tracks', null, {}); + await queryInterface.bulkDelete('project_audio_tracks', {}, {}); - await queryInterface.bulkDelete('publish_events', null, {}); + await queryInterface.bulkDelete('publish_events', {}, {}); - await queryInterface.bulkDelete('pwa_caches', null, {}); + await queryInterface.bulkDelete('pwa_caches', {}, {}); - await queryInterface.bulkDelete('access_logs', null, {}); + await queryInterface.bulkDelete('access_logs', {}, {}); }, }; + +export default sampleDataSeeder; diff --git a/backend/src/db/sync.js b/backend/src/db/sync.ts similarity index 69% rename from backend/src/db/sync.js rename to backend/src/db/sync.ts index d844f1e..54e88ce 100644 --- a/backend/src/db/sync.js +++ b/backend/src/db/sync.ts @@ -1,9 +1,9 @@ -const db = require('./models'); +import db from './models/index.ts'; -async function syncDatabase() { +async function syncDatabase(): Promise { if (process.env.NODE_ENV === 'production') { console.error( - 'ERROR: sync.js should not be run in production. Use migrations instead.', + 'ERROR: sync.ts should not be run in production. Use migrations instead.', ); process.exit(1); } @@ -19,4 +19,4 @@ async function syncDatabase() { } } -syncDatabase(); +void syncDatabase(); diff --git a/backend/src/db/umzug.ts b/backend/src/db/umzug.ts new file mode 100644 index 0000000..f21673d --- /dev/null +++ b/backend/src/db/umzug.ts @@ -0,0 +1,420 @@ +import { createRequire } from 'node:module'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +import { Sequelize } from 'sequelize'; +import * as SequelizeModule from 'sequelize'; +import type { QueryInterface } from 'sequelize'; +import { SequelizeStorage, Umzug } from 'umzug'; +import type { + MigrationMeta, + MigrationParams, + RunnableMigration, +} from 'umzug'; + +import '../load-env.ts'; +import dbConfig from './db-config.ts'; +import type { + DatabaseEnvironmentConfig, + SequelizeSeeder, +} from '../types/index.ts'; + +type DatabaseNodeEnvironment = 'production' | 'development' | 'dev_stage'; + +type DbUmzugCommand = + | 'migrate:up' + | 'migrate:down' + | 'migrate:down:all' + | 'migrate:pending' + | 'migrate:executed' + | 'migrate:status' + | 'seed:up' + | 'seed:down' + | 'seed:down:all' + | 'seed:pending' + | 'seed:executed' + | 'seed:status' + | 'db:create' + | 'db:drop'; + +interface DbUmzugContext { + queryInterface: QueryInterface; +} + +interface LegacyMigrationModule { + up: ( + queryInterface: QueryInterface, + sequelizeModule: typeof SequelizeModule, + ) => Promise; + down?: ( + queryInterface: QueryInterface, + sequelizeModule: typeof SequelizeModule, + ) => Promise; +} + +interface DbMigrators { + migrations: Umzug; + seeders: Umzug; +} + +const requireModule = createRequire(import.meta.url); +const migrationTableName = 'SequelizeMeta'; +const seederTableName = 'SequelizeData'; +const runtimeFilePath = fileURLToPath(import.meta.url); +const runtimeDbDirectory = path.dirname(runtimeFilePath); +const runtimeSourceExtension = + path.extname(runtimeFilePath) === '.ts' ? 'ts' : 'js'; +const migrationGlob = path.join(runtimeDbDirectory, 'migrations/*.js'); +const seederGlob = path.join( + runtimeDbDirectory, + `seeders/*.${runtimeSourceExtension}`, +); +const dbCommands: readonly DbUmzugCommand[] = [ + 'migrate:up', + 'migrate:down', + 'migrate:down:all', + 'migrate:pending', + 'migrate:executed', + 'migrate:status', + 'seed:up', + 'seed:down', + 'seed:down:all', + 'seed:pending', + 'seed:executed', + 'seed:status', + 'db:create', + 'db:drop', +]; + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function isDbUmzugCommand(value: string | undefined): value is DbUmzugCommand { + switch (value) { + case 'migrate:up': + case 'migrate:down': + case 'migrate:down:all': + case 'migrate:pending': + case 'migrate:executed': + case 'migrate:status': + case 'seed:up': + case 'seed:down': + case 'seed:down:all': + case 'seed:pending': + case 'seed:executed': + case 'seed:status': + case 'db:create': + case 'db:drop': + return true; + default: + return false; + } +} + +function getModuleDefault(value: unknown): unknown { + if (isRecord(value) && 'default' in value) { + return value.default; + } + + return value; +} + +function isLegacyMigrationModule(value: unknown): value is LegacyMigrationModule { + return ( + isRecord(value) && + typeof value.up === 'function' && + (value.down === undefined || typeof value.down === 'function') + ); +} + +function isSequelizeSeeder(value: unknown): value is SequelizeSeeder { + return ( + isRecord(value) && + typeof value.up === 'function' && + (value.down === undefined || typeof value.down === 'function') + ); +} + +function requireMigrationPath(filePath: string | undefined): string { + if (filePath === undefined) { + throw new Error('Umzug did not provide a migration file path.'); + } + + return filePath; +} + +function loadLegacyMigration(filePath: string): LegacyMigrationModule { + const loadedModule: unknown = requireModule(filePath); + const migrationModule = getModuleDefault(loadedModule); + + if (!isLegacyMigrationModule(migrationModule)) { + throw new Error(`Invalid legacy migration module: ${filePath}`); + } + + return migrationModule; +} + +async function loadSeeder(filePath: string): Promise { + const loadedModule: unknown = await import(pathToFileURL(filePath).href); + const seederModule = getModuleDefault(loadedModule); + + if (!isSequelizeSeeder(seederModule)) { + throw new Error(`Invalid seeder module: ${filePath}`); + } + + return seederModule; +} + +function getSeederStorageName(name: string): string { + return name.replace(/\.ts$/, '.js'); +} + +function resolveLegacyMigration( + params: MigrationParams, +): RunnableMigration { + const migrationPath = requireMigrationPath(params.path); + + return { + name: params.name, + path: migrationPath, + async up({ context }) { + const migration = loadLegacyMigration(migrationPath); + return migration.up(context.queryInterface, SequelizeModule); + }, + async down({ context }) { + const migration = loadLegacyMigration(migrationPath); + return migration.down?.(context.queryInterface, SequelizeModule); + }, + }; +} + +function resolveSeeder( + params: MigrationParams, +): RunnableMigration { + const seederPath = requireMigrationPath(params.path); + const storageName = getSeederStorageName(params.name); + + return { + name: storageName, + path: seederPath, + async up({ context }) { + const seeder = await loadSeeder(seederPath); + return seeder.up(context.queryInterface); + }, + async down({ context }) { + const seeder = await loadSeeder(seederPath); + return seeder.down?.(context.queryInterface); + }, + }; +} + +function getNodeEnvironment(): DatabaseNodeEnvironment { + const environment = process.env.NODE_ENV ?? 'development'; + + if ( + environment === 'production' || + environment === 'development' || + environment === 'dev_stage' + ) { + return environment; + } + + throw new Error(`Unsupported NODE_ENV for database commands: ${environment}`); +} + +function getDatabaseConfig(): DatabaseEnvironmentConfig { + return dbConfig[getNodeEnvironment()]; +} + +function createSequelize(): Sequelize { + return new Sequelize(getDatabaseConfig()); +} + +function createMigrators(sequelize: Sequelize): DbMigrators { + const context: DbUmzugContext = { + queryInterface: sequelize.getQueryInterface(), + }; + + return { + migrations: new Umzug({ + context, + logger: console, + migrations: { + glob: migrationGlob, + resolve: resolveLegacyMigration, + }, + storage: new SequelizeStorage({ + sequelize, + modelName: migrationTableName, + tableName: migrationTableName, + }), + }), + seeders: new Umzug({ + context, + logger: console, + migrations: { + glob: seederGlob, + resolve: resolveSeeder, + }, + storage: new SequelizeStorage({ + sequelize, + modelName: seederTableName, + tableName: seederTableName, + }), + }), + }; +} + +function printMigrationList(title: string, migrations: readonly MigrationMeta[]): void { + console.log(`${title}: ${migrations.length}`); + for (const migration of migrations) { + console.log(`- ${migration.name}`); + } +} + +async function printStatus(umzug: Umzug): Promise { + const executed = await umzug.executed(); + const pending = await umzug.pending(); + printMigrationList('Executed', executed); + printMigrationList('Pending', pending); +} + +async function runMigrationCommand( + command: DbUmzugCommand, + umzug: Umzug, +): Promise { + switch (command) { + case 'migrate:up': + case 'seed:up': + await umzug.up(); + return; + case 'migrate:down': + case 'seed:down': + await umzug.down(); + return; + case 'migrate:down:all': + case 'seed:down:all': + await umzug.down({ to: 0 }); + return; + case 'migrate:pending': + case 'seed:pending': + printMigrationList('Pending', await umzug.pending()); + return; + case 'migrate:executed': + case 'seed:executed': + printMigrationList('Executed', await umzug.executed()); + return; + case 'migrate:status': + case 'seed:status': + await printStatus(umzug); + return; + case 'db:create': + case 'db:drop': + throw new Error(`${command} must be handled as a database command.`); + } +} + +function quoteDatabaseIdentifier(identifier: string): string { + return `"${identifier.replaceAll('"', '""')}"`; +} + +function getRequiredDatabaseName(config: DatabaseEnvironmentConfig): string { + if (!config.database) { + throw new Error('Database name is required for this command.'); + } + + return config.database; +} + +function createMaintenanceConfig( + config: DatabaseEnvironmentConfig, +): DatabaseEnvironmentConfig { + return { + ...config, + database: 'postgres', + }; +} + +async function createDatabase(): Promise { + const config = getDatabaseConfig(); + const databaseName = getRequiredDatabaseName(config); + const sequelize = new Sequelize(createMaintenanceConfig(config)); + + try { + await sequelize.query( + `CREATE DATABASE ${quoteDatabaseIdentifier(databaseName)}`, + ); + console.log(`Database created: ${databaseName}`); + } finally { + await sequelize.close(); + } +} + +async function dropDatabase(): Promise { + const config = getDatabaseConfig(); + const databaseName = getRequiredDatabaseName(config); + const sequelize = new Sequelize(createMaintenanceConfig(config)); + + try { + await sequelize.query( + `DROP DATABASE IF EXISTS ${quoteDatabaseIdentifier(databaseName)}`, + ); + console.log(`Database dropped: ${databaseName}`); + } finally { + await sequelize.close(); + } +} + +function selectMigrator( + command: DbUmzugCommand, + migrators: DbMigrators, +): Umzug { + return command.startsWith('seed:') + ? migrators.seeders + : migrators.migrations; +} + +async function runCommand(command: DbUmzugCommand): Promise { + if (command === 'db:create') { + await createDatabase(); + return; + } + + if (command === 'db:drop') { + await dropDatabase(); + return; + } + + const sequelize = createSequelize(); + + try { + await sequelize.authenticate(); + const migrators = createMigrators(sequelize); + await runMigrationCommand(command, selectMigrator(command, migrators)); + } finally { + await sequelize.close(); + } +} + +async function main(): Promise { + const command = process.argv[2]; + + if (!isDbUmzugCommand(command)) { + console.error(`Usage: node src/db/umzug.ts <${dbCommands.join('|')}>`); + process.exitCode = 1; + return; + } + + await runCommand(command); +} + +const mainScriptUrl = + process.argv[1] === undefined ? undefined : pathToFileURL(process.argv[1]).href; + +if (import.meta.url === mainScriptUrl) { + void main(); +} + +export type { DbUmzugCommand, DbUmzugContext }; +export { createMigrators, runCommand }; diff --git a/backend/src/db/utils.js b/backend/src/db/utils.js deleted file mode 100644 index 9641d8f..0000000 --- a/backend/src/db/utils.js +++ /dev/null @@ -1,45 +0,0 @@ -const validator = require('validator'); -const { v4: uuidv4 } = require('uuid'); -const Sequelize = require('./models').Sequelize; - -module.exports = class Utils { - /** - * Check if value is a valid UUID - * @param {*} value - The value to check - * @returns {boolean} - True if valid UUID, false otherwise - */ - static isValidUuid(value) { - return Boolean(value && validator.isUUID(String(value))); - } - - /** - * Generate a new UUID v4 - * @returns {string} - A new UUID v4 string - */ - static generateUuid() { - return uuidv4(); - } - - /** - * Filter array to only valid UUIDs - * @param {Array} values - Array of values to filter - * @returns {string[]} - Array containing only valid UUIDs - */ - static filterValidUuids(values) { - return values.filter((v) => this.isValidUuid(v)); - } - - /** - * Case-insensitive LIKE query - * @param {string} model - The model/table name - * @param {string} column - The column name - * @param {string} value - The value to search for - * @returns {Object} - Sequelize where clause - */ - static ilike(model, column, value) { - return Sequelize.where( - Sequelize.fn('lower', Sequelize.col(`${model}.${column}`)), - { [Sequelize.Op.like]: `%${value}%`.toLowerCase() }, - ); - } -}; diff --git a/backend/src/db/utils.ts b/backend/src/db/utils.ts new file mode 100644 index 0000000..dfc8c24 --- /dev/null +++ b/backend/src/db/utils.ts @@ -0,0 +1,27 @@ +import { randomUUID } from 'node:crypto'; +import { col, fn, Op, where } from 'sequelize'; +import type { Where } from 'sequelize/types/utils'; +import validator from 'validator'; + +export default class Utils { + static isValidUuid(value: unknown): boolean { + return typeof value === 'string' && validator.isUUID(value); + } + + static generateUuid(): string { + return randomUUID(); + } + + static filterValidUuids(values: readonly unknown[]): string[] { + return values + .filter((value): value is string => this.isValidUuid(value)) + .map((value) => String(value)); + } + + static ilike(model: string, column: string, value: string): Where { + return where( + fn('lower', col(`${model}.${column}`)), + { [Op.like]: `%${value}%`.toLowerCase() }, + ); + } +} diff --git a/backend/src/factories/router.factory.js b/backend/src/factories/router.factory.js deleted file mode 100644 index f25ce72..0000000 --- a/backend/src/factories/router.factory.js +++ /dev/null @@ -1,222 +0,0 @@ -const express = require('express'); -const { - wrapAsync, - commonErrorHandler, - isUuidV4, - assertRouteIdMatchesBody, -} = require('../helpers'); -const { checkCrudPermissions } = require('../middlewares/check-permissions'); -const { parse } = require('json2csv'); -const { logger } = require('../utils/logger'); -const { validateRequest } = require('../middlewares/validate-request'); -const { crud: crudSchemas } = require('../validators/request-schemas'); - -const DEFAULT_LIST_LIMIT = 50; -const MAX_LIST_LIMIT = 1000; -const MAX_AUTOCOMPLETE_LIMIT = 50; -const MAX_CSV_LIMIT = 1000; - -function clampLimit(value, { defaultLimit, maxLimit }) { - const parsed = Number.parseInt(value, 10); - if (!Number.isFinite(parsed) || parsed <= 0) return defaultLimit; - return Math.min(parsed, maxLimit); -} - -function getSortableFields(DBApi) { - if (Array.isArray(DBApi.SORTABLE_FIELDS)) return DBApi.SORTABLE_FIELDS; - if (DBApi.MODEL?.rawAttributes) return Object.keys(DBApi.MODEL.rawAttributes); - return []; -} - -function normalizeQuery(query = {}, DBApi, { csv = false } = {}) { - const normalized = { ...query }; - const maxLimit = csv ? MAX_CSV_LIMIT : MAX_LIST_LIMIT; - normalized.limit = clampLimit(normalized.limit, { - defaultLimit: DEFAULT_LIST_LIMIT, - maxLimit, - }); - - const page = Number.parseInt(normalized.page, 10); - normalized.page = Number.isFinite(page) && page > 0 ? page : 1; - - if (normalized.sort) { - const sort = String(normalized.sort).toUpperCase(); - normalized.sort = sort === 'ASC' ? 'ASC' : 'DESC'; - } - - const sortableFields = getSortableFields(DBApi); - if (normalized.field && !sortableFields.includes(normalized.field)) { - delete normalized.field; - } - - return normalized; -} - -function createEntityRouter(entityName, Service, DBApi, options = {}) { - const router = express.Router(); - - const permissionEntity = options.permissionEntity || entityName; - const validation = options.validation || {}; - const schemaFor = (name) => validation[name] || crudSchemas[name]; - router.use(checkCrudPermissions(permissionEntity)); - - router.post( - '/', - validateRequest(schemaFor('create')), - wrapAsync(async (req, res) => { - const referer = - req.headers.referer || - `${req.protocol}://${req.hostname}${req.originalUrl}`; - const link = new URL(referer); - const payload = await Service.create({ - data: req.body.data, - currentUser: req.currentUser, - runtimeContext: req.runtimeContext, - sendInvitationEmails: true, - host: link.origin, - }); - res.status(200).send(payload); - }), - ); - - router.post( - '/bulk-import', - wrapAsync(async (req, res) => { - const referer = - req.headers.referer || - `${req.protocol}://${req.hostname}${req.originalUrl}`; - const link = new URL(referer); - await Service.bulkImport(req, res, true, link.origin); - res.status(200).send(true); - }), - ); - - router.put( - '/:id', - validateRequest(schemaFor('update')), - wrapAsync(async (req, res) => { - assertRouteIdMatchesBody(req); - await Service.update({ - id: req.params.id, - data: req.body.data, - currentUser: req.currentUser, - runtimeContext: req.runtimeContext, - }); - res.status(200).send(true); - }), - ); - - router.delete( - '/:id', - validateRequest(schemaFor('remove')), - wrapAsync(async (req, res) => { - await Service.remove({ - id: req.params.id, - currentUser: req.currentUser, - runtimeContext: req.runtimeContext, - }); - res.status(200).send(true); - }), - ); - - router.post( - '/deleteByIds', - validateRequest(schemaFor('deleteByIds')), - wrapAsync(async (req, res) => { - await Service.deleteByIds({ - ids: req.body.data, - currentUser: req.currentUser, - runtimeContext: req.runtimeContext, - }); - res.status(200).send(true); - }), - ); - - router.get( - '/', - validateRequest(schemaFor('list')), - wrapAsync(async (req, res) => { - const filetype = req.query.filetype; - const currentUser = req.currentUser; - const runtimeContext = req.runtimeContext; - const normalizedQuery = normalizeQuery(req.query, DBApi, { - csv: filetype === 'csv', - }); - - const payload = await DBApi.findAll(normalizedQuery, { - currentUser, - runtimeContext, - }); - - if (filetype === 'csv') { - const fields = options.csvFields || - DBApi.CSV_FIELDS || ['id', 'createdAt']; - const opts = { fields }; - try { - const csv = parse(payload.rows, opts); - res.status(200).attachment('export.csv').send(csv); - } catch (err) { - logger.error({ err, entityName }, 'CSV export error'); - res.status(500).send('CSV export error'); - } - } else { - res.status(200).send(payload); - } - }), - ); - - router.get( - '/count', - validateRequest(schemaFor('count')), - wrapAsync(async (req, res) => { - const currentUser = req.currentUser; - const runtimeContext = req.runtimeContext; - const payload = await DBApi.findAll(normalizeQuery(req.query, DBApi), { - countOnly: true, - currentUser, - runtimeContext, - }); - res.status(200).send(payload); - }), - ); - - router.get( - '/autocomplete', - validateRequest(schemaFor('autocomplete')), - wrapAsync(async (req, res) => { - const limit = clampLimit(req.query.limit, { - defaultLimit: 20, - maxLimit: MAX_AUTOCOMPLETE_LIMIT, - }); - const payload = await DBApi.findAllAutocomplete({ - query: req.query.query, - limit, - offset: req.query.offset, - }); - res.status(200).send(payload); - }), - ); - - router.get( - '/:id', - validateRequest(schemaFor('findOne')), - wrapAsync(async (req, res) => { - const runtimeContext = req.runtimeContext; - const payload = await DBApi.findBy( - { id: req.params.id }, - { runtimeContext }, - ); - res.status(200).send(payload); - }), - ); - - if (options.customRoutes) { - options.customRoutes(router, Service, DBApi); - } - - router.use('/', commonErrorHandler); - - return router; -} - -module.exports = { createEntityRouter, isUuidV4 }; diff --git a/backend/src/factories/router.factory.ts b/backend/src/factories/router.factory.ts new file mode 100644 index 0000000..5f86b17 --- /dev/null +++ b/backend/src/factories/router.factory.ts @@ -0,0 +1,433 @@ +import express from 'express'; +import type { Router } from 'express'; +import { parse } from 'json2csv'; + +import { + assertRouteIdMatchesBody, + commonErrorHandler, + isUuidV4, + wrapAsync, +} from '../helpers.ts'; +import { checkCrudPermissions } from '../middlewares/check-permissions.ts'; +import { validateRequest } from '../middlewares/validate-request.ts'; +import { crud as crudSchemas } from '../validators/request-schemas.ts'; +import { logger } from '../utils/logger.ts'; +import { + getCurrentUser, + getRuntimeContext, +} from '../utils/request-context.ts'; +import type { + EntityRouterDbApi, + EntityRouterOptions, + EntityRouterQuery, + EntityRouterService, + EntityDataRequestBody, + EntityIdOptions, + CreateOptions, + DbFindByOptions, + DeleteByIdsOptions, + NormalizedEntityRouterQuery, + PaginatedResult, + RequestSchemaMap, + RouteEntityDataRequestBody, + ServiceOptions, + UpdateOptions, + AutocompleteOptions, +} from '../types/index.ts'; + +const DEFAULT_LIST_LIMIT = 50; +const MAX_LIST_LIMIT = 1000; +const MAX_AUTOCOMPLETE_LIMIT = 50; +const MAX_CSV_LIMIT = 1000; + +interface ClampLimitOptions { + defaultLimit: number; + maxLimit: number; +} + +interface EntityDeleteByIdsRequestBody { + data: string[]; +} + +interface RouterServiceOptionsInput { + currentUser?: ServiceOptions['currentUser'] | undefined; + runtimeContext?: ServiceOptions['runtimeContext'] | undefined; +} + +const NON_FILTER_QUERY_KEYS = new Set(['limit', 'page', 'sort', 'field']); + +function hasRawAttributes(value: unknown): value is { + rawAttributes: Record; +} { + return ( + value !== null && + typeof value === 'object' && + 'rawAttributes' in value && + value.rawAttributes !== null && + typeof value.rawAttributes === 'object' + ); +} + +function isStringArray(value: unknown): value is string[] { + return Array.isArray(value) && value.every((item) => typeof item === 'string'); +} + +function clampLimit(value: unknown, options: ClampLimitOptions): number { + const parsed = + typeof value === 'string' || typeof value === 'number' + ? Number.parseInt(`${value}`, 10) + : Number.NaN; + if (!Number.isFinite(parsed) || parsed <= 0) return options.defaultLimit; + return Math.min(parsed, options.maxLimit); +} + +function getSortableFields(DBApi: EntityRouterDbApi): readonly string[] { + if (isStringArray(DBApi.SORTABLE_FIELDS)) return DBApi.SORTABLE_FIELDS; + if (hasRawAttributes(DBApi.MODEL)) return Object.keys(DBApi.MODEL.rawAttributes); + return []; +} + +function normalizeQuery( + query: EntityRouterQuery = {}, + DBApi: EntityRouterDbApi, + options: { csv?: boolean } = {}, +): NormalizedEntityRouterQuery { + const normalized: NormalizedEntityRouterQuery = { + limit: clampLimit(query.limit, { + defaultLimit: DEFAULT_LIST_LIMIT, + maxLimit: options.csv ? MAX_CSV_LIMIT : MAX_LIST_LIMIT, + }), + page: clampLimit(query.page, { + defaultLimit: 1, + maxLimit: Number.MAX_SAFE_INTEGER, + }), + }; + + for (const [key, value] of Object.entries(query)) { + if (!NON_FILTER_QUERY_KEYS.has(key)) { + normalized[key] = value; + } + } + + if (typeof query.sort === 'string' || typeof query.sort === 'number') { + const sort = `${query.sort}`.toUpperCase(); + normalized.sort = sort === 'ASC' ? 'ASC' : 'DESC'; + } + + const sortableFields = getSortableFields(DBApi); + if ( + typeof query.field === 'string' && + sortableFields.includes(query.field) + ) { + normalized.field = query.field; + } + + return normalized; +} + +function buildRouterServiceOptions( + options: RouterServiceOptionsInput, +): ServiceOptions { + const result: ServiceOptions = {}; + if (options.currentUser !== undefined) { + result.currentUser = options.currentUser; + } + if (options.runtimeContext !== undefined) { + result.runtimeContext = options.runtimeContext; + } + return result; +} + +function buildCreateOptions( + data: TCreate, + options: RouterServiceOptionsInput & { host: string }, +): CreateOptions { + const result: CreateOptions = { + data, + sendInvitationEmails: true, + host: options.host, + }; + const serviceOptions = buildRouterServiceOptions(options); + if (serviceOptions.currentUser !== undefined) { + result.currentUser = serviceOptions.currentUser; + } + if (serviceOptions.runtimeContext !== undefined) { + result.runtimeContext = serviceOptions.runtimeContext; + } + return result; +} + +function buildUpdateOptions( + id: string, + data: TUpdate, + options: RouterServiceOptionsInput, +): UpdateOptions { + const result: UpdateOptions = { id, data }; + const serviceOptions = buildRouterServiceOptions(options); + if (serviceOptions.currentUser !== undefined) { + result.currentUser = serviceOptions.currentUser; + } + if (serviceOptions.runtimeContext !== undefined) { + result.runtimeContext = serviceOptions.runtimeContext; + } + return result; +} + +function buildIdOptions( + id: string, + options: RouterServiceOptionsInput, +): EntityIdOptions { + const result: EntityIdOptions = { id }; + const serviceOptions = buildRouterServiceOptions(options); + if (serviceOptions.currentUser !== undefined) { + result.currentUser = serviceOptions.currentUser; + } + if (serviceOptions.runtimeContext !== undefined) { + result.runtimeContext = serviceOptions.runtimeContext; + } + return result; +} + +function buildDeleteByIdsOptions( + ids: string[], + options: RouterServiceOptionsInput, +): DeleteByIdsOptions { + const result: DeleteByIdsOptions = { ids }; + const serviceOptions = buildRouterServiceOptions(options); + if (serviceOptions.currentUser !== undefined) { + result.currentUser = serviceOptions.currentUser; + } + if (serviceOptions.runtimeContext !== undefined) { + result.runtimeContext = serviceOptions.runtimeContext; + } + return result; +} + +function buildDbFindByOptions( + id: string, + options: RouterServiceOptionsInput, +): DbFindByOptions { + const result: DbFindByOptions = { where: { id } }; + if (options.runtimeContext !== undefined) { + result.runtimeContext = options.runtimeContext; + } + return result; +} + +function buildAutocompleteOptions( + query: EntityRouterQuery, +): AutocompleteOptions { + const result: AutocompleteOptions = { + limit: clampLimit(query.limit, { + defaultLimit: 20, + maxLimit: MAX_AUTOCOMPLETE_LIMIT, + }), + }; + + if (typeof query.query === 'string') { + result.query = query.query; + } + if (typeof query.offset === 'number') { + result.offset = query.offset; + } else if (typeof query.offset === 'string') { + result.offset = clampLimit(query.offset, { + defaultLimit: 0, + maxLimit: Number.MAX_SAFE_INTEGER, + }); + } + + return result; +} + +function schemaFor( + validation: EntityRouterOptions['validation'], + name: string, +): RequestSchemaMap { + const override = validation?.[name]; + if (override) return override; + + if (name === 'create') return crudSchemas.create; + if (name === 'update') return crudSchemas.update; + if (name === 'remove') return crudSchemas.remove; + if (name === 'deleteByIds') return crudSchemas.deleteByIds; + if (name === 'list') return crudSchemas.list; + if (name === 'count') return crudSchemas.count; + if (name === 'autocomplete') return crudSchemas.autocomplete; + if (name === 'findOne') return crudSchemas.findOne; + + throw new Error(`Unknown CRUD schema: ${name}`); +} + +function isPaginatedPayload( + payload: PaginatedResult | number, +): payload is PaginatedResult { + return typeof payload === 'object' && payload !== null && 'rows' in payload; +} + +function createEntityRouter( + entityName: string, + Service: EntityRouterService, + DBApi: EntityRouterDbApi, + options: EntityRouterOptions = {}, +): Router { + const router = express.Router(); + + const permissionEntity = options.permissionEntity || entityName; + router.use(checkCrudPermissions(permissionEntity)); + + router.post( + '/', + validateRequest(schemaFor(options.validation, 'create')), + wrapAsync>(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + const payload = await Service.create( + buildCreateOptions(req.body.data, { + currentUser: getCurrentUser(req), + runtimeContext: getRuntimeContext(req), + host: link.origin, + }), + ); + res.status(200).send(payload); + }), + ); + + router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + await Service.bulkImport(req, res); + res.status(200).send(true); + }), + ); + + router.put( + '/:id', + validateRequest(schemaFor(options.validation, 'update')), + wrapAsync<{ id: string }, unknown, RouteEntityDataRequestBody>( + async (req, res) => { + assertRouteIdMatchesBody(req); + await Service.update( + buildUpdateOptions(req.params.id, req.body.data, { + currentUser: getCurrentUser(req), + runtimeContext: getRuntimeContext(req), + }), + ); + res.status(200).send(true); + }, + ), + ); + + router.delete( + '/:id', + validateRequest(schemaFor(options.validation, 'remove')), + wrapAsync<{ id: string }>(async (req, res) => { + await Service.remove( + buildIdOptions(req.params.id, { + currentUser: getCurrentUser(req), + runtimeContext: getRuntimeContext(req), + }), + ); + res.status(200).send(true); + }), + ); + + router.post( + '/deleteByIds', + validateRequest(schemaFor(options.validation, 'deleteByIds')), + wrapAsync(async (req, res) => { + await Service.deleteByIds( + buildDeleteByIdsOptions(req.body.data, { + currentUser: getCurrentUser(req), + runtimeContext: getRuntimeContext(req), + }), + ); + res.status(200).send(true); + }), + ); + + router.get( + '/', + validateRequest(schemaFor(options.validation, 'list')), + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + const normalizedQuery = normalizeQuery(req.query, DBApi, { + csv: filetype === 'csv', + }); + + const payload = await DBApi.findAll(normalizedQuery, { + currentUser: getCurrentUser(req), + runtimeContext: getRuntimeContext(req), + }); + + if (filetype === 'csv') { + if (!isPaginatedPayload(payload)) { + res.status(500).send('CSV export error'); + return; + } + + const fields = options.csvFields || DBApi.CSV_FIELDS || [ + 'id', + 'createdAt', + ]; + try { + const csv = parse(payload.rows, { fields: [...fields] }); + res.status(200).attachment('export.csv').send(csv); + } catch (error) { + logger.error({ err: error, entityName }, 'CSV export error'); + res.status(500).send('CSV export error'); + } + } else { + res.status(200).send(payload); + } + }), + ); + + router.get( + '/count', + validateRequest(schemaFor(options.validation, 'count')), + wrapAsync(async (req, res) => { + const payload = await DBApi.findAll(normalizeQuery(req.query, DBApi), { + countOnly: true, + currentUser: getCurrentUser(req), + runtimeContext: getRuntimeContext(req), + }); + res.status(200).send(payload); + }), + ); + + router.get( + '/autocomplete', + validateRequest(schemaFor(options.validation, 'autocomplete')), + wrapAsync(async (req, res) => { + const payload = await DBApi.findAllAutocomplete( + buildAutocompleteOptions(req.query), + ); + res.status(200).send(payload); + }), + ); + + router.get( + '/:id', + validateRequest(schemaFor(options.validation, 'findOne')), + wrapAsync<{ id: string }>(async (req, res) => { + const payload = await DBApi.findBy( + buildDbFindByOptions(req.params.id, { + runtimeContext: getRuntimeContext(req), + }), + ); + res.status(200).send(payload); + }), + ); + + if (options.customRoutes) { + options.customRoutes(router, Service, DBApi); + } + + router.use('/', commonErrorHandler); + + return router; +} + +export { createEntityRouter, isUuidV4 }; diff --git a/backend/src/factories/service.factory.js b/backend/src/factories/service.factory.js deleted file mode 100644 index 73feefc..0000000 --- a/backend/src/factories/service.factory.js +++ /dev/null @@ -1,180 +0,0 @@ -const db = require('../db/models'); -const processFile = require('../middlewares/upload'); -const ValidationError = require('../services/notifications/errors/validation'); -const csv = require('csv-parser'); -const stream = require('stream'); -const { - assertCreateOptions, - assertDeleteByIdsOptions, - assertIdOptions, - assertUpdateOptions, -} = require('../contracts/entity-options'); - -function createEntityService(DBApi, options = {}) { - const entityName = options.entityName || 'Entity'; - - return class GenericService { - static async create(options) { - assertCreateOptions(options, 'Service'); - - const { - data, - currentUser, - transaction: externalTransaction, - runtimeContext, - } = options; - const transaction = - externalTransaction || (await db.sequelize.transaction()); - const ownsTransaction = !externalTransaction; - - try { - const record = await DBApi.create({ - data, - currentUser, - transaction, - runtimeContext, - }); - if (ownsTransaction) await transaction.commit(); - return record; - } catch (error) { - if (ownsTransaction) await transaction.rollback(); - throw error; - } - } - - static async bulkImport(req, res) { - const transaction = await db.sequelize.transaction(); - - try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', () => resolve()) - .on('error', (error) => reject(error)); - }); - - await DBApi.bulkImport(results, { - transaction, - ignoreDuplicates: true, - validate: true, - currentUser: req.currentUser, - }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - /** - * @param {Object} options - * @param {string} options.id - * @param {Object} options.data - * @param {Object} [options.currentUser] - * @param {Object} [options.transaction] - * @param {Object} [options.runtimeContext] - */ - static async update(options) { - assertUpdateOptions(options, 'Service'); - - const { - id, - data, - currentUser, - transaction: externalTransaction, - runtimeContext, - } = options; - const transaction = - externalTransaction || (await db.sequelize.transaction()); - const ownsTransaction = !externalTransaction; - - try { - const record = await DBApi.findBy( - { id }, - { transaction, runtimeContext }, - ); - - if (!record) { - throw new ValidationError(`${entityName}NotFound`); - } - - const updated = await DBApi.update({ - id, - data, - currentUser, - transaction, - runtimeContext, - }); - if (ownsTransaction) await transaction.commit(); - return updated; - } catch (error) { - if (ownsTransaction) await transaction.rollback(); - throw error; - } - } - - static async deleteByIds(options) { - assertDeleteByIdsOptions(options, 'Service'); - - const { - ids, - currentUser, - transaction: externalTransaction, - runtimeContext, - } = options; - const transaction = - externalTransaction || (await db.sequelize.transaction()); - const ownsTransaction = !externalTransaction; - - try { - await DBApi.deleteByIds({ - ids, - currentUser, - transaction, - runtimeContext, - }); - if (ownsTransaction) await transaction.commit(); - } catch (error) { - if (ownsTransaction) await transaction.rollback(); - throw error; - } - } - - static async remove(options) { - assertIdOptions(options, 'Service', 'remove'); - - const { - id, - currentUser, - transaction: externalTransaction, - runtimeContext, - } = options; - const transaction = - externalTransaction || (await db.sequelize.transaction()); - const ownsTransaction = !externalTransaction; - - try { - await DBApi.remove({ - id, - currentUser, - transaction, - runtimeContext, - }); - if (ownsTransaction) await transaction.commit(); - } catch (error) { - if (ownsTransaction) await transaction.rollback(); - throw error; - } - } - }; -} - -module.exports = { createEntityService }; diff --git a/backend/src/factories/service.factory.ts b/backend/src/factories/service.factory.ts new file mode 100644 index 0000000..aab88c9 --- /dev/null +++ b/backend/src/factories/service.factory.ts @@ -0,0 +1,347 @@ +import type { Request, Response } from 'express'; +import { PassThrough } from 'node:stream'; +import csv from 'csv-parser'; + +import db from '../db/models/index.ts'; +import processFile from '../middlewares/upload.ts'; +import { getCurrentUser } from '../utils/request-context.ts'; +import { + assertCreateOptions, + assertDeleteByIdsOptions, + assertIdOptions, + assertUpdateOptions, +} from '../contracts/entity-options.ts'; +import ValidationError from '../services/notifications/errors/validation.ts'; +import type { + BulkImportOptions, + CreateOptions, + DeleteByIdsOptions, + EntityRecord, + EntityIdOptions, + EntityServiceConstructor, + EntityServiceDbApi, + EntityServiceFactoryOptions, + RuntimeContext, + ServiceOptions, + UpdateOptions, +} from '../types/index.ts'; + +interface CsvUploadRequest extends Request { + file: Express.Multer.File & { + buffer: Buffer; + }; +} + +interface ServiceOptionsInput { + currentUser?: ServiceOptions['currentUser'] | undefined; + transaction?: ServiceOptions['transaction'] | undefined; + runtimeContext?: ServiceOptions['runtimeContext'] | undefined; +} + +function hasCsvUploadFile(req: Request): req is CsvUploadRequest { + return Buffer.isBuffer(req.file?.buffer); +} + +function buildServiceOptions(options: ServiceOptionsInput): ServiceOptions { + const result: ServiceOptions = {}; + if (options.currentUser !== undefined) { + result.currentUser = options.currentUser; + } + if (options.transaction !== undefined) { + result.transaction = options.transaction; + } + if (options.runtimeContext !== undefined) { + result.runtimeContext = options.runtimeContext; + } + return result; +} + +function buildCreateOptions( + data: TData, + options: ServiceOptions, +): CreateOptions { + const result: CreateOptions = { data }; + if (options.currentUser !== undefined) { + result.currentUser = options.currentUser; + } + if (options.transaction !== undefined) { + result.transaction = options.transaction; + } + if (options.runtimeContext !== undefined) { + result.runtimeContext = options.runtimeContext; + } + return result; +} + +function buildUpdateOptions( + id: string, + data: TData, + options: ServiceOptions, +): UpdateOptions { + const result: UpdateOptions = { id, data }; + if (options.currentUser !== undefined) { + result.currentUser = options.currentUser; + } + if (options.transaction !== undefined) { + result.transaction = options.transaction; + } + if (options.runtimeContext !== undefined) { + result.runtimeContext = options.runtimeContext; + } + return result; +} + +function buildDeleteByIdsOptions( + ids: string[], + options: ServiceOptions, +): DeleteByIdsOptions { + const result: DeleteByIdsOptions = { ids }; + if (options.currentUser !== undefined) { + result.currentUser = options.currentUser; + } + if (options.transaction !== undefined) { + result.transaction = options.transaction; + } + if (options.runtimeContext !== undefined) { + result.runtimeContext = options.runtimeContext; + } + return result; +} + +function buildIdOptions(id: string, options: ServiceOptions): EntityIdOptions { + const result: EntityIdOptions = { id }; + if (options.currentUser !== undefined) { + result.currentUser = options.currentUser; + } + if (options.transaction !== undefined) { + result.transaction = options.transaction; + } + if (options.runtimeContext !== undefined) { + result.runtimeContext = options.runtimeContext; + } + return result; +} + +function buildBulkImportOptions(options: ServiceOptions): BulkImportOptions { + const result: BulkImportOptions = { + ignoreDuplicates: true, + validate: true, + }; + if (options.currentUser !== undefined) { + result.currentUser = options.currentUser; + } + if (options.transaction !== undefined) { + result.transaction = options.transaction; + } + return result; +} + +function buildFindByOptions( + transaction: ServiceOptions['transaction'], + runtimeContext: RuntimeContext | undefined, +): Pick { + const result: Pick = {}; + if (transaction !== undefined) { + result.transaction = transaction; + } + if (runtimeContext !== undefined) { + result.runtimeContext = runtimeContext; + } + return result; +} + +function createEntityService< + TEntity extends EntityRecord = EntityRecord, + TCreate = unknown, + TUpdate = unknown, + TFilter = unknown, + TAutocomplete extends EntityRecord | { id: string } = TEntity, +>( + DBApi: EntityServiceDbApi, + options: EntityServiceFactoryOptions = {}, +): EntityServiceConstructor { + const entityName = options.entityName || 'Entity'; + + return class GenericService { + static async create(options: unknown) { + assertCreateOptions(options, 'Service'); + + const { + data, + currentUser, + transaction: externalTransaction, + runtimeContext, + } = options; + const transaction = + externalTransaction || (await db.sequelize.transaction()); + const ownsTransaction = !externalTransaction; + + try { + const record = await DBApi.create( + buildCreateOptions( + data, + buildServiceOptions({ + currentUser, + transaction, + runtimeContext, + }), + ), + ); + if (ownsTransaction) await transaction.commit(); + return record; + } catch (error) { + if (ownsTransaction) await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req: Request, res: Response): Promise { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + + if (!hasCsvUploadFile(req)) { + throw new ValidationError('errors.validation.message'); + } + + const bufferStream = new PassThrough(); + const results: Record[] = []; + + bufferStream.end(req.file.buffer); + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data: Record) => results.push(data)) + .on('end', resolve) + .on('error', reject); + }); + + await DBApi.bulkImport( + results, + buildBulkImportOptions( + buildServiceOptions({ + transaction, + currentUser: getCurrentUser(req), + }), + ), + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(options: unknown) { + assertUpdateOptions(options, 'Service'); + + const { + id, + data, + currentUser, + transaction: externalTransaction, + runtimeContext, + } = options; + const transaction = + externalTransaction || (await db.sequelize.transaction()); + const ownsTransaction = !externalTransaction; + + try { + const record = await DBApi.findBy( + { id }, + buildFindByOptions(transaction, runtimeContext), + ); + + if (!record) { + throw new ValidationError(`${entityName}NotFound`); + } + + const updated = await DBApi.update( + buildUpdateOptions( + id, + data, + buildServiceOptions({ + currentUser, + transaction, + runtimeContext, + }), + ), + ); + if (ownsTransaction) await transaction.commit(); + return updated; + } catch (error) { + if (ownsTransaction) await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(options: unknown): Promise { + assertDeleteByIdsOptions(options, 'Service'); + + const { + ids, + currentUser, + transaction: externalTransaction, + runtimeContext, + } = options; + const transaction = + externalTransaction || (await db.sequelize.transaction()); + const ownsTransaction = !externalTransaction; + + try { + const result = await DBApi.deleteByIds( + buildDeleteByIdsOptions( + ids, + buildServiceOptions({ + currentUser, + transaction, + runtimeContext, + }), + ), + ); + if (ownsTransaction) await transaction.commit(); + return result; + } catch (error) { + if (ownsTransaction) await transaction.rollback(); + throw error; + } + } + + static async remove(options: unknown): Promise { + assertIdOptions(options, 'Service', 'remove'); + + const { + id, + currentUser, + transaction: externalTransaction, + runtimeContext, + } = options; + const transaction = + externalTransaction || (await db.sequelize.transaction()); + const ownsTransaction = !externalTransaction; + + try { + const result = await DBApi.remove( + buildIdOptions( + id, + buildServiceOptions({ + currentUser, + transaction, + runtimeContext, + }), + ), + ); + if (ownsTransaction) await transaction.commit(); + return result; + } catch (error) { + if (ownsTransaction) await transaction.rollback(); + throw error; + } + } + }; +} + +export { createEntityService }; diff --git a/backend/src/helpers.js b/backend/src/helpers.js deleted file mode 100644 index 24dc29e..0000000 --- a/backend/src/helpers.js +++ /dev/null @@ -1,47 +0,0 @@ -const jwt = require('jsonwebtoken'); -const config = require('./config'); -const ValidationError = require('./services/notifications/errors/validation'); -const { logger } = require('./utils/logger'); - -module.exports = class Helpers { - static wrapAsync(fn) { - return function (req, res, next) { - fn(req, res, next).catch(next); - }; - } - - static commonErrorHandler(error, req, res, _next) { - const statusCode = error.code || error.status; - - if (error.isRequestValidation) { - return res.status(400).send({ - error: error.message, - details: error.details || [], - }); - } - - if ([400, 401, 403, 404, 409, 422].includes(statusCode)) { - return res.status(statusCode).send(error.message); - } - - logger.error({ err: error }, 'Unhandled route error'); - return res.status(500).send('Internal server error'); - } - - static jwtSign(data) { - return jwt.sign(data, config.secret_key, { expiresIn: '6h' }); - } - - static isUuidV4(value) { - return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( - value, - ); - } - - static assertRouteIdMatchesBody(req) { - const bodyId = req.body?.id || req.body?.data?.id; - if (bodyId && bodyId !== req.params.id) { - throw new ValidationError('Request body id does not match route id'); - } - } -}; diff --git a/backend/src/helpers.ts b/backend/src/helpers.ts new file mode 100644 index 0000000..0270069 --- /dev/null +++ b/backend/src/helpers.ts @@ -0,0 +1,86 @@ +import type { + ErrorRequestHandler, + NextFunction, + Request, + RequestHandler, + Response, +} from 'express'; +import type { + ParamsDictionary, + Query, +} from 'express-serve-static-core'; +import jwt from 'jsonwebtoken'; + +import config from './config.ts'; +import type { + AsyncRequestHandler, + RouteError, + RouteIdRequestLike, +} from './types/index.ts'; +import { logger } from './utils/logger.ts'; +import { assertBodyIdMatchesRouteId } from './utils/request-body.ts'; + +function wrapAsync< + TParams extends ParamsDictionary = ParamsDictionary, + TResBody = unknown, + TReqBody = unknown, + TReqQuery extends Query = Query, +>( + fn: AsyncRequestHandler, +): RequestHandler { + return function asyncMiddleware( + req: Request, + res: Response, + next: NextFunction, + ): void { + fn(req, res, next).catch(next); + }; +} + +const commonErrorHandler: ErrorRequestHandler = ( + error: RouteError, + _req: Request, + res: Response, + _next: NextFunction, +): Response => { + const statusCode = error.code ?? error.status; + + if (error.isRequestValidation) { + return res.status(400).send({ + error: error.message, + details: error.details ?? [], + }); + } + + if ( + statusCode !== undefined && + [400, 401, 403, 404, 409, 422].includes(statusCode) + ) { + return res.status(statusCode).send(error.message); + } + + logger.error({ err: error }, 'Unhandled route error'); + return res.status(500).send('Internal server error'); +}; + +function jwtSign(data: string | Buffer | object): string { + return jwt.sign(data, config.secret_key, { expiresIn: '6h' }); +} + +function isUuidV4(value: string): boolean { + return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( + value, + ); +} + +function assertRouteIdMatchesBody(req: RouteIdRequestLike): void { + assertBodyIdMatchesRouteId(req.params.id, req.body); +} + +export { + assertRouteIdMatchesBody, + commonErrorHandler, + isUuidV4, + jwtSign, + wrapAsync, +}; diff --git a/backend/src/index.js b/backend/src/index.js deleted file mode 100644 index 72e4f0b..0000000 --- a/backend/src/index.js +++ /dev/null @@ -1,344 +0,0 @@ -const express = require('express'); -const cors = require('cors'); -const helmet = require('helmet'); -const app = express(); -const passport = require('passport'); -const path = require('path'); -const fs = require('fs'); -const bodyParser = require('body-parser'); -const config = require('./config'); -const swaggerUI = require('swagger-ui-express'); -const swaggerJsDoc = require('swagger-jsdoc'); -const { logger, requestLogger } = require('./utils/logger'); -const { - uploadLimiter, - downloadLimiter, - searchLimiter, - aiLimiter, -} = require('./middlewares/rateLimiter'); - -const authRoutes = require('./routes/auth'); -const fileRoutes = require('./routes/file'); -const searchRoutes = require('./routes/search'); -const sqlRoutes = require('./routes/sql'); - -const openaiRoutes = require('./routes/openai'); - -const usersRoutes = require('./routes/users'); - -const rolesRoutes = require('./routes/roles'); - -const permissionsRoutes = require('./routes/permissions'); - -const projectsRoutes = require('./routes/projects'); - -const project_membershipsRoutes = require('./routes/project_memberships'); - -const assetsRoutes = require('./routes/assets'); - -const asset_variantsRoutes = require('./routes/asset_variants'); - -const presigned_url_requestsRoutes = require('./routes/presigned_url_requests'); - -const tour_pagesRoutes = require('./routes/tour_pages'); - -const project_audio_tracksRoutes = require('./routes/project_audio_tracks'); - -const publish_eventsRoutes = require('./routes/publish_events'); - -const pwa_cachesRoutes = require('./routes/pwa_caches'); - -const access_logsRoutes = require('./routes/access_logs'); -const element_type_defaultsRoutes = require('./routes/element_type_defaults'); -const project_element_defaultsRoutes = require('./routes/project_element_defaults'); -const global_transition_defaultsRoutes = require('./routes/global_transition_defaults'); -const project_transition_settingsRoutes = require('./routes/project_transition_settings'); -const global_ui_control_defaultsRoutes = require('./routes/global_ui_control_defaults'); -const project_ui_control_settingsRoutes = require('./routes/project_ui_control_settings'); - -const publishRoutes = require('./routes/publish'); -const runtimeContextRoutes = require('./routes/runtime-context'); -const runtimeAccessRoutes = require('./routes/runtime-access'); -const { runtimeContextMiddleware } = require('./middlewares/runtime-context'); -const { - blockNonPublicRuntimeListEndpoints, - sanitizePublicRuntimeListResponse, -} = require('./middlewares/runtime-public'); -const RuntimePresentationAccessService = require('./services/runtime-presentation-access'); - -const getBaseUrl = (url) => { - if (!url) return ''; - return url.endsWith('/api') ? url.slice(0, -4) : url; -}; - -const options = { - definition: { - openapi: '3.0.0', - info: { - version: '1.0.0', - title: 'Tour Builder Platform', - description: - 'Tour Builder Platform Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.', - }, - servers: [ - { - url: getBaseUrl(process.env.NEXT_PUBLIC_BACK_API) || config.swaggerUrl, - description: 'Development server', - }, - ], - components: { - securitySchemes: { - bearerAuth: { - type: 'http', - scheme: 'bearer', - bearerFormat: 'JWT', - }, - }, - responses: { - UnauthorizedError: { - description: 'Access token is missing or invalid', - }, - }, - }, - security: [ - { - bearerAuth: [], - }, - ], - }, - apis: ['./src/routes/*.js'], -}; - -const specs = swaggerJsDoc(options); -app.use( - '/api-docs', - function (req, res, next) { - swaggerUI.host = - getBaseUrl(process.env.NEXT_PUBLIC_BACK_API) || req.get('host'); - next(); - }, - swaggerUI.serve, - swaggerUI.setup(specs), -); - -app.enable('trust proxy'); -app.use( - helmet({ - contentSecurityPolicy: false, - crossOriginEmbedderPolicy: false, - }), -); -app.use(cors({ origin: true, credentials: true })); -require('./auth/auth'); - -// Request logger applied early so all routes are logged -app.use(requestLogger); - -// Initialize passport JWT auth early (before file routes) -const jwtAuth = passport.authenticate('jwt', { session: false }); - -// Mount file routes BEFORE body-parser to avoid JSON parsing on binary uploads -// These routes handle their own body parsing (JSON for init/finalize, raw streams for chunks) -// Use downloadLimiter for download/presign (high traffic), uploadLimiter for uploads (strict) -app.use('/api/file/download', downloadLimiter); -app.use('/api/file/presign', downloadLimiter); -app.use('/api/file/upload', uploadLimiter); -app.use('/api/file/upload-sessions', uploadLimiter); -app.use('/api/file', fileRoutes); - -// Body parser for all other routes -app.use(bodyParser.json({ limit: '50mb' })); -app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' })); -app.use(runtimeContextMiddleware); - -const requireRuntimeReadOrAuth = async (req, res, next) => { - try { - const headerEnvironment = req.runtimeContext?.headerEnvironment; - const headerProjectSlug = req.runtimeContext?.headerProjectSlug; - const isReadOnlyRequest = ['GET', 'OPTIONS'].includes(req.method); - const hasAuthHeader = Boolean(req.headers.authorization); - - // Only production is public. Stage requires authentication (workspace for review). - const isPublicEnvironment = headerEnvironment === 'production'; - - if (!isPublicEnvironment || !isReadOnlyRequest) { - req.isRuntimePublicRequest = false; - return jwtAuth(req, res, next); - } - - const isPrivateProductionPresentation = - await RuntimePresentationAccessService.isPrivateProductionPresentation( - headerProjectSlug, - ); - - if (!isPrivateProductionPresentation) { - req.isRuntimePublicRequest = true; - return next(); - } - - if (!hasAuthHeader) { - req.isRuntimePublicRequest = false; - return res.status(401).send({ message: 'Authentication required' }); - } - - return passport.authenticate( - 'jwt', - { session: false }, - async (error, user) => { - if (error) return next(error); - - if (!user) { - req.isRuntimePublicRequest = false; - return res.status(401).send({ message: 'Authentication required' }); - } - - req.currentUser = user; - - try { - const canAccess = - await RuntimePresentationAccessService.canUserAccessPrivateProductionPresentation( - user, - headerProjectSlug, - ); - - if (!canAccess) { - req.isRuntimePublicRequest = false; - return res - .status(403) - .send({ message: 'Presentation access denied' }); - } - - req.isRuntimePublicRequest = true; - return next(); - } catch (accessError) { - return next(accessError); - } - }, - )(req, res, next); - } catch (error) { - return next(error); - } -}; - -// Health check endpoint (no auth required) -app.get('/api/health', async (req, res) => { - const db = require('./db/models'); - const health = { - status: 'ok', - timestamp: new Date().toISOString(), - uptime: process.uptime(), - environment: process.env.NODE_ENV || 'development', - }; - - try { - await db.sequelize.authenticate(); - health.database = 'connected'; - } catch (error) { - health.status = 'degraded'; - health.database = 'disconnected'; - health.databaseError = error.message; - } - - const statusCode = health.status === 'ok' ? 200 : 503; - res.status(statusCode).json(health); -}); - -app.use('/api/auth', authRoutes); -app.use('/api/runtime-context', runtimeContextRoutes); -app.use('/api/runtime-access', runtimeAccessRoutes); - -app.use('/api/users', jwtAuth, usersRoutes); - -app.use('/api/roles', jwtAuth, rolesRoutes); - -app.use('/api/permissions', jwtAuth, permissionsRoutes); - -const mountRuntimeEntityRoute = (path, entityName, router) => { - app.use( - path, - requireRuntimeReadOrAuth, - blockNonPublicRuntimeListEndpoints(entityName), - sanitizePublicRuntimeListResponse(entityName), - router, - ); -}; - -mountRuntimeEntityRoute('/api/projects', 'projects', projectsRoutes); - -app.use('/api/project_memberships', jwtAuth, project_membershipsRoutes); - -app.use('/api/assets', jwtAuth, assetsRoutes); - -app.use('/api/asset_variants', jwtAuth, asset_variantsRoutes); - -app.use('/api/presigned_url_requests', jwtAuth, presigned_url_requestsRoutes); - -mountRuntimeEntityRoute('/api/tour_pages', 'tour_pages', tour_pagesRoutes); - -mountRuntimeEntityRoute( - '/api/project_audio_tracks', - 'project_audio_tracks', - project_audio_tracksRoutes, -); - -app.use('/api/publish_events', jwtAuth, publish_eventsRoutes); - -app.use('/api/pwa_caches', jwtAuth, pwa_cachesRoutes); - -app.use('/api/access_logs', jwtAuth, access_logsRoutes); -app.use('/api/element-type-defaults', jwtAuth, element_type_defaultsRoutes); -// Backwards compatibility alias for old API endpoint -app.use('/api/ui-elements', jwtAuth, element_type_defaultsRoutes); -app.use( - '/api/project-element-defaults', - jwtAuth, - project_element_defaultsRoutes, -); -// Global transition defaults - routes handle their own auth (GET public, PUT protected) -app.use('/api/global-transition-defaults', global_transition_defaultsRoutes); - -// Project transition settings - routes handle their own auth (production GET public, else protected) -app.use('/api/project-transition-settings', project_transition_settingsRoutes); - -// Global UI controls - routes handle their own auth (GET public, PUT protected) -app.use('/api/global-ui-control-defaults', global_ui_control_defaultsRoutes); - -// Project UI controls - routes handle their own auth (production GET public, else protected) -app.use('/api/project-ui-control-settings', project_ui_control_settingsRoutes); - -app.use('/api/publish', jwtAuth, publishRoutes); - -app.use('/api/openai', jwtAuth, aiLimiter, openaiRoutes); -app.use('/api/ai', jwtAuth, aiLimiter, openaiRoutes); - -app.use('/api/search', jwtAuth, searchLimiter, searchRoutes); -app.use('/api/sql', jwtAuth, sqlRoutes); - -const publicDir = path.join(__dirname, '../public'); - -if (fs.existsSync(publicDir)) { - app.use('/', express.static(publicDir)); - - app.get('*', function (request, response) { - response.sendFile(path.resolve(publicDir, 'index.html')); - }); -} - -// Generic error handler -app.use((err, req, res, _next) => { - if (!res.headersSent) { - logger.error({ err, url: req.url, method: req.method }, 'Unhandled error'); - res.status(500).json({ message: 'Internal server error' }); - } -}); - -const PORT = process.env.NODE_ENV === 'dev_stage' ? 3000 : 8080; - -app.listen(PORT, () => { - logger.info( - { port: PORT, env: process.env.NODE_ENV || 'development' }, - 'Server started', - ); -}); - -module.exports = app; diff --git a/backend/src/index.ts b/backend/src/index.ts new file mode 100644 index 0000000..a6fd557 --- /dev/null +++ b/backend/src/index.ts @@ -0,0 +1,441 @@ +import bodyParser from 'body-parser'; +import cors from 'cors'; +import express from 'express'; +import fs from 'node:fs'; +import path from 'node:path'; +import helmet from 'helmet'; +import swaggerJsDoc from 'swagger-jsdoc'; +import * as swaggerUI from 'swagger-ui-express'; + +import './auth/auth.ts'; +import { authenticateJwt, authenticateJwtWithCallback } from './auth/passport-middleware.ts'; +import config from './config.ts'; +import db from './db/models/index.ts'; +import { wrapAsync } from './helpers.ts'; +import { + blockNonPublicRuntimeListEndpoints, + sanitizePublicRuntimeListResponse, +} from './middlewares/runtime-public.ts'; +import { initializePermissionsMiddleware } from './middlewares/check-permissions.ts'; +import { runtimeContextMiddleware } from './middlewares/runtime-context.ts'; +import { + uploadLimiter, + downloadLimiter, + searchLimiter, +} from './middlewares/rateLimiter.ts'; +import accessLogsRoutesModule from './routes/access_logs.ts'; +import assetVariantsRoutesModule from './routes/asset_variants.ts'; +import assetsRoutesModule from './routes/assets.ts'; +import authRoutesModule from './routes/auth.ts'; +import elementTypeDefaultsRoutesModule from './routes/element_type_defaults.ts'; +import fileRoutesModule from './routes/file.ts'; +import globalTransitionDefaultsRoutesModule from './routes/global_transition_defaults.ts'; +import globalUiControlDefaultsRoutesModule from './routes/global_ui_control_defaults.ts'; +import permissionsRoutesModule from './routes/permissions.ts'; +import presignedUrlRequestsRoutesModule from './routes/presigned_url_requests.ts'; +import projectAudioTracksRoutesModule from './routes/project_audio_tracks.ts'; +import projectElementDefaultsRoutesModule from './routes/project_element_defaults.ts'; +import projectMembershipsRoutesModule from './routes/project_memberships.ts'; +import projectTransitionSettingsRoutesModule from './routes/project_transition_settings.ts'; +import projectUiControlSettingsRoutesModule from './routes/project_ui_control_settings.ts'; +import projectsRoutesModule from './routes/projects.ts'; +import publishRoutesModule from './routes/publish.ts'; +import publishEventsRoutesModule from './routes/publish_events.ts'; +import pwaCachesRoutesModule from './routes/pwa_caches.ts'; +import rolesRoutesModule from './routes/roles.ts'; +import runtimeAccessRoutesModule from './routes/runtime-access.ts'; +import runtimeContextRoutesModule from './routes/runtime-context.ts'; +import searchRoutesModule from './routes/search.ts'; +import sqlRoutesModule from './routes/sql.ts'; +import tourPagesRoutesModule from './routes/tour_pages.ts'; +import usersRoutesModule from './routes/users.ts'; +import RuntimePresentationAccessService from './services/runtime-presentation-access.ts'; +import type { + AppErrorHandler, + ExpressRouter, + HealthResponse, + MountRuntimeEntityRoute, + RuntimeReadOrAuthMiddleware, + SwaggerDocumentOptions, + SwaggerHostMiddleware, + SwaggerUiModuleWithHost, +} from './types/index.ts'; +import { logger, requestLogger } from './utils/logger.ts'; +import { + getRuntimeContext, + setCurrentUser, + setRuntimePublicRequest, +} from './utils/request-context.ts'; + +const app = express(); +const swaggerUiWithHost: SwaggerUiModuleWithHost = swaggerUI; + +initializePermissionsMiddleware(); + +function isExpressRouter(value: unknown): value is ExpressRouter { + return typeof value === 'function'; +} + +function getExpressRouter(name: string, router: unknown): ExpressRouter { + if (!isExpressRouter(router)) { + throw new Error(`Route module ${name} did not export an Express router.`); + } + + return router; +} + +const accessLogsRoutes = getExpressRouter('access_logs', accessLogsRoutesModule); +const assetVariantsRoutes = getExpressRouter( + 'asset_variants', + assetVariantsRoutesModule, +); +const assetsRoutes = getExpressRouter('assets', assetsRoutesModule); +const authRoutes = getExpressRouter('auth', authRoutesModule); +const elementTypeDefaultsRoutes = getExpressRouter( + 'element_type_defaults', + elementTypeDefaultsRoutesModule, +); +const fileRoutes = getExpressRouter('file', fileRoutesModule); +const globalTransitionDefaultsRoutes = getExpressRouter( + 'global_transition_defaults', + globalTransitionDefaultsRoutesModule, +); +const globalUiControlDefaultsRoutes = getExpressRouter( + 'global_ui_control_defaults', + globalUiControlDefaultsRoutesModule, +); +const permissionsRoutes = getExpressRouter('permissions', permissionsRoutesModule); +const presignedUrlRequestsRoutes = getExpressRouter( + 'presigned_url_requests', + presignedUrlRequestsRoutesModule, +); +const projectAudioTracksRoutes = getExpressRouter( + 'project_audio_tracks', + projectAudioTracksRoutesModule, +); +const projectElementDefaultsRoutes = getExpressRouter( + 'project_element_defaults', + projectElementDefaultsRoutesModule, +); +const projectMembershipsRoutes = getExpressRouter( + 'project_memberships', + projectMembershipsRoutesModule, +); +const projectTransitionSettingsRoutes = getExpressRouter( + 'project_transition_settings', + projectTransitionSettingsRoutesModule, +); +const projectUiControlSettingsRoutes = getExpressRouter( + 'project_ui_control_settings', + projectUiControlSettingsRoutesModule, +); +const projectsRoutes = getExpressRouter('projects', projectsRoutesModule); +const publishRoutes = getExpressRouter('publish', publishRoutesModule); +const publishEventsRoutes = getExpressRouter('publish_events', publishEventsRoutesModule); +const pwaCachesRoutes = getExpressRouter('pwa_caches', pwaCachesRoutesModule); +const rolesRoutes = getExpressRouter('roles', rolesRoutesModule); +const runtimeAccessRoutes = getExpressRouter('runtime-access', runtimeAccessRoutesModule); +const runtimeContextRoutes = getExpressRouter( + 'runtime-context', + runtimeContextRoutesModule, +); +const searchRoutes = getExpressRouter('search', searchRoutesModule); +const sqlRoutes = getExpressRouter('sql', sqlRoutesModule); +const tourPagesRoutes = getExpressRouter('tour_pages', tourPagesRoutesModule); +const usersRoutes = getExpressRouter('users', usersRoutesModule); + +const getBaseUrl = (url: string | undefined): string => { + if (!url) return ''; + return url.endsWith('/api') ? url.slice(0, -4) : url; +}; + +const options: SwaggerDocumentOptions = { + definition: { + openapi: '3.0.0', + info: { + version: '1.0.0', + title: 'Tour Builder Platform', + description: + 'Tour Builder Platform Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.', + }, + servers: [ + { + url: getBaseUrl(process.env.NEXT_PUBLIC_BACK_API) || config.swaggerUrl, + description: 'Development server', + }, + ], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + responses: { + UnauthorizedError: { + description: 'Access token is missing or invalid', + }, + }, + }, + security: [ + { + bearerAuth: [], + }, + ], + }, + apis: ['./src/routes/*.{js,ts}'], +}; + +const specs = swaggerJsDoc(options); +const swaggerHostMiddleware: SwaggerHostMiddleware = (req, _res, next) => { + swaggerUiWithHost.host = + getBaseUrl(process.env.NEXT_PUBLIC_BACK_API) || req.get('host') || ''; + next(); +}; + +app.use( + '/api-docs', + swaggerHostMiddleware, + swaggerUI.serve, + swaggerUI.setup(specs), +); + +app.enable('trust proxy'); +app.use( + helmet({ + contentSecurityPolicy: false, + crossOriginEmbedderPolicy: false, + }), +); +app.use(cors({ origin: true, credentials: true })); + +// Request logger applied early so all routes are logged +app.use(requestLogger); + +// Initialize passport JWT auth early (before file routes) +const jwtAuth = authenticateJwt(); + +// Mount file routes BEFORE body-parser to avoid JSON parsing on binary uploads +// These routes handle their own body parsing (JSON for init/finalize, raw streams for chunks) +// Use downloadLimiter for download/presign (high traffic), uploadLimiter for uploads (strict) +app.use('/api/file/download', downloadLimiter); +app.use('/api/file/presign', downloadLimiter); +app.use('/api/file/upload', uploadLimiter); +app.use('/api/file/upload-sessions', uploadLimiter); +app.use('/api/file', fileRoutes); + +// Body parser for all other routes +app.use(bodyParser.json({ limit: '50mb' })); +app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' })); +app.use(runtimeContextMiddleware); + +const requireRuntimeReadOrAuth: RuntimeReadOrAuthMiddleware = wrapAsync(async ( + req, + res, + next, +) => { + try { + const runtimeContext = getRuntimeContext(req); + const headerEnvironment = runtimeContext?.headerEnvironment; + const headerProjectSlug = runtimeContext?.headerProjectSlug; + const isReadOnlyRequest = ['GET', 'OPTIONS'].includes(req.method); + const hasAuthHeader = Boolean(req.headers.authorization); + + // Only production is public. Stage requires authentication (workspace for review). + const isPublicEnvironment = headerEnvironment === 'production'; + + if (!isPublicEnvironment || !isReadOnlyRequest) { + setRuntimePublicRequest(req, false); + jwtAuth(req, res, next); + return; + } + + const isPrivateProductionPresentation = + await RuntimePresentationAccessService.isPrivateProductionPresentation( + headerProjectSlug, + ); + + if (!isPrivateProductionPresentation) { + setRuntimePublicRequest(req, true); + return next(); + } + + if (!hasAuthHeader) { + setRuntimePublicRequest(req, false); + res.status(401).send({ message: 'Authentication required' }); + return; + } + + const privatePresentationAuth = authenticateJwtWithCallback( + async (error, user) => { + if (error) return next(error); + + if (!user) { + setRuntimePublicRequest(req, false); + res.status(401).send({ message: 'Authentication required' }); + return; + } + + setCurrentUser(req, user); + + try { + const canAccess = + await RuntimePresentationAccessService.canUserAccessPrivateProductionPresentation( + user, + headerProjectSlug, + ); + + if (!canAccess) { + setRuntimePublicRequest(req, false); + res.status(403).send({ message: 'Presentation access denied' }); + return; + } + + setRuntimePublicRequest(req, true); + return next(); + } catch (accessError) { + return next(accessError); + } + }, + ); + + privatePresentationAuth(req, res, next); + } catch (error) { + return next(error); + } +}); + +// Health check endpoint (no auth required) +app.get('/api/health', wrapAsync(async (_req, res) => { + const health: HealthResponse = { + status: 'ok', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + environment: process.env.NODE_ENV || 'development', + }; + + try { + await db.sequelize.authenticate(); + health.database = 'connected'; + } catch (error) { + health.status = 'degraded'; + health.database = 'disconnected'; + health.databaseError = + error instanceof Error ? error.message : 'Unknown database error'; + } + + const statusCode = health.status === 'ok' ? 200 : 503; + res.status(statusCode).json(health); +})); + +app.use('/api/auth', authRoutes); +app.use('/api/runtime-context', runtimeContextRoutes); +app.use('/api/runtime-access', runtimeAccessRoutes); + +app.use('/api/users', jwtAuth, usersRoutes); + +app.use('/api/roles', jwtAuth, rolesRoutes); + +app.use('/api/permissions', jwtAuth, permissionsRoutes); + +const mountRuntimeEntityRoute: MountRuntimeEntityRoute = ( + path, + entityName, + router, +) => { + app.use( + path, + requireRuntimeReadOrAuth, + blockNonPublicRuntimeListEndpoints(entityName), + sanitizePublicRuntimeListResponse(entityName), + router, + ); +}; + +mountRuntimeEntityRoute('/api/projects', 'projects', projectsRoutes); + +app.use('/api/project_memberships', jwtAuth, projectMembershipsRoutes); + +app.use('/api/assets', jwtAuth, assetsRoutes); + +app.use('/api/asset_variants', jwtAuth, assetVariantsRoutes); + +app.use('/api/presigned_url_requests', jwtAuth, presignedUrlRequestsRoutes); + +mountRuntimeEntityRoute('/api/tour_pages', 'tour_pages', tourPagesRoutes); + +mountRuntimeEntityRoute( + '/api/project_audio_tracks', + 'project_audio_tracks', + projectAudioTracksRoutes, +); + +app.use('/api/publish_events', jwtAuth, publishEventsRoutes); + +app.use('/api/pwa_caches', jwtAuth, pwaCachesRoutes); + +app.use('/api/access_logs', jwtAuth, accessLogsRoutes); +app.use('/api/element-type-defaults', jwtAuth, elementTypeDefaultsRoutes); +// Backwards compatibility alias for old API endpoint +app.use('/api/ui-elements', jwtAuth, elementTypeDefaultsRoutes); +app.use( + '/api/project-element-defaults', + jwtAuth, + projectElementDefaultsRoutes, +); +// Global transition defaults - routes handle their own auth (GET public, PUT protected) +app.use('/api/global-transition-defaults', globalTransitionDefaultsRoutes); + +// Project transition settings - routes handle their own auth (production GET public, else protected) +app.use('/api/project-transition-settings', projectTransitionSettingsRoutes); + +// Global UI controls - routes handle their own auth (GET public, PUT protected) +app.use('/api/global-ui-control-defaults', globalUiControlDefaultsRoutes); + +// Project UI controls - routes handle their own auth (production GET public, else protected) +app.use('/api/project-ui-control-settings', projectUiControlSettingsRoutes); + +app.use('/api/publish', jwtAuth, publishRoutes); + +app.use('/api/search', jwtAuth, searchLimiter, searchRoutes); +app.use('/api/sql', jwtAuth, sqlRoutes); + +const publicDir = path.join(process.cwd(), 'public'); + +if (fs.existsSync(publicDir)) { + app.use('/', express.static(publicDir)); + + app.get('*', function (_request, response) { + response.sendFile(path.resolve(publicDir, 'index.html')); + }); +} + +// Generic error handler +const appErrorHandler: AppErrorHandler = (err, req, res, _next) => { + if (!res.headersSent) { + logger.error({ err, url: req.url, method: req.method }, 'Unhandled error'); + res.status(500).json({ message: 'Internal server error' }); + } +}; +app.use(appErrorHandler); + +function getServerPort(): number { + const configuredPort = Number.parseInt(process.env.PORT || '', 10); + if (Number.isFinite(configuredPort)) { + return configuredPort; + } + + return process.env.NODE_ENV === 'dev_stage' ? 3000 : 8080; +} + +const PORT = getServerPort(); + +app.listen(PORT, () => { + logger.info( + { port: PORT, env: process.env.NODE_ENV || 'development' }, + 'Server started', + ); +}); + +export default app; diff --git a/backend/src/load-env.ts b/backend/src/load-env.ts new file mode 100644 index 0000000..894c974 --- /dev/null +++ b/backend/src/load-env.ts @@ -0,0 +1,19 @@ +import path from 'path'; + +import dotenv from 'dotenv'; + +const defaultEnvPath = path.resolve(process.cwd(), '.env'); + +function loadEnv(): void { + dotenv.config({ + path: process.env.DOTENV_CONFIG_PATH ?? defaultEnvPath, + }); + + if (!process.env.NODE_ENV) { + process.env.NODE_ENV = 'dev_stage'; + } +} + +loadEnv(); + +export { loadEnv }; diff --git a/backend/src/middlewares/check-permissions.js b/backend/src/middlewares/check-permissions.js deleted file mode 100644 index 790d10a..0000000 --- a/backend/src/middlewares/check-permissions.js +++ /dev/null @@ -1,202 +0,0 @@ -const ForbiddenError = require('../services/notifications/errors/forbidden'); -const RolesDBApi = require('../db/api/roles'); -const { logger } = require('../utils/logger'); -const AccessPolicy = require('../services/access-policy'); - -// Cache for the 'Public' role object -let publicRoleCache = null; - -// Function to asynchronously fetch and cache the 'Public' role -async function fetchAndCachePublicRole() { - try { - // Use RolesDBApi to find the role by name 'Public' - publicRoleCache = await RolesDBApi.findBy({ name: 'Public' }); - - if (!publicRoleCache) { - logger.warn( - { role: 'Public' }, - 'Role not found during permissions middleware startup', - ); - // The system might not function correctly without this role. May need to throw an error or use a fallback stub. - } else { - logger.info( - { role: 'Public', roleId: publicRoleCache.id }, - 'Role loaded and cached', - ); - } - } catch (error) { - logger.error( - { err: error, role: 'Public' }, - 'Error fetching role during permissions middleware startup', - ); - // Handle the error during startup fetch - throw error; // Important to know if the app can proceed without the Public role - } -} - -// Trigger the role fetching when the check-permissions.js module is imported/loaded -// This should happen during application startup when routes are being configured. -fetchAndCachePublicRole().catch((error) => { - // Handle the case where the fetchAndCachePublicRole promise is rejected - logger.error( - { err: error }, - 'Critical error during permissions middleware initialization', - ); -}); - -/** - * Middleware creator to check if the current user (or Public role) has a specific permission. - * @param {string} permission - The name of the required permission. - * @return {import("express").RequestHandler} Express middleware function. - */ -function checkPermissions(permission) { - return async (req, res, next) => { - const { currentUser } = req; - - if (await AccessPolicy.hasPermission(currentUser, permission)) { - return next(); - } - - if (currentUser && AccessPolicy.isPublicUser(currentUser)) { - return next(new ForbiddenError()); - } - - let effectiveRole = null; - try { - if (currentUser && currentUser.app_role) { - effectiveRole = currentUser.app_role; - } else { - if (!publicRoleCache) { - const log = req.log || logger; - log.warn( - { role: 'Public' }, - 'Role cache is empty, attempting synchronous fetch', - ); - effectiveRole = await RolesDBApi.findBy({ name: 'Public' }); - if (!effectiveRole) { - return next( - new Error( - 'Internal Server Error: Public role missing and cannot be fetched.', - ), - ); - } - } else { - effectiveRole = publicRoleCache; - } - } - - if (!effectiveRole) { - return next( - new Error( - 'Internal Server Error: Could not determine effective role.', - ), - ); - } - - const rolePermissionNames = - await AccessPolicy.getRolePermissionNames(effectiveRole); - - if (!rolePermissionNames) { - const log = req.log || logger; - log.error( - { roleId: effectiveRole.id, roleName: effectiveRole.name }, - 'Role object lacks getPermissions method or permissions property', - ); - return next( - new Error('Internal Server Error: Invalid role object format.'), - ); - } - - if (rolePermissionNames.has(permission)) { - next(); - } else { - const roleName = effectiveRole.name || 'unknown role'; - next( - new ForbiddenError( - `Role '${roleName}' denied access to '${permission}'.`, - ), - ); - } - } catch (e) { - // Handle errors during role or permission fetching - const log = req.log || logger; - log.error({ err: e, permission }, 'Error during permission check'); - next(e); // Pass the error to the next middleware - } - }; -} - -const METHOD_MAP = { - POST: 'CREATE', - GET: 'READ', - PUT: 'UPDATE', - PATCH: 'UPDATE', - DELETE: 'DELETE', -}; - -const RUNTIME_PUBLIC_READ_ENTITIES = new Set([ - 'PROJECTS', - 'TOUR_PAGES', - 'PAGE_ELEMENTS', - 'PAGE_LINKS', - 'TRANSITIONS', - 'PROJECT_AUDIO_TRACKS', - 'GLOBAL_TRANSITION_DEFAULTS', - 'PROJECT_TRANSITION_SETTINGS', - 'GLOBAL_UI_CONTROL_DEFAULTS', - 'PROJECT_UI_CONTROL_SETTINGS', -]); - -function getRouteId(req) { - if (req.params?.id) return req.params.id; - - const path = req.path || req.url || ''; - const match = path.match(/^\/([^/?#]+)\/?$/); - if (!match) return null; - - try { - return decodeURIComponent(match[1]); - } catch (_error) { - return match[1]; - } -} - -/** - * Middleware creator to check standard CRUD permissions based on HTTP method and entity name. - * @param {string} name - The name of the entity. - * @return {import("express").RequestHandler} Express middleware function. - */ -function checkCrudPermissions(name) { - return (req, res, next) => { - const isSelfUserRoute = - name === 'users' && - req.currentUser && - req.currentUser.id === getRouteId(req) && - ['GET', 'PUT', 'PATCH'].includes(req.method); - - if (isSelfUserRoute) { - return next(); - } - - const isRuntimePublicRead = - req.isRuntimePublicRequest === true && - req.method === 'GET' && - RUNTIME_PUBLIC_READ_ENTITIES.has(name.toUpperCase()); - - if (isRuntimePublicRead) { - return next(); - } - - // Dynamically determine the permission name (e.g., 'READ_USERS') - const permissionName = - req.permissionNameOverride || - `${METHOD_MAP[req.method]}_${name.toUpperCase()}`; - // Call the checkPermissions middleware with the determined permission - return checkPermissions(permissionName)(req, res, next); - }; -} - -module.exports = { - checkPermissions, - checkCrudPermissions, -}; diff --git a/backend/src/middlewares/check-permissions.ts b/backend/src/middlewares/check-permissions.ts new file mode 100644 index 0000000..cd2fc68 --- /dev/null +++ b/backend/src/middlewares/check-permissions.ts @@ -0,0 +1,224 @@ +import type { NextFunction, Request, RequestHandler, Response } from 'express'; +import ForbiddenError from '../services/notifications/errors/forbidden.ts'; +import RolesDBApi from '../db/api/roles.ts'; +import { logger } from '../utils/logger.ts'; +import AccessPolicy from '../services/access-policy.ts'; +import type { RoleWithPermissionLoader } from '../types/index.ts'; +import { + getCurrentUser, + getPermissionNameOverride, + getRequestLogger, + isRuntimePublicRequest, +} from '../utils/request-context.ts'; + +// Cache for the 'Public' role object +let publicRoleCache: RoleWithPermissionLoader | null = null; + +// Function to asynchronously fetch and cache the 'Public' role +async function fetchAndCachePublicRole(): Promise { + try { + // Use RolesDBApi to find the role by name 'Public' + publicRoleCache = await RolesDBApi.findBy({ name: 'Public' }); + + if (!publicRoleCache) { + logger.warn( + { role: 'Public' }, + 'Role not found during permissions middleware startup', + ); + // The system might not function correctly without this role. May need to throw an error or use a fallback stub. + } else { + logger.info( + { role: 'Public', roleId: publicRoleCache.id }, + 'Role loaded and cached', + ); + } + } catch (error) { + logger.error( + { err: error, role: 'Public' }, + 'Error fetching role during permissions middleware startup', + ); + // Handle the error during startup fetch + throw error; // Important to know if the app can proceed without the Public role + } +} + +function initializePermissionsMiddleware(): void { + fetchAndCachePublicRole().catch((error) => { + logger.error( + { err: error }, + 'Critical error during permissions middleware initialization', + ); + }); +} + +async function checkPermissionRequest( + req: Request, + next: NextFunction, + permission: string, +): Promise { + const currentUser = getCurrentUser(req); + + if (await AccessPolicy.hasPermission(currentUser, permission)) { + return next(); + } + + if (currentUser && AccessPolicy.isPublicUser(currentUser)) { + return next(new ForbiddenError()); + } + + let effectiveRole: RoleWithPermissionLoader | null = null; + try { + if (currentUser && currentUser.app_role) { + effectiveRole = currentUser.app_role; + } else { + if (!publicRoleCache) { + const log = getRequestLogger(req) || logger; + log.warn( + { role: 'Public' }, + 'Role cache is empty, attempting synchronous fetch', + ); + effectiveRole = await RolesDBApi.findBy({ name: 'Public' }); + if (!effectiveRole) { + return next( + new Error( + 'Internal Server Error: Public role missing and cannot be fetched.', + ), + ); + } + } else { + effectiveRole = publicRoleCache; + } + } + + if (!effectiveRole) { + return next( + new Error( + 'Internal Server Error: Could not determine effective role.', + ), + ); + } + + const rolePermissionNames = + await AccessPolicy.getRolePermissionNames(effectiveRole); + + if (!rolePermissionNames) { + const log = getRequestLogger(req) || logger; + log.error( + { roleId: effectiveRole.id, roleName: effectiveRole.name }, + 'Role object lacks getPermissions method or permissions property', + ); + return next( + new Error('Internal Server Error: Invalid role object format.'), + ); + } + + if (rolePermissionNames.has(permission)) { + next(); + } else { + const roleName = effectiveRole.name || 'unknown role'; + next( + new ForbiddenError( + `Role '${roleName}' denied access to '${permission}'.`, + ), + ); + } + } catch (error) { + const log = getRequestLogger(req) || logger; + log.error({ err: error, permission }, 'Error during permission check'); + next(error); + } +} + +function checkPermissions(permission: string): RequestHandler { + return (req: Request, _res: Response, next: NextFunction) => { + checkPermissionRequest(req, next, permission).catch(next); + }; +} + +const METHOD_MAP: Readonly>> = { + POST: 'CREATE', + GET: 'READ', + PUT: 'UPDATE', + PATCH: 'UPDATE', + DELETE: 'DELETE', +}; + +const RUNTIME_PUBLIC_READ_ENTITIES: ReadonlySet = new Set([ + 'PROJECTS', + 'TOUR_PAGES', + 'PAGE_ELEMENTS', + 'PAGE_LINKS', + 'TRANSITIONS', + 'PROJECT_AUDIO_TRACKS', + 'GLOBAL_TRANSITION_DEFAULTS', + 'PROJECT_TRANSITION_SETTINGS', + 'GLOBAL_UI_CONTROL_DEFAULTS', + 'PROJECT_UI_CONTROL_SETTINGS', +]); + +function getCrudPermissionName( + method: string, + name: string, + permissionNameOverride?: string, +): string { + return ( + permissionNameOverride || + `${METHOD_MAP[method]}_${name.toUpperCase()}` + ); +} + +function getRouteId(req: Request): string | null { + if (req.params?.id) return req.params.id; + + const path = req.path || req.url || ''; + const match = path.match(/^\/([^/?#]+)\/?$/); + if (!match) return null; + const routeId = match[1]; + if (!routeId) return null; + + try { + return decodeURIComponent(routeId); + } catch { + return routeId; + } +} + +function checkCrudPermissions(name: string): RequestHandler { + return (req, res, next) => { + const currentUser = getCurrentUser(req); + const isSelfUserRoute = + name === 'users' && + currentUser && + currentUser.id === getRouteId(req) && + ['GET', 'PUT', 'PATCH'].includes(req.method); + + if (isSelfUserRoute) { + return next(); + } + + const isRuntimePublicRead = + isRuntimePublicRequest(req) && + req.method === 'GET' && + RUNTIME_PUBLIC_READ_ENTITIES.has(name.toUpperCase()); + + if (isRuntimePublicRead) { + return next(); + } + + // Dynamically determine the permission name (e.g., 'READ_USERS') + const permissionName = getCrudPermissionName( + req.method, + name, + getPermissionNameOverride(req), + ); + // Call the checkPermissions middleware with the determined permission + return checkPermissions(permissionName)(req, res, next); + }; +} + +export { + checkPermissions, + checkCrudPermissions, + getCrudPermissionName, + initializePermissionsMiddleware, +}; diff --git a/backend/src/middlewares/project-settings-runtime-auth.ts b/backend/src/middlewares/project-settings-runtime-auth.ts new file mode 100644 index 0000000..e658f8b --- /dev/null +++ b/backend/src/middlewares/project-settings-runtime-auth.ts @@ -0,0 +1,172 @@ +import type { NextFunction, Request, RequestHandler, Response } from 'express'; + +import { + authenticateJwt, + authenticateJwtWithCallback, +} from '../auth/passport-middleware.ts'; +import db from '../db/models/index.ts'; +import RuntimePresentationAccessService from '../services/runtime-presentation-access.ts'; +import type { + CurrentUser, + ProjectEnvironmentRouteParams, +} from '../types/index.ts'; +import { isUuidV4 } from '../helpers.ts'; +import { + getRuntimeContext, + setCurrentUser, + setPermissionNameOverride, + setRuntimePublicRequest, +} from '../utils/request-context.ts'; + +const runtimeReadMethods = new Set(['GET', 'OPTIONS']); + +interface ProjectEnvironmentRouteParamCandidate { + projectId?: string; + environment?: string; +} + +const markProjectSettingsReadPublic: RequestHandler = (req, _res, next) => { + if (runtimeReadMethods.has(req.method)) { + setRuntimePublicRequest(req, true); + } + return next(); +}; + +function authenticateProjectSettingsWrites(): RequestHandler { + const jwtAuth = authenticateJwt(); + + return function projectSettingsWriteAuth(req, res, next): void { + if (runtimeReadMethods.has(req.method)) { + next(); + return; + } + + jwtAuth(req, res, next); + }; +} + +const useUpdatePermissionForProjectEnvironmentReset: RequestHandler = ( + req, + _res, + next, +) => { + if ( + req.method === 'DELETE' && + /^\/project\/[^/]+\/env\/[^/]+\/?$/.test(req.path) + ) { + setPermissionNameOverride(req, 'UPDATE_PAGE_ELEMENTS'); + } + return next(); +}; + +function isProjectEnvironmentRouteParams( + params: ProjectEnvironmentRouteParamCandidate, +): params is ProjectEnvironmentRouteParams { + return ( + typeof params.projectId === 'string' && + isUuidV4(params.projectId) && + (params.environment === 'dev' || + params.environment === 'stage' || + params.environment === 'production') + ); +} + +async function getRuntimeProjectSlug(req: Request): Promise { + const runtimeContext = getRuntimeContext(req); + if (runtimeContext?.headerProjectSlug) { + return runtimeContext.headerProjectSlug; + } + + if (!isProjectEnvironmentRouteParams(req.params)) { + return null; + } + + const project = await db.projects.findByPk(req.params.projectId, { + attributes: ['slug'], + }); + return project?.slug ?? null; +} + +async function handlePrivateProductionReadAuth( + req: Request, + res: Response, + next: NextFunction, + user: CurrentUser, + runtimeProjectSlug: string | null, +): Promise { + setCurrentUser(req, user); + + const canAccess = + await RuntimePresentationAccessService.canUserAccessPrivateProductionPresentation( + user, + runtimeProjectSlug, + ); + + if (!canAccess) { + res.status(403).send({ message: 'Presentation access denied' }); + return; + } + + setRuntimePublicRequest(req, true); + next(); +} + +async function handleProjectSettingsReadOrAuth( + req: Request, + res: Response, + next: NextFunction, +): Promise { + const isProduction = req.params.environment === 'production'; + const isReadOnly = runtimeReadMethods.has(req.method); + const jwtAuth = authenticateJwt(); + + const runtimeProjectSlug = await getRuntimeProjectSlug(req); + const isPrivateProductionPresentation = + await RuntimePresentationAccessService.isPrivateProductionPresentation( + runtimeProjectSlug, + ); + + if (isProduction && isReadOnly && !isPrivateProductionPresentation) { + next(); + return; + } + + if (isProduction && isReadOnly && isPrivateProductionPresentation) { + const privateReadAuth = authenticateJwtWithCallback((error, user) => { + if (error) { + next(error); + return; + } + + if (!user) { + res.status(401).send({ message: 'Authentication required' }); + return; + } + + handlePrivateProductionReadAuth(req, res, next, user, runtimeProjectSlug).catch( + next, + ); + }); + + privateReadAuth(req, res, next); + return; + } + + jwtAuth(req, res, next); +} + +const requireProductionProjectSettingsReadOrAuth: RequestHandler = ( + req, + res, + next, +) => { + handleProjectSettingsReadOrAuth(req, res, next).catch(next); +}; + +export { + authenticateProjectSettingsWrites, + isProjectEnvironmentRouteParams, + markProjectSettingsReadPublic, + requireProductionProjectSettingsReadOrAuth, + useUpdatePermissionForProjectEnvironmentReset, +}; diff --git a/backend/src/middlewares/public-read-auth.ts b/backend/src/middlewares/public-read-auth.ts new file mode 100644 index 0000000..a53c1c1 --- /dev/null +++ b/backend/src/middlewares/public-read-auth.ts @@ -0,0 +1,28 @@ +import type { RequestHandler } from 'express'; + +import { authenticateJwt } from '../auth/passport-middleware.ts'; +import { setRuntimePublicRequest } from '../utils/request-context.ts'; + +const PUBLIC_READ_METHODS = new Set(['GET', 'OPTIONS']); + +const markRuntimePublicRead: RequestHandler = (req, _res, next) => { + if (PUBLIC_READ_METHODS.has(req.method)) { + setRuntimePublicRequest(req, true); + } + return next(); +}; + +function authenticateRuntimeWrites(): RequestHandler { + const jwtAuth = authenticateJwt(); + + return function runtimeWriteAuth(req, res, next): void { + if (PUBLIC_READ_METHODS.has(req.method)) { + next(); + return; + } + + jwtAuth(req, res, next); + }; +} + +export { authenticateRuntimeWrites, markRuntimePublicRead }; diff --git a/backend/src/middlewares/rateLimiter.js b/backend/src/middlewares/rateLimiter.ts similarity index 90% rename from backend/src/middlewares/rateLimiter.js rename to backend/src/middlewares/rateLimiter.ts index 5940f14..84ed3a4 100644 --- a/backend/src/middlewares/rateLimiter.js +++ b/backend/src/middlewares/rateLimiter.ts @@ -5,16 +5,20 @@ * memory store with optional Redis support for horizontal scaling. * * Usage: - * const { authLimiter, apiLimiter, uploadLimiter } = require('./middlewares/rateLimiter'); + * import { authLimiter, apiLimiter, uploadLimiter } from './middlewares/rateLimiter.ts'; * app.use('/api/auth', authLimiter); * app.use('/api', apiLimiter); */ -const { logger } = require('../utils/logger'); +import type { RequestHandler } from 'express'; + +import type { RateLimitEntry, RateLimiterOptions } from '../types/index.ts'; +import { logger } from '../utils/logger.ts'; +import { getCurrentUser } from '../utils/request-context.ts'; // In-memory store for rate limiting // For horizontal scaling, replace with Redis store -const rateLimitStore = new Map(); +const rateLimitStore = new Map(); // Cleanup interval for expired entries (every 5 minutes) const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; @@ -49,7 +53,7 @@ setInterval(() => { * @param {Function} [options.skip] - Skip rate limiting for certain requests (req) => boolean * @returns {Function} Express middleware */ -const createRateLimiter = (options = {}) => { +const createRateLimiter = (options: RateLimiterOptions = {}): RequestHandler => { const { keyPrefix = 'rate-limit', windowMs = 15 * 60 * 1000, // 15 minutes @@ -130,7 +134,7 @@ const createRateLimiter = (options = {}) => { // If skipFailedRequests is enabled, decrement on failed response if (skipFailedRequests) { const originalSend = res.send.bind(res); - res.send = function (body) { + res.send = function sendWithRateLimitRefund(body?: unknown) { if (res.statusCode >= 400) { const currentEntry = rateLimitStore.get(key); if (currentEntry && currentEntry.count > 0) { @@ -150,11 +154,13 @@ const createRateLimiter = (options = {}) => { * Create a rate limiter that uses both IP and user ID as key * Useful for authenticated endpoints */ -const createAuthenticatedRateLimiter = (options = {}) => { +const createAuthenticatedRateLimiter = ( + options: RateLimiterOptions = {}, +): RequestHandler => { return createRateLimiter({ ...options, keyGenerator: (req) => { - const userId = req.currentUser?.id || 'anonymous'; + const userId = getCurrentUser(req)?.id || 'anonymous'; const ip = req.ip || 'unknown'; return `${ip}:${userId}`; }, @@ -233,18 +239,7 @@ const searchLimiter = createRateLimiter({ message: 'Too many search requests. Please slow down.', }); -/** - * AI/OpenAI limiter - Strict limits for expensive AI operations - * 20 requests per minute per IP - */ -const aiLimiter = createRateLimiter({ - keyPrefix: 'ai', - windowMs: 60 * 1000, // 1 minute - max: 20, - message: 'Too many AI requests. Please wait before making more.', -}); - -module.exports = { +export { createRateLimiter, createAuthenticatedRateLimiter, authLimiter, @@ -253,5 +248,4 @@ module.exports = { uploadLimiter, downloadLimiter, searchLimiter, - aiLimiter, }; diff --git a/backend/src/middlewares/runtime-context.js b/backend/src/middlewares/runtime-context.js deleted file mode 100644 index 1e2ac2b..0000000 --- a/backend/src/middlewares/runtime-context.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Runtime Context Middleware - * Reads environment and project slug from headers for route-based access. - * Routes: /p/[slug] (production), /p/[slug]/stage (stage), /constructor (dev) - */ - -function runtimeContextMiddleware(req, res, next) { - const context = { - mode: 'admin', - projectSlug: null, - }; - - // Read environment from header (X-Runtime-Environment) - const headerEnvironment = req.headers['x-runtime-environment']; - if ( - headerEnvironment && - ['production', 'stage', 'dev'].includes(headerEnvironment) - ) { - context.headerEnvironment = headerEnvironment; - } - - // Read project slug from header (X-Runtime-Project-Slug) - const headerProjectSlug = req.headers['x-runtime-project-slug']; - if (headerProjectSlug) { - context.headerProjectSlug = headerProjectSlug; - } - - req.runtimeContext = context; - next(); -} - -module.exports = { - runtimeContextMiddleware, -}; diff --git a/backend/src/middlewares/runtime-context.ts b/backend/src/middlewares/runtime-context.ts new file mode 100644 index 0000000..6c63b4e --- /dev/null +++ b/backend/src/middlewares/runtime-context.ts @@ -0,0 +1,30 @@ +import type { RequestHandler } from 'express'; + +import type { RuntimeContext, RuntimeEnvironment } from '../types/index.ts'; +import { setRuntimeContext } from '../utils/request-context.ts'; + +function isRuntimeEnvironment(value: unknown): value is RuntimeEnvironment { + return value === 'production' || value === 'stage' || value === 'dev'; +} + +const runtimeContextMiddleware: RequestHandler = (req, _res, next) => { + const context: RuntimeContext = { + mode: 'admin', + projectSlug: null, + }; + + const headerEnvironment = req.headers['x-runtime-environment']; + if (isRuntimeEnvironment(headerEnvironment)) { + context.headerEnvironment = headerEnvironment; + } + + const headerProjectSlug = req.headers['x-runtime-project-slug']; + if (typeof headerProjectSlug === 'string') { + context.headerProjectSlug = headerProjectSlug; + } + + setRuntimeContext(req, context); + next(); +}; + +export { runtimeContextMiddleware }; diff --git a/backend/src/middlewares/runtime-public.js b/backend/src/middlewares/runtime-public.js deleted file mode 100644 index 6dd9893..0000000 --- a/backend/src/middlewares/runtime-public.js +++ /dev/null @@ -1,159 +0,0 @@ -const PUBLIC_RUNTIME_ENTITY_FIELDS = { - projects: [ - 'id', - 'name', - 'slug', - 'description', - 'logo_url', - 'favicon_url', - 'og_image_url', - 'production_presentation_visibility', - ], - tour_pages: [ - 'id', - 'projectId', - 'environment', - 'source_key', - 'name', - 'slug', - 'sort_order', - 'background_image_url', - 'background_video_url', - 'background_embed_url', - 'background_audio_url', - 'background_loop', - 'requires_auth', - 'ui_schema_json', - 'global_ui_controls_settings_json', - ], - project_audio_tracks: [ - 'id', - 'projectId', - 'environment', - 'source_key', - 'name', - 'slug', - 'url', - 'loop', - 'volume', - 'sort_order', - 'is_enabled', - ], - global_transition_defaults: [ - 'id', - 'transition_type', - 'duration_ms', - 'easing', - 'overlay_color', - ], - project_transition_settings: [ - 'id', - 'projectId', - 'environment', - 'transition_type', - 'duration_ms', - 'easing', - 'overlay_color', - ], -}; - -// Entity-aware path patterns for public runtime access -// Entities not listed here default to allowing only '/' -const PUBLIC_RUNTIME_ALLOWED_PATHS = { - project_transition_settings: [ - '/', - /^\/project\/[a-fA-F0-9-]+\/env\/(dev|stage|production)$/, - ], -}; - -const pickFields = (record, fields) => { - if (!record || typeof record !== 'object') { - return record; - } - - // Convert Sequelize instance to plain object if needed - const plainRecord = - typeof record.get === 'function' ? record.get({ plain: true }) : record; - - return fields.reduce((acc, field) => { - if (field in plainRecord && plainRecord[field] !== undefined) { - acc[field] = plainRecord[field]; - } - return acc; - }, {}); -}; - -const isPublicRuntimeReadRequest = (req) => { - return req.isRuntimePublicRequest === true && req.method === 'GET'; -}; - -const blockNonPublicRuntimeListEndpoints = (entityName) => (req, res, next) => { - if (!isPublicRuntimeReadRequest(req)) { - return next(); - } - - const allowedPaths = PUBLIC_RUNTIME_ALLOWED_PATHS[entityName] || ['/']; - const pathMatches = allowedPaths.some((pattern) => - pattern instanceof RegExp ? pattern.test(req.path) : req.path === pattern, - ); - - if (!pathMatches) { - return res.status(404).send({ message: 'Not found' }); - } - - if (req.query.filetype === 'csv') { - return res.status(404).send({ message: 'Not found' }); - } - - return next(); -}; - -const sanitizePublicRuntimeListResponse = (entityName) => { - const fields = PUBLIC_RUNTIME_ENTITY_FIELDS[entityName] || []; - const allowedPaths = PUBLIC_RUNTIME_ALLOWED_PATHS[entityName] || ['/']; - - return (req, res, next) => { - const pathMatches = allowedPaths.some((pattern) => - pattern instanceof RegExp ? pattern.test(req.path) : req.path === pattern, - ); - - if ( - !isPublicRuntimeReadRequest(req) || - !pathMatches || - fields.length === 0 - ) { - return next(); - } - - const originalSend = res.send.bind(res); - - res.send = (body) => { - if (!body || typeof body !== 'object') { - return originalSend(body); - } - - // Handle list responses with rows array - if (Array.isArray(body.rows)) { - const sanitizedRows = body.rows.map((row) => pickFields(row, fields)); - return originalSend({ - ...body, - rows: sanitizedRows, - }); - } - - // Handle single object responses (e.g., from findOne or project/:id/env/:env) - if (!Array.isArray(body) && body !== null) { - return originalSend(pickFields(body, fields)); - } - - return originalSend(body); - }; - - return next(); - }; -}; - -module.exports = { - blockNonPublicRuntimeListEndpoints, - sanitizePublicRuntimeListResponse, -}; diff --git a/backend/src/middlewares/runtime-public.ts b/backend/src/middlewares/runtime-public.ts new file mode 100644 index 0000000..4d1fb07 --- /dev/null +++ b/backend/src/middlewares/runtime-public.ts @@ -0,0 +1,201 @@ +import type { Request, RequestHandler, Response } from 'express'; + +import type { + RuntimePublicAllowedPathMap, + RuntimePublicFieldMap, + RuntimePublicListResponse, + RuntimePublicPathPattern, + RuntimePublicPlainRecord, + RuntimePublicSequelizeRecord, +} from '../types/index.ts'; +import { isRuntimePublicRequest } from '../utils/request-context.ts'; + +const PUBLIC_RUNTIME_ENTITY_FIELDS: RuntimePublicFieldMap = { + projects: [ + 'id', + 'name', + 'slug', + 'description', + 'logo_url', + 'favicon_url', + 'og_image_url', + 'production_presentation_visibility', + ], + tour_pages: [ + 'id', + 'projectId', + 'environment', + 'source_key', + 'name', + 'slug', + 'sort_order', + 'background_image_url', + 'background_video_url', + 'background_embed_url', + 'background_audio_url', + 'background_loop', + 'requires_auth', + 'ui_schema_json', + 'global_ui_controls_settings_json', + ], + project_audio_tracks: [ + 'id', + 'projectId', + 'environment', + 'source_key', + 'name', + 'slug', + 'url', + 'loop', + 'volume', + 'sort_order', + 'is_enabled', + ], + global_transition_defaults: [ + 'id', + 'transition_type', + 'duration_ms', + 'easing', + 'overlay_color', + ], + project_transition_settings: [ + 'id', + 'projectId', + 'environment', + 'transition_type', + 'duration_ms', + 'easing', + 'overlay_color', + ], +}; + +// Entity-aware path patterns for public runtime access. +// Entities not listed here default to allowing only '/'. +const PUBLIC_RUNTIME_ALLOWED_PATHS: RuntimePublicAllowedPathMap = { + project_transition_settings: [ + '/', + /^\/project\/[a-fA-F0-9-]+\/env\/(dev|stage|production)$/, + ], +}; + +function isRecord(value: unknown): value is RuntimePublicPlainRecord { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function hasPlainGetter( + value: RuntimePublicPlainRecord, +): value is RuntimePublicSequelizeRecord { + return typeof value.get === 'function'; +} + +function toPlainRecord(value: RuntimePublicPlainRecord): RuntimePublicPlainRecord { + return hasPlainGetter(value) ? value.get({ plain: true }) : value; +} + +function isRuntimePublicListResponse( + value: RuntimePublicPlainRecord, +): value is RuntimePublicListResponse { + return Array.isArray(value.rows); +} + +function matchesPublicRuntimePath( + requestPath: string, + pattern: RuntimePublicPathPattern, +): boolean { + return pattern instanceof RegExp + ? pattern.test(requestPath) + : requestPath === pattern; +} + +function getAllowedPaths(entityName: string): readonly RuntimePublicPathPattern[] { + return PUBLIC_RUNTIME_ALLOWED_PATHS[entityName] ?? ['/']; +} + +function pickFields(record: unknown, fields: readonly string[]): unknown { + if (!isRecord(record)) { + return record; + } + + const plainRecord = toPlainRecord(record); + + return fields.reduce((acc, field) => { + if (field in plainRecord && plainRecord[field] !== undefined) { + acc[field] = plainRecord[field]; + } + return acc; + }, {}); +} + +function isPublicRuntimeReadRequest(req: Request): boolean { + return isRuntimePublicRequest(req) && req.method === 'GET'; +} + +const blockNonPublicRuntimeListEndpoints = + (entityName: string): RequestHandler => + (req, res, next) => { + if (!isPublicRuntimeReadRequest(req)) { + next(); + return; + } + + const allowedPaths = getAllowedPaths(entityName); + const pathMatches = allowedPaths.some((pattern) => + matchesPublicRuntimePath(req.path, pattern), + ); + + if (!pathMatches) { + res.status(404).send({ message: 'Not found' }); + return; + } + + if (req.query.filetype === 'csv') { + res.status(404).send({ message: 'Not found' }); + return; + } + + next(); + }; + +function sanitizeBody(body: unknown, fields: readonly string[]): unknown { + if (!isRecord(body)) { + return body; + } + + if (isRuntimePublicListResponse(body)) { + return { + ...body, + rows: body.rows.map((row) => pickFields(row, fields)), + }; + } + + return pickFields(body, fields); +} + +const sanitizePublicRuntimeListResponse = + (entityName: string): RequestHandler => + (req, res, next) => { + const fields = PUBLIC_RUNTIME_ENTITY_FIELDS[entityName] ?? []; + const allowedPaths = getAllowedPaths(entityName); + const pathMatches = allowedPaths.some((pattern) => + matchesPublicRuntimePath(req.path, pattern), + ); + + if ( + !isPublicRuntimeReadRequest(req) || + !pathMatches || + fields.length === 0 + ) { + next(); + return; + } + + const originalSend = res.send.bind(res); + + res.send = function sendPublicRuntimeSanitized(body?: unknown): Response { + return originalSend(sanitizeBody(body, fields)); + }; + + next(); + }; + +export { blockNonPublicRuntimeListEndpoints, sanitizePublicRuntimeListResponse }; diff --git a/backend/src/middlewares/upload.js b/backend/src/middlewares/upload.js deleted file mode 100644 index db07aeb..0000000 --- a/backend/src/middlewares/upload.js +++ /dev/null @@ -1,9 +0,0 @@ -const util = require('util'); -const Multer = require('multer'); - -let processFile = Multer({ - storage: Multer.memoryStorage(), -}).single('file'); - -let processFileMiddleware = util.promisify(processFile); -module.exports = processFileMiddleware; diff --git a/backend/src/middlewares/upload.ts b/backend/src/middlewares/upload.ts new file mode 100644 index 0000000..24fca1f --- /dev/null +++ b/backend/src/middlewares/upload.ts @@ -0,0 +1,34 @@ +import type { Request, Response } from 'express'; +import multer from 'multer'; + +import type { FileUploadProcessor } from '../types/index.ts'; + +const processFile = multer({ + storage: multer.memoryStorage(), +}).single('file'); + +function formatUnknownError(error: unknown): string { + if (typeof error === 'string') return error; + if (typeof error === 'number') return `${error}`; + if (typeof error === 'boolean') return `${error}`; + return 'File upload failed'; +} + +const processFileMiddleware: FileUploadProcessor = ( + req: Request, + res: Response, +) => + new Promise((resolve, reject) => { + processFile(req, res, (error: unknown) => { + if (error) { + reject( + error instanceof Error ? error : new Error(formatUnknownError(error)), + ); + return; + } + + resolve(); + }); + }); + +export default processFileMiddleware; diff --git a/backend/src/middlewares/validate-request.js b/backend/src/middlewares/validate-request.js deleted file mode 100644 index 07a58d9..0000000 --- a/backend/src/middlewares/validate-request.js +++ /dev/null @@ -1,58 +0,0 @@ -const Joi = require('joi'); -const ValidationError = require('../services/notifications/errors/validation'); - -const VALID_REQUEST_PARTS = ['params', 'query', 'body']; - -function formatDetails(details = [], part) { - return details.map((detail) => ({ - path: [part, ...detail.path].join('.'), - message: detail.message, - type: detail.type, - })); -} - -function validateRequest(schemas) { - if (!schemas || typeof schemas !== 'object') { - throw new Error('validateRequest requires a schema map'); - } - - for (const part of Object.keys(schemas)) { - if (!VALID_REQUEST_PARTS.includes(part)) { - throw new Error(`Unsupported request validation part: ${part}`); - } - - if (!Joi.isSchema(schemas[part])) { - throw new Error( - `Request validation schema for ${part} must be a Joi schema`, - ); - } - } - - return function requestValidationMiddleware(req, _res, next) { - for (const part of VALID_REQUEST_PARTS) { - const schema = schemas[part]; - if (!schema) continue; - - const { value, error } = schema.validate(req[part], { - abortEarly: false, - convert: true, - stripUnknown: true, - }); - - if (error) { - return next( - new ValidationError('Invalid request', { - isRequestValidation: true, - details: formatDetails(error.details, part), - }), - ); - } - - req[part] = value; - } - - return next(); - }; -} - -module.exports = { validateRequest }; diff --git a/backend/src/middlewares/validate-request.ts b/backend/src/middlewares/validate-request.ts new file mode 100644 index 0000000..42622a0 --- /dev/null +++ b/backend/src/middlewares/validate-request.ts @@ -0,0 +1,101 @@ +import type { NextFunction, Request, RequestHandler, Response } from 'express'; +import Joi from 'joi'; + +import ValidationError from '../services/notifications/errors/validation.ts'; +import type { + RequestValidationDetail, + RequestValidationPart, + RequestSchemaMap, +} from '../types/index.ts'; + +const VALID_REQUEST_PARTS: RequestValidationPart[] = ['params', 'query', 'body']; + +interface RequestPartsTarget { + params: unknown; + query: unknown; + body: unknown; +} + +function isRequestValidationPart(part: string): part is RequestValidationPart { + return part === 'params' || part === 'query' || part === 'body'; +} + +function formatDetails( + details: Joi.ValidationErrorItem[] = [], + part: RequestValidationPart, +): RequestValidationDetail[] { + return details.map((detail) => ({ + path: [part, ...detail.path].join('.'), + message: detail.message, + type: detail.type, + })); +} + +function assignValidatedPart( + req: RequestPartsTarget, + part: RequestValidationPart, + value: unknown, +): void { + if (part === 'params') { + req.params = value; + return; + } + + if (part === 'query') { + req.query = value; + return; + } + + req.body = value; +} + +function validateRequest(schemas: RequestSchemaMap): RequestHandler { + if (!schemas || typeof schemas !== 'object') { + throw new Error('validateRequest requires a schema map'); + } + + for (const part of Object.keys(schemas)) { + if (!isRequestValidationPart(part)) { + throw new Error(`Unsupported request validation part: ${part}`); + } + + if (!Joi.isSchema(schemas[part])) { + throw new Error( + `Request validation schema for ${part} must be a Joi schema`, + ); + } + } + + return function requestValidationMiddleware( + req: Request, + _res: Response, + next: NextFunction, + ): void { + for (const part of VALID_REQUEST_PARTS) { + const schema = schemas[part]; + if (!schema) continue; + + const result: Joi.ValidationResult = schema.validate(req[part], { + abortEarly: false, + convert: true, + stripUnknown: true, + }); + + if (result.error) { + next( + new ValidationError('Invalid request', { + isRequestValidation: true, + details: formatDetails(result.error.details, part), + }), + ); + return; + } + + assignValidatedPart(req, part, result.value); + } + + next(); + }; +} + +export { validateRequest }; diff --git a/backend/src/routes/access_logs.js b/backend/src/routes/access_logs.ts similarity index 93% rename from backend/src/routes/access_logs.js rename to backend/src/routes/access_logs.ts index ac92b6a..3fedf1c 100644 --- a/backend/src/routes/access_logs.js +++ b/backend/src/routes/access_logs.ts @@ -1,6 +1,6 @@ -const Access_logsService = require('../services/access_logs'); -const Access_logsDBApi = require('../db/api/access_logs'); -const { createEntityRouter } = require('../factories/router.factory'); +import Access_logsDBApi from '../db/api/access_logs.ts'; +import { createEntityRouter } from '../factories/router.factory.ts'; +import Access_logsService from '../services/access_logs.ts'; /** * @swagger @@ -138,7 +138,7 @@ const { createEntityRouter } = require('../factories/router.factory'); * description: Some server error */ -module.exports = createEntityRouter( +export default createEntityRouter( 'access_logs', Access_logsService, Access_logsDBApi, diff --git a/backend/src/routes/asset_variants.js b/backend/src/routes/asset_variants.ts similarity index 93% rename from backend/src/routes/asset_variants.js rename to backend/src/routes/asset_variants.ts index 491649a..e87a52d 100644 --- a/backend/src/routes/asset_variants.js +++ b/backend/src/routes/asset_variants.ts @@ -1,6 +1,6 @@ -const Asset_variantsService = require('../services/asset_variants'); -const Asset_variantsDBApi = require('../db/api/asset_variants'); -const { createEntityRouter } = require('../factories/router.factory'); +import Asset_variantsDBApi from '../db/api/asset_variants.ts'; +import { createEntityRouter } from '../factories/router.factory.ts'; +import Asset_variantsService from '../services/asset_variants.ts'; /** * @swagger @@ -140,7 +140,7 @@ const { createEntityRouter } = require('../factories/router.factory'); * description: Some server error */ -module.exports = createEntityRouter( +export default createEntityRouter( 'asset_variants', Asset_variantsService, Asset_variantsDBApi, diff --git a/backend/src/routes/assets.js b/backend/src/routes/assets.ts similarity index 93% rename from backend/src/routes/assets.js rename to backend/src/routes/assets.ts index efe2d67..1589de0 100644 --- a/backend/src/routes/assets.js +++ b/backend/src/routes/assets.ts @@ -1,6 +1,6 @@ -const AssetsService = require('../services/assets'); -const AssetsDBApi = require('../db/api/assets'); -const { createEntityRouter } = require('../factories/router.factory'); +import AssetsDBApi from '../db/api/assets.ts'; +import { createEntityRouter } from '../factories/router.factory.ts'; +import AssetsService from '../services/assets.ts'; /** * @swagger @@ -152,4 +152,4 @@ const { createEntityRouter } = require('../factories/router.factory'); * description: Some server error */ -module.exports = createEntityRouter('assets', AssetsService, AssetsDBApi); +export default createEntityRouter('assets', AssetsService, AssetsDBApi); diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.ts similarity index 57% rename from backend/src/routes/auth.js rename to backend/src/routes/auth.ts index f535f4c..66e47ba 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.ts @@ -1,42 +1,62 @@ -const express = require('express'); -const passport = require('passport'); +import express from 'express'; +import type { Request, RequestHandler, Response } from 'express'; -const config = require('../config'); -const AuthService = require('../services/auth'); -const ForbiddenError = require('../services/notifications/errors/forbidden'); -const EmailSender = require('../services/email'); -const RuntimePresentationAccessService = require('../services/runtime-presentation-access'); -const wrapAsync = require('../helpers').wrapAsync; -const { - authLimiter: signinLimiter, +import { + authenticateJwt, + authenticatePassport, +} from '../auth/passport-middleware.ts'; +import config from '../config.ts'; +import { commonErrorHandler, wrapAsync } from '../helpers.ts'; +import { + authLimiter as signinLimiter, passwordResetLimiter, -} = require('../middlewares/rateLimiter'); -const { validateRequest } = require('../middlewares/validate-request'); -const { auth: authSchemas } = require('../validators/request-schemas'); +} from '../middlewares/rateLimiter.ts'; +import { validateRequest } from '../middlewares/validate-request.ts'; +import AuthService from '../services/auth.ts'; +import EmailSender from '../services/email/index.ts'; +import ForbiddenError from '../services/notifications/errors/forbidden.ts'; +import RuntimePresentationAccessService from '../services/runtime-presentation-access.ts'; +import type { + AuthMeResponse, + PasswordResetBody, + PasswordUpdateBody, + ProfileBody, + SendPasswordResetEmailBody, + SigninLocalBody, + SocialSigninQuery, + VerifyEmailBody, +} from '../types/index.ts'; +import { + getCurrentUser, + getSocialAuthToken, +} from '../utils/request-context.ts'; +import { getRouteServiceContext } from '../utils/route-context.ts'; +import { auth as authSchemas } from '../validators/request-schemas.ts'; const router = express.Router(); +const jwtAuth = authenticateJwt(); -function safeParseUrl(value) { +function safeParseUrl(value: unknown): URL | null { if (!value || typeof value !== 'string') { return null; } try { return new URL(value); - } catch (error) { + } catch { try { return new URL(`https://${value}`); - } catch (innerError) { + } catch { return null; } } } -function getRequestHost(req) { +function getRequestHost(req: Request): string { const uiUrl = safeParseUrl(config.uiUrl); const fallbackHost = uiUrl ? uiUrl.origin - : config.backUrl || 'http://localhost:3000'; + : config.backUrl ?? 'http://localhost:3000'; const origin = safeParseUrl(req.headers.origin); const referer = safeParseUrl(req.headers.referer); @@ -59,6 +79,33 @@ function getRequestHost(req) { return fallbackHost; } +function getSocialToken(req: Request): string { + const token = getSocialAuthToken(req); + if (!token) { + throw new ForbiddenError(); + } + + return token; +} + +function authenticateSocialStart( + strategy: string, + scope: string[], +): RequestHandler { + return (req, res, next) => { + const middleware = authenticatePassport(strategy, { + scope, + state: req.query.app, + }); + + return middleware(req, res, next); + }; +} + +const socialCallbackRedirect: RequestHandler = (req, res) => { + socialRedirect(res, getSocialToken(req)); +}; + /** * @swagger * components: @@ -111,11 +158,10 @@ router.post( '/signin/local', signinLimiter, validateRequest(authSchemas.signinLocal), - wrapAsync(async (req, res) => { + wrapAsync(async (req: Request, res) => { const payload = await AuthService.signin( req.body.email, req.body.password, - req, ); res.status(200).send(payload); }), @@ -140,17 +186,18 @@ router.post( router.get( '/me', - passport.authenticate('jwt', { session: false }), + jwtAuth, wrapAsync(async (req, res) => { - if (!req.currentUser || !req.currentUser.id) { + const currentUser = getCurrentUser(req); + if (!currentUser?.id) { throw new ForbiddenError(); } - const payload = { - ...req.currentUser, + const payload: AuthMeResponse = { + ...currentUser, allowedPrivateProductionSlugs: await RuntimePresentationAccessService.getAllowedPrivateProductionSlugs( - req.currentUser, + currentUser, ), }; delete payload.password; @@ -161,11 +208,11 @@ router.get( router.put( '/password-reset', validateRequest(authSchemas.passwordReset), - wrapAsync(async (req, res) => { + wrapAsync(async (req: Request, res) => { const payload = await AuthService.passwordReset( req.body.token, req.body.password, - req, + getRouteServiceContext(req), ); res.status(200).send(payload); }), @@ -173,13 +220,13 @@ router.put( router.put( '/password-update', - passport.authenticate('jwt', { session: false }), + jwtAuth, validateRequest(authSchemas.passwordUpdate), - wrapAsync(async (req, res) => { + wrapAsync(async (req: Request, res) => { const payload = await AuthService.passwordUpdate( req.body.currentPassword, req.body.newPassword, - req, + getRouteServiceContext(req), ); res.status(200).send(payload); }), @@ -187,13 +234,14 @@ router.put( router.post( '/send-email-address-verification-email', - passport.authenticate('jwt', { session: false }), + jwtAuth, wrapAsync(async (req, res) => { - if (!req.currentUser) { + const currentUser = getCurrentUser(req); + if (!currentUser?.email) { throw new ForbiddenError(); } - await AuthService.sendEmailAddressVerificationEmail(req.currentUser.email); + await AuthService.sendEmailAddressVerificationEmail(currentUser.email); const payload = true; res.status(200).send(payload); }), @@ -203,24 +251,27 @@ router.post( '/send-password-reset-email', passwordResetLimiter, validateRequest(authSchemas.sendPasswordResetEmail), - wrapAsync(async (req, res) => { + wrapAsync( + async (req: Request, res) => { const host = getRequestHost(req); await AuthService.sendPasswordResetEmail(req.body.email, 'register', host); const payload = true; res.status(200).send(payload); - }), + }, + ), ); router.put( '/profile', - passport.authenticate('jwt', { session: false }), + jwtAuth, validateRequest(authSchemas.profile), - wrapAsync(async (req, res) => { - if (!req.currentUser || !req.currentUser.id) { + wrapAsync(async (req: Request, res) => { + const currentUser = getCurrentUser(req); + if (!currentUser?.id) { throw new ForbiddenError(); } - await AuthService.updateProfile(req.body.profile, req.currentUser); + await AuthService.updateProfile(req.body.profile, currentUser); const payload = true; res.status(200).send(payload); }), @@ -229,17 +280,16 @@ router.put( router.put( '/verify-email', validateRequest(authSchemas.verifyEmail), - wrapAsync(async (req, res) => { + wrapAsync(async (req: Request, res) => { const payload = await AuthService.verifyEmail( req.body.token, - req, - req.headers.referer, + getRouteServiceContext(req), ); res.status(200).send(payload); }), ); -router.get('/email-configured', (req, res) => { +router.get('/email-configured', (_req, res) => { const payload = EmailSender.isConfigured; res.status(200).send(payload); }); @@ -247,52 +297,39 @@ router.get('/email-configured', (req, res) => { router.get( '/signin/google', validateRequest(authSchemas.socialSignin), - (req, res, next) => { - passport.authenticate('google', { - scope: ['profile', 'email'], - state: req.query.app, - })(req, res, next); - }, + authenticateSocialStart('google', ['profile', 'email']), ); router.get( '/signin/google/callback', - passport.authenticate('google', { + authenticatePassport('google', { failureRedirect: '/login', session: false, }), - - function (req, res) { - socialRedirect(res, req.query.state, req.user.token, config); - }, + socialCallbackRedirect, ); router.get( '/signin/microsoft', validateRequest(authSchemas.socialSignin), - (req, res, next) => { - passport.authenticate('microsoft', { - scope: ['https://graph.microsoft.com/user.read openid'], - state: req.query.app, - })(req, res, next); - }, + authenticateSocialStart('microsoft', [ + 'https://graph.microsoft.com/user.read openid', + ]), ); router.get( '/signin/microsoft/callback', - passport.authenticate('microsoft', { + authenticatePassport('microsoft', { failureRedirect: '/login', session: false, }), - function (req, res) { - socialRedirect(res, req.query.state, req.user.token, config); - }, + socialCallbackRedirect, ); -router.use('/', require('../helpers').commonErrorHandler); +router.use('/', commonErrorHandler); -function socialRedirect(res, state, token, config) { +function socialRedirect(res: Response, token: string): void { res.redirect(config.uiUrl + '/login?token=' + token); } -module.exports = router; +export default router; diff --git a/backend/src/routes/element_type_defaults.js b/backend/src/routes/element_type_defaults.js deleted file mode 100644 index f383a64..0000000 --- a/backend/src/routes/element_type_defaults.js +++ /dev/null @@ -1,12 +0,0 @@ -const Element_type_defaultsService = require('../services/element_type_defaults'); -const Element_type_defaultsDBApi = require('../db/api/element_type_defaults'); -const { createEntityRouter } = require('../factories/router.factory'); - -module.exports = createEntityRouter( - 'element_type_defaults', - Element_type_defaultsService, - Element_type_defaultsDBApi, - { - permissionEntity: 'page_elements', - }, -); diff --git a/backend/src/routes/element_type_defaults.ts b/backend/src/routes/element_type_defaults.ts new file mode 100644 index 0000000..aa1ce82 --- /dev/null +++ b/backend/src/routes/element_type_defaults.ts @@ -0,0 +1,12 @@ +import Element_type_defaultsDBApi from '../db/api/element_type_defaults.ts'; +import { createEntityRouter } from '../factories/router.factory.ts'; +import Element_type_defaultsService from '../services/element_type_defaults.ts'; + +export default createEntityRouter( + 'element_type_defaults', + Element_type_defaultsService, + Element_type_defaultsDBApi, + { + permissionEntity: 'page_elements', + }, +); diff --git a/backend/src/routes/file.js b/backend/src/routes/file.js deleted file mode 100644 index 7dcf074..0000000 --- a/backend/src/routes/file.js +++ /dev/null @@ -1,119 +0,0 @@ -const express = require('express'); -const passport = require('passport'); -const bodyParser = require('body-parser'); -const services = require('../services/file/'); -const { isValidPath, createErrorResponse } = require('../services/file'); -const { logger } = require('../utils/logger'); -const { commonErrorHandler } = require('../helpers'); -const { validateRequest } = require('../middlewares/validate-request'); -const { file: fileSchemas } = require('../validators/request-schemas'); - -const router = express.Router(); - -// JSON body parser that ONLY parses application/json content-type -// This prevents errors when binary data is sent or no body is present -const jsonParser = bodyParser.json({ - limit: '1mb', - type: (req) => { - const contentType = req.headers['content-type'] || ''; - return contentType.includes('application/json'); - }, -}); - -router.get('/download', (req, res) => { - services.downloadFile(req, res); -}); - -// POST /api/file/presign - Generate presigned URLs for multiple assets -router.post( - '/presign', - jsonParser, - validateRequest(fileSchemas.presign), - async (req, res) => { - const log = req.log || logger; - const { urls } = req.body || {}; - - // Validate paths for security (no traversal, no protocols) - const unsafeUrls = urls.filter((url) => !isValidPath(url)); - if (unsafeUrls.length > 0) { - log.warn({ unsafeUrls }, 'Presign request with invalid paths rejected'); - return res.status(400).json( - createErrorResponse('Invalid file paths detected', 'INVALID_PATH', { - invalidPaths: unsafeUrls, - }), - ); - } - - try { - const presignedUrls = await services.generatePresignedUrls(urls); - res.json({ presignedUrls }); - } catch (error) { - log.error( - { err: error, urlCount: urls.length }, - 'Failed to generate presigned URLs', - ); - res - .status(500) - .json( - createErrorResponse( - 'Failed to generate presigned URLs', - 'PRESIGN_ERROR', - ), - ); - } - }, -); - -router.post( - '/upload/:table/:field', - passport.authenticate('jwt', { session: false }), - validateRequest(fileSchemas.upload), - (req, res) => { - const fileName = `${req.params.table}/${req.params.field}`; - - services.uploadFile(fileName, req, res); - }, -); - -router.post( - '/upload-sessions/init', - passport.authenticate('jwt', { session: false }), - jsonParser, - validateRequest(fileSchemas.initUploadSession), - (req, res) => { - services.initUploadSession(req, res); - }, -); - -router.get( - '/upload-sessions/:sessionId', - passport.authenticate('jwt', { session: false }), - validateRequest(fileSchemas.session), - (req, res) => { - services.getUploadSession(req, res); - }, -); - -// Chunk upload - NO body parser, raw stream is read directly by uploadChunk -router.put( - '/upload-sessions/:sessionId/chunks/:chunkIndex', - passport.authenticate('jwt', { session: false }), - validateRequest(fileSchemas.chunk), - (req, res) => { - services.uploadChunk(req, res); - }, -); - -// Finalize - NO body parser needed, only uses req.params.sessionId -router.post( - '/upload-sessions/:sessionId/finalize', - passport.authenticate('jwt', { session: false }), - validateRequest(fileSchemas.session), - (req, res) => { - services.finalizeUploadSession(req, res); - }, -); - -router.use('/', commonErrorHandler); - -module.exports = router; diff --git a/backend/src/routes/file.ts b/backend/src/routes/file.ts new file mode 100644 index 0000000..a22779a --- /dev/null +++ b/backend/src/routes/file.ts @@ -0,0 +1,151 @@ +import bodyParser from 'body-parser'; +import express from 'express'; +import type { RequestHandler } from 'express'; +import type { IncomingMessage } from 'http'; + +import { authenticateJwt } from '../auth/passport-middleware.ts'; +import { commonErrorHandler, wrapAsync } from '../helpers.ts'; +import { validateRequest } from '../middlewares/validate-request.ts'; +import services from '../services/file/index.ts'; +import type { + FileErrorResponse, + FilePresignRequest, + FilePresignResponse, + FileUploadParams, + PresignSuccessResponse, + UploadChunkParams, + UploadSessionParams, +} from '../types/index.ts'; +import { logger } from '../utils/logger.ts'; +import { getRequestLogger } from '../utils/request-context.ts'; +import { file as fileSchemas } from '../validators/request-schemas.ts'; + +const router = express.Router(); + +// JSON body parser that ONLY parses application/json content-type +// This prevents errors when binary data is sent or no body is present +const jsonParser = bodyParser.json({ + limit: '1mb', + type: (req: IncomingMessage) => { + const contentType = req.headers['content-type'] || ''; + return contentType.includes('application/json'); + }, +}); + +const jwtAuth = authenticateJwt(); + +const downloadHandler: RequestHandler = wrapAsync(async (req, res) => + services.downloadFile(req, res), +); + +const presignHandler = async ( + req: FilePresignRequest, + res: FilePresignResponse, +) => { + const log = getRequestLogger(req) || logger; + const { urls } = req.body; + + // Validate paths for security (no traversal, no protocols) + const unsafeUrls = urls.filter((url) => !services.isValidPath(url)); + if (unsafeUrls.length > 0) { + log.warn({ unsafeUrls }, 'Presign request with invalid paths rejected'); + const response = services.createErrorResponse( + 'Invalid file paths detected', + 'INVALID_PATH', + { + invalidPaths: unsafeUrls, + }, + ); + return res.status(400).json(response); + } + + try { + const presignedUrls = await services.generatePresignedUrls(urls); + const response: PresignSuccessResponse = { presignedUrls }; + return res.json(response); + } catch (error) { + log.error( + { err: error, urlCount: urls.length }, + 'Failed to generate presigned URLs', + ); + const response: FileErrorResponse = services.createErrorResponse( + 'Failed to generate presigned URLs', + 'PRESIGN_ERROR', + ); + return res.status(500).json(response); + } +}; + +const uploadHandler: RequestHandler = wrapAsync( + async (req, res) => { + const fileName = `${req.params.table}/${req.params.field}`; + return services.uploadFile(fileName, req, res); + }, +); + +const initUploadSessionHandler: RequestHandler = wrapAsync((req, res) => + services.initUploadSession(req, res), +); + +const getUploadSessionHandler: RequestHandler = wrapAsync( + (req, res) => services.getUploadSession(req, res), +); + +const uploadChunkHandler: RequestHandler = wrapAsync( + (req, res) => services.uploadChunk(req, res), +); + +const finalizeUploadSessionHandler: RequestHandler = + wrapAsync((req, res) => services.finalizeUploadSession(req, res)); + +router.get('/download', downloadHandler); + +// POST /api/file/presign - Generate presigned URLs for multiple assets +router.post( + '/presign', + jsonParser, + validateRequest(fileSchemas.presign), + wrapAsync(presignHandler), +); + +router.post( + '/upload/:table/:field', + jwtAuth, + validateRequest(fileSchemas.upload), + uploadHandler, +); + +router.post( + '/upload-sessions/init', + jwtAuth, + jsonParser, + validateRequest(fileSchemas.initUploadSession), + initUploadSessionHandler, +); + +router.get( + '/upload-sessions/:sessionId', + jwtAuth, + validateRequest(fileSchemas.session), + getUploadSessionHandler, +); + +// Chunk upload - NO body parser, raw stream is read directly by uploadChunk +router.put( + '/upload-sessions/:sessionId/chunks/:chunkIndex', + jwtAuth, + validateRequest(fileSchemas.chunk), + uploadChunkHandler, +); + +// Finalize - NO body parser needed, only uses req.params.sessionId +router.post( + '/upload-sessions/:sessionId/finalize', + jwtAuth, + validateRequest(fileSchemas.session), + finalizeUploadSessionHandler, +); + +router.use('/', commonErrorHandler); + +export default router; diff --git a/backend/src/routes/global_transition_defaults.js b/backend/src/routes/global_transition_defaults.ts similarity index 56% rename from backend/src/routes/global_transition_defaults.js rename to backend/src/routes/global_transition_defaults.ts index 1f76a19..834038f 100644 --- a/backend/src/routes/global_transition_defaults.js +++ b/backend/src/routes/global_transition_defaults.ts @@ -1,40 +1,24 @@ -const express = require('express'); -const passport = require('passport'); -const Global_transition_defaultsService = require('../services/global_transition_defaults'); -const Global_transition_defaultsDBApi = require('../db/api/global_transition_defaults'); -const { - wrapAsync, - commonErrorHandler, - isUuidV4, - assertRouteIdMatchesBody, -} = require('../helpers'); -const { checkCrudPermissions } = require('../middlewares/check-permissions'); +import express from 'express'; + +import GlobalTransitionDefaultsDBApi from '../db/api/global_transition_defaults.ts'; +import { commonErrorHandler, isUuidV4, wrapAsync } from '../helpers.ts'; +import { checkCrudPermissions } from '../middlewares/check-permissions.ts'; +import { + authenticateRuntimeWrites, + markRuntimePublicRead, +} from '../middlewares/public-read-auth.ts'; +import GlobalTransitionDefaultsService from '../services/global_transition_defaults.ts'; +import { + assertBodyIdMatchesRouteId, + isEntityDataRequestBody, +} from '../utils/request-body.ts'; +import { isGlobalTransitionDefaultsData } from '../utils/global-defaults.ts'; +import { getRouteServiceContext } from '../utils/route-context.ts'; const router = express.Router(); -const jwtAuth = passport.authenticate('jwt', { session: false }); -/** - * Middleware for public GET access. - * Marks GET requests as public runtime requests for permission bypass. - * MUST run before checkCrudPermissions to set the flag first. - */ -const allowPublicRead = (req, _res, next) => { - if (['GET', 'OPTIONS'].includes(req.method)) { - req.isRuntimePublicRequest = true; - } - return next(); -}; - -const authenticateWrites = (req, res, next) => { - if (['GET', 'OPTIONS'].includes(req.method)) { - return next(); - } - return jwtAuth(req, res, next); -}; - -// Apply public read first, then CRUD permission checks -router.use(allowPublicRead); -router.use(authenticateWrites); +router.use(markRuntimePublicRead); +router.use(authenticateRuntimeWrites()); router.use(checkCrudPermissions('page_elements')); /** @@ -51,8 +35,8 @@ router.use(checkCrudPermissions('page_elements')); router.get( '/', wrapAsync(async (_req, res) => { - const payload = await Global_transition_defaultsDBApi.findOne(); - res.status(200).send(payload); + const payload = await GlobalTransitionDefaultsDBApi.findOne(); + return res.status(200).send(payload); }), ); @@ -77,14 +61,15 @@ router.get( router.get( '/:id', wrapAsync(async (req, res) => { - if (!isUuidV4(req.params.id)) { + const { id } = req.params; + if (typeof id !== 'string' || !isUuidV4(id)) { return res.status(400).send('Invalid global_transition_defaults id'); } - const payload = await Global_transition_defaultsDBApi.findBy({ - id: req.params.id, + const payload = await GlobalTransitionDefaultsDBApi.findBy({ + id, }); - res.status(200).send(payload); + return res.status(200).send(payload); }), ); @@ -129,17 +114,32 @@ router.get( router.put( '/:id', wrapAsync(async (req, res) => { - assertRouteIdMatchesBody(req); - await Global_transition_defaultsService.update({ - id: req.params.id, - data: req.body.data, - currentUser: req.currentUser, - runtimeContext: req.runtimeContext, + const { id } = req.params; + if (typeof id !== 'string' || !isUuidV4(id)) { + return res.status(400).send('Invalid global_transition_defaults id'); + } + + const body: unknown = req.body; + if (!isEntityDataRequestBody(body)) { + return res.status(400).send('Request body data is required'); + } + + const { data } = body; + if (!isGlobalTransitionDefaultsData(data)) { + return res.status(400).send('Invalid global_transition_defaults data'); + } + + assertBodyIdMatchesRouteId(id, body); + + await GlobalTransitionDefaultsService.update({ + id, + data, + ...getRouteServiceContext(req), }); - res.status(200).send(true); + return res.status(200).send(true); }), ); router.use('/', commonErrorHandler); -module.exports = router; +export default router; diff --git a/backend/src/routes/global_ui_control_defaults.js b/backend/src/routes/global_ui_control_defaults.js deleted file mode 100644 index 10fbf46..0000000 --- a/backend/src/routes/global_ui_control_defaults.js +++ /dev/null @@ -1,67 +0,0 @@ -const express = require('express'); -const passport = require('passport'); -const Global_ui_control_defaultsService = require('../services/global_ui_control_defaults'); -const Global_ui_control_defaultsDBApi = require('../db/api/global_ui_control_defaults'); -const { wrapAsync, commonErrorHandler, isUuidV4 } = require('../helpers'); -const { checkCrudPermissions } = require('../middlewares/check-permissions'); - -const router = express.Router(); -const jwtAuth = passport.authenticate('jwt', { session: false }); - -const allowPublicRead = (req, _res, next) => { - if (['GET', 'OPTIONS'].includes(req.method)) { - req.isRuntimePublicRequest = true; - } - return next(); -}; - -const authenticateWrites = (req, res, next) => { - if (['GET', 'OPTIONS'].includes(req.method)) { - return next(); - } - return jwtAuth(req, res, next); -}; - -router.use(allowPublicRead); -router.use(authenticateWrites); -router.use(checkCrudPermissions('page_elements')); - -router.get( - '/', - wrapAsync(async (_req, res) => { - const payload = await Global_ui_control_defaultsDBApi.findOne(); - res.status(200).send(payload); - }), -); - -router.get( - '/:id', - wrapAsync(async (req, res) => { - if (!isUuidV4(req.params.id)) { - return res.status(400).send('Invalid global_ui_control_defaults id'); - } - - const payload = await Global_ui_control_defaultsDBApi.findBy({ - id: req.params.id, - }); - res.status(200).send(payload); - }), -); - -router.put( - '/:id', - wrapAsync(async (req, res) => { - await Global_ui_control_defaultsService.update({ - id: req.params.id, - data: req.body.data, - currentUser: req.currentUser, - runtimeContext: req.runtimeContext, - }); - const payload = await Global_ui_control_defaultsDBApi.findOne(); - res.status(200).send(payload); - }), -); - -router.use('/', commonErrorHandler); - -module.exports = router; diff --git a/backend/src/routes/global_ui_control_defaults.ts b/backend/src/routes/global_ui_control_defaults.ts new file mode 100644 index 0000000..5c0f608 --- /dev/null +++ b/backend/src/routes/global_ui_control_defaults.ts @@ -0,0 +1,79 @@ +import express from 'express'; + +import GlobalUiControlDefaultsDBApi from '../db/api/global_ui_control_defaults.ts'; +import { commonErrorHandler, isUuidV4, wrapAsync } from '../helpers.ts'; +import { checkCrudPermissions } from '../middlewares/check-permissions.ts'; +import { + authenticateRuntimeWrites, + markRuntimePublicRead, +} from '../middlewares/public-read-auth.ts'; +import GlobalUiControlDefaultsService from '../services/global_ui_control_defaults.ts'; +import { + assertBodyIdMatchesRouteId, + isEntityDataRequestBody, +} from '../utils/request-body.ts'; +import { isGlobalUiControlDefaultsData } from '../utils/global-defaults.ts'; +import { getRouteServiceContext } from '../utils/route-context.ts'; + +const router = express.Router(); + +router.use(markRuntimePublicRead); +router.use(authenticateRuntimeWrites()); +router.use(checkCrudPermissions('page_elements')); + +router.get( + '/', + wrapAsync(async (_req, res) => { + const payload = await GlobalUiControlDefaultsDBApi.findOne(); + return res.status(200).send(payload); + }), +); + +router.get( + '/:id', + wrapAsync(async (req, res) => { + const { id } = req.params; + if (typeof id !== 'string' || !isUuidV4(id)) { + return res.status(400).send('Invalid global_ui_control_defaults id'); + } + + const payload = await GlobalUiControlDefaultsDBApi.findBy({ + id, + }); + return res.status(200).send(payload); + }), +); + +router.put( + '/:id', + wrapAsync(async (req, res) => { + const { id } = req.params; + if (typeof id !== 'string' || !isUuidV4(id)) { + return res.status(400).send('Invalid global_ui_control_defaults id'); + } + + const body: unknown = req.body; + if (!isEntityDataRequestBody(body)) { + return res.status(400).send('Request body data is required'); + } + + const { data } = body; + if (!isGlobalUiControlDefaultsData(data)) { + return res.status(400).send('Invalid global_ui_control_defaults data'); + } + + assertBodyIdMatchesRouteId(id, body); + + await GlobalUiControlDefaultsService.update({ + id, + data, + ...getRouteServiceContext(req), + }); + const payload = await GlobalUiControlDefaultsDBApi.findOne(); + return res.status(200).send(payload); + }), +); + +router.use('/', commonErrorHandler); + +export default router; diff --git a/backend/src/routes/openai.js b/backend/src/routes/openai.js deleted file mode 100644 index 06235c6..0000000 --- a/backend/src/routes/openai.js +++ /dev/null @@ -1,337 +0,0 @@ -const express = require('express'); -const db = require('../db/models'); -const wrapAsync = require('../helpers').wrapAsync; -const router = express.Router(); -const sjs = require('sequelize-json-schema'); -const { getWidget, askGpt } = require('../services/openai'); -const { LocalAIApi } = require('../ai/LocalAIApi'); - -const loadRolesModules = () => { - try { - return { - RolesService: require('../services/roles'), - RolesDBApi: require('../db/api/roles'), - }; - } catch (error) { - console.error( - 'Roles modules are missing. Advanced roles are required for this endpoint.', - error, - ); - const err = new Error( - 'Roles modules are missing. Advanced roles are required for this endpoint.', - ); - err.originalError = error; - throw err; - } -}; - -/** - * @swagger - * /api/roles/roles-info/{infoId}: - * delete: - * security: - * - bearerAuth: [] - * tags: [Roles] - * summary: Remove role information by ID - * description: Remove specific role information by ID - * parameters: - * - in: path - * name: infoId - * description: ID of role information to remove - * required: true - * schema: - * type: string - * - in: query - * name: userId - * description: ID of the user - * required: true - * schema: - * type: string - * - in: query - * name: key - * description: Key of the role information to remove - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Role information successfully removed - * content: - * application/json: - * schema: - * type: object - * properties: - * user: - * type: string - * description: The user information - * 400: - * description: Invalid ID or key supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Role not found - * 500: - * description: Some server error - */ - -router.delete( - '/roles-info/:infoId', - wrapAsync(async (req, res) => { - const { RolesService } = loadRolesModules(); - const role = await RolesService.removeRoleInfoById( - req.query.infoId, - req.query.roleId, - req.query.key, - req.currentUser, - ); - - res.status(200).send(role); - }), -); - -/** - * @swagger - * /api/roles/role-info/{roleId}: - * get: - * security: - * - bearerAuth: [] - * tags: [Roles] - * summary: Get role information by key - * description: Get specific role information by key - * parameters: - * - in: path - * name: roleId - * description: ID of role to get information for - * required: true - * schema: - * type: string - * - in: query - * name: key - * description: Key of the role information to retrieve - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Role information successfully received - * content: - * application/json: - * schema: - * type: object - * properties: - * info: - * type: string - * description: The role information - * 400: - * description: Invalid ID or key supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Role not found - * 500: - * description: Some server error - */ - -router.get( - '/info-by-key', - wrapAsync(async (req, res) => { - const { RolesService, RolesDBApi } = loadRolesModules(); - const roleId = req.query.roleId; - const key = req.query.key; - const currentUser = req.currentUser; - let info = await RolesService.getRoleInfoByKey(key, roleId, currentUser); - const role = await RolesDBApi.findBy({ id: roleId }); - if (!role?.role_customization) { - try { - await Promise.all( - ['pie', 'bar'].map(async (e) => { - const schema = await sjs.getSequelizeSchema(db.sequelize, {}); - const payload = { - description: `Create some cool ${e} chart`, - modelDefinition: schema.definitions, - }; - const widgetId = await getWidget(payload, currentUser?.id, roleId); - if (widgetId) { - await RolesService.addRoleInfo( - roleId, - currentUser?.id, - 'widgets', - widgetId, - req.currentUser, - ); - } - }), - ); - info = await RolesService.getRoleInfoByKey(key, roleId, currentUser); - } catch (error) { - console.warn( - 'Widget creation skipped (external API unavailable):', - error.message, - ); - // Return empty widgets when external API is unavailable - if (key === 'widgets') { - info = []; - } - } - } - res.status(200).send(info); - }), -); - -router.post( - '/create_widget', - wrapAsync(async (req, res) => { - const { RolesService } = loadRolesModules(); - const { description, userId, roleId } = req.body; - - const currentUser = req.currentUser; - const schema = await sjs.getSequelizeSchema(db.sequelize, {}); - const payload = { - description, - modelDefinition: schema.definitions, - }; - - const widgetId = await getWidget(payload, userId, roleId); - - if (widgetId) { - await RolesService.addRoleInfo( - roleId, - userId, - 'widgets', - widgetId, - currentUser, - ); - - return res.status(200).send(widgetId); - } else { - return res.status(400).send(widgetId); - } - }), -); - -/** - * @swagger - * /api/openai/response: - * post: - * security: - * - bearerAuth: [] - * tags: [OpenAI] - * summary: Proxy a Responses API request - * description: Sends the payload to the Flatlogic AI proxy and returns the response. - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * input: - * type: array - * description: List of messages with roles and content. - * items: - * type: object - * properties: - * role: - * type: string - * content: - * type: string - * options: - * type: object - * description: Optional polling controls. - * properties: - * poll_interval: - * type: number - * poll_timeout: - * type: number - * responses: - * 200: - * description: AI response received - * 400: - * description: Invalid request - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 502: - * description: Proxy error - */ -router.post( - '/response', - wrapAsync(async (req, res) => { - const body = req.body || {}; - const options = body.options || {}; - const payload = { ...body }; - delete payload.options; - - const response = await LocalAIApi.createResponse(payload, options); - - if (response.success) { - return res.status(200).send(response); - } - - console.error('AI proxy error:', response); - const status = response.error === 'input_missing' ? 400 : 502; - return res.status(status).send(response); - }), -); - -/** - * @swagger - * /api/openai/ask: - * post: - * security: - * - bearerAuth: [] - * tags: [OpenAI] - * summary: Ask a question to ChatGPT - * description: Send a question through the Flatlogic AI proxy and get a response - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * prompt: - * type: string - * description: The question to ask ChatGPT - * responses: - * 200: - * description: Question successfully answered - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Whether the request was successful - * data: - * type: string - * description: The answer from ChatGPT - * 400: - * description: Invalid request - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 500: - * description: Some server error - */ -router.post( - '/ask-gpt', - wrapAsync(async (req, res) => { - const { prompt } = req.body; - if (!prompt) { - return res.status(400).send({ - success: false, - error: 'Prompt is required', - }); - } - - const response = await askGpt(prompt); - - if (response.success) { - return res.status(200).send(response); - } else { - return res.status(500).send(response); - } - }), -); - -module.exports = router; diff --git a/backend/src/routes/permissions.js b/backend/src/routes/permissions.ts similarity index 95% rename from backend/src/routes/permissions.js rename to backend/src/routes/permissions.ts index b73fa37..bd8e805 100644 --- a/backend/src/routes/permissions.js +++ b/backend/src/routes/permissions.ts @@ -1,6 +1,6 @@ -const PermissionsService = require('../services/permissions'); -const PermissionsDBApi = require('../db/api/permissions'); -const { createEntityRouter } = require('../factories/router.factory'); +import PermissionsDBApi from '../db/api/permissions.ts'; +import { createEntityRouter } from '../factories/router.factory.ts'; +import PermissionsService from '../services/permissions.ts'; /** * @swagger @@ -181,7 +181,7 @@ const { createEntityRouter } = require('../factories/router.factory'); * description: Some server error */ -module.exports = createEntityRouter( +export default createEntityRouter( 'permissions', PermissionsService, PermissionsDBApi, diff --git a/backend/src/routes/presigned_url_requests.js b/backend/src/routes/presigned_url_requests.ts similarity index 93% rename from backend/src/routes/presigned_url_requests.js rename to backend/src/routes/presigned_url_requests.ts index c6afed1..64c69c7 100644 --- a/backend/src/routes/presigned_url_requests.js +++ b/backend/src/routes/presigned_url_requests.ts @@ -1,6 +1,6 @@ -const Presigned_url_requestsService = require('../services/presigned_url_requests'); -const Presigned_url_requestsDBApi = require('../db/api/presigned_url_requests'); -const { createEntityRouter } = require('../factories/router.factory'); +import Presigned_url_requestsDBApi from '../db/api/presigned_url_requests.ts'; +import { createEntityRouter } from '../factories/router.factory.ts'; +import Presigned_url_requestsService from '../services/presigned_url_requests.ts'; /** * @swagger @@ -143,7 +143,7 @@ const { createEntityRouter } = require('../factories/router.factory'); * description: Some server error */ -module.exports = createEntityRouter( +export default createEntityRouter( 'presigned_url_requests', Presigned_url_requestsService, Presigned_url_requestsDBApi, diff --git a/backend/src/routes/project_audio_tracks.js b/backend/src/routes/project_audio_tracks.ts similarity index 93% rename from backend/src/routes/project_audio_tracks.js rename to backend/src/routes/project_audio_tracks.ts index 138865d..d0ebc9b 100644 --- a/backend/src/routes/project_audio_tracks.js +++ b/backend/src/routes/project_audio_tracks.ts @@ -1,6 +1,6 @@ -const Project_audio_tracksService = require('../services/project_audio_tracks'); -const Project_audio_tracksDBApi = require('../db/api/project_audio_tracks'); -const { createEntityRouter } = require('../factories/router.factory'); +import Project_audio_tracksDBApi from '../db/api/project_audio_tracks.ts'; +import { createEntityRouter } from '../factories/router.factory.ts'; +import Project_audio_tracksService from '../services/project_audio_tracks.ts'; /** * @swagger @@ -144,7 +144,7 @@ const { createEntityRouter } = require('../factories/router.factory'); * description: Some server error */ -module.exports = createEntityRouter( +export default createEntityRouter( 'project_audio_tracks', Project_audio_tracksService, Project_audio_tracksDBApi, diff --git a/backend/src/routes/project_element_defaults.js b/backend/src/routes/project_element_defaults.ts similarity index 57% rename from backend/src/routes/project_element_defaults.js rename to backend/src/routes/project_element_defaults.ts index fe8c751..f29e923 100644 --- a/backend/src/routes/project_element_defaults.js +++ b/backend/src/routes/project_element_defaults.ts @@ -1,13 +1,14 @@ -const Project_element_defaultsService = require('../services/project_element_defaults'); -const Project_element_defaultsDBApi = require('../db/api/project_element_defaults'); -const { createEntityRouter } = require('../factories/router.factory'); -const wrapAsync = require('../helpers').wrapAsync; +import ProjectElementDefaultsDBApi from '../db/api/project_element_defaults.ts'; +import { createEntityRouter } from '../factories/router.factory.ts'; +import { wrapAsync } from '../helpers.ts'; +import ProjectElementDefaultsService from '../services/project_element_defaults.ts'; +import type { RouteMessageResponse } from '../types/index.ts'; +import { getRouteServiceContext } from '../utils/route-context.ts'; -// Create base router with standard CRUD operations const baseRouter = createEntityRouter( 'project_element_defaults', - Project_element_defaultsService, - Project_element_defaultsDBApi, + ProjectElementDefaultsService, + ProjectElementDefaultsDBApi, { permissionEntity: 'page_elements', }, @@ -38,11 +39,17 @@ const baseRouter = createEntityRouter( baseRouter.post( '/:id/reset', wrapAsync(async (req, res) => { - const payload = await Project_element_defaultsService.resetToGlobal( - req.params.id, - { currentUser: req.currentUser }, + const { id } = req.params; + if (typeof id !== 'string') { + const response: RouteMessageResponse = { message: 'Invalid ID' }; + return res.status(400).json(response); + } + + const payload = await ProjectElementDefaultsService.resetToGlobal( + id, + getRouteServiceContext(req), ); - res.status(200).json(payload); + return res.status(200).json(payload); }), ); @@ -71,11 +78,15 @@ baseRouter.post( baseRouter.get( '/:id/diff', wrapAsync(async (req, res) => { - const payload = await Project_element_defaultsService.getDiffFromGlobal( - req.params.id, - ); - res.status(200).json(payload); + const { id } = req.params; + if (typeof id !== 'string') { + const response: RouteMessageResponse = { message: 'Invalid ID' }; + return res.status(400).json(response); + } + + const payload = await ProjectElementDefaultsService.getDiffFromGlobal(id); + return res.status(200).json(payload); }), ); -module.exports = baseRouter; +export default baseRouter; diff --git a/backend/src/routes/project_memberships.js b/backend/src/routes/project_memberships.ts similarity index 93% rename from backend/src/routes/project_memberships.js rename to backend/src/routes/project_memberships.ts index a7d918d..d927d75 100644 --- a/backend/src/routes/project_memberships.js +++ b/backend/src/routes/project_memberships.ts @@ -1,6 +1,6 @@ -const Project_membershipsService = require('../services/project_memberships'); -const Project_membershipsDBApi = require('../db/api/project_memberships'); -const { createEntityRouter } = require('../factories/router.factory'); +import Project_membershipsDBApi from '../db/api/project_memberships.ts'; +import { createEntityRouter } from '../factories/router.factory.ts'; +import Project_membershipsService from '../services/project_memberships.ts'; /** * @swagger @@ -138,7 +138,7 @@ const { createEntityRouter } = require('../factories/router.factory'); * description: Some server error */ -module.exports = createEntityRouter( +export default createEntityRouter( 'project_memberships', Project_membershipsService, Project_membershipsDBApi, diff --git a/backend/src/routes/project_transition_settings.js b/backend/src/routes/project_transition_settings.js deleted file mode 100644 index 320c4aa..0000000 --- a/backend/src/routes/project_transition_settings.js +++ /dev/null @@ -1,478 +0,0 @@ -const express = require('express'); -const passport = require('passport'); -const db = require('../db/models'); -const Project_transition_settingsService = require('../services/project_transition_settings'); -const Project_transition_settingsDBApi = require('../db/api/project_transition_settings'); -const { - wrapAsync, - commonErrorHandler, - isUuidV4, - assertRouteIdMatchesBody, -} = require('../helpers'); -const { checkCrudPermissions } = require('../middlewares/check-permissions'); -const RuntimePresentationAccessService = require('../services/runtime-presentation-access'); - -const router = express.Router(); -const jwtAuth = passport.authenticate('jwt', { session: false }); - -/** - * Middleware: Mark authenticated reads as public to bypass permission check. - * Constructor page users are authenticated but may not have explicit permission. - */ -const allowAuthenticatedRead = (req, _res, next) => { - if (['GET', 'OPTIONS'].includes(req.method)) { - req.isRuntimePublicRequest = true; - } - return next(); -}; - -const authenticateWrites = (req, res, next) => { - if (['GET', 'OPTIONS'].includes(req.method)) { - return next(); - } - return jwtAuth(req, res, next); -}; - -const useUpdatePermissionForEnvironmentReset = (req, _res, next) => { - if ( - req.method === 'DELETE' && - /^\/project\/[^/]+\/env\/[^/]+\/?$/.test(req.path) - ) { - req.permissionNameOverride = 'UPDATE_PAGE_ELEMENTS'; - } - return next(); -}; - -/** - * Middleware: Production GET is public, everything else requires JWT. - * Determines public access from URL path, not headers. - */ -const getRuntimeProjectSlug = async (req) => { - if (req.runtimeContext?.headerProjectSlug) { - return req.runtimeContext.headerProjectSlug; - } - - if (!isUuidV4(req.params.projectId)) { - return null; - } - - const project = await db.projects.findByPk(req.params.projectId, { - attributes: ['slug'], - }); - return project?.slug || null; -}; - -const requireProductionOrAuth = async (req, res, next) => { - const { environment } = req.params; - const isProduction = environment === 'production'; - const isReadOnly = ['GET', 'OPTIONS'].includes(req.method); - - let runtimeProjectSlug = null; - let isPrivateProductionPresentation = false; - - try { - runtimeProjectSlug = await getRuntimeProjectSlug(req); - isPrivateProductionPresentation = - await RuntimePresentationAccessService.isPrivateProductionPresentation( - runtimeProjectSlug, - ); - } catch (error) { - return next(error); - } - - if (isProduction && isReadOnly && !isPrivateProductionPresentation) { - // Public access for production GET - return next(); - } - - if (isProduction && isReadOnly && isPrivateProductionPresentation) { - return passport.authenticate( - 'jwt', - { session: false }, - async (error, user) => { - if (error) return next(error); - - if (!user) { - return res.status(401).send({ message: 'Authentication required' }); - } - - req.currentUser = user; - - const canAccess = - await RuntimePresentationAccessService.canUserAccessPrivateProductionPresentation( - user, - runtimeProjectSlug, - ); - - if (!canAccess) { - return res - .status(403) - .send({ message: 'Presentation access denied' }); - } - - req.isRuntimePublicRequest = true; - return next(); - }, - )(req, res, next); - } - - // Require JWT for non-production or write operations - return jwtAuth(req, res, next); -}; - -// Mark reads as public first, then apply CRUD permission checks -router.use(allowAuthenticatedRead); -router.use(authenticateWrites); -router.use(useUpdatePermissionForEnvironmentReset); -router.use(checkCrudPermissions('page_elements')); - -/** - * @swagger - * tags: - * name: Project_transition_settings - * description: Environment-aware project transition settings - */ - -/** - * @swagger - * /api/project-transition-settings/project/{projectId}/env/{environment}: - * get: - * summary: Get transition settings for a project in a specific environment - * tags: [Project_transition_settings] - * description: Production environment is publicly accessible. Dev/stage require authentication. - * parameters: - * - in: path - * name: projectId - * required: true - * schema: - * type: string - * format: uuid - * - in: path - * name: environment - * required: true - * schema: - * type: string - * enum: [dev, stage, production] - * responses: - * 200: - * description: Transition settings for the project/environment (null if none exist) - * 401: - * description: Authentication required (for dev/stage environments) - */ -router.get( - '/project/:projectId/env/:environment', - requireProductionOrAuth, - wrapAsync(async (req, res) => { - const { projectId, environment } = req.params; - - if (!isUuidV4(projectId)) { - return res.status(400).send({ message: 'Invalid project ID' }); - } - - if (!['dev', 'stage', 'production'].includes(environment)) { - return res.status(400).send({ message: 'Invalid environment' }); - } - - const settings = - await Project_transition_settingsService.findByProjectAndEnvironment( - projectId, - environment, - req.currentUser, - ); - - // Return null if no settings exist (frontend will use global defaults) - res.status(200).send(settings); - }), -); - -/** - * @swagger - * /api/project-transition-settings/project/{projectId}/env/{environment}: - * put: - * summary: Create or update transition settings for a project in a specific environment - * tags: [Project_transition_settings] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: projectId - * required: true - * schema: - * type: string - * format: uuid - * - in: path - * name: environment - * required: true - * schema: - * type: string - * enum: [dev, stage, production] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * data: - * type: object - * properties: - * transition_type: - * type: string - * enum: [fade, none, video] - * duration_ms: - * type: integer - * minimum: 0 - * easing: - * type: string - * enum: [ease-in-out, ease-in, ease-out, linear] - * overlay_color: - * type: string - * responses: - * 200: - * description: Settings created or updated successfully - */ -router.put( - '/project/:projectId/env/:environment', - wrapAsync(async (req, res) => { - const { projectId, environment } = req.params; - - if (!isUuidV4(projectId)) { - return res.status(400).send({ message: 'Invalid project ID' }); - } - - if (!['dev', 'stage', 'production'].includes(environment)) { - return res.status(400).send({ message: 'Invalid environment' }); - } - - const settings = await Project_transition_settingsService.upsertForProject( - projectId, - environment, - req.body.data || {}, - req.currentUser, - ); - - res.status(200).send(settings); - }), -); - -/** - * @swagger - * /api/project-transition-settings/project/{projectId}/env/{environment}: - * delete: - * summary: Delete transition settings for a project in a specific environment (reverts to global defaults) - * tags: [Project_transition_settings] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: projectId - * required: true - * schema: - * type: string - * format: uuid - * - in: path - * name: environment - * required: true - * schema: - * type: string - * enum: [dev, stage, production] - * responses: - * 200: - * description: Settings deleted successfully - */ -router.delete( - '/project/:projectId/env/:environment', - wrapAsync(async (req, res) => { - const { projectId, environment } = req.params; - - if (!isUuidV4(projectId)) { - return res.status(400).send({ message: 'Invalid project ID' }); - } - - if (!['dev', 'stage', 'production'].includes(environment)) { - return res.status(400).send({ message: 'Invalid environment' }); - } - - const settings = - await Project_transition_settingsService.findByProjectAndEnvironment( - projectId, - environment, - req.currentUser, - ); - - if (settings) { - await Project_transition_settingsService.remove({ - id: settings.id, - currentUser: req.currentUser, - runtimeContext: req.runtimeContext, - }); - } - - res.status(200).send({ success: true }); - }), -); - -/** - * @swagger - * /api/project-transition-settings: - * get: - * summary: Get all project transition settings - * tags: [Project_transition_settings] - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: List of project transition settings - */ -router.get( - '/', - jwtAuth, - wrapAsync(async (req, res) => { - const payload = await Project_transition_settingsDBApi.findAll(req.query); - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/project-transition-settings: - * post: - * summary: Create new project transition settings - * tags: [Project_transition_settings] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * data: - * type: object - * responses: - * 200: - * description: Settings created successfully - */ -router.post( - '/', - wrapAsync(async (req, res) => { - const payload = await Project_transition_settingsService.create({ - data: req.body.data, - currentUser: req.currentUser, - runtimeContext: req.runtimeContext, - }); - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/project-transition-settings/{id}: - * get: - * summary: Get project transition settings by ID - * tags: [Project_transition_settings] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * format: uuid - * responses: - * 200: - * description: Project transition settings - */ -router.get( - '/:id', - jwtAuth, - wrapAsync(async (req, res) => { - if (!isUuidV4(req.params.id)) { - return res.status(400).send({ message: 'Invalid ID' }); - } - - const payload = await Project_transition_settingsDBApi.findBy({ - id: req.params.id, - }); - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/project-transition-settings/{id}: - * put: - * summary: Update project transition settings by ID - * tags: [Project_transition_settings] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * format: uuid - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * data: - * type: object - * responses: - * 200: - * description: Settings updated successfully - */ -router.put( - '/:id', - wrapAsync(async (req, res) => { - assertRouteIdMatchesBody(req); - await Project_transition_settingsService.update({ - id: req.params.id, - data: req.body.data, - currentUser: req.currentUser, - runtimeContext: req.runtimeContext, - }); - res.status(200).send(true); - }), -); - -/** - * @swagger - * /api/project-transition-settings/{id}: - * delete: - * summary: Delete project transition settings by ID - * tags: [Project_transition_settings] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * format: uuid - * responses: - * 200: - * description: Settings deleted successfully - */ -router.delete( - '/:id', - wrapAsync(async (req, res) => { - await Project_transition_settingsService.remove({ - id: req.params.id, - currentUser: req.currentUser, - runtimeContext: req.runtimeContext, - }); - res.status(200).send(true); - }), -); - -router.use('/', commonErrorHandler); - -module.exports = router; diff --git a/backend/src/routes/project_transition_settings.ts b/backend/src/routes/project_transition_settings.ts new file mode 100644 index 0000000..e15a229 --- /dev/null +++ b/backend/src/routes/project_transition_settings.ts @@ -0,0 +1,213 @@ +import express from 'express'; + +import { authenticateJwt } from '../auth/passport-middleware.ts'; +import ProjectTransitionSettingsDBApi from '../db/api/project_transition_settings.ts'; +import { commonErrorHandler, isUuidV4, wrapAsync } from '../helpers.ts'; +import { checkCrudPermissions } from '../middlewares/check-permissions.ts'; +import { + authenticateProjectSettingsWrites, + isProjectEnvironmentRouteParams, + markProjectSettingsReadPublic, + requireProductionProjectSettingsReadOrAuth, + useUpdatePermissionForProjectEnvironmentReset, +} from '../middlewares/project-settings-runtime-auth.ts'; +import ProjectTransitionSettingsService from '../services/project_transition_settings.ts'; +import type { RouteMessageResponse, RouteSuccessResponse } from '../types/index.ts'; +import { + assertBodyIdMatchesRouteId, + isEntityDataRequestBody, +} from '../utils/request-body.ts'; +import { getRouteServiceContext } from '../utils/route-context.ts'; +import { getProjectTransitionSettingsListFilter } from '../utils/project-settings-query.ts'; +import { isProjectTransitionSettingsData } from '../utils/project-transition-settings.ts'; +import { getCurrentUser } from '../utils/request-context.ts'; + +const router = express.Router(); +const jwtAuth = authenticateJwt(); + +router.use(markProjectSettingsReadPublic); +router.use(authenticateProjectSettingsWrites()); +router.use(useUpdatePermissionForProjectEnvironmentReset); +router.use(checkCrudPermissions('page_elements')); + +/** + * @swagger + * tags: + * name: Project_transition_settings + * description: Environment-aware project transition settings + */ +router.get( + '/project/:projectId/env/:environment', + requireProductionProjectSettingsReadOrAuth, + wrapAsync(async (req, res) => { + if (!isProjectEnvironmentRouteParams(req.params)) { + const response: RouteMessageResponse = { message: 'Invalid route params' }; + return res.status(400).send(response); + } + + const { projectId, environment } = req.params; + const settings = + await ProjectTransitionSettingsService.findByProjectAndEnvironment( + projectId, + environment, + getCurrentUser(req), + ); + + return res.status(200).send(settings); + }), +); + +router.put( + '/project/:projectId/env/:environment', + wrapAsync(async (req, res) => { + if (!isProjectEnvironmentRouteParams(req.params)) { + const response: RouteMessageResponse = { message: 'Invalid route params' }; + return res.status(400).send(response); + } + + const body: unknown = req.body; + const data: unknown = isEntityDataRequestBody(body) ? body.data : {}; + + if (!isProjectTransitionSettingsData(data)) { + const response: RouteMessageResponse = { + message: 'Invalid project_transition_settings data', + }; + return res.status(400).send(response); + } + + const { projectId, environment } = req.params; + const settings = await ProjectTransitionSettingsService.upsertForProject( + projectId, + environment, + data, + getCurrentUser(req), + ); + + return res.status(200).send(settings); + }), +); + +router.delete( + '/project/:projectId/env/:environment', + wrapAsync(async (req, res) => { + if (!isProjectEnvironmentRouteParams(req.params)) { + const response: RouteMessageResponse = { message: 'Invalid route params' }; + return res.status(400).send(response); + } + + const { projectId, environment } = req.params; + const settings = + await ProjectTransitionSettingsService.findByProjectAndEnvironment( + projectId, + environment, + getCurrentUser(req), + ); + + if (settings) { + await ProjectTransitionSettingsService.remove({ + id: settings.id, + ...getRouteServiceContext(req), + }); + } + + const response: RouteSuccessResponse = { success: true }; + return res.status(200).send(response); + }), +); + +router.get( + '/', + jwtAuth, + wrapAsync(async (req, res) => { + const filter = getProjectTransitionSettingsListFilter(req.query); + const payload = await ProjectTransitionSettingsDBApi.findAll(filter); + return res.status(200).send(payload); + }), +); + +router.post( + '/', + wrapAsync(async (req, res) => { + const body: unknown = req.body; + const data: unknown = isEntityDataRequestBody(body) ? body.data : {}; + + if (!isProjectTransitionSettingsData(data)) { + const response: RouteMessageResponse = { + message: 'Invalid project_transition_settings data', + }; + return res.status(400).send(response); + } + + const payload = await ProjectTransitionSettingsService.create({ + data, + ...getRouteServiceContext(req), + }); + return res.status(200).send(payload); + }), +); + +router.get( + '/:id', + jwtAuth, + wrapAsync(async (req, res) => { + const { id } = req.params; + if (typeof id !== 'string' || !isUuidV4(id)) { + const response: RouteMessageResponse = { message: 'Invalid ID' }; + return res.status(400).send(response); + } + + const payload = await ProjectTransitionSettingsDBApi.findBy({ id }); + return res.status(200).send(payload); + }), +); + +router.put( + '/:id', + wrapAsync(async (req, res) => { + const { id } = req.params; + if (typeof id !== 'string' || !isUuidV4(id)) { + const response: RouteMessageResponse = { message: 'Invalid ID' }; + return res.status(400).send(response); + } + + const body: unknown = req.body; + const data: unknown = isEntityDataRequestBody(body) ? body.data : {}; + + if (!isProjectTransitionSettingsData(data)) { + const response: RouteMessageResponse = { + message: 'Invalid project_transition_settings data', + }; + return res.status(400).send(response); + } + + assertBodyIdMatchesRouteId(id, body); + + await ProjectTransitionSettingsService.update({ + id, + data, + ...getRouteServiceContext(req), + }); + return res.status(200).send(true); + }), +); + +router.delete( + '/:id', + wrapAsync(async (req, res) => { + const { id } = req.params; + if (typeof id !== 'string' || !isUuidV4(id)) { + const response: RouteMessageResponse = { message: 'Invalid ID' }; + return res.status(400).send(response); + } + + await ProjectTransitionSettingsService.remove({ + id, + ...getRouteServiceContext(req), + }); + return res.status(200).send(true); + }), +); + +router.use('/', commonErrorHandler); + +export default router; diff --git a/backend/src/routes/project_ui_control_settings.js b/backend/src/routes/project_ui_control_settings.js deleted file mode 100644 index 56374ad..0000000 --- a/backend/src/routes/project_ui_control_settings.js +++ /dev/null @@ -1,203 +0,0 @@ -const express = require('express'); -const passport = require('passport'); -const db = require('../db/models'); -const Project_ui_control_settingsService = require('../services/project_ui_control_settings'); -const Project_ui_control_settingsDBApi = require('../db/api/project_ui_control_settings'); -const { wrapAsync, commonErrorHandler, isUuidV4 } = require('../helpers'); -const { checkCrudPermissions } = require('../middlewares/check-permissions'); -const RuntimePresentationAccessService = require('../services/runtime-presentation-access'); - -const router = express.Router(); -const jwtAuth = passport.authenticate('jwt', { session: false }); - -const allowAuthenticatedRead = (req, _res, next) => { - if (['GET', 'OPTIONS'].includes(req.method)) { - req.isRuntimePublicRequest = true; - } - return next(); -}; - -const authenticateWrites = (req, res, next) => { - if (['GET', 'OPTIONS'].includes(req.method)) { - return next(); - } - return jwtAuth(req, res, next); -}; - -const useUpdatePermissionForEnvironmentReset = (req, _res, next) => { - if ( - req.method === 'DELETE' && - /^\/project\/[^/]+\/env\/[^/]+\/?$/.test(req.path) - ) { - req.permissionNameOverride = 'UPDATE_PAGE_ELEMENTS'; - } - return next(); -}; - -const getRuntimeProjectSlug = async (req) => { - if (req.runtimeContext?.headerProjectSlug) { - return req.runtimeContext.headerProjectSlug; - } - - if (!isUuidV4(req.params.projectId)) { - return null; - } - - const project = await db.projects.findByPk(req.params.projectId, { - attributes: ['slug'], - }); - return project?.slug || null; -}; - -const requireProductionOrAuth = async (req, res, next) => { - const { environment } = req.params; - const isProduction = environment === 'production'; - const isReadOnly = ['GET', 'OPTIONS'].includes(req.method); - - let runtimeProjectSlug = null; - let isPrivateProductionPresentation = false; - - try { - runtimeProjectSlug = await getRuntimeProjectSlug(req); - isPrivateProductionPresentation = - await RuntimePresentationAccessService.isPrivateProductionPresentation( - runtimeProjectSlug, - ); - } catch (error) { - return next(error); - } - - if (isProduction && isReadOnly && !isPrivateProductionPresentation) { - return next(); - } - - if (isProduction && isReadOnly && isPrivateProductionPresentation) { - return passport.authenticate( - 'jwt', - { session: false }, - async (error, user) => { - if (error) return next(error); - if (!user) { - return res.status(401).send({ message: 'Authentication required' }); - } - - req.currentUser = user; - const canAccess = - await RuntimePresentationAccessService.canUserAccessPrivateProductionPresentation( - user, - runtimeProjectSlug, - ); - - if (!canAccess) { - return res - .status(403) - .send({ message: 'Presentation access denied' }); - } - - req.isRuntimePublicRequest = true; - return next(); - }, - )(req, res, next); - } - - return jwtAuth(req, res, next); -}; - -router.use(allowAuthenticatedRead); -router.use(authenticateWrites); -router.use(useUpdatePermissionForEnvironmentReset); -router.use(checkCrudPermissions('page_elements')); - -router.get( - '/project/:projectId/env/:environment', - requireProductionOrAuth, - wrapAsync(async (req, res) => { - const { projectId, environment } = req.params; - - if (!isUuidV4(projectId)) { - return res.status(400).send({ message: 'Invalid project ID' }); - } - - if (!['dev', 'stage', 'production'].includes(environment)) { - return res.status(400).send({ message: 'Invalid environment' }); - } - - const settings = - await Project_ui_control_settingsService.findByProjectAndEnvironment( - projectId, - environment, - req.currentUser, - ); - - res.status(200).send(settings); - }), -); - -router.put( - '/project/:projectId/env/:environment', - wrapAsync(async (req, res) => { - const { projectId, environment } = req.params; - - if (!isUuidV4(projectId)) { - return res.status(400).send({ message: 'Invalid project ID' }); - } - - if (!['dev', 'stage', 'production'].includes(environment)) { - return res.status(400).send({ message: 'Invalid environment' }); - } - - const settings = await Project_ui_control_settingsService.upsertForProject( - projectId, - environment, - req.body.data || {}, - req.currentUser, - ); - - res.status(200).send(settings); - }), -); - -router.delete( - '/project/:projectId/env/:environment', - wrapAsync(async (req, res) => { - const { projectId, environment } = req.params; - - if (!isUuidV4(projectId)) { - return res.status(400).send({ message: 'Invalid project ID' }); - } - - if (!['dev', 'stage', 'production'].includes(environment)) { - return res.status(400).send({ message: 'Invalid environment' }); - } - - const settings = - await Project_ui_control_settingsService.findByProjectAndEnvironment( - projectId, - environment, - req.currentUser, - ); - - if (settings) { - await Project_ui_control_settingsService.remove({ - id: settings.id, - currentUser: req.currentUser, - runtimeContext: req.runtimeContext, - }); - } - - res.status(200).send({ success: true }); - }), -); - -router.get( - '/', - jwtAuth, - wrapAsync(async (req, res) => { - const payload = await Project_ui_control_settingsDBApi.findAll(req.query); - res.status(200).send(payload); - }), -); - -router.use('/', commonErrorHandler); - -module.exports = router; diff --git a/backend/src/routes/project_ui_control_settings.ts b/backend/src/routes/project_ui_control_settings.ts new file mode 100644 index 0000000..33bb2b1 --- /dev/null +++ b/backend/src/routes/project_ui_control_settings.ts @@ -0,0 +1,126 @@ +import express from 'express'; + +import { authenticateJwt } from '../auth/passport-middleware.ts'; +import ProjectUiControlSettingsDBApi from '../db/api/project_ui_control_settings.ts'; +import { commonErrorHandler, wrapAsync } from '../helpers.ts'; +import { checkCrudPermissions } from '../middlewares/check-permissions.ts'; +import { + authenticateProjectSettingsWrites, + isProjectEnvironmentRouteParams, + markProjectSettingsReadPublic, + requireProductionProjectSettingsReadOrAuth, + useUpdatePermissionForProjectEnvironmentReset, +} from '../middlewares/project-settings-runtime-auth.ts'; +import ProjectUiControlSettingsService from '../services/project_ui_control_settings.ts'; +import type { + RouteMessageResponse, + RouteSuccessResponse, +} from '../types/index.ts'; +import { isEntityDataRequestBody } from '../utils/request-body.ts'; +import { getRouteServiceContext } from '../utils/route-context.ts'; +import { getProjectSettingsListFilter } from '../utils/project-settings-query.ts'; +import { isProjectUiControlSettingsData } from '../utils/project-ui-control-settings.ts'; +import { getCurrentUser } from '../utils/request-context.ts'; + +const router = express.Router(); +const jwtAuth = authenticateJwt(); + +router.use(markProjectSettingsReadPublic); +router.use(authenticateProjectSettingsWrites()); +router.use(useUpdatePermissionForProjectEnvironmentReset); +router.use(checkCrudPermissions('page_elements')); + +router.get( + '/project/:projectId/env/:environment', + requireProductionProjectSettingsReadOrAuth, + wrapAsync(async (req, res) => { + if (!isProjectEnvironmentRouteParams(req.params)) { + const response: RouteMessageResponse = { message: 'Invalid route params' }; + return res.status(400).send(response); + } + + const { projectId, environment } = req.params; + const settings = + await ProjectUiControlSettingsService.findByProjectAndEnvironment( + projectId, + environment, + getCurrentUser(req), + ); + + return res.status(200).send(settings); + }), +); + +router.put( + '/project/:projectId/env/:environment', + wrapAsync(async (req, res) => { + if (!isProjectEnvironmentRouteParams(req.params)) { + const response: RouteMessageResponse = { message: 'Invalid route params' }; + return res.status(400).send(response); + } + + const body: unknown = req.body; + const data: unknown = isEntityDataRequestBody(body) + ? body.data + : {}; + + if (!isProjectUiControlSettingsData(data)) { + const response: RouteMessageResponse = { + message: 'Invalid project_ui_control_settings data', + }; + return res.status(400).send(response); + } + + const { projectId, environment } = req.params; + const settings = await ProjectUiControlSettingsService.upsertForProject( + projectId, + environment, + data, + getCurrentUser(req), + ); + + return res.status(200).send(settings); + }), +); + +router.delete( + '/project/:projectId/env/:environment', + wrapAsync(async (req, res) => { + if (!isProjectEnvironmentRouteParams(req.params)) { + const response: RouteMessageResponse = { message: 'Invalid route params' }; + return res.status(400).send(response); + } + + const { projectId, environment } = req.params; + const settings = + await ProjectUiControlSettingsService.findByProjectAndEnvironment( + projectId, + environment, + getCurrentUser(req), + ); + + if (settings) { + await ProjectUiControlSettingsService.remove({ + id: settings.id, + ...getRouteServiceContext(req), + }); + } + + const response: RouteSuccessResponse = { success: true }; + return res.status(200).send(response); + }), +); + +router.get( + '/', + jwtAuth, + wrapAsync(async (req, res) => { + const filter = getProjectSettingsListFilter(req.query); + const payload = await ProjectUiControlSettingsDBApi.findAll(filter); + return res.status(200).send(payload); + }), +); + +router.use('/', commonErrorHandler); + +export default router; diff --git a/backend/src/routes/projects.js b/backend/src/routes/projects.js deleted file mode 100644 index 8f32a6d..0000000 --- a/backend/src/routes/projects.js +++ /dev/null @@ -1,41 +0,0 @@ -const { createEntityRouter } = require('../factories/router.factory'); -const ProjectsService = require('../services/projects'); -const ProjectsDBApi = require('../db/api/projects'); -const { wrapAsync, commonErrorHandler } = require('../helpers'); -const { validateRequest } = require('../middlewares/validate-request'); -const { projects: projectSchemas } = require('../validators/request-schemas'); - -// Create base router with factory (includes all standard CRUD endpoints) -const router = createEntityRouter('projects', ProjectsService, ProjectsDBApi, { - permissionEntity: 'projects', - csvFields: [ - 'id', - 'name', - 'slug', - 'description', - 'logo_url', - 'favicon_url', - 'og_image_url', - ], - validation: { - create: projectSchemas.create, - update: projectSchemas.update, - }, -}); - -// Custom endpoint: Clone project -router.post( - '/:id/clone', - validateRequest(projectSchemas.clone), - wrapAsync(async (req, res) => { - const payload = await ProjectsService.cloneFromProject( - req.params.id, - req.currentUser, - ); - res.status(200).send(payload); - }), -); - -router.use('/', commonErrorHandler); - -module.exports = router; diff --git a/backend/src/routes/projects.ts b/backend/src/routes/projects.ts new file mode 100644 index 0000000..15022c4 --- /dev/null +++ b/backend/src/routes/projects.ts @@ -0,0 +1,47 @@ +import ProjectsDBApi from '../db/api/projects.ts'; +import { createEntityRouter } from '../factories/router.factory.ts'; +import { commonErrorHandler, wrapAsync } from '../helpers.ts'; +import { validateRequest } from '../middlewares/validate-request.ts'; +import ProjectsService from '../services/projects.ts'; +import type { RouteMessageResponse } from '../types/index.ts'; +import { projects as projectSchemas } from '../validators/request-schemas.ts'; +import { getCurrentUser } from '../utils/request-context.ts'; + +const router = createEntityRouter('projects', ProjectsService, ProjectsDBApi, { + permissionEntity: 'projects', + csvFields: [ + 'id', + 'name', + 'slug', + 'description', + 'logo_url', + 'favicon_url', + 'og_image_url', + ], + validation: { + create: projectSchemas.create, + update: projectSchemas.update, + }, +}); + +router.post( + '/:id/clone', + validateRequest(projectSchemas.clone), + wrapAsync(async (req, res) => { + const { id } = req.params; + if (typeof id !== 'string') { + const response: RouteMessageResponse = { message: 'Invalid project ID' }; + return res.status(400).send(response); + } + + const payload = await ProjectsService.cloneFromProject( + id, + getCurrentUser(req), + ); + return res.status(200).send(payload); + }), +); + +router.use('/', commonErrorHandler); + +export default router; diff --git a/backend/src/routes/publish.js b/backend/src/routes/publish.js deleted file mode 100644 index 300fe09..0000000 --- a/backend/src/routes/publish.js +++ /dev/null @@ -1,64 +0,0 @@ -const express = require('express'); -const PublishService = require('../services/publish'); -const wrapAsync = require('../helpers').wrapAsync; -const { checkCrudPermissions } = require('../middlewares/check-permissions'); -const { validateRequest } = require('../middlewares/validate-request'); -const { publish: publishSchemas } = require('../validators/request-schemas'); - -const router = express.Router(); - -router.use(checkCrudPermissions('publish_events')); - -const publishHandler = wrapAsync(async (req, res) => { - const { projectId, title, description } = req.body; - const result = await PublishService.publishToProduction( - projectId, - req.currentUser, - title, - description, - ); - res.status(200).send(result); -}); - -router.post('/', validateRequest(publishSchemas.publish), publishHandler); - -/** - * @swagger - * /api/publish/save-to-stage: - * post: - * security: - * - bearerAuth: [] - * tags: [Publish] - * summary: Save dev content to stage - * description: Copies all dev environment content (pages, elements, transitions, audio) to stage environment - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - projectId - * properties: - * projectId: - * type: string - * format: uuid - * responses: - * 200: - * description: Successfully saved to stage - * 400: - * description: Invalid request or publish already in progress - */ -router.post( - '/save-to-stage', - validateRequest(publishSchemas.saveToStage), - wrapAsync(async (req, res) => { - const { projectId } = req.body; - const result = await PublishService.saveToStage(projectId, req.currentUser); - res.status(200).json(result); - }), -); - -router.use('/', require('../helpers').commonErrorHandler); - -module.exports = router; diff --git a/backend/src/routes/publish.ts b/backend/src/routes/publish.ts new file mode 100644 index 0000000..66fd9e5 --- /dev/null +++ b/backend/src/routes/publish.ts @@ -0,0 +1,108 @@ +import express from 'express'; + +import { commonErrorHandler, wrapAsync } from '../helpers.ts'; +import { checkCrudPermissions } from '../middlewares/check-permissions.ts'; +import { validateRequest } from '../middlewares/validate-request.ts'; +import PublishService from '../services/publish.ts'; +import type { + PublishToProductionRequestBody, + SaveToStageRequestBody, +} from '../types/index.ts'; +import { publish as publishSchemas } from '../validators/request-schemas.ts'; +import { getCurrentUser } from '../utils/request-context.ts'; + +const router = express.Router(); + +router.use(checkCrudPermissions('publish_events')); + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function isOptionalText(value: unknown): value is string | null | undefined { + return value === undefined || value === null || typeof value === 'string'; +} + +function isPublishToProductionRequestBody( + body: unknown, +): body is PublishToProductionRequestBody { + return ( + isRecord(body) && + typeof body.projectId === 'string' && + isOptionalText(body.title) && + isOptionalText(body.description) + ); +} + +function isSaveToStageRequestBody( + body: unknown, +): body is SaveToStageRequestBody { + return isRecord(body) && typeof body.projectId === 'string'; +} + +const publishHandler = wrapAsync(async (req, res) => { + const body: unknown = req.body; + + if (!isPublishToProductionRequestBody(body)) { + return res.status(400).json({ error: 'Invalid publish request' }); + } + + const result = await PublishService.publishToProduction( + body.projectId, + getCurrentUser(req), + body.title, + body.description, + ); + return res.status(200).send(result); +}); + +router.post('/', validateRequest(publishSchemas.publish), publishHandler); + +/** + * @swagger + * /api/publish/save-to-stage: + * post: + * security: + * - bearerAuth: [] + * tags: [Publish] + * summary: Save dev content to stage + * description: Copies all dev environment content (pages, elements, transitions, audio) to stage environment + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - projectId + * properties: + * projectId: + * type: string + * format: uuid + * responses: + * 200: + * description: Successfully saved to stage + * 400: + * description: Invalid request or publish already in progress + */ +router.post( + '/save-to-stage', + validateRequest(publishSchemas.saveToStage), + wrapAsync(async (req, res) => { + const body: unknown = req.body; + + if (!isSaveToStageRequestBody(body)) { + return res.status(400).json({ error: 'Invalid save-to-stage request' }); + } + + const result = await PublishService.saveToStage( + body.projectId, + getCurrentUser(req), + ); + return res.status(200).json(result); + }), +); + +router.use('/', commonErrorHandler); + +export default router; diff --git a/backend/src/routes/publish_events.js b/backend/src/routes/publish_events.ts similarity index 94% rename from backend/src/routes/publish_events.js rename to backend/src/routes/publish_events.ts index e73d8ad..94aafe2 100644 --- a/backend/src/routes/publish_events.js +++ b/backend/src/routes/publish_events.ts @@ -1,6 +1,6 @@ -const Publish_eventsService = require('../services/publish_events'); -const Publish_eventsDBApi = require('../db/api/publish_events'); -const { createEntityRouter } = require('../factories/router.factory'); +import Publish_eventsDBApi from '../db/api/publish_events.ts'; +import { createEntityRouter } from '../factories/router.factory.ts'; +import Publish_eventsService from '../services/publish_events.ts'; /** * @swagger @@ -150,7 +150,7 @@ const { createEntityRouter } = require('../factories/router.factory'); * description: Some server error */ -module.exports = createEntityRouter( +export default createEntityRouter( 'publish_events', Publish_eventsService, Publish_eventsDBApi, diff --git a/backend/src/routes/pwa_caches.js b/backend/src/routes/pwa_caches.ts similarity index 93% rename from backend/src/routes/pwa_caches.js rename to backend/src/routes/pwa_caches.ts index 268731a..57fea25 100644 --- a/backend/src/routes/pwa_caches.js +++ b/backend/src/routes/pwa_caches.ts @@ -1,6 +1,6 @@ -const Pwa_cachesService = require('../services/pwa_caches'); -const Pwa_cachesDBApi = require('../db/api/pwa_caches'); -const { createEntityRouter } = require('../factories/router.factory'); +import Pwa_cachesDBApi from '../db/api/pwa_caches.ts'; +import { createEntityRouter } from '../factories/router.factory.ts'; +import Pwa_cachesService from '../services/pwa_caches.ts'; /** * @swagger @@ -141,7 +141,7 @@ const { createEntityRouter } = require('../factories/router.factory'); * description: Some server error */ -module.exports = createEntityRouter( +export default createEntityRouter( 'pwa_caches', Pwa_cachesService, Pwa_cachesDBApi, diff --git a/backend/src/routes/roles.js b/backend/src/routes/roles.ts similarity index 93% rename from backend/src/routes/roles.js rename to backend/src/routes/roles.ts index 74a1929..85e34b0 100644 --- a/backend/src/routes/roles.js +++ b/backend/src/routes/roles.ts @@ -1,6 +1,6 @@ -const RolesService = require('../services/roles'); -const RolesDBApi = require('../db/api/roles'); -const { createEntityRouter } = require('../factories/router.factory'); +import RolesDBApi from '../db/api/roles.ts'; +import { createEntityRouter } from '../factories/router.factory.ts'; +import RolesService from '../services/roles.ts'; /** * @swagger @@ -138,4 +138,4 @@ const { createEntityRouter } = require('../factories/router.factory'); * description: Some server error */ -module.exports = createEntityRouter('roles', RolesService, RolesDBApi); +export default createEntityRouter('roles', RolesService, RolesDBApi); diff --git a/backend/src/routes/runtime-access.js b/backend/src/routes/runtime-access.ts similarity index 70% rename from backend/src/routes/runtime-access.js rename to backend/src/routes/runtime-access.ts index 591e25f..4a97e7c 100644 --- a/backend/src/routes/runtime-access.js +++ b/backend/src/routes/runtime-access.ts @@ -1,10 +1,13 @@ -const express = require('express'); -const passport = require('passport'); -const RuntimePresentationAccessService = require('../services/runtime-presentation-access'); -const { checkPermissions } = require('../middlewares/check-permissions'); -const { wrapAsync } = require('../helpers'); +import express from 'express'; + +import { authenticateJwt } from '../auth/passport-middleware.ts'; +import { wrapAsync } from '../helpers.ts'; +import { checkPermissions } from '../middlewares/check-permissions.ts'; +import RuntimePresentationAccessService from '../services/runtime-presentation-access.ts'; +import { getCurrentUser } from '../utils/request-context.ts'; const router = express.Router(); +const jwtAuth = authenticateJwt(); router.get( '/presentations/:slug', @@ -24,7 +27,7 @@ router.get( router.get( '/private-production-presentations', - passport.authenticate('jwt', { session: false }), + jwtAuth, checkPermissions('CREATE_USERS'), wrapAsync(async (_req, res) => { res @@ -37,7 +40,7 @@ router.get( router.get( '/private-production-presentations/autocomplete', - passport.authenticate('jwt', { session: false }), + jwtAuth, checkPermissions('CREATE_USERS'), wrapAsync(async (_req, res) => { res @@ -50,15 +53,15 @@ router.get( router.get( '/me', - passport.authenticate('jwt', { session: false }), + jwtAuth, wrapAsync(async (req, res) => { res.status(200).send({ allowedPrivateProductionSlugs: await RuntimePresentationAccessService.getAllowedPrivateProductionSlugs( - req.currentUser, + getCurrentUser(req), ), }); }), ); -module.exports = router; +export default router; diff --git a/backend/src/routes/runtime-context.js b/backend/src/routes/runtime-context.js deleted file mode 100644 index 5149c1f..0000000 --- a/backend/src/routes/runtime-context.js +++ /dev/null @@ -1,11 +0,0 @@ -const express = require('express'); - -const router = express.Router(); - -router.get('/', (req, res) => { - res - .status(200) - .send(req.runtimeContext || { mode: 'unknown', projectSlug: null }); -}); - -module.exports = router; diff --git a/backend/src/routes/runtime-context.ts b/backend/src/routes/runtime-context.ts new file mode 100644 index 0000000..a0130cb --- /dev/null +++ b/backend/src/routes/runtime-context.ts @@ -0,0 +1,17 @@ +import express from 'express'; + +import type { RuntimeContextInspectionResponse } from '../types/index.ts'; +import { getRuntimeContext } from '../utils/request-context.ts'; + +const router = express.Router(); + +const unknownRuntimeContext: RuntimeContextInspectionResponse = { + mode: 'unknown', + projectSlug: null, +}; + +router.get('/', (req, res) => { + res.status(200).send(getRuntimeContext(req) ?? unknownRuntimeContext); +}); + +export default router; diff --git a/backend/src/routes/search.js b/backend/src/routes/search.ts similarity index 54% rename from backend/src/routes/search.js rename to backend/src/routes/search.ts index d3e2db4..8550f9a 100644 --- a/backend/src/routes/search.js +++ b/backend/src/routes/search.ts @@ -1,12 +1,24 @@ -const express = require('express'); -const SearchService = require('../services/search'); -const { wrapAsync } = require('../helpers'); +import express from 'express'; + +import { wrapAsync } from '../helpers.ts'; +import { checkCrudPermissions } from '../middlewares/check-permissions.ts'; +import SearchService from '../services/search.ts'; +import type { SearchRequestBody } from '../types/index.ts'; +import { getCurrentUser } from '../utils/request-context.ts'; const router = express.Router(); -const { checkCrudPermissions } = require('../middlewares/check-permissions'); router.use(checkCrudPermissions('search')); +function isSearchRequestBody(body: unknown): body is SearchRequestBody { + return ( + body !== null && + typeof body === 'object' && + 'searchQuery' in body && + typeof body.searchQuery === 'string' + ); +} + /** * @swagger * path: @@ -36,18 +48,18 @@ router.use(checkCrudPermissions('search')); router.post( '/', wrapAsync(async (req, res) => { - const { searchQuery } = req.body; + const body: unknown = req.body; - if (!searchQuery) { + if (!isSearchRequestBody(body) || !body.searchQuery) { return res.status(400).json({ error: 'Please enter a search query' }); } const foundMatches = await SearchService.search( - searchQuery, - req.currentUser, + body.searchQuery, + getCurrentUser(req), ); - res.json(foundMatches); + return res.json(foundMatches); }), ); -module.exports = router; +export default router; diff --git a/backend/src/routes/sql.js b/backend/src/routes/sql.js deleted file mode 100644 index e406a9c..0000000 --- a/backend/src/routes/sql.js +++ /dev/null @@ -1,87 +0,0 @@ -const express = require('express'); -const db = require('../db/models'); -const wrapAsync = require('../helpers').wrapAsync; -const { validateReadOnlySql } = require('../utils/sqlValidator'); - -const router = express.Router(); -const MAX_SQL_LENGTH = 5000; -const MAX_SQL_ROWS = 1000; -const SQL_TIMEOUT_MS = 5000; - -/** - * @swagger - * /api/sql: - * post: - * security: - * - bearerAuth: [] - * summary: Execute a SELECT-only SQL query - * description: Executes a read-only SQL query and returns rows. - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * sql: - * type: string - * required: - * - sql - * responses: - * 200: - * description: Query result - * 400: - * description: Invalid SQL - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 500: - * description: Internal server error - */ -router.post( - '/', - wrapAsync(async (req, res) => { - const { currentUser } = req; - const isAdminUser = Boolean( - currentUser && - currentUser.app_role && - (currentUser.app_role.name === 'Administrator' || - currentUser.app_role.globalAccess === true), - ); - - if (!isAdminUser) { - return res - .status(403) - .json({ error: 'Only administrators can execute SQL queries' }); - } - - const { sql } = req.body; - const validation = validateReadOnlySql(sql, { maxLength: MAX_SQL_LENGTH }); - if (!validation.valid) { - return res.status(400).json({ error: validation.error }); - } - - const normalized = validation.normalized; - const wrappedSql = `SELECT * FROM (${normalized}) AS query_result LIMIT ${MAX_SQL_ROWS}`; - - const rows = await db.sequelize.transaction(async (transaction) => { - await db.sequelize.query( - `SET LOCAL statement_timeout = ${SQL_TIMEOUT_MS}`, - { transaction }, - ); - return db.sequelize.query(wrappedSql, { - transaction, - type: db.Sequelize.QueryTypes.SELECT, - }); - }); - - return res.status(200).json({ - rows, - meta: { - maxRows: MAX_SQL_ROWS, - statementTimeoutMs: SQL_TIMEOUT_MS, - }, - }); - }), -); - -module.exports = router; diff --git a/backend/src/routes/sql.ts b/backend/src/routes/sql.ts new file mode 100644 index 0000000..24c39d3 --- /dev/null +++ b/backend/src/routes/sql.ts @@ -0,0 +1,114 @@ +import express from 'express'; + +import db from '../db/models/index.ts'; +import { wrapAsync } from '../helpers.ts'; +import type { + ExecuteSqlErrorResponse, + ExecuteSqlRequestBody, + ExecuteSqlSuccessResponse, + SqlQueryRow, +} from '../types/index.ts'; +import { getCurrentUser } from '../utils/request-context.ts'; +import { validateReadOnlySql } from '../utils/sqlValidator.ts'; + +const router = express.Router(); +const MAX_SQL_LENGTH = 5000; +const MAX_SQL_ROWS = 1000; +const SQL_TIMEOUT_MS = 5000; + +function isExecuteSqlRequestBody(body: unknown): body is ExecuteSqlRequestBody { + return ( + body !== null && + typeof body === 'object' && + 'sql' in body && + typeof body.sql === 'string' + ); +} + +/** + * @swagger + * /api/sql: + * post: + * security: + * - bearerAuth: [] + * summary: Execute a SELECT-only SQL query + * description: Executes a read-only SQL query and returns rows. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * sql: + * type: string + * required: + * - sql + * responses: + * 200: + * description: Query result + * 400: + * description: Invalid SQL + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 500: + * description: Internal server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const currentUser = getCurrentUser(req); + const isAdminUser = Boolean( + currentUser && + currentUser.app_role && + (currentUser.app_role.name === 'Administrator' || + currentUser.app_role.globalAccess === true), + ); + + if (!isAdminUser) { + const response: ExecuteSqlErrorResponse = { + error: 'Only administrators can execute SQL queries', + }; + return res.status(403).json(response); + } + + const body: unknown = req.body; + if (!isExecuteSqlRequestBody(body)) { + const response: ExecuteSqlErrorResponse = { + error: 'SQL query must be a non-empty string', + }; + return res.status(400).json(response); + } + + const validation = validateReadOnlySql(body.sql, { + maxLength: MAX_SQL_LENGTH, + }); + if (!validation.valid) { + const response: ExecuteSqlErrorResponse = { error: validation.error }; + return res.status(400).json(response); + } + + const wrappedSql = `SELECT * FROM (${validation.normalized}) AS query_result LIMIT ${MAX_SQL_ROWS}`; + + const rows = await db.sequelize.transaction(async (transaction) => { + await db.sequelize.query(`SET LOCAL statement_timeout = ${SQL_TIMEOUT_MS}`, { + transaction, + }); + return db.sequelize.query(wrappedSql, { + transaction, + type: db.Sequelize.QueryTypes.SELECT, + }); + }); + + const response: ExecuteSqlSuccessResponse = { + rows, + meta: { + maxRows: MAX_SQL_ROWS, + statementTimeoutMs: SQL_TIMEOUT_MS, + }, + }; + return res.status(200).json(response); + }), +); + +export default router; diff --git a/backend/src/routes/tour_pages.js b/backend/src/routes/tour_pages.ts similarity index 77% rename from backend/src/routes/tour_pages.js rename to backend/src/routes/tour_pages.ts index a6f6f28..0952334 100644 --- a/backend/src/routes/tour_pages.js +++ b/backend/src/routes/tour_pages.ts @@ -1,19 +1,33 @@ -const express = require('express'); -const Tour_pagesService = require('../services/tour_pages'); -const Tour_pagesDBApi = require('../db/api/tour_pages'); -const { +import express from 'express'; +import { parse } from 'json2csv'; + +import Tour_pagesDBApi from '../db/api/tour_pages.ts'; +import { wrapAsync, commonErrorHandler, - assertRouteIdMatchesBody, -} = require('../helpers'); -const { checkCrudPermissions } = require('../middlewares/check-permissions'); -const { parse } = require('json2csv'); -const { logger } = require('../utils/logger'); -const { validateRequest } = require('../middlewares/validate-request'); -const { - crud: crudSchemas, - tourPages: tourPageSchemas, -} = require('../validators/request-schemas'); +} from '../helpers.ts'; +import { checkCrudPermissions } from '../middlewares/check-permissions.ts'; +import { validateRequest } from '../middlewares/validate-request.ts'; +import Tour_pagesService from '../services/tour_pages.ts'; +import type { + TourPageCreateRequest, + TourPageDeleteByIdsRequest, + TourPageDuplicateRequest, + TourPageListRequest, + TourPageReorderRequest, + TourPageReverseVideoStatusRequest, + TourPageRouteParams, + TourPageUpdateRequest, +} from '../types/index.ts'; +import { logger } from '../utils/logger.ts'; +import { assertBodyIdMatchesRouteId } from '../utils/request-body.ts'; +import { getRouteServiceContext } from '../utils/route-context.ts'; +import { getCurrentUser } from '../utils/request-context.ts'; +import { + crud as crudSchemas, + tourPages as tourPageSchemas, +} from '../validators/request-schemas.ts'; +import type { Request } from 'express'; /** * @swagger @@ -163,6 +177,9 @@ const { const router = express.Router(); +const getStringQueryValue = (value: unknown): string | undefined => + typeof value === 'string' ? value : undefined; + // Apply permission checks router.use(checkCrudPermissions('tour_pages')); @@ -170,15 +187,14 @@ router.use(checkCrudPermissions('tour_pages')); router.post( '/', validateRequest(tourPageSchemas.create), - wrapAsync(async (req, res) => { + wrapAsync(async (req: TourPageCreateRequest, res) => { const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; const link = new URL(referer); const payload = await Tour_pagesService.create({ data: req.body.data, - currentUser: req.currentUser, - runtimeContext: req.runtimeContext, + ...getRouteServiceContext(req), sendInvitationEmails: true, host: link.origin, }); @@ -190,11 +206,7 @@ router.post( router.post( '/bulk-import', wrapAsync(async (req, res) => { - const referer = - req.headers.referer || - `${req.protocol}://${req.hostname}${req.originalUrl}`; - const link = new URL(referer); - await Tour_pagesService.bulkImport(req, res, true, link.origin); + await Tour_pagesService.bulkImport(req, res); res.status(200).send(true); }), ); @@ -203,10 +215,10 @@ router.post( router.post( '/reorder', validateRequest(tourPageSchemas.reorder), - wrapAsync(async (req, res) => { + wrapAsync(async (req: TourPageReorderRequest, res) => { const payload = await Tour_pagesService.reorder( req.body.data, - req.currentUser, + getCurrentUser(req), ); res.status(200).send(payload); }), @@ -216,11 +228,11 @@ router.post( router.post( '/:id/duplicate', validateRequest(tourPageSchemas.duplicate), - wrapAsync(async (req, res) => { + wrapAsync(async (req: TourPageDuplicateRequest, res) => { const payload = await Tour_pagesService.duplicatePage( req.params.id, req.body.data, - req.currentUser, + getCurrentUser(req), ); res.status(200).send(payload); }), @@ -230,13 +242,12 @@ router.post( router.put( '/:id', validateRequest(tourPageSchemas.update), - wrapAsync(async (req, res) => { - assertRouteIdMatchesBody(req); + wrapAsync(async (req: TourPageUpdateRequest, res) => { + assertBodyIdMatchesRouteId(req.params.id, req.body); await Tour_pagesService.update({ id: req.params.id, data: req.body.data, - currentUser: req.currentUser, - runtimeContext: req.runtimeContext, + ...getRouteServiceContext(req), }); res.status(200).send(true); }), @@ -246,11 +257,10 @@ router.put( router.delete( '/:id', validateRequest(crudSchemas.remove), - wrapAsync(async (req, res) => { + wrapAsync(async (req: Request, res) => { await Tour_pagesService.remove({ id: req.params.id, - currentUser: req.currentUser, - runtimeContext: req.runtimeContext, + ...getRouteServiceContext(req), }); res.status(200).send(true); }), @@ -260,11 +270,10 @@ router.delete( router.post( '/deleteByIds', validateRequest(crudSchemas.deleteByIds), - wrapAsync(async (req, res) => { + wrapAsync(async (req: TourPageDeleteByIdsRequest, res) => { await Tour_pagesService.deleteByIds({ ids: req.body.data, - currentUser: req.currentUser, - runtimeContext: req.runtimeContext, + ...getRouteServiceContext(req), }); res.status(200).send(true); }), @@ -274,14 +283,11 @@ router.post( router.get( '/', validateRequest(crudSchemas.list), - wrapAsync(async (req, res) => { + wrapAsync(async (req: TourPageListRequest, res) => { const filetype = req.query.filetype; - const currentUser = req.currentUser; - const runtimeContext = req.runtimeContext; const payload = await Tour_pagesDBApi.findAll(req.query, { - currentUser, - runtimeContext, + ...getRouteServiceContext(req), }); // Eagerly populate reverseVideoUrl for navigation elements @@ -315,7 +321,7 @@ router.get( router.post( '/reverse-video-status', validateRequest(tourPageSchemas.reverseVideoStatus), - wrapAsync(async (req, res) => { + wrapAsync(async (req: TourPageReverseVideoStatusRequest, res) => { const { storageKeys } = req.body; const status = await Tour_pagesService.checkReverseVideoStatus(storageKeys); @@ -328,12 +334,9 @@ router.get( '/count', validateRequest(crudSchemas.count), wrapAsync(async (req, res) => { - const currentUser = req.currentUser; - const runtimeContext = req.runtimeContext; const payload = await Tour_pagesDBApi.findAll(req.query, { countOnly: true, - currentUser, - runtimeContext, + ...getRouteServiceContext(req), }); res.status(200).send(payload); }), @@ -345,9 +348,9 @@ router.get( validateRequest(crudSchemas.autocomplete), wrapAsync(async (req, res) => { const payload = await Tour_pagesDBApi.findAllAutocomplete({ - query: req.query.query, - limit: req.query.limit, - offset: req.query.offset, + query: getStringQueryValue(req.query.query), + limit: getStringQueryValue(req.query.limit), + offset: getStringQueryValue(req.query.offset), }); res.status(200).send(payload); }), @@ -357,11 +360,10 @@ router.get( router.get( '/:id', validateRequest(crudSchemas.findOne), - wrapAsync(async (req, res) => { - const runtimeContext = req.runtimeContext; + wrapAsync(async (req: Request, res) => { let payload = await Tour_pagesDBApi.findBy( { id: req.params.id }, - { runtimeContext }, + getRouteServiceContext(req), ); // Eagerly populate reverseVideoUrl for navigation elements @@ -375,4 +377,4 @@ router.get( router.use('/', commonErrorHandler); -module.exports = router; +export default router; diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js deleted file mode 100644 index 9c6e177..0000000 --- a/backend/src/routes/users.js +++ /dev/null @@ -1,35 +0,0 @@ -const { createEntityRouter } = require('../factories/router.factory'); -const UsersService = require('../services/users'); -const UsersDBApi = require('../db/api/users'); -const { wrapAsync } = require('../helpers'); -const { users: userSchemas } = require('../validators/request-schemas'); - -// Create base router with factory (includes all standard CRUD endpoints) -const router = createEntityRouter('users', UsersService, UsersDBApi, { - permissionEntity: 'users', - csvFields: ['id', 'firstName', 'lastName', 'phoneNumber', 'email'], - validation: { - create: userSchemas.create, - update: userSchemas.update, - }, -}); - -// Override GET /:id to remove password from response -// Note: This needs to be added BEFORE the router is exported -// The factory already registered this route, so we add middleware to sanitize -const originalGetById = router.stack.find( - (layer) => layer.route?.path === '/:id' && layer.route?.methods?.get, -); - -if (originalGetById) { - originalGetById.route.stack[0].handle = wrapAsync(async (req, res) => { - // Call original handler with a custom response - const payload = await UsersDBApi.findBy({ id: req.params.id }); - if (payload) { - delete payload.password; - } - res.status(200).send(payload); - }); -} - -module.exports = router; diff --git a/backend/src/routes/users.ts b/backend/src/routes/users.ts new file mode 100644 index 0000000..14f03be --- /dev/null +++ b/backend/src/routes/users.ts @@ -0,0 +1,64 @@ +import { createEntityRouter } from '../factories/router.factory.ts'; +import { wrapAsync } from '../helpers.ts'; +import UsersDBApi from '../db/api/users.ts'; +import UsersService from '../services/users.ts'; +import type { MutableRouteLayer } from '../types/index.ts'; +import { users as userSchemas } from '../validators/request-schemas.ts'; +import type { Request } from 'express'; + +// Create base router with factory (includes all standard CRUD endpoints) +const router = createEntityRouter('users', UsersService, UsersDBApi, { + permissionEntity: 'users', + csvFields: ['id', 'firstName', 'lastName', 'phoneNumber', 'email'], + validation: { + create: userSchemas.create, + update: userSchemas.update, + }, +}); + +// Override GET /:id to remove password from response +// Note: This needs to be added BEFORE the router is exported +// The factory already registered this route, so we add middleware to sanitize +function isGetByIdRouteLayer(value: unknown): value is MutableRouteLayer { + if (!value || typeof value !== 'object' || !('route' in value)) { + return false; + } + + const { route } = value; + if (!route || typeof route !== 'object') { + return false; + } + + const methods: unknown = 'methods' in route ? route.methods : undefined; + + return ( + 'path' in route && + route.path === '/:id' && + methods !== null && + methods !== undefined && + typeof methods === 'object' && + 'get' in methods && + methods.get === true && + 'stack' in route && + Array.isArray(route.stack) + ); +} + +const routerLayers: readonly unknown[] = router.stack; +const originalGetById = routerLayers.find(isGetByIdRouteLayer); +const originalGetByIdHandler = originalGetById?.route.stack[0]; + +if (originalGetByIdHandler) { + originalGetByIdHandler.handle = wrapAsync( + async (req: Request<{ id: string }>, res) => { + // Call original handler with a custom response + const payload = await UsersDBApi.findBy({ id: req.params.id }); + if (payload) { + delete payload.password; + } + res.status(200).send(payload); + }, + ); +} + +export default router; diff --git a/backend/src/services/access-policy-audit.js b/backend/src/services/access-policy-audit.ts similarity index 62% rename from backend/src/services/access-policy-audit.js rename to backend/src/services/access-policy-audit.ts index ef70bcb..24c846a 100644 --- a/backend/src/services/access-policy-audit.js +++ b/backend/src/services/access-policy-audit.ts @@ -1,7 +1,43 @@ -const db = require('../db/models'); +import db from '../db/models/index.ts'; +import type { + AccessPolicyAuditCleanupResult, + AccessPolicyAuditGrantRow, + AccessPolicyAuditOptions, + AccessPolicyAuditReport, + AccessPolicyAuditRoleRow, + PublicRolePermissionViolation, +} from '../types/index.ts'; -class AccessPolicyAuditService { - static async findViolations(options = {}) { +function isNonEmptyString(value: string | undefined): value is string { + return typeof value === 'string' && value.length > 0; +} + +function getPublicRolePermissions( + roles: readonly AccessPolicyAuditRoleRow[], +): PublicRolePermissionViolation[] { + return roles.flatMap((role) => + (role.permissions || []).map((permission) => ({ + roleId: role.id, + id: permission.id, + name: permission.name, + })), + ); +} + +function isNonPublicGrant(grant: AccessPolicyAuditGrantRow): boolean { + return grant.user?.app_role?.name !== 'Public'; +} + +function withTransactionOption( + options: AccessPolicyAuditOptions, +): AccessPolicyAuditOptions { + return options.transaction ? { transaction: options.transaction } : {}; +} + +export default class AccessPolicyAuditService { + static async findViolations( + options: AccessPolicyAuditOptions = {}, + ): Promise { const transaction = options.transaction; const publicRoles = await db.roles.findAll({ @@ -10,14 +46,6 @@ class AccessPolicyAuditService { transaction, }); - const publicRolePermissions = publicRoles.flatMap((role) => - (role.permissions || []).map((permission) => ({ - roleId: role.id, - permissionId: permission.id, - permissionName: permission.name, - })), - ); - const publicUsersWithCustomPermissions = await db.users.findAll({ attributes: ['id', 'email'], include: [ @@ -62,16 +90,10 @@ class AccessPolicyAuditService { }); const nonPublicGrants = - productionPresentationAccessForNonPublicUsers.filter( - (grant) => grant.user?.app_role?.name !== 'Public', - ); + productionPresentationAccessForNonPublicUsers.filter(isNonPublicGrant); return { - publicRolePermissions: publicRolePermissions.map((entry) => ({ - roleId: entry.roleId, - id: entry.permissionId, - name: entry.permissionName, - })), + publicRolePermissions: getPublicRolePermissions(publicRoles), publicUsersWithCustomPermissions: publicUsersWithCustomPermissions.map( (user) => ({ id: user.id, @@ -97,7 +119,7 @@ class AccessPolicyAuditService { }; } - static hasViolations(report) { + static hasViolations(report: AccessPolicyAuditReport): boolean { return ( report.publicRolePermissions.length > 0 || report.publicUsersWithCustomPermissions.length > 0 || @@ -105,28 +127,34 @@ class AccessPolicyAuditService { ); } - static async cleanupViolations(options = {}) { - const transaction = options.transaction; - const report = await this.findViolations({ transaction }); + static async cleanupViolations( + options: AccessPolicyAuditOptions = {}, + ): Promise { + const transactionOptions = withTransactionOption(options); + const report = await this.findViolations(transactionOptions); const publicRoleIds = [ ...new Set( report.publicRolePermissions .map((permission) => permission.roleId) - .filter(Boolean), + .filter(isNonEmptyString), ), ]; for (const publicRoleId of publicRoleIds) { - const publicRole = await db.roles.findByPk(publicRoleId, { transaction }); + const publicRole = await db.roles.findByPk(publicRoleId, { + ...transactionOptions, + }); if (publicRole) { - await publicRole.setPermissions([], { transaction }); + await publicRole.setPermissions([], transactionOptions); } } for (const userReport of report.publicUsersWithCustomPermissions) { - const user = await db.users.findByPk(userReport.id, { transaction }); - await user.setCustom_permissions([], { transaction }); + const user = await db.users.findByPk(userReport.id, transactionOptions); + if (user) { + await user.setCustom_permissions([], transactionOptions); + } } const grantIds = report.productionPresentationAccessForNonPublicUsers.map( @@ -136,7 +164,7 @@ class AccessPolicyAuditService { if (grantIds.length > 0) { await db.production_presentation_access.destroy({ where: { id: { [db.Sequelize.Op.in]: grantIds } }, - transaction, + ...transactionOptions, }); } @@ -149,5 +177,3 @@ class AccessPolicyAuditService { }; } } - -module.exports = AccessPolicyAuditService; diff --git a/backend/src/services/access-policy.js b/backend/src/services/access-policy.js deleted file mode 100644 index f260ca0..0000000 --- a/backend/src/services/access-policy.js +++ /dev/null @@ -1,146 +0,0 @@ -const db = require('../db/models'); - -const PUBLIC_ROLE = 'Public'; -const PLATFORM_WIDE_ROLES = new Set([ - 'Administrator', - 'Platform Owner', - 'Account Manager', -]); - -class AccessPolicy { - static normalizeSlug(slug) { - return String(slug || '') - .trim() - .replace(/^\/+|\/+$/g, '') - .toLowerCase(); - } - - static getRoleName(user) { - return user?.app_role?.name || user?.role?.name || null; - } - - static getStandaloneRoleName(role) { - return role?.name || null; - } - - static getCustomPermissions(user) { - return Array.isArray(user?.custom_permissions) - ? user.custom_permissions - : []; - } - - static getRolePermissions(user) { - const permissions = []; - - if (Array.isArray(user?.app_role?.permissions)) { - permissions.push(...user.app_role.permissions); - } - - if (Array.isArray(user?.app_role_permissions)) { - permissions.push(...user.app_role_permissions); - } - - return permissions; - } - - static getPermissionName(permission) { - return typeof permission === 'string' ? permission : permission?.name; - } - - static getEffectivePermissionNames(user) { - return new Set( - [...this.getRolePermissions(user), ...this.getCustomPermissions(user)] - .map((permission) => this.getPermissionName(permission)) - .filter(Boolean), - ); - } - - static async getRolePermissionNames(role) { - if (!role) return new Set(); - if (this.getStandaloneRoleName(role) === PUBLIC_ROLE) return new Set(); - - if (Array.isArray(role.permissions)) { - return new Set( - role.permissions - .map((permission) => this.getPermissionName(permission)) - .filter(Boolean), - ); - } - - if (typeof role.getPermissions === 'function') { - const permissions = await role.getPermissions(); - return new Set( - permissions - .map((permission) => this.getPermissionName(permission)) - .filter(Boolean), - ); - } - - return null; - } - - static async hasPermission(user, permission) { - if (!user || !permission) return false; - if (this.isPublicUser(user)) return false; - return this.getEffectivePermissionNames(user).has(permission); - } - - static isPublicUser(user) { - return this.getRoleName(user) === PUBLIC_ROLE; - } - - static isInternalUser(user) { - return Boolean(user?.id) && !this.isPublicUser(user); - } - - static isPlatformWideRole(user) { - return PLATFORM_WIDE_ROLES.has(this.getRoleName(user)); - } - - static canUseAdminApi(user) { - return ( - !this.isPublicUser(user) && - this.getEffectivePermissionNames(user).size > 0 - ); - } - - static async getProjectBySlug(slug, options = {}) { - const normalizedSlug = this.normalizeSlug(slug); - if (!normalizedSlug) return null; - - return db.projects.findOne({ - where: { slug: normalizedSlug }, - attributes: ['id', 'name', 'slug', 'production_presentation_visibility'], - transaction: options.transaction, - }); - } - - static async canViewProductionPresentation(user, projectSlug, options = {}) { - const project = await this.getProjectBySlug(projectSlug, options); - if (!project) return false; - - if (project.production_presentation_visibility !== 'private') { - return true; - } - - if (this.canUseAdminApi(user)) { - return true; - } - - if (!this.isPublicUser(user) || !user?.id) { - return false; - } - - const access = await db.production_presentation_access.findOne({ - where: { - projectId: project.id, - userId: user.id, - }, - transaction: options.transaction, - }); - - return Boolean(access); - } -} - -module.exports = AccessPolicy; diff --git a/backend/src/services/access-policy.ts b/backend/src/services/access-policy.ts new file mode 100644 index 0000000..090527f --- /dev/null +++ b/backend/src/services/access-policy.ts @@ -0,0 +1,189 @@ +import db from '../db/models/index.ts'; +import type { + AccessPolicyOptions, + AccessPolicyUser, + PermissionName, + PermissionRecord, + ProductionPresentationProject, + RoleRecord, + RoleWithPermissionLoader, +} from '../types/index.ts'; + +const PUBLIC_ROLE = 'Public'; +const PLATFORM_WIDE_ROLES: ReadonlySet = new Set([ + 'Administrator', + 'Platform Owner', + 'Account Manager', +]); + +function isNonEmptyString(value: string | undefined): value is string { + return typeof value === 'string' && value.length > 0; +} + +export default class AccessPolicy { + static normalizeSlug(slug: unknown): string { + const rawSlug = typeof slug === 'string' ? slug : ''; + return rawSlug + .trim() + .replace(/^\/+|\/+$/g, '') + .toLowerCase(); + } + + static getRoleName(user: AccessPolicyUser): string | null { + return user?.app_role?.name || user?.role?.name || null; + } + + static getStandaloneRoleName(role: RoleRecord | null | undefined): string | null { + return role?.name || null; + } + + static getCustomPermissions( + user: AccessPolicyUser, + ): ReadonlyArray { + return user?.custom_permissions || []; + } + + static getRolePermissions( + user: AccessPolicyUser, + ): ReadonlyArray { + const permissions: Array = []; + + if (user?.app_role?.permissions) { + permissions.push(...user.app_role.permissions); + } + + if (user?.app_role_permissions) { + permissions.push(...user.app_role_permissions); + } + + return permissions; + } + + static getPermissionName( + permission: PermissionRecord | PermissionName, + ): string | undefined { + return typeof permission === 'string' ? permission : permission.name; + } + + static getEffectivePermissionNames(user: AccessPolicyUser): Set { + const permissionNames = new Set(); + + for (const permission of [ + ...this.getRolePermissions(user), + ...this.getCustomPermissions(user), + ]) { + const permissionName = this.getPermissionName(permission); + if (isNonEmptyString(permissionName)) { + permissionNames.add(permissionName); + } + } + + return permissionNames; + } + + static async getRolePermissionNames( + role: RoleWithPermissionLoader | null | undefined, + ): Promise | null> { + if (!role) return new Set(); + if (this.getStandaloneRoleName(role) === PUBLIC_ROLE) return new Set(); + + if (Array.isArray(role.permissions)) { + return this.collectPermissionNames(role.permissions); + } + + if (typeof role.getPermissions === 'function') { + const permissions = await role.getPermissions(); + return this.collectPermissionNames(permissions); + } + + return null; + } + + static hasPermission( + user: AccessPolicyUser, + permission: string | null | undefined, + ): Promise { + if (!user || !permission) return Promise.resolve(false); + if (this.isPublicUser(user)) return Promise.resolve(false); + return Promise.resolve(this.getEffectivePermissionNames(user).has(permission)); + } + + static isPublicUser(user: AccessPolicyUser): boolean { + return this.getRoleName(user) === PUBLIC_ROLE; + } + + static isInternalUser(user: AccessPolicyUser): boolean { + return Boolean(user?.id) && !this.isPublicUser(user); + } + + static isPlatformWideRole(user: AccessPolicyUser): boolean { + return PLATFORM_WIDE_ROLES.has(this.getRoleName(user) || ''); + } + + static canUseAdminApi(user: AccessPolicyUser): boolean { + return ( + !this.isPublicUser(user) && + this.getEffectivePermissionNames(user).size > 0 + ); + } + + static async getProjectBySlug( + slug: unknown, + options: AccessPolicyOptions = {}, + ): Promise { + const normalizedSlug = this.normalizeSlug(slug); + if (!normalizedSlug) return null; + + return db.projects.findOne({ + where: { slug: normalizedSlug }, + attributes: ['id', 'name', 'slug', 'production_presentation_visibility'], + transaction: options.transaction, + }); + } + + static async canViewProductionPresentation( + user: AccessPolicyUser, + projectSlug: unknown, + options: AccessPolicyOptions = {}, + ): Promise { + const project = await this.getProjectBySlug(projectSlug, options); + if (!project) return false; + + if (project.production_presentation_visibility !== 'private') { + return true; + } + + if (this.canUseAdminApi(user)) { + return true; + } + + if (!this.isPublicUser(user) || !user?.id) { + return false; + } + + const access = await db.production_presentation_access.findOne({ + where: { + projectId: project.id, + userId: user.id, + }, + transaction: options.transaction, + }); + + return Boolean(access); + } + + private static collectPermissionNames( + permissions: ReadonlyArray, + ): Set { + const permissionNames = new Set(); + + for (const permission of permissions) { + const permissionName = this.getPermissionName(permission); + if (isNonEmptyString(permissionName)) { + permissionNames.add(permissionName); + } + } + + return permissionNames; + } +} diff --git a/backend/src/services/access_logs.js b/backend/src/services/access_logs.js deleted file mode 100644 index d963bfb..0000000 --- a/backend/src/services/access_logs.js +++ /dev/null @@ -1,6 +0,0 @@ -const Access_logsDBApi = require('../db/api/access_logs'); -const { createEntityService } = require('../factories/service.factory'); - -module.exports = createEntityService(Access_logsDBApi, { - entityName: 'access_logs', -}); diff --git a/backend/src/services/access_logs.ts b/backend/src/services/access_logs.ts new file mode 100644 index 0000000..5e01597 --- /dev/null +++ b/backend/src/services/access_logs.ts @@ -0,0 +1,6 @@ +import Access_logsDBApi from '../db/api/access_logs.ts'; +import { createEntityService } from '../factories/service.factory.ts'; + +export default createEntityService(Access_logsDBApi, { + entityName: 'access_logs', +}); diff --git a/backend/src/services/asset_variants.js b/backend/src/services/asset_variants.js deleted file mode 100644 index e3749e0..0000000 --- a/backend/src/services/asset_variants.js +++ /dev/null @@ -1,6 +0,0 @@ -const Asset_variantsDBApi = require('../db/api/asset_variants'); -const { createEntityService } = require('../factories/service.factory'); - -module.exports = createEntityService(Asset_variantsDBApi, { - entityName: 'asset_variants', -}); diff --git a/backend/src/services/asset_variants.ts b/backend/src/services/asset_variants.ts new file mode 100644 index 0000000..7a04f6b --- /dev/null +++ b/backend/src/services/asset_variants.ts @@ -0,0 +1,6 @@ +import Asset_variantsDBApi from '../db/api/asset_variants.ts'; +import { createEntityService } from '../factories/service.factory.ts'; + +export default createEntityService(Asset_variantsDBApi, { + entityName: 'asset_variants', +}); diff --git a/backend/src/services/assets.js b/backend/src/services/assets.ts similarity index 51% rename from backend/src/services/assets.js rename to backend/src/services/assets.ts index 6cf9c61..6e8982a 100644 --- a/backend/src/services/assets.js +++ b/backend/src/services/assets.ts @@ -1,21 +1,26 @@ -const AssetsDBApi = require('../db/api/assets'); -const { createEntityService } = require('../factories/service.factory'); -const { +import AssetsDBApi from '../db/api/assets.ts'; +import { createEntityService } from '../factories/service.factory.ts'; +import { assertCreateOptions, assertUpdateOptions, -} = require('../contracts/entity-options'); -const ValidationError = require('./notifications/errors/validation'); -const { downloadToTempFile } = require('./file'); -const { probeMediaMetadata } = require('./videoProcessing'); -const { logger } = require('../utils/logger'); +} from '../contracts/entity-options.ts'; +import ValidationError from './notifications/errors/validation.ts'; +import FileService from './file.ts'; +import { probeMediaMetadata } from './videoProcessing.ts'; +import { logger } from '../utils/logger.ts'; +import type { + AssetCreateOptions, + AssetData, + AssetEmbedUrlResult, + AssetFindByOptions, + AssetListFilter, + AssetMimePatternMap, + AssetMimeValidationResult, + AssetRecord, + AssetUpdateOptions, +} from '../types/index.ts'; -// Note: Reversed video generation was moved to tour_pages.js to generate -// only when videos are actually used as transitions (not all video uploads). - -/** - * Valid MIME type patterns for each asset type - */ -const VALID_MIME_PATTERNS = { +const VALID_MIME_PATTERNS: AssetMimePatternMap = { image: { prefixes: ['image/'], description: 'image (jpeg, png, gif, webp, svg, etc.)', @@ -29,16 +34,12 @@ const VALID_MIME_PATTERNS = { description: 'audio (mp3, wav, ogg, etc.)', }, embed: { - // Embeds don't have MIME types - skip validation prefixes: [], description: 'embed (360/3D iframe)', skipValidation: true, }, }; -/** - * Allowed domains for embed URLs (security: only trusted providers) - */ const ALLOWED_EMBED_DOMAINS = [ 'matterport.com', 'my.matterport.com', @@ -54,22 +55,15 @@ const ALLOWED_EMBED_DOMAINS = [ '360stories.com', ]; -/** - * Extract and validate embed URL from iframe code or direct URL - * @param {string} embedCode - Full iframe HTML or direct URL - * @returns {{ url: string, provider: string }} - * @throws {ValidationError} If URL is invalid or domain not allowed - */ -function extractEmbedUrl(embedCode) { +function extractEmbedUrl(embedCode: string | null | undefined): AssetEmbedUrlResult { if (!embedCode?.trim()) { throw new ValidationError('Embed code is required'); } - // Extract src from iframe HTML const srcMatch = embedCode.match(/src=["']([^"']+)["']/i); - const urlString = srcMatch ? srcMatch[1] : embedCode.trim(); + const urlString = srcMatch?.[1] ?? embedCode.trim(); - let url; + let url: URL; try { url = new URL(urlString); } catch { @@ -82,7 +76,7 @@ function extractEmbedUrl(embedCode) { const hostname = url.hostname.replace(/^www\./, ''); const isAllowed = ALLOWED_EMBED_DOMAINS.some( - (domain) => hostname === domain || hostname.endsWith('.' + domain), + (domain) => hostname === domain || hostname.endsWith(`.${domain}`), ); if (!isAllowed) { @@ -91,45 +85,34 @@ function extractEmbedUrl(embedCode) { ); } - // Extract provider name (e.g., 'matterport', 'kuula') - const provider = hostname.split('.').slice(-2, -1)[0]; + const provider = hostname.split('.').slice(-2, -1)[0] ?? hostname; return { url: urlString, provider }; } -/** - * Validate that mime_type matches asset_type - * @param {string} assetType - Expected asset type (image, video, audio, embed) - * @param {string} mimeType - Actual MIME type of the file - * @returns {{ valid: boolean, error?: string, skipValidation?: boolean }} - */ -function validateAssetMimeType(assetType, mimeType) { - // If no asset_type specified, skip validation +function validateAssetMimeType( + assetType: string | null | undefined, + mimeType: string | null | undefined, +): AssetMimeValidationResult { if (!assetType) { return { valid: true }; } const patterns = VALID_MIME_PATTERNS[assetType]; - // If asset_type is not one we validate (e.g., 'file'), skip validation if (!patterns) { return { valid: true }; } - // If patterns has skipValidation flag (e.g., embeds), skip MIME validation if (patterns.skipValidation) { return { valid: true, skipValidation: true }; } - // If no mime_type provided, we can't validate but allow it - // (browser may not always send mime type) if (!mimeType) { return { valid: true }; } - const normalizedMime = String(mimeType).toLowerCase().trim(); - - // Check if mime_type matches any of the valid prefixes + const normalizedMime = mimeType.toLowerCase().trim(); const isValid = patterns.prefixes.some((prefix) => normalizedMime.startsWith(prefix), ); @@ -144,16 +127,72 @@ function validateAssetMimeType(assetType, mimeType) { return { valid: true }; } -// Create base service from factory -const BaseService = createEntityService(AssetsDBApi, { +function buildCreateServiceOptions( + options: AssetCreateOptions, +): Pick { + const serviceOptions: Pick< + AssetCreateOptions, + 'currentUser' | 'transaction' | 'runtimeContext' + > = {}; + + if (options.currentUser !== undefined) { + serviceOptions.currentUser = options.currentUser; + } + if (options.transaction !== undefined) { + serviceOptions.transaction = options.transaction; + } + if (options.runtimeContext !== undefined) { + serviceOptions.runtimeContext = options.runtimeContext; + } + + return serviceOptions; +} + +function buildUpdateServiceOptions( + options: AssetUpdateOptions, +): Pick { + const serviceOptions: Pick< + AssetUpdateOptions, + 'currentUser' | 'transaction' | 'runtimeContext' + > = {}; + + if (options.currentUser !== undefined) { + serviceOptions.currentUser = options.currentUser; + } + if (options.transaction !== undefined) { + serviceOptions.transaction = options.transaction; + } + if (options.runtimeContext !== undefined) { + serviceOptions.runtimeContext = options.runtimeContext; + } + + return serviceOptions; +} + +function buildAssetFindByOptions(options: AssetUpdateOptions): AssetFindByOptions { + const findByOptions: AssetFindByOptions = {}; + + if (options.transaction !== undefined) { + findByOptions.transaction = options.transaction; + } + if (options.runtimeContext !== undefined) { + findByOptions.runtimeContext = options.runtimeContext; + } + + return findByOptions; +} + +const BaseService = createEntityService< + AssetRecord, + AssetData, + AssetData, + AssetListFilter +>(AssetsDBApi, { entityName: 'assets', }); -/** - * Assets Service with validation and video pre-processing - */ -class AssetsService extends BaseService { - static async enrichStoredMediaMetadata(data) { +export default class AssetsService extends BaseService { + static async enrichStoredMediaMetadata(data: AssetData): Promise { const assetType = data.asset_type; const storageKey = data.storage_key; @@ -165,7 +204,7 @@ class AssetsService extends BaseService { let tempFile = null; try { - tempFile = await downloadToTempFile(storageKey); + tempFile = await FileService.downloadToTempFile(storageKey); const metadata = await probeMediaMetadata(tempFile.filePath); if (!metadata) { @@ -186,8 +225,8 @@ class AssetsService extends BaseService { ); return data; } finally { - if (tempFile?.cleanup) { - await tempFile.cleanup().catch((cleanupError) => { + if (tempFile) { + await tempFile.cleanup().catch((cleanupError: unknown) => { logger.warn( { err: cleanupError, storageKey }, 'Failed to cleanup media probe temp file', @@ -197,15 +236,12 @@ class AssetsService extends BaseService { } } - /** - * Create asset with MIME type validation, embed URL validation, and video pre-processing - */ - static async create(options) { + static override async create( + options: AssetCreateOptions, + ): Promise { assertCreateOptions(options, 'Service'); let { data } = options; - const { currentUser, transaction, runtimeContext } = options; - // Validate asset_type and mime_type match const assetType = data.asset_type; const mimeType = data.mime_type; @@ -214,49 +250,41 @@ class AssetsService extends BaseService { throw new ValidationError(validation.error); } - // Handle embed assets: extract and validate URL, set provider if (assetType === 'embed') { const { url, provider } = extractEmbedUrl(data.embed_code); - data.cdn_url = url; // Store extracted URL for easy access - data.embed_provider = provider; + data = { + ...data, + cdn_url: url, + embed_provider: provider, + }; } data = await AssetsService.enrichStoredMediaMetadata(data); - // Call parent create - const asset = await super.create({ + const serviceOptions = buildCreateServiceOptions(options); + + return super.create({ data, - currentUser, - transaction, - runtimeContext, + ...serviceOptions, }); - - // Note: Reversed video generation is handled by tour_pages.js when the video - // is assigned to a navigation element. This avoids unnecessary processing - // for videos that aren't used as transitions. - - return asset; } - /** - * Update asset with MIME type validation and embed URL validation - */ - static async update(options) { + static override async update( + options: AssetUpdateOptions, + ): Promise { assertUpdateOptions(options, 'Service'); let { data } = options; - const { id, currentUser, transaction, runtimeContext } = options; + const { id } = options; const existingAsset = await AssetsDBApi.findBy( { id }, - { transaction, runtimeContext }, + buildAssetFindByOptions(options), ); - // If updating asset_type or mime_type, validate they match if (data.asset_type || data.mime_type) { const assetType = data.asset_type || existingAsset?.asset_type; const mimeType = data.mime_type || existingAsset?.mime_type; - // Only validate if both are provided in the update if (assetType && mimeType) { const validation = validateAssetMimeType(assetType, mimeType); if (!validation.valid) { @@ -265,27 +293,24 @@ class AssetsService extends BaseService { } } - // Handle embed assets: if embed_code is being updated, re-extract URL and provider if (data.embed_code) { const { url, provider } = extractEmbedUrl(data.embed_code); - data.cdn_url = url; - data.embed_provider = provider; + data = { + ...data, + cdn_url: url, + embed_provider: provider, + }; } - data = await AssetsService.enrichStoredMediaMetadata({ - ...existingAsset, - ...data, - }); + const metadataInput = existingAsset ? { ...existingAsset, ...data } : data; + data = await AssetsService.enrichStoredMediaMetadata(metadataInput); + + const serviceOptions = buildUpdateServiceOptions(options); - // Call parent update return super.update({ id, data, - currentUser, - transaction, - runtimeContext, + ...serviceOptions, }); } } - -module.exports = AssetsService; diff --git a/backend/src/services/auth.js b/backend/src/services/auth.ts similarity index 55% rename from backend/src/services/auth.js rename to backend/src/services/auth.ts index 53961d2..8751bbc 100644 --- a/backend/src/services/auth.js +++ b/backend/src/services/auth.ts @@ -1,17 +1,30 @@ -const db = require('../db/models'); -const UsersDBApi = require('../db/api/users'); -const ValidationError = require('./notifications/errors/validation'); -const ForbiddenError = require('./notifications/errors/forbidden'); -const bcrypt = require('bcrypt'); -const EmailAddressVerificationEmail = require('./email/list/addressVerification'); -const InvitationEmail = require('./email/list/invitation'); -const PasswordResetEmail = require('./email/list/passwordReset'); -const EmailSender = require('./email'); -const config = require('../config'); -const helpers = require('../helpers'); +import bcrypt from 'bcrypt'; -class Auth { - static async signin(email, password) { +import db from '../db/models/index.ts'; +import UsersDBApi from '../db/api/users.ts'; +import ValidationError from './notifications/errors/validation.ts'; +import ForbiddenError from './notifications/errors/forbidden.ts'; +import EmailAddressVerificationEmail from './email/list/addressVerification.ts'; +import InvitationEmail from './email/list/invitation.ts'; +import PasswordResetEmail from './email/list/passwordReset.ts'; +import EmailSender from './email/index.ts'; +import config from '../config.ts'; +import { jwtSign } from '../helpers.ts'; +import { logger } from '../utils/logger.ts'; +import type { + AuthPasswordRequestContext, + AuthRequestContext, + AuthTokenPayload, + CurrentUser, + EmailSendResult, + EmailTemplate, + PasswordResetEmailType, + ProfileBody, + UserRecord, +} from '../types/index.ts'; + +export default class Auth { + static async signin(email: string, password: string): Promise { const user = await UsersDBApi.findBy({ email }); if (!user) { @@ -40,57 +53,74 @@ class Auth { throw new ValidationError('auth.wrongPassword'); } - const data = { + const data: AuthTokenPayload = { user: { id: user.id, email: user.email, }, }; - return helpers.jwtSign(data); + return jwtSign(data); } - static async sendEmailAddressVerificationEmail(email, host) { - let link; + static async sendEmailAddressVerificationEmail( + email: string, + host?: string, + ): Promise { + let link: string; + try { const token = await UsersDBApi.generateEmailVerificationToken(email); link = `${host}/verify-email?token=${token}`; } catch (error) { - console.error(error); + logger.error( + { err: error, email }, + 'Failed to generate email verification token', + ); throw new ValidationError('auth.emailAddressVerificationEmail.error'); } - const emailAddressVerificationEmail = new EmailAddressVerificationEmail( - email, + const emailAddressVerificationEmail = new EmailAddressVerificationEmail({ + to: email, link, - ); + }); return new EmailSender(emailAddressVerificationEmail).send(); } - static async sendPasswordResetEmail(email, type = 'register', host) { - let link; + static async sendPasswordResetEmail( + email: string, + type: PasswordResetEmailType = 'register', + host?: string, + ): Promise { + let link: string; try { const token = await UsersDBApi.generatePasswordResetToken(email); link = `${host}/password-reset?token=${token}`; } catch (error) { - console.error(error); + logger.error( + { err: error, email, type }, + 'Failed to generate password reset token', + ); throw new ValidationError('auth.passwordReset.error'); } - let passwordResetEmail; + let passwordResetEmail: EmailTemplate; + if (type === 'register') { - passwordResetEmail = new PasswordResetEmail(email, link); - } - if (type === 'invitation') { - passwordResetEmail = new InvitationEmail(email, link); + passwordResetEmail = new PasswordResetEmail({ to: email, link }); + } else { + passwordResetEmail = new InvitationEmail({ to: email, host: link }); } return new EmailSender(passwordResetEmail).send(); } - static async verifyEmail(token, options = {}) { + static async verifyEmail( + token: string, + options: AuthRequestContext = {}, + ): Promise { const user = await UsersDBApi.findByEmailVerificationToken(token, options); if (!user) { @@ -102,12 +132,20 @@ class Auth { return UsersDBApi.markEmailVerified(user.id, options); } - static async passwordUpdate(currentPassword, newPassword, options) { - const currentUser = options.currentUser || null; + static async passwordUpdate( + currentPassword: string, + newPassword: string, + options: AuthPasswordRequestContext, + ): Promise { + const currentUser = options.currentUser ?? null; if (!currentUser) { throw new ForbiddenError(); } + if (!currentUser.password) { + throw new ValidationError('auth.wrongPassword'); + } + const currentPasswordMatch = await bcrypt.compare( currentPassword, currentUser.password, @@ -134,7 +172,11 @@ class Auth { return UsersDBApi.updatePassword(currentUser.id, hashedPassword, options); } - static async passwordReset(token, password, options = {}) { + static async passwordReset( + token: string, + password: string, + options: AuthRequestContext = {}, + ): Promise { const user = await UsersDBApi.findByPasswordResetToken(token, options); if (!user) { @@ -149,8 +191,11 @@ class Auth { return UsersDBApi.updatePassword(user.id, hashedPassword, options); } - static async updateProfile(data, currentUser) { - let transaction = await db.sequelize.transaction(); + static async updateProfile( + data: ProfileBody['profile'], + currentUser: CurrentUser, + ): Promise { + const transaction = await db.sequelize.transaction(); try { await UsersDBApi.findBy({ id: currentUser.id }, { transaction }); @@ -169,5 +214,3 @@ class Auth { } } } - -module.exports = Auth; diff --git a/backend/src/services/element_type_defaults.js b/backend/src/services/element_type_defaults.js deleted file mode 100644 index 2429716..0000000 --- a/backend/src/services/element_type_defaults.js +++ /dev/null @@ -1,6 +0,0 @@ -const Element_type_defaultsDBApi = require('../db/api/element_type_defaults'); -const { createEntityService } = require('../factories/service.factory'); - -module.exports = createEntityService(Element_type_defaultsDBApi, { - entityName: 'element_type_defaults', -}); diff --git a/backend/src/services/element_type_defaults.ts b/backend/src/services/element_type_defaults.ts new file mode 100644 index 0000000..23f96ba --- /dev/null +++ b/backend/src/services/element_type_defaults.ts @@ -0,0 +1,6 @@ +import Element_type_defaultsDBApi from '../db/api/element_type_defaults.ts'; +import { createEntityService } from '../factories/service.factory.ts'; + +export default createEntityService(Element_type_defaultsDBApi, { + entityName: 'element_type_defaults', +}); diff --git a/backend/src/services/email/index.js b/backend/src/services/email/index.js deleted file mode 100644 index bc97a3d..0000000 --- a/backend/src/services/email/index.js +++ /dev/null @@ -1,44 +0,0 @@ -const config = require('../../config'); -const assert = require('assert'); -const nodemailer = require('nodemailer'); - -module.exports = class EmailSender { - constructor(email) { - this.email = email; - } - - async send() { - assert(this.email, 'email is required'); - assert(this.email.to, 'email.to is required'); - assert(this.email.subject, 'email.subject is required'); - assert(this.email.html, 'email.html is required'); - - const htmlContent = await this.email.html(); - - const transporter = nodemailer.createTransport(this.transportConfig); - - const mailOptions = { - from: this.from, - to: this.email.to, - subject: this.email.subject, - html: htmlContent, - headers: { - 'X-SES-CONFIGURATION-SET': 'flatlogic-app', - }, - }; - - return transporter.sendMail(mailOptions); - } - - static get isConfigured() { - return !!config.email?.auth?.pass && !!config.email?.auth?.user; - } - - get transportConfig() { - return config.email; - } - - get from() { - return config.email.from; - } -}; diff --git a/backend/src/services/email/index.ts b/backend/src/services/email/index.ts new file mode 100644 index 0000000..8997137 --- /dev/null +++ b/backend/src/services/email/index.ts @@ -0,0 +1,48 @@ +import assert from 'node:assert'; + +import nodemailer from 'nodemailer'; +import type SMTPTransport from 'nodemailer/lib/smtp-transport/index.js'; + +import config from '../../config.ts'; +import type { EmailSendResult, EmailTemplate } from '../../types/index.ts'; + +export default class EmailSender { + private readonly email: EmailTemplate; + + constructor(email: EmailTemplate) { + this.email = email; + } + + async send(): Promise { + assert(this.email, 'email is required'); + assert(this.email.to, 'email.to is required'); + assert(this.email.subject, 'email.subject is required'); + assert(typeof this.email.html === 'function', 'email.html is required'); + + const htmlContent = await this.email.html(); + const transporter = nodemailer.createTransport(this.transportConfig); + const mailOptions: SMTPTransport.MailOptions = { + from: this.from, + to: this.email.to, + subject: this.email.subject, + html: htmlContent, + headers: { + 'X-SES-CONFIGURATION-SET': 'flatlogic-app', + }, + }; + + return transporter.sendMail(mailOptions); + } + + static get isConfigured(): boolean { + return Boolean(config.email.auth?.pass && config.email.auth.user); + } + + get transportConfig(): SMTPTransport.Options { + return config.email; + } + + get from(): string { + return config.email.from; + } +} diff --git a/backend/src/services/email/list/addressVerification.js b/backend/src/services/email/list/addressVerification.js deleted file mode 100644 index 89be6d3..0000000 --- a/backend/src/services/email/list/addressVerification.js +++ /dev/null @@ -1,41 +0,0 @@ -const { getNotification } = require('../../notifications/helpers'); -const fs = require('fs').promises; -const path = require('path'); - -module.exports = class EmailAddressVerificationEmail { - constructor(to, link) { - this.to = to; - this.link = link; - } - - get subject() { - return getNotification( - 'emails.emailAddressVerification.subject', - getNotification('app.title'), - ); - } - - async html() { - try { - const templatePath = path.join( - __dirname, - '../../email/htmlTemplates/addressVerification/emailAddressVerification.html', - ); - - const template = await fs.readFile(templatePath, 'utf8'); - - const appTitle = getNotification('app.title'); - const signupUrl = this.link; - - let html = template - .replace(/{appTitle}/g, appTitle) - .replace(/{signupUrl}/g, signupUrl) - .replace(/{to}/g, this.to); - - return html; - } catch (error) { - console.error('Error generating invitation email HTML:', error); - throw error; - } - } -}; diff --git a/backend/src/services/email/list/addressVerification.ts b/backend/src/services/email/list/addressVerification.ts new file mode 100644 index 0000000..06c1005 --- /dev/null +++ b/backend/src/services/email/list/addressVerification.ts @@ -0,0 +1,51 @@ +import { promises as fs } from 'fs'; +import path from 'path'; + +import { getNotification } from '../../notifications/helpers.ts'; +import type { + EmailTemplate, + LinkEmailTemplateOptions, +} from '../../../types/index.ts'; +import { logger } from '../../../utils/logger.ts'; + +const emailTemplatesDir = path.resolve( + process.cwd(), + 'src/services/email/htmlTemplates', +); + +export default class EmailAddressVerificationEmail implements EmailTemplate { + to: string; + private readonly link: string; + + constructor({ to, link }: LinkEmailTemplateOptions) { + this.to = to; + this.link = link; + } + + get subject(): string { + return getNotification( + 'emails.emailAddressVerification.subject', + getNotification('app.title'), + ); + } + + async html(): Promise { + try { + const templatePath = path.join( + emailTemplatesDir, + 'addressVerification/emailAddressVerification.html', + ); + + const template = await fs.readFile(templatePath, 'utf8'); + const appTitle = getNotification('app.title'); + + return template + .replace(/{appTitle}/g, appTitle) + .replace(/{signupUrl}/g, this.link) + .replace(/{to}/g, this.to); + } catch (error) { + logger.error({ error }, 'Error generating email verification HTML'); + throw error; + } + } +} diff --git a/backend/src/services/email/list/invitation.js b/backend/src/services/email/list/invitation.js deleted file mode 100644 index d2afc1e..0000000 --- a/backend/src/services/email/list/invitation.js +++ /dev/null @@ -1,41 +0,0 @@ -const fs = require('fs').promises; -const path = require('path'); -const { getNotification } = require('../../notifications/helpers'); - -module.exports = class InvitationEmail { - constructor(to, host) { - this.to = to; - this.host = host; - } - - get subject() { - return getNotification( - 'emails.invitation.subject', - getNotification('app.title'), - ); - } - - async html() { - try { - const templatePath = path.join( - __dirname, - '../../email/htmlTemplates/invitation/invitationTemplate.html', - ); - - const template = await fs.readFile(templatePath, 'utf8'); - - const appTitle = getNotification('app.title'); - const signupUrl = `${this.host}&invitation=true`; - - let html = template - .replace(/{appTitle}/g, appTitle) - .replace(/{signupUrl}/g, signupUrl) - .replace(/{to}/g, this.to); - - return html; - } catch (error) { - console.error('Error generating invitation email HTML:', error); - throw error; - } - } -}; diff --git a/backend/src/services/email/list/invitation.ts b/backend/src/services/email/list/invitation.ts new file mode 100644 index 0000000..45a1496 --- /dev/null +++ b/backend/src/services/email/list/invitation.ts @@ -0,0 +1,52 @@ +import { promises as fs } from 'fs'; +import path from 'path'; + +import { getNotification } from '../../notifications/helpers.ts'; +import type { + EmailTemplate, + InvitationEmailTemplateOptions, +} from '../../../types/index.ts'; +import { logger } from '../../../utils/logger.ts'; + +const emailTemplatesDir = path.resolve( + process.cwd(), + 'src/services/email/htmlTemplates', +); + +export default class InvitationEmail implements EmailTemplate { + to: string; + private readonly host: string; + + constructor({ to, host }: InvitationEmailTemplateOptions) { + this.to = to; + this.host = host; + } + + get subject(): string { + return getNotification( + 'emails.invitation.subject', + getNotification('app.title'), + ); + } + + async html(): Promise { + try { + const templatePath = path.join( + emailTemplatesDir, + 'invitation/invitationTemplate.html', + ); + + const template = await fs.readFile(templatePath, 'utf8'); + const appTitle = getNotification('app.title'); + const signupUrl = `${this.host}&invitation=true`; + + return template + .replace(/{appTitle}/g, appTitle) + .replace(/{signupUrl}/g, signupUrl) + .replace(/{to}/g, this.to); + } catch (error) { + logger.error({ error }, 'Error generating invitation email HTML'); + throw error; + } + } +} diff --git a/backend/src/services/email/list/passwordReset.js b/backend/src/services/email/list/passwordReset.js deleted file mode 100644 index 68ba353..0000000 --- a/backend/src/services/email/list/passwordReset.js +++ /dev/null @@ -1,42 +0,0 @@ -const { getNotification } = require('../../notifications/helpers'); -const path = require('path'); -const { promises: fs } = require('fs'); - -module.exports = class PasswordResetEmail { - constructor(to, link) { - this.to = to; - this.link = link; - } - - get subject() { - return getNotification( - 'emails.passwordReset.subject', - getNotification('app.title'), - ); - } - - async html() { - try { - const templatePath = path.join( - __dirname, - '../../email/htmlTemplates/passwordReset/passwordResetEmail.html', - ); - - const template = await fs.readFile(templatePath, 'utf8'); - - const appTitle = getNotification('app.title'); - const resetUrl = this.link; - const accountName = this.to; - - let html = template - .replace(/{appTitle}/g, appTitle) - .replace(/{resetUrl}/g, resetUrl) - .replace(/{accountName}/g, accountName); - - return html; - } catch (error) { - console.error('Error generating invitation email HTML:', error); - throw error; - } - } -}; diff --git a/backend/src/services/email/list/passwordReset.ts b/backend/src/services/email/list/passwordReset.ts new file mode 100644 index 0000000..29b59d1 --- /dev/null +++ b/backend/src/services/email/list/passwordReset.ts @@ -0,0 +1,51 @@ +import { promises as fs } from 'fs'; +import path from 'path'; + +import { getNotification } from '../../notifications/helpers.ts'; +import type { + EmailTemplate, + LinkEmailTemplateOptions, +} from '../../../types/index.ts'; +import { logger } from '../../../utils/logger.ts'; + +const emailTemplatesDir = path.resolve( + process.cwd(), + 'src/services/email/htmlTemplates', +); + +export default class PasswordResetEmail implements EmailTemplate { + to: string; + private readonly link: string; + + constructor({ to, link }: LinkEmailTemplateOptions) { + this.to = to; + this.link = link; + } + + get subject(): string { + return getNotification( + 'emails.passwordReset.subject', + getNotification('app.title'), + ); + } + + async html(): Promise { + try { + const templatePath = path.join( + emailTemplatesDir, + 'passwordReset/passwordResetEmail.html', + ); + + const template = await fs.readFile(templatePath, 'utf8'); + const appTitle = getNotification('app.title'); + + return template + .replace(/{appTitle}/g, appTitle) + .replace(/{resetUrl}/g, this.link) + .replace(/{accountName}/g, this.to); + } catch (error) { + logger.error({ error }, 'Error generating password reset email HTML'); + throw error; + } + } +} diff --git a/backend/src/services/file.js b/backend/src/services/file.ts similarity index 65% rename from backend/src/services/file.js rename to backend/src/services/file.ts index fb6d912..748937d 100644 --- a/backend/src/services/file.js +++ b/backend/src/services/file.ts @@ -11,14 +11,52 @@ * - Path validation for security */ -const fs = require('fs'); -const path = require('path'); -const crypto = require('crypto'); -const os = require('os'); -const { pipeline } = require('stream/promises'); -const { format } = require('util'); +import crypto from 'crypto'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { PassThrough, type Readable } from 'stream'; +import { pipeline } from 'stream/promises'; +import { Storage } from '@google-cloud/storage'; -const config = require('../config'); +import config from '../config.ts'; +import processFile from '../middlewares/upload.ts'; +import { + getCurrentUser, + getRequestLogger, +} from '../utils/request-context.ts'; +import type { + CachedFileLookupResult, + FileByteRange, + FileCopyFailure, + FileCopyOperation, + FileCopyOptions, + FileCopyParallelOptions, + FileCopyParallelResult, + FileCopyResult, + FileCopySuccess, + FileDeleteOptions, + FileDeleteResult, + FileDownloadToTempFileResult, + FileErrorDetails, + FileErrorResponse, + FileMimeTypeMap, + FileServiceFacade, + FileServiceRequest, + FileServiceResponse, + FileStorageProviderName, + FileUploadBufferOptions, + FileUploadBufferResult, + FileUploadServiceRequest, + GCloudBucketState, + UploadChunkRequest, + UploadSessionInitRequest, + UploadSessionRequest, +} from '../types/index.ts'; +import { logger } from '../utils/logger.ts'; +import LocalStorageProvider from './file/LocalStorageProvider.ts'; +import S3StorageProvider from './file/S3StorageProvider.ts'; +import UploadSessionManager from './file/UploadSessionManager.ts'; // ============================================================================ // S3 Cache Helpers @@ -27,7 +65,7 @@ const config = require('../config'); /** * Get the local cache path for an S3 key */ -const getCachePath = (privateUrl) => { +const getCachePath = (privateUrl: string): string => { // Create a safe filename from the URL const hash = crypto.createHash('md5').update(privateUrl).digest('hex'); const ext = path.extname(privateUrl) || ''; @@ -38,7 +76,9 @@ const getCachePath = (privateUrl) => { * Check if a cached file exists and is still valid * Returns invalid if a download is in progress (.downloading file exists) */ -const getCachedFile = async (cachePath) => { +const getCachedFile = async ( + cachePath: string, +): Promise => { try { // Check if download is in progress - if so, don't use cache const downloadingPath = cachePath + '.downloading'; @@ -64,40 +104,141 @@ const getCachedFile = async (cachePath) => { /** * Ensure cache directory exists */ -const ensureCacheDir = async () => { +const ensureCacheDir = async (): Promise => { try { await fs.promises.mkdir(config.s3CacheDir, { recursive: true }); } catch (err) { - if (err.code !== 'EEXIST') throw err; + if (!hasErrorCode(err, 'EEXIST')) throw err; } }; /** * Generate ETag from file stats */ -const generateETag = (stats) => { +const generateETag = (stats: fs.Stats): string => { return `"${stats.size.toString(16)}-${stats.mtimeMs.toString(16)}"`; }; -const { logger } = require('../utils/logger'); -const S3StorageProvider = require('./file/S3StorageProvider'); -const LocalStorageProvider = require('./file/LocalStorageProvider'); -const UploadSessionManager = require('./file/UploadSessionManager'); + +const MIME_TYPES: FileMimeTypeMap = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + '.mp4': 'video/mp4', + '.webm': 'video/webm', + '.mp3': 'audio/mpeg', + '.wav': 'audio/wav', + '.ogg': 'audio/ogg', + '.pdf': 'application/pdf', + '.json': 'application/json', +}; + +const hasErrorCode = (error: unknown, code: string): boolean => { + return error instanceof Error && 'code' in error && error.code === code; +}; + +const getErrorStringProperty = ( + error: unknown, + property: 'name' | 'code' | 'message', +): string | undefined => { + if (error instanceof Error && property === 'name') { + return error.name; + } + + if (error instanceof Error && property === 'message') { + return error.message; + } + + if (typeof error !== 'object' || error === null) { + return undefined; + } + + const descriptor = Object.getOwnPropertyDescriptor(error, property); + return typeof descriptor?.value === 'string' ? descriptor.value : undefined; +}; + +const getUnknownErrorMessage = (error: unknown): string => { + return getErrorStringProperty(error, 'message') || String(error); +}; + +const toError = (error: unknown): Error => { + return error instanceof Error + ? error + : new Error(getUnknownErrorMessage(error)); +}; + +const getStorageUploadUrl = (resultUrl: string | undefined): string => { + return resultUrl || ''; +}; + +const toBufferChunk = (chunk: unknown): Buffer => { + if (Buffer.isBuffer(chunk)) { + return chunk; + } + + if (typeof chunk === 'string') { + return Buffer.from(chunk); + } + + if (chunk instanceof Uint8Array) { + return Buffer.from(chunk); + } + + throw new Error('Unsupported stream chunk type'); +}; + +const getSanitizedStringInput = (value: unknown): string => { + if (typeof value === 'string') { + return value; + } + + if (typeof value === 'number' || typeof value === 'boolean') { + return `${value}`; + } + + return ''; +}; + +const isReadableStream = (value: unknown): value is Readable => { + if (typeof value !== 'object' || value === null || !('pipe' in value)) { + return false; + } + + const descriptor = Object.getOwnPropertyDescriptor(value, 'pipe'); + return typeof descriptor?.value === 'function'; +}; + +const hasByteArrayTransformer = ( + value: unknown, +): value is { transformToByteArray(): Promise } => { + return ( + typeof value === 'object' && + value !== null && + 'transformToByteArray' in value && + typeof value.transformToByteArray === 'function' + ); +}; // ============================================================================ // Provider Initialization (Singleton) // ============================================================================ -let s3Provider = null; -let localProvider = null; -let gcloudBucket = null; -let gcloudHash = null; -let uploadSessionManager = null; +let s3Provider: S3StorageProvider | null = null; +let localProvider: LocalStorageProvider | null = null; +let gcloudBucket: GCloudBucketState['bucket'] | null = null; +let gcloudHash: string | null = null; +let uploadSessionManager: UploadSessionManager | null = null; -const getFileStorageProvider = () => { +const getFileStorageProvider = (): FileStorageProviderName => { const provider = (process.env.FILE_STORAGE_PROVIDER || '') .trim() .toLowerCase(); - if (provider) return provider; + if (provider === 's3' || provider === 'gcloud' || provider === 'local') { + return provider; + } const hasS3 = Boolean( config.s3.bucket && @@ -119,7 +260,7 @@ const getFileStorageProvider = () => { return 'local'; }; -const getS3Provider = () => { +const getS3Provider = (): S3StorageProvider => { if (!s3Provider) { s3Provider = new S3StorageProvider({ bucket: config.s3.bucket, @@ -150,31 +291,35 @@ const getS3Provider = () => { return s3Provider; }; -const getLocalProvider = () => { +const getLocalProvider = (): LocalStorageProvider => { if (!localProvider) { localProvider = new LocalStorageProvider({ basePath: config.uploadDir }); } return localProvider; }; -const getGCloudBucket = () => { +const getGCloudBucket = (): GCloudBucketState => { if (!gcloudBucket) { - const { Storage } = require('@google-cloud/storage'); - const privateKey = process.env.GC_PRIVATE_KEY.replace(/\\\n/g, '\n'); + const privateKey = (process.env.GC_PRIVATE_KEY || '').replace( + /\\\n/g, + '\n', + ); + const clientEmail = process.env.GC_CLIENT_EMAIL || ''; + const projectId = process.env.GC_PROJECT_ID || ''; const storage = new Storage({ - projectId: process.env.GC_PROJECT_ID, + projectId, credentials: { - client_email: process.env.GC_CLIENT_EMAIL, + client_email: clientEmail, private_key: privateKey, }, }); gcloudBucket = storage.bucket(config.gcloud.bucket); gcloudHash = config.gcloud.hash; } - return { bucket: gcloudBucket, hash: gcloudHash }; + return { bucket: gcloudBucket, hash: gcloudHash || '' }; }; -const getUploadSessionManager = () => { +const getUploadSessionManager = (): UploadSessionManager => { if (!uploadSessionManager) { uploadSessionManager = new UploadSessionManager({ sessionDir: path.join(config.uploadDir, 'upload_sessions'), @@ -194,8 +339,12 @@ const getUploadSessionManager = () => { * @param {string} [code] - Error code for programmatic handling * @param {Object} [details] - Additional error details */ -const createErrorResponse = (message, code = null, details = null) => { - const response = { message }; +const createErrorResponse = ( + message: string, + code: string | null = null, + details: FileErrorDetails | null = null, +): FileErrorResponse => { + const response: FileErrorResponse = { message }; if (code) response.code = code; if (details) response.details = details; return response; @@ -204,16 +353,19 @@ const createErrorResponse = (message, code = null, details = null) => { /** * Get HTTP status code for S3 errors */ -const getS3ErrorStatusCode = (error) => { +const getS3ErrorStatusCode = (error: unknown): number => { return S3StorageProvider.getErrorStatusCode(error); }; /** * Build user-friendly error message based on error type */ -const getErrorMessage = (error, operation = 'process') => { - const errorName = error?.name || ''; - const errorCode = error?.code || ''; +const getErrorMessage = ( + error: unknown, + operation = 'process', +): string => { + const errorName = getErrorStringProperty(error, 'name') || ''; + const errorCode = getErrorStringProperty(error, 'code') || ''; if ( errorName === 'NoSuchKey' || @@ -231,7 +383,7 @@ const getErrorMessage = (error, operation = 'process') => { if (errorCode === 'ECONNRESET' || errorCode === 'ECONNREFUSED') { return 'Connection error while accessing storage'; } - if (error?.name === 'AbortError') { + if (errorName === 'AbortError') { return 'Request was cancelled'; } @@ -247,7 +399,7 @@ const getErrorMessage = (error, operation = 'process') => { * @param {string} urlPath - The path to validate * @returns {boolean} Whether the path is valid */ -const isValidPath = (urlPath) => { +const isValidPath = (urlPath: unknown): urlPath is string => { if (!urlPath || typeof urlPath !== 'string') return false; const trimmed = urlPath.trim(); @@ -270,18 +422,22 @@ const isValidPath = (urlPath) => { // Unified Upload/Download/Delete Interface // ============================================================================ -const uploadFile = async (folder, req, res) => { +const uploadFile = async ( + folder: string, + req: FileUploadServiceRequest, + res: FileServiceResponse, +): Promise => { const provider = getFileStorageProvider(); - const log = req.log || logger; + const log = getRequestLogger(req) || logger; try { - const processFile = require('../middlewares/upload'); await processFile(req, res); if (!req.file) return res .status(400) .send(createErrorResponse('Please upload a file!', 'MISSING_FILE')); + const uploadedFile = req.file; const filename = req.body.filename; if (!filename) @@ -294,26 +450,24 @@ const uploadFile = async (folder, req, res) => { if (provider === 's3') { const s3 = getS3Provider(); - const result = await s3.upload(privateUrl, req.file.buffer, { - contentType: req.file.mimetype, + const result = await s3.upload(privateUrl, uploadedFile.buffer, { + contentType: uploadedFile.mimetype, }); - publicUrl = result.url; + publicUrl = getStorageUploadUrl(result.url); } else if (provider === 'gcloud') { const { bucket, hash } = getGCloudBucket(); const filePath = `${hash}/${privateUrl}`; const blob = bucket.file(filePath); - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { const blobStream = blob.createWriteStream({ resumable: false }); blobStream.on('error', reject); blobStream.on('finish', resolve); - blobStream.end(req.file.buffer); + blobStream.end(uploadedFile.buffer); }); - publicUrl = format( - `https://storage.googleapis.com/${bucket.name}/${blob.name}`, - ); + publicUrl = `https://storage.googleapis.com/${bucket.name}/${blob.name}`; } else { const local = getLocalProvider(); - await local.upload(privateUrl, req.file.buffer); + await local.upload(privateUrl, uploadedFile.buffer); publicUrl = `/api/file/download?privateUrl=${encodeURIComponent(privateUrl)}`; } @@ -329,7 +483,7 @@ const uploadFile = async (folder, req, res) => { .status(500) .send( createErrorResponse( - `Could not upload the file. ${error.message || error}`, + `Could not upload the file. ${getUnknownErrorMessage(error)}`, 'UPLOAD_ERROR', ), ); @@ -342,14 +496,19 @@ const uploadFile = async (folder, req, res) => { * @param {number} totalSize - Total file size * @returns {{start: number, end: number} | null} */ -const parseRangeHeader = (rangeHeader, totalSize) => { +const parseRangeHeader = ( + rangeHeader: string | undefined, + totalSize: number, +): FileByteRange | null => { if (!rangeHeader || !rangeHeader.startsWith('bytes=')) return null; const range = rangeHeader.slice(6); // Remove "bytes=" const parts = range.split('-'); + const rangeStart = parts[0] || ''; + const rangeEnd = parts[1]; - let start = parseInt(parts[0], 10); - let end = parts[1] ? parseInt(parts[1], 10) : totalSize - 1; + let start = parseInt(rangeStart, 10); + let end = rangeEnd ? parseInt(rangeEnd, 10) : totalSize - 1; // Handle suffix ranges (e.g., bytes=-500 means last 500 bytes) if (isNaN(start)) { @@ -368,10 +527,33 @@ const parseRangeHeader = (rangeHeader, totalSize) => { return { start, end }; }; -const downloadFile = async (req, res) => { +const getSingleQueryValue = (value: unknown): string | undefined => { + return typeof value === 'string' ? value : undefined; +}; + +const sendStorageBody = async ( + body: unknown, + res: FileServiceResponse, +): Promise => { + if (isReadableStream(body)) { + return body.pipe(res); + } + + if (hasByteArrayTransformer(body)) { + const bytes = await body.transformToByteArray(); + return res.send(Buffer.from(bytes)); + } + + return res.send(body); +}; + +const downloadFile = async ( + req: FileServiceRequest, + res: FileServiceResponse, +): Promise => { const provider = getFileStorageProvider(); - const privateUrl = req.query.privateUrl; - const log = req.log || logger; + const privateUrl = getSingleQueryValue(req.query.privateUrl); + const log = getRequestLogger(req) || logger; if (!privateUrl) return res @@ -429,22 +611,8 @@ const downloadFile = async (req, res) => { // Determine content type from extension const ext = path.extname(privateUrl).toLowerCase(); - const mimeTypes = { - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.png': 'image/png', - '.gif': 'image/gif', - '.webp': 'image/webp', - '.svg': 'image/svg+xml', - '.ico': 'image/x-icon', - '.mp4': 'video/mp4', - '.webm': 'video/webm', - '.mp3': 'audio/mpeg', - '.wav': 'audio/wav', - '.pdf': 'application/pdf', - }; - if (mimeTypes[ext]) { - res.setHeader('Content-Type', mimeTypes[ext]); + if (MIME_TYPES[ext]) { + res.setHeader('Content-Type', MIME_TYPES[ext]); } // Handle Range requests for cached files @@ -534,16 +702,7 @@ const downloadFile = async (req, res) => { `public, max-age=${config.s3CacheMaxAge}`, ); - if (typeof rangeResult.body.pipe === 'function') { - return rangeResult.body.pipe(res); - } else if ( - typeof rangeResult.body.transformToByteArray === 'function' - ) { - const bytes = await rangeResult.body.transformToByteArray(); - return res.send(Buffer.from(bytes)); - } else { - return res.send(rangeResult.body); - } + return sendStorageBody(rangeResult.body, res); } // Download from S3 (full file) @@ -556,7 +715,7 @@ const downloadFile = async (req, res) => { // Add caching headers for browser res.setHeader('Cache-Control', `public, max-age=${config.s3CacheMaxAge}`); - if (useCache && typeof result.body.pipe === 'function') { + if (useCache && isReadableStream(result.body)) { // Stream to both response and cache file using atomic writes await ensureCacheDir(); const tempPath = cachePath + '.tmp'; @@ -567,7 +726,6 @@ const downloadFile = async (req, res) => { const cacheStream = fs.createWriteStream(tempPath); // Use pipeline for proper error handling - const { PassThrough } = require('stream'); const passThrough = new PassThrough(); result.body.pipe(passThrough); @@ -576,45 +734,49 @@ const downloadFile = async (req, res) => { // Track bytes written to verify complete download let bytesWritten = 0; - passThrough.on('data', (chunk) => { + passThrough.on('data', (chunk: Buffer) => { bytesWritten += chunk.length; }); - cacheStream.on('finish', async () => { - try { - // Verify we got the expected size - const expectedSize = result.contentLength; - if (expectedSize && bytesWritten !== expectedSize) { - log.warn( - { cachePath, bytesWritten, expectedSize }, - 'Cache file size mismatch, discarding', - ); - await fs.promises.unlink(tempPath).catch(() => {}); - } else { - // Atomic rename: temp → final - await fs.promises.rename(tempPath, cachePath); - log.debug( - { cachePath, bytesWritten }, - 'Cache file written successfully', - ); + cacheStream.on('finish', () => { + void (async () => { + try { + // Verify we got the expected size + const expectedSize = result.contentLength; + if (expectedSize && bytesWritten !== expectedSize) { + log.warn( + { cachePath, bytesWritten, expectedSize }, + 'Cache file size mismatch, discarding', + ); + await fs.promises.unlink(tempPath).catch(() => {}); + } else { + // Atomic rename: temp → final + await fs.promises.rename(tempPath, cachePath); + log.debug( + { cachePath, bytesWritten }, + 'Cache file written successfully', + ); + } + } catch (err) { + log.warn({ err, cachePath }, 'Failed to finalize cache file'); + } finally { + // Remove download marker + await fs.promises.unlink(downloadingPath).catch(() => {}); } - } catch (err) { - log.warn({ err, cachePath }, 'Failed to finalize cache file'); - } finally { - // Remove download marker - await fs.promises.unlink(downloadingPath).catch(() => {}); - } + })(); }); - cacheStream.on('error', async (err) => { + cacheStream.on('error', (err) => { log.warn({ err, cachePath }, 'Failed to write to cache'); - // Cleanup temp and marker files - await fs.promises.unlink(tempPath).catch(() => {}); - await fs.promises.unlink(downloadingPath).catch(() => {}); + void (async () => { + // Cleanup temp and marker files + await fs.promises.unlink(tempPath).catch(() => {}); + await fs.promises.unlink(downloadingPath).catch(() => {}); + })(); }); - } else if (typeof result.body.pipe === 'function') { + } else if (isReadableStream(result.body)) { result.body.pipe(res); - } else if (typeof result.body.transformToByteArray === 'function') { + } else if (hasByteArrayTransformer(result.body)) { const bytes = await result.body.transformToByteArray(); const buffer = Buffer.from(bytes); @@ -671,22 +833,8 @@ const downloadFile = async (req, res) => { // Determine content type from extension const ext = path.extname(privateUrl).toLowerCase(); - const mimeTypes = { - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.png': 'image/png', - '.gif': 'image/gif', - '.webp': 'image/webp', - '.svg': 'image/svg+xml', - '.ico': 'image/x-icon', - '.mp4': 'video/mp4', - '.webm': 'video/webm', - '.mp3': 'audio/mpeg', - '.wav': 'audio/wav', - '.pdf': 'application/pdf', - }; - if (mimeTypes[ext]) { - res.setHeader('Content-Type', mimeTypes[ext]); + if (MIME_TYPES[ext]) { + res.setHeader('Content-Type', MIME_TYPES[ext]); } // Handle Range requests @@ -714,7 +862,10 @@ const downloadFile = async (req, res) => { } } catch (error) { // Don't log abort errors as they're expected when client disconnects - if (error.name === 'AbortError') { + const errorName = getErrorStringProperty(error, 'name'); + const errorCode = getErrorStringProperty(error, 'code'); + + if (errorName === 'AbortError') { log.debug({ privateUrl }, 'Download aborted by client'); if (!res.headersSent) { return res.status(499).end(); // Client Closed Request @@ -731,8 +882,8 @@ const downloadFile = async (req, res) => { provider, privateUrl, statusCode, - errorName: error?.name, - errorCode: error?.code, + errorName, + errorCode, }, 'Failed to download file', ); @@ -741,10 +892,12 @@ const downloadFile = async (req, res) => { return res .status(statusCode) .send( - createErrorResponse(errorMessage, error?.name || 'DOWNLOAD_ERROR'), + createErrorResponse(errorMessage, errorName || 'DOWNLOAD_ERROR'), ); } } + + return undefined; }; /** @@ -754,7 +907,10 @@ const downloadFile = async (req, res) => { * @param {boolean} [options.throwOnError=false] - Whether to throw errors instead of swallowing them * @returns {Promise<{ success: boolean, error?: Error }>} */ -const deleteFile = async (privateUrl, options = {}) => { +const deleteFile = async ( + privateUrl: string, + options: FileDeleteOptions = {}, +): Promise => { if (!privateUrl) return { success: false, error: new Error('Missing privateUrl') }; @@ -784,7 +940,7 @@ const deleteFile = async (privateUrl, options = {}) => { throw error; } - return { success: false, error }; + return { success: false, error: toError(error) }; } }; @@ -797,7 +953,7 @@ const deleteFile = async (privateUrl, options = {}) => { * @param {string} privateUrl - Storage key/path * @returns {Promise} */ -const downloadToBuffer = async (privateUrl) => { +const downloadToBuffer = async (privateUrl: string): Promise => { const provider = getFileStorageProvider(); if (provider === 's3') { @@ -805,14 +961,18 @@ const downloadToBuffer = async (privateUrl) => { const result = await s3.download(privateUrl); // Convert stream to buffer - if (typeof result.body.transformToByteArray === 'function') { + if (hasByteArrayTransformer(result.body)) { const bytes = await result.body.transformToByteArray(); return Buffer.from(bytes); } // Handle readable stream - const chunks = []; + if (!isReadableStream(result.body)) { + return Buffer.from([]); + } + + const chunks: Buffer[] = []; for await (const chunk of result.body) { - chunks.push(chunk); + chunks.push(toBufferChunk(chunk)); } return Buffer.concat(chunks); } else if (provider === 'gcloud') { @@ -833,7 +993,9 @@ const downloadToBuffer = async (privateUrl) => { * @param {string} privateUrl - Storage key/path * @returns {Promise<{filePath: string, cleanup: () => Promise}>} */ -const downloadToTempFile = async (privateUrl) => { +const downloadToTempFile = async ( + privateUrl: string, +): Promise => { const provider = getFileStorageProvider(); const tempDir = await fs.promises.mkdtemp( path.join(os.tmpdir(), 'asset-probe-'), @@ -849,10 +1011,17 @@ const downloadToTempFile = async (privateUrl) => { if (provider === 's3') { const s3 = getS3Provider(); const result = await s3.download(privateUrl); - if (!result?.body) { + if (!result.body) { throw new Error(`Empty S3 response body for ${privateUrl}`); } - await pipeline(result.body, fs.createWriteStream(filePath)); + if (isReadableStream(result.body)) { + await pipeline(result.body, fs.createWriteStream(filePath)); + } else if (hasByteArrayTransformer(result.body)) { + const bytes = await result.body.transformToByteArray(); + await fs.promises.writeFile(filePath, Buffer.from(bytes)); + } else { + throw new Error(`Unsupported S3 response body for ${privateUrl}`); + } } else if (provider === 'gcloud') { const { bucket, hash } = getGCloudBucket(); const file = bucket.file(`${hash}/${privateUrl}`); @@ -877,19 +1046,23 @@ const downloadToTempFile = async (privateUrl) => { * @param {string} [options.contentType] - MIME type * @returns {Promise<{ url: string }>} */ -const uploadBuffer = async (privateUrl, buffer, options = {}) => { +const uploadBuffer = async ( + privateUrl: string, + buffer: Buffer, + options: FileUploadBufferOptions = {}, +): Promise => { const provider = getFileStorageProvider(); const { contentType = 'application/octet-stream' } = options; if (provider === 's3') { const s3 = getS3Provider(); const result = await s3.upload(privateUrl, buffer, { contentType }); - return { url: result.url }; + return { url: getStorageUploadUrl(result.url) }; } else if (provider === 'gcloud') { const { bucket, hash } = getGCloudBucket(); const filePath = `${hash}/${privateUrl}`; const blob = bucket.file(filePath); - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { const blobStream = blob.createWriteStream({ resumable: false }); blobStream.on('error', reject); blobStream.on('finish', resolve); @@ -911,116 +1084,132 @@ const uploadBuffer = async (privateUrl, buffer, options = {}) => { // Chunked Upload Session Management // ============================================================================ -const sanitizeFolder = (folder) => { - const value = String(folder || '') +const sanitizeFolder = (folder: unknown): string | null => { + const value = getSanitizedStringInput(folder) .trim() .replace(/^\/+|\/+$/g, ''); return !value || value.includes('..') ? null : value; }; -const sanitizeFilename = (filename) => { - const value = path.basename(String(filename || '').trim()); +const sanitizeFilename = (filename: unknown): string | null => { + const value = path.basename(getSanitizedStringInput(filename).trim()); return !value || value === '.' || value === '..' ? null : value; }; -const initUploadSession = async (req, res) => { - const log = req.log || logger; +const initUploadSession = ( + req: UploadSessionInitRequest, + res: FileServiceResponse, +): Promise => + Promise.resolve().then(() => { + const log = getRequestLogger(req) || logger; - try { - if (!req.currentUser?.id) return res.sendStatus(403); + try { + const currentUser = getCurrentUser(req); + if (!currentUser?.id) return res.sendStatus(403); - const sessionManager = getUploadSessionManager(); - sessionManager.cleanupExpiredSessions(); + const sessionManager = getUploadSessionManager(); + sessionManager.cleanupExpiredSessions(); - const folder = sanitizeFolder(req.body?.folder); - const filename = sanitizeFilename(req.body?.filename); - const totalChunks = Number(req.body?.totalChunks); - const size = Number(req.body?.size); - const contentType = String(req.body?.contentType || '').trim(); + const folder = sanitizeFolder(req.body?.folder); + const filename = sanitizeFilename(req.body?.filename); + const totalChunks = Number(req.body?.totalChunks); + const size = Number(req.body?.size); + const contentType = getSanitizedStringInput( + req.body?.contentType, + ).trim(); - if (!folder || !filename) + if (!folder || !filename) + return res + .status(400) + .send( + createErrorResponse('Invalid folder or filename', 'INVALID_INPUT'), + ); + if (!Number.isInteger(totalChunks) || totalChunks <= 0) + return res + .status(400) + .send(createErrorResponse('Invalid totalChunks', 'INVALID_INPUT')); + if (!Number.isFinite(size) || size < 0) + return res + .status(400) + .send(createErrorResponse('Invalid file size', 'INVALID_INPUT')); + + const sessionId = sessionManager.createSession({ + userId: currentUser.id, + folder, + filename, + totalChunks, + totalSize: size, + contentType, + }); + + return res.status(200).send({ + sessionId, + uploadedChunks: [], + totalChunks, + }); + } catch (error) { + log.error({ err: error }, 'Failed to initialize upload session'); return res - .status(400) + .status(500) .send( - createErrorResponse('Invalid folder or filename', 'INVALID_INPUT'), + createErrorResponse( + 'Failed to initialize upload session', + 'SESSION_INIT_ERROR', + ), ); - if (!Number.isInteger(totalChunks) || totalChunks <= 0) + } + }); + +const getUploadSession = ( + req: UploadSessionRequest, + res: FileServiceResponse, +): Promise => + Promise.resolve().then(() => { + try { + const currentUser = getCurrentUser(req); + if (!currentUser?.id) return res.sendStatus(403); + + const sessionId = String(req.params.sessionId || ''); + const sessionManager = getUploadSessionManager(); + const session = sessionManager.readMeta(sessionId); + + if (!session) + return res + .status(404) + .send( + createErrorResponse('Upload session not found', 'SESSION_NOT_FOUND'), + ); + if (session.userId !== currentUser.id) return res.sendStatus(403); + + return res.status(200).send({ + sessionId: session.sessionId, + totalChunks: session.totalChunks, + uploadedChunks: Object.keys(session.uploadedChunks || {}).map(Number), + status: sessionManager.isComplete(sessionId) ? 'complete' : 'uploading', + }); + } catch (error) { + const log = getRequestLogger(req) || logger; + log.error({ err: error }, 'Failed to get upload session'); return res - .status(400) - .send(createErrorResponse('Invalid totalChunks', 'INVALID_INPUT')); - if (!Number.isFinite(size) || size < 0) - return res - .status(400) - .send(createErrorResponse('Invalid file size', 'INVALID_INPUT')); - - const sessionId = sessionManager.createSession({ - userId: req.currentUser.id, - folder, - filename, - totalChunks, - totalSize: size, - contentType, - }); - - return res.status(200).send({ - sessionId, - uploadedChunks: [], - totalChunks, - }); - } catch (error) { - log.error({ err: error }, 'Failed to initialize upload session'); - return res - .status(500) - .send( - createErrorResponse( - 'Failed to initialize upload session', - 'SESSION_INIT_ERROR', - ), - ); - } -}; - -const getUploadSession = async (req, res) => { - try { - if (!req.currentUser?.id) return res.sendStatus(403); - - const sessionId = String(req.params.sessionId || ''); - const sessionManager = getUploadSessionManager(); - const session = sessionManager.readMeta(sessionId); - - if (!session) - return res - .status(404) + .status(500) .send( - createErrorResponse('Upload session not found', 'SESSION_NOT_FOUND'), + createErrorResponse( + 'Failed to get upload session', + 'SESSION_GET_ERROR', + ), ); - if (session.userId !== req.currentUser.id) return res.sendStatus(403); + } + }); - return res.status(200).send({ - sessionId: session.sessionId, - totalChunks: session.totalChunks, - uploadedChunks: Object.keys(session.uploadedChunks || {}).map(Number), - status: sessionManager.isComplete(sessionId) ? 'complete' : 'uploading', - }); - } catch (error) { - const log = req.log || logger; - log.error({ err: error }, 'Failed to get upload session'); - return res - .status(500) - .send( - createErrorResponse( - 'Failed to get upload session', - 'SESSION_GET_ERROR', - ), - ); - } -}; - -const uploadChunk = async (req, res) => { - const log = req.log || logger; +const uploadChunk = async ( + req: UploadChunkRequest, + res: FileServiceResponse, +): Promise => { + const log = getRequestLogger(req) || logger; try { - if (!req.currentUser?.id) return res.sendStatus(403); + const currentUser = getCurrentUser(req); + if (!currentUser?.id) return res.sendStatus(403); const sessionId = String(req.params.sessionId || ''); const chunkIndex = Number(req.params.chunkIndex); @@ -1040,7 +1229,7 @@ const uploadChunk = async (req, res) => { .send( createErrorResponse('Upload session not found', 'SESSION_NOT_FOUND'), ); - if (session.userId !== req.currentUser.id) return res.sendStatus(403); + if (session.userId !== currentUser.id) return res.sendStatus(403); if (chunkIndex >= session.totalChunks) return res .status(400) @@ -1049,12 +1238,15 @@ const uploadChunk = async (req, res) => { ); // Collect chunk data - const chunks = []; - for await (const chunk of req) chunks.push(chunk); + const chunks: Buffer[] = []; + for await (const chunk of req) chunks.push(toBufferChunk(chunk)); const chunkBuffer = Buffer.concat(chunks); await sessionManager.saveChunk(sessionId, chunkIndex, chunkBuffer); const updatedSession = sessionManager.readMeta(sessionId); + if (!updatedSession) { + throw new Error(`Upload session disappeared after chunk save: ${sessionId}`); + } return res.status(200).send({ sessionId, @@ -1072,11 +1264,15 @@ const uploadChunk = async (req, res) => { } }; -const finalizeUploadSession = async (req, res) => { - const log = req.log || logger; +const finalizeUploadSession = async ( + req: UploadSessionRequest, + res: FileServiceResponse, +): Promise => { + const log = getRequestLogger(req) || logger; try { - if (!req.currentUser?.id) return res.sendStatus(403); + const currentUser = getCurrentUser(req); + if (!currentUser?.id) return res.sendStatus(403); const sessionId = String(req.params.sessionId || ''); const sessionManager = getUploadSessionManager(); @@ -1088,7 +1284,7 @@ const finalizeUploadSession = async (req, res) => { .send( createErrorResponse('Upload session not found', 'SESSION_NOT_FOUND'), ); - if (session.userId !== req.currentUser.id) return res.sendStatus(403); + if (session.userId !== currentUser.id) return res.sendStatus(403); // Verify all chunks exist for (let i = 0; i < session.totalChunks; i++) { @@ -1117,10 +1313,11 @@ const finalizeUploadSession = async (req, res) => { const s3 = getS3Provider(); const data = fs.readFileSync(assembledPath); - const result = await s3.upload(privateUrl, data, { - contentType: session.contentType, - }); - publicUrl = result.url; + const uploadOptions = session.contentType + ? { contentType: session.contentType } + : {}; + const result = await s3.upload(privateUrl, data, uploadOptions); + publicUrl = getStorageUploadUrl(result.url); } else if (provider === 'gcloud') { const { bucket, hash } = getGCloudBucket(); const blob = bucket.file(`${hash}/${privateUrl}`); @@ -1128,9 +1325,7 @@ const finalizeUploadSession = async (req, res) => { fs.createReadStream(assembledPath), blob.createWriteStream({ resumable: false }), ); - publicUrl = format( - `https://storage.googleapis.com/${bucket.name}/${blob.name}`, - ); + publicUrl = `https://storage.googleapis.com/${bucket.name}/${blob.name}`; } else { const local = getLocalProvider(); const data = fs.readFileSync(assembledPath); @@ -1174,25 +1369,9 @@ const finalizeUploadSession = async (req, res) => { * @param {string} filepath - File path or storage key * @returns {string} MIME type or default 'application/octet-stream' */ -const getMimeTypeFromExtension = (filepath) => { +const getMimeTypeFromExtension = (filepath: string): string => { const ext = path.extname(filepath).toLowerCase(); - const mimeTypes = { - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.png': 'image/png', - '.gif': 'image/gif', - '.webp': 'image/webp', - '.svg': 'image/svg+xml', - '.ico': 'image/x-icon', - '.mp4': 'video/mp4', - '.webm': 'video/webm', - '.mp3': 'audio/mpeg', - '.wav': 'audio/wav', - '.ogg': 'audio/ogg', - '.pdf': 'application/pdf', - '.json': 'application/json', - }; - return mimeTypes[ext] || 'application/octet-stream'; + return MIME_TYPES[ext] || 'application/octet-stream'; }; /** @@ -1207,7 +1386,11 @@ const getMimeTypeFromExtension = (filepath) => { * @param {string} [options.contentType] - MIME type (auto-detected if not provided) * @returns {Promise<{ url: string } | { key: string }>} */ -const copyFile = async (sourceKey, destKey, options = {}) => { +const copyFile = async ( + sourceKey: string, + destKey: string, + options: FileCopyOptions = {}, +): Promise => { const provider = getFileStorageProvider(); const contentType = options.contentType || getMimeTypeFromExtension(sourceKey); @@ -1219,7 +1402,7 @@ const copyFile = async (sourceKey, destKey, options = {}) => { { sourceKey, destKey, provider: 's3' }, 'File copied (server-side)', ); - return { url: result.url }; + return { url: getStorageUploadUrl(result.url) }; } if (provider === 'local') { @@ -1241,7 +1424,7 @@ const copyFile = async (sourceKey, destKey, options = {}) => { return uploadBuffer(destKey, buffer, { contentType }); } - throw new Error(`Unknown storage provider: ${provider}`); + throw new Error('Unknown storage provider'); }; /** @@ -1252,32 +1435,39 @@ const copyFile = async (sourceKey, destKey, options = {}) => { * @param {boolean} [options.continueOnError=true] - Continue if individual copy fails * @returns {Promise<{succeeded: Array, failed: Array<{sourceKey: string, error: string}>}>} */ -const copyFilesParallel = async (copies, options = {}) => { +const copyFilesParallel = async ( + copies: FileCopyOperation[], + options: FileCopyParallelOptions = {}, +): Promise => { const { concurrency = 10, continueOnError = true } = options; - const succeeded = []; - const failed = []; + const succeeded: FileCopySuccess[] = []; + const failed: FileCopyFailure[] = []; // Process in chunks for concurrency control for (let i = 0; i < copies.length; i += concurrency) { const chunk = copies.slice(i, i + concurrency); const results = await Promise.allSettled( - chunk.map(({ sourceKey, destKey, contentType }) => - copyFile(sourceKey, destKey, { contentType }).then((result) => ({ + chunk.map(({ sourceKey, destKey, contentType }) => { + const copyOptions = contentType ? { contentType } : {}; + return copyFile(sourceKey, destKey, copyOptions).then((result) => ({ sourceKey, destKey, result, - })), - ), + })); + }), ); for (let j = 0; j < results.length; j++) { const result = results[j]; const copy = chunk[j]; + if (!result || !copy) { + continue; + } if (result.status === 'fulfilled') { succeeded.push({ sourceKey: copy.sourceKey, destKey: copy.destKey }); } else { - const errorMsg = result.reason?.message || 'Unknown error'; + const errorMsg = getUnknownErrorMessage(result.reason); failed.push({ sourceKey: copy.sourceKey, destKey: copy.destKey, @@ -1312,20 +1502,23 @@ const copyFilesParallel = async (copies, options = {}) => { // Presigned URLs // ============================================================================ -const getPresignExpirySeconds = () => config.s3.presignExpirySeconds || 3600; +const getPresignExpirySeconds = (): number => + config.s3.presignExpirySeconds || 3600; -const generatePresignedUrls = async (urls) => { +const generatePresignedUrls = async ( + urls: readonly string[], +): Promise> => { const provider = getFileStorageProvider(); if (provider !== 's3') { - return urls.reduce((acc, url) => { + return urls.reduce>((acc, url) => { acc[url] = `/api/file/download?privateUrl=${encodeURIComponent(url)}`; return acc; }, {}); } const s3 = getS3Provider(); - const presignedUrls = {}; + const presignedUrls: Record = {}; const expirySeconds = getPresignExpirySeconds(); await Promise.all( @@ -1341,7 +1534,7 @@ const generatePresignedUrls = async (urls) => { // Exports // ============================================================================ -module.exports = { +const FileService: FileServiceFacade = { // Provider detection getFileStorageProvider, getS3Provider, @@ -1371,3 +1564,29 @@ module.exports = { createErrorResponse, getS3ErrorStatusCode, }; + +export { + createErrorResponse, + deleteFile, + downloadFile, + downloadToBuffer, + downloadToTempFile, + generatePresignedUrls, + getFileStorageProvider, + getGCloudBucket, + getLocalProvider, + getMimeTypeFromExtension, + getS3ErrorStatusCode, + getS3Provider, + isValidPath, + uploadBuffer, + uploadFile, + copyFile, + copyFilesParallel, + finalizeUploadSession, + getUploadSession, + initUploadSession, + uploadChunk, +}; + +export default FileService; diff --git a/backend/src/services/file/BaseStorageProvider.js b/backend/src/services/file/BaseStorageProvider.js deleted file mode 100644 index d45c36a..0000000 --- a/backend/src/services/file/BaseStorageProvider.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * BaseStorageProvider - * - * Abstract base class for storage providers (Strategy Pattern). - * Subclasses implement specific storage backends (S3, GCloud, Local). - */ - -class BaseStorageProvider { - /** - * Provider name for identification - * @returns {string} - */ - static get providerName() { - throw new Error('providerName must be defined in subclass'); - } - - /** - * Upload a file to storage - * @param {string} _key - Storage key/path - * @param {Buffer|ReadableStream} _data - File data - * @param {Object} _options - Upload options - * @param {string} [_options.contentType] - MIME type - * @param {Object} [_options.metadata] - Additional metadata - * @returns {Promise<{ key: string, url?: string }>} - */ - async upload(_key, _data, _options) { - throw new Error('upload must be implemented in subclass'); - } - - /** - * Download a file from storage - * @param {string} _key - Storage key/path - * @returns {Promise<{ body: ReadableStream, contentType?: string }>} - */ - async download(_key) { - throw new Error('download must be implemented in subclass'); - } - - /** - * Delete a file from storage - * @param {string} _key - Storage key/path - * @returns {Promise} - */ - async delete(_key) { - throw new Error('delete must be implemented in subclass'); - } - - /** - * Check if a file exists - * @param {string} _key - Storage key/path - * @returns {Promise} - */ - async exists(_key) { - throw new Error('exists must be implemented in subclass'); - } - - /** - * List files with a given prefix - * @param {string} _prefix - Key prefix - * @returns {Promise} Array of keys - */ - async list(_prefix) { - throw new Error('list must be implemented in subclass'); - } - - /** - * Get a signed URL for direct access (if supported) - * @param {string} _key - Storage key/path - * @param {number} _expiresIn - Expiration time in seconds - * @returns {Promise} Signed URL or null if not supported - */ - async getSignedUrl(_key, _expiresIn) { - return null; - } - - /** - * Delete multiple files - * @param {string[]} keys - Array of keys to delete - * @returns {Promise} - */ - async deleteMany(keys) { - for (const key of keys) { - await this.delete(key); - } - } -} - -module.exports = BaseStorageProvider; diff --git a/backend/src/services/file/BaseStorageProvider.ts b/backend/src/services/file/BaseStorageProvider.ts new file mode 100644 index 0000000..cc5226c --- /dev/null +++ b/backend/src/services/file/BaseStorageProvider.ts @@ -0,0 +1,47 @@ +import type { + StorageDownloadResult, + StorageDeleteManyResult, + StorageUploadData, + StorageUploadOptions, + StorageUploadResult, +} from '../../types/index.ts'; + +export default class BaseStorageProvider { + static get providerName(): string { + throw new Error('providerName must be defined in subclass'); + } + + upload( + _key: string, + _data: StorageUploadData, + _options: StorageUploadOptions, + ): Promise { + throw new Error('upload must be implemented in subclass'); + } + + download(_key: string): Promise { + throw new Error('download must be implemented in subclass'); + } + + delete(_key: string): Promise { + throw new Error('delete must be implemented in subclass'); + } + + exists(_key: string): Promise { + throw new Error('exists must be implemented in subclass'); + } + + list(_prefix: string): Promise { + throw new Error('list must be implemented in subclass'); + } + + getSignedUrl(_key: string, _expiresIn: number): Promise { + return Promise.resolve(null); + } + + async deleteMany(keys: string[]): Promise { + for (const key of keys) { + await this.delete(key); + } + } +} diff --git a/backend/src/services/file/LocalStorageProvider.js b/backend/src/services/file/LocalStorageProvider.js deleted file mode 100644 index b5b1485..0000000 --- a/backend/src/services/file/LocalStorageProvider.js +++ /dev/null @@ -1,250 +0,0 @@ -/** - * LocalStorageProvider - * - * Local filesystem storage implementation following the Strategy Pattern. - * Implements BaseStorageProvider interface for local disk operations. - */ - -const fs = require('fs'); -const path = require('path'); -const { pipeline } = require('stream/promises'); -const BaseStorageProvider = require('./BaseStorageProvider'); - -/** - * Ensure directory exists for a file path - */ -const ensureDirectoryExistence = (filePath) => { - const dirname = path.dirname(filePath); - - if (fs.existsSync(dirname)) { - return true; - } - - ensureDirectoryExistence(dirname); - fs.mkdirSync(dirname); -}; - -class LocalStorageProvider extends BaseStorageProvider { - /** - * @param {Object} options - * @param {string} options.basePath - Base directory for file storage - */ - constructor(options = {}) { - super(); - this.basePath = options.basePath || './uploads'; - - // Ensure base path exists - if (!fs.existsSync(this.basePath)) { - fs.mkdirSync(this.basePath, { recursive: true }); - } - } - - static get providerName() { - return 'local'; - } - - /** - * Build full file path - */ - buildPath(key) { - const cleanKey = (key || '').replace(/^\/+/, ''); - return path.join(this.basePath, cleanKey); - } - - /** - * Upload a file to local storage - * @param {string} key - Storage key/path - * @param {Buffer|ReadableStream} data - File data - * @param {Object} _options - Upload options (not used for local) - * @returns {Promise<{ key: string }>} - */ - async upload(key, data, _options = {}) { - const filePath = this.buildPath(key); - ensureDirectoryExistence(filePath); - - if (Buffer.isBuffer(data)) { - fs.writeFileSync(filePath, data); - } else if (data && typeof data.pipe === 'function') { - // Handle stream - const writeStream = fs.createWriteStream(filePath); - await pipeline(data, writeStream); - } else { - throw new Error('Data must be a Buffer or ReadableStream'); - } - - return { key }; - } - - /** - * Download a file from local storage - * @param {string} key - Storage key/path - * @returns {Promise<{ body: ReadableStream, contentType?: string }>} - */ - async download(key) { - const filePath = this.buildPath(key); - - if (!fs.existsSync(filePath)) { - throw new Error(`File not found: ${key}`); - } - - const body = fs.createReadStream(filePath); - - // Try to determine content type from extension - const ext = path.extname(filePath).toLowerCase(); - const contentType = this.getContentType(ext); - - return { body, contentType }; - } - - /** - * Delete a file from local storage - * @param {string} key - Storage key/path - * @returns {Promise} - */ - async delete(key) { - const filePath = this.buildPath(key); - - if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); - } - } - - /** - * Delete multiple files from local storage - * @param {string[]} keys - Array of keys to delete - * @returns {Promise} - */ - async deleteMany(keys) { - for (const key of keys) { - await this.delete(key); - } - } - - /** - * Check if a file exists in local storage - * @param {string} key - Storage key/path - * @returns {Promise} - */ - async exists(key) { - const filePath = this.buildPath(key); - return fs.existsSync(filePath); - } - - /** - * List files with a given prefix - * @param {string} prefix - Key prefix (directory path) - * @returns {Promise} Array of keys - */ - async list(prefix) { - const dirPath = this.buildPath(prefix); - - if (!fs.existsSync(dirPath)) { - return []; - } - - const stat = fs.statSync(dirPath); - if (!stat.isDirectory()) { - return [prefix]; - } - - const files = []; - const readDir = (dir, relativePath = '') => { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - - for (const entry of entries) { - const entryPath = path.join(relativePath, entry.name); - const fullPath = path.join(dir, entry.name); - - if (entry.isDirectory()) { - readDir(fullPath, entryPath); - } else { - files.push(path.join(prefix, entryPath)); - } - } - }; - - readDir(dirPath); - return files; - } - - /** - * Copy a file within local storage - * Uses fs.promises.copyFile for efficient filesystem copying. - * - * @param {string} sourceKey - Source storage key/path - * @param {string} destinationKey - Destination storage key/path - * @param {Object} [_options] - Copy options (unused, for interface consistency) - * @returns {Promise<{ key: string }>} - */ - async copy(sourceKey, destinationKey, _options = {}) { - const sourcePath = this.buildPath(sourceKey); - const destPath = this.buildPath(destinationKey); - - // Check source exists before copying - if (!fs.existsSync(sourcePath)) { - const error = new Error(`Source file not found: ${sourceKey}`); - error.code = 'ENOENT'; - throw error; - } - - // Ensure destination directory exists (using existing helper) - ensureDirectoryExistence(destPath); - - // Use async copyFile for non-blocking operation - await fs.promises.copyFile(sourcePath, destPath); - - return { key: destinationKey }; - } - - /** - * Get a signed URL for direct access (not supported for local storage) - * For local storage, return the file path that can be served by express.static - * @param {string} key - Storage key/path - * @param {number} _expiresIn - Expiration time (ignored for local) - * @returns {Promise} - */ - async getSignedUrl(key, _expiresIn) { - // Local storage doesn't support signed URLs - // Return the relative path that can be served by a static file server - return `/uploads/${key}`; - } - - /** - * Get base path - * @returns {string} - */ - getBasePath() { - return this.basePath; - } - - /** - * Get content type from file extension - * @param {string} ext - File extension - * @returns {string} - */ - getContentType(ext) { - const types = { - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.png': 'image/png', - '.gif': 'image/gif', - '.webp': 'image/webp', - '.svg': 'image/svg+xml', - '.mp4': 'video/mp4', - '.webm': 'video/webm', - '.mp3': 'audio/mpeg', - '.wav': 'audio/wav', - '.ogg': 'audio/ogg', - '.pdf': 'application/pdf', - '.json': 'application/json', - '.txt': 'text/plain', - '.html': 'text/html', - '.css': 'text/css', - '.js': 'application/javascript', - }; - - return types[ext] || 'application/octet-stream'; - } -} - -module.exports = LocalStorageProvider; diff --git a/backend/src/services/file/LocalStorageProvider.ts b/backend/src/services/file/LocalStorageProvider.ts new file mode 100644 index 0000000..f8cb9c3 --- /dev/null +++ b/backend/src/services/file/LocalStorageProvider.ts @@ -0,0 +1,195 @@ +/** + * LocalStorageProvider + * + * Local filesystem storage implementation following the Strategy Pattern. + * Implements BaseStorageProvider interface for local disk operations. + */ + +import fs from 'fs'; +import path from 'path'; +import { pipeline } from 'stream/promises'; +import BaseStorageProvider from './BaseStorageProvider.ts'; +import type { + LocalStorageProviderOptions, + StorageContentTypeMap, + StorageDownloadResult, + StorageUploadData, + StorageUploadOptions, + StorageUploadResult, +} from '../../types/index.ts'; + +const DEFAULT_BASE_PATH = './uploads'; + +const CONTENT_TYPES: StorageContentTypeMap = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.svg': 'image/svg+xml', + '.mp4': 'video/mp4', + '.webm': 'video/webm', + '.mp3': 'audio/mpeg', + '.wav': 'audio/wav', + '.ogg': 'audio/ogg', + '.pdf': 'application/pdf', + '.json': 'application/json', + '.txt': 'text/plain', + '.html': 'text/html', + '.css': 'text/css', + '.js': 'application/javascript', +}; + +const ensureDirectoryExistence = (filePath: string): void => { + const dirname = path.dirname(filePath); + + if (fs.existsSync(dirname)) { + return; + } + + ensureDirectoryExistence(dirname); + fs.mkdirSync(dirname); +}; + +export default class LocalStorageProvider extends BaseStorageProvider { + private readonly basePath: string; + + constructor(options: LocalStorageProviderOptions = {}) { + super(); + this.basePath = options.basePath || DEFAULT_BASE_PATH; + + if (!fs.existsSync(this.basePath)) { + fs.mkdirSync(this.basePath, { recursive: true }); + } + } + + static override get providerName(): string { + return 'local'; + } + + buildPath(key: string): string { + const cleanKey = key.replace(/^\/+/, ''); + return path.join(this.basePath, cleanKey); + } + + override async upload( + key: string, + data: StorageUploadData, + _options: StorageUploadOptions = {}, + ): Promise { + const filePath = this.buildPath(key); + ensureDirectoryExistence(filePath); + + if (Buffer.isBuffer(data)) { + fs.writeFileSync(filePath, data); + } else { + const writeStream = fs.createWriteStream(filePath); + await pipeline(data, writeStream); + } + + return { key }; + } + + override download(key: string): Promise { + const filePath = this.buildPath(key); + + if (!fs.existsSync(filePath)) { + throw new Error(`File not found: ${key}`); + } + + const body = fs.createReadStream(filePath); + const ext = path.extname(filePath).toLowerCase(); + const contentType = this.getContentType(ext); + + return Promise.resolve({ body, contentType }); + } + + override delete(key: string): Promise { + const filePath = this.buildPath(key); + + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + + return Promise.resolve(); + } + + override async deleteMany(keys: string[]): Promise { + for (const key of keys) { + await this.delete(key); + } + } + + override exists(key: string): Promise { + const filePath = this.buildPath(key); + return Promise.resolve(fs.existsSync(filePath)); + } + + override list(prefix: string): Promise { + const dirPath = this.buildPath(prefix); + + if (!fs.existsSync(dirPath)) { + return Promise.resolve([]); + } + + const stat = fs.statSync(dirPath); + if (!stat.isDirectory()) { + return Promise.resolve([prefix]); + } + + const files: string[] = []; + + const readDir = (dir: string, relativePath = ''): void => { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const entryPath = path.join(relativePath, entry.name); + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + readDir(fullPath, entryPath); + } else { + files.push(path.join(prefix, entryPath)); + } + } + }; + + readDir(dirPath); + return Promise.resolve(files); + } + + async copy( + sourceKey: string, + destinationKey: string, + _options: StorageUploadOptions = {}, + ): Promise { + const sourcePath = this.buildPath(sourceKey); + const destPath = this.buildPath(destinationKey); + + if (!fs.existsSync(sourcePath)) { + throw Object.assign(new Error(`Source file not found: ${sourceKey}`), { + code: 'ENOENT', + }); + } + + ensureDirectoryExistence(destPath); + await fs.promises.copyFile(sourcePath, destPath); + + return { key: destinationKey }; + } + + override getSignedUrl( + key: string, + _expiresIn: number, + ): Promise { + return Promise.resolve(`/uploads/${key}`); + } + + getBasePath(): string { + return this.basePath; + } + + getContentType(ext: string): string { + return CONTENT_TYPES[ext] || 'application/octet-stream'; + } +} diff --git a/backend/src/services/file/S3StorageProvider.js b/backend/src/services/file/S3StorageProvider.js deleted file mode 100644 index 20ed25e..0000000 --- a/backend/src/services/file/S3StorageProvider.js +++ /dev/null @@ -1,523 +0,0 @@ -/** - * S3StorageProvider - * - * AWS S3 storage implementation following the Strategy Pattern. - * Implements BaseStorageProvider interface for S3-specific operations. - * - * Features: - * - Request timeout and connection pool management - * - Retry strategy with exponential backoff - * - AbortController support for request cancellation - * - Comprehensive error handling - */ - -const https = require('https'); -const { - S3Client, - PutObjectCommand, - GetObjectCommand, - DeleteObjectCommand, - DeleteObjectsCommand, - ListObjectsV2Command, - HeadObjectCommand, - CopyObjectCommand, -} = require('@aws-sdk/client-s3'); -const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); -const { NodeHttpHandler } = require('@smithy/node-http-handler'); -const BaseStorageProvider = require('./BaseStorageProvider'); - -/** - * Map S3 error names to HTTP status codes - */ -const S3_ERROR_STATUS_MAP = { - NoSuchKey: 404, - NotFound: 404, - NoSuchBucket: 404, - AccessDenied: 403, - InvalidAccessKeyId: 403, - SignatureDoesNotMatch: 403, - InvalidObjectState: 403, - ExpiredToken: 401, - TimeoutError: 504, - RequestTimeout: 504, - NetworkingError: 503, - ServiceUnavailable: 503, - SlowDown: 503, - InternalError: 500, - ThrottlingException: 429, - TooManyRequestsException: 429, -}; - -/** - * Errors that should be retried - */ -const RETRYABLE_ERRORS = new Set([ - 'TimeoutError', - 'RequestTimeout', - 'NetworkingError', - 'ServiceUnavailable', - 'SlowDown', - 'InternalError', - 'ThrottlingException', - 'TooManyRequestsException', - 'ECONNRESET', - 'ECONNREFUSED', - 'ETIMEDOUT', - 'EPIPE', -]); - -class S3StorageProvider extends BaseStorageProvider { - /** - * @param {Object} options - * @param {string} options.bucket - S3 bucket name - * @param {string} options.region - AWS region - * @param {string} [options.accessKeyId] - AWS access key ID - * @param {string} [options.secretAccessKey] - AWS secret access key - * @param {string} [options.prefix] - Key prefix for all operations - * @param {number} [options.connectionTimeout=5000] - Connection timeout in ms - * @param {number} [options.requestTimeout=30000] - Request timeout in ms - * @param {number} [options.maxAttempts=3] - Maximum retry attempts - * @param {number} [options.maxSockets=50] - Maximum concurrent connections - * @param {boolean} [options.keepAlive=true] - Enable connection keep-alive - */ - constructor(options = {}) { - super(); - this.bucket = options.bucket; - this.prefix = options.prefix || ''; - - // Timeout and connection pool settings - const connectionTimeout = options.connectionTimeout || 5000; - const requestTimeout = options.requestTimeout || 30000; - const maxSockets = options.maxSockets || 50; - const keepAlive = options.keepAlive !== false; - - // Create HTTPS agent with connection pooling - this.httpsAgent = new https.Agent({ - maxSockets, - keepAlive, - keepAliveMsecs: 1000, - }); - - // Create NodeHttpHandler with timeout and connection pool - const requestHandler = new NodeHttpHandler({ - connectionTimeout, - requestTimeout, - httpsAgent: this.httpsAgent, - }); - - // Retry configuration - const maxAttempts = options.maxAttempts || 3; - - this.client = new S3Client({ - region: options.region || 'us-east-1', - credentials: - options.accessKeyId && options.secretAccessKey - ? { - accessKeyId: options.accessKeyId, - secretAccessKey: options.secretAccessKey, - } - : undefined, - requestHandler, - maxAttempts, - retryMode: 'adaptive', // Use adaptive retry with exponential backoff - }); - - // Store config for health checks and logging - this.config = { - region: options.region || 'us-east-1', - connectionTimeout, - requestTimeout, - maxAttempts, - maxSockets, - keepAlive, - }; - } - - static get providerName() { - return 's3'; - } - - /** - * Get HTTP status code for an S3 error - * @param {Error} error - The error object - * @returns {number} HTTP status code - */ - static getErrorStatusCode(error) { - if (!error) return 500; - - // Check by error name - if (error.name && S3_ERROR_STATUS_MAP[error.name]) { - return S3_ERROR_STATUS_MAP[error.name]; - } - - // Check by error code (for network errors) - if (error.code && RETRYABLE_ERRORS.has(error.code)) { - return 503; - } - - // Check by $metadata (AWS SDK v3 format) - if (error.$metadata?.httpStatusCode) { - return error.$metadata.httpStatusCode; - } - - return 500; - } - - /** - * Check if an error is retryable - * @param {Error} error - The error object - * @returns {boolean} - */ - static isRetryableError(error) { - if (!error) return false; - return ( - RETRYABLE_ERRORS.has(error.name) || - RETRYABLE_ERRORS.has(error.code) || - (error.$metadata?.httpStatusCode >= 500 && - error.$metadata?.httpStatusCode < 600) - ); - } - - /** - * Build full key with prefix - */ - buildKey(key) { - const cleanPrefix = (this.prefix || '').replace(/^\/+|\/+$/g, ''); - const cleanKey = (key || '').replace(/^\/+/, ''); - return cleanPrefix ? `${cleanPrefix}/${cleanKey}` : cleanKey; - } - - /** - * Upload a file to S3 - * @param {string} key - Storage key/path - * @param {Buffer|ReadableStream} data - File data - * @param {Object} options - Upload options - * @param {AbortSignal} [options.signal] - AbortController signal for cancellation - * @returns {Promise<{ key: string, url?: string }>} - */ - async upload(key, data, options = {}) { - const fullKey = this.buildKey(key); - const { signal, ...uploadOptions } = options; - - const params = { - Bucket: this.bucket, - Key: fullKey, - Body: data, - }; - - if (uploadOptions.contentType) { - params.ContentType = uploadOptions.contentType; - } - - if (uploadOptions.metadata) { - params.Metadata = uploadOptions.metadata; - } - - const sendOptions = signal ? { abortSignal: signal } : {}; - await this.client.send(new PutObjectCommand(params), sendOptions); - - return { - key: fullKey, - url: `https://${this.bucket}.s3.amazonaws.com/${fullKey}`, - }; - } - - /** - * Download a file from S3 - * @param {string} key - Storage key/path - * @param {Object} [options] - Download options - * @param {AbortSignal} [options.signal] - AbortController signal for cancellation - * @param {boolean} [options.headOnly] - Only get metadata (HEAD request) - * @param {string} [options.range] - HTTP Range header value (e.g., "bytes=0-1000") - * @returns {Promise<{ body: ReadableStream, contentType?: string, contentLength?: number }>} - */ - async download(key, options = {}) { - const fullKey = this.buildKey(key); - const { signal, headOnly, range } = options; - - const sendOptions = signal ? { abortSignal: signal } : {}; - - // HEAD request for metadata only - if (headOnly) { - const output = await this.client.send( - new HeadObjectCommand({ - Bucket: this.bucket, - Key: fullKey, - }), - sendOptions, - ); - return { - body: null, - contentType: output.ContentType, - contentLength: output.ContentLength, - }; - } - - // Build GetObjectCommand with optional Range header - const commandParams = { - Bucket: this.bucket, - Key: fullKey, - }; - - if (range) { - commandParams.Range = range; - } - - const output = await this.client.send( - new GetObjectCommand(commandParams), - sendOptions, - ); - - return { - body: output.Body, - contentType: output.ContentType, - contentLength: output.ContentLength, - }; - } - - /** - * Delete a file from S3 - * @param {string} key - Storage key/path - * @param {Object} [options] - Delete options - * @param {AbortSignal} [options.signal] - AbortController signal for cancellation - * @returns {Promise} - */ - async delete(key, options = {}) { - const fullKey = this.buildKey(key); - const { signal } = options; - - const sendOptions = signal ? { abortSignal: signal } : {}; - await this.client.send( - new DeleteObjectCommand({ - Bucket: this.bucket, - Key: fullKey, - }), - sendOptions, - ); - } - - /** - * Delete multiple files from S3 - * @param {string[]} keys - Array of keys to delete - * @param {Object} [options] - Delete options - * @param {AbortSignal} [options.signal] - AbortController signal for cancellation - * @returns {Promise<{ deleted: string[], errors: Array<{ key: string, error: string }> }>} - */ - async deleteMany(keys, options = {}) { - if (!keys || keys.length === 0) { - return { deleted: [], errors: [] }; - } - - const { signal } = options; - const sendOptions = signal ? { abortSignal: signal } : {}; - const objects = keys.map((key) => ({ Key: this.buildKey(key) })); - const deleted = []; - const errors = []; - - // S3 DeleteObjects supports max 1000 objects per request - const chunks = []; - for (let i = 0; i < objects.length; i += 1000) { - chunks.push(objects.slice(i, i + 1000)); - } - - for (const chunk of chunks) { - const result = await this.client.send( - new DeleteObjectsCommand({ - Bucket: this.bucket, - Delete: { Objects: chunk }, - }), - sendOptions, - ); - - if (result.Deleted) { - deleted.push(...result.Deleted.map((d) => d.Key)); - } - if (result.Errors) { - errors.push( - ...result.Errors.map((e) => ({ - key: e.Key, - error: e.Message || e.Code, - })), - ); - } - } - - return { deleted, errors }; - } - - /** - * Check if a file exists in S3 - * @param {string} key - Storage key/path - * @param {Object} [options] - Options - * @param {AbortSignal} [options.signal] - AbortController signal for cancellation - * @returns {Promise} - */ - async exists(key, options = {}) { - const fullKey = this.buildKey(key); - const { signal } = options; - - try { - const sendOptions = signal ? { abortSignal: signal } : {}; - await this.client.send( - new HeadObjectCommand({ - Bucket: this.bucket, - Key: fullKey, - }), - sendOptions, - ); - return true; - } catch (error) { - if (error.name === 'NotFound' || error.name === 'NoSuchKey') { - return false; - } - throw error; - } - } - - /** - * List files with a given prefix - * @param {string} prefix - Key prefix - * @param {Object} [options] - Options - * @param {AbortSignal} [options.signal] - AbortController signal for cancellation - * @returns {Promise} Array of keys - */ - async list(prefix, options = {}) { - const fullPrefix = this.buildKey(prefix); - const { signal } = options; - const sendOptions = signal ? { abortSignal: signal } : {}; - const keys = []; - let continuationToken = null; - - do { - const params = { - Bucket: this.bucket, - Prefix: fullPrefix, - }; - - if (continuationToken) { - params.ContinuationToken = continuationToken; - } - - const result = await this.client.send( - new ListObjectsV2Command(params), - sendOptions, - ); - - if (result.Contents) { - keys.push(...result.Contents.map((obj) => obj.Key)); - } - - continuationToken = result.IsTruncated - ? result.NextContinuationToken - : null; - } while (continuationToken); - - return keys; - } - - /** - * Get a signed URL for direct access - * @param {string} key - Storage key/path - * @param {number} expiresIn - Expiration time in seconds - * @returns {Promise} Signed URL - */ - async getSignedUrl(key, expiresIn = 3600) { - const fullKey = this.buildKey(key); - - const command = new GetObjectCommand({ - Bucket: this.bucket, - Key: fullKey, - }); - - return getSignedUrl(this.client, command, { expiresIn }); - } - - /** - * Copy a file within S3 (server-side copy - no download/upload) - * Uses CopyObjectCommand for efficient server-side copying. - * Inherits retry behavior from S3Client's adaptive retry mode. - * - * @param {string} sourceKey - Source storage key/path - * @param {string} destinationKey - Destination storage key/path - * @param {Object} [options] - Copy options - * @param {AbortSignal} [options.signal] - AbortController signal for cancellation - * @param {string} [options.contentType] - Override content type (uses MetadataDirective: REPLACE) - * @returns {Promise<{ key: string, url: string }>} - */ - async copy(sourceKey, destinationKey, options = {}) { - const fullSourceKey = this.buildKey(sourceKey); - const fullDestinationKey = this.buildKey(destinationKey); - const { signal, contentType } = options; - - // CopySource requires URL-encoded path for special characters - // Format: bucket/key (no leading slash) - const encodedSourceKey = fullSourceKey - .split('/') - .map((segment) => encodeURIComponent(segment)) - .join('/'); - - const params = { - Bucket: this.bucket, - CopySource: `${this.bucket}/${encodedSourceKey}`, - Key: fullDestinationKey, - }; - - // Override content type if provided (requires MetadataDirective: REPLACE) - if (contentType) { - params.ContentType = contentType; - params.MetadataDirective = 'REPLACE'; - } - - // Use same pattern as other methods for AbortSignal support - const sendOptions = signal ? { abortSignal: signal } : {}; - await this.client.send(new CopyObjectCommand(params), sendOptions); - - return { - key: fullDestinationKey, - url: `https://${this.bucket}.s3.amazonaws.com/${fullDestinationKey}`, - }; - } - - /** - * Get the underlying S3 client for advanced operations - * @returns {S3Client} - */ - getClient() { - return this.client; - } - - /** - * Get bucket name - * @returns {string} - */ - getBucket() { - return this.bucket; - } - - /** - * Get prefix - * @returns {string} - */ - getPrefix() { - return this.prefix; - } - - /** - * Get provider configuration (for logging/debugging) - * @returns {Object} - */ - getConfig() { - return { ...this.config }; - } - - /** - * Cleanup resources (close connections) - */ - destroy() { - if (this.httpsAgent) { - this.httpsAgent.destroy(); - } - } -} - -module.exports = S3StorageProvider; diff --git a/backend/src/services/file/S3StorageProvider.ts b/backend/src/services/file/S3StorageProvider.ts new file mode 100644 index 0000000..7fe2d86 --- /dev/null +++ b/backend/src/services/file/S3StorageProvider.ts @@ -0,0 +1,481 @@ +/** + * S3StorageProvider + * + * AWS S3 storage implementation following the Strategy Pattern. + * Implements BaseStorageProvider interface for S3-specific operations. + */ + +import https from 'https'; +import { + CopyObjectCommand, + DeleteObjectCommand, + DeleteObjectsCommand, + GetObjectCommand, + HeadObjectCommand, + ListObjectsV2Command, + PutObjectCommand, + S3Client, + S3ServiceException, + type CopyObjectCommandInput, + type DeleteObjectsCommandInput, + type GetObjectCommandInput, + type ListObjectsV2CommandInput, + type ObjectIdentifier, + type PutObjectCommandInput, + type S3ClientConfig, +} from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { NodeHttpHandler } from '@smithy/node-http-handler'; +import type { HttpHandlerOptions } from '@smithy/types'; +import BaseStorageProvider from './BaseStorageProvider.ts'; +import type { + S3StorageProviderConfig, + S3StorageProviderOptions, + StorageCopyOptions, + StorageDeleteManyResult, + StorageDownloadOptions, + StorageDownloadResult, + StorageOperationOptions, + StorageUploadData, + StorageUploadOptions, + StorageUploadResult, +} from '../../types/index.ts'; + +const DEFAULT_REGION = 'us-east-1'; +const DEFAULT_CONNECTION_TIMEOUT = 5000; +const DEFAULT_REQUEST_TIMEOUT = 30000; +const DEFAULT_MAX_ATTEMPTS = 3; +const DEFAULT_MAX_SOCKETS = 50; + +const S3_ERROR_STATUS_MAP: Record = { + NoSuchKey: 404, + NotFound: 404, + NoSuchBucket: 404, + AccessDenied: 403, + InvalidAccessKeyId: 403, + SignatureDoesNotMatch: 403, + InvalidObjectState: 403, + ExpiredToken: 401, + TimeoutError: 504, + RequestTimeout: 504, + NetworkingError: 503, + ServiceUnavailable: 503, + SlowDown: 503, + InternalError: 500, + ThrottlingException: 429, + TooManyRequestsException: 429, +}; + +const RETRYABLE_ERRORS = new Set([ + 'TimeoutError', + 'RequestTimeout', + 'NetworkingError', + 'ServiceUnavailable', + 'SlowDown', + 'InternalError', + 'ThrottlingException', + 'TooManyRequestsException', + 'ECONNRESET', + 'ECONNREFUSED', + 'ETIMEDOUT', + 'EPIPE', +]); + +const hasErrorCode = (error: unknown): error is NodeJS.ErrnoException => { + return error instanceof Error && 'code' in error; +}; + +const getErrorName = (error: unknown): string | undefined => { + return error instanceof Error ? error.name : undefined; +}; + +const getErrorCode = (error: unknown): string | undefined => { + if (!hasErrorCode(error)) { + return undefined; + } + + return typeof error.code === 'string' ? error.code : undefined; +}; + +const getSendOptions = ( + signal?: AbortSignal, +): HttpHandlerOptions | undefined => { + return signal ? { abortSignal: signal } : undefined; +}; + +export default class S3StorageProvider extends BaseStorageProvider { + private readonly bucket: string; + private readonly prefix: string; + private readonly httpsAgent: https.Agent; + private readonly client: S3Client; + private readonly config: S3StorageProviderConfig; + + constructor(options: S3StorageProviderOptions) { + super(); + this.bucket = options.bucket; + this.prefix = options.prefix || ''; + + const connectionTimeout = + options.connectionTimeout || DEFAULT_CONNECTION_TIMEOUT; + const requestTimeout = options.requestTimeout || DEFAULT_REQUEST_TIMEOUT; + const maxSockets = options.maxSockets || DEFAULT_MAX_SOCKETS; + const keepAlive = options.keepAlive !== false; + const maxAttempts = options.maxAttempts || DEFAULT_MAX_ATTEMPTS; + const region = options.region || DEFAULT_REGION; + + this.httpsAgent = new https.Agent({ + maxSockets, + keepAlive, + keepAliveMsecs: 1000, + }); + + const requestHandler = new NodeHttpHandler({ + connectionTimeout, + requestTimeout, + httpsAgent: this.httpsAgent, + }); + + const clientConfig: S3ClientConfig = { + region, + requestHandler, + maxAttempts, + retryMode: 'adaptive', + }; + + if (options.accessKeyId && options.secretAccessKey) { + clientConfig.credentials = { + accessKeyId: options.accessKeyId, + secretAccessKey: options.secretAccessKey, + }; + } + + this.client = new S3Client(clientConfig); + + this.config = { + region, + connectionTimeout, + requestTimeout, + maxAttempts, + maxSockets, + keepAlive, + }; + } + + static override get providerName(): string { + return 's3'; + } + + static getErrorStatusCode(error: unknown): number { + const errorName = getErrorName(error); + const errorCode = getErrorCode(error); + + if (errorName && S3_ERROR_STATUS_MAP[errorName]) { + return S3_ERROR_STATUS_MAP[errorName]; + } + + if (errorCode && RETRYABLE_ERRORS.has(errorCode)) { + return 503; + } + + if ( + error instanceof S3ServiceException && + error.$metadata.httpStatusCode + ) { + return error.$metadata.httpStatusCode; + } + + return 500; + } + + static isRetryableError(error: unknown): boolean { + const errorName = getErrorName(error); + const errorCode = getErrorCode(error); + + return Boolean( + (errorName && RETRYABLE_ERRORS.has(errorName)) || + (errorCode && RETRYABLE_ERRORS.has(errorCode)) || + (error instanceof S3ServiceException && + error.$metadata.httpStatusCode !== undefined && + error.$metadata.httpStatusCode >= 500 && + error.$metadata.httpStatusCode < 600), + ); + } + + buildKey(key: string): string { + const cleanPrefix = this.prefix.replace(/^\/+|\/+$/g, ''); + const cleanKey = key.replace(/^\/+/, ''); + return cleanPrefix ? `${cleanPrefix}/${cleanKey}` : cleanKey; + } + + override async upload( + key: string, + data: StorageUploadData, + options: StorageUploadOptions = {}, + ): Promise { + const fullKey = this.buildKey(key); + + const params: PutObjectCommandInput = { + Bucket: this.bucket, + Key: fullKey, + Body: data, + }; + + if (options.contentType) { + params.ContentType = options.contentType; + } + + if (options.metadata) { + params.Metadata = options.metadata; + } + + await this.client.send( + new PutObjectCommand(params), + getSendOptions(options.signal), + ); + + return { + key: fullKey, + url: `https://${this.bucket}.s3.amazonaws.com/${fullKey}`, + }; + } + + override async download( + key: string, + options: StorageDownloadOptions = {}, + ): Promise { + const fullKey = this.buildKey(key); + const sendOptions = getSendOptions(options.signal); + + if (options.headOnly) { + const output = await this.client.send( + new HeadObjectCommand({ + Bucket: this.bucket, + Key: fullKey, + }), + sendOptions, + ); + const result: StorageDownloadResult = { body: null }; + if (output.ContentType) { + result.contentType = output.ContentType; + } + if (output.ContentLength !== undefined) { + result.contentLength = output.ContentLength; + } + return result; + } + + const commandParams: GetObjectCommandInput = { + Bucket: this.bucket, + Key: fullKey, + }; + + if (options.range) { + commandParams.Range = options.range; + } + + const output = await this.client.send( + new GetObjectCommand(commandParams), + sendOptions, + ); + + const result: StorageDownloadResult = { body: output.Body || null }; + if (output.ContentType) { + result.contentType = output.ContentType; + } + if (output.ContentLength !== undefined) { + result.contentLength = output.ContentLength; + } + + return result; + } + + override async delete( + key: string, + options: StorageOperationOptions = {}, + ): Promise { + const fullKey = this.buildKey(key); + + await this.client.send( + new DeleteObjectCommand({ + Bucket: this.bucket, + Key: fullKey, + }), + getSendOptions(options.signal), + ); + } + + override async deleteMany( + keys: string[], + options: StorageOperationOptions = {}, + ): Promise { + if (keys.length === 0) { + return { deleted: [], errors: [] }; + } + + const objects: ObjectIdentifier[] = keys.map((key) => ({ + Key: this.buildKey(key), + })); + const deleted: string[] = []; + const errors: StorageDeleteManyResult['errors'] = []; + const chunks: ObjectIdentifier[][] = []; + + for (let i = 0; i < objects.length; i += 1000) { + chunks.push(objects.slice(i, i + 1000)); + } + + for (const chunk of chunks) { + const params: DeleteObjectsCommandInput = { + Bucket: this.bucket, + Delete: { Objects: chunk }, + }; + const result = await this.client.send( + new DeleteObjectsCommand(params), + getSendOptions(options.signal), + ); + + for (const deletedObject of result.Deleted || []) { + if (deletedObject.Key) { + deleted.push(deletedObject.Key); + } + } + + for (const deleteError of result.Errors || []) { + errors.push({ + key: deleteError.Key || '', + error: deleteError.Message || deleteError.Code || 'UnknownError', + }); + } + } + + return { deleted, errors }; + } + + override async exists( + key: string, + options: StorageOperationOptions = {}, + ): Promise { + const fullKey = this.buildKey(key); + + try { + await this.client.send( + new HeadObjectCommand({ + Bucket: this.bucket, + Key: fullKey, + }), + getSendOptions(options.signal), + ); + return true; + } catch (error) { + const errorName = getErrorName(error); + if (errorName === 'NotFound' || errorName === 'NoSuchKey') { + return false; + } + throw error; + } + } + + override async list( + prefix: string, + options: StorageOperationOptions = {}, + ): Promise { + const fullPrefix = this.buildKey(prefix); + const keys: string[] = []; + let continuationToken: string | undefined; + + do { + const params: ListObjectsV2CommandInput = { + Bucket: this.bucket, + Prefix: fullPrefix, + }; + + if (continuationToken) { + params.ContinuationToken = continuationToken; + } + + const result = await this.client.send( + new ListObjectsV2Command(params), + getSendOptions(options.signal), + ); + + for (const object of result.Contents || []) { + if (object.Key) { + keys.push(object.Key); + } + } + + continuationToken = result.IsTruncated + ? result.NextContinuationToken + : undefined; + } while (continuationToken); + + return keys; + } + + override async getSignedUrl( + key: string, + expiresIn = 3600, + ): Promise { + const fullKey = this.buildKey(key); + + const command = new GetObjectCommand({ + Bucket: this.bucket, + Key: fullKey, + }); + + return getSignedUrl(this.client, command, { expiresIn }); + } + + async copy( + sourceKey: string, + destinationKey: string, + options: StorageCopyOptions = {}, + ): Promise { + const fullSourceKey = this.buildKey(sourceKey); + const fullDestinationKey = this.buildKey(destinationKey); + const encodedSourceKey = fullSourceKey + .split('/') + .map((segment) => encodeURIComponent(segment)) + .join('/'); + + const params: CopyObjectCommandInput = { + Bucket: this.bucket, + CopySource: `${this.bucket}/${encodedSourceKey}`, + Key: fullDestinationKey, + }; + + if (options.contentType) { + params.ContentType = options.contentType; + params.MetadataDirective = 'REPLACE'; + } + + await this.client.send( + new CopyObjectCommand(params), + getSendOptions(options.signal), + ); + + return { + key: fullDestinationKey, + url: `https://${this.bucket}.s3.amazonaws.com/${fullDestinationKey}`, + }; + } + + getClient(): S3Client { + return this.client; + } + + getBucket(): string { + return this.bucket; + } + + getPrefix(): string { + return this.prefix; + } + + getConfig(): S3StorageProviderConfig { + return { ...this.config }; + } + + destroy(): void { + this.httpsAgent.destroy(); + } +} diff --git a/backend/src/services/file/UploadSessionManager.js b/backend/src/services/file/UploadSessionManager.ts similarity index 55% rename from backend/src/services/file/UploadSessionManager.js rename to backend/src/services/file/UploadSessionManager.ts index 805193d..a93c119 100644 --- a/backend/src/services/file/UploadSessionManager.js +++ b/backend/src/services/file/UploadSessionManager.ts @@ -5,31 +5,35 @@ * Handles session lifecycle, chunk tracking, and assembly. */ -const fs = require('fs'); -const path = require('path'); -const { v4: uuid } = require('uuid'); +import fs from 'fs'; +import path from 'path'; +import { v4 as uuid } from 'uuid'; +import type { + UploadSessionChunkMeta, + UploadSessionCreateOptions, + UploadSessionManagerOptions, + UploadSessionMeta, + UploadSessionUploadedChunks, +} from '../../types/index.ts'; -const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours +const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; -/** - * Ensure directory exists - */ -const ensureDirectoryExistence = (filePath) => { +const ensureDirectoryExistence = (filePath: string): void => { const dirname = path.dirname(filePath); if (fs.existsSync(dirname)) { - return true; + return; } ensureDirectoryExistence(dirname); fs.mkdirSync(dirname); }; -/** - * Append one file to another - */ -const streamAppendFile = async (targetPath, sourcePath) => { - await new Promise((resolve, reject) => { +const streamAppendFile = ( + targetPath: string, + sourcePath: string, +): Promise => { + return new Promise((resolve, reject) => { const writeStream = fs.createWriteStream(targetPath, { flags: 'a' }); const readStream = fs.createReadStream(sourcePath); @@ -40,53 +44,75 @@ const streamAppendFile = async (targetPath, sourcePath) => { }); }; -class UploadSessionManager { - /** - * @param {Object} options - * @param {string} options.sessionDir - Base directory for upload sessions - * @param {number} [options.ttlMs] - Session TTL in milliseconds - */ - constructor(options = {}) { +const isRecord = (value: unknown): value is Record => { + return typeof value === 'object' && value !== null && !Array.isArray(value); +}; + +const isUploadSessionChunkMeta = ( + value: unknown, +): value is UploadSessionChunkMeta => { + return ( + isRecord(value) && + typeof value.size === 'number' && + typeof value.uploadedAt === 'string' + ); +}; + +const isUploadSessionUploadedChunks = ( + value: unknown, +): value is UploadSessionUploadedChunks => { + return isRecord(value) && Object.values(value).every(isUploadSessionChunkMeta); +}; + +const isNullableString = (value: unknown): value is string | null => { + return typeof value === 'string' || value === null; +}; + +const isUploadSessionMeta = (value: unknown): value is UploadSessionMeta => { + return ( + isRecord(value) && + typeof value.sessionId === 'string' && + typeof value.filename === 'string' && + typeof value.folder === 'string' && + typeof value.totalChunks === 'number' && + typeof value.totalSize === 'number' && + isNullableString(value.userId) && + isNullableString(value.contentType) && + isUploadSessionUploadedChunks(value.uploadedChunks) && + typeof value.createdAt === 'string' && + typeof value.updatedAt === 'string' + ); +}; + +export default class UploadSessionManager { + private readonly sessionDir: string; + private readonly ttlMs: number; + + constructor(options: UploadSessionManagerOptions) { this.sessionDir = options.sessionDir; this.ttlMs = options.ttlMs || DEFAULT_TTL_MS; } - /** - * Get session directory path - */ - getSessionDir(sessionId) { + getSessionDir(sessionId: string): string { return path.join(this.sessionDir, sessionId); } - /** - * Get session metadata path - */ - getMetaPath(sessionId) { + getMetaPath(sessionId: string): string { return path.join(this.getSessionDir(sessionId), 'meta.json'); } - /** - * Get session chunks directory - */ - getChunksDir(sessionId) { + getChunksDir(sessionId: string): string { return path.join(this.getSessionDir(sessionId), 'chunks'); } - /** - * Get chunk file path - */ - getChunkPath(sessionId, chunkIndex) { + getChunkPath(sessionId: string, chunkIndex: number): string { return path.join( this.getChunksDir(sessionId), `${String(chunkIndex)}.part`, ); } - /** - * Read session metadata - * @returns {Object|null} - */ - readMeta(sessionId) { + readMeta(sessionId: string): UploadSessionMeta | null { const metaPath = this.getMetaPath(sessionId); if (!fs.existsSync(metaPath)) { @@ -94,30 +120,22 @@ class UploadSessionManager { } const raw = fs.readFileSync(metaPath, 'utf8'); - return JSON.parse(raw); + const parsed: unknown = JSON.parse(raw); + + if (!isUploadSessionMeta(parsed)) { + throw new Error(`Invalid upload session metadata: ${sessionId}`); + } + + return parsed; } - /** - * Write session metadata - */ - writeMeta(sessionId, payload) { + writeMeta(sessionId: string, payload: UploadSessionMeta): void { const metaPath = this.getMetaPath(sessionId); ensureDirectoryExistence(metaPath); fs.writeFileSync(metaPath, JSON.stringify(payload, null, 2), 'utf8'); } - /** - * Create a new upload session - * @param {Object} options - * @param {string} options.filename - Original filename - * @param {string} options.folder - Target folder - * @param {number} options.totalChunks - Total number of chunks - * @param {number} [options.totalSize] - Total file size - * @param {string} [options.userId] - User ID - * @param {string} [options.contentType] - File content type - * @returns {string} Session ID - */ - createSession(options) { + createSession(options: UploadSessionCreateOptions): string { const sessionId = uuid(); const chunksDir = this.getChunksDir(sessionId); @@ -125,7 +143,7 @@ class UploadSessionManager { fs.mkdirSync(chunksDir, { recursive: true }); const now = new Date().toISOString(); - const meta = { + const meta: UploadSessionMeta = { sessionId, filename: options.filename, folder: options.folder, @@ -142,13 +160,7 @@ class UploadSessionManager { return sessionId; } - /** - * Save a chunk - * @param {string} sessionId - * @param {number} chunkIndex - * @param {Buffer} data - */ - async saveChunk(sessionId, chunkIndex, data) { + saveChunk(sessionId: string, chunkIndex: number, data: Buffer): Promise { const chunkPath = this.getChunkPath(sessionId, chunkIndex); ensureDirectoryExistence(chunkPath); fs.writeFileSync(chunkPath, data); @@ -162,45 +174,35 @@ class UploadSessionManager { meta.updatedAt = new Date().toISOString(); this.writeMeta(sessionId, meta); } + + return Promise.resolve(); } - /** - * Check if a chunk exists - */ - chunkExists(sessionId, chunkIndex) { + chunkExists(sessionId: string, chunkIndex: number): boolean { const chunkPath = this.getChunkPath(sessionId, chunkIndex); return fs.existsSync(chunkPath); } - /** - * Check if session is complete - */ - isComplete(sessionId) { + isComplete(sessionId: string): boolean { const meta = this.readMeta(sessionId); - if (!meta) return false; + if (!meta) { + return false; + } const uploadedCount = Object.keys(meta.uploadedChunks).length; return uploadedCount >= meta.totalChunks; } - /** - * Assemble chunks into final file - * @param {string} sessionId - * @param {string} targetPath - Path for assembled file - */ - async assembleChunks(sessionId, targetPath) { + async assembleChunks(sessionId: string, targetPath: string): Promise { const meta = this.readMeta(sessionId); if (!meta) { throw new Error('Session not found'); } ensureDirectoryExistence(targetPath); - - // Create empty target file fs.writeFileSync(targetPath, ''); - // Append chunks in order - for (let i = 0; i < meta.totalChunks; i++) { + for (let i = 0; i < meta.totalChunks; i += 1) { const chunkPath = this.getChunkPath(sessionId, i); if (!fs.existsSync(chunkPath)) { throw new Error(`Missing chunk ${i}`); @@ -211,10 +213,7 @@ class UploadSessionManager { return targetPath; } - /** - * Remove an upload session - */ - removeSession(sessionId) { + removeSession(sessionId: string): void { const sessionDir = this.getSessionDir(sessionId); if (fs.existsSync(sessionDir)) { @@ -222,10 +221,7 @@ class UploadSessionManager { } } - /** - * Cleanup expired sessions - */ - cleanupExpiredSessions() { + cleanupExpiredSessions(): void { if (!fs.existsSync(this.sessionDir)) { return; } @@ -247,11 +243,9 @@ class UploadSessionManager { if (!updatedAt || now - updatedAt > this.ttlMs) { this.removeSession(sessionId); } - } catch (error) { + } catch { this.removeSession(sessionId); } }); } } - -module.exports = UploadSessionManager; diff --git a/backend/src/services/file/index.js b/backend/src/services/file/index.js deleted file mode 100644 index dccee10..0000000 --- a/backend/src/services/file/index.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * File Service Module - * - * Modular file storage service with Strategy Pattern providers: - * - Local filesystem (LocalStorageProvider) - * - AWS S3 (S3StorageProvider) - * - Google Cloud Storage - */ - -const BaseStorageProvider = require('./BaseStorageProvider'); -const S3StorageProvider = require('./S3StorageProvider'); -const LocalStorageProvider = require('./LocalStorageProvider'); -const UploadSessionManager = require('./UploadSessionManager'); - -// Re-export the unified file service -const FileService = require('../file'); - -module.exports = { - // Unified service API - ...FileService, - - // Storage providers (for direct usage if needed) - BaseStorageProvider, - S3StorageProvider, - LocalStorageProvider, - UploadSessionManager, -}; diff --git a/backend/src/services/file/index.ts b/backend/src/services/file/index.ts new file mode 100644 index 0000000..b14eb90 --- /dev/null +++ b/backend/src/services/file/index.ts @@ -0,0 +1,23 @@ +import FileService from '../file.ts'; + +import BaseStorageProvider from './BaseStorageProvider.ts'; +import LocalStorageProvider from './LocalStorageProvider.ts'; +import S3StorageProvider from './S3StorageProvider.ts'; +import UploadSessionManager from './UploadSessionManager.ts'; + +const fileServiceModule = { + ...FileService, + BaseStorageProvider, + S3StorageProvider, + LocalStorageProvider, + UploadSessionManager, +}; + +export { + BaseStorageProvider, + S3StorageProvider, + LocalStorageProvider, + UploadSessionManager, +}; + +export default fileServiceModule; diff --git a/backend/src/services/global_transition_defaults.js b/backend/src/services/global_transition_defaults.js deleted file mode 100644 index 674e55e..0000000 --- a/backend/src/services/global_transition_defaults.js +++ /dev/null @@ -1,6 +0,0 @@ -const Global_transition_defaultsDBApi = require('../db/api/global_transition_defaults'); -const { createEntityService } = require('../factories/service.factory'); - -module.exports = createEntityService(Global_transition_defaultsDBApi, { - entityName: 'global_transition_defaults', -}); diff --git a/backend/src/services/global_transition_defaults.ts b/backend/src/services/global_transition_defaults.ts new file mode 100644 index 0000000..f5ea620 --- /dev/null +++ b/backend/src/services/global_transition_defaults.ts @@ -0,0 +1,6 @@ +import Global_transition_defaultsDBApi from '../db/api/global_transition_defaults.ts'; +import { createEntityService } from '../factories/service.factory.ts'; + +export default createEntityService(Global_transition_defaultsDBApi, { + entityName: 'global_transition_defaults', +}); diff --git a/backend/src/services/global_ui_control_defaults.js b/backend/src/services/global_ui_control_defaults.js deleted file mode 100644 index 4ba6e2f..0000000 --- a/backend/src/services/global_ui_control_defaults.js +++ /dev/null @@ -1,6 +0,0 @@ -const Global_ui_control_defaultsDBApi = require('../db/api/global_ui_control_defaults'); -const { createEntityService } = require('../factories/service.factory'); - -module.exports = createEntityService(Global_ui_control_defaultsDBApi, { - entityName: 'global_ui_control_defaults', -}); diff --git a/backend/src/services/global_ui_control_defaults.ts b/backend/src/services/global_ui_control_defaults.ts new file mode 100644 index 0000000..229046b --- /dev/null +++ b/backend/src/services/global_ui_control_defaults.ts @@ -0,0 +1,6 @@ +import Global_ui_control_defaultsDBApi from '../db/api/global_ui_control_defaults.ts'; +import { createEntityService } from '../factories/service.factory.ts'; + +export default createEntityService(Global_ui_control_defaultsDBApi, { + entityName: 'global_ui_control_defaults', +}); diff --git a/backend/src/services/notifications/errors/forbidden.js b/backend/src/services/notifications/errors/forbidden.js deleted file mode 100644 index 192fa10..0000000 --- a/backend/src/services/notifications/errors/forbidden.js +++ /dev/null @@ -1,16 +0,0 @@ -const { getNotification, isNotification } = require('../helpers'); - -module.exports = class ForbiddenError extends Error { - constructor(messageCode) { - let message; - - if (messageCode && isNotification(messageCode)) { - message = getNotification(messageCode); - } - - message = message || getNotification('errors.forbidden.message'); - - super(message); - this.code = 403; - } -}; diff --git a/backend/src/services/notifications/errors/forbidden.ts b/backend/src/services/notifications/errors/forbidden.ts new file mode 100644 index 0000000..dcd6fac --- /dev/null +++ b/backend/src/services/notifications/errors/forbidden.ts @@ -0,0 +1,18 @@ +import { getNotification, isNotification } from '../helpers.ts'; + +export default class ForbiddenError extends Error { + code: 403; + + constructor(messageCode?: string) { + const notificationMessage = + messageCode && isNotification(messageCode) + ? getNotification(messageCode) + : null; + + const message = + notificationMessage ?? getNotification('errors.forbidden.message'); + + super(message); + this.code = 403; + } +} diff --git a/backend/src/services/notifications/errors/validation.js b/backend/src/services/notifications/errors/validation.js deleted file mode 100644 index 0ef973b..0000000 --- a/backend/src/services/notifications/errors/validation.js +++ /dev/null @@ -1,17 +0,0 @@ -const { getNotification } = require('../helpers'); - -module.exports = class ValidationError extends Error { - constructor(messageCode, options = {}) { - // getNotification returns the translated message if key exists, - // or the key itself as the message if not found - // This allows both notification keys and plain string messages - const message = messageCode - ? getNotification(messageCode) - : getNotification('errors.validation.message'); - - super(message); - this.code = 400; - this.details = options.details; - this.isRequestValidation = Boolean(options.isRequestValidation); - } -}; diff --git a/backend/src/services/notifications/errors/validation.ts b/backend/src/services/notifications/errors/validation.ts new file mode 100644 index 0000000..36ba3f9 --- /dev/null +++ b/backend/src/services/notifications/errors/validation.ts @@ -0,0 +1,24 @@ +import { getNotification } from '../helpers.ts'; +import type { RequestValidationDetail } from '../../../types/index.ts'; + +interface ValidationErrorOptions { + details?: RequestValidationDetail[]; + isRequestValidation?: boolean; +} + +export default class ValidationError extends Error { + code: 400; + details: RequestValidationDetail[] | undefined; + isRequestValidation: boolean; + + constructor(messageCode?: string, options: ValidationErrorOptions = {}) { + const message = messageCode + ? getNotification(messageCode) + : getNotification('errors.validation.message'); + + super(message); + this.code = 400; + this.details = options.details; + this.isRequestValidation = Boolean(options.isRequestValidation); + } +} diff --git a/backend/src/services/notifications/helpers.js b/backend/src/services/notifications/helpers.js deleted file mode 100644 index 1c3a60f..0000000 --- a/backend/src/services/notifications/helpers.js +++ /dev/null @@ -1,30 +0,0 @@ -const _get = require('lodash/get'); -const errors = require('./list'); - -function format(message, args) { - if (!message) { - return null; - } - - return message.replace(/{(\d+)}/g, function (match, number) { - return typeof args[number] != 'undefined' ? args[number] : match; - }); -} - -const isNotification = (key) => { - const message = _get(errors, key); - return !!message; -}; - -const getNotification = (key, ...args) => { - const message = _get(errors, key); - - if (!message) { - return key; - } - - return format(message, args); -}; - -exports.getNotification = getNotification; -exports.isNotification = isNotification; diff --git a/backend/src/services/notifications/helpers.ts b/backend/src/services/notifications/helpers.ts new file mode 100644 index 0000000..ada5a4d --- /dev/null +++ b/backend/src/services/notifications/helpers.ts @@ -0,0 +1,51 @@ +import notifications from './list.ts'; + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function getNestedString(key: string): string | null { + const segments = key.split('.'); + let current: unknown = notifications; + + for (const segment of segments) { + if (!isRecord(current)) return null; + current = current[segment]; + } + + return typeof current === 'string' ? current : null; +} + +function format(message: string | null, args: unknown[]): string | null { + if (!message) { + return null; + } + + return message.replace(/{(\d+)}/g, (match, number) => { + const index = Number(number); + const replacement = args[index]; + if (replacement === undefined) return match; + if (replacement === null) return 'null'; + if (typeof replacement === 'string') return replacement; + if (typeof replacement === 'number') return `${replacement}`; + if (typeof replacement === 'boolean') return `${replacement}`; + if (typeof replacement === 'bigint') return `${replacement}`; + return match; + }); +} + +function isNotification(key: string): boolean { + return Boolean(getNestedString(key)); +} + +function getNotification(key: string, ...args: unknown[]): string { + const message = getNestedString(key); + + if (!message) { + return key; + } + + return format(message, args) ?? key; +} + +export { getNotification, isNotification }; diff --git a/backend/src/services/notifications/list.js b/backend/src/services/notifications/list.ts similarity index 95% rename from backend/src/services/notifications/list.js rename to backend/src/services/notifications/list.ts index e9ce120..e240c5e 100644 --- a/backend/src/services/notifications/list.js +++ b/backend/src/services/notifications/list.ts @@ -1,4 +1,6 @@ -const errors = { +import type { NotificationCatalog } from '../../types/index.ts'; + +const notifications: NotificationCatalog = { app: { title: 'Tour Builder Platform', }, @@ -98,4 +100,4 @@ const errors = { }, }; -module.exports = errors; +export default notifications; diff --git a/backend/src/services/openai.js b/backend/src/services/openai.js deleted file mode 100644 index 7cb6ff0..0000000 --- a/backend/src/services/openai.js +++ /dev/null @@ -1,85 +0,0 @@ -const axios = require('axios'); -const config = require('../config'); -const { LocalAIApi } = require('../ai/LocalAIApi'); - -const loadRoleService = () => { - try { - return require('./roles'); - } catch (error) { - console.error( - 'Role service is missing. Advanced roles are required for this operation.', - error, - ); - const err = new Error( - 'Role service is missing. Advanced roles are required for this operation.', - ); - err.originalError = error; - throw err; - } -}; - -module.exports = class OpenAiService { - static async getWidget(payload, userId, roleId) { - const RoleService = loadRoleService(); - const response = await axios.post( - `${config.flHost}/${config.project_uuid}/project_customization_widgets.json`, - payload, - ); - - if (response.status >= 200 && response.status < 300) { - const { widget_id } = await response.data; - await RoleService.addRoleInfo(roleId, userId, 'widgets', widget_id); - return widget_id; - } else { - console.error('=======error=======', response.data); - return { value: null, error: response.data }; - } - } - - static async askGpt(prompt) { - if (!prompt) { - return { - success: false, - error: 'Prompt is required', - }; - } - - const response = await LocalAIApi.createResponse( - { - input: [{ role: 'user', content: prompt }], - }, - { - poll_interval: 5, - poll_timeout: 300, - }, - ); - - if (response.success) { - let text = LocalAIApi.extractText(response); - if (!text) { - try { - const decoded = LocalAIApi.decodeJsonFromResponse(response); - text = JSON.stringify(decoded); - } catch (error) { - console.error('AI JSON decode failed:', error); - return { - success: false, - error: 'AI response parsing failed', - details: error.message || String(error), - }; - } - } - return { - success: true, - data: text, - }; - } - - console.error('AI proxy error:', response); - return { - success: false, - error: response.error || response.message || 'AI proxy error', - response, - }; - } -}; diff --git a/backend/src/services/permissions.js b/backend/src/services/permissions.js deleted file mode 100644 index 5e2f070..0000000 --- a/backend/src/services/permissions.js +++ /dev/null @@ -1,6 +0,0 @@ -const PermissionsDBApi = require('../db/api/permissions'); -const { createEntityService } = require('../factories/service.factory'); - -module.exports = createEntityService(PermissionsDBApi, { - entityName: 'permissions', -}); diff --git a/backend/src/services/permissions.ts b/backend/src/services/permissions.ts new file mode 100644 index 0000000..c090dcd --- /dev/null +++ b/backend/src/services/permissions.ts @@ -0,0 +1,6 @@ +import PermissionsDBApi from '../db/api/permissions.ts'; +import { createEntityService } from '../factories/service.factory.ts'; + +export default createEntityService(PermissionsDBApi, { + entityName: 'permissions', +}); diff --git a/backend/src/services/presigned_url_requests.js b/backend/src/services/presigned_url_requests.js deleted file mode 100644 index 23b8cb8..0000000 --- a/backend/src/services/presigned_url_requests.js +++ /dev/null @@ -1,6 +0,0 @@ -const Presigned_url_requestsDBApi = require('../db/api/presigned_url_requests'); -const { createEntityService } = require('../factories/service.factory'); - -module.exports = createEntityService(Presigned_url_requestsDBApi, { - entityName: 'presigned_url_requests', -}); diff --git a/backend/src/services/presigned_url_requests.ts b/backend/src/services/presigned_url_requests.ts new file mode 100644 index 0000000..1c40193 --- /dev/null +++ b/backend/src/services/presigned_url_requests.ts @@ -0,0 +1,6 @@ +import Presigned_url_requestsDBApi from '../db/api/presigned_url_requests.ts'; +import { createEntityService } from '../factories/service.factory.ts'; + +export default createEntityService(Presigned_url_requestsDBApi, { + entityName: 'presigned_url_requests', +}); diff --git a/backend/src/services/project_audio_tracks.js b/backend/src/services/project_audio_tracks.js deleted file mode 100644 index c98fa76..0000000 --- a/backend/src/services/project_audio_tracks.js +++ /dev/null @@ -1,171 +0,0 @@ -const db = require('../db/models'); -const Project_audio_tracksDBApi = require('../db/api/project_audio_tracks'); -const processFile = require('../middlewares/upload'); -const ValidationError = require('./notifications/errors/validation'); -const { - assertCreateOptions, - assertDeleteByIdsOptions, - assertIdOptions, - assertUpdateOptions, -} = require('../contracts/entity-options'); -const csv = require('csv-parser'); -const stream = require('stream'); - -module.exports = class Project_audio_tracksService { - static async create(options) { - assertCreateOptions(options, 'Service'); - const { - data, - currentUser, - transaction: externalTransaction, - runtimeContext, - } = options; - const transaction = - externalTransaction || (await db.sequelize.transaction()); - const ownsTransaction = !externalTransaction; - - try { - const createdTrack = await Project_audio_tracksDBApi.create({ - data, - currentUser, - transaction, - runtimeContext, - }); - - if (ownsTransaction) await transaction.commit(); - return createdTrack; - } catch (error) { - if (ownsTransaction) await transaction.rollback(); - throw error; - } - } - - static async bulkImport(req, res) { - const transaction = await db.sequelize.transaction(); - - try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }); - - await Project_audio_tracksDBApi.bulkImport(results, { - transaction, - ignoreDuplicates: true, - validate: true, - currentUser: req.currentUser, - }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async update(options) { - assertUpdateOptions(options, 'Service'); - const { - id, - data, - currentUser, - transaction: externalTransaction, - runtimeContext, - } = options; - const transaction = - externalTransaction || (await db.sequelize.transaction()); - const ownsTransaction = !externalTransaction; - - try { - let project_audio_tracks = await Project_audio_tracksDBApi.findBy( - { id }, - { transaction, runtimeContext }, - ); - - if (!project_audio_tracks) { - throw new ValidationError('project_audio_tracksNotFound'); - } - - const updatedProject_audio_tracks = - await Project_audio_tracksDBApi.update({ - id, - data, - currentUser, - transaction, - runtimeContext, - }); - - if (ownsTransaction) await transaction.commit(); - return updatedProject_audio_tracks; - } catch (error) { - if (ownsTransaction) await transaction.rollback(); - throw error; - } - } - - static async deleteByIds(options) { - assertDeleteByIdsOptions(options, 'Service'); - const { - ids, - currentUser, - transaction: externalTransaction, - runtimeContext, - } = options; - const transaction = - externalTransaction || (await db.sequelize.transaction()); - const ownsTransaction = !externalTransaction; - - try { - await Project_audio_tracksDBApi.deleteByIds({ - ids, - currentUser, - transaction, - runtimeContext, - }); - - if (ownsTransaction) await transaction.commit(); - } catch (error) { - if (ownsTransaction) await transaction.rollback(); - throw error; - } - } - - static async remove(options) { - assertIdOptions(options, 'Service', 'remove'); - const { - id, - currentUser, - transaction: externalTransaction, - runtimeContext, - } = options; - const transaction = - externalTransaction || (await db.sequelize.transaction()); - const ownsTransaction = !externalTransaction; - - try { - await Project_audio_tracksDBApi.remove({ - id, - currentUser, - transaction, - runtimeContext, - }); - - if (ownsTransaction) await transaction.commit(); - } catch (error) { - if (ownsTransaction) await transaction.rollback(); - throw error; - } - } -}; diff --git a/backend/src/services/project_audio_tracks.ts b/backend/src/services/project_audio_tracks.ts new file mode 100644 index 0000000..2e81624 --- /dev/null +++ b/backend/src/services/project_audio_tracks.ts @@ -0,0 +1,295 @@ +import csv from 'csv-parser'; +import { PassThrough } from 'stream'; + +import db from '../db/models/index.ts'; +import ProjectAudioTracksDBApi from '../db/api/project_audio_tracks.ts'; +import processFile from '../middlewares/upload.ts'; +import ValidationError from './notifications/errors/validation.ts'; +import { + assertCreateOptions, + assertDeleteByIdsOptions, + assertIdOptions, + assertUpdateOptions, +} from '../contracts/entity-options.ts'; +import { logger } from '../utils/logger.ts'; +import { getCurrentUser } from '../utils/request-context.ts'; +import type { + ProjectAudioTrackBulkImportRequest, + ProjectAudioTrackBulkImportResponse, + ProjectAudioTrackCreateOptions, + ProjectAudioTrackCsvRow, + ProjectAudioTrackDeleteByIdsOptions, + ProjectAudioTrackRecord, + ProjectAudioTrackRemoveOptions, + ProjectAudioTrackUpdateOptions, + RuntimeContext, +} from '../types/index.ts'; +import type { CurrentUser } from '../types/auth.ts'; +import type { Transaction } from 'sequelize'; + +interface ProjectAudioTrackContextInput { + currentUser: CurrentUser | null | undefined; + transaction: Transaction; + runtimeContext: RuntimeContext | undefined; +} + +interface ProjectAudioTrackContextOptions { + transaction: Transaction; + currentUser?: CurrentUser | null; + runtimeContext?: RuntimeContext; +} + +interface ProjectAudioTrackTransactionInput { + transaction: Transaction; + runtimeContext: RuntimeContext | undefined; +} + +interface ProjectAudioTrackTransactionOptions { + transaction: Transaction; + runtimeContext?: RuntimeContext; +} + +const buildContextOptions = ({ + currentUser, + transaction, + runtimeContext, +}: ProjectAudioTrackContextInput): ProjectAudioTrackContextOptions => { + const options: ProjectAudioTrackContextOptions = { transaction }; + if (currentUser !== undefined) { + options.currentUser = currentUser; + } + if (runtimeContext !== undefined) { + options.runtimeContext = runtimeContext; + } + return options; +}; + +const buildTransactionOptions = ({ + transaction, + runtimeContext, +}: ProjectAudioTrackTransactionInput): ProjectAudioTrackTransactionOptions => { + const options: ProjectAudioTrackTransactionOptions = { transaction }; + if (runtimeContext !== undefined) { + options.runtimeContext = runtimeContext; + } + return options; +}; + +const parseCsvRows = (buffer: Buffer): Promise => { + const bufferStream = new PassThrough(); + const results: ProjectAudioTrackCsvRow[] = []; + + bufferStream.end(buffer); + + return new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data: ProjectAudioTrackCsvRow) => { + results.push(data); + }) + .on('end', () => { + resolve(results); + }) + .on('error', reject); + }); +}; + +export default class ProjectAudioTracksService { + static async create( + options: ProjectAudioTrackCreateOptions, + ): Promise { + assertCreateOptions(options, 'Service'); + const { + data, + currentUser, + transaction: externalTransaction, + runtimeContext, + } = options; + const transaction = + externalTransaction || (await db.sequelize.transaction()); + const ownsTransaction = !externalTransaction; + + try { + const contextOptions = buildContextOptions({ + currentUser, + transaction, + runtimeContext, + }); + const createdTrack = await ProjectAudioTracksDBApi.create({ + data, + ...contextOptions, + }); + + if (ownsTransaction) { + await transaction.commit(); + } + return createdTrack; + } catch (error) { + if (ownsTransaction) { + await transaction.rollback(); + } + throw error; + } + } + + static async bulkImport( + req: ProjectAudioTrackBulkImportRequest, + res: ProjectAudioTrackBulkImportResponse, + ): Promise { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + + if (!req.file) { + throw new ValidationError('errors.validation.message'); + } + + const results = await parseCsvRows(req.file.buffer); + + logger.debug( + { rows: results.length }, + 'Project audio tracks CSV parsed', + ); + + const bulkImportOptions = buildContextOptions({ + currentUser: getCurrentUser(req), + transaction, + runtimeContext: undefined, + }); + + await ProjectAudioTracksDBApi.bulkImport(results, { + ...bulkImportOptions, + ignoreDuplicates: true, + validate: true, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update( + options: ProjectAudioTrackUpdateOptions, + ): Promise { + assertUpdateOptions(options, 'Service'); + const { + id, + data, + currentUser, + transaction: externalTransaction, + runtimeContext, + } = options; + const transaction = + externalTransaction || (await db.sequelize.transaction()); + const ownsTransaction = !externalTransaction; + + try { + const transactionOptions = buildTransactionOptions({ + transaction, + runtimeContext, + }); + const projectAudioTrack = await ProjectAudioTracksDBApi.findBy( + { id }, + transactionOptions, + ); + + if (!projectAudioTrack) { + throw new ValidationError('project_audio_tracksNotFound'); + } + + const contextOptions = buildContextOptions({ + currentUser, + transaction, + runtimeContext, + }); + const updatedProjectAudioTrack = await ProjectAudioTracksDBApi.update({ + id, + data, + ...contextOptions, + }); + + if (ownsTransaction) { + await transaction.commit(); + } + return updatedProjectAudioTrack; + } catch (error) { + if (ownsTransaction) { + await transaction.rollback(); + } + throw error; + } + } + + static async deleteByIds( + options: ProjectAudioTrackDeleteByIdsOptions, + ): Promise { + assertDeleteByIdsOptions(options, 'Service'); + const { + ids, + currentUser, + transaction: externalTransaction, + runtimeContext, + } = options; + const transaction = + externalTransaction || (await db.sequelize.transaction()); + const ownsTransaction = !externalTransaction; + + try { + const contextOptions = buildContextOptions({ + currentUser, + transaction, + runtimeContext, + }); + await ProjectAudioTracksDBApi.deleteByIds({ + ids, + ...contextOptions, + }); + + if (ownsTransaction) { + await transaction.commit(); + } + } catch (error) { + if (ownsTransaction) { + await transaction.rollback(); + } + throw error; + } + } + + static async remove(options: ProjectAudioTrackRemoveOptions): Promise { + assertIdOptions(options, 'Service', 'remove'); + const { + id, + currentUser, + transaction: externalTransaction, + runtimeContext, + } = options; + const transaction = + externalTransaction || (await db.sequelize.transaction()); + const ownsTransaction = !externalTransaction; + + try { + const contextOptions = buildContextOptions({ + currentUser, + transaction, + runtimeContext, + }); + await ProjectAudioTracksDBApi.remove({ + id, + ...contextOptions, + }); + + if (ownsTransaction) { + await transaction.commit(); + } + } catch (error) { + if (ownsTransaction) { + await transaction.rollback(); + } + throw error; + } + } +} diff --git a/backend/src/services/project_element_defaults.js b/backend/src/services/project_element_defaults.js deleted file mode 100644 index 73a6164..0000000 --- a/backend/src/services/project_element_defaults.js +++ /dev/null @@ -1,34 +0,0 @@ -const Project_element_defaultsDBApi = require('../db/api/project_element_defaults'); -const { createEntityService } = require('../factories/service.factory'); - -const BaseService = createEntityService(Project_element_defaultsDBApi, { - entityName: 'project_element_defaults', -}); - -class Project_element_defaultsService extends BaseService { - /** - * Reset a project element default to the current global default - */ - static async resetToGlobal(id, options = {}) { - return Project_element_defaultsDBApi.resetToGlobal(id, options); - } - - /** - * Get diff between project default and current global default - */ - static async getDiffFromGlobal(id) { - return Project_element_defaultsDBApi.getDiffFromGlobal(id); - } - - /** - * Snapshot all global element defaults to a project - */ - static async snapshotGlobalDefaults(projectId, options = {}) { - return Project_element_defaultsDBApi.snapshotGlobalDefaults( - projectId, - options, - ); - } -} - -module.exports = Project_element_defaultsService; diff --git a/backend/src/services/project_element_defaults.ts b/backend/src/services/project_element_defaults.ts new file mode 100644 index 0000000..022dfd6 --- /dev/null +++ b/backend/src/services/project_element_defaults.ts @@ -0,0 +1,34 @@ +import Project_element_defaultsDBApi from '../db/api/project_element_defaults.ts'; +import { createEntityService } from '../factories/service.factory.ts'; +import type { + ProjectElementDefaultRecord, + ProjectElementDefaultsDiff, + ProjectElementDefaultsOptions, +} from '../types/index.ts'; + +const BaseService = createEntityService(Project_element_defaultsDBApi, { + entityName: 'project_element_defaults', +}); + +export default class Project_element_defaultsService extends BaseService { + static resetToGlobal( + id: string, + options: ProjectElementDefaultsOptions = {}, + ): Promise { + return Project_element_defaultsDBApi.resetToGlobal(id, options); + } + + static getDiffFromGlobal(id: string): Promise { + return Project_element_defaultsDBApi.getDiffFromGlobal(id); + } + + static snapshotGlobalDefaults( + projectId: string, + options: ProjectElementDefaultsOptions = {}, + ): Promise { + return Project_element_defaultsDBApi.snapshotGlobalDefaults( + projectId, + options, + ); + } +} diff --git a/backend/src/services/project_memberships.js b/backend/src/services/project_memberships.js deleted file mode 100644 index 17ae3d7..0000000 --- a/backend/src/services/project_memberships.js +++ /dev/null @@ -1,6 +0,0 @@ -const Project_membershipsDBApi = require('../db/api/project_memberships'); -const { createEntityService } = require('../factories/service.factory'); - -module.exports = createEntityService(Project_membershipsDBApi, { - entityName: 'project_memberships', -}); diff --git a/backend/src/services/project_memberships.ts b/backend/src/services/project_memberships.ts new file mode 100644 index 0000000..cb0e7f7 --- /dev/null +++ b/backend/src/services/project_memberships.ts @@ -0,0 +1,6 @@ +import Project_membershipsDBApi from '../db/api/project_memberships.ts'; +import { createEntityService } from '../factories/service.factory.ts'; + +export default createEntityService(Project_membershipsDBApi, { + entityName: 'project_memberships', +}); diff --git a/backend/src/services/project_transition_settings.js b/backend/src/services/project_transition_settings.js deleted file mode 100644 index 2f8401e..0000000 --- a/backend/src/services/project_transition_settings.js +++ /dev/null @@ -1,209 +0,0 @@ -const db = require('../db/models'); -const Project_transition_settingsDBApi = require('../db/api/project_transition_settings'); -const processFile = require('../middlewares/upload'); -const ValidationError = require('./notifications/errors/validation'); -const { - assertCreateOptions, - assertDeleteByIdsOptions, - assertIdOptions, - assertUpdateOptions, -} = require('../contracts/entity-options'); -const csv = require('csv-parser'); -const stream = require('stream'); - -module.exports = class Project_transition_settingsService { - static async create(options) { - assertCreateOptions(options, 'Service'); - const { - data, - currentUser, - transaction: externalTransaction, - runtimeContext, - } = options; - const transaction = - externalTransaction || (await db.sequelize.transaction()); - const ownsTransaction = !externalTransaction; - - try { - const createdRecord = await Project_transition_settingsDBApi.create({ - data, - currentUser, - transaction, - runtimeContext, - }); - - if (ownsTransaction) await transaction.commit(); - return createdRecord; - } catch (error) { - if (ownsTransaction) await transaction.rollback(); - throw error; - } - } - - static async bulkImport(req, res) { - const transaction = await db.sequelize.transaction(); - - try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }); - - await Project_transition_settingsDBApi.bulkImport(results, { - transaction, - ignoreDuplicates: true, - validate: true, - currentUser: req.currentUser, - }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async update(options) { - assertUpdateOptions(options, 'Service'); - const { - id, - data, - currentUser, - transaction: externalTransaction, - runtimeContext, - } = options; - const transaction = - externalTransaction || (await db.sequelize.transaction()); - const ownsTransaction = !externalTransaction; - - try { - let record = await Project_transition_settingsDBApi.findBy( - { id }, - { transaction, runtimeContext }, - ); - - if (!record) { - throw new ValidationError('project_transition_settingsNotFound'); - } - - const updatedRecord = await Project_transition_settingsDBApi.update({ - id, - data, - currentUser, - transaction, - runtimeContext, - }); - - if (ownsTransaction) await transaction.commit(); - return updatedRecord; - } catch (error) { - if (ownsTransaction) await transaction.rollback(); - throw error; - } - } - - static async deleteByIds(options) { - assertDeleteByIdsOptions(options, 'Service'); - const { - ids, - currentUser, - transaction: externalTransaction, - runtimeContext, - } = options; - const transaction = - externalTransaction || (await db.sequelize.transaction()); - const ownsTransaction = !externalTransaction; - - try { - await Project_transition_settingsDBApi.deleteByIds({ - ids, - currentUser, - transaction, - runtimeContext, - }); - - if (ownsTransaction) await transaction.commit(); - } catch (error) { - if (ownsTransaction) await transaction.rollback(); - throw error; - } - } - - static async remove(options) { - assertIdOptions(options, 'Service', 'remove'); - const { - id, - currentUser, - transaction: externalTransaction, - runtimeContext, - } = options; - const transaction = - externalTransaction || (await db.sequelize.transaction()); - const ownsTransaction = !externalTransaction; - - try { - await Project_transition_settingsDBApi.remove({ - id, - currentUser, - transaction, - runtimeContext, - }); - - if (ownsTransaction) await transaction.commit(); - } catch (error) { - if (ownsTransaction) await transaction.rollback(); - throw error; - } - } - - /** - * Find settings by project ID and environment - */ - static async findByProjectAndEnvironment( - projectId, - environment, - currentUser, - ) { - return Project_transition_settingsDBApi.findByProjectAndEnvironment( - projectId, - environment, - { currentUser }, - ); - } - - /** - * Create or update settings for a project/environment combination - */ - static async upsertForProject(projectId, environment, data, currentUser) { - const transaction = await db.sequelize.transaction(); - try { - const result = await Project_transition_settingsDBApi.upsertForProject( - projectId, - environment, - data, - { - currentUser, - transaction, - }, - ); - - await transaction.commit(); - return result; - } catch (error) { - await transaction.rollback(); - throw error; - } - } -}; diff --git a/backend/src/services/project_transition_settings.ts b/backend/src/services/project_transition_settings.ts new file mode 100644 index 0000000..1e20ac8 --- /dev/null +++ b/backend/src/services/project_transition_settings.ts @@ -0,0 +1,342 @@ +import csv from 'csv-parser'; +import { PassThrough } from 'stream'; + +import db from '../db/models/index.ts'; +import ProjectTransitionSettingsDBApi from '../db/api/project_transition_settings.ts'; +import processFile from '../middlewares/upload.ts'; +import ValidationError from './notifications/errors/validation.ts'; +import { + assertCreateOptions, + assertDeleteByIdsOptions, + assertIdOptions, + assertUpdateOptions, +} from '../contracts/entity-options.ts'; +import { logger } from '../utils/logger.ts'; +import { getCurrentUser } from '../utils/request-context.ts'; +import type { + CurrentUser, + ProjectTransitionSettingsBulkImportRequest, + ProjectTransitionSettingsBulkImportResponse, + ProjectTransitionSettingsCreateOptions, + ProjectTransitionSettingsCsvRow, + ProjectTransitionSettingsData, + ProjectTransitionSettingsDeleteByIdsOptions, + ProjectTransitionSettingsRecord, + ProjectTransitionSettingsRemoveOptions, + ProjectTransitionSettingsUpdateOptions, + RuntimeContext, + RuntimeEnvironment, +} from '../types/index.ts'; +import type { Transaction } from 'sequelize'; + +interface ProjectTransitionSettingsContextInput { + currentUser: CurrentUser | null | undefined; + transaction: Transaction; + runtimeContext: RuntimeContext | undefined; +} + +interface ProjectTransitionSettingsContextOptions { + transaction: Transaction; + currentUser?: CurrentUser | null; + runtimeContext?: RuntimeContext; +} + +interface ProjectTransitionSettingsTransactionInput { + transaction: Transaction; + runtimeContext: RuntimeContext | undefined; +} + +interface ProjectTransitionSettingsTransactionOptions { + transaction: Transaction; + runtimeContext?: RuntimeContext; +} + +const buildContextOptions = ({ + currentUser, + transaction, + runtimeContext, +}: ProjectTransitionSettingsContextInput): ProjectTransitionSettingsContextOptions => { + const options: ProjectTransitionSettingsContextOptions = { transaction }; + if (currentUser !== undefined) { + options.currentUser = currentUser; + } + if (runtimeContext !== undefined) { + options.runtimeContext = runtimeContext; + } + return options; +}; + +const buildTransactionOptions = ({ + transaction, + runtimeContext, +}: ProjectTransitionSettingsTransactionInput): ProjectTransitionSettingsTransactionOptions => { + const options: ProjectTransitionSettingsTransactionOptions = { transaction }; + if (runtimeContext !== undefined) { + options.runtimeContext = runtimeContext; + } + return options; +}; + +const parseCsvRows = ( + buffer: Buffer, +): Promise => { + const bufferStream = new PassThrough(); + const results: ProjectTransitionSettingsCsvRow[] = []; + + bufferStream.end(buffer); + + return new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data: ProjectTransitionSettingsCsvRow) => { + results.push(data); + }) + .on('end', () => { + resolve(results); + }) + .on('error', reject); + }); +}; + +export default class ProjectTransitionSettingsService { + static async create( + options: ProjectTransitionSettingsCreateOptions, + ): Promise { + assertCreateOptions(options, 'Service'); + const { + data, + currentUser, + transaction: externalTransaction, + runtimeContext, + } = options; + const transaction = + externalTransaction || (await db.sequelize.transaction()); + const ownsTransaction = !externalTransaction; + + try { + const contextOptions = buildContextOptions({ + currentUser, + transaction, + runtimeContext, + }); + const createdRecord = await ProjectTransitionSettingsDBApi.create({ + data, + ...contextOptions, + }); + + if (ownsTransaction) { + await transaction.commit(); + } + return createdRecord; + } catch (error) { + if (ownsTransaction) { + await transaction.rollback(); + } + throw error; + } + } + + static async bulkImport( + req: ProjectTransitionSettingsBulkImportRequest, + res: ProjectTransitionSettingsBulkImportResponse, + ): Promise { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + + if (!req.file) { + throw new ValidationError('errors.validation.message'); + } + + const results = await parseCsvRows(req.file.buffer); + + logger.debug( + { rows: results.length }, + 'Project transition settings CSV parsed', + ); + + const bulkImportOptions = buildContextOptions({ + currentUser: getCurrentUser(req), + transaction, + runtimeContext: undefined, + }); + + await ProjectTransitionSettingsDBApi.bulkImport(results, { + ...bulkImportOptions, + ignoreDuplicates: true, + validate: true, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update( + options: ProjectTransitionSettingsUpdateOptions, + ): Promise { + assertUpdateOptions(options, 'Service'); + const { + id, + data, + currentUser, + transaction: externalTransaction, + runtimeContext, + } = options; + const transaction = + externalTransaction || (await db.sequelize.transaction()); + const ownsTransaction = !externalTransaction; + + try { + const transactionOptions = buildTransactionOptions({ + transaction, + runtimeContext, + }); + const record = await ProjectTransitionSettingsDBApi.findBy( + { id }, + transactionOptions, + ); + + if (!record) { + throw new ValidationError('project_transition_settingsNotFound'); + } + + const contextOptions = buildContextOptions({ + currentUser, + transaction, + runtimeContext, + }); + const updatedRecord = await ProjectTransitionSettingsDBApi.update({ + id, + data, + ...contextOptions, + }); + + if (ownsTransaction) { + await transaction.commit(); + } + return updatedRecord; + } catch (error) { + if (ownsTransaction) { + await transaction.rollback(); + } + throw error; + } + } + + static async deleteByIds( + options: ProjectTransitionSettingsDeleteByIdsOptions, + ): Promise { + assertDeleteByIdsOptions(options, 'Service'); + const { + ids, + currentUser, + transaction: externalTransaction, + runtimeContext, + } = options; + const transaction = + externalTransaction || (await db.sequelize.transaction()); + const ownsTransaction = !externalTransaction; + + try { + const contextOptions = buildContextOptions({ + currentUser, + transaction, + runtimeContext, + }); + await ProjectTransitionSettingsDBApi.deleteByIds({ + ids, + ...contextOptions, + }); + + if (ownsTransaction) { + await transaction.commit(); + } + } catch (error) { + if (ownsTransaction) { + await transaction.rollback(); + } + throw error; + } + } + + static async remove( + options: ProjectTransitionSettingsRemoveOptions, + ): Promise { + assertIdOptions(options, 'Service', 'remove'); + const { + id, + currentUser, + transaction: externalTransaction, + runtimeContext, + } = options; + const transaction = + externalTransaction || (await db.sequelize.transaction()); + const ownsTransaction = !externalTransaction; + + try { + const contextOptions = buildContextOptions({ + currentUser, + transaction, + runtimeContext, + }); + await ProjectTransitionSettingsDBApi.remove({ + id, + ...contextOptions, + }); + + if (ownsTransaction) { + await transaction.commit(); + } + } catch (error) { + if (ownsTransaction) { + await transaction.rollback(); + } + throw error; + } + } + + static async findByProjectAndEnvironment( + projectId: string, + environment: RuntimeEnvironment, + currentUser?: CurrentUser | null, + ): Promise { + const options = currentUser === undefined ? {} : { currentUser }; + return ProjectTransitionSettingsDBApi.findByProjectAndEnvironment( + projectId, + environment, + options, + ); + } + + static async upsertForProject( + projectId: string, + environment: RuntimeEnvironment, + data: ProjectTransitionSettingsData, + currentUser?: CurrentUser | null, + ): Promise { + const transaction = await db.sequelize.transaction(); + try { + const contextOptions = buildContextOptions({ + currentUser, + transaction, + runtimeContext: undefined, + }); + const result = await ProjectTransitionSettingsDBApi.upsertForProject( + projectId, + environment, + data, + contextOptions, + ); + + await transaction.commit(); + return result; + } catch (error) { + await transaction.rollback(); + throw error; + } + } +} diff --git a/backend/src/services/project_ui_control_settings.js b/backend/src/services/project_ui_control_settings.js deleted file mode 100644 index 152a1a0..0000000 --- a/backend/src/services/project_ui_control_settings.js +++ /dev/null @@ -1,73 +0,0 @@ -const db = require('../db/models'); -const Project_ui_control_settingsDBApi = require('../db/api/project_ui_control_settings'); -const ValidationError = require('./notifications/errors/validation'); -const { assertIdOptions } = require('../contracts/entity-options'); - -module.exports = class Project_ui_control_settingsService { - static async findByProjectAndEnvironment( - projectId, - environment, - currentUser, - ) { - return Project_ui_control_settingsDBApi.findByProjectAndEnvironment( - projectId, - environment, - { currentUser }, - ); - } - - static async upsertForProject(projectId, environment, data, currentUser) { - const transaction = await db.sequelize.transaction(); - - try { - const result = await Project_ui_control_settingsDBApi.upsertForProject( - projectId, - environment, - data, - { currentUser, transaction }, - ); - - await transaction.commit(); - return result; - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async remove(options) { - assertIdOptions(options, 'Service', 'remove'); - const { - id, - currentUser, - transaction: externalTransaction, - runtimeContext, - } = options; - const transaction = - externalTransaction || (await db.sequelize.transaction()); - const ownsTransaction = !externalTransaction; - - try { - const record = await Project_ui_control_settingsDBApi.findBy( - { id }, - { transaction, runtimeContext }, - ); - - if (!record) { - throw new ValidationError('project_ui_control_settingsNotFound'); - } - - await Project_ui_control_settingsDBApi.remove({ - id, - currentUser, - transaction, - runtimeContext, - }); - - if (ownsTransaction) await transaction.commit(); - } catch (error) { - if (ownsTransaction) await transaction.rollback(); - throw error; - } - } -}; diff --git a/backend/src/services/project_ui_control_settings.ts b/backend/src/services/project_ui_control_settings.ts new file mode 100644 index 0000000..b6b7cc8 --- /dev/null +++ b/backend/src/services/project_ui_control_settings.ts @@ -0,0 +1,115 @@ +import db from '../db/models/index.ts'; +import Project_ui_control_settingsDBApi from '../db/api/project_ui_control_settings.ts'; +import ValidationError from './notifications/errors/validation.ts'; +import { assertIdOptions } from '../contracts/entity-options.ts'; +import type { + CurrentUser, + EntityIdOptions, + ProjectUiControlSettingsData, + ProjectUiControlSettingsRecord, + RuntimeEnvironment, + ServiceContext, +} from '../types/index.ts'; +import type { Transaction } from 'sequelize'; + +function buildTransactionOptions( + transaction: Transaction | undefined, +): { transaction?: Transaction } { + return transaction ? { transaction } : {}; +} + +function buildCurrentUserOptions( + currentUser: CurrentUser | null | undefined, +): Pick { + return currentUser ? { currentUser } : {}; +} + +function buildRuntimeContextOptions( + options: EntityIdOptions, +): Pick { + return options.runtimeContext + ? { runtimeContext: options.runtimeContext } + : {}; +} + +export default class Project_ui_control_settingsService { + static findByProjectAndEnvironment( + projectId: string, + environment: RuntimeEnvironment, + currentUser?: CurrentUser | null, + ): Promise { + return Project_ui_control_settingsDBApi.findByProjectAndEnvironment( + projectId, + environment, + buildCurrentUserOptions(currentUser), + ); + } + + static async upsertForProject( + projectId: string, + environment: RuntimeEnvironment, + data: ProjectUiControlSettingsData, + currentUser?: CurrentUser | null, + ): Promise { + const transaction = await db.sequelize.transaction(); + + try { + const result = await Project_ui_control_settingsDBApi.upsertForProject( + projectId, + environment, + data, + { + ...buildCurrentUserOptions(currentUser), + transaction, + }, + ); + + await transaction.commit(); + return result; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(options: unknown): Promise { + assertIdOptions(options, 'Service', 'remove'); + const { + id, + currentUser, + transaction: externalTransaction, + }: EntityIdOptions = options; + const transaction = + externalTransaction || (await db.sequelize.transaction()); + const ownsTransaction = !externalTransaction; + const transactionOptions = buildTransactionOptions(transaction); + const runtimeContextOptions = buildRuntimeContextOptions(options); + const currentUserOptions = buildCurrentUserOptions(currentUser); + + try { + const record = await Project_ui_control_settingsDBApi.findBy( + { id }, + { + ...transactionOptions, + ...runtimeContextOptions, + }, + ); + + if (!record) { + throw new ValidationError('project_ui_control_settingsNotFound'); + } + + await Project_ui_control_settingsDBApi.remove({ + id, + transaction, + ...currentUserOptions, + ...runtimeContextOptions, + }); + + if (ownsTransaction) await transaction.commit(); + } catch (error) { + if (ownsTransaction) await transaction.rollback(); + throw error; + } + } +} diff --git a/backend/src/services/projects.js b/backend/src/services/projects.js deleted file mode 100644 index b919c5c..0000000 --- a/backend/src/services/projects.js +++ /dev/null @@ -1,631 +0,0 @@ -const path = require('path'); -const { v4: uuidv4 } = require('uuid'); -const db = require('../db/models'); -const ProjectsDBApi = require('../db/api/projects'); -const { createEntityService } = require('../factories/service.factory'); -const { - assertCreateOptions, - assertUpdateOptions, -} = require('../contracts/entity-options'); -const ValidationError = require('./notifications/errors/validation'); -const FileService = require('./file'); -const { logger } = require('../utils/logger'); - -/** - * Transform asset paths in ui_schema_json recursively - * Handles nested objects and arrays. - * - * @param {Object|Array|string} uiSchema - The ui_schema_json object, array, or JSON string - * @param {Map} assetPathMap - Map of old storage paths to new paths - * @returns {Object|Array} Transformed ui_schema_json with updated asset paths - */ -function transformUiSchemaAssetPaths(uiSchema, assetPathMap) { - if (!uiSchema) return uiSchema; - - // Parse JSON string if needed (may be double-stringified from DB) - let data = uiSchema; - while (typeof data === 'string') { - try { - data = JSON.parse(data); - } catch { - // Not valid JSON, return as-is - return uiSchema; - } - } - - if (typeof data !== 'object') return data; - - // Clone to avoid mutating source - const result = Array.isArray(data) ? [...data] : { ...data }; - - // Known fields that contain asset storage paths - // Note: reverseVideoUrl is included - if not in assetPathMap, keeps original value - const assetFields = new Set([ - 'src', - 'mediaUrl', - 'imageUrl', - 'videoUrl', - 'audioUrl', - 'transitionVideoUrl', - 'reverseVideoUrl', - 'thumbnail', - 'storage_key', - 'iconUrl', - 'carouselPrevIconUrl', - 'carouselNextIconUrl', - 'galleryCarouselPrevIconUrl', - 'galleryCarouselNextIconUrl', - 'galleryCarouselBackIconUrl', - ]); - - for (const key of Object.keys(result)) { - const value = result[key]; - - if (assetFields.has(key) && typeof value === 'string' && value) { - // Map to new storage path if available - result[key] = assetPathMap.get(value) || value; - } else if (typeof value === 'object' && value !== null) { - // Recurse into nested objects/arrays - result[key] = transformUiSchemaAssetPaths(value, assetPathMap); - } - } - - return result; -} - -// Generate base service from factory -const BaseProjectsService = createEntityService(ProjectsDBApi, { - entityName: 'Projects', -}); - -/** - * Projects service with slug validation and cloning functionality - * Extends factory-generated service with custom project logic - */ -class ProjectsService extends BaseProjectsService { - /** - * Normalize slug to URL-safe format - */ - static normalizeSlug(value) { - return ( - String(value || 'project') - .toLowerCase() - .trim() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') || 'project' - ); - } - - /** - * Generate unique slug for cloning - */ - static async generateUniqueSlug(baseSlug, transaction) { - const normalizedBase = ProjectsService.normalizeSlug(baseSlug); - let counter = 0; - let uniqueSlug = null; - - while (uniqueSlug === null) { - const suffix = counter === 0 ? '-copy' : `-copy-${counter + 1}`; - const candidate = `${normalizedBase}${suffix}`; - - const existing = await db.projects.findOne({ - where: { slug: candidate }, - paranoid: false, - transaction, - }); - - if (!existing) { - uniqueSlug = candidate; - } else { - counter += 1; - } - } - - return uniqueSlug; - } - - /** - * Validate slug uniqueness - */ - static async validateSlugUniqueness(slug, excludeId, transaction) { - const normalizedSlug = ProjectsService.normalizeSlug(slug); - - const whereClause = { slug: normalizedSlug }; - if (excludeId) { - whereClause.id = { [db.Sequelize.Op.ne]: excludeId }; - } - - const existing = await db.projects.findOne({ - where: whereClause, - paranoid: false, - transaction, - }); - - if (existing) { - throw new ValidationError('iam.errors.slugAlreadyExists'); - } - - return normalizedSlug; - } - - /** - * Create project with slug validation - */ - static async create(options) { - assertCreateOptions(options, 'Service'); - const { - data, - currentUser, - transaction: externalTransaction, - runtimeContext, - } = options; - const transaction = - externalTransaction || (await db.sequelize.transaction()); - const ownsTransaction = !externalTransaction; - - try { - if (data.slug) { - data.slug = await ProjectsService.validateSlugUniqueness( - data.slug, - null, - transaction, - ); - } - - const project = await ProjectsDBApi.create({ - data, - currentUser, - transaction, - runtimeContext, - }); - - if (ownsTransaction) await transaction.commit(); - return project; - } catch (error) { - if (ownsTransaction) await transaction.rollback(); - throw error; - } - } - - /** - * Update project with slug validation - */ - static async update(options) { - assertUpdateOptions(options, 'Service'); - const { - id, - data, - currentUser, - transaction: externalTransaction, - runtimeContext, - } = options; - const transaction = - externalTransaction || (await db.sequelize.transaction()); - const ownsTransaction = !externalTransaction; - - try { - const project = await ProjectsDBApi.findBy( - { id }, - { transaction, runtimeContext }, - ); - - if (!project) { - throw new ValidationError('projectsNotFound'); - } - - if (data.slug && data.slug !== project.slug) { - data.slug = await ProjectsService.validateSlugUniqueness( - data.slug, - id, - transaction, - ); - } - - const updated = await ProjectsDBApi.update({ - id, - data, - currentUser, - transaction, - runtimeContext, - }); - - if (ownsTransaction) await transaction.commit(); - return updated; - } catch (error) { - if (ownsTransaction) await transaction.rollback(); - throw error; - } - } - - /** - * Clone project with all assets - */ - static async cloneFromProject(sourceProjectId, currentUser) { - const transaction = await db.sequelize.transaction(); - - try { - const sourceProject = await db.projects.findByPk(sourceProjectId, { - include: [ - { - model: db.assets, - as: 'assets_project', - required: false, - include: [ - { - model: db.asset_variants, - as: 'asset_variants_asset', - required: false, - }, - ], - }, - ], - transaction, - }); - - if (!sourceProject) { - throw new ValidationError('projectsNotFound'); - } - - const uniqueSlug = await ProjectsService.generateUniqueSlug( - sourceProject.slug, - transaction, - ); - - const clonedProject = await ProjectsDBApi.create({ - data: { - name: `${sourceProject.name} (Copy)`, - slug: uniqueSlug, - description: sourceProject.description, - logo_url: sourceProject.logo_url, - favicon_url: sourceProject.favicon_url, - og_image_url: sourceProject.og_image_url, - design_width: sourceProject.design_width, - design_height: sourceProject.design_height, - }, - currentUser, - transaction, - }); - - // ============================================ - // Phase B: Collect all copy operations - // ============================================ - const copyOperations = []; - - for (const sourceAsset of sourceProject.assets_project || []) { - if (sourceAsset.storage_key) { - const ext = path.extname(sourceAsset.storage_key || ''); - const newStorageKey = `assets/${clonedProject.id}/${uuidv4()}${ext}`; - - copyOperations.push({ - sourceKey: sourceAsset.storage_key, - destKey: newStorageKey, - contentType: sourceAsset.mime_type, - }); - } - - // Collect variant copy operations (skip 'reversed' - handled in Phase F with asset ID paths) - const variants = sourceAsset.asset_variants_asset || []; - for (const sourceVariant of variants) { - if (sourceVariant.variant_type === 'reversed') continue; // Handled in Phase F - if (sourceVariant.storage_key) { - const variantExt = path.extname(sourceVariant.storage_key); - const newVariantKey = `assets/${clonedProject.id}/${uuidv4()}${variantExt}`; - - copyOperations.push({ - sourceKey: sourceVariant.storage_key, - destKey: newVariantKey, - }); - } - } - } - - // ============================================ - // Phase C: Execute parallel copy - // ============================================ - logger.info( - { sourceProjectId, copyCount: copyOperations.length }, - 'Starting parallel file copy for project clone', - ); - - const { succeeded, failed } = await FileService.copyFilesParallel( - copyOperations, - { - concurrency: 10, - continueOnError: true, - }, - ); - - // ============================================ - // Phase D: Build assetPathMap from results - // ============================================ - const assetPathMap = new Map(); - for (const { sourceKey, destKey } of succeeded) { - assetPathMap.set(sourceKey, destKey); - } - // Fallback: failed copies use original path (project still functional) - for (const { sourceKey } of failed) { - assetPathMap.set(sourceKey, sourceKey); - } - - // ============================================ - // Phase E: Create asset/variant records, track ID mapping for reversed videos - // ============================================ - const assetIdMap = new Map(); // oldAssetId → newAssetId - - for (const sourceAsset of sourceProject.assets_project || []) { - const clonedAsset = await db.assets.create( - { - name: sourceAsset.name, - asset_type: sourceAsset.asset_type, - type: sourceAsset.type || 'general', - cdn_url: '', // Will be populated on first presigned URL request - storage_key: - assetPathMap.get(sourceAsset.storage_key) || - sourceAsset.storage_key, - mime_type: sourceAsset.mime_type, - size_mb: sourceAsset.size_mb, - width_px: sourceAsset.width_px, - height_px: sourceAsset.height_px, - duration_sec: sourceAsset.duration_sec, - frame_rate: sourceAsset.frame_rate, - checksum: sourceAsset.checksum, - is_public: sourceAsset.is_public, - projectId: clonedProject.id, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { transaction }, - ); - - // Track ID mapping for reversed video copying - assetIdMap.set(sourceAsset.id, clonedAsset.id); - - // Clone non-reversed variants (reversed handled in Phase F) - for (const sourceVariant of sourceAsset.asset_variants_asset || []) { - if (sourceVariant.variant_type === 'reversed') continue; // Handled in Phase F - - const variantStorageKey = - assetPathMap.get(sourceVariant.storage_key) || - sourceVariant.storage_key; - - await db.asset_variants.create( - { - variant_type: sourceVariant.variant_type, - cdn_url: '', - storage_key: variantStorageKey, - width_px: sourceVariant.width_px, - height_px: sourceVariant.height_px, - size_mb: sourceVariant.size_mb, - assetId: clonedAsset.id, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { transaction }, - ); - } - } - - // ============================================ - // Phase F: Copy reversed videos using asset ID mapping - // Reversed videos use pattern: assets/{assetId}/reversed.mp4 - // ============================================ - const reversedCopyOps = []; - for (const [oldAssetId, newAssetId] of assetIdMap) { - const oldReversedKey = `assets/${oldAssetId}/reversed.mp4`; - const newReversedKey = `assets/${newAssetId}/reversed.mp4`; - reversedCopyOps.push({ - sourceKey: oldReversedKey, - destKey: newReversedKey, - contentType: 'video/mp4', - }); - } - - if (reversedCopyOps.length > 0) { - logger.info( - { count: reversedCopyOps.length }, - 'Copying reversed videos for cloned assets', - ); - - const reversedResults = await FileService.copyFilesParallel( - reversedCopyOps, - { - concurrency: 10, - continueOnError: true, // Many assets won't have reversed videos - that's OK - }, - ); - - // Add successful reversed video copies to assetPathMap - for (const { sourceKey, destKey } of reversedResults.succeeded) { - assetPathMap.set(sourceKey, destKey); - } - - logger.info( - { - succeeded: reversedResults.succeeded.length, - failed: reversedResults.failed.length, - }, - 'Reversed video copy completed', - ); - } - - // Clone tour pages (dev environment only - stage/production are populated via publishing) - const sourcePages = await db.tour_pages.findAll({ - where: { projectId: sourceProjectId, environment: 'dev' }, - transaction, - }); - - for (const sourcePage of sourcePages) { - const pageData = sourcePage.toJSON(); - // Remove fields that should be regenerated - delete pageData.id; - delete pageData.createdAt; - delete pageData.updatedAt; - delete pageData.deletedAt; - delete pageData.deletedBy; - delete pageData.importHash; - - // Transform ui_schema_json asset paths (including reverseVideoUrl clearing) - if (pageData.ui_schema_json) { - pageData.ui_schema_json = transformUiSchemaAssetPaths( - pageData.ui_schema_json, - assetPathMap, - ); - } - - if (pageData.global_ui_controls_settings_json) { - pageData.global_ui_controls_settings_json = - transformUiSchemaAssetPaths( - pageData.global_ui_controls_settings_json, - assetPathMap, - ); - } - - // Transform background URLs to new storage keys - if (pageData.background_image_url) { - pageData.background_image_url = - assetPathMap.get(pageData.background_image_url) || - pageData.background_image_url; - } - if (pageData.background_video_url) { - pageData.background_video_url = - assetPathMap.get(pageData.background_video_url) || - pageData.background_video_url; - } - if (pageData.background_embed_url) { - pageData.background_embed_url = - assetPathMap.get(pageData.background_embed_url) || - pageData.background_embed_url; - } - if (pageData.background_audio_url) { - pageData.background_audio_url = - assetPathMap.get(pageData.background_audio_url) || - pageData.background_audio_url; - } - - await db.tour_pages.create( - { - ...pageData, - projectId: clonedProject.id, - environment: 'dev', - source_key: sourcePage.id, // Link back to original page - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { transaction }, - ); - } - - // Clone audio tracks (dev environment only) - const sourceAudioTracks = await db.project_audio_tracks.findAll({ - where: { projectId: sourceProjectId, environment: 'dev' }, - transaction, - }); - - for (const sourceTrack of sourceAudioTracks) { - const trackData = sourceTrack.toJSON(); - delete trackData.id; - delete trackData.createdAt; - delete trackData.updatedAt; - delete trackData.deletedAt; - delete trackData.deletedBy; - delete trackData.importHash; - - // Transform audio URL to new storage key - if (trackData.storage_key) { - trackData.storage_key = - assetPathMap.get(trackData.storage_key) || trackData.storage_key; - } - - await db.project_audio_tracks.create( - { - ...trackData, - projectId: clonedProject.id, - environment: 'dev', - source_key: sourceTrack.id, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { transaction }, - ); - } - - // Clone project element defaults - // Note: ProjectsDBApi.create() auto-creates defaults from global templates. - // We need to delete those and replace with source project's customized defaults. - await db.project_element_defaults.destroy({ - where: { projectId: clonedProject.id }, - transaction, - force: true, // Hard delete to avoid soft-delete conflicts - }); - - const sourceElementDefaults = await db.project_element_defaults.findAll({ - where: { projectId: sourceProjectId }, - transaction, - }); - - for (const sourceDefault of sourceElementDefaults) { - const defaultData = sourceDefault.toJSON(); - delete defaultData.id; - delete defaultData.createdAt; - delete defaultData.updatedAt; - delete defaultData.deletedAt; - delete defaultData.deletedBy; - delete defaultData.importHash; - - await db.project_element_defaults.create( - { - ...defaultData, - projectId: clonedProject.id, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { transaction }, - ); - } - - // Clone project UI control settings (dev environment only) - const sourceUiControlSettings = - await db.project_ui_control_settings.findOne({ - where: { projectId: sourceProjectId, environment: 'dev' }, - transaction, - }); - - if (sourceUiControlSettings) { - const settingsData = sourceUiControlSettings.toJSON(); - delete settingsData.id; - delete settingsData.createdAt; - delete settingsData.updatedAt; - delete settingsData.deletedAt; - delete settingsData.deletedBy; - delete settingsData.importHash; - - if (settingsData.settings_json) { - settingsData.settings_json = transformUiSchemaAssetPaths( - settingsData.settings_json, - assetPathMap, - ); - } - - await db.project_ui_control_settings.create( - { - ...settingsData, - projectId: clonedProject.id, - environment: 'dev', - source_key: sourceUiControlSettings.id, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { transaction }, - ); - } - - await transaction.commit(); - return clonedProject; - } catch (error) { - await transaction.rollback(); - throw error; - } - } -} - -module.exports = ProjectsService; diff --git a/backend/src/services/projects.ts b/backend/src/services/projects.ts new file mode 100644 index 0000000..d4a7d0f --- /dev/null +++ b/backend/src/services/projects.ts @@ -0,0 +1,811 @@ +import path from 'path'; +import { v4 as uuidv4 } from 'uuid'; +import type { Transaction } from 'sequelize'; + +import db from '../db/models/index.ts'; +import ProjectsDBApi from '../db/api/projects.ts'; +import { createEntityService } from '../factories/service.factory.ts'; +import { + assertCreateOptions, + assertUpdateOptions, +} from '../contracts/entity-options.ts'; +import ValidationError from './notifications/errors/validation.ts'; +import FileService from './file/index.ts'; +import { logger } from '../utils/logger.ts'; +import type { + CurrentUser, + FileCopyOperation, + ProjectCloneAssetRecord, + ProjectCloneCurrentUser, + ProjectCloneGenericPayload, + ProjectCloneJsonData, + ProjectCloneAssetPayload, + ProjectCloneVariantPayload, + ProjectCreateOptions, + ProjectData, + ProjectRecord, + ProjectSlugLookupWhere, + ProjectUpdateOptions, + RuntimeContext, +} from '../types/index.ts'; + +const ASSET_FIELDS = new Set([ + 'src', + 'mediaUrl', + 'imageUrl', + 'videoUrl', + 'audioUrl', + 'transitionVideoUrl', + 'reverseVideoUrl', + 'thumbnail', + 'storage_key', + 'iconUrl', + 'carouselPrevIconUrl', + 'carouselNextIconUrl', + 'galleryCarouselPrevIconUrl', + 'galleryCarouselNextIconUrl', + 'galleryCarouselBackIconUrl', +]); + +const buildWriteOptions = ( + transaction: Transaction, + runtimeContext: RuntimeContext | undefined, + currentUser: CurrentUser | null | undefined, +): { + transaction: Transaction; + runtimeContext?: RuntimeContext; + currentUser?: CurrentUser | null; +} => { + const options: { + transaction: Transaction; + runtimeContext?: RuntimeContext; + currentUser?: CurrentUser | null; + } = { transaction }; + + if (runtimeContext !== undefined) { + options.runtimeContext = runtimeContext; + } + if (currentUser !== undefined) { + options.currentUser = currentUser; + } + + return options; +}; + +const transformUiSchemaAssetPaths = ( + uiSchema: unknown, + assetPathMap: ReadonlyMap, +): unknown => { + if (!uiSchema) { + return uiSchema; + } + + let data: unknown = uiSchema; + while (typeof data === 'string') { + try { + data = JSON.parse(data); + } catch { + return uiSchema; + } + } + + if (Array.isArray(data)) { + return data.map((item) => transformUiSchemaAssetPaths(item, assetPathMap)); + } + + if (typeof data !== 'object' || data === null) { + return data; + } + + return Object.fromEntries( + Object.entries(data).map(([key, value]) => { + if (ASSET_FIELDS.has(key) && typeof value === 'string' && value) { + return [key, assetPathMap.get(value) || value]; + } + + if (typeof value === 'object' && value !== null) { + return [key, transformUiSchemaAssetPaths(value, assetPathMap)]; + } + + return [key, value]; + }), + ); +}; + +const deleteGeneratedFields = (data: ProjectCloneJsonData): void => { + delete data.id; + delete data.createdAt; + delete data.updatedAt; + delete data.deletedAt; + delete data.deletedBy; + delete data.importHash; +}; + +const readMappedString = ( + value: unknown, + assetPathMap: ReadonlyMap, +): unknown => { + if (typeof value !== 'string') { + return value; + } + return assetPathMap.get(value) || value; +}; + +const requireCloneActor = ( + currentUser: ProjectCloneCurrentUser | null | undefined, +): ProjectCloneCurrentUser => { + if (!currentUser?.id) { + throw new ValidationError('errors.forbidden.message'); + } + return currentUser; +}; + +const BaseProjectsService = createEntityService< + ProjectRecord, + ProjectData, + ProjectData +>(ProjectsDBApi, { + entityName: 'Projects', +}); + +export default class ProjectsService extends BaseProjectsService { + static normalizeSlug(value: string | null | undefined): string { + return ( + String(value || 'project') + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') || 'project' + ); + } + + static async generateUniqueSlug( + baseSlug: string, + transaction: Transaction, + ): Promise { + const normalizedBase = ProjectsService.normalizeSlug(baseSlug); + let counter = 0; + let uniqueSlug: string | null = null; + + while (uniqueSlug === null) { + const suffix = counter === 0 ? '-copy' : `-copy-${counter + 1}`; + const candidate = `${normalizedBase}${suffix}`; + + const existing = await db.projects.findOne({ + where: { slug: candidate }, + paranoid: false, + transaction, + }); + + if (!existing) { + uniqueSlug = candidate; + } else { + counter += 1; + } + } + + return uniqueSlug; + } + + static async validateSlugUniqueness( + slug: string, + excludeId: string | null, + transaction: Transaction, + ): Promise { + const normalizedSlug = ProjectsService.normalizeSlug(slug); + + const where: ProjectSlugLookupWhere = { slug: normalizedSlug }; + if (excludeId) { + where.id = { [db.Sequelize.Op.ne]: excludeId }; + } + + const existing = await db.projects.findOne({ + where, + paranoid: false, + transaction, + }); + + if (existing) { + throw new ValidationError('iam.errors.slugAlreadyExists'); + } + + return normalizedSlug; + } + + static override async create( + options: ProjectCreateOptions, + ): Promise { + assertCreateOptions(options, 'Service'); + const { + data, + currentUser, + transaction: externalTransaction, + runtimeContext, + } = options; + const transaction = + externalTransaction || (await db.sequelize.transaction()); + const ownsTransaction = !externalTransaction; + + try { + if (data.slug) { + data.slug = await ProjectsService.validateSlugUniqueness( + data.slug, + null, + transaction, + ); + } + + const project = await ProjectsDBApi.create({ + data, + ...buildWriteOptions(transaction, runtimeContext, currentUser), + }); + + if (ownsTransaction) { + await transaction.commit(); + } + return project; + } catch (error) { + if (ownsTransaction) { + await transaction.rollback(); + } + throw error; + } + } + + static override async update( + options: ProjectUpdateOptions, + ): Promise { + assertUpdateOptions(options, 'Service'); + const { + id, + data, + currentUser, + transaction: externalTransaction, + runtimeContext, + } = options; + const transaction = + externalTransaction || (await db.sequelize.transaction()); + const ownsTransaction = !externalTransaction; + const runtimeOptions = buildWriteOptions( + transaction, + runtimeContext, + currentUser, + ); + + try { + const project = await ProjectsDBApi.findBy({ id }, runtimeOptions); + + if (!project) { + throw new ValidationError('projectsNotFound'); + } + + if (data.slug && data.slug !== project.slug) { + data.slug = await ProjectsService.validateSlugUniqueness( + data.slug, + id, + transaction, + ); + } + + const updated = await ProjectsDBApi.update({ + id, + data, + ...runtimeOptions, + }); + + if (ownsTransaction) { + await transaction.commit(); + } + return updated; + } catch (error) { + if (ownsTransaction) { + await transaction.rollback(); + } + throw error; + } + } + + static async cloneFromProject( + sourceProjectId: string, + currentUser?: ProjectCloneCurrentUser | null, + ): Promise { + const actor = requireCloneActor(currentUser); + const transaction = await db.sequelize.transaction(); + + try { + const sourceProject = await db.projects.findByPk(sourceProjectId, { + include: [ + { + model: db.assets, + as: 'assets_project', + required: false, + include: [ + { + model: db.asset_variants, + as: 'asset_variants_asset', + required: false, + }, + ], + }, + ], + transaction, + }); + + if (!sourceProject) { + throw new ValidationError('projectsNotFound'); + } + + const uniqueSlug = await ProjectsService.generateUniqueSlug( + sourceProject.slug, + transaction, + ); + + const clonedProject = await ProjectsDBApi.create({ + data: this.buildClonedProjectData(sourceProject, uniqueSlug), + currentUser: actor, + transaction, + }); + + const copyOperations: FileCopyOperation[] = []; + + for (const sourceAsset of sourceProject.assets_project || []) { + if (sourceAsset.storage_key) { + const ext = path.extname(sourceAsset.storage_key); + const newStorageKey = `assets/${clonedProject.id}/${uuidv4()}${ext}`; + + const copyOperation: FileCopyOperation = { + sourceKey: sourceAsset.storage_key, + destKey: newStorageKey, + }; + if (sourceAsset.mime_type) { + copyOperation.contentType = sourceAsset.mime_type; + } + copyOperations.push(copyOperation); + } + + const variants = sourceAsset.asset_variants_asset || []; + for (const sourceVariant of variants) { + if (sourceVariant.variant_type === 'reversed') { + continue; + } + if (sourceVariant.storage_key) { + const variantExt = path.extname(sourceVariant.storage_key); + const newVariantKey = `assets/${clonedProject.id}/${uuidv4()}${variantExt}`; + + copyOperations.push({ + sourceKey: sourceVariant.storage_key, + destKey: newVariantKey, + }); + } + } + } + + logger.info( + { sourceProjectId, copyCount: copyOperations.length }, + 'Starting parallel file copy for project clone', + ); + + const { succeeded, failed } = await FileService.copyFilesParallel( + copyOperations, + { + concurrency: 10, + continueOnError: true, + }, + ); + + const assetPathMap = new Map(); + for (const { sourceKey, destKey } of succeeded) { + assetPathMap.set(sourceKey, destKey); + } + for (const { sourceKey } of failed) { + assetPathMap.set(sourceKey, sourceKey); + } + + const assetIdMap = new Map(); + + for (const sourceAsset of sourceProject.assets_project || []) { + const clonedAsset = await this.cloneAssetRecord( + sourceAsset, + clonedProject.id, + actor.id, + assetPathMap, + transaction, + ); + + assetIdMap.set(sourceAsset.id, clonedAsset.id); + + for (const sourceVariant of sourceAsset.asset_variants_asset || []) { + if (sourceVariant.variant_type === 'reversed') { + continue; + } + + const variantStorageKey = + typeof sourceVariant.storage_key === 'string' + ? assetPathMap.get(sourceVariant.storage_key) || + sourceVariant.storage_key + : sourceVariant.storage_key; + + await db.asset_variants.create( + this.buildVariantPayload( + sourceVariant.variant_type, + variantStorageKey, + sourceVariant.width_px, + sourceVariant.height_px, + sourceVariant.size_mb, + clonedAsset.id, + actor.id, + ), + { transaction }, + ); + } + } + + await this.copyReversedVideos(assetIdMap, assetPathMap); + await this.cloneTourPages( + sourceProjectId, + clonedProject.id, + actor.id, + assetPathMap, + transaction, + ); + await this.cloneAudioTracks( + sourceProjectId, + clonedProject.id, + actor.id, + assetPathMap, + transaction, + ); + await this.cloneElementDefaults( + sourceProjectId, + clonedProject.id, + actor.id, + transaction, + ); + await this.cloneUiControlSettings( + sourceProjectId, + clonedProject.id, + actor.id, + assetPathMap, + transaction, + ); + + await transaction.commit(); + return clonedProject; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + private static buildClonedProjectData( + sourceProject: ProjectRecord, + uniqueSlug: string, + ): ProjectData { + const data: ProjectData = { + name: `${sourceProject.name} (Copy)`, + slug: uniqueSlug, + }; + + if (sourceProject.description !== undefined) { + data.description = sourceProject.description; + } + if (sourceProject.logo_url !== undefined) { + data.logo_url = sourceProject.logo_url; + } + if (sourceProject.favicon_url !== undefined) { + data.favicon_url = sourceProject.favicon_url; + } + if (sourceProject.og_image_url !== undefined) { + data.og_image_url = sourceProject.og_image_url; + } + if (sourceProject.design_width !== undefined) { + data.design_width = sourceProject.design_width; + } + if (sourceProject.design_height !== undefined) { + data.design_height = sourceProject.design_height; + } + + return data; + } + + private static async cloneAssetRecord( + sourceAsset: ProjectCloneAssetRecord, + clonedProjectId: string, + actorId: string, + assetPathMap: ReadonlyMap, + transaction: Transaction, + ): Promise<{ id: string }> { + const storageKey = + typeof sourceAsset.storage_key === 'string' + ? assetPathMap.get(sourceAsset.storage_key) || sourceAsset.storage_key + : sourceAsset.storage_key; + + const payload: ProjectCloneAssetPayload = { + type: sourceAsset.type || 'general', + cdn_url: '', + projectId: clonedProjectId, + createdById: actorId, + updatedById: actorId, + }; + + if (sourceAsset.name !== undefined) payload.name = sourceAsset.name; + if (sourceAsset.asset_type !== undefined) { + payload.asset_type = sourceAsset.asset_type; + } + if (storageKey !== undefined) payload.storage_key = storageKey; + if (sourceAsset.mime_type !== undefined) { + payload.mime_type = sourceAsset.mime_type; + } + if (sourceAsset.size_mb !== undefined) payload.size_mb = sourceAsset.size_mb; + if (sourceAsset.width_px !== undefined) { + payload.width_px = sourceAsset.width_px; + } + if (sourceAsset.height_px !== undefined) { + payload.height_px = sourceAsset.height_px; + } + if (sourceAsset.duration_sec !== undefined) { + payload.duration_sec = sourceAsset.duration_sec; + } + if (sourceAsset.frame_rate !== undefined) { + payload.frame_rate = sourceAsset.frame_rate; + } + if (sourceAsset.checksum !== undefined) { + payload.checksum = sourceAsset.checksum; + } + if (sourceAsset.is_public !== undefined) { + payload.is_public = sourceAsset.is_public; + } + + return db.assets.create(payload, { transaction }); + } + + private static buildVariantPayload( + variantType: string, + storageKey: string | null | undefined, + widthPx: number | null | undefined, + heightPx: number | null | undefined, + sizeMb: number | null | undefined, + assetId: string, + actorId: string, + ): ProjectCloneVariantPayload { + const payload: ProjectCloneVariantPayload = { + variant_type: variantType, + cdn_url: '', + assetId, + createdById: actorId, + updatedById: actorId, + }; + + if (storageKey !== undefined) payload.storage_key = storageKey; + if (widthPx !== undefined) payload.width_px = widthPx; + if (heightPx !== undefined) payload.height_px = heightPx; + if (sizeMb !== undefined) payload.size_mb = sizeMb; + + return payload; + } + + private static async copyReversedVideos( + assetIdMap: ReadonlyMap, + assetPathMap: Map, + ): Promise { + const reversedCopyOps: FileCopyOperation[] = []; + for (const [oldAssetId, newAssetId] of assetIdMap) { + const oldReversedKey = `assets/${oldAssetId}/reversed.mp4`; + const newReversedKey = `assets/${newAssetId}/reversed.mp4`; + reversedCopyOps.push({ + sourceKey: oldReversedKey, + destKey: newReversedKey, + contentType: 'video/mp4', + }); + } + + if (reversedCopyOps.length === 0) { + return; + } + + logger.info( + { count: reversedCopyOps.length }, + 'Copying reversed videos for cloned assets', + ); + + const reversedResults = await FileService.copyFilesParallel( + reversedCopyOps, + { + concurrency: 10, + continueOnError: true, + }, + ); + + for (const { sourceKey, destKey } of reversedResults.succeeded) { + assetPathMap.set(sourceKey, destKey); + } + + logger.info( + { + succeeded: reversedResults.succeeded.length, + failed: reversedResults.failed.length, + }, + 'Reversed video copy completed', + ); + } + + private static async cloneTourPages( + sourceProjectId: string, + clonedProjectId: string, + actorId: string, + assetPathMap: ReadonlyMap, + transaction: Transaction, + ): Promise { + const sourcePages = await db.tour_pages.findAll({ + where: { projectId: sourceProjectId, environment: 'dev' }, + transaction, + }); + + for (const sourcePage of sourcePages) { + const pageData = sourcePage.toJSON(); + deleteGeneratedFields(pageData); + + if (pageData.ui_schema_json) { + pageData.ui_schema_json = transformUiSchemaAssetPaths( + pageData.ui_schema_json, + assetPathMap, + ); + } + + if (pageData.global_ui_controls_settings_json) { + pageData.global_ui_controls_settings_json = transformUiSchemaAssetPaths( + pageData.global_ui_controls_settings_json, + assetPathMap, + ); + } + + pageData.background_image_url = readMappedString( + pageData.background_image_url, + assetPathMap, + ); + pageData.background_video_url = readMappedString( + pageData.background_video_url, + assetPathMap, + ); + pageData.background_embed_url = readMappedString( + pageData.background_embed_url, + assetPathMap, + ); + pageData.background_audio_url = readMappedString( + pageData.background_audio_url, + assetPathMap, + ); + + await db.tour_pages.create( + this.buildClonePayload(pageData, clonedProjectId, actorId, { + environment: 'dev', + source_key: sourcePage.id, + }), + { transaction }, + ); + } + } + + private static async cloneAudioTracks( + sourceProjectId: string, + clonedProjectId: string, + actorId: string, + assetPathMap: ReadonlyMap, + transaction: Transaction, + ): Promise { + const sourceAudioTracks = await db.project_audio_tracks.findAll({ + where: { projectId: sourceProjectId, environment: 'dev' }, + transaction, + }); + + for (const sourceTrack of sourceAudioTracks) { + const trackData = sourceTrack.toJSON(); + deleteGeneratedFields(trackData); + + if (trackData.storage_key) { + trackData.storage_key = readMappedString( + trackData.storage_key, + assetPathMap, + ); + } + + await db.project_audio_tracks.create( + this.buildClonePayload(trackData, clonedProjectId, actorId, { + environment: 'dev', + source_key: sourceTrack.id, + }), + { transaction }, + ); + } + } + + private static async cloneElementDefaults( + sourceProjectId: string, + clonedProjectId: string, + actorId: string, + transaction: Transaction, + ): Promise { + await db.project_element_defaults.destroy({ + where: { projectId: clonedProjectId }, + transaction, + force: true, + }); + + const sourceElementDefaults = await db.project_element_defaults.findAll({ + where: { projectId: sourceProjectId }, + transaction, + }); + + for (const sourceDefault of sourceElementDefaults) { + const defaultData = sourceDefault.toJSON(); + deleteGeneratedFields(defaultData); + + await db.project_element_defaults.create( + this.buildClonePayload(defaultData, clonedProjectId, actorId), + { transaction }, + ); + } + } + + private static async cloneUiControlSettings( + sourceProjectId: string, + clonedProjectId: string, + actorId: string, + assetPathMap: ReadonlyMap, + transaction: Transaction, + ): Promise { + const sourceUiControlSettings = + await db.project_ui_control_settings.findOne({ + where: { projectId: sourceProjectId, environment: 'dev' }, + transaction, + }); + + if (!sourceUiControlSettings) { + return; + } + + const settingsData = sourceUiControlSettings.toJSON(); + deleteGeneratedFields(settingsData); + + if (settingsData.settings_json) { + settingsData.settings_json = transformUiSchemaAssetPaths( + settingsData.settings_json, + assetPathMap, + ); + } + + await db.project_ui_control_settings.create( + this.buildClonePayload(settingsData, clonedProjectId, actorId, { + environment: 'dev', + source_key: sourceUiControlSettings.id, + }), + { transaction }, + ); + } + + private static buildClonePayload( + data: ProjectCloneJsonData, + projectId: string, + actorId: string, + options?: { + environment?: 'dev'; + source_key?: string; + }, + ): ProjectCloneGenericPayload { + const payload: ProjectCloneGenericPayload = { + ...data, + projectId, + createdById: actorId, + updatedById: actorId, + }; + + if (options?.environment !== undefined) { + payload.environment = options.environment; + } + if (options?.source_key !== undefined) { + payload.source_key = options.source_key; + } + + return payload; + } +} diff --git a/backend/src/services/publish.js b/backend/src/services/publish.ts similarity index 54% rename from backend/src/services/publish.js rename to backend/src/services/publish.ts index 599eaa9..3b3e297 100644 --- a/backend/src/services/publish.js +++ b/backend/src/services/publish.ts @@ -1,19 +1,62 @@ -const db = require('../db/models'); +import type { Transaction } from 'sequelize'; -const EVENT_STATUS = { +import db from '../db/models/index.ts'; +import { logger } from '../utils/logger.ts'; +import type { + PublishCloneData, + PublishClonePayload, + PublishCloneSource, + PublishEventRecord, + PublishEventStatus, + PublishLockCallback, + PublishServiceCurrentUser, + PublishSourceEnvironment, + PublishSummary, + PublishTargetEnvironment, + PublishToProductionResult, + SaveToStageResult, +} from '../types/index.ts'; + +const EVENT_STATUS: Record< + 'QUEUED' | 'RUNNING' | 'SUCCESS' | 'FAILED', + PublishEventStatus +> = { QUEUED: 'queued', RUNNING: 'running', SUCCESS: 'success', FAILED: 'failed', }; -const ENVIRONMENT = { +const ENVIRONMENT: { + DEV: PublishSourceEnvironment; + STAGE: PublishSourceEnvironment & PublishTargetEnvironment; + PRODUCTION: PublishTargetEnvironment; +} = { DEV: 'dev', STAGE: 'stage', PRODUCTION: 'production', }; -const sanitizeRecordForClone = (modelInstance) => { +class PublishServiceError extends Error { + code: number; + + constructor(message: string, code: number) { + super(message); + this.name = 'PublishServiceError'; + this.code = code; + } +} + +const getErrorMessage = (error: unknown): string => { + if (error instanceof Error) { + return error.message; + } + return String(error); +}; + +const sanitizeRecordForClone = ( + modelInstance: PublishCloneSource, +): PublishCloneData => { const data = modelInstance.toJSON(); delete data.id; delete data.createdAt; @@ -22,20 +65,23 @@ const sanitizeRecordForClone = (modelInstance) => { delete data.deletedBy; delete data.importHash; - // Ensure JSON fields are objects, not strings (avoid double-encoding) - if (data.ui_schema_json && typeof data.ui_schema_json === 'string') { + // Avoid double-encoding JSON fields stored as string by legacy records. + if (typeof data.ui_schema_json === 'string') { try { data.ui_schema_json = JSON.parse(data.ui_schema_json); } catch { - // Keep as-is if parsing fails + // Keep as-is if parsing fails. } } return data; }; -module.exports = class PublishService { - static async withProjectPublishLock(projectId, callback) { +export default class PublishService { + static async withProjectPublishLock( + projectId: string, + callback: PublishLockCallback, + ): Promise { return db.sequelize.transaction(async (transaction) => { const project = await db.projects.findByPk(projectId, { transaction, @@ -43,9 +89,7 @@ module.exports = class PublishService { }); if (!project) { - const error = new Error('Project not found'); - error.code = 404; - throw error; + throw new PublishServiceError('Project not found', 404); } const runningEvent = await db.publish_events.findOne({ @@ -59,20 +103,24 @@ module.exports = class PublishService { }); if (runningEvent) { - const error = new Error('Publish is already running for this project'); - error.code = 400; - throw error; + throw new PublishServiceError( + 'Publish is already running for this project', + 400, + ); } return callback(transaction); }); } - static async publishToProduction(projectId, currentUser, title, description) { + static async publishToProduction( + projectId: string, + currentUser: PublishServiceCurrentUser | undefined, + title: string | null | undefined, + description: string | null | undefined, + ): Promise { if (!projectId) { - const error = new Error('projectId is required'); - error.code = 400; - throw error; + throw new PublishServiceError('projectId is required', 400); } const eventTitle = typeof title === 'string' ? title.trim() : ''; @@ -80,27 +128,24 @@ module.exports = class PublishService { typeof description === 'string' ? description.trim() : ''; if (!eventTitle) { - const error = new Error('title is required'); - error.code = 400; - throw error; + throw new PublishServiceError('title is required', 400); } if (!eventDescription) { - const error = new Error('description is required'); - error.code = 400; - throw error; + throw new PublishServiceError('description is required', 400); } + const actorId = currentUser?.id || null; const publishEvent = await db.publish_events.create({ projectId, - userId: currentUser?.id || null, + userId: actorId, title: eventTitle, description: eventDescription, from_environment: ENVIRONMENT.STAGE, to_environment: ENVIRONMENT.PRODUCTION, status: EVENT_STATUS.QUEUED, - createdById: currentUser?.id || null, - updatedById: currentUser?.id || null, + createdById: actorId, + updatedById: actorId, }); try { @@ -112,16 +157,12 @@ module.exports = class PublishService { started_at: new Date(), status: EVENT_STATUS.RUNNING, error_message: null, - updatedById: currentUser?.id || null, + updatedById: actorId, }, { transaction }, ); - return this.copyStageToProduction( - projectId, - currentUser, - transaction, - ); + return this.copyStageToProduction(projectId, currentUser, transaction); }, ); @@ -131,7 +172,7 @@ module.exports = class PublishService { pages_copied: summary.pages_copied, audios_copied: summary.audios_copied, error_message: null, - updatedById: currentUser?.id || null, + updatedById: actorId, }); return { @@ -143,79 +184,93 @@ module.exports = class PublishService { await publishEvent.update({ status: EVENT_STATUS.FAILED, finished_at: new Date(), - error_message: error.message, - updatedById: currentUser?.id || null, + error_message: getErrorMessage(error), + updatedById: actorId, }); throw error; } } - /** - * Save dev content to stage environment (non-blocking) - * Returns immediately, processing continues in background. - */ - static async saveToStage(projectId, currentUser) { + static async saveToStage( + projectId: string, + currentUser: PublishServiceCurrentUser | undefined, + ): Promise { if (!projectId) { - const error = new Error('projectId is required'); - error.code = 400; - throw error; + throw new PublishServiceError('projectId is required', 400); } + const actorId = currentUser?.id || null; const publishEvent = await db.publish_events.create({ projectId, - userId: currentUser?.id || null, + userId: actorId, title: 'Save to Stage', description: 'Copy dev content to stage environment', from_environment: ENVIRONMENT.DEV, to_environment: ENVIRONMENT.STAGE, status: EVENT_STATUS.QUEUED, - createdById: currentUser?.id || null, - updatedById: currentUser?.id || null, + createdById: actorId, + updatedById: actorId, }); - // Process in background - setImmediate(async () => { - try { - const summary = await this.withProjectPublishLock( - projectId, - async (transaction) => { - await publishEvent.update( - { - started_at: new Date(), - status: EVENT_STATUS.RUNNING, - updatedById: currentUser?.id || null, - }, - { transaction }, - ); - return this.copyDevToStage(projectId, currentUser, transaction); - }, - ); - - await publishEvent.update({ - status: EVENT_STATUS.SUCCESS, - finished_at: new Date(), - pages_copied: summary.pages_copied, - audios_copied: summary.audios_copied, - updatedById: currentUser?.id || null, - }); - } catch (error) { - await publishEvent.update({ - status: EVENT_STATUS.FAILED, - finished_at: new Date(), - error_message: error.message, - updatedById: currentUser?.id || null, - }); - console.error('[SaveToStage] Background error:', error); - } + setImmediate(() => { + this.processSaveToStage(projectId, currentUser, publishEvent).catch( + (error: unknown) => { + logger.error( + { err: error, projectId, publishEventId: publishEvent.id }, + 'Save to stage background job failed', + ); + }, + ); }); return { success: true, publishEventId: publishEvent.id }; } - /** - * Copy dev content to stage environment - */ - static async copyDevToStage(projectId, currentUser, transaction) { + private static async processSaveToStage( + projectId: string, + currentUser: PublishServiceCurrentUser | undefined, + publishEvent: PublishEventRecord, + ): Promise { + const actorId = currentUser?.id || null; + try { + const summary = await this.withProjectPublishLock( + projectId, + async (transaction) => { + await publishEvent.update( + { + started_at: new Date(), + status: EVENT_STATUS.RUNNING, + updatedById: actorId, + }, + { transaction }, + ); + return this.copyDevToStage(projectId, currentUser, transaction); + }, + ); + + await publishEvent.update({ + status: EVENT_STATUS.SUCCESS, + finished_at: new Date(), + pages_copied: summary.pages_copied, + audios_copied: summary.audios_copied, + updatedById: actorId, + }); + } catch (error) { + await publishEvent.update({ + status: EVENT_STATUS.FAILED, + finished_at: new Date(), + error_message: getErrorMessage(error), + updatedById: actorId, + }); + throw error; + } + } + + static async copyDevToStage( + projectId: string, + currentUser: PublishServiceCurrentUser | undefined, + transaction: Transaction, + ): Promise { return this.copyEnvironment( projectId, ENVIRONMENT.DEV, @@ -225,7 +280,11 @@ module.exports = class PublishService { ); } - static async copyStageToProduction(projectId, currentUser, transaction) { + static async copyStageToProduction( + projectId: string, + currentUser: PublishServiceCurrentUser | undefined, + transaction: Transaction, + ): Promise { return this.copyEnvironment( projectId, ENVIRONMENT.STAGE, @@ -235,28 +294,13 @@ module.exports = class PublishService { ); } - /** - * Generic method to copy content from one environment to another. - * Used for both dev->stage and stage->production flows. - * - * SIMPLIFIED: Now uses targetPageSlug for navigation which is consistent across environments. - * No ID remapping needed since slugs are the same in all environments. - * - * @param {string} projectId - Project ID - * @param {string} fromEnv - Source environment (dev, stage) - * @param {string} toEnv - Target environment (stage, production) - * @param {object} currentUser - Current user for audit fields - * @param {object} transaction - Sequelize transaction - * @returns {object} Summary of copied items - */ static async copyEnvironment( - projectId, - fromEnv, - toEnv, - currentUser, - transaction, - ) { - // Get source content + projectId: string, + fromEnv: PublishSourceEnvironment, + toEnv: PublishTargetEnvironment, + currentUser: PublishServiceCurrentUser | undefined, + transaction: Transaction, + ): Promise { const [ sourcePages, sourceAudioTracks, @@ -281,7 +325,6 @@ module.exports = class PublishService { }), ]); - // Clean up target environment (hard delete - paranoid models need force: true) await Promise.all([ db.tour_pages.destroy({ where: { projectId, environment: toEnv }, @@ -307,33 +350,28 @@ module.exports = class PublishService { const actorId = currentUser?.id || null; - // Create target pages - ui_schema_json uses targetPageSlug which is consistent across environments - const targetPagesPayload = sourcePages.map((sourcePage) => { - const data = sanitizeRecordForClone(sourcePage); - return { - ...data, + const targetPagesPayload: PublishClonePayload[] = sourcePages.map( + (sourcePage) => ({ + ...sanitizeRecordForClone(sourcePage), projectId, environment: toEnv, source_key: sourcePage.id, createdById: actorId, updatedById: actorId, - }; - }); + }), + ); - // Create target audio tracks - const targetAudioPayload = sourceAudioTracks.map((sourceAudio) => { - const data = sanitizeRecordForClone(sourceAudio); - return { - ...data, + const targetAudioPayload: PublishClonePayload[] = sourceAudioTracks.map( + (sourceAudio) => ({ + ...sanitizeRecordForClone(sourceAudio), projectId, environment: toEnv, source_key: sourceAudio.id, createdById: actorId, updatedById: actorId, - }; - }); + }), + ); - // Bulk create pages and audio tracks if (targetPagesPayload.length) { await db.tour_pages.bulkCreate(targetPagesPayload, { transaction, @@ -348,7 +386,6 @@ module.exports = class PublishService { }); } - // Create target transition settings (if source exists) if (sourceTransitionSettings) { const settingsData = sanitizeRecordForClone(sourceTransitionSettings); await db.project_transition_settings.create( @@ -386,4 +423,4 @@ module.exports = class PublishService { ui_control_settings_copied: sourceUiControlSettings ? 1 : 0, }; } -}; +} diff --git a/backend/src/services/publish_events.js b/backend/src/services/publish_events.js deleted file mode 100644 index 7175d50..0000000 --- a/backend/src/services/publish_events.js +++ /dev/null @@ -1,6 +0,0 @@ -const Publish_eventsDBApi = require('../db/api/publish_events'); -const { createEntityService } = require('../factories/service.factory'); - -module.exports = createEntityService(Publish_eventsDBApi, { - entityName: 'publish_events', -}); diff --git a/backend/src/services/publish_events.ts b/backend/src/services/publish_events.ts new file mode 100644 index 0000000..0bf49eb --- /dev/null +++ b/backend/src/services/publish_events.ts @@ -0,0 +1,6 @@ +import Publish_eventsDBApi from '../db/api/publish_events.ts'; +import { createEntityService } from '../factories/service.factory.ts'; + +export default createEntityService(Publish_eventsDBApi, { + entityName: 'publish_events', +}); diff --git a/backend/src/services/pwa_caches.js b/backend/src/services/pwa_caches.js deleted file mode 100644 index 940af4e..0000000 --- a/backend/src/services/pwa_caches.js +++ /dev/null @@ -1,6 +0,0 @@ -const Pwa_cachesDBApi = require('../db/api/pwa_caches'); -const { createEntityService } = require('../factories/service.factory'); - -module.exports = createEntityService(Pwa_cachesDBApi, { - entityName: 'pwa_caches', -}); diff --git a/backend/src/services/pwa_caches.ts b/backend/src/services/pwa_caches.ts new file mode 100644 index 0000000..ef468d8 --- /dev/null +++ b/backend/src/services/pwa_caches.ts @@ -0,0 +1,6 @@ +import Pwa_cachesDBApi from '../db/api/pwa_caches.ts'; +import { createEntityService } from '../factories/service.factory.ts'; + +export default createEntityService(Pwa_cachesDBApi, { + entityName: 'pwa_caches', +}); diff --git a/backend/src/services/roles.js b/backend/src/services/roles.js deleted file mode 100644 index 5c14b94..0000000 --- a/backend/src/services/roles.js +++ /dev/null @@ -1,393 +0,0 @@ -const db = require('../db/models'); -const RolesDBApi = require('../db/api/roles'); -const processFile = require('../middlewares/upload'); -const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); -const { validateReadOnlySql } = require('../utils/sqlValidator'); -const { logger } = require('../utils/logger'); -const { - assertCreateOptions, - assertDeleteByIdsOptions, - assertIdOptions, - assertUpdateOptions, -} = require('../contracts/entity-options'); - -const WIDGET_SQL_MAX_LENGTH = 5000; -const WIDGET_SQL_MAX_ROWS = 1000; -const WIDGET_SQL_TIMEOUT_MS = 5000; - -const validateWidgetSql = (sql) => { - const result = validateReadOnlySql(sql, { maxLength: WIDGET_SQL_MAX_LENGTH }); - if (!result.valid) { - throw new ValidationError(result.error); - } - return result.normalized; -}; - -const runSafeWidgetQuery = async (sql) => { - const normalized = validateWidgetSql(sql); - const wrappedSql = `SELECT * FROM (${normalized}) AS widget_query_result LIMIT ${WIDGET_SQL_MAX_ROWS}`; - - return db.sequelize.transaction(async (transaction) => { - await db.sequelize.query( - `SET LOCAL statement_timeout = ${WIDGET_SQL_TIMEOUT_MS}`, - { transaction }, - ); - return db.sequelize.query(wrappedSql, { transaction }); - }); -}; - -module.exports = class RolesService { - static assertPublicRoleHasNoPermissions(data, existingRole) { - const nextName = data?.name || existingRole?.name; - if (nextName !== 'Public') return; - - const permissions = Array.isArray(data?.permissions) - ? data.permissions.filter(Boolean) - : []; - - if (permissions.length > 0) { - throw new ValidationError('Public role cannot receive permissions'); - } - } - - static async create(options) { - assertCreateOptions(options, 'Service'); - const { - data, - currentUser, - transaction: externalTransaction, - runtimeContext, - } = options; - const transaction = - externalTransaction || (await db.sequelize.transaction()); - const ownsTransaction = !externalTransaction; - - try { - this.assertPublicRoleHasNoPermissions(data); - - const createdRole = await RolesDBApi.create({ - data, - currentUser, - transaction, - runtimeContext, - }); - - if (ownsTransaction) await transaction.commit(); - return createdRole; - } catch (error) { - if (ownsTransaction) await transaction.rollback(); - throw error; - } - } - - static async bulkImport(req, res) { - const transaction = await db.sequelize.transaction(); - - try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - logger.info( - { count: results.length }, - 'Parsed role CSV import rows', - ); - resolve(); - }) - .on('error', (error) => reject(error)); - }); - - await RolesDBApi.bulkImport(results, { - transaction, - ignoreDuplicates: true, - validate: true, - currentUser: req.currentUser, - }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async update(options) { - assertUpdateOptions(options, 'Service'); - const { - id, - data, - currentUser, - transaction: externalTransaction, - runtimeContext, - } = options; - const transaction = - externalTransaction || (await db.sequelize.transaction()); - const ownsTransaction = !externalTransaction; - - try { - let roles = await RolesDBApi.findBy( - { id }, - { transaction, runtimeContext }, - ); - - if (!roles) { - throw new ValidationError('rolesNotFound'); - } - - this.assertPublicRoleHasNoPermissions(data, roles); - - const updatedRoles = await RolesDBApi.update({ - id, - data, - currentUser, - transaction, - runtimeContext, - }); - - if (ownsTransaction) await transaction.commit(); - return updatedRoles; - } catch (error) { - if (ownsTransaction) await transaction.rollback(); - throw error; - } - } - - static async deleteByIds(options) { - assertDeleteByIdsOptions(options, 'Service'); - const { - ids, - currentUser, - transaction: externalTransaction, - runtimeContext, - } = options; - const transaction = - externalTransaction || (await db.sequelize.transaction()); - const ownsTransaction = !externalTransaction; - - try { - await RolesDBApi.deleteByIds({ - ids, - currentUser, - transaction, - runtimeContext, - }); - - if (ownsTransaction) await transaction.commit(); - } catch (error) { - if (ownsTransaction) await transaction.rollback(); - throw error; - } - } - - static async remove(options) { - assertIdOptions(options, 'Service', 'remove'); - const { - id, - currentUser, - transaction: externalTransaction, - runtimeContext, - } = options; - const transaction = - externalTransaction || (await db.sequelize.transaction()); - const ownsTransaction = !externalTransaction; - - try { - await RolesDBApi.remove({ - id, - currentUser, - transaction, - runtimeContext, - }); - - if (ownsTransaction) await transaction.commit(); - } catch (error) { - if (ownsTransaction) await transaction.rollback(); - throw error; - } - } - - static async addRoleInfo(roleId, userId, key, widgetId, currentUser) { - const regexExpForUuid = - /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi; - const widgetIdIsUUID = regexExpForUuid.test(widgetId); - - const transaction = await db.sequelize.transaction(); - let role; - if (roleId) { - role = await RolesDBApi.findBy({ id: roleId }, { transaction }); - } else { - role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); - } - - if (!role) { - throw new ValidationError('rolesNotFound'); - } - - try { - let customization = {}; - try { - customization = JSON.parse(role.role_customization || '{}'); - } catch (e) { - logger.warn( - { err: e, roleId: role.id }, - 'Failed to parse role customization JSON', - ); - } - - if (widgetIdIsUUID && Array.isArray(customization[key])) { - const el = customization[key].find((e) => e === widgetId); - !el ? customization[key].unshift(widgetId) : null; - } - - if (widgetIdIsUUID && !customization[key]) { - customization[key] = [widgetId]; - } - - const newRole = await RolesDBApi.update({ - id: role.id, - data: { - role_customization: JSON.stringify(customization), - name: role.name, - permissions: role.permissions, - }, - currentUser, - transaction, - }); - - await transaction.commit(); - - return newRole; - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async removeRoleInfoById(infoId, roleId, key, currentUser) { - const transaction = await db.sequelize.transaction(); - - let role; - if (roleId) { - role = await RolesDBApi.findBy({ id: roleId }, { transaction }); - } else { - role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); - } - if (!role) { - await transaction.rollback(); - throw new ValidationError('rolesNotFound'); - } - - let customization = {}; - try { - customization = JSON.parse(role.role_customization || '{}'); - } catch (e) { - logger.warn( - { err: e, roleId: role.id }, - 'Failed to parse role customization JSON', - ); - } - - customization[key] = customization[key].filter((item) => item !== infoId); - - await axios.delete( - `${config.flHost}/${config.project_uuid}/project_customization_widgets/${infoId}.json`, - ); - try { - const result = await RolesDBApi.update({ - id: role.id, - data: { - role_customization: JSON.stringify(customization), - name: role.name, - permissions: role.permissions, - }, - currentUser, - transaction, - }); - - await transaction.commit(); - return result; - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async getRoleInfoByKey(key, roleId) { - const transaction = await db.sequelize.transaction(); - - let role; - try { - if (roleId) { - role = await RolesDBApi.findBy({ id: roleId }, { transaction }); - } else { - role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); - } - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - - if (!role) { - throw new ValidationError('Role not found'); - } - - let customization = '{}'; - - try { - customization = JSON.parse(role.role_customization || '{}'); - } catch (e) { - logger.error( - { err: e, roleId: role.id }, - 'Failed to parse role customization JSON', - ); - throw e; - } - - if (key === 'widgets') { - const widgets = customization[key] || []; - const widgetArray = widgets.map((widget) => { - return axios.get( - `${config.flHost}/${config.project_uuid}/project_customization_widgets/${widget}.json`, - ); - }); - const widgetResults = await Promise.allSettled(widgetArray); - - const fulfilledWidgets = widgetResults - .filter((result) => result.status === 'fulfilled') - .map((result) => result.value.data); - - const widgetsResults = []; - - if (Array.isArray(fulfilledWidgets)) { - for (const widget of fulfilledWidgets) { - const result = await runSafeWidgetQuery(widget.query); - - if (result[0] && result[0].length) { - const key = Object.keys(result[0][0])[0]; - const value = - widget.widget_type === 'scalar' ? result[0][0][key] : result[0]; - const widgetData = JSON.parse(widget.data); - widgetsResults.push({ ...widget, ...widgetData, value }); - } else { - widgetsResults.push({ ...widget, value: null }); - } - } - } - return widgetsResults; - } - return customization[key]; - } -}; diff --git a/backend/src/services/roles.ts b/backend/src/services/roles.ts new file mode 100644 index 0000000..c6822a2 --- /dev/null +++ b/backend/src/services/roles.ts @@ -0,0 +1,306 @@ +import csv from 'csv-parser'; +import { PassThrough } from 'stream'; +import type { Transaction } from 'sequelize'; + +import db from '../db/models/index.ts'; +import RolesDBApi from '../db/api/roles.ts'; +import processFile from '../middlewares/upload.ts'; +import ValidationError from './notifications/errors/validation.ts'; +import { logger } from '../utils/logger.ts'; +import { getCurrentUser } from '../utils/request-context.ts'; +import { + assertCreateOptions, + assertDeleteByIdsOptions, + assertIdOptions, + assertUpdateOptions, +} from '../contracts/entity-options.ts'; +import type { + CurrentUser, + RoleBulkImportRequest, + RoleBulkImportResponse, + RoleCreateOptions, + RoleCsvRow, + RoleData, + RoleDeleteByIdsOptions, + RoleRemoveOptions, + RoleServiceRecord, + RoleUpdateOptions, + RuntimeContext, +} from '../types/index.ts'; + +interface RoleContextInput { + currentUser: CurrentUser | null | undefined; + transaction: Transaction; + runtimeContext: RuntimeContext | undefined; +} + +interface RoleContextOptions { + transaction: Transaction; + currentUser?: CurrentUser | null; + runtimeContext?: RuntimeContext; +} + +interface RoleTransactionInput { + transaction: Transaction; + runtimeContext: RuntimeContext | undefined; +} + +interface RoleTransactionOptions { + transaction: Transaction; + runtimeContext?: RuntimeContext; +} + +const buildContextOptions = ({ + currentUser, + transaction, + runtimeContext, +}: RoleContextInput): RoleContextOptions => { + const options: RoleContextOptions = { transaction }; + if (currentUser !== undefined) { + options.currentUser = currentUser; + } + if (runtimeContext !== undefined) { + options.runtimeContext = runtimeContext; + } + return options; +}; + +const buildTransactionOptions = ({ + transaction, + runtimeContext, +}: RoleTransactionInput): RoleTransactionOptions => { + const options: RoleTransactionOptions = { transaction }; + if (runtimeContext !== undefined) { + options.runtimeContext = runtimeContext; + } + return options; +}; + +const parseCsvRows = (buffer: Buffer): Promise => { + const bufferStream = new PassThrough(); + const results: RoleCsvRow[] = []; + + bufferStream.end(buffer); + + return new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data: RoleCsvRow) => { + results.push(data); + }) + .on('end', () => { + resolve(results); + }) + .on('error', reject); + }); +}; + +export default class RolesService { + static assertPublicRoleHasNoPermissions( + data: RoleData | undefined, + existingRole?: RoleServiceRecord | null, + ): void { + const nextName = data?.name || existingRole?.name; + if (nextName !== 'Public') { + return; + } + + const permissions = Array.isArray(data?.permissions) + ? data.permissions.filter(Boolean) + : []; + + if (permissions.length > 0) { + throw new ValidationError('Public role cannot receive permissions'); + } + } + + static async create(options: RoleCreateOptions): Promise { + assertCreateOptions(options, 'Service'); + const { + data, + currentUser, + transaction: externalTransaction, + runtimeContext, + } = options; + const transaction = + externalTransaction || (await db.sequelize.transaction()); + const ownsTransaction = !externalTransaction; + + try { + this.assertPublicRoleHasNoPermissions(data); + + const contextOptions = buildContextOptions({ + currentUser, + transaction, + runtimeContext, + }); + const createdRole = await RolesDBApi.create({ + data, + ...contextOptions, + }); + + if (ownsTransaction) { + await transaction.commit(); + } + return createdRole; + } catch (error) { + if (ownsTransaction) { + await transaction.rollback(); + } + throw error; + } + } + + static async bulkImport( + req: RoleBulkImportRequest, + res: RoleBulkImportResponse, + ): Promise { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + + if (!req.file) { + throw new ValidationError('errors.validation.message'); + } + + const results = await parseCsvRows(req.file.buffer); + + logger.info({ count: results.length }, 'Parsed role CSV import rows'); + + const bulkImportOptions = buildContextOptions({ + currentUser: getCurrentUser(req), + transaction, + runtimeContext: undefined, + }); + + await RolesDBApi.bulkImport(results, { + ...bulkImportOptions, + ignoreDuplicates: true, + validate: true, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(options: RoleUpdateOptions): Promise { + assertUpdateOptions(options, 'Service'); + const { + id, + data, + currentUser, + transaction: externalTransaction, + runtimeContext, + } = options; + const transaction = + externalTransaction || (await db.sequelize.transaction()); + const ownsTransaction = !externalTransaction; + + try { + const transactionOptions = buildTransactionOptions({ + transaction, + runtimeContext, + }); + const role = await RolesDBApi.findBy({ id }, transactionOptions); + + if (!role) { + throw new ValidationError('rolesNotFound'); + } + + this.assertPublicRoleHasNoPermissions(data, role); + + const contextOptions = buildContextOptions({ + currentUser, + transaction, + runtimeContext, + }); + const updatedRole = await RolesDBApi.update({ + id, + data, + ...contextOptions, + }); + + if (ownsTransaction) { + await transaction.commit(); + } + return updatedRole; + } catch (error) { + if (ownsTransaction) { + await transaction.rollback(); + } + throw error; + } + } + + static async deleteByIds(options: RoleDeleteByIdsOptions): Promise { + assertDeleteByIdsOptions(options, 'Service'); + const { + ids, + currentUser, + transaction: externalTransaction, + runtimeContext, + } = options; + const transaction = + externalTransaction || (await db.sequelize.transaction()); + const ownsTransaction = !externalTransaction; + + try { + const contextOptions = buildContextOptions({ + currentUser, + transaction, + runtimeContext, + }); + await RolesDBApi.deleteByIds({ + ids, + ...contextOptions, + }); + + if (ownsTransaction) { + await transaction.commit(); + } + } catch (error) { + if (ownsTransaction) { + await transaction.rollback(); + } + throw error; + } + } + + static async remove(options: RoleRemoveOptions): Promise { + assertIdOptions(options, 'Service', 'remove'); + const { + id, + currentUser, + transaction: externalTransaction, + runtimeContext, + } = options; + const transaction = + externalTransaction || (await db.sequelize.transaction()); + const ownsTransaction = !externalTransaction; + + try { + const contextOptions = buildContextOptions({ + currentUser, + transaction, + runtimeContext, + }); + await RolesDBApi.remove({ + id, + ...contextOptions, + }); + + if (ownsTransaction) { + await transaction.commit(); + } + } catch (error) { + if (ownsTransaction) { + await transaction.rollback(); + } + throw error; + } + } +} diff --git a/backend/src/services/runtime-presentation-access.js b/backend/src/services/runtime-presentation-access.js deleted file mode 100644 index 76fde4e..0000000 --- a/backend/src/services/runtime-presentation-access.js +++ /dev/null @@ -1,80 +0,0 @@ -const db = require('../db/models'); -const AccessPolicy = require('./access-policy'); - -class RuntimePresentationAccessService { - static normalizeSlug(slug) { - return AccessPolicy.normalizeSlug(slug); - } - - static async getProjectBySlug(slug, options = {}) { - return AccessPolicy.getProjectBySlug(slug, options); - } - - static async isPrivateProductionPresentation(slug, options = {}) { - const project = await this.getProjectBySlug(slug, options); - return project?.production_presentation_visibility === 'private'; - } - - static canUseAdminApi(user) { - return AccessPolicy.canUseAdminApi(user); - } - - static async canUserAccessPrivateProductionPresentation( - user, - slug, - options = {}, - ) { - return AccessPolicy.canViewProductionPresentation(user, slug, options); - } - - static async getAllowedPrivateProductionSlugs(user, options = {}) { - if (!user?.id || this.canUseAdminApi(user)) return []; - - const accessRows = await db.production_presentation_access.findAll({ - where: { userId: user.id }, - include: [ - { - association: 'project', - attributes: ['slug', 'production_presentation_visibility'], - where: { production_presentation_visibility: 'private' }, - required: true, - }, - ], - transaction: options.transaction, - }); - - return accessRows - .map((row) => { - const plain = - typeof row.get === 'function' ? row.get({ plain: true }) : row; - return plain.project?.slug; - }) - .filter(Boolean); - } - - static async listPrivateProductionPresentations(options = {}) { - const projects = await db.projects.findAll({ - where: { - production_presentation_visibility: 'private', - }, - attributes: ['id', 'name', 'slug'], - order: [['name', 'ASC']], - transaction: options.transaction, - }); - - return projects.map((project) => { - const plain = - typeof project.get === 'function' - ? project.get({ plain: true }) - : project; - return { - id: plain.id, - label: `${plain.name} (${plain.slug})`, - name: plain.name, - slug: plain.slug, - }; - }); - } -} - -module.exports = RuntimePresentationAccessService; diff --git a/backend/src/services/runtime-presentation-access.ts b/backend/src/services/runtime-presentation-access.ts new file mode 100644 index 0000000..ef43812 --- /dev/null +++ b/backend/src/services/runtime-presentation-access.ts @@ -0,0 +1,103 @@ +import db from '../db/models/index.ts'; +import AccessPolicy from './access-policy.ts'; +import type { + AccessPolicyUser, + PrivateProductionPresentationOption, + PrivateProductionProjectRow, + ProductionPresentationAccessGrantPlain, + ProductionPresentationAccessGrantRow, + ProductionPresentationProject, + RuntimePresentationAccessOptions, +} from '../types/index.ts'; + +function getAccessGrantPlain( + row: ProductionPresentationAccessGrantRow, +): ProductionPresentationAccessGrantPlain { + return row.get ? row.get({ plain: true }) : row; +} + +function getProjectPlain( + project: PrivateProductionProjectRow, +): Pick { + return project.get ? project.get({ plain: true }) : project; +} + +export default class RuntimePresentationAccessService { + static normalizeSlug(slug: unknown): string { + return AccessPolicy.normalizeSlug(slug); + } + + static getProjectBySlug( + slug: unknown, + options: RuntimePresentationAccessOptions = {}, + ): Promise { + return AccessPolicy.getProjectBySlug(slug, options); + } + + static async isPrivateProductionPresentation( + slug: unknown, + options: RuntimePresentationAccessOptions = {}, + ): Promise { + const project = await this.getProjectBySlug(slug, options); + return project?.production_presentation_visibility === 'private'; + } + + static canUseAdminApi(user: AccessPolicyUser): boolean { + return AccessPolicy.canUseAdminApi(user); + } + + static canUserAccessPrivateProductionPresentation( + user: AccessPolicyUser, + slug: unknown, + options: RuntimePresentationAccessOptions = {}, + ): Promise { + return AccessPolicy.canViewProductionPresentation(user, slug, options); + } + + static async getAllowedPrivateProductionSlugs( + user: AccessPolicyUser, + options: RuntimePresentationAccessOptions = {}, + ): Promise { + if (!user?.id || this.canUseAdminApi(user)) return []; + + const accessRows = await db.production_presentation_access.findAll({ + where: { userId: user.id }, + include: [ + { + association: 'project', + attributes: ['slug', 'production_presentation_visibility'], + where: { production_presentation_visibility: 'private' }, + required: true, + }, + ], + transaction: options.transaction, + }); + + return accessRows + .map((row) => getAccessGrantPlain(row).project?.slug) + .filter((slug): slug is string => Boolean(slug)); + } + + static async listPrivateProductionPresentations( + options: RuntimePresentationAccessOptions = {}, + ): Promise { + const projects = await db.projects.findAll({ + where: { + production_presentation_visibility: 'private', + }, + attributes: ['id', 'name', 'slug'], + order: [['name', 'ASC']], + transaction: options.transaction, + }); + + return projects.map((project) => { + const plain = getProjectPlain(project); + return { + id: plain.id, + label: `${plain.name} (${plain.slug})`, + name: plain.name, + slug: plain.slug, + }; + }); + } +} diff --git a/backend/src/services/search.js b/backend/src/services/search.js deleted file mode 100644 index eb101dc..0000000 --- a/backend/src/services/search.js +++ /dev/null @@ -1,185 +0,0 @@ -const db = require('../db/models'); -const ValidationError = require('./notifications/errors/validation'); - -const Sequelize = db.Sequelize; -const Op = Sequelize.Op; - -/** - * @param {object} currentUser - * @returns {Promise>} - */ -async function getUserPermissions(currentUser) { - if (!currentUser) { - throw new ValidationError('auth.unauthorized'); - } - - const permissions = new Set( - (currentUser.custom_permissions || []).map((cp) => cp.name).filter(Boolean), - ); - - if (!currentUser.app_role) { - throw new ValidationError('auth.forbidden'); - } - - let rolePermissions = []; - - if (typeof currentUser.app_role.getPermissions === 'function') { - rolePermissions = await currentUser.app_role.getPermissions(); - } else { - rolePermissions = currentUser.app_role.permissions || []; - } - - for (const permission of rolePermissions) { - if (permission?.name) { - permissions.add(permission.name); - } - } - - return permissions; -} - -/** - * @param {Set} permissions - * @param {object} currentUser - */ -function hasPermission(permissions, permissionName) { - return permissions.has(permissionName); -} - -module.exports = class SearchService { - static async search(searchQuery, currentUser) { - if (!searchQuery) { - throw new ValidationError('iam.errors.searchQueryRequired'); - } - const tableColumns = { - users: ['firstName', 'lastName', 'phoneNumber', 'email'], - - projects: [ - 'name', - 'slug', - 'description', - 'logo_url', - 'favicon_url', - 'og_image_url', - ], - - assets: ['name', 'cdn_url', 'storage_key', 'mime_type', 'checksum'], - - asset_variants: ['cdn_url'], - - presigned_url_requests: ['requested_key', 'mime_type', 'status'], - - tour_pages: [ - 'source_key', - - 'name', - - 'slug', - - 'background_image_url', - - 'background_video_url', - - 'background_audio_url', - - 'ui_schema_json', - ], - - project_audio_tracks: ['source_key', 'name', 'slug', 'url'], - - publish_events: ['error_message'], - - pwa_caches: ['cache_version', 'manifest_json', 'asset_list_json'], - - access_logs: ['path', 'ip_address', 'user_agent'], - }; - const columnsInt = { - assets: ['size_mb', 'width_px', 'height_px', 'duration_sec'], - - asset_variants: ['width_px', 'height_px', 'size_mb'], - - presigned_url_requests: ['requested_size_mb'], - - tour_pages: ['sort_order'], - - project_audio_tracks: ['volume', 'sort_order'], - - publish_events: ['pages_copied', 'transitions_copied', 'audios_copied'], - }; - - const permissionSet = await getUserPermissions(currentUser); - const normalizedSearchQuery = searchQuery.toLowerCase(); - const SEARCH_LIMIT_PER_TABLE = 50; - - const searchTasks = Object.keys(tableColumns).map(async (tableName) => { - if ( - !Object.prototype.hasOwnProperty.call(tableColumns, tableName) || - !db[tableName] - ) { - return []; - } - - if (!hasPermission(permissionSet, `READ_${tableName.toUpperCase()}`)) { - return []; - } - - const attributesToSearch = tableColumns[tableName]; - const attributesIntToSearch = columnsInt[tableName] || []; - const whereCondition = { - [Op.or]: [ - ...attributesToSearch.map((attribute) => ({ - [attribute]: { - [Op.iLike]: `%${searchQuery}%`, - }, - })), - ...attributesIntToSearch.map((attribute) => - Sequelize.where( - Sequelize.cast( - Sequelize.col(`${tableName}.${attribute}`), - 'varchar', - ), - { [Op.iLike]: `%${searchQuery}%` }, - ), - ), - ], - }; - - const foundRecords = await db[tableName].findAll({ - where: whereCondition, - attributes: [...attributesToSearch, 'id', ...attributesIntToSearch], - limit: SEARCH_LIMIT_PER_TABLE, - }); - - return foundRecords.map((record) => { - const matchAttribute = []; - - for (const attribute of attributesToSearch) { - if ( - record[attribute]?.toLowerCase()?.includes(normalizedSearchQuery) - ) { - matchAttribute.push(attribute); - } - } - - for (const attribute of attributesIntToSearch) { - const castedValue = String(record[attribute]); - if ( - castedValue && - castedValue.toLowerCase().includes(normalizedSearchQuery) - ) { - matchAttribute.push(attribute); - } - } - - return { - ...record.get(), - matchAttribute, - tableName, - }; - }); - }); - - const resultsByTable = await Promise.all(searchTasks); - return resultsByTable.flat(); - } -}; diff --git a/backend/src/services/search.ts b/backend/src/services/search.ts new file mode 100644 index 0000000..021adfe --- /dev/null +++ b/backend/src/services/search.ts @@ -0,0 +1,265 @@ +import db from '../db/models/index.ts'; +import type { + CurrentUser, + PermissionName, + PermissionRecord, + SearchResult, + SearchResultRow, +} from '../types/index.ts'; +import ValidationError from './notifications/errors/validation.ts'; + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +interface RoleWithPermissionMethod { + getPermissions(): Promise; +} + +interface SearchRecord { + [field: string]: unknown; + get(): Omit; +} + +interface SearchModel { + findAll(options: { + where: object; + attributes: readonly string[]; + limit: number; + }): Promise; +} + +type SearchTableName = + | 'users' + | 'projects' + | 'assets' + | 'asset_variants' + | 'presigned_url_requests' + | 'tour_pages' + | 'project_audio_tracks' + | 'publish_events' + | 'pwa_caches' + | 'access_logs'; + +interface SearchTableConfig { + name: SearchTableName; + columns: readonly string[]; +} + +const searchTables = [ + { name: 'users', columns: ['firstName', 'lastName', 'phoneNumber', 'email'] }, + { + name: 'projects', + columns: [ + 'name', + 'slug', + 'description', + 'logo_url', + 'favicon_url', + 'og_image_url', + ], + }, + { + name: 'assets', + columns: ['name', 'cdn_url', 'storage_key', 'mime_type', 'checksum'], + }, + { name: 'asset_variants', columns: ['cdn_url'] }, + { + name: 'presigned_url_requests', + columns: ['requested_key', 'mime_type', 'status'], + }, + { + name: 'tour_pages', + columns: [ + 'source_key', + 'name', + 'slug', + 'background_image_url', + 'background_video_url', + 'background_audio_url', + 'ui_schema_json', + ], + }, + { + name: 'project_audio_tracks', + columns: ['source_key', 'name', 'slug', 'url'], + }, + { name: 'publish_events', columns: ['error_message'] }, + { + name: 'pwa_caches', + columns: ['cache_version', 'manifest_json', 'asset_list_json'], + }, + { name: 'access_logs', columns: ['path', 'ip_address', 'user_agent'] }, +] satisfies readonly SearchTableConfig[]; + +const columnsInt: Partial> = { + assets: ['size_mb', 'width_px', 'height_px', 'duration_sec'], + asset_variants: ['width_px', 'height_px', 'size_mb'], + presigned_url_requests: ['requested_size_mb'], + tour_pages: ['sort_order'], + project_audio_tracks: ['volume', 'sort_order'], + publish_events: ['pages_copied', 'transitions_copied', 'audios_copied'], +}; + +const SEARCH_LIMIT_PER_TABLE = 50; + +function hasPermissionMethod(value: unknown): value is RoleWithPermissionMethod { + return ( + value !== null && + typeof value === 'object' && + 'getPermissions' in value && + typeof value.getPermissions === 'function' + ); +} + +function isSearchModel(value: unknown): value is SearchModel { + return ( + value !== null && + typeof value === 'object' && + 'findAll' in value && + typeof value.findAll === 'function' + ); +} + +function isSearchableString(value: unknown): value is string { + return typeof value === 'string'; +} + +function toSearchText(value: unknown): string { + if (typeof value === 'string') return value; + if (typeof value === 'number' || typeof value === 'boolean') { + return String(value); + } + if (value instanceof Date) return value.toISOString(); + return ''; +} + +async function getUserPermissions( + currentUser: CurrentUser | undefined, +): Promise> { + if (!currentUser) { + throw new ValidationError('auth.unauthorized'); + } + + const permissions = new Set( + (currentUser.custom_permissions || []) + .map((permission) => + typeof permission === 'string' ? permission : permission.name, + ) + .filter(Boolean), + ); + + if (!currentUser.app_role) { + throw new ValidationError('auth.forbidden'); + } + + let rolePermissions: ReadonlyArray = []; + + if (hasPermissionMethod(currentUser.app_role)) { + rolePermissions = await currentUser.app_role.getPermissions(); + } else { + rolePermissions = currentUser.app_role.permissions || []; + } + + for (const permission of rolePermissions) { + if (typeof permission === 'string') { + permissions.add(permission); + } else if (permission.name) { + permissions.add(permission.name); + } + } + + return permissions; +} + +function hasPermission( + permissions: Set, + permissionName: PermissionName, +): boolean { + return permissions.has(permissionName); +} + +export default class SearchService { + static async search( + searchQuery: string, + currentUser: CurrentUser | undefined, + ): Promise { + if (!searchQuery) { + throw new ValidationError('iam.errors.searchQueryRequired'); + } + + const permissionSet = await getUserPermissions(currentUser); + const normalizedSearchQuery = searchQuery.toLowerCase(); + + const searchTasks = searchTables.map(async ({ name, columns }) => { + const tableName = name; + const attributesToSearch = columns; + const model = db[tableName]; + if (!isSearchModel(model)) { + return []; + } + + if (!hasPermission(permissionSet, `READ_${tableName.toUpperCase()}`)) { + return []; + } + + const attributesIntToSearch = columnsInt[tableName] || []; + const whereCondition = { + [Op.or]: [ + ...attributesToSearch.map((attribute) => ({ + [attribute]: { + [Op.iLike]: `%${searchQuery}%`, + }, + })), + ...attributesIntToSearch.map((attribute) => + Sequelize.where( + Sequelize.cast( + Sequelize.col(`${tableName}.${attribute}`), + 'varchar', + ), + { [Op.iLike]: `%${searchQuery}%` }, + ), + ), + ], + }; + + const foundRecords = await model.findAll({ + where: whereCondition, + attributes: [...attributesToSearch, 'id', ...attributesIntToSearch], + limit: SEARCH_LIMIT_PER_TABLE, + }); + + return foundRecords.map((record) => { + const matchAttribute: string[] = []; + + for (const attribute of attributesToSearch) { + const recordValue = record[attribute]; + if ( + isSearchableString(recordValue) && + recordValue.toLowerCase().includes(normalizedSearchQuery) + ) { + matchAttribute.push(attribute); + } + } + + for (const attribute of attributesIntToSearch) { + const castedValue = toSearchText(record[attribute]); + if ( + castedValue && + castedValue.toLowerCase().includes(normalizedSearchQuery) + ) { + matchAttribute.push(attribute); + } + } + + return { + ...record.get(), + matchAttribute, + tableName, + }; + }); + }); + + const resultsByTable = await Promise.all(searchTasks); + return resultsByTable.flat(); + } +} diff --git a/backend/src/services/tour_pages.js b/backend/src/services/tour_pages.ts similarity index 69% rename from backend/src/services/tour_pages.js rename to backend/src/services/tour_pages.ts index 3bbaa60..3537ef6 100644 --- a/backend/src/services/tour_pages.js +++ b/backend/src/services/tour_pages.ts @@ -5,28 +5,53 @@ * Reversed videos are always generated for forward navigation elements to support back navigation. */ -const Tour_pagesDBApi = require('../db/api/tour_pages'); -const AssetsDBApi = require('../db/api/assets'); -const Asset_variantsDBApi = require('../db/api/asset_variants'); -const { createEntityService } = require('../factories/service.factory'); -const { +import crypto from 'crypto'; + +import Tour_pagesDBApi from '../db/api/tour_pages.ts'; +import AssetsDBApi from '../db/api/assets.ts'; +import Asset_variantsDBApi from '../db/api/asset_variants.ts'; +import { createEntityService } from '../factories/service.factory.ts'; +import { assertCreateOptions, assertUpdateOptions, -} = require('../contracts/entity-options'); -const { - downloadToBuffer, - downloadToTempFile, - uploadBuffer, -} = require('./file'); -const ValidationError = require('./notifications/errors/validation'); -const videoProcessing = require('./videoProcessing'); -const { logger } = require('../utils/logger'); -const db = require('../db/models'); -const crypto = require('crypto'); +} from '../contracts/entity-options.ts'; +import FileService from './file.ts'; +import ValidationError from './notifications/errors/validation.ts'; +import { + isFFmpegAvailable, + probeMediaMetadata, + reverseVideo, +} from './videoProcessing.ts'; +import { logger } from '../utils/logger.ts'; +import db from '../db/models/index.ts'; +import type { + AssetRecord, + CurrentUser, + RuntimeContext, + TourPageBuildDuplicateSlugOptions, + TourPageCreateOptions, + TourPageData, + TourPageDecodedBytesEstimateInput, + TourPageElement, + TourPageMaybeSequelizeRecord, + TourPageNestedUiItem, + TourPageProcessReverseOptions, + TourPageRecord, + TourPageReorderData, + TourPageReverseGenerationTask, + TourPageReverseVideoStatus, + TourPageStructuredUiSchema, + TourPageUpdateOptions, + TourPageVideoMetadata, +} from '../types/index.ts'; +import type { Transaction } from 'sequelize'; -const projectRegenInProgress = new Set(); -const singleReverseGenerationInProgress = new Set(); -const reverseGenerationPromiseByStorageKey = new Map(); +const projectRegenInProgress = new Set(); +const singleReverseGenerationInProgress = new Set(); +const reverseGenerationPromiseByStorageKey = new Map< + string, + Promise +>(); const MAX_AUTO_REVERSE_SOURCE_SIZE_BYTES = 16 * 1024 * 1024 * 1024; const DEFAULT_AUTO_REVERSE_FALLBACK_FPS = 30; const YUV420_BYTES_PER_PIXEL = 1.5; @@ -34,7 +59,7 @@ const MAX_AUTO_REVERSE_ESTIMATED_DECODED_BYTES = 2 * 1024 * 1024 * 1024; const createLocalElementId = () => crypto.randomUUID(); -const sanitizeSlug = (value) => { +const sanitizeSlug = (value: string | null | undefined): string => { const slug = String(value || '') .toLowerCase() .trim() @@ -43,24 +68,36 @@ const sanitizeSlug = (value) => { return slug || 'page'; }; -const buildCopyName = (name) => { +const buildCopyName = (name: string | null | undefined): string => { const baseName = String(name || 'Page').trim() || 'Page'; const copyName = `${baseName} Copy`; return copyName.length > 255 ? copyName.slice(0, 255) : copyName; }; -const parseUiSchema = (uiSchema) => { +const parseUnknownJson = (value: string): unknown => JSON.parse(value); + +const isStructuredUiSchema = ( + value: unknown, +): value is TourPageStructuredUiSchema => + Boolean(value && typeof value === 'object' && !Array.isArray(value)); + +const parseUiSchema = ( + uiSchema: TourPageData['ui_schema_json'], +): TourPageStructuredUiSchema | string | null => { if (!uiSchema) return null; - if (typeof uiSchema !== 'string') return JSON.parse(JSON.stringify(uiSchema)); + if (typeof uiSchema !== 'string') return uiSchema; try { - return JSON.parse(uiSchema); + const parsed = parseUnknownJson(uiSchema); + return isStructuredUiSchema(parsed) ? parsed : uiSchema; } catch { return uiSchema; } }; -const regenerateNestedItemIds = (items) => { +const regenerateNestedItemIds = ( + items: TourPageNestedUiItem[] | undefined, +): TourPageNestedUiItem[] | undefined => { if (!Array.isArray(items)) return items; return items.map((item) => ({ ...item, @@ -68,36 +105,55 @@ const regenerateNestedItemIds = (items) => { })); }; -const regenerateElementInstanceIds = (uiSchema) => { +const regenerateElementInstanceIds = ( + uiSchema: TourPageStructuredUiSchema | string | null, +): TourPageStructuredUiSchema | string | null => { if (!uiSchema || typeof uiSchema !== 'object') return uiSchema; - const clonedSchema = JSON.parse(JSON.stringify(uiSchema)); + const parsedClone = parseUnknownJson(JSON.stringify(uiSchema)); + const clonedSchema = isStructuredUiSchema(parsedClone) ? parsedClone : uiSchema; if (!Array.isArray(clonedSchema.elements)) return clonedSchema; - clonedSchema.elements = clonedSchema.elements.map((element) => { - const clonedElement = { + clonedSchema.elements = clonedSchema.elements.map((element): TourPageElement => { + const clonedElement: TourPageElement = { ...element, id: createLocalElementId(), }; - clonedElement.galleryCards = regenerateNestedItemIds( - clonedElement.galleryCards, - ); - clonedElement.galleryInfoSpans = regenerateNestedItemIds( + const galleryCards = regenerateNestedItemIds(clonedElement.galleryCards); + if (galleryCards !== undefined) { + clonedElement.galleryCards = galleryCards; + } + + const galleryInfoSpans = regenerateNestedItemIds( clonedElement.galleryInfoSpans, ); - clonedElement.carouselSlides = regenerateNestedItemIds( - clonedElement.carouselSlides, - ); + if (galleryInfoSpans !== undefined) { + clonedElement.galleryInfoSpans = galleryInfoSpans; + } + + const carouselSlides = regenerateNestedItemIds(clonedElement.carouselSlides); + if (carouselSlides !== undefined) { + clonedElement.carouselSlides = carouselSlides; + } if (Array.isArray(clonedElement.infoPanelSections)) { clonedElement.infoPanelSections = clonedElement.infoPanelSections.map( - (section) => ({ - ...section, - id: `section-${createLocalElementId()}`, - spans: regenerateNestedItemIds(section.spans), - images: regenerateNestedItemIds(section.images), - }), + (section) => { + const clonedSection = { + ...section, + id: `section-${createLocalElementId()}`, + }; + const spans = regenerateNestedItemIds(section.spans); + if (spans !== undefined) { + clonedSection.spans = spans; + } + const images = regenerateNestedItemIds(section.images); + if (images !== undefined) { + clonedSection.images = images; + } + return clonedSection; + }, ); } @@ -108,19 +164,114 @@ const regenerateElementInstanceIds = (uiSchema) => { }; // Create base service from factory -const BaseService = createEntityService(Tour_pagesDBApi, { +const BaseService = createEntityService( + Tour_pagesDBApi, + { entityName: 'tour_pages', -}); + }, +); + +const buildCreateServiceOptions = ( + options: TourPageCreateOptions, +): Pick => { + const serviceOptions: Pick< + TourPageCreateOptions, + 'currentUser' | 'transaction' | 'runtimeContext' + > = {}; + + if (options.currentUser !== undefined) { + serviceOptions.currentUser = options.currentUser; + } + if (options.transaction !== undefined) { + serviceOptions.transaction = options.transaction; + } + if (options.runtimeContext !== undefined) { + serviceOptions.runtimeContext = options.runtimeContext; + } + + return serviceOptions; +}; + +const buildUpdateServiceOptions = ( + options: TourPageUpdateOptions, +): Pick => { + const serviceOptions: Pick< + TourPageUpdateOptions, + 'currentUser' | 'transaction' | 'runtimeContext' + > = {}; + + if (options.currentUser !== undefined) { + serviceOptions.currentUser = options.currentUser; + } + if (options.transaction !== undefined) { + serviceOptions.transaction = options.transaction; + } + if (options.runtimeContext !== undefined) { + serviceOptions.runtimeContext = options.runtimeContext; + } + + return serviceOptions; +}; + +const buildFindByOptions = ( + options: TourPageUpdateOptions, +): { transaction?: Transaction; runtimeContext?: RuntimeContext } => { + const findByOptions: { transaction?: Transaction; runtimeContext?: RuntimeContext } = + {}; + + if (options.transaction !== undefined) { + findByOptions.transaction = options.transaction; + } + if (options.runtimeContext !== undefined) { + findByOptions.runtimeContext = options.runtimeContext; + } + + return findByOptions; +}; + +const buildCurrentUserOptions = ( + currentUser: CurrentUser | null | undefined, +): { currentUser?: CurrentUser | null } => { + const options: { currentUser?: CurrentUser | null } = {}; + if (currentUser !== undefined) { + options.currentUser = currentUser; + } + return options; +}; + +const normalizeProjectId = ( + value: TourPageData['project'] | TourPageData['projectId'] | undefined, +): string | null => { + if (!value) return null; + if (typeof value === 'string') return value; + return value.id; +}; + +const parseStructuredUiSchema = ( + value: TourPageData['ui_schema_json'], +): TourPageStructuredUiSchema | null => { + const parsed = parseUiSchema(value); + return isStructuredUiSchema(parsed) ? parsed : null; +}; + +const toPlainTourPage = ( + page: TourPageMaybeSequelizeRecord, +): TourPageRecord => { + if ('get' in page && typeof page.get === 'function') { + return page.get({ plain: true }); + } + return page; +}; /** * Tour Pages Service with reversed video generation */ class TourPagesService extends BaseService { - static formatBytesToGiB(bytes) { + static formatBytesToGiB(bytes: number): string { return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GiB`; } - static getAssetSizeBytes(asset) { + static getAssetSizeBytes(asset: AssetRecord | null | undefined): number | null { if (!asset || asset.size_mb == null) return null; const sizeMb = Number(asset.size_mb); @@ -129,7 +280,9 @@ class TourPagesService extends BaseService { return Math.round(sizeMb * 1024 * 1024); } - static getAssetVideoMetadata(asset) { + static getAssetVideoMetadata( + asset: AssetRecord | null | undefined, + ): TourPageVideoMetadata { const widthPx = Number(asset?.width_px); const heightPx = Number(asset?.height_px); const durationSec = Number(asset?.duration_sec); @@ -144,7 +297,12 @@ class TourPagesService extends BaseService { }; } - static getEstimatedDecodedBytes({ widthPx, heightPx, durationSec, fps }) { + static getEstimatedDecodedBytes({ + widthPx, + heightPx, + durationSec, + fps, + }: TourPageDecodedBytesEstimateInput): number | null { if (!widthPx || !heightPx || !durationSec || !fps) { return null; } @@ -154,11 +312,13 @@ class TourPagesService extends BaseService { ); } - static formatBytesToMiB(bytes) { + static formatBytesToMiB(bytes: number): string { return `${(bytes / (1024 * 1024)).toFixed(0)} MiB`; } - static async resolveAssetFrameRate(asset) { + static async resolveAssetFrameRate( + asset: AssetRecord | null | undefined, + ): Promise { const metadata = TourPagesService.getAssetVideoMetadata(asset); if (metadata.frameRate) { return metadata.frameRate; @@ -171,10 +331,8 @@ class TourPagesService extends BaseService { let tempFile = null; try { - tempFile = await downloadToTempFile(asset.storage_key); - const probedMetadata = await videoProcessing.probeMediaMetadata( - tempFile.filePath, - ); + tempFile = await FileService.downloadToTempFile(asset.storage_key); + const probedMetadata = await probeMediaMetadata(tempFile.filePath); const probedFrameRate = Number(probedMetadata?.frameRate); return Number.isFinite(probedFrameRate) && probedFrameRate > 0 @@ -188,7 +346,7 @@ class TourPagesService extends BaseService { return null; } finally { if (tempFile?.cleanup) { - await tempFile.cleanup().catch((cleanupError) => { + await tempFile.cleanup().catch((cleanupError: unknown) => { logger.warn( { err: cleanupError, storageKey: asset?.storage_key }, 'Failed to cleanup frame-rate probe temp file', @@ -198,7 +356,9 @@ class TourPagesService extends BaseService { } } - static async validateAutoReverseAssets(storageKeys) { + static async validateAutoReverseAssets( + storageKeys: Iterable, + ): Promise { for (const storageKey of storageKeys) { const asset = await AssetsDBApi.findBy({ storage_key: storageKey }); @@ -259,7 +419,10 @@ class TourPagesService extends BaseService { } } - static async reorder(data, currentUser) { + static async reorder( + data: TourPageReorderData, + currentUser?: CurrentUser | null, + ): Promise { const projectId = data?.projectId || data?.project; const environment = data?.environment || 'dev'; const orderedPageIds = Array.isArray(data?.orderedPageIds) @@ -267,19 +430,17 @@ class TourPagesService extends BaseService { : []; if (!projectId) { - throw { status: 400, message: 'Project is required' }; + throw new ValidationError('Project is required'); } if (!orderedPageIds.length) { - throw { status: 400, message: 'orderedPageIds is required' }; + throw new ValidationError('orderedPageIds is required'); } if (environment !== 'dev') { - throw { - status: 400, - message: - 'Page order can only be changed in dev. Use Save to Stage and Publish to update presentations.', - }; + throw new ValidationError( + 'Page order can only be changed in dev. Use Save to Stage and Publish to update presentations.', + ); } return db.sequelize.transaction(async (transaction) => { @@ -297,20 +458,18 @@ class TourPagesService extends BaseService { requestedIdSet.size !== orderedPageIds.length || orderedPageIds.some((id) => !existingIdSet.has(id)) ) { - throw { - status: 400, - message: - 'orderedPageIds must include each page from the selected project/environment exactly once', - }; + throw new ValidationError( + 'orderedPageIds must include each page from the selected project/environment exactly once', + ); } - const updatedPages = []; + const updatedPages: TourPageRecord[] = []; for (const [index, pageId] of orderedPageIds.entries()) { const page = await Tour_pagesDBApi.partialUpdate({ id: pageId, data: { sort_order: index + 1 }, - currentUser, transaction, + ...buildCurrentUserOptions(currentUser), }); updatedPages.push(page); } @@ -325,7 +484,7 @@ class TourPagesService extends BaseService { preferredSlug, sourceSlug, transaction, - }) { + }: TourPageBuildDuplicateSlugOptions): Promise { const baseSlug = sanitizeSlug( preferredSlug || `${sourceSlug || 'page'} copy`, ); @@ -350,7 +509,11 @@ class TourPagesService extends BaseService { return `${baseSlug}-${Date.now().toString(36)}`; } - static async duplicatePage(sourcePageId, data = {}, currentUser) { + static async duplicatePage( + sourcePageId: string, + data: TourPageData = {}, + currentUser?: CurrentUser | null, + ): Promise { if (!sourcePageId) { throw new ValidationError('Source page is required'); } @@ -365,10 +528,15 @@ class TourPagesService extends BaseService { throw new ValidationError('Source page not found'); } - const source = sourcePage.get({ plain: true }); - const projectId = data.projectId || data.project || source.projectId; + const source = toPlainTourPage(sourcePage); + const projectId = + data.projectId || normalizeProjectId(data.project) || source.projectId; const environment = data.environment || source.environment || 'dev'; + if (!projectId) { + throw new ValidationError('Project is required'); + } + if (source.projectId !== projectId) { throw new ValidationError( 'Source page does not belong to the selected project', @@ -385,19 +553,24 @@ class TourPagesService extends BaseService { where: { projectId, environment }, transaction, }); - const slug = await TourPagesService.buildUniqueDuplicateSlug({ + const slugOptions: TourPageBuildDuplicateSlugOptions = { projectId, environment, - preferredSlug: data.slug, - sourceSlug: source.slug, transaction, - }); + }; + if (data.slug !== undefined) { + slugOptions.preferredSlug = data.slug; + } + if (source.slug !== undefined) { + slugOptions.sourceSlug = source.slug; + } + const slug = await TourPagesService.buildUniqueDuplicateSlug(slugOptions); const uiSchema = regenerateElementInstanceIds( parseUiSchema(source.ui_schema_json), ); const requestedName = String(data.name || '').trim(); - const duplicatePayload = { + const duplicatePayload: TourPageData = { project: projectId, environment, source_key: '', @@ -436,8 +609,8 @@ class TourPagesService extends BaseService { return Tour_pagesDBApi.create({ data: processedPayload, - currentUser, transaction, + ...buildCurrentUserOptions(currentUser), }); }); } @@ -445,9 +618,11 @@ class TourPagesService extends BaseService { /** * Create tour page - generate reversed videos if needed */ - static async create(options) { + static override async create( + options: TourPageCreateOptions, + ): Promise { assertCreateOptions(options, 'Service'); - const { data, currentUser, transaction, runtimeContext } = options; + const { data, currentUser } = options; // Process reversed videos and get updated ui_schema_json const updatedData = @@ -458,40 +633,45 @@ class TourPagesService extends BaseService { return super.create({ data: updatedData, - currentUser, - transaction, - runtimeContext, + ...buildCreateServiceOptions(options), }); } /** * Update tour page - generate reversed videos if needed */ - static async update(options) { + static override async update( + options: TourPageUpdateOptions, + ): Promise { assertUpdateOptions(options, 'Service'); - const { id, data, currentUser, transaction, runtimeContext } = options; + const { id, data, currentUser } = options; // Fetch existing page to get projectId (not included in update request body) const existingPage = await Tour_pagesDBApi.findBy( { id }, - { transaction, runtimeContext }, + buildFindByOptions(options), ); const projectId = existingPage?.projectId || data.projectId || data.project_id; + const dataWithPageContext: TourPageData = { + ...data, + id, + }; + if (projectId !== undefined) { + dataWithPageContext.projectId = projectId; + } // Process reversed videos and get updated ui_schema_json const updatedData = await TourPagesService.processReversedVideosAndUpdateSchema( - { ...data, projectId, id }, + dataWithPageContext, currentUser, ); return super.update({ id, data: updatedData, - currentUser, - transaction, - runtimeContext, + ...buildUpdateServiceOptions(options), }); } @@ -499,10 +679,10 @@ class TourPagesService extends BaseService { * Check if element is a back navigation button * @private */ - static isBackElement(element) { - return ( + static isBackElement(element: TourPageElement): boolean { + return Boolean( element.type === 'navigation_prev' || - (element.type?.startsWith?.('navigation') && element.navType === 'back') + (element.type?.startsWith?.('navigation') && element.navType === 'back'), ); } @@ -510,7 +690,7 @@ class TourPagesService extends BaseService { * Check if element is a forward navigation button with a target and transition * @private */ - static isForwardElementWithTarget(element) { + static isForwardElementWithTarget(element: TourPageElement): boolean { if (element.navigationTargetMode === 'external_url') { return false; } @@ -522,7 +702,7 @@ class TourPagesService extends BaseService { element.type !== 'navigation_prev'); // Check for target (slug or legacy ID) and transition video const hasTarget = element.targetPageSlug || element.targetPageId; - return isForward && hasTarget && element.transitionVideoUrl; + return Boolean(isForward && hasTarget && element.transitionVideoUrl); } /** @@ -537,20 +717,13 @@ class TourPagesService extends BaseService { * @param {boolean} options._skipHistoryModeCheck - Skip initial regeneration check */ static async processReversedVideosAndUpdateSchema( - data, - currentUser, - options = {}, - ) { - let uiSchema = data.ui_schema_json; - const wasString = typeof uiSchema === 'string'; - - // Parse if string - if (wasString) { - try { - uiSchema = JSON.parse(uiSchema); - } catch { - return data; // Return original data if parsing fails - } + data: TourPageData, + currentUser?: CurrentUser | null, + options: TourPageProcessReverseOptions = {}, + ): Promise { + const uiSchema = parseStructuredUiSchema(data.ui_schema_json); + if (!uiSchema && typeof data.ui_schema_json === 'string') { + return data; } if (!uiSchema?.elements || !Array.isArray(uiSchema.elements)) { @@ -559,14 +732,15 @@ class TourPagesService extends BaseService { } // Get project ID - const projectId = data.projectId || data.project_id || data.project; + const projectId = + data.projectId || data.project_id || normalizeProjectId(data.project); logger.info( { projectId }, 'Processing reversed videos for navigation elements', ); - const storageKeysToValidate = new Set(); + const storageKeysToValidate = new Set(); let wasModified = false; for (const element of uiSchema.elements) { @@ -676,7 +850,10 @@ class TourPagesService extends BaseService { return { ...data, // Return same type as input: string if input was string, object otherwise - ui_schema_json: wasString ? JSON.stringify(uiSchema) : uiSchema, + ui_schema_json: + typeof data.ui_schema_json === 'string' + ? JSON.stringify(uiSchema) + : uiSchema, }; } @@ -690,7 +867,9 @@ class TourPagesService extends BaseService { * @param {string} storageKey - Original video storage key * @returns {Promise} */ - static async getExistingReversedVariant(storageKey) { + static async getExistingReversedVariant( + storageKey: string, + ): Promise { const asset = await AssetsDBApi.findBy({ storage_key: storageKey }); if (!asset) { @@ -719,7 +898,7 @@ class TourPagesService extends BaseService { storageKey, currentUser, pageId, - }) { + }: TourPageReverseGenerationTask): void { if (!projectId || !storageKey) return; const taskKey = `${projectId}:${storageKey}`; @@ -729,7 +908,8 @@ class TourPagesService extends BaseService { singleReverseGenerationInProgress.add(taskKey); - setImmediate(async () => { + setImmediate(() => { + void (async () => { const log = logger.child({ projectId, storageKey, @@ -762,6 +942,7 @@ class TourPagesService extends BaseService { } finally { singleReverseGenerationInProgress.delete(taskKey); } + })(); }); } @@ -773,16 +954,17 @@ class TourPagesService extends BaseService { * @param {string} [excludePageId] */ static enqueueProjectReversedVideosRegeneration( - projectId, - currentUser, - excludePageId, - ) { + projectId: string, + currentUser?: CurrentUser | null, + excludePageId?: string | null, + ): void { if (!projectId) return; if (projectRegenInProgress.has(projectId)) return; projectRegenInProgress.add(projectId); - setImmediate(async () => { + setImmediate(() => { + void (async () => { try { await TourPagesService.regenerateProjectReversedVideos( projectId, @@ -797,6 +979,7 @@ class TourPagesService extends BaseService { } finally { projectRegenInProgress.delete(projectId); } + })(); }); } @@ -809,11 +992,11 @@ class TourPagesService extends BaseService { * @param {Object} currentUser */ static async applyReversedUrlToProjectElements( - projectId, - storageKey, - reversedUrl, - currentUser, - ) { + projectId: string, + storageKey: string, + reversedUrl: string, + currentUser?: CurrentUser | null, + ): Promise { if (!projectId || !storageKey || !reversedUrl) return; const log = logger.child({ @@ -822,20 +1005,20 @@ class TourPagesService extends BaseService { operation: 'applyReversedUrlToProjectElements', }); - const { rows: pages } = await Tour_pagesDBApi.findAll({ - projectId, - environment: 'dev', - }); + const { rows: pages } = await Tour_pagesDBApi.findAll( + { + projectId, + environment: 'dev', + }, + {}, + ); let pagesUpdated = 0; for (const page of pages) { - const uiSchema = - typeof page.ui_schema_json === 'string' - ? JSON.parse(page.ui_schema_json || '{}') - : page.ui_schema_json || {}; + const uiSchema = parseStructuredUiSchema(page.ui_schema_json); - if (!uiSchema.elements || !Array.isArray(uiSchema.elements)) continue; + if (!uiSchema?.elements || !Array.isArray(uiSchema.elements)) continue; let pageModified = false; @@ -857,7 +1040,7 @@ class TourPagesService extends BaseService { await Tour_pagesDBApi.partialUpdate({ id: page.id, data: { ui_schema_json: JSON.stringify(uiSchema) }, - currentUser, + ...buildCurrentUserOptions(currentUser), }); pagesUpdated++; } @@ -873,7 +1056,10 @@ class TourPagesService extends BaseService { * @param {Object} currentUser - Current user for permissions * @returns {Promise} Reversed video storage key or null */ - static async getOrGenerateReversedVariant(storageKey, currentUser) { + static async getOrGenerateReversedVariant( + storageKey: string, + currentUser?: CurrentUser | null, + ): Promise { const existingVariant = await TourPagesService.getExistingReversedVariant(storageKey); @@ -882,7 +1068,11 @@ class TourPagesService extends BaseService { } if (reverseGenerationPromiseByStorageKey.has(storageKey)) { - return reverseGenerationPromiseByStorageKey.get(storageKey); + const existingPromise = + reverseGenerationPromiseByStorageKey.get(storageKey); + if (existingPromise) { + return existingPromise; + } } const generationPromise = (async () => { @@ -911,23 +1101,33 @@ class TourPagesService extends BaseService { * Generate reversed video variant for an asset * @returns {Promise} Reversed video storage key or null */ - static async generateReversedVariant(asset, currentUser) { + static async generateReversedVariant( + asset: AssetRecord, + currentUser?: CurrentUser | null, + ): Promise { const log = logger.child({ assetId: asset.id }); log.info('Generating reversed video variant'); try { // Check if FFmpeg is available - const ffmpegAvailable = await videoProcessing.isFFmpegAvailable(); + const ffmpegAvailable = await isFFmpegAvailable(); if (!ffmpegAvailable) { log.error('FFmpeg is not available on this server'); return null; } // Download original video to buffer - const originalBuffer = await downloadToBuffer(asset.storage_key); + if (!asset.storage_key) { + log.warn('Asset has no storage key for reversed variant generation'); + return null; + } + + const originalBuffer = await FileService.downloadToBuffer( + asset.storage_key, + ); // Generate reversed video - const reversedBuffer = await videoProcessing.reverseVideo( + const reversedBuffer = await reverseVideo( originalBuffer, asset.original_file_name || 'video.mp4', ); @@ -935,7 +1135,7 @@ class TourPagesService extends BaseService { // Upload reversed video to storage const reversedKey = `assets/${asset.id}/reversed.mp4`; - const result = await uploadBuffer(reversedKey, reversedBuffer, { + const result = await FileService.uploadBuffer(reversedKey, reversedBuffer, { contentType: 'video/mp4', }); @@ -948,7 +1148,7 @@ class TourPagesService extends BaseService { storage_key: reversedKey, size_mb: reversedBuffer.length / (1024 * 1024), }, - currentUser, + ...buildCurrentUserOptions(currentUser), }); log.info( @@ -972,19 +1172,22 @@ class TourPagesService extends BaseService { * @param {string} excludePageId - Optional page ID to exclude (the page being saved) */ static async regenerateProjectReversedVideos( - projectId, - currentUser, - excludePageId, - ) { + projectId: string, + currentUser?: CurrentUser | null, + excludePageId?: string | null, + ): Promise { const log = logger.child({ projectId, operation: 'regenerateReversed' }); log.info('Starting project-wide reversed video regeneration'); try { // Only process dev environment pages (constructor works with dev) - const { rows: pages } = await Tour_pagesDBApi.findAll({ - projectId, - environment: 'dev', - }); + const { rows: pages } = await Tour_pagesDBApi.findAll( + { + projectId, + environment: 'dev', + }, + {}, + ); let pagesUpdated = 0; let elementsProcessed = 0; @@ -995,12 +1198,9 @@ class TourPagesService extends BaseService { continue; } - const uiSchema = - typeof page.ui_schema_json === 'string' - ? JSON.parse(page.ui_schema_json || '{}') - : page.ui_schema_json || {}; + const uiSchema = parseStructuredUiSchema(page.ui_schema_json); - if (!uiSchema.elements || !Array.isArray(uiSchema.elements)) { + if (!uiSchema?.elements || !Array.isArray(uiSchema.elements)) { continue; } @@ -1010,9 +1210,10 @@ class TourPagesService extends BaseService { // Process both forward elements AND back elements with their own transition const isForward = TourPagesService.isForwardElementWithTarget(element); - const isBackWithTransition = + const isBackWithTransition = Boolean( TourPagesService.isBackElement(element) && - element.transitionVideoUrl; + element.transitionVideoUrl, + ); log.debug( { @@ -1063,7 +1264,7 @@ class TourPagesService extends BaseService { await Tour_pagesDBApi.partialUpdate({ id: page.id, data: { ui_schema_json: JSON.stringify(uiSchema) }, - currentUser, + ...buildCurrentUserOptions(currentUser), }); pagesUpdated++; } @@ -1088,22 +1289,27 @@ class TourPagesService extends BaseService { * @param {Object|Array} pages - Single page or array of pages * @returns {Promise} Pages with populated reverseVideoUrl fields */ - static async populateReverseVideoUrls(pages) { + static async populateReverseVideoUrls( + pages: TourPageMaybeSequelizeRecord[], + ): Promise; + static async populateReverseVideoUrls( + pages: TourPageMaybeSequelizeRecord, + ): Promise; + static async populateReverseVideoUrls( + pages: TourPageMaybeSequelizeRecord | TourPageMaybeSequelizeRecord[], + ): Promise { if (!pages) return pages; const isArray = Array.isArray(pages); - const pageList = isArray ? pages : [pages]; + const pageList: TourPageMaybeSequelizeRecord[] = isArray ? pages : [pages]; // Collect all storage keys that need lookup - const storageKeysToLookup = new Set(); + const storageKeysToLookup = new Set(); for (const page of pageList) { if (!page?.ui_schema_json) continue; - const uiSchema = - typeof page.ui_schema_json === 'string' - ? JSON.parse(page.ui_schema_json || '{}') - : page.ui_schema_json || {}; + const uiSchema = parseStructuredUiSchema(page.ui_schema_json); if (!uiSchema?.elements || !Array.isArray(uiSchema.elements)) continue; @@ -1123,7 +1329,7 @@ class TourPagesService extends BaseService { } // Batch lookup all reversed variants - const reversedUrlByStorageKey = new Map(); + const reversedUrlByStorageKey = new Map(); for (const storageKey of storageKeysToLookup) { try { @@ -1145,10 +1351,7 @@ class TourPagesService extends BaseService { const modifiedPages = pageList.map((page) => { if (!page?.ui_schema_json) return page; - const uiSchema = - typeof page.ui_schema_json === 'string' - ? JSON.parse(page.ui_schema_json || '{}') - : page.ui_schema_json || {}; + const uiSchema = parseStructuredUiSchema(page.ui_schema_json); if (!uiSchema?.elements || !Array.isArray(uiSchema.elements)) return page; @@ -1171,14 +1374,18 @@ class TourPagesService extends BaseService { if (!modified) return page; // Return modified page with updated ui_schema_json - const plainPage = page.get ? page.get({ plain: true }) : { ...page }; + const plainPage = toPlainTourPage(page); return { ...plainPage, ui_schema_json: JSON.stringify(uiSchema), }; }); - return isArray ? modifiedPages : modifiedPages[0]; + if (isArray) { + return modifiedPages; + } + + return modifiedPages[0] ?? pages; } /** @@ -1188,7 +1395,9 @@ class TourPagesService extends BaseService { * @param {string[]} storageKeys - Array of original video storage keys to check * @returns {Promise} Status object with ready keys and their reversed URLs */ - static async checkReverseVideoStatus(storageKeys) { + static async checkReverseVideoStatus( + storageKeys: string[], + ): Promise { if ( !storageKeys || !Array.isArray(storageKeys) || @@ -1197,8 +1406,8 @@ class TourPagesService extends BaseService { return { ready: {}, pending: [], allReady: true }; } - const ready = {}; - const pending = []; + const ready: Record = {}; + const pending: string[] = []; for (const storageKey of storageKeys) { if (!storageKey) continue; @@ -1224,4 +1433,4 @@ class TourPagesService extends BaseService { } } -module.exports = TourPagesService; +export default TourPagesService; diff --git a/backend/src/services/users.js b/backend/src/services/users.js deleted file mode 100644 index ff3b661..0000000 --- a/backend/src/services/users.js +++ /dev/null @@ -1,316 +0,0 @@ -const db = require('../db/models'); -const UsersDBApi = require('../db/api/users'); -const { createEntityService } = require('../factories/service.factory'); -const { - assertCreateOptions, - assertIdOptions, - assertUpdateOptions, -} = require('../contracts/entity-options'); -const ValidationError = require('./notifications/errors/validation'); -const config = require('../config'); -const AuthService = require('./auth'); -const { logger } = require('../utils/logger'); -const AccessPolicy = require('./access-policy'); - -// Generate base service from factory -const BaseUsersService = createEntityService(UsersDBApi, { - entityName: 'Users', -}); - -/** - * Users service with email invitation functionality - * Extends factory-generated service with custom user logic - */ -class UsersService extends BaseUsersService { - static normalizeIdArray(value) { - if (!Array.isArray(value)) return []; - return value - .map((item) => { - if (typeof item === 'string') return item; - if (item && typeof item === 'object') return item.id || item.value; - return null; - }) - .filter(Boolean); - } - - static normalizeRoleId(value) { - if (!value) return null; - if (typeof value === 'string') return value; - if (typeof value === 'object') return value.id || value.value || null; - return null; - } - - static async createProductionPresentationAccessForPublicUser({ - user, - data, - currentUser, - transaction, - }) { - if (!user?.id) return; - - await db.production_presentation_access.destroy({ - where: { userId: user.id }, - transaction, - }); - - const selectedProjectIds = this.normalizeIdArray( - data.allowed_private_production_project_ids, - ); - - const roleId = this.normalizeRoleId(data.app_role); - if (!selectedProjectIds.length || !roleId) return; - - const role = await db.roles.findByPk(roleId, { transaction }); - if (role?.name !== 'Public') return; - - const privateProjects = await db.projects.findAll({ - where: { - id: { - [db.Sequelize.Op.in]: selectedProjectIds, - }, - production_presentation_visibility: 'private', - }, - attributes: ['id'], - transaction, - }); - - if (!privateProjects.length) return; - - const now = new Date(); - await db.production_presentation_access.bulkCreate( - privateProjects.map((project) => ({ - projectId: project.id, - userId: user.id, - createdById: currentUser?.id || null, - updatedById: currentUser?.id || null, - createdAt: now, - updatedAt: now, - })), - { ignoreDuplicates: true, transaction }, - ); - } - - static async assertPublicUserHasNoAdminPermissions(data, transaction) { - const roleId = this.normalizeRoleId(data.app_role); - if (!roleId) return false; - - const role = await db.roles.findByPk(roleId, { transaction }); - if (role?.name !== 'Public') return false; - - const customPermissions = this.normalizeIdArray(data.custom_permissions); - if (customPermissions.length > 0) { - throw new ValidationError( - 'Public users cannot receive custom permissions', - ); - } - - return true; - } - - static async updateProductionPresentationAccessForPublicUser({ - user, - data, - currentUser, - transaction, - }) { - if (!user?.id) return; - - await db.production_presentation_access.destroy({ - where: { userId: user.id }, - transaction, - }); - - await this.createProductionPresentationAccessForPublicUser({ - user, - data, - currentUser, - transaction, - }); - } - - /** - * Create user with email validation and optional invitation - */ - static async create(options) { - assertCreateOptions(options, 'Service'); - const { - data, - currentUser, - sendInvitationEmails = true, - host, - transaction: externalTransaction, - runtimeContext, - } = options; - const transaction = - externalTransaction || (await db.sequelize.transaction()); - const ownsTransaction = !externalTransaction; - const email = data.email; - - try { - if (!email) { - throw new ValidationError('iam.errors.emailRequired'); - } - - const existingUser = await db.users.findOne({ - where: { email }, - paranoid: false, - transaction, - }); - - if (existingUser && !existingUser.deletedAt) { - throw new ValidationError('iam.errors.userAlreadyExists'); - } - - const isPublicUserData = await this.assertPublicUserHasNoAdminPermissions( - data, - transaction, - ); - const sanitizedData = isPublicUserData - ? { ...data, custom_permissions: [] } - : data; - - let user; - - if (existingUser?.deletedAt) { - await existingUser.restore({ transaction }); - user = await UsersDBApi.update({ - id: existingUser.id, - data: sanitizedData, - currentUser, - transaction, - runtimeContext, - }); - } else { - user = await UsersDBApi.create({ - data: sanitizedData, - currentUser, - transaction, - runtimeContext, - }); - } - - await this.createProductionPresentationAccessForPublicUser({ - user, - data: sanitizedData, - currentUser, - transaction, - }); - if (ownsTransaction) await transaction.commit(); - - // Send invitation email after successful commit - if (sendInvitationEmails) { - AuthService.sendPasswordResetEmail(email, 'invitation', host).catch( - (error) => { - logger.error( - { err: error, email }, - 'Failed to send user invitation email', - ); - }, - ); - } - } catch (error) { - if (ownsTransaction) await transaction.rollback(); - throw error; - } - } - - static async update(options) { - assertUpdateOptions(options, 'Service'); - const { - id, - data, - currentUser, - transaction: externalTransaction, - runtimeContext, - } = options; - const transaction = - externalTransaction || (await db.sequelize.transaction()); - const ownsTransaction = !externalTransaction; - - try { - const existingUser = await UsersDBApi.findBy( - { id }, - { transaction, runtimeContext }, - ); - - if (!existingUser) { - throw new ValidationError('UsersNotFound'); - } - - const roleId = this.normalizeRoleId(data.app_role); - const role = roleId - ? await db.roles.findByPk(roleId, { transaction }) - : null; - const nextRoleName = role?.name || existingUser.app_role?.name; - const nextUser = { - ...existingUser, - app_role: { name: nextRoleName }, - custom_permissions: - data.custom_permissions || existingUser.custom_permissions, - }; - - if (AccessPolicy.isPublicUser(nextUser)) { - await this.assertPublicUserHasNoAdminPermissions( - { - ...data, - app_role: roleId || existingUser.app_role?.id, - }, - transaction, - ); - } - - const sanitizedData = AccessPolicy.isPublicUser(nextUser) - ? { ...data, custom_permissions: [] } - : data; - - const user = await UsersDBApi.update({ - id, - data: sanitizedData, - currentUser, - transaction, - runtimeContext, - }); - - if ( - Object.prototype.hasOwnProperty.call( - data, - 'allowed_private_production_project_ids', - ) - ) { - await this.updateProductionPresentationAccessForPublicUser({ - user, - data, - currentUser, - transaction, - }); - } - - if (ownsTransaction) await transaction.commit(); - return user; - } catch (error) { - if (ownsTransaction) await transaction.rollback(); - throw error; - } - } - - /** - * Remove user with self-deletion and permission checks - */ - static async remove(options) { - assertIdOptions(options, 'Service', 'remove'); - const { id, currentUser, transaction, runtimeContext } = options; - - if (currentUser.id === id) { - throw new ValidationError('iam.errors.deletingHimself'); - } - - if (currentUser.app_role?.name !== config.roles.admin) { - throw new ValidationError('errors.forbidden.message'); - } - - // Delegate to parent (factory) implementation - return super.remove({ id, currentUser, transaction, runtimeContext }); - } -} - -module.exports = UsersService; diff --git a/backend/src/services/users.ts b/backend/src/services/users.ts new file mode 100644 index 0000000..8c4d360 --- /dev/null +++ b/backend/src/services/users.ts @@ -0,0 +1,434 @@ +import type { Transaction } from 'sequelize'; + +import db from '../db/models/index.ts'; +import UsersDBApi from '../db/api/users.ts'; +import { createEntityService } from '../factories/service.factory.ts'; +import { + assertCreateOptions, + assertIdOptions, + assertUpdateOptions, +} from '../contracts/entity-options.ts'; +import ValidationError from './notifications/errors/validation.ts'; +import config from '../config.ts'; +import AuthService from './auth.ts'; +import { logger } from '../utils/logger.ts'; +import AccessPolicy from './access-policy.ts'; +import type { + UserAccessMutationOptions, + UserCreateOptions, + UserData, + UserRecord, + UserRemoveOptions, + UserSelectableIdInput, + UserUpdateOptions, + CurrentUser, + RuntimeContext, + UserAutocompleteOption, + UserListFilter, +} from '../types/index.ts'; + +const BaseUsersService = createEntityService< + UserRecord, + UserData, + UserData, + UserListFilter, + UserAutocompleteOption +>( + UsersDBApi, + { + entityName: 'Users', + }, +); + +const buildRuntimeOptions = ( + transaction: Transaction, + runtimeContext: RuntimeContext | undefined, +): { transaction: Transaction; runtimeContext?: RuntimeContext } => { + const options: { + transaction: Transaction; + runtimeContext?: RuntimeContext; + } = { transaction }; + + if (runtimeContext !== undefined) { + options.runtimeContext = runtimeContext; + } + + return options; +}; + +const normalizeSelectableId = (value: UserSelectableIdInput): string | null => { + if (!value) { + return null; + } + if (typeof value === 'string') { + return value; + } + return value.id || value.value || null; +}; + +const normalizeSelectableIdArray = ( + value: UserData['custom_permissions'], +): string[] => { + if (!Array.isArray(value)) { + return []; + } + + return value.flatMap((item) => { + const normalized = normalizeSelectableId(item); + return normalized ? [normalized] : []; + }); +}; + +const buildWriteOptions = ( + transaction: Transaction, + runtimeContext: RuntimeContext | undefined, + currentUser: CurrentUser | null | undefined, +): { + transaction: Transaction; + runtimeContext?: RuntimeContext; + currentUser?: CurrentUser | null; +} => { + const options: { + transaction: Transaction; + runtimeContext?: RuntimeContext; + currentUser?: CurrentUser | null; + } = buildRuntimeOptions(transaction, runtimeContext); + + if (currentUser !== undefined) { + options.currentUser = currentUser; + } + + return options; +}; + +const buildAccessPolicyUser = ( + existingUser: UserRecord, + roleName: string | undefined, + customPermissions: UserData['custom_permissions'], +): CurrentUser => { + const user: CurrentUser = { + id: existingUser.id, + app_role: roleName ? { name: roleName } : null, + }; + + if (typeof existingUser.email === 'string') { + user.email = existingUser.email; + } + + if (customPermissions !== undefined) { + user.custom_permissions = normalizeSelectableIdArray(customPermissions); + } else if (existingUser.custom_permissions !== undefined) { + user.custom_permissions = existingUser.custom_permissions; + } + + return user; +}; + +export default class UsersService extends BaseUsersService { + static normalizeIdArray(value: UserData['custom_permissions']): string[] { + return normalizeSelectableIdArray(value); + } + + static normalizeRoleId(value: UserSelectableIdInput): string | null { + return normalizeSelectableId(value); + } + + static async createProductionPresentationAccessForPublicUser({ + user, + data, + currentUser, + transaction, + }: UserAccessMutationOptions): Promise { + if (!user?.id) { + return; + } + + await db.production_presentation_access.destroy({ + where: { userId: user.id }, + transaction, + }); + + const selectedProjectIds = this.normalizeIdArray( + data.allowed_private_production_project_ids, + ); + + const roleId = this.normalizeRoleId(data.app_role); + if (!selectedProjectIds.length || !roleId) { + return; + } + + const role = await db.roles.findByPk(roleId, { transaction }); + if (role?.name !== 'Public') { + return; + } + + const privateProjects = await db.projects.findAll({ + where: { + id: { + [db.Sequelize.Op.in]: selectedProjectIds, + }, + production_presentation_visibility: 'private', + }, + attributes: ['id'], + transaction, + }); + + if (!privateProjects.length) { + return; + } + + const now = new Date(); + await db.production_presentation_access.bulkCreate( + privateProjects.map((project) => ({ + projectId: project.id, + userId: user.id, + createdById: currentUser?.id || null, + updatedById: currentUser?.id || null, + createdAt: now, + updatedAt: now, + })), + { ignoreDuplicates: true, transaction }, + ); + } + + static async assertPublicUserHasNoAdminPermissions( + data: UserData, + transaction: Transaction, + ): Promise { + const roleId = this.normalizeRoleId(data.app_role); + if (!roleId) { + return false; + } + + const role = await db.roles.findByPk(roleId, { transaction }); + if (role?.name !== 'Public') { + return false; + } + + const customPermissions = this.normalizeIdArray(data.custom_permissions); + if (customPermissions.length > 0) { + throw new ValidationError( + 'Public users cannot receive custom permissions', + ); + } + + return true; + } + + static async updateProductionPresentationAccessForPublicUser( + options: UserAccessMutationOptions, + ): Promise { + if (!options.user?.id) { + return; + } + + await db.production_presentation_access.destroy({ + where: { userId: options.user.id }, + transaction: options.transaction, + }); + + await this.createProductionPresentationAccessForPublicUser(options); + } + + static override async create(options: UserCreateOptions): Promise { + assertCreateOptions(options, 'Service'); + const { + data, + currentUser, + sendInvitationEmails = true, + host, + transaction: externalTransaction, + runtimeContext, + } = options; + const transaction = + externalTransaction || (await db.sequelize.transaction()); + const ownsTransaction = !externalTransaction; + const email = data.email; + + try { + if (!email) { + throw new ValidationError('iam.errors.emailRequired'); + } + + const existingUser = await db.users.findOne({ + where: { email }, + paranoid: false, + transaction, + }); + + if (existingUser && !existingUser.deletedAt) { + throw new ValidationError('iam.errors.userAlreadyExists'); + } + + const isPublicUserData = await this.assertPublicUserHasNoAdminPermissions( + data, + transaction, + ); + const sanitizedData = isPublicUserData + ? { ...data, custom_permissions: [] } + : data; + + const writeOptions = buildWriteOptions( + transaction, + runtimeContext, + currentUser, + ); + let user: UserRecord; + + if (existingUser?.deletedAt) { + await existingUser.restore({ transaction }); + user = await UsersDBApi.update({ + id: existingUser.id, + data: sanitizedData, + ...writeOptions, + }); + } else { + user = await UsersDBApi.create({ + data: sanitizedData, + ...writeOptions, + }); + } + + await this.createProductionPresentationAccessForPublicUser({ + user, + data: sanitizedData, + currentUser, + transaction, + }); + if (ownsTransaction) { + await transaction.commit(); + } + + if (sendInvitationEmails) { + AuthService.sendPasswordResetEmail(email, 'invitation', host).catch( + (error: unknown) => { + logger.error( + { err: error, email }, + 'Failed to send user invitation email', + ); + }, + ); + } + + return user; + } catch (error) { + if (ownsTransaction) { + await transaction.rollback(); + } + throw error; + } + } + + static override async update(options: UserUpdateOptions): Promise { + assertUpdateOptions(options, 'Service'); + const { + id, + data, + currentUser, + transaction: externalTransaction, + runtimeContext, + } = options; + const transaction = + externalTransaction || (await db.sequelize.transaction()); + const ownsTransaction = !externalTransaction; + + try { + const runtimeOptions = buildRuntimeOptions(transaction, runtimeContext); + const existingUser = await UsersDBApi.findBy({ id }, runtimeOptions); + + if (!existingUser) { + throw new ValidationError('UsersNotFound'); + } + + const roleId = this.normalizeRoleId(data.app_role); + const role = roleId + ? await db.roles.findByPk(roleId, { transaction }) + : null; + const nextRoleName = role?.name || existingUser.app_role?.name; + const nextUser = buildAccessPolicyUser( + existingUser, + nextRoleName, + data.custom_permissions, + ); + + if (AccessPolicy.isPublicUser(nextUser)) { + const publicCheckData: UserData = { ...data }; + const nextRoleId = roleId || existingUser.app_role?.id; + if (nextRoleId !== undefined) { + publicCheckData.app_role = nextRoleId; + } + + await this.assertPublicUserHasNoAdminPermissions( + publicCheckData, + transaction, + ); + } + + const sanitizedData = AccessPolicy.isPublicUser(nextUser) + ? { ...data, custom_permissions: [] } + : data; + + const writeOptions = buildWriteOptions( + transaction, + runtimeContext, + currentUser, + ); + const user = await UsersDBApi.update({ + id, + data: sanitizedData, + ...writeOptions, + }); + + if ( + Object.prototype.hasOwnProperty.call( + data, + 'allowed_private_production_project_ids', + ) + ) { + await this.updateProductionPresentationAccessForPublicUser({ + user, + data, + currentUser, + transaction, + }); + } + + if (ownsTransaction) { + await transaction.commit(); + } + return user; + } catch (error) { + if (ownsTransaction) { + await transaction.rollback(); + } + throw error; + } + } + + static override async remove(options: UserRemoveOptions): Promise { + assertIdOptions(options, 'Service', 'remove'); + const { id, currentUser, transaction, runtimeContext } = options; + + if (!currentUser) { + throw new ValidationError('errors.forbidden.message'); + } + + if (currentUser.id === id) { + throw new ValidationError('iam.errors.deletingHimself'); + } + + if (currentUser.app_role?.name !== config.roles.admin) { + throw new ValidationError('errors.forbidden.message'); + } + + const removeOptions: UserRemoveOptions = { id, currentUser }; + if (transaction !== undefined) { + removeOptions.transaction = transaction; + } + if (runtimeContext !== undefined) { + removeOptions.runtimeContext = runtimeContext; + } + + return super.remove(removeOptions); + } +} diff --git a/backend/src/services/videoProcessing.js b/backend/src/services/videoProcessing.ts similarity index 64% rename from backend/src/services/videoProcessing.js rename to backend/src/services/videoProcessing.ts index ca554f6..87ba3f2 100644 --- a/backend/src/services/videoProcessing.js +++ b/backend/src/services/videoProcessing.ts @@ -5,40 +5,63 @@ * Used for generating reversed videos for back navigation transitions. */ -const ffmpeg = require('fluent-ffmpeg'); -const ffmpegPath = require('ffmpeg-static'); -const ffprobePath = require('ffprobe-static').path; -const fs = require('fs').promises; -const path = require('path'); -const os = require('os'); -const { logger } = require('../utils/logger'); +import { promises as fs } from 'fs'; +import { createRequire } from 'node:module'; +import os from 'os'; +import path from 'path'; -// Configure fluent-ffmpeg to use bundled binaries -ffmpeg.setFfmpegPath(ffmpegPath); +import ffmpeg from 'fluent-ffmpeg'; +import { path as ffprobePath } from 'ffprobe-static'; + +import type { FfmpegJobRunner, MediaMetadata } from '../types/index.ts'; +import { logger } from '../utils/logger.ts'; + +const loadCommonJsModule = createRequire(import.meta.url); +const ffmpegStaticValue: unknown = loadCommonJsModule('ffmpeg-static'); +const ffmpegPath = typeof ffmpegStaticValue === 'string' ? ffmpegStaticValue : null; + +if (ffmpegPath) { + ffmpeg.setFfmpegPath(ffmpegPath); +} ffmpeg.setFfprobePath(ffprobePath); -let ffmpegQueueTail = Promise.resolve(); +let ffmpegQueueTail: Promise = Promise.resolve(); let queuedFfmpegJobs = 0; let ffmpegJobSequence = 0; -function parseFrameRate(value) { - if (!value) return null; +function parseFrameRate(value: unknown): number | null { + if (!value) { + return null; + } if (typeof value === 'number') { return Number.isFinite(value) && value > 0 ? value : null; } - const normalized = String(value).trim(); - if (!normalized || normalized === '0/0') return null; + if (typeof value !== 'string') { + return null; + } + + const normalized = value.trim(); + if (!normalized || normalized === '0/0') { + return null; + } if (normalized.includes('/')) { const [numeratorRaw, denominatorRaw] = normalized.split('/'); + if (!numeratorRaw || !denominatorRaw) { + return null; + } + const numerator = Number(numeratorRaw); const denominator = Number(denominatorRaw); if (!Number.isFinite(numerator) || !Number.isFinite(denominator)) { return null; } - if (denominator === 0) return null; + if (denominator === 0) { + return null; + } + const fps = numerator / denominator; return Number.isFinite(fps) && fps > 0 ? fps : null; } @@ -47,7 +70,14 @@ function parseFrameRate(value) { return Number.isFinite(parsed) && parsed > 0 ? parsed : null; } -async function enqueueFfmpegJob(jobName, runJob) { +function toError(value: unknown): Error { + return value instanceof Error ? value : new Error(String(value)); +} + +async function enqueueFfmpegJob( + jobName: string, + runJob: FfmpegJobRunner, +): Promise { const jobId = ++ffmpegJobSequence; const queuedAhead = queuedFfmpegJobs; @@ -80,32 +110,30 @@ async function enqueueFfmpegJob(jobName, runJob) { return jobPromise; } -/** - * Reverse a video using FFmpeg - * @param {Buffer} inputBuffer - Input video buffer - * @param {string} filename - Original filename (for extension) - * @returns {Promise} Reversed video buffer - */ -async function reverseVideo(inputBuffer, filename) { +async function reverseVideo( + inputBuffer: Buffer, + filename: string, +): Promise { return enqueueFfmpegJob('reverseVideo', () => reverseVideoWithoutQueue(inputBuffer, filename), ); } -async function reverseVideoWithoutQueue(inputBuffer, filename) { +async function reverseVideoWithoutQueue( + inputBuffer: Buffer, + filename: string, +): Promise { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'video-reverse-')); const ext = path.extname(filename) || '.mp4'; const inputPath = path.join(tempDir, `input${ext}`); const outputPath = path.join(tempDir, `reversed${ext}`); try { - // Write input buffer to temp file await fs.writeFile(inputPath, inputBuffer); logger.info({ inputPath, outputPath }, 'Starting video reversal'); - // Reverse video with FFmpeg - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { ffmpeg(inputPath) .outputOptions([ '-vf', @@ -143,10 +171,8 @@ async function reverseVideoWithoutQueue(inputBuffer, filename) { .run(); }); - // Read output as buffer return await fs.readFile(outputPath); } finally { - // Cleanup temp files try { await fs.rm(tempDir, { recursive: true, force: true }); } catch (cleanupErr) { @@ -155,22 +181,22 @@ async function reverseVideoWithoutQueue(inputBuffer, filename) { } } -async function probeMediaMetadata(filePath) { +async function probeMediaMetadata(filePath: string): Promise { return new Promise((resolve, reject) => { - ffmpeg.ffprobe(filePath, (err, metadata) => { + ffmpeg.ffprobe(filePath, (err: unknown, metadata) => { if (err) { - reject(err); + reject(toError(err)); return; } - const videoStream = metadata?.streams?.find( - (stream) => stream.codec_type === 'video', - ); - const audioStream = metadata?.streams?.find( - (stream) => stream.codec_type === 'audio', - ); - const primaryStream = videoStream || audioStream || null; - const formatDuration = Number(metadata?.format?.duration); + const videoStream = + metadata.streams.find((stream) => stream.codec_type === 'video') ?? + null; + const audioStream = + metadata.streams.find((stream) => stream.codec_type === 'audio') ?? + null; + const primaryStream = videoStream ?? audioStream; + const formatDuration = Number(metadata.format.duration); const streamDuration = Number(primaryStream?.duration); const durationSec = Number.isFinite(formatDuration) && formatDuration > 0 @@ -182,7 +208,7 @@ async function probeMediaMetadata(filePath) { const widthPx = Number(videoStream?.width); const heightPx = Number(videoStream?.height); const frameRate = - parseFrameRate(videoStream?.avg_frame_rate) || + parseFrameRate(videoStream?.avg_frame_rate) ?? parseFrameRate(videoStream?.r_frame_rate); resolve({ @@ -195,20 +221,12 @@ async function probeMediaMetadata(filePath) { }); } -/** - * Check if FFmpeg is available - * @returns {Promise} - */ -async function isFFmpegAvailable() { +async function isFFmpegAvailable(): Promise { return new Promise((resolve) => { - ffmpeg.getAvailableFormats((err) => { + ffmpeg.getAvailableFormats((err: unknown) => { resolve(!err); }); }); } -module.exports = { - reverseVideo, - isFFmpegAvailable, - probeMediaMetadata, -}; +export { isFFmpegAvailable, probeMediaMetadata, reverseVideo }; diff --git a/backend/src/types/access-logs-db.ts b/backend/src/types/access-logs-db.ts new file mode 100644 index 0000000..7e6905b --- /dev/null +++ b/backend/src/types/access-logs-db.ts @@ -0,0 +1,44 @@ +import type { + DbAssociationConfig, + DbRelationFilterConfig, + EntityDbApi, + EntityRecord, +} from './index.ts'; + +export type AccessLogEnvironment = string; + +export interface AccessLogData { + id?: string; + environment?: AccessLogEnvironment | null; + path?: string | null; + ip_address?: string | null; + user_agent?: string | null; + accessed_at?: string | Date | null; +} + +export interface AccessLogRecord extends EntityRecord { + environment?: AccessLogEnvironment | null; + path?: string | null; + ip_address?: string | null; + user_agent?: string | null; + accessed_at?: string | Date | null; +} + +export interface AccessLogFieldMapping { + id: string | undefined; + environment: AccessLogEnvironment | null; + path: string | null; + ip_address: string | null; + user_agent: string | null; + accessed_at: string | Date | null; +} + +export type AccessLogAssociationConfig = DbAssociationConfig; +export type AccessLogRelationFilterConfig = DbRelationFilterConfig; + +export type AccessLogsDbApi = EntityDbApi< + AccessLogRecord, + AccessLogData, + AccessLogData, + unknown +>; diff --git a/backend/src/types/access-policy-audit.ts b/backend/src/types/access-policy-audit.ts new file mode 100644 index 0000000..4ce6a2e --- /dev/null +++ b/backend/src/types/access-policy-audit.ts @@ -0,0 +1,87 @@ +import type { AccessPolicyOptions } from './access-policy.ts'; + +export interface PublicRolePermissionViolation { + roleId: string; + id: string; + name: string; +} + +export interface PublicUserCustomPermission { + id: string; + name: string; +} + +export interface PublicUserCustomPermissionsViolation { + id: string; + email: string; + customPermissions: PublicUserCustomPermission[]; +} + +export interface NonPublicProductionPresentationGrantViolation { + id: string; + userId: string; + userEmail: string | null; + userRole: string | null; + projectId: string; + projectSlug: string | null; +} + +export interface AccessPolicyAuditReport { + publicRolePermissions: PublicRolePermissionViolation[]; + publicUsersWithCustomPermissions: PublicUserCustomPermissionsViolation[]; + productionPresentationAccessForNonPublicUsers: NonPublicProductionPresentationGrantViolation[]; +} + +export interface AccessPolicyAuditCleanupResult { + before: AccessPolicyAuditReport; + removedPublicRolePermissions: number; + clearedPublicUserCustomPermissions: number; + removedNonPublicProductionPresentationGrants: number; +} + +export type AccessPolicyAuditOptions = AccessPolicyOptions; + +export interface AccessPolicyAuditPermissionRow { + id: string; + name: string; +} + +export interface AccessPolicyAuditRoleRow { + id: string; + permissions?: AccessPolicyAuditPermissionRow[]; +} + +export interface AccessPolicyAuditUserRow { + id: string; + email: string; + custom_permissions?: AccessPolicyAuditPermissionRow[]; +} + +export interface AccessPolicyAuditGrantRow { + id: string; + userId: string; + projectId: string; + user?: { + email?: string; + app_role?: { + name?: string; + } | null; + } | null; + project?: { + slug?: string; + } | null; +} + +export interface AccessPolicyAuditRoleMutator { + setPermissions( + permissions: readonly never[], + options: AccessPolicyAuditOptions, + ): Promise; +} + +export interface AccessPolicyAuditUserMutator { + setCustom_permissions( + permissions: readonly never[], + options: AccessPolicyAuditOptions, + ): Promise; +} diff --git a/backend/src/types/access-policy.ts b/backend/src/types/access-policy.ts new file mode 100644 index 0000000..c867359 --- /dev/null +++ b/backend/src/types/access-policy.ts @@ -0,0 +1,27 @@ +import type { Transaction } from 'sequelize'; + +import type { + CurrentUser, + PermissionName, + PermissionRecord, + RoleRecord, +} from './auth.ts'; + +export type ProductionPresentationVisibility = 'public' | 'private'; + +export interface AccessPolicyOptions { + transaction?: Transaction; +} + +export interface ProductionPresentationProject { + id: string; + name: string; + slug: string; + production_presentation_visibility: ProductionPresentationVisibility; +} + +export interface RoleWithPermissionLoader extends RoleRecord { + getPermissions?: () => Promise>; +} + +export type AccessPolicyUser = CurrentUser | null | undefined; diff --git a/backend/src/types/app.ts b/backend/src/types/app.ts new file mode 100644 index 0000000..e19f56f --- /dev/null +++ b/backend/src/types/app.ts @@ -0,0 +1,44 @@ +import type { ErrorRequestHandler, NextFunction, Request, RequestHandler, Response, Router } from 'express'; +import type swaggerJSDoc from 'swagger-jsdoc'; +import type * as swaggerUiExpress from 'swagger-ui-express'; + +import type { CurrentUser } from './auth.ts'; + +export type SwaggerDocumentOptions = swaggerJSDoc.Options; + +export type SwaggerUiModuleWithHost = typeof swaggerUiExpress & { + host?: string; +}; + +export type ExpressRouter = Router; + +export interface HealthResponse { + status: 'ok' | 'degraded'; + timestamp: string; + uptime: number; + environment: string; + database?: 'connected' | 'disconnected'; + databaseError?: string; +} + +export interface RuntimeJwtVerifyHandler { + (error: unknown, user: CurrentUser | false | null | undefined): Promise; +} + +export type RuntimeReadOrAuthMiddleware = RequestHandler; + +export interface MountRuntimeEntityRoute { + (path: string, entityName: string, router: ExpressRouter): void; +} + +export type AppErrorHandler = ErrorRequestHandler; + +export interface SwaggerHostMiddleware { + (req: Request, res: Response, next: NextFunction): void; +} + +export interface PublicAccessHardeningSummary { + publicRolePermissions: number; + publicUsersWithCustomPermissions: number; + productionPresentationAccessForNonPublicUsers: number; +} diff --git a/backend/src/types/assets.ts b/backend/src/types/assets.ts new file mode 100644 index 0000000..fc72db3 --- /dev/null +++ b/backend/src/types/assets.ts @@ -0,0 +1,168 @@ +import type { + CreateOptions, + DeleteByIdsOptions, + DbAssociationConfig, + DbFindByOptions, + DbRelationFilterConfig, + EntityDbApi, + EntityIdOptions, + UpdateOptions, +} from './index.ts'; + +export type AssetType = 'image' | 'video' | 'audio' | 'file' | 'embed'; + +export type AssetUsageType = + | 'icon' + | 'background_image' + | 'audio' + | 'video' + | 'transition' + | 'logo' + | 'favicon' + | 'document' + | 'general' + | 'embed_360' + | 'embed_3d' + | 'embed_iframe'; + +export interface AssetData { + id?: string; + name?: string | null; + asset_type?: AssetType | null; + type?: AssetUsageType | null; + cdn_url?: string | null; + storage_key?: string | null; + mime_type?: string | null; + size_mb?: number | string | null; + width_px?: number | null; + height_px?: number | null; + duration_sec?: number | string | null; + frame_rate?: number | string | null; + embed_code?: string | null; + embed_provider?: string | null; + checksum?: string | null; + is_public?: boolean; + projectId?: string | null; + project?: string | null; +} + +export interface AssetRecord extends AssetData { + id: string; + original_file_name?: string | null; + asset_variants_asset?: AssetVariantRecord[]; +} + +export interface AssetFieldMapping { + id: string | undefined; + name: string | null; + asset_type: AssetType | null; + type: AssetUsageType; + cdn_url: string | null; + storage_key: string | null; + mime_type: string | null; + size_mb: number | string | null; + width_px: number | null; + height_px: number | null; + duration_sec: number | string | null; + frame_rate: number | string | null; + embed_code: string | null; + embed_provider: string | null; + checksum: string | null; + is_public: boolean; + projectId: string | null; +} + +export interface AssetVariantData { + id?: string; + assetId?: string; + variant_type?: string; + cdn_url?: string | null; + storage_key?: string | null; + width_px?: number | null; + height_px?: number | null; + size_mb?: number | string | null; +} + +export interface AssetVariantRecord extends AssetVariantData { + id: string; +} + +export interface AssetVariantFieldMapping { + id: string | undefined; + assetId: string | null; + variant_type: string | null; + cdn_url: string | null; + storage_key: string | null; + width_px: number | null; + height_px: number | null; + size_mb: number | string | null; +} + +export type AssetVariantAssociationConfig = DbAssociationConfig; +export type AssetVariantRelationFilterConfig = DbRelationFilterConfig; + +export type AssetVariantsDbApi = EntityDbApi< + AssetVariantRecord, + AssetVariantData, + AssetVariantData, + unknown +>; + +export interface AssetListFilter { + name?: string; + cdn_url?: string; + storage_key?: string; + mime_type?: string; + checksum?: string; + embed_provider?: string; + asset_type?: AssetType; + type?: AssetUsageType; + is_public?: boolean; + projectId?: string; +} + +export interface AssetEmbedUrlResult { + url: string; + provider: string; +} + +export interface AssetMimePattern { + prefixes: string[]; + description: string; + skipValidation?: boolean; +} + +export type AssetMimePatternMap = Record; + +export interface ValidAssetMimeValidationResult { + valid: true; + skipValidation?: boolean; +} + +export interface InvalidAssetMimeValidationResult { + valid: false; + error: string; +} + +export type AssetMimeValidationResult = + | ValidAssetMimeValidationResult + | InvalidAssetMimeValidationResult; + +export type AssetFindByOptions = Pick< + UpdateOptions, + 'transaction' | 'runtimeContext' +>; + +export interface AssetsDbApi + extends EntityDbApi { + findBy(options: DbFindByOptions): Promise; + findBy( + where: { id?: string; storage_key?: string }, + options?: AssetFindByOptions, + ): Promise; +} + +export type AssetCreateOptions = CreateOptions; +export type AssetUpdateOptions = UpdateOptions; +export type AssetDeleteByIdsOptions = DeleteByIdsOptions; +export type AssetRemoveOptions = EntityIdOptions; diff --git a/backend/src/types/auth-routes.ts b/backend/src/types/auth-routes.ts new file mode 100644 index 0000000..01b0f0e --- /dev/null +++ b/backend/src/types/auth-routes.ts @@ -0,0 +1,40 @@ +import type { Query } from 'express-serve-static-core'; + +import type { CurrentUser } from './auth.ts'; +import type { UserData } from './users.ts'; + +export interface SigninLocalBody { + email: string; + password: string; +} + +export interface PasswordResetBody { + token: string; + password: string; +} + +export interface PasswordUpdateBody { + currentPassword: string; + newPassword: string; +} + +export interface SendPasswordResetEmailBody { + email: string; +} + +export interface ProfileBody { + profile: UserData; +} + +export interface VerifyEmailBody { + token: string; +} + +export interface SocialSigninQuery extends Query { + app?: string; +} + +export interface AuthMeResponse extends CurrentUser { + password?: string; + allowedPrivateProductionSlugs: string[]; +} diff --git a/backend/src/types/auth-service.ts b/backend/src/types/auth-service.ts new file mode 100644 index 0000000..828e43f --- /dev/null +++ b/backend/src/types/auth-service.ts @@ -0,0 +1,65 @@ +import type { CurrentUser } from './auth.ts'; +import type { EmailSendResult } from './email.ts'; +import type { + PasswordResetBody, + PasswordUpdateBody, + ProfileBody, + SigninLocalBody, +} from './auth-routes.ts'; +import type { ServiceOptions } from './service-options.ts'; +import type { UserRecord } from './users.ts'; + +export type PasswordResetEmailType = 'register' | 'invitation'; + +export interface AuthRequestContext extends ServiceOptions { + currentUser?: CurrentUser | null; +} + +export interface AuthPasswordUser extends CurrentUser { + password?: string | null; +} + +export interface AuthPasswordRequestContext extends ServiceOptions { + currentUser?: AuthPasswordUser | null; +} + +export interface AuthTokenPayload { + user: { + id: string; + email: string | null; + }; +} + +export interface AuthServiceClass { + signin( + email: SigninLocalBody['email'], + password: SigninLocalBody['password'], + ): Promise; + sendEmailAddressVerificationEmail( + email: string, + host?: string, + ): Promise; + sendPasswordResetEmail( + email: string, + type?: PasswordResetEmailType, + host?: string, + ): Promise; + verifyEmail( + token: PasswordResetBody['token'], + options?: AuthRequestContext, + ): Promise; + passwordUpdate( + currentPassword: PasswordUpdateBody['currentPassword'], + newPassword: PasswordUpdateBody['newPassword'], + options: AuthPasswordRequestContext, + ): Promise; + passwordReset( + token: PasswordResetBody['token'], + password: PasswordResetBody['password'], + options?: AuthRequestContext, + ): Promise; + updateProfile( + data: ProfileBody['profile'], + currentUser: CurrentUser, + ): Promise; +} diff --git a/backend/src/types/auth.ts b/backend/src/types/auth.ts new file mode 100644 index 0000000..12cb150 --- /dev/null +++ b/backend/src/types/auth.ts @@ -0,0 +1,38 @@ +export type PermissionName = string; +export type KnownRoleName = + | 'Administrator' + | 'Platform Owner' + | 'Account Manager' + | 'Tour Designer' + | 'Content Reviewer' + | 'Analytics Viewer' + | 'Public'; +export type RoleName = string; + +export interface PermissionRecord { + id?: string; + name: PermissionName; +} + +export interface RoleRecord { + id?: string; + name: RoleName; + globalAccess?: boolean; + permissions?: ReadonlyArray; +} + +export interface CurrentUser { + id: string; + email?: string; + firstName?: string; + lastName?: string; + app_role?: RoleRecord | null; + role?: RoleRecord | null; + app_role_permissions?: ReadonlyArray; + custom_permissions?: ReadonlyArray; +} + +export interface SocialAuthUserRecord { + id: string; + email: string | null; +} diff --git a/backend/src/types/config.ts b/backend/src/types/config.ts new file mode 100644 index 0000000..4f697af --- /dev/null +++ b/backend/src/types/config.ts @@ -0,0 +1,69 @@ +import type SMTPConnection from 'nodemailer/lib/smtp-connection/index.js'; +import type SMTPTransport from 'nodemailer/lib/smtp-transport/index.js'; + +export interface BackendEmailConfig + extends Omit { + from: string; + auth: SMTPConnection.Credentials; +} + +export interface BackendConfig { + gcloud: { + bucket: string; + hash: string; + }; + s3: { + bucket: string; + region: string; + accessKeyId: string; + secretAccessKey: string; + prefix: string; + connectionTimeout: number; + requestTimeout: number; + maxAttempts: number; + maxSockets: number; + keepAlive: boolean; + presignExpirySeconds: number; + }; + secret_key: string; + admin_pass: string; + user_pass: string; + admin_email: string; + email: BackendEmailConfig; + bcrypt: { + saltRounds: number; + }; + providers: { + LOCAL: string; + GOOGLE: string; + MICROSOFT: string; + }; + google: { + clientId: string; + clientSecret: string; + }; + microsoft: { + clientId: string; + clientSecret: string; + }; + roles: { + admin: string; + user?: string; + }; + remote: string; + port: string; + host: string; + hostUI: string; + portUI: string; + portUIProd: string; + swaggerUI: string; + swaggerPort: string; + swaggerUrl: string; + apiUrl: string; + uiUrl: string; + backUrl: string; + uploadDir: string; + s3CacheDir: string; + s3CacheEnabled: boolean; + s3CacheMaxAge: number; +} diff --git a/backend/src/types/db-api.ts b/backend/src/types/db-api.ts new file mode 100644 index 0000000..8e73aed --- /dev/null +++ b/backend/src/types/db-api.ts @@ -0,0 +1,147 @@ +import type { Includeable, WhereOptions } from 'sequelize'; +import type { Transaction } from 'sequelize'; + +import type { PaginatedResult } from './pagination.ts'; +import type { + AutocompleteOptions, + CreateOptions, + DeleteByIdsOptions, + EntityIdOptions, + ServiceOptions, + UpdateOptions, +} from './service-options.ts'; + +export interface EntityRecord { + id: string; +} + +export type DbPrimitive = string | number | boolean | Date | null | undefined; +export type DbData = Record; + +export interface DbFieldDefaultConfig { + default?: unknown; + nullDefault?: unknown; +} + +export type DbFieldTransformer = (value: unknown) => unknown; +export type DbFieldTransformers = Record; +export type DbFieldDefaults = Record; + +export interface GenericDbRecord extends EntityRecord { + [field: string]: unknown; + get(options: { plain: true }): EntityRecord; + update( + payload: DbData, + options: { transaction?: Transaction | undefined }, + ): Promise; + destroy(options: { transaction?: Transaction | undefined }): Promise; +} + +export interface GenericDbModel { + rawAttributes?: Record; + getTableName(): string; + create( + payload: DbData, + options: { transaction?: Transaction | undefined }, + ): Promise; + bulkCreate( + payload: DbData[], + options: { transaction?: Transaction | undefined }, + ): Promise; + findByPk( + id: string, + options: { transaction?: Transaction | undefined }, + ): Promise; + findOne(options: { + where: Record; + transaction?: Transaction | undefined; + include?: unknown[]; + }): Promise; + findAll(options: { + where?: Record; + attributes?: string[]; + limit?: number | undefined; + offset?: number | undefined; + order?: string[][]; + transaction?: Transaction | undefined; + }): Promise; + findAndCountAll(options: { + where: Record; + include: unknown[]; + distinct: true; + order: string[][]; + transaction?: Transaction | undefined; + limit?: number | undefined; + offset?: number | undefined; + }): Promise>; + count(options: { + where: Record; + include: unknown[]; + distinct: true; + transaction?: Transaction | undefined; + }): Promise; +} + +export interface GenericDbAutocompleteRecord extends EntityRecord { + [field: string]: unknown; +} + +export interface GenericDbListFilter { + [key: string]: unknown; + id?: string; + active?: string | boolean; + createdAtRange?: readonly [DbPrimitive, DbPrimitive]; + field?: string; + sort?: string; + limit?: string | number; + page?: string | number; +} + +export interface DbFindAllOptions extends ServiceOptions { + filter?: TFilter; + limit?: number; + offset?: number; +} + +export interface DbFindByOptions + extends ServiceOptions { + where: TWhere; + include?: Includeable[]; +} + +export interface DbAssociationConfig { + field: string; + setter: string; + isArray: boolean; +} + +export interface DbRelationFilterConfig { + filterKey: string; + model: unknown; + as: string; + searchField?: string; +} + +export interface EntityDbApi< + TEntity extends EntityRecord, + TCreate, + TUpdate, + TFilter, + TAutocomplete extends EntityRecord | { id: string } = TEntity, +> { + create(options: CreateOptions): Promise; + update(options: UpdateOptions): Promise; + partialUpdate(options: UpdateOptions>): Promise; + remove(options: EntityIdOptions): Promise; + deleteByIds(options: DeleteByIdsOptions): Promise; + findBy(options: DbFindByOptions): Promise; + findAll(options: DbFindAllOptions): Promise>; + findAllAutocomplete(options: AutocompleteOptions): Promise; +} + +export interface SingletonDbApi + extends EntityDbApi { + findOne(options?: ServiceOptions): Promise; + findBy(options: DbFindByOptions): Promise; + findBy(where: { id: string }, options?: ServiceOptions): Promise; +} diff --git a/backend/src/types/db-config.ts b/backend/src/types/db-config.ts new file mode 100644 index 0000000..00bca03 --- /dev/null +++ b/backend/src/types/db-config.ts @@ -0,0 +1,23 @@ +import type { Options } from 'sequelize'; + +import type { NodeEnvironment } from './env.ts'; + +export type SequelizeCliStorage = 'sequelize'; + +export interface SequelizeCliStorageOptions { + seederStorage: SequelizeCliStorage; + migrationStorage: SequelizeCliStorage; + migrationStorageTableName: string; +} + +export interface DatabaseEnvironmentConfig + extends Options, + SequelizeCliStorageOptions { + dialect: 'postgres'; + use_env_variable?: string; +} + +export type DatabaseConfigMap = Record< + Extract, + DatabaseEnvironmentConfig +>; diff --git a/backend/src/types/db-models.ts b/backend/src/types/db-models.ts new file mode 100644 index 0000000..dbbd56a --- /dev/null +++ b/backend/src/types/db-models.ts @@ -0,0 +1,593 @@ +import type { + AccessPolicyAuditGrantRow, + AccessPolicyAuditRoleMutator, + AccessPolicyAuditRoleRow, + AccessPolicyAuditUserMutator, + AccessPolicyAuditUserRow, + AccessPolicyOptions, + PrivateProductionProjectRow, + ProjectCloneAssetPayload, + ProjectCloneCreatedAsset, + ProjectCloneGenericPayload, + ProjectCloneJsonRecord, + ProjectModelApi, + ProjectModelRecord, + ProjectCloneSourceRecord, + ProjectCloneVariantPayload, + ElementTypeDefaultsModel, + ProjectElementDefaultsModel, + ProjectSlugLookupWhere, + ProjectAudioTrackModel, + ProjectTransitionSettingsModel, + ProjectUiControlSettingsModel, + PublishClonePayload, + PublishCloneSource, + PublishEventRecord, + PublishEventStatus, + PublishServiceDbProject, + PublishSourceEnvironment, + PublishTargetEnvironment, + GlobalTransitionDefaultsModel, + GlobalUiControlDefaultsModel, + FileModel, + ProductionPresentationAccessGrantRow, + ProductionPresentationProject, + PaginatedResult, + QueryWhere, + SqlQueryRow, + RuntimeProjectInclude, + RuntimeEnvironment, + TourPageRecord, + TourPageCreatePayload, + UserPrivateProject, + UserProductionPresentationAccessPayload, + UserProductionPresentationAccessRecord, + UserPublicRole, + UserRestorableRecord, + SocialAuthUserRecord, + UserModelApi, + UserModelRecord, + PermissionRecord, + RoleModelRecord, + SampleDataModel, +} from './index.ts'; +import type { QueryTypes, SyncOptions, Transaction } from 'sequelize'; + +export interface SequelizeQueryOptions { + transaction?: Transaction; + type?: QueryTypes.SELECT; +} + +export interface ProjectModel extends ProjectModelApi { + findByPk( + id: string, + options: { + include: readonly [ + { + model: unknown; + as: 'assets_project'; + required: false; + include: readonly [ + { + model: unknown; + as: 'asset_variants_asset'; + required: false; + }, + ]; + }, + ]; + transaction: Transaction; + }, + ): Promise; + findByPk( + id: string, + options: { + transaction: Transaction; + lock: unknown; + }, + ): Promise; + findByPk( + id: string, + options: { + attributes: readonly ['slug']; + }, + ): Promise<{ slug: string } | null>; + findOne(options: { + where: QueryWhere; + transaction?: Transaction | undefined; + include: unknown[]; + }): Promise; + findOne(options: { + where: { slug: string }; + attributes: readonly [ + 'id', + 'name', + 'slug', + 'production_presentation_visibility', + ]; + transaction?: AccessPolicyOptions['transaction']; + }): Promise; + findOne(options: { + where: ProjectSlugLookupWhere; + paranoid: false; + transaction: Transaction; + }): Promise; + findAll(options: { + where: { + production_presentation_visibility: 'private'; + }; + attributes: readonly ['id', 'name', 'slug']; + order: readonly [readonly ['name', 'ASC']]; + transaction?: AccessPolicyOptions['transaction']; + }): Promise; + findAll(options: { + where: { + id: Record; + production_presentation_visibility: 'private'; + }; + attributes: readonly ['id']; + transaction: Transaction; + }): Promise; +} + +export interface ProjectCloneAssetModel { + create( + data: ProjectCloneAssetPayload, + options: { transaction: Transaction }, + ): Promise; +} + +export interface ProjectCloneVariantModel { + create( + data: ProjectCloneVariantPayload, + options: { transaction: Transaction }, + ): Promise; +} + +export interface ProjectCloneCollectionModel { + findAll(options: { + where: { + projectId: string; + environment?: 'dev'; + }; + transaction: Transaction; + }): Promise; + destroy(options: { + where: { + projectId: string; + }; + transaction: Transaction; + force: true; + }): Promise; + create( + data: ProjectCloneGenericPayload, + options: { transaction: Transaction }, + ): Promise; +} + +export interface ProjectCloneSettingsModel { + findOne(options: { + where: { + projectId: string; + environment: 'dev'; + }; + transaction: Transaction; + }): Promise; + create( + data: ProjectCloneGenericPayload, + options: { transaction: Transaction }, + ): Promise; +} + +export interface PublishEventModel { + create(data: { + projectId: string; + userId: string | null; + title: string; + description: string; + from_environment: PublishSourceEnvironment; + to_environment: PublishTargetEnvironment; + status: PublishEventStatus; + createdById: string | null; + updatedById: string | null; + }): Promise; + findOne(options: { + where: { + projectId: string; + status: PublishEventStatus; + }; + order: readonly [readonly ['createdAt', 'DESC']]; + transaction: Transaction; + lock: unknown; + }): Promise; +} + +export interface PublishCloneCollectionModel { + findAll(options: { + where: { + projectId: string; + environment: PublishSourceEnvironment; + }; + transaction: Transaction; + }): Promise; + destroy(options: { + where: { + projectId: string; + environment: PublishTargetEnvironment; + }; + transaction: Transaction; + force: true; + }): Promise; + bulkCreate( + data: PublishClonePayload[], + options: { + transaction: Transaction; + returning: false; + }, + ): Promise; +} + +export interface PublishCloneSingletonModel { + findOne(options: { + where: { + projectId: string; + environment: PublishSourceEnvironment; + }; + transaction: Transaction; + }): Promise; + destroy(options: { + where: { + projectId: string; + environment: PublishTargetEnvironment; + }; + transaction: Transaction; + force: true; + }): Promise; + create( + data: PublishClonePayload, + options: { + transaction: Transaction; + }, + ): Promise; +} + +export interface TourPageModelRecord extends TourPageRecord { + get(options: { plain: true }): TourPageRecord; + setProject( + projectId: string | null, + options: { transaction?: Transaction | undefined }, + ): Promise; +} + +export interface TourPageModel { + create( + data: TourPageCreatePayload, + options: { + transaction?: Transaction | undefined; + }, + ): Promise; + create( + data: ProjectCloneGenericPayload, + options: { + transaction: Transaction; + }, + ): Promise; + destroy(options: { + where: { + projectId: string; + environment: PublishTargetEnvironment; + }; + transaction: Transaction; + force: true; + }): Promise; + destroy(options: { + where: { + projectId: string; + }; + transaction: Transaction; + force: true; + }): Promise; + bulkCreate( + data: PublishClonePayload[], + options: { + transaction: Transaction; + returning: false; + }, + ): Promise; + findAndCountAll(options: { + where: QueryWhere; + include: RuntimeProjectInclude[]; + distinct: true; + order: string[][]; + transaction?: Transaction | undefined; + limit?: number | undefined; + offset?: number | undefined; + }): Promise>; + findOne(options: { + where: QueryWhere; + transaction?: Transaction | undefined; + include?: RuntimeProjectInclude[]; + }): Promise; + findAll(options: { + where: { + projectId: string; + environment: PublishSourceEnvironment | 'dev'; + }; + transaction: Transaction; + }): Promise; + findAll(options: { + where: { + projectId: string; + environment: 'dev'; + }; + attributes: readonly ['id']; + transaction: Transaction; + }): Promise>>; + findOne(options: { + where: { + id: string; + }; + transaction: Transaction; + }): Promise; + findOne(options: { + where: { + projectId: string; + environment: RuntimeEnvironment; + slug: string; + }; + attributes: readonly ['id']; + transaction: Transaction; + }): Promise | null>; + findOne(options: { + where: { + id?: string; + projectId?: string; + environment?: RuntimeEnvironment; + slug?: string; + }; + attributes?: readonly ['id']; + transaction: Transaction; + }): Promise | null>; + max( + field: 'sort_order', + options: { + where: { + projectId: string; + environment: RuntimeEnvironment; + }; + transaction: Transaction; + }, + ): Promise; +} + +export interface ProductionPresentationAccessModel { + create( + data: UserProductionPresentationAccessPayload, + options: { transaction: Transaction }, + ): Promise; + findOne(options: { + where: { + projectId: string; + userId: string; + }; + transaction?: AccessPolicyOptions['transaction']; + }): Promise; + findAll(options: { + where: { userId: string }; + include: readonly [ + { + association: 'project'; + attributes: readonly ['id', 'name', 'slug']; + where: { production_presentation_visibility: 'private' }; + required: true; + }, + ]; + transaction?: Transaction | undefined; + }): Promise; + findAll(options: { + where: { userId: string }; + include: readonly [ + { + association: 'project'; + attributes: readonly ['slug', 'production_presentation_visibility']; + where: { production_presentation_visibility: 'private' }; + required: true; + }, + ]; + transaction?: AccessPolicyOptions['transaction']; + }): Promise; + findAll(options: { + attributes: readonly ['id', 'userId', 'projectId']; + include: readonly [ + { + association: 'user'; + attributes: readonly ['id', 'email']; + required: true; + include: readonly [ + { + association: 'app_role'; + attributes: readonly ['id', 'name']; + required: false; + }, + ]; + }, + { + association: 'project'; + attributes: readonly ['id', 'name', 'slug']; + required: false; + }, + ]; + transaction?: AccessPolicyOptions['transaction']; + }): Promise; + destroy(options: { + where: { id: Record }; + transaction?: AccessPolicyOptions['transaction']; + }): Promise; + destroy(options: { + where: { userId: string }; + transaction: Transaction; + }): Promise; + bulkCreate( + data: UserProductionPresentationAccessPayload[], + options: { + ignoreDuplicates: true; + transaction: Transaction; + }, + ): Promise; +} + +export interface RoleModel { + create( + data: { name: string }, + options: { transaction: Transaction }, + ): Promise; + findOne(options: { + where: { name: string }; + }): Promise; + findAll(options: { + where: { name: 'Public' }; + include: readonly [{ association: 'permissions' }]; + transaction?: AccessPolicyOptions['transaction']; + }): Promise; + findByPk( + id: string, + options: { + transaction: Transaction; + }, + ): Promise; + findByPk( + id: string, + options: AccessPolicyOptions, + ): Promise; +} + +export interface PermissionModel { + create( + data: { name: string }, + options: { transaction: Transaction }, + ): Promise; +} + +export interface UserModel extends UserModelApi { + findByPk( + id: string, + options: { transaction?: Transaction | undefined }, + ): Promise; + findAll(options: { + where: QueryWhere; + transaction?: Transaction | undefined; + attributes?: readonly string[]; + limit?: number | undefined; + offset?: number | undefined; + order?: string[][]; + orderBy?: string[][]; + include?: unknown[]; + }): Promise; + findOne(options: { + where: QueryWhere; + transaction?: Transaction | undefined; + include?: unknown[]; + attributes?: readonly string[]; + paranoid?: boolean; + }): Promise; + findAll(options: { + attributes: readonly ['id', 'email']; + include: readonly [ + { + association: 'app_role'; + attributes: readonly ['id', 'name']; + where: { name: 'Public' }; + required: true; + }, + { + association: 'custom_permissions'; + attributes: readonly ['id', 'name']; + required: true; + }, + ]; + transaction?: AccessPolicyOptions['transaction']; + }): Promise; + findByPk( + id: string, + options: AccessPolicyOptions, + ): Promise; + findOne(options: { + where: { email: string }; + paranoid: false; + transaction: Transaction; + }): Promise; + findOne(options: { + where: { email: string }; + transaction?: Transaction | undefined; + }): Promise; + findOrCreate(options: { + where: { + email: string; + provider: string; + }; + }): Promise<[SocialAuthUserRecord, boolean]>; +} + +export interface DbModels { + [modelName: string]: unknown; + sequelize: { + authenticate(): Promise; + close(): Promise; + transaction(): Promise; + transaction( + callback: (transaction: Transaction) => Promise, + ): Promise; + query( + sql: string, + options?: SequelizeQueryOptions, + ): Promise; + sync(options?: SyncOptions): Promise; + }; + Sequelize: { + QueryTypes: { + SELECT: QueryTypes.SELECT; + }; + Op: { + and: symbol; + gt: symbol; + in: symbol; + or: symbol; + iLike: symbol; + gte: symbol; + lte: symbol; + ne: symbol; + notIn: symbol; + }; + col(column: string): unknown; + cast(value: unknown, type: string): unknown; + where(leftOperand: unknown, whereAttributeHash: object): object; + }; + projects: ProjectModel & SampleDataModel; + assets: ProjectCloneAssetModel & SampleDataModel; + asset_variants: ProjectCloneVariantModel & SampleDataModel; + project_memberships: SampleDataModel; + presigned_url_requests: SampleDataModel; + publish_events: PublishEventModel & SampleDataModel; + tour_pages: TourPageModel & SampleDataModel; + project_audio_tracks: PublishCloneCollectionModel & + ProjectCloneCollectionModel & + ProjectAudioTrackModel & + SampleDataModel; + project_element_defaults: ProjectCloneCollectionModel & + ProjectElementDefaultsModel; + project_transition_settings: PublishCloneSingletonModel & + ProjectTransitionSettingsModel; + element_type_defaults: ElementTypeDefaultsModel; + project_ui_control_settings: PublishCloneSingletonModel & + ProjectCloneSettingsModel & + ProjectUiControlSettingsModel; + global_transition_defaults: GlobalTransitionDefaultsModel; + global_ui_control_defaults: GlobalUiControlDefaultsModel; + pwa_caches: SampleDataModel; + access_logs: SampleDataModel; + file: FileModel; + production_presentation_access: ProductionPresentationAccessModel; + roles: RoleModel; + permissions: PermissionModel; + users: UserModel & SampleDataModel; +} diff --git a/backend/src/types/db-seeders.ts b/backend/src/types/db-seeders.ts new file mode 100644 index 0000000..5d967f9 --- /dev/null +++ b/backend/src/types/db-seeders.ts @@ -0,0 +1,72 @@ +import type { Model, QueryInterface } from 'sequelize'; + +export interface SequelizeSeeder { + up(queryInterface: QueryInterface): Promise; + down?(queryInterface: QueryInterface): Promise; +} + +export interface SampleDataAssociationRecord extends Model { + setProject?: (project: Model | null) => Promise; + setUser?: (user: Model | null) => Promise; + setAsset?: (asset: Model | null) => Promise; +} + +export interface SampleDataModel { + bulkCreate(records: object[]): Promise; + count(): Promise; + findOne(options: { + order?: string[][]; + offset?: number; + }): Promise; +} + +export interface AdminUserSeedRow { + id: string; + firstName: string; + email: string; + emailVerified: boolean; + provider: string; + password: string; + createdAt: Date; + updatedAt: Date; +} + +export interface AdminUserSeedExistingIdRow { + id: string; +} + +export interface RbacSeedRoleDefinition { + key: string; + name: string; +} + +export interface RbacSeedRoleRow { + id: string; + name: string; + createdAt: Date; + updatedAt: Date; +} + +export interface RbacSeedPermissionRow { + id: string; + name: string; + createdAt: Date; + updatedAt: Date; +} + +export interface RbacSeedRolePermissionRow { + createdAt: Date; + updatedAt: Date; + roles_permissionsId: string; + permissionId: string; +} + +export interface RbacSeedExistingNamedIdRow { + id: string; + name: string; +} + +export interface RbacSeedExistingRolePermissionRow { + roles_permissionsId: string; + permissionId: string; +} diff --git a/backend/src/types/element-type-defaults.ts b/backend/src/types/element-type-defaults.ts new file mode 100644 index 0000000..bd8d732 --- /dev/null +++ b/backend/src/types/element-type-defaults.ts @@ -0,0 +1,113 @@ +import type { + CreateOptions, + DbFindAllOptions, + DbFindByOptions, + DeleteByIdsOptions, + ElementSettingsJson, + EntityDbApi, + EntityIdOptions, + PaginatedResult, + QueryWhere, + ServiceOptions, + UpdateOptions, +} from './index.ts'; + +export interface ElementTypeDefaultsData { + id?: string; + element_type?: string | null; + name?: string | null; + sort_order?: number | null; + default_settings_json?: ElementSettingsJson | string | null; + importHash?: string | null; +} + +export interface ElementTypeDefaultsRecord { + id: string; + element_type: string; + name: string; + sort_order: number; + default_settings_json?: ElementSettingsJson | string | null; +} + +export interface ElementTypeDefaultsFieldMapping { + id: string | undefined; + element_type: string | null; + name: string | null; + sort_order: number; + default_settings_json: string | null; +} + +export interface ElementTypeDefaultsFieldDefault { + default?: unknown; + nullDefault?: unknown; +} + +export type ElementTypeDefaultsFieldDefaults = Record< + string, + ElementTypeDefaultsFieldDefault +>; + +export interface ElementTypeDefaultsSeedRow { + element_type: string; + name: string; + sort_order: number; + default_settings_json: ElementSettingsJson; +} + +export interface ElementTypeDefaultsModelRecord extends ElementTypeDefaultsRecord { + get(options: { plain: true }): ElementTypeDefaultsRecord; +} + +export interface ElementTypeDefaultsModel { + count(): Promise; + sync(): Promise; + bulkCreate( + data: Array< + ElementTypeDefaultsFieldMapping & { + createdAt: Date; + updatedAt: Date; + } + >, + ): Promise; + findOne(options: { + where: QueryWhere; + transaction?: ServiceOptions['transaction']; + include?: unknown[]; + }): Promise; +} + +export interface ElementTypeDefaultsDbApi + extends EntityDbApi< + ElementTypeDefaultsRecord, + ElementTypeDefaultsData, + ElementTypeDefaultsData, + unknown + > { + ensureInitialized(): Promise; + bulkImport( + data: ElementTypeDefaultsData[], + options?: ServiceOptions, + ): Promise; + findBy( + where: { id: string }, + options?: ServiceOptions, + ): Promise; + findBy(options: DbFindByOptions): Promise; + findAll( + filter?: unknown, + options?: ServiceOptions, + ): Promise>; + findAll( + options?: DbFindAllOptions, + ): Promise>; + create( + options: CreateOptions, + ): Promise; + update( + options: UpdateOptions, + ): Promise; + deleteByIds( + options: DeleteByIdsOptions, + ): Promise; + remove(options: EntityIdOptions): Promise; +} diff --git a/backend/src/types/email.ts b/backend/src/types/email.ts new file mode 100644 index 0000000..89ef020 --- /dev/null +++ b/backend/src/types/email.ts @@ -0,0 +1,19 @@ +import type SMTPTransport from 'nodemailer/lib/smtp-transport/index.js'; + +export interface EmailTemplate { + to: string; + subject: string; + html(): Promise | string; +} + +export type EmailSendResult = SMTPTransport.SentMessageInfo; + +export interface LinkEmailTemplateOptions { + to: string; + link: string; +} + +export interface InvitationEmailTemplateOptions { + to: string; + host: string; +} diff --git a/backend/src/types/entity-router.ts b/backend/src/types/entity-router.ts new file mode 100644 index 0000000..a6860d5 --- /dev/null +++ b/backend/src/types/entity-router.ts @@ -0,0 +1,72 @@ +import type { Router } from 'express'; + +import type { EntityRecord } from './db-api.ts'; +import type { DbFindByOptions } from './db-api.ts'; +import type { PaginatedResult } from './pagination.ts'; +import type { RequestSchemaMap } from './request.ts'; +import type { + AutocompleteOptions, + CreateOptions, + DeleteByIdsOptions, + EntityIdOptions, + ServiceOptions, + UpdateOptions, +} from './service-options.ts'; + +export type EntityRouterValidationMap = Record; + +export interface EntityRouterOptions { + permissionEntity?: string; + validation?: EntityRouterValidationMap; + csvFields?: readonly string[]; + customRoutes?: ( + router: Router, + service: EntityRouterService, + dbApi: EntityRouterDbApi, + ) => void; +} + +export interface EntityRouterService { + create(options: CreateOptions): Promise; + bulkImport(req: Express.Request, res: Express.Response): Promise; + update(options: UpdateOptions): Promise; + remove(options: EntityIdOptions): Promise; + deleteByIds(options: DeleteByIdsOptions): Promise; +} + +export type EntityRouterSortDirection = 'ASC' | 'DESC'; + +export interface EntityRouterQuery { + [key: string]: unknown; + filetype?: unknown; + limit?: unknown; + page?: unknown; + sort?: unknown; + field?: unknown; + query?: unknown; + offset?: unknown; +} + +export interface NormalizedEntityRouterQuery + extends Omit { + limit: number; + page: number; + sort?: EntityRouterSortDirection; + field?: string; +} + +export interface EntityRouterDbApi { + SORTABLE_FIELDS?: readonly string[]; + CSV_FIELDS?: readonly string[]; + MODEL?: unknown; + findAll( + filter: NormalizedEntityRouterQuery, + options: { + countOnly?: boolean; + currentUser?: ServiceOptions['currentUser']; + runtimeContext?: ServiceOptions['runtimeContext']; + }, + ): Promise | number>; + findAllAutocomplete(options: AutocompleteOptions): Promise; + findBy(options: DbFindByOptions): Promise; +} diff --git a/backend/src/types/entity-service.ts b/backend/src/types/entity-service.ts new file mode 100644 index 0000000..aadca97 --- /dev/null +++ b/backend/src/types/entity-service.ts @@ -0,0 +1,69 @@ +import type { Request, Response } from 'express'; + +import type { EntityRecord } from './db-api.ts'; +import type { + BulkImportOptions, + CreateOptions, + DeleteByIdsOptions, + EntityIdOptions, + UpdateOptions, +} from './service-options.ts'; + +export interface EntityServiceClass< + TEntity extends EntityRecord = EntityRecord, + TCreate = unknown, + TUpdate = unknown, + TFilter = unknown, + TAutocomplete extends EntityRecord | { id: string } = TEntity, +> { + readonly __createType?: TCreate; + readonly __updateType?: TUpdate; + readonly __filterType?: TFilter; + readonly __autocompleteType?: TAutocomplete; + create(options: unknown): Promise; + bulkImport(req: Request, res: Response): Promise; + update(options: unknown): Promise; + remove(options: unknown): Promise; + deleteByIds(options: unknown): Promise; +} + +export interface EntityServiceConstructor< + TEntity extends EntityRecord = EntityRecord, + TCreate = unknown, + TUpdate = unknown, + TFilter = unknown, + TAutocomplete extends EntityRecord | { id: string } = TEntity, +> extends EntityServiceClass< + TEntity, + TCreate, + TUpdate, + TFilter, + TAutocomplete + > { + new (): object; +} + +export interface EntityServiceFactoryOptions { + entityName?: string; +} + +export interface EntityServiceDbApi< + TEntity extends EntityRecord = EntityRecord, + TCreate = unknown, + TUpdate = unknown, + TFilter = unknown, + TAutocomplete extends EntityRecord | { id: string } = TEntity, +> { + readonly __createType?: TCreate; + readonly __filterType?: TFilter; + readonly __autocompleteType?: TAutocomplete; + create(options: CreateOptions): Promise; + bulkImport(rows: readonly unknown[], options: BulkImportOptions): Promise; + update(options: UpdateOptions): Promise; + findBy( + where: { id: string }, + options: Pick, 'transaction' | 'runtimeContext'>, + ): Promise; + remove(options: EntityIdOptions): Promise; + deleteByIds(options: DeleteByIdsOptions): Promise; +} diff --git a/backend/src/types/env.ts b/backend/src/types/env.ts new file mode 100644 index 0000000..139e87e --- /dev/null +++ b/backend/src/types/env.ts @@ -0,0 +1,33 @@ +export type NodeEnvironment = + | 'development' + | 'test' + | 'production' + | 'dev_stage'; + +export interface ValidatedEnvironment { + NODE_ENV: NodeEnvironment; + PORT: number; + DB_HOST: string; + DB_PORT: number; + DB_NAME: string; + DB_USER: string; + DB_PASS: string; + SECRET_KEY: string; + ADMIN_PASS: string; + USER_PASS: string; + ADMIN_EMAIL: string; + GOOGLE_CLIENT_ID: string; + GOOGLE_CLIENT_SECRET: string; + MS_CLIENT_ID: string; + MS_CLIENT_SECRET: string; + AWS_ACCESS_KEY_ID: string; + AWS_SECRET_ACCESS_KEY: string; + AWS_S3_BUCKET: string; + AWS_S3_REGION: string; + AWS_S3_PREFIX: string; + EMAIL_USER: string; + EMAIL_PASS: string; + EMAIL_TLS_REJECT_UNAUTHORIZED: 'true' | 'false'; + PEXELS_KEY: string; + LOG_LEVEL: 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace'; +} diff --git a/backend/src/types/errors.ts b/backend/src/types/errors.ts new file mode 100644 index 0000000..c70adcf --- /dev/null +++ b/backend/src/types/errors.ts @@ -0,0 +1,7 @@ +export type ErrorDetails = unknown; + +export interface OperationalErrorOptions { + message: string; + statusCode?: number; + details?: ErrorDetails; +} diff --git a/backend/src/types/file.ts b/backend/src/types/file.ts new file mode 100644 index 0000000..61f8c26 --- /dev/null +++ b/backend/src/types/file.ts @@ -0,0 +1,404 @@ +import type { Request, Response } from 'express'; +import type { ParamsDictionary } from 'express-serve-static-core'; +import type { Stats } from 'fs'; +import type { Readable } from 'stream'; +import type { Bucket } from '@google-cloud/storage'; +import type { StreamingBlobPayloadOutputTypes } from '@smithy/types'; +import type { Transaction } from 'sequelize'; +import type S3StorageProvider from '../services/file/S3StorageProvider.ts'; +import type LocalStorageProvider from '../services/file/LocalStorageProvider.ts'; + +export interface FileErrorDetails { + invalidPaths?: string[]; + missingChunk?: number; +} + +export interface FileErrorResponse { + message: string; + code?: string; + details?: FileErrorDetails; +} + +export interface PresignRequestBody { + urls: string[]; +} + +export interface PresignSuccessResponse { + presignedUrls: Record; +} + +export interface FileUploadParams extends ParamsDictionary { + table: string; + field: string; +} + +export interface UploadSessionParams extends ParamsDictionary { + sessionId: string; +} + +export interface UploadChunkParams extends UploadSessionParams { + chunkIndex: string; +} + +export type FileServiceRequest = Request; +export type FileServiceResponse = Response; +export type StorageUploadData = Buffer | Readable; +export type StorageContentTypeMap = Record; + +export type FileUploadRequest = Request; +export type FileStorageProviderName = 's3' | 'gcloud' | 'local'; +export type FileMimeTypeMap = Record; + +export interface CachedFileLookupResult { + stats: Stats | null; + valid: boolean; +} + +export interface FileByteRange { + start: number; + end: number; +} + +export interface GCloudBucketState { + bucket: Bucket; + hash: string; +} + +export interface FileUploadBody { + filename?: string; +} + +export interface UploadSessionInitBody { + folder?: string; + filename?: string; + totalChunks?: string | number; + size?: string | number; + contentType?: string; +} + +export type FileUploadServiceRequest = Request< + ParamsDictionary, + unknown, + FileUploadBody +>; + +export type UploadSessionInitRequest = Request< + ParamsDictionary, + unknown, + UploadSessionInitBody +>; + +export type UploadSessionRequest = Request; +export type UploadChunkRequest = Request; + +export interface StorageOperationOptions { + signal?: AbortSignal; +} + +export interface StorageUploadOptions extends StorageOperationOptions { + contentType?: string; + metadata?: Record; +} + +export interface StorageUploadResult { + key: string; + url?: string; +} + +export interface StorageDownloadResult { + body: Readable | StreamingBlobPayloadOutputTypes | null; + contentType?: string; + contentLength?: number; +} + +export interface LocalStorageProviderOptions { + basePath?: string; +} + +export interface StorageDownloadOptions extends StorageOperationOptions { + headOnly?: boolean; + range?: string; +} + +export interface StorageCopyOptions extends StorageOperationOptions { + contentType?: string; +} + +export interface FileCopyOperation { + sourceKey: string; + destKey: string; + contentType?: string; +} + +export interface FileCopyParallelOptions { + concurrency?: number; + continueOnError?: boolean; +} + +export interface FileCopySuccess { + sourceKey: string; + destKey: string; +} + +export interface FileCopyFailure extends FileCopySuccess { + error: string; +} + +export interface FileCopyParallelResult { + succeeded: FileCopySuccess[]; + failed: FileCopyFailure[]; +} + +export interface FileDeleteOptions { + throwOnError?: boolean; +} + +export interface FileDeleteResult { + success: boolean; + error?: Error; +} + +export interface FileDownloadToTempFileResult { + filePath: string; + cleanup(): Promise; +} + +export interface FileUploadBufferOptions { + contentType?: string; +} + +export interface FileUploadBufferResult { + url: string; +} + +export interface FileCopyOptions { + contentType?: string; +} + +export interface FileCopyResult { + url?: string; + key?: string; +} + +export interface FileServiceFacade { + getFileStorageProvider(): FileStorageProviderName; + getS3Provider(): S3StorageProvider; + getLocalProvider(): LocalStorageProvider; + getGCloudBucket(): GCloudBucketState; + uploadFile( + folder: string, + req: FileServiceRequest, + res: FileServiceResponse, + ): Promise; + downloadFile( + req: FileServiceRequest, + res: FileServiceResponse, + ): Promise; + deleteFile( + privateUrl: string, + options?: FileDeleteOptions, + ): Promise; + downloadToBuffer(privateUrl: string): Promise; + downloadToTempFile(privateUrl: string): Promise; + uploadBuffer( + privateUrl: string, + buffer: Buffer, + options?: FileUploadBufferOptions, + ): Promise; + copyFile( + sourceKey: string, + destKey: string, + options?: FileCopyOptions, + ): Promise; + copyFilesParallel( + copies: FileCopyOperation[], + options?: FileCopyParallelOptions, + ): Promise; + getMimeTypeFromExtension(filepath: string): string; + initUploadSession( + req: FileServiceRequest, + res: FileServiceResponse, + ): Promise; + getUploadSession( + req: FileServiceRequest, + res: FileServiceResponse, + ): Promise; + uploadChunk( + req: FileServiceRequest, + res: FileServiceResponse, + ): Promise; + finalizeUploadSession( + req: FileServiceRequest, + res: FileServiceResponse, + ): Promise; + generatePresignedUrls(urls: readonly string[]): Promise>; + isValidPath(urlPath: unknown): urlPath is string; + createErrorResponse( + message: string, + code?: string | null, + details?: FileErrorDetails | null, + ): FileErrorResponse; + getS3ErrorStatusCode(error: unknown): number; +} + +export interface RelationFileRecord { + id?: string; + name?: string; + sizeInBytes?: number; + privateUrl?: string; + publicUrl?: string; + new?: boolean; +} + +export interface FileRelationDescriptor { + belongsTo: string; + belongsToColumn: string; + belongsToId: string; +} + +export interface FileDbCurrentUser { + id: string | null; +} + +export interface FileDbOptions { + currentUser?: FileDbCurrentUser | null; + transaction?: Transaction; +} + +export type RelationFileInput = + | RelationFileRecord + | RelationFileRecord[] + | null + | undefined; + +export interface FileModelCreatePayload { + belongsTo: string; + belongsToColumn: string; + belongsToId: string; + name?: string | undefined; + sizeInBytes?: number | undefined; + privateUrl?: string | undefined; + publicUrl?: string | undefined; + createdById: string | null; + updatedById: string | null; +} + +export interface FileModelRecord { + id: string; + privateUrl: string; + destroy(options: { transaction?: Transaction | undefined }): Promise; +} + +export interface FileModel { + create( + payload: FileModelCreatePayload, + options: { transaction?: Transaction | undefined }, + ): Promise; + findAll(options: { + where: { + belongsTo: string; + belongsToId: string; + belongsToColumn: string; + id: Record; + }; + transaction?: Transaction | undefined; + }): Promise; +} + +export interface FileDbApi { + replaceRelationFiles( + relation: FileRelationDescriptor, + rawFiles: RelationFileInput, + options?: FileDbOptions, + ): Promise; +} + +export interface StorageDeleteManyError { + key: string; + error: string; +} + +export interface StorageDeleteManyResult { + deleted: string[]; + errors: StorageDeleteManyError[]; +} + +export interface S3StorageProviderOptions { + bucket: string; + region?: string; + accessKeyId?: string; + secretAccessKey?: string; + prefix?: string; + connectionTimeout?: number; + requestTimeout?: number; + maxAttempts?: number; + maxSockets?: number; + keepAlive?: boolean; +} + +export interface S3StorageProviderConfig { + region: string; + connectionTimeout: number; + requestTimeout: number; + maxAttempts: number; + maxSockets: number; + keepAlive: boolean; +} + +export interface UploadSessionManagerOptions { + sessionDir: string; + ttlMs?: number; +} + +export interface UploadSessionCreateOptions { + filename: string; + folder: string; + totalChunks: number; + totalSize?: number; + userId?: string; + contentType?: string; +} + +export interface UploadSessionChunkMeta { + size: number; + uploadedAt: string; +} + +export type UploadSessionUploadedChunks = Record; + +export interface UploadSessionMeta { + sessionId: string; + filename: string; + folder: string; + totalChunks: number; + totalSize: number; + userId: string | null; + contentType: string | null; + uploadedChunks: UploadSessionUploadedChunks; + createdAt: string; + updatedAt: string; +} + +export type S3RetryableErrorCode = + | 'TimeoutError' + | 'RequestTimeout' + | 'NetworkingError' + | 'ServiceUnavailable' + | 'SlowDown' + | 'InternalError' + | 'ThrottlingException' + | 'TooManyRequestsException' + | 'ECONNRESET' + | 'ECONNREFUSED' + | 'ETIMEDOUT' + | 'EPIPE'; + +export type FilePresignRequest = Request< + ParamsDictionary, + PresignSuccessResponse | FileErrorResponse, + PresignRequestBody +>; + +export type FilePresignResponse = Response< + PresignSuccessResponse | FileErrorResponse +>; diff --git a/backend/src/types/global-defaults.ts b/backend/src/types/global-defaults.ts new file mode 100644 index 0000000..1902cbf --- /dev/null +++ b/backend/src/types/global-defaults.ts @@ -0,0 +1,99 @@ +export type TransitionType = + | 'fade' + | 'slide-left' + | 'slide-right' + | 'zoom' + | 'none'; + +export type TransitionEasing = + | 'ease-in-out' + | 'ease-in' + | 'ease-out' + | 'linear'; + +export interface GlobalTransitionDefaultsData { + id?: string; + transition_type?: TransitionType; + duration_ms?: number; + easing?: TransitionEasing; + overlay_color?: string; +} + +export interface GlobalTransitionDefaultsRecord + extends Required { + id: string; + createdAt?: Date; + updatedAt?: Date; +} + +export interface GlobalTransitionDefaultsFieldMapping { + id: string | undefined; + transition_type: TransitionType; + duration_ms: number; + easing: TransitionEasing; + overlay_color: string; +} + +export interface GlobalTransitionDefaultsFieldDefaults { + transition_type: { default: TransitionType }; + duration_ms: { default: number }; + easing: { default: TransitionEasing }; + overlay_color: { default: string }; +} + +export interface GlobalTransitionDefaultsModelRecord { + get(options: { plain: true }): GlobalTransitionDefaultsRecord; +} + +export interface GlobalTransitionDefaultsModel { + count(): Promise; + sync(): Promise; + create( + data: GlobalTransitionDefaultsFieldMapping & { + createdAt: Date; + updatedAt: Date; + }, + ): Promise; + findOne(options: { + transaction?: unknown; + }): Promise; +} + +export type GlobalUiControlSettingsJson = Record; + +export interface GlobalUiControlDefaultsData { + id?: string; + settings_json?: GlobalUiControlSettingsJson; + settings?: GlobalUiControlSettingsJson; +} + +export interface GlobalUiControlDefaultsRecord { + id: string; + settings_json: GlobalUiControlSettingsJson; + createdAt?: Date; + updatedAt?: Date; +} + +export interface GlobalUiControlDefaultsFieldMapping { + id: string | undefined; + settings_json: GlobalUiControlSettingsJson; +} + +export interface GlobalUiControlDefaultsModelRecord { + get(options: { plain: true }): GlobalUiControlDefaultsRecord; +} + +export interface GlobalUiControlDefaultsModel { + count(): Promise; + sync(): Promise; + create( + data: { + settings_json: GlobalUiControlSettingsJson; + createdAt: Date; + updatedAt: Date; + }, + ): Promise; + findOne(options: { + transaction?: unknown; + }): Promise; +} diff --git a/backend/src/types/http.ts b/backend/src/types/http.ts new file mode 100644 index 0000000..f24fc1e --- /dev/null +++ b/backend/src/types/http.ts @@ -0,0 +1,54 @@ +import type { NextFunction, Request, Response } from 'express'; +import type { + ParamsDictionary, + Query, +} from 'express-serve-static-core'; + +import type { RequestValidationDetail } from './validation.ts'; + +export type AsyncRequestHandler< + TParams extends ParamsDictionary = ParamsDictionary, + TResBody = unknown, + TReqBody = unknown, + TReqQuery extends Query = Query, +> = ( + req: Request, + res: Response, + next: NextFunction, +) => Promise; + +export interface RouteError extends Error { + code?: number; + status?: number; + details?: RequestValidationDetail[]; + isRequestValidation?: boolean; +} + +export interface RouteIdRequestLike { + params: { + id: string; + [key: string]: string; + }; + body: unknown; +} + +export type RouteIdRequest = Request<{ id: string }, unknown, RouteIdRequestBody>; + +export interface RouteIdRequestBody { + id?: string; + data?: { + id?: string; + [key: string]: unknown; + }; + [key: string]: unknown; +} + +export interface RouteEntityDataRequestBody { + id?: string; + data: TData; + [key: string]: unknown; +} + +export interface EntityDataRequestBody { + data: TData; +} diff --git a/backend/src/types/index.ts b/backend/src/types/index.ts new file mode 100644 index 0000000..57a61a5 --- /dev/null +++ b/backend/src/types/index.ts @@ -0,0 +1,622 @@ +export type { + AssetCreateOptions, + AssetVariantAssociationConfig, + AssetData, + AssetDeleteByIdsOptions, + AssetEmbedUrlResult, + AssetFieldMapping, + AssetVariantFieldMapping, + AssetVariantRelationFilterConfig, + AssetFindByOptions, + AssetListFilter, + AssetMimePattern, + AssetMimePatternMap, + AssetMimeValidationResult, + AssetRecord, + AssetRemoveOptions, + AssetVariantData, + AssetVariantRecord, + AssetVariantsDbApi, + AssetsDbApi, + AssetType, + AssetUpdateOptions, + AssetUsageType, + InvalidAssetMimeValidationResult, + ValidAssetMimeValidationResult, +} from './assets.ts'; +export type { + SequelizeDataTypes, + SequelizeModel, + SequelizeModelFactory, + SequelizeModelRegistry, +} from './sequelize-models.ts'; +export type { + ElementTypeDefaultsData, + ElementTypeDefaultsDbApi, + ElementTypeDefaultsFieldDefault, + ElementTypeDefaultsFieldDefaults, + ElementTypeDefaultsFieldMapping, + ElementTypeDefaultsModel, + ElementTypeDefaultsModelRecord, + ElementTypeDefaultsRecord, + ElementTypeDefaultsSeedRow, +} from './element-type-defaults.ts'; +export type { + AuthPasswordRequestContext, + AuthPasswordUser, + AuthRequestContext, + AuthServiceClass, + AuthTokenPayload, + PasswordResetEmailType, +} from './auth-service.ts'; +export type { + AccessPolicyOptions, + AccessPolicyUser, + ProductionPresentationProject, + ProductionPresentationVisibility, + RoleWithPermissionLoader, +} from './access-policy.ts'; +export type { + AccessPolicyAuditCleanupResult, + AccessPolicyAuditGrantRow, + AccessPolicyAuditOptions, + AccessPolicyAuditPermissionRow, + AccessPolicyAuditReport, + AccessPolicyAuditRoleMutator, + AccessPolicyAuditRoleRow, + AccessPolicyAuditUserMutator, + AccessPolicyAuditUserRow, + NonPublicProductionPresentationGrantViolation, + PublicRolePermissionViolation, + PublicUserCustomPermission, + PublicUserCustomPermissionsViolation, +} from './access-policy-audit.ts'; +export type { + AccessLogAssociationConfig, + AccessLogData, + AccessLogEnvironment, + AccessLogFieldMapping, + AccessLogRecord, + AccessLogRelationFilterConfig, + AccessLogsDbApi, +} from './access-logs-db.ts'; +export type { + CurrentUser, + KnownRoleName, + PermissionName, + PermissionRecord, + RoleName, + RoleRecord, + SocialAuthUserRecord, +} from './auth.ts'; +export type { + PermissionData, + PermissionDbRecord, + PermissionFieldMapping, + PermissionsDbApi, +} from './permissions.ts'; +export type { + RoleBulkImportOptions, + RoleBulkImportRequest, + RoleBulkImportResponse, + RoleAssociationConfig, + RoleCreateOptions, + RoleCsvRow, + RoleData, + RoleDeleteByIdsOptions, + RoleFieldMapping, + RoleModelRecord, + RoleRelationFilterConfig, + RoleRemoveOptions, + RolesDbApi, + RoleServiceRecord, + RoleUpdateOptions, +} from './roles.ts'; +export type { + AuthMeResponse, + PasswordResetBody, + PasswordUpdateBody, + ProfileBody, + SendPasswordResetEmailBody, + SigninLocalBody, + SocialSigninQuery, + VerifyEmailBody, +} from './auth-routes.ts'; +export type { BackendConfig, BackendEmailConfig } from './config.ts'; +export type { + AppErrorHandler, + ExpressRouter, + HealthResponse, + MountRuntimeEntityRoute, + PublicAccessHardeningSummary, + RuntimeJwtVerifyHandler, + RuntimeReadOrAuthMiddleware, + SwaggerDocumentOptions, + SwaggerHostMiddleware, + SwaggerUiModuleWithHost, +} from './app.ts'; +export type { + EmailSendResult, + EmailTemplate, + InvitationEmailTemplateOptions, + LinkEmailTemplateOptions, +} from './email.ts'; +export type { + DatabaseConfigMap, + DatabaseEnvironmentConfig, + SequelizeCliStorage, + SequelizeCliStorageOptions, +} from './db-config.ts'; +export type { + AdminUserSeedExistingIdRow, + AdminUserSeedRow, + RbacSeedExistingNamedIdRow, + RbacSeedExistingRolePermissionRow, + RbacSeedPermissionRow, + RbacSeedRoleDefinition, + RbacSeedRolePermissionRow, + RbacSeedRoleRow, + SampleDataAssociationRecord, + SampleDataModel, + SequelizeSeeder, +} from './db-seeders.ts'; +export type { + RateLimitEntry, + RateLimitKeyGenerator, + RateLimiterOptions, + RateLimitSkipPredicate, +} from './rate-limit.ts'; +export type { + QueryWhere, + RuntimeContext, + RuntimeEnvironment, + RuntimeContextInspectionResponse, + RuntimePublicAllowedPathMap, + RuntimePublicFieldMap, + RuntimePublicListResponse, + RuntimePublicPathPattern, + RuntimePublicPlainRecord, + RuntimePublicSequelizeRecord, + RuntimeFilterOptions, + RuntimeMode, + RuntimeProjectInclude, + RuntimeReadableEnvironment, + UnknownRuntimeContext, +} from './runtime.ts'; +export type { + ElementSettingsJson, + GlobalElementDefaultRecord, + ProjectElementDefaultRecord, + ProjectElementDefaultsData, + ProjectElementDefaultsDbApi, + ProjectElementDefaultsDiff, + ProjectElementDefaultsDiffWithGlobal, + ProjectElementDefaultsDiffWithoutGlobal, + ProjectElementDefaultsFieldMapping, + ProjectElementDefaultsListFilter, + ProjectElementDefaultsModel, + ProjectElementDefaultsModelRecord, + ProjectElementDefaultsOptions, + ProjectElementDefaultsRangeFilter, + ProjectElementDefaultsRangeValue, +} from './project-element-defaults.ts'; +export type { + ProjectUiControlSettingsData, + ProjectUiControlSettingsDbApi, + ProjectUiControlSettingsFieldMapping, + ProjectUiControlSettingsModel, + ProjectUiControlSettingsModelRecord, + ProjectUiControlSettingsRecord, + ProjectUiControlSettingsRuntimeOptions, + ProjectUiControlSettingsUpsertOptions, +} from './project-ui-control-settings.ts'; +export type { + PwaCacheAssociationConfig, + PwaCacheData, + PwaCacheEnvironment, + PwaCacheFieldMapping, + PwaCacheRecord, + PwaCacheRelationFilterConfig, + PwaCachesDbApi, +} from './pwa-caches-db.ts'; +export type { + PresignedUrlRequestAssetType, + PresignedUrlRequestAssociationConfig, + PresignedUrlRequestData, + PresignedUrlRequestFieldMapping, + PresignedUrlRequestPurpose, + PresignedUrlRequestRecord, + PresignedUrlRequestRelationFilterConfig, + PresignedUrlRequestsDbApi, +} from './presigned-url-requests-db.ts'; +export type { + ProjectTransitionEasing, + ProjectTransitionSettingsBulkImportOptions, + ProjectTransitionSettingsBulkImportRequest, + ProjectTransitionSettingsBulkImportResponse, + ProjectTransitionSettingsCreateOptions, + ProjectTransitionSettingsCsvRow, + ProjectTransitionSettingsData, + ProjectTransitionSettingsDbApi, + ProjectTransitionSettingsDeleteByIdsOptions, + ProjectTransitionSettingsFieldMapping, + ProjectTransitionSettingsFindAllOptions, + ProjectTransitionSettingsListFilter, + ProjectTransitionSettingsModel, + ProjectTransitionSettingsModelRecord, + ProjectTransitionSettingsRangeFilter, + ProjectTransitionSettingsRangeValue, + ProjectTransitionSettingsRecord, + ProjectTransitionSettingsRemoveOptions, + ProjectTransitionSettingsRuntimeOptions, + ProjectTransitionSettingsUpdateOptions, + ProjectTransitionType, +} from './project-transition-settings.ts'; +export type { + ProjectEnvironmentRouteParams, + ProjectSettingsListFilter, + RouteMessageResponse, + RouteSuccessResponse, +} from './project-settings.ts'; +export type { + ProjectCloneAssetPayload, + ProjectCloneAssetRecord, + ProjectCloneCreatedAsset, + ProjectCloneCreateOptions, + ProjectCloneCurrentUser, + ProjectCloneGenericPayload, + ProjectCloneJsonData, + ProjectCloneJsonRecord, + ProjectCloneSourceRecord, + ProjectCloneTransactionOptions, + ProjectCloneUiSchemaValue, + ProjectCloneVariantPayload, + ProjectCloneVariantRecord, + ProjectCreateOptions, + ProjectCreatePayload, + ProjectData, + ProjectFieldMapping, + ProjectFindAllOptions, + ProjectListFilter, + ProjectModelApi, + ProjectModelRecord, + ProjectProductionPresentationVisibility, + ProjectRangeFilter, + ProjectRangeValue, + ProjectRecord, + ProjectSlugLookupWhere, + ProjectsDbApi, + ProjectUpdateOptions, +} from './projects.ts'; +export type { + ProjectMembershipAccessLevel, + ProjectMembershipAssociationConfig, + ProjectMembershipData, + ProjectMembershipFieldMapping, + ProjectMembershipRecord, + ProjectMembershipRelationFilterConfig, + ProjectMembershipsDbApi, +} from './project-memberships-db.ts'; +export type { + PublishEventAssociationConfig, + PublishEventData, + PublishEventDbRecord, + PublishEventFieldMapping, + PublishEventRelationFilterConfig, + PublishEventsDbApi, +} from './publish-events-db.ts'; +export type { + MutableRouteLayer, + PrivateProductionProjectOption, + UserAccessMutationOptions, + UserAuthCreatePayload, + UserAutocompleteOption, + UserCreatePayload, + UserCreateBody, + UserCreateOptions, + UserData, + UserFileRecord, + UserFindAllOptions, + UserFindAndCountAllOptions, + UserFindByWhere, + UserListFilter, + UserModelApi, + UserModelRecord, + UserPrivateProject, + UserProductionPresentationAccessPlain, + UserProductionPresentationAccessRecord, + UserProductionPresentationAccessPayload, + UserRecord, + UserPublicRole, + UserRangeFilter, + UserRangeValue, + UserRemoveOptions, + UserRestorableRecord, + UserSelectableIdArrayInput, + UserSelectableIdInput, + UserTokenFields, + UserUpdatePayload, + UsersDbApi, + UserUpdateOptions, +} from './users.ts'; +export type { + TourPageAutocompleteOptions, + TourPageBuildDuplicateSlugOptions, + TourPageCreateBody, + TourPageCreateOptions, + TourPageCreatePayload, + TourPageCreateRequest, + TourPageData, + TourPageDecodedBytesEstimateInput, + TourPageDeleteByIdsBody, + TourPageDeleteByIdsOptions, + TourPageDeleteByIdsRequest, + TourPageDuplicateBody, + TourPageDuplicateData, + TourPageDuplicateRequest, + TourPageElement, + TourPageFieldMapping, + TourPageFindAllOptions, + TourPageInfoPanelSection, + TourPageListQuery, + TourPageListRequest, + TourPageListResult, + TourPageMaybeSequelizeRecord, + TourPageModelApi, + TourPageModelRecord, + TourPageNestedUiItem, + TourPagePlainRecord, + TourPageProcessReverseOptions, + TourPageRecord, + TourPageRangeFilter, + TourPageRangeValue, + TourPageRemoveOptions, + TourPageReorderBody, + TourPageReorderData, + TourPageReorderRequest, + TourPageReverseGenerationTask, + TourPageReverseVideoStatus, + TourPageReverseVideoStatusBody, + TourPageReverseVideoStatusRequest, + TourPageRouteParams, + TourPageSequelizeRecord, + TourPagesDbApi, + TourPageStructuredUiSchema, + TourPageUpdateOptions, + TourPageUiSchema, + TourPageVideoMetadata, + TourPageUpdateBody, + TourPageUpdateRequest, +} from './tour-pages.ts'; +export type { + PrivateProductionPresentationOption, + PrivateProductionProjectRow, + ProductionPresentationAccessGrantPlain, + ProductionPresentationAccessGrantRow, + ProductionPresentationAccessProject, + RuntimePresentationAccessOptions, +} from './runtime-presentation-access.ts'; +export type { ErrorDetails, OperationalErrorOptions } from './errors.ts'; +export type { + AsyncRequestHandler, + EntityDataRequestBody, + RouteEntityDataRequestBody, + RouteError, + RouteIdRequest, + RouteIdRequestBody, + RouteIdRequestLike, +} from './http.ts'; +export type { + ExecuteSqlErrorResponse, + ExecuteSqlRequestBody, + ExecuteSqlResponseMeta, + ExecuteSqlSuccessResponse, + InvalidSqlValidationResult, + ReadOnlySqlValidationOptions, + SqlQueryRow, + SqlQueryScalar, + SqlValidationResult, + SqlQueryValue, + ValidSqlValidationResult, +} from './sql.ts'; +export type { NodeEnvironment, ValidatedEnvironment } from './env.ts'; +export type { + DbModels, + PermissionModel, + ProductionPresentationAccessModel, + ProjectModel, + RoleModel, + SequelizeQueryOptions, + TourPageModel, + UserModel, +} from './db-models.ts'; +export type { NotificationCatalog } from './notifications.ts'; +export type { + FileCopyFailure, + FileCopyOperation, + FileCopyOptions, + FileCopyParallelOptions, + FileCopyParallelResult, + FileCopyResult, + FileCopySuccess, + FileDeleteOptions, + FileDeleteResult, + FileDbApi, + FileDbCurrentUser, + FileDbOptions, + FileDownloadToTempFileResult, + FileErrorDetails, + FileErrorResponse, + FileModel, + FileModelCreatePayload, + FileModelRecord, + FilePresignRequest, + FilePresignResponse, + FileRelationDescriptor, + FileServiceFacade, + FileServiceRequest, + FileServiceResponse, + FileStorageProviderName, + FileUploadBufferOptions, + FileUploadBufferResult, + FileUploadRequest, + FileUploadServiceRequest, + FileUploadParams, + CachedFileLookupResult, + FileByteRange, + FileMimeTypeMap, + GCloudBucketState, + LocalStorageProviderOptions, + PresignRequestBody, + PresignSuccessResponse, + RelationFileInput, + RelationFileRecord, + S3RetryableErrorCode, + S3StorageProviderConfig, + S3StorageProviderOptions, + StorageContentTypeMap, + StorageCopyOptions, + StorageDeleteManyError, + StorageDeleteManyResult, + StorageDownloadResult, + StorageDownloadOptions, + StorageOperationOptions, + StorageUploadData, + StorageUploadOptions, + StorageUploadResult, + UploadChunkParams, + UploadChunkRequest, + UploadSessionChunkMeta, + UploadSessionCreateOptions, + UploadSessionInitBody, + UploadSessionInitRequest, + UploadSessionManagerOptions, + UploadSessionMeta, + UploadSessionParams, + UploadSessionRequest, + UploadSessionUploadedChunks, +} from './file.ts'; +export type { + ProjectAudioTrackBulkImportOptions, + ProjectAudioTrackBulkImportRequest, + ProjectAudioTrackBulkImportResponse, + ProjectAudioTrackCreateOptions, + ProjectAudioTrackCsvRow, + ProjectAudioTrackData, + ProjectAudioTrackDeleteByIdsOptions, + ProjectAudioTrackFieldMapping, + ProjectAudioTrackListFilter, + ProjectAudioTrackModel, + ProjectAudioTrackModelRecord, + ProjectAudioTrackRangeFilter, + ProjectAudioTrackRangeValue, + ProjectAudioTrackRecord, + ProjectAudioTrackRemoveOptions, + ProjectAudioTrackRuntimeOptions, + ProjectAudioTracksDbApi, + ProjectAudioTrackUpdateOptions, +} from './project-audio-tracks.ts'; +export type { FileUploadProcessor } from './upload.ts'; +export type { + PublishCloneData, + PublishClonePayload, + PublishCloneSource, + PublishEventRecord, + PublishEventStatus, + PublishLockCallback, + PublishServiceCurrentUser, + PublishServiceDbProject, + PublishSourceEnvironment, + PublishSummary, + PublishTargetEnvironment, + PublishToProductionResult, + PublishToProductionRequestBody, + SaveToStageResult, + SaveToStageRequestBody, +} from './publish.ts'; +export type { + SearchFieldValue, + SearchRequestBody, + SearchResult, + SearchResultRow, +} from './search.ts'; +export type { FfmpegJobRunner, MediaMetadata } from './video-processing.ts'; +export type { + ListQueryOptions, + PaginatedResult, + PaginationOptions, + SortDirection, + SortOptions, +} from './pagination.ts'; +export type { + RequestValidationDetail, + RequestValidationErrorPayload, + RequestValidationPart, + ValidatedRequestParts, +} from './validation.ts'; +export type { + RequestSchemaCatalog, + RequestSchemaGroup, + RequestSchemaMap, +} from './request.ts'; +export type { + AutocompleteOptions, + BulkImportOptions, + CreateOptions, + DeleteByIdsOptions, + EntityIdOptions, + ServiceContext, + ServiceOptions, + TransactionalOptions, + UpdateOptions, +} from './service-options.ts'; +export type { + DbAssociationConfig, + DbData, + DbFieldDefaultConfig, + DbFieldDefaults, + DbFieldTransformer, + DbFieldTransformers, + DbFindAllOptions, + DbFindByOptions, + DbPrimitive, + DbRelationFilterConfig, + EntityDbApi, + EntityRecord, + GenericDbAutocompleteRecord, + GenericDbListFilter, + GenericDbModel, + GenericDbRecord, + SingletonDbApi, +} from './db-api.ts'; +export type { + EntityServiceClass, + EntityServiceConstructor, + EntityServiceDbApi, + EntityServiceFactoryOptions, +} from './entity-service.ts'; +export type { + EntityRouterDbApi, + EntityRouterOptions, + EntityRouterQuery, + EntityRouterService, + EntityRouterSortDirection, + EntityRouterValidationMap, + NormalizedEntityRouterQuery, +} from './entity-router.ts'; +export type { + GlobalTransitionDefaultsData, + GlobalTransitionDefaultsFieldDefaults, + GlobalTransitionDefaultsFieldMapping, + GlobalTransitionDefaultsModel, + GlobalTransitionDefaultsModelRecord, + GlobalTransitionDefaultsRecord, + GlobalUiControlDefaultsData, + GlobalUiControlDefaultsFieldMapping, + GlobalUiControlDefaultsModel, + GlobalUiControlDefaultsModelRecord, + GlobalUiControlDefaultsRecord, + GlobalUiControlSettingsJson, + TransitionEasing, + TransitionType, +} from './global-defaults.ts'; diff --git a/backend/src/types/notifications.ts b/backend/src/types/notifications.ts new file mode 100644 index 0000000..655a0d3 --- /dev/null +++ b/backend/src/types/notifications.ts @@ -0,0 +1,3 @@ +export interface NotificationCatalog { + [key: string]: string | NotificationCatalog; +} diff --git a/backend/src/types/pagination.ts b/backend/src/types/pagination.ts new file mode 100644 index 0000000..5cdfe0c --- /dev/null +++ b/backend/src/types/pagination.ts @@ -0,0 +1,24 @@ +export type SortDirection = 'ASC' | 'DESC'; + +export interface PaginationOptions { + limit?: number; + offset?: number; + page?: number; +} + +export interface SortOptions { + field?: TSortField; + sort?: SortDirection; +} + +export interface ListQueryOptions + extends PaginationOptions, + SortOptions { + filter?: TFilter; + query?: string; +} + +export interface PaginatedResult { + rows: TEntity[]; + count: number; +} diff --git a/backend/src/types/permissions.ts b/backend/src/types/permissions.ts new file mode 100644 index 0000000..982fdf6 --- /dev/null +++ b/backend/src/types/permissions.ts @@ -0,0 +1,23 @@ +import type { PermissionRecord } from './auth.ts'; +import type { EntityDbApi } from './db-api.ts'; + +export interface PermissionDbRecord extends PermissionRecord { + id: string; +} + +export interface PermissionData { + id?: string; + name?: string | null; +} + +export interface PermissionFieldMapping { + id: string | undefined; + name: string | null; +} + +export type PermissionsDbApi = EntityDbApi< + PermissionDbRecord, + PermissionData, + PermissionData, + unknown +>; diff --git a/backend/src/types/presigned-url-requests-db.ts b/backend/src/types/presigned-url-requests-db.ts new file mode 100644 index 0000000..78d7937 --- /dev/null +++ b/backend/src/types/presigned-url-requests-db.ts @@ -0,0 +1,51 @@ +import type { + DbAssociationConfig, + DbRelationFilterConfig, + EntityDbApi, + EntityRecord, +} from './index.ts'; + +export type PresignedUrlRequestPurpose = 'upload' | 'download'; +export type PresignedUrlRequestAssetType = 'image' | 'video' | 'audio' | 'file'; + +export interface PresignedUrlRequestData { + id?: string; + purpose?: PresignedUrlRequestPurpose | null; + asset_type?: PresignedUrlRequestAssetType | null; + requested_key?: string | null; + mime_type?: string | null; + requested_size_mb?: number | string | null; + expires_at?: string | Date | null; + status?: string | null; +} + +export interface PresignedUrlRequestRecord extends EntityRecord { + purpose?: PresignedUrlRequestPurpose | null; + asset_type?: PresignedUrlRequestAssetType | null; + requested_key?: string | null; + mime_type?: string | null; + requested_size_mb?: number | string | null; + expires_at?: string | Date | null; + status?: string | null; +} + +export interface PresignedUrlRequestFieldMapping { + id: string | undefined; + purpose: PresignedUrlRequestPurpose | null; + asset_type: PresignedUrlRequestAssetType | null; + requested_key: string | null; + mime_type: string | null; + requested_size_mb: number | string | null; + expires_at: string | Date | null; + status: string | null; +} + +export type PresignedUrlRequestAssociationConfig = DbAssociationConfig; +export type PresignedUrlRequestRelationFilterConfig = DbRelationFilterConfig; + +export type PresignedUrlRequestsDbApi = EntityDbApi< + PresignedUrlRequestRecord, + PresignedUrlRequestData, + PresignedUrlRequestData, + unknown +>; diff --git a/backend/src/types/project-audio-tracks.ts b/backend/src/types/project-audio-tracks.ts new file mode 100644 index 0000000..34f79e4 --- /dev/null +++ b/backend/src/types/project-audio-tracks.ts @@ -0,0 +1,152 @@ +import type { Response } from 'express'; +import type { Transaction } from 'sequelize'; + +import type { + CreateOptions, + DbFindAllOptions, + DbFindByOptions, + DeleteByIdsOptions, + EntityIdOptions, + FileUploadRequest, + PaginatedResult, + QueryWhere, + RuntimeContext, + RuntimeEnvironment, + RuntimeProjectInclude, + UpdateOptions, +} from './index.ts'; + +export interface ProjectAudioTrackData { + id?: string; + environment?: RuntimeEnvironment | null; + source_key?: string | null; + name?: string | null; + slug?: string | null; + url?: string | null; + loop?: boolean; + volume?: number | null; + sort_order?: number | null; + is_enabled?: boolean; + projectId?: string; +} + +export interface ProjectAudioTrackRecord extends ProjectAudioTrackData { + id: string; +} + +export type ProjectAudioTrackRangeValue = string | number | Date; +export type ProjectAudioTrackRangeFilter = readonly [ + ProjectAudioTrackRangeValue | null | undefined, + ProjectAudioTrackRangeValue | null | undefined, +]; + +export interface ProjectAudioTrackListFilter { + [key: string]: unknown; + id?: string; + project?: string; + environment?: RuntimeEnvironment; + source_key?: string; + name?: string; + slug?: string; + url?: string; + loop?: boolean | string; + is_enabled?: boolean | string; + active?: boolean | string; + volumeRange?: ProjectAudioTrackRangeFilter; + sort_orderRange?: ProjectAudioTrackRangeFilter; + createdAtRange?: ProjectAudioTrackRangeFilter; + limit?: string | number; + page?: string | number; + field?: string; + sort?: string; +} + +export interface ProjectAudioTrackFieldMapping { + id: string | undefined; + environment: RuntimeEnvironment | null; + source_key: string | null; + name: string | null; + slug: string | null; + url: string | null; + loop: boolean; + volume: number | null; + sort_order: number | null; + is_enabled: boolean; +} + +export interface ProjectAudioTrackRuntimeOptions { + countOnly?: boolean; + runtimeContext?: RuntimeContext | null; + transaction?: Transaction; +} + +export interface ProjectAudioTrackModelRecord { + get(options: { plain: true }): ProjectAudioTrackRecord; +} + +export interface ProjectAudioTrackModel { + findOne(options: { + where: QueryWhere; + transaction?: Transaction; + include: RuntimeProjectInclude[]; + }): Promise; + findAndCountAll(options: { + where: QueryWhere; + include: RuntimeProjectInclude[]; + distinct: true; + order: string[][]; + transaction?: Transaction; + limit?: number; + offset?: number; + }): Promise>; +} + +export interface ProjectAudioTrackCsvRow { + [column: string]: string; +} + +export interface ProjectAudioTrackBulkImportOptions { + transaction: Transaction; + ignoreDuplicates: boolean; + validate: boolean; + currentUser?: CreateOptions['currentUser']; +} + +export interface ProjectAudioTracksDbApi { + create( + options: CreateOptions, + ): Promise; + bulkImport( + rows: ProjectAudioTrackCsvRow[], + options: ProjectAudioTrackBulkImportOptions, + ): Promise; + findBy( + where: { id: string }, + options?: ProjectAudioTrackRuntimeOptions, + ): Promise; + findBy( + options: DbFindByOptions, + ): Promise; + findAll( + filter?: ProjectAudioTrackListFilter, + options?: ProjectAudioTrackRuntimeOptions, + ): Promise>; + findAll( + options?: DbFindAllOptions, + ): Promise>; + update( + options: UpdateOptions, + ): Promise; + deleteByIds(options: DeleteByIdsOptions): Promise; + remove(options: EntityIdOptions): Promise; +} + +export type ProjectAudioTrackCreateOptions = + CreateOptions; +export type ProjectAudioTrackUpdateOptions = + UpdateOptions; +export type ProjectAudioTrackDeleteByIdsOptions = DeleteByIdsOptions; +export type ProjectAudioTrackRemoveOptions = EntityIdOptions; + +export type ProjectAudioTrackBulkImportRequest = FileUploadRequest; +export type ProjectAudioTrackBulkImportResponse = Response; diff --git a/backend/src/types/project-element-defaults.ts b/backend/src/types/project-element-defaults.ts new file mode 100644 index 0000000..9413b3e --- /dev/null +++ b/backend/src/types/project-element-defaults.ts @@ -0,0 +1,204 @@ +import type { + AutocompleteOptions, + CreateOptions, + DbFindAllOptions, + DbFindByOptions, + DeleteByIdsOptions, + EntityDbApi, + EntityIdOptions, + PaginatedResult, + QueryWhere, + RuntimeProjectInclude, + ServiceOptions, + UpdateOptions, +} from './index.ts'; + +export type ElementSettingsJson = Record; + +export interface ProjectElementDefaultRecord { + id: string; + projectId?: string; + element_type: string; + name: string; + sort_order: number; + settings_json?: ElementSettingsJson | string | null; + source_element_id?: string | null; + snapshot_version: number; +} + +export interface ProjectElementDefaultsData { + id?: string; + projectId?: string | null; + project?: string | null; + element_type?: string | null; + name?: string | null; + sort_order?: number | null; + settings_json?: ElementSettingsJson | string | null; + source_element_id?: string | null; + snapshot_version?: number | null; +} + +export interface ProjectElementDefaultsFieldMapping { + id: string | undefined; + element_type: string | null; + name: string | null; + sort_order: number; + settings_json: string | null; + source_element_id: string | null; + snapshot_version: number; + projectId: string | null; +} + +export interface ProjectElementDefaultsListFilter { + [key: string]: unknown; + id?: string; + project?: string; + projectId?: string; + element_type?: string; + name?: string; + sort_orderRange?: ProjectElementDefaultsRangeFilter; + snapshot_versionRange?: ProjectElementDefaultsRangeFilter; + createdAtRange?: ProjectElementDefaultsRangeFilter; + limit?: string | number; + page?: string | number; + field?: string; + sort?: string; +} + +export type ProjectElementDefaultsRangeValue = string | number | Date; +export type ProjectElementDefaultsRangeFilter = readonly [ + ProjectElementDefaultsRangeValue | null | undefined, + ProjectElementDefaultsRangeValue | null | undefined, +]; + +export interface GlobalElementDefaultRecord { + id: string; + element_type: string; + name: string; + sort_order: number; + default_settings_json?: ElementSettingsJson | string | null; +} + +export interface ProjectElementDefaultsDiffWithGlobal { + projectDefault: ProjectElementDefaultRecord; + globalDefault: GlobalElementDefaultRecord; + hasGlobalDefault: true; + isDifferent: boolean; + projectSettings: ElementSettingsJson; + globalSettings: ElementSettingsJson; +} + +export interface ProjectElementDefaultsDiffWithoutGlobal { + projectDefault: ProjectElementDefaultRecord; + globalDefault: null; + hasGlobalDefault: false; + isDifferent: true; +} + +export type ProjectElementDefaultsDiff = + | ProjectElementDefaultsDiffWithGlobal + | ProjectElementDefaultsDiffWithoutGlobal; + +export interface ProjectElementDefaultsOptions extends ServiceOptions { + countOnly?: boolean; +} + +export interface ProjectElementDefaultsModelRecord + extends ProjectElementDefaultRecord { + update( + data: Partial & { + updatedById?: string | null; + updatedAt?: Date; + }, + options: { + transaction?: ServiceOptions['transaction']; + }, + ): Promise; + reload(): Promise; + get(options: { plain: true }): ProjectElementDefaultRecord; +} + +export interface ProjectElementDefaultsModel { + findOne(options: { + where: QueryWhere; + transaction?: ServiceOptions['transaction']; + include?: unknown[]; + }): Promise; + findByPk( + id: string, + options?: { + transaction?: ServiceOptions['transaction']; + }, + ): Promise; + bulkCreate( + data: Array< + ProjectElementDefaultsFieldMapping & { + createdById?: string | null; + updatedById?: string | null; + createdAt?: Date; + updatedAt?: Date; + } + >, + options?: { + transaction?: ServiceOptions['transaction']; + returning?: boolean; + }, + ): Promise; + findAndCountAll(options: { + where: QueryWhere; + include: RuntimeProjectInclude[]; + distinct: true; + order: string[][]; + transaction?: ServiceOptions['transaction']; + limit?: number; + offset?: number; + }): Promise>; +} + +export interface ProjectElementDefaultsDbApi + extends EntityDbApi< + ProjectElementDefaultRecord, + ProjectElementDefaultsData, + ProjectElementDefaultsData, + ProjectElementDefaultsListFilter + > { + findByElementType( + projectId: string, + elementType: string, + options?: ServiceOptions, + ): Promise; + snapshotGlobalDefaults( + projectId: string, + options?: ProjectElementDefaultsOptions, + ): Promise; + resetToGlobal( + id: string, + options?: ProjectElementDefaultsOptions, + ): Promise; + getDiffFromGlobal(id: string): Promise; + findAll( + filter?: ProjectElementDefaultsListFilter, + options?: ProjectElementDefaultsOptions, + ): Promise>; + findAll( + options?: DbFindAllOptions, + ): Promise>; + findBy(options: DbFindByOptions): Promise; + findBy( + where: { id: string }, + options?: ServiceOptions, + ): Promise; + create( + options: CreateOptions, + ): Promise; + update( + options: UpdateOptions, + ): Promise; + deleteByIds( + options: DeleteByIdsOptions, + ): Promise; + remove(options: EntityIdOptions): Promise; + findAllAutocomplete( + options: AutocompleteOptions, + ): Promise; +} diff --git a/backend/src/types/project-memberships-db.ts b/backend/src/types/project-memberships-db.ts new file mode 100644 index 0000000..45b3aa4 --- /dev/null +++ b/backend/src/types/project-memberships-db.ts @@ -0,0 +1,45 @@ +import type { + DbAssociationConfig, + DbRelationFilterConfig, + EntityDbApi, + EntityRecord, +} from './index.ts'; + +export type ProjectMembershipAccessLevel = + | 'owner' + | 'editor' + | 'reviewer' + | 'viewer'; + +export interface ProjectMembershipData { + id?: string; + access_level?: ProjectMembershipAccessLevel | null; + is_active?: boolean | null; + invited_at?: string | Date | null; + accepted_at?: string | Date | null; +} + +export interface ProjectMembershipRecord extends EntityRecord { + access_level?: ProjectMembershipAccessLevel | null; + is_active?: boolean | null; + invited_at?: string | Date | null; + accepted_at?: string | Date | null; +} + +export interface ProjectMembershipFieldMapping { + id: string | undefined; + access_level: ProjectMembershipAccessLevel | null; + is_active: boolean; + invited_at: string | Date | null; + accepted_at: string | Date | null; +} + +export type ProjectMembershipAssociationConfig = DbAssociationConfig; +export type ProjectMembershipRelationFilterConfig = DbRelationFilterConfig; + +export type ProjectMembershipsDbApi = EntityDbApi< + ProjectMembershipRecord, + ProjectMembershipData, + ProjectMembershipData, + unknown +>; diff --git a/backend/src/types/project-settings.ts b/backend/src/types/project-settings.ts new file mode 100644 index 0000000..7a45aff --- /dev/null +++ b/backend/src/types/project-settings.ts @@ -0,0 +1,25 @@ +import type { RuntimeEnvironment } from './runtime.ts'; + +export interface ProjectEnvironmentRouteParams { + projectId: string; + environment: RuntimeEnvironment; +} + +export interface ProjectSettingsListFilter { + id?: string; + project?: string; + source_key?: string; + environment?: RuntimeEnvironment; + limit?: string | number; + page?: string | number; + field?: string; + sort?: string; +} + +export interface RouteMessageResponse { + message: string; +} + +export interface RouteSuccessResponse { + success: true; +} diff --git a/backend/src/types/project-transition-settings.ts b/backend/src/types/project-transition-settings.ts new file mode 100644 index 0000000..404d048 --- /dev/null +++ b/backend/src/types/project-transition-settings.ts @@ -0,0 +1,191 @@ +import type { + CreateOptions, + DbFindAllOptions, + DbFindByOptions, + DeleteByIdsOptions, + EntityIdOptions, + FileUploadRequest, + PaginatedResult, + QueryWhere, + RuntimeContext, + RuntimeEnvironment, + RuntimeProjectInclude, + ServiceOptions, + UpdateOptions, +} from './index.ts'; +import type { Response } from 'express'; +import type { Transaction } from 'sequelize'; + +export type ProjectTransitionType = 'fade' | 'none' | 'video'; +export type ProjectTransitionEasing = + | 'ease-in-out' + | 'ease-in' + | 'ease-out' + | 'linear'; + +export interface ProjectTransitionSettingsData { + id?: string; + source_key?: string | null; + transition_type?: ProjectTransitionType; + duration_ms?: number; + easing?: ProjectTransitionEasing; + overlay_color?: string; +} + +export interface ProjectTransitionSettingsRecord { + id: string; + projectId?: string; + environment: RuntimeEnvironment; + source_key?: string | null; + transition_type: ProjectTransitionType; + duration_ms: number; + easing: ProjectTransitionEasing; + overlay_color: string; +} + +export type ProjectTransitionSettingsRangeValue = string | number | Date; +export type ProjectTransitionSettingsRangeFilter = readonly [ + ProjectTransitionSettingsRangeValue | null | undefined, + ProjectTransitionSettingsRangeValue | null | undefined, +]; + +export interface ProjectTransitionSettingsListFilter { + [key: string]: unknown; + id?: string; + project?: string; + source_key?: string; + transition_type?: ProjectTransitionType; + easing?: ProjectTransitionEasing; + overlay_color?: string; + environment?: RuntimeEnvironment; + duration_msRange?: ProjectTransitionSettingsRangeFilter; + createdAtRange?: ProjectTransitionSettingsRangeFilter; + active?: boolean | string; + limit?: string | number; + page?: string | number; + field?: string; + sort?: string; +} + +export interface ProjectTransitionSettingsFieldMapping { + id: string | undefined; + source_key: string | null; + transition_type: ProjectTransitionType; + duration_ms: number; + easing: ProjectTransitionEasing; + overlay_color: string; +} + +export interface ProjectTransitionSettingsRuntimeOptions { + countOnly?: boolean; + currentUser?: CreateOptions['currentUser']; + runtimeContext?: RuntimeContext | null; + transaction?: Transaction; +} + +export interface ProjectTransitionSettingsModelRecord { + update( + data: ProjectTransitionSettingsFieldMapping & { + updatedById: string | null; + }, + options: { + transaction?: Transaction; + }, + ): Promise; + get(options: { plain: true }): ProjectTransitionSettingsRecord; +} + +export interface ProjectTransitionSettingsModel { + findOne(options: { + where: QueryWhere; + transaction?: Transaction; + include?: RuntimeProjectInclude[]; + }): Promise; + create( + data: ProjectTransitionSettingsFieldMapping & { + projectId: string; + environment: RuntimeEnvironment; + createdById: string | null; + updatedById: string | null; + }, + options: { + transaction?: Transaction; + }, + ): Promise; + findAndCountAll(options: { + where: QueryWhere; + include: RuntimeProjectInclude[]; + distinct: true; + order: string[][]; + transaction?: Transaction; + limit?: number; + offset?: number; + }): Promise>; +} + +export interface ProjectTransitionSettingsCsvRow { + [column: string]: string; +} + +export interface ProjectTransitionSettingsBulkImportOptions { + transaction: Transaction; + ignoreDuplicates: boolean; + validate: boolean; + currentUser?: CreateOptions['currentUser']; +} + +export interface ProjectTransitionSettingsFindAllOptions extends ServiceOptions { + countOnly?: boolean; +} + +export interface ProjectTransitionSettingsDbApi { + create( + options: CreateOptions, + ): Promise; + bulkImport( + rows: ProjectTransitionSettingsCsvRow[], + options: ProjectTransitionSettingsBulkImportOptions, + ): Promise; + update( + options: UpdateOptions, + ): Promise; + deleteByIds( + options: DeleteByIdsOptions, + ): Promise; + findBy( + where: { id: string }, + options?: ProjectTransitionSettingsRuntimeOptions, + ): Promise; + findBy( + options: DbFindByOptions, + ): Promise; + findAll( + filter?: ProjectTransitionSettingsListFilter, + options?: ProjectTransitionSettingsRuntimeOptions, + ): Promise>; + findAll( + options?: DbFindAllOptions, + ): Promise>; + findByProjectAndEnvironment( + projectId: string, + environment: RuntimeEnvironment, + options?: ProjectTransitionSettingsRuntimeOptions, + ): Promise; + upsertForProject( + projectId: string, + environment: RuntimeEnvironment, + data: ProjectTransitionSettingsData, + options?: ProjectTransitionSettingsRuntimeOptions, + ): Promise; + remove(options: EntityIdOptions): Promise; +} + +export type ProjectTransitionSettingsCreateOptions = + CreateOptions; +export type ProjectTransitionSettingsUpdateOptions = + UpdateOptions; +export type ProjectTransitionSettingsDeleteByIdsOptions = DeleteByIdsOptions; +export type ProjectTransitionSettingsRemoveOptions = EntityIdOptions; + +export type ProjectTransitionSettingsBulkImportRequest = FileUploadRequest; +export type ProjectTransitionSettingsBulkImportResponse = Response; diff --git a/backend/src/types/project-ui-control-settings.ts b/backend/src/types/project-ui-control-settings.ts new file mode 100644 index 0000000..6e0e181 --- /dev/null +++ b/backend/src/types/project-ui-control-settings.ts @@ -0,0 +1,113 @@ +import type { Transaction } from 'sequelize'; + +import type { CurrentUser } from './auth.ts'; +import type { DbFindAllOptions, DbFindByOptions } from './db-api.ts'; +import type { PaginatedResult } from './pagination.ts'; +import type { + QueryWhere, + RuntimeContext, + RuntimeEnvironment, + RuntimeProjectInclude, +} from './runtime.ts'; +import type { ProjectSettingsListFilter } from './project-settings.ts'; + +export interface ProjectUiControlSettingsData { + id?: string; + source_key?: string | null; + settings_json?: Record; + settings?: Record; +} + +export interface ProjectUiControlSettingsRecord { + id: string; + projectId?: string; + environment: RuntimeEnvironment; + source_key?: string | null; + settings_json: Record; +} + +export interface ProjectUiControlSettingsFieldMapping { + id: string | undefined; + source_key: string | null; + settings_json: Record; +} + +export interface ProjectUiControlSettingsRuntimeOptions { + countOnly?: boolean; + currentUser?: CurrentUser | null; + runtimeContext?: RuntimeContext | null; + transaction?: Transaction; +} + +export interface ProjectUiControlSettingsUpsertOptions + extends ProjectUiControlSettingsRuntimeOptions { + currentUser?: CurrentUser | null; +} + +export interface ProjectUiControlSettingsModelRecord { + update( + data: ProjectUiControlSettingsFieldMapping & { + updatedById: string | null; + }, + options: { + transaction?: Transaction; + }, + ): Promise; + get(options: { plain: true }): ProjectUiControlSettingsRecord; +} + +export interface ProjectUiControlSettingsModel { + findOne(options: { + where: QueryWhere; + transaction?: Transaction; + include?: RuntimeProjectInclude[]; + }): Promise; + create( + data: ProjectUiControlSettingsFieldMapping & { + projectId: string; + environment: RuntimeEnvironment; + createdById: string | null; + updatedById: string | null; + }, + options: { + transaction?: Transaction; + }, + ): Promise; + findAndCountAll(options: { + where: QueryWhere; + include: RuntimeProjectInclude[]; + distinct: true; + order: (string | undefined)[][]; + transaction?: Transaction; + limit?: number; + offset?: number; + }): Promise>; +} + +export interface ProjectUiControlSettingsDbApi { + findByProjectAndEnvironment( + projectId: string, + environment: RuntimeEnvironment, + options?: ProjectUiControlSettingsRuntimeOptions, + ): Promise; + upsertForProject( + projectId: string, + environment: RuntimeEnvironment, + data: ProjectUiControlSettingsData, + options?: ProjectUiControlSettingsUpsertOptions, + ): Promise; + findBy( + where: { id: string }, + options?: ProjectUiControlSettingsRuntimeOptions, + ): Promise; + findBy( + options: DbFindByOptions, + ): Promise; + findAll( + filter?: ProjectSettingsListFilter, + options?: ProjectUiControlSettingsRuntimeOptions, + ): Promise>; + findAll( + options?: DbFindAllOptions, + ): Promise>; +} diff --git a/backend/src/types/projects.ts b/backend/src/types/projects.ts new file mode 100644 index 0000000..7649691 --- /dev/null +++ b/backend/src/types/projects.ts @@ -0,0 +1,251 @@ +import type { Transaction } from 'sequelize'; + +import type { CurrentUser } from './auth.ts'; +import type { DbFindAllOptions, DbFindByOptions, EntityDbApi } from './db-api.ts'; +import type { PaginatedResult } from './pagination.ts'; +import type { QueryWhere } from './runtime.ts'; +import type { CreateOptions, ServiceOptions, UpdateOptions } from './service-options.ts'; + +export type ProjectProductionPresentationVisibility = 'public' | 'private'; + +export interface ProjectData { + id?: string; + name?: string | null; + slug?: string | null; + description?: string | null; + logo_url?: string | null; + favicon_url?: string | null; + og_image_url?: string | null; + design_width?: number | null; + design_height?: number | null; + production_presentation_visibility?: ProjectProductionPresentationVisibility; +} + +export interface ProjectRecord extends ProjectData { + id: string; + name: string; + slug: string; +} + +export interface ProjectFieldMapping { + id: string | undefined; + name: string | null | undefined; + slug: string | null | undefined; + description: string | null | undefined; + logo_url: string | null | undefined; + favicon_url: string | null | undefined; + og_image_url: string | null | undefined; + design_width: number | null | undefined; + design_height: number | null | undefined; + production_presentation_visibility: + | ProjectProductionPresentationVisibility + | undefined; +} + +export interface ProjectCreatePayload extends ProjectFieldMapping { + importHash: string | null; + createdById: string | null; + updatedById: string | null; +} + +export type ProjectRangeValue = string | number | Date; +export type ProjectRangeFilter = readonly [ + ProjectRangeValue | null | undefined, + ProjectRangeValue | null | undefined, +]; + +export interface ProjectListFilter { + [key: string]: unknown; + id?: string; + name?: string; + slug?: string; + description?: string; + logo_url?: string; + favicon_url?: string; + og_image_url?: string; + production_presentation_visibility?: ProjectProductionPresentationVisibility; + active?: string; + createdAtRange?: ProjectRangeFilter; + field?: string; + sort?: string; + limit?: string | number; + page?: string | number; +} + +export interface ProjectFindAllOptions extends ServiceOptions { + countOnly?: boolean; + include?: unknown[]; +} + +export interface ProjectModelRecord extends ProjectRecord { + get(options: { plain: true }): ProjectRecord; +} + +export interface ProjectModelApi { + create( + data: ProjectCreatePayload, + options: { transaction?: Transaction | undefined }, + ): Promise; + findOne(options: { + where: QueryWhere; + transaction?: Transaction | undefined; + include: unknown[]; + }): Promise; + findAndCountAll(options: { + where: QueryWhere; + include: unknown[]; + distinct: true; + order: string[][]; + transaction?: Transaction | undefined; + limit?: number | undefined; + offset?: number | undefined; + }): Promise>; + count(options: { + where: QueryWhere; + include: unknown[]; + distinct: true; + transaction?: Transaction | undefined; + }): Promise; +} + +export interface ProjectCloneVariantRecord { + id: string; + variant_type: string; + storage_key?: string | null; + width_px?: number | null; + height_px?: number | null; + size_mb?: number | null; +} + +export interface ProjectCloneAssetRecord { + id: string; + name?: string | null; + asset_type?: string | null; + type?: string | null; + storage_key?: string | null; + mime_type?: string | null; + size_mb?: number | null; + width_px?: number | null; + height_px?: number | null; + duration_sec?: number | null; + frame_rate?: number | null; + checksum?: string | null; + is_public?: boolean | null; + asset_variants_asset?: ProjectCloneVariantRecord[]; +} + +export interface ProjectCloneSourceRecord extends ProjectRecord { + description?: string | null; + logo_url?: string | null; + favicon_url?: string | null; + og_image_url?: string | null; + design_width?: number | null; + design_height?: number | null; + assets_project?: ProjectCloneAssetRecord[]; +} + +export interface ProjectCloneJsonData { + id?: unknown; + createdAt?: unknown; + updatedAt?: unknown; + deletedAt?: unknown; + deletedBy?: unknown; + importHash?: unknown; + ui_schema_json?: unknown; + global_ui_controls_settings_json?: unknown; + background_image_url?: unknown; + background_video_url?: unknown; + background_embed_url?: unknown; + background_audio_url?: unknown; + storage_key?: unknown; + settings_json?: unknown; + [field: string]: unknown; +} + +export type ProjectCloneUiSchemaValue = + | null + | string + | number + | boolean + | ProjectCloneUiSchemaValue[] + | { [field: string]: ProjectCloneUiSchemaValue }; + +export interface ProjectCloneJsonRecord { + id: string; + toJSON(): ProjectCloneJsonData; +} + +export interface ProjectCloneCreatedAsset { + id: string; +} + +export interface ProjectCloneAssetPayload { + name?: string | null; + asset_type?: string | null; + type: string; + cdn_url: string; + storage_key?: string | null; + mime_type?: string | null; + size_mb?: number | null; + width_px?: number | null; + height_px?: number | null; + duration_sec?: number | null; + frame_rate?: number | null; + checksum?: string | null; + is_public?: boolean | null; + projectId: string; + createdById: string; + updatedById: string; +} + +export interface ProjectCloneVariantPayload { + variant_type: string; + cdn_url: string; + storage_key?: string | null; + width_px?: number | null; + height_px?: number | null; + size_mb?: number | null; + assetId: string; + createdById: string; + updatedById: string; +} + +export interface ProjectSlugLookupWhere { + slug: string; + id?: Record; +} + +export interface ProjectCloneGenericPayload extends ProjectCloneJsonData { + projectId: string; + environment?: 'dev'; + source_key?: string; + createdById: string; + updatedById: string; +} + +export interface ProjectCloneTransactionOptions { + transaction: Transaction; +} + +export interface ProjectCloneCreateOptions extends ProjectCloneTransactionOptions { + force?: true; +} + +export type ProjectCloneCurrentUser = Pick; + +export interface ProjectsDbApi + extends EntityDbApi { + findBy(options: DbFindByOptions): Promise; + findBy( + where: { id: string }, + options?: ProjectFindAllOptions, + ): Promise; + findAll(options: DbFindAllOptions): Promise>; + findAll( + filter?: ProjectListFilter, + options?: ProjectFindAllOptions, + ): Promise>; +} + +export type ProjectCreateOptions = CreateOptions; +export type ProjectUpdateOptions = UpdateOptions; diff --git a/backend/src/types/publish-events-db.ts b/backend/src/types/publish-events-db.ts new file mode 100644 index 0000000..1d7c46a --- /dev/null +++ b/backend/src/types/publish-events-db.ts @@ -0,0 +1,62 @@ +import type { + DbAssociationConfig, + DbRelationFilterConfig, + EntityDbApi, + EntityRecord, + PublishEventStatus, + RuntimeEnvironment, +} from './index.ts'; + +export interface PublishEventData { + id?: string; + title?: string | null; + description?: string | null; + from_environment?: RuntimeEnvironment | null; + to_environment?: RuntimeEnvironment | null; + started_at?: string | Date | null; + finished_at?: string | Date | null; + status?: PublishEventStatus | null; + error_message?: string | null; + pages_copied?: number | null; + transitions_copied?: number | null; + audios_copied?: number | null; +} + +export interface PublishEventDbRecord extends EntityRecord { + title?: string | null; + description?: string | null; + from_environment?: RuntimeEnvironment | null; + to_environment?: RuntimeEnvironment | null; + started_at?: string | Date | null; + finished_at?: string | Date | null; + status?: PublishEventStatus | null; + error_message?: string | null; + pages_copied?: number | null; + transitions_copied?: number | null; + audios_copied?: number | null; +} + +export interface PublishEventFieldMapping { + id: string | undefined; + title: string | null; + description: string | null; + from_environment: RuntimeEnvironment | null; + to_environment: RuntimeEnvironment | null; + started_at: string | Date | null; + finished_at: string | Date | null; + status: PublishEventStatus | null; + error_message: string | null; + pages_copied: number | null; + transitions_copied: number | null; + audios_copied: number | null; +} + +export type PublishEventAssociationConfig = DbAssociationConfig; +export type PublishEventRelationFilterConfig = DbRelationFilterConfig; + +export type PublishEventsDbApi = EntityDbApi< + PublishEventDbRecord, + PublishEventData, + PublishEventData, + unknown +>; diff --git a/backend/src/types/publish.ts b/backend/src/types/publish.ts new file mode 100644 index 0000000..77100ea --- /dev/null +++ b/backend/src/types/publish.ts @@ -0,0 +1,91 @@ +import type { Transaction } from 'sequelize'; + +import type { CurrentUser } from './auth.ts'; +import type { RuntimeEnvironment } from './runtime.ts'; + +export interface PublishToProductionRequestBody { + projectId: string; + title?: string | null; + description?: string | null; +} + +export interface SaveToStageRequestBody { + projectId: string; +} + +export interface PublishSummary { + pages_copied: number; + audios_copied: number; + transition_settings_copied: number; + ui_control_settings_copied: number; +} + +export interface PublishToProductionResult { + success: true; + publishEventId: string; + summary: PublishSummary; +} + +export interface SaveToStageResult { + success: true; + publishEventId: string; +} + +export type PublishEventStatus = 'queued' | 'running' | 'success' | 'failed'; + +export type PublishSourceEnvironment = Extract; + +export type PublishTargetEnvironment = Extract< + RuntimeEnvironment, + 'stage' | 'production' +>; + +export interface PublishCloneData { + id?: unknown; + createdAt?: unknown; + updatedAt?: unknown; + deletedAt?: unknown; + deletedBy?: unknown; + importHash?: unknown; + ui_schema_json?: unknown; + [field: string]: unknown; +} + +export interface PublishCloneSource { + id: string; + toJSON(): PublishCloneData; +} + +export interface PublishClonePayload extends PublishCloneData { + projectId: string; + environment: PublishTargetEnvironment; + source_key: string; + createdById: string | null; + updatedById: string | null; +} + +export interface PublishEventRecord { + id: string; + update( + data: Partial<{ + started_at: Date; + finished_at: Date; + status: PublishEventStatus; + error_message: string | null; + pages_copied: number; + audios_copied: number; + updatedById: string | null; + }>, + options?: { transaction: Transaction }, + ): Promise; +} + +export interface PublishServiceDbProject { + id: string; +} + +export type PublishServiceCurrentUser = Pick; + +export type PublishLockCallback = ( + transaction: Transaction, +) => Promise; diff --git a/backend/src/types/pwa-caches-db.ts b/backend/src/types/pwa-caches-db.ts new file mode 100644 index 0000000..0fe28ff --- /dev/null +++ b/backend/src/types/pwa-caches-db.ts @@ -0,0 +1,47 @@ +import type { + DbAssociationConfig, + DbRelationFilterConfig, + EntityDbApi, + EntityRecord, +} from './index.ts'; + +export type PwaCacheEnvironment = string; + +export interface PwaCacheData { + id?: string; + environment?: PwaCacheEnvironment | null; + cache_version?: string | null; + manifest_json?: string | null; + asset_list_json?: string | null; + generated_at?: string | Date | null; + is_active?: boolean | null; +} + +export interface PwaCacheRecord extends EntityRecord { + environment?: PwaCacheEnvironment | null; + cache_version?: string | null; + manifest_json?: string | null; + asset_list_json?: string | null; + generated_at?: string | Date | null; + is_active?: boolean | null; +} + +export interface PwaCacheFieldMapping { + id: string | undefined; + environment: PwaCacheEnvironment | null; + cache_version: string | null; + manifest_json: string | null; + asset_list_json: string | null; + generated_at: string | Date | null; + is_active: boolean; +} + +export type PwaCacheAssociationConfig = DbAssociationConfig; +export type PwaCacheRelationFilterConfig = DbRelationFilterConfig; + +export type PwaCachesDbApi = EntityDbApi< + PwaCacheRecord, + PwaCacheData, + PwaCacheData, + unknown +>; diff --git a/backend/src/types/rate-limit.ts b/backend/src/types/rate-limit.ts new file mode 100644 index 0000000..8af07f4 --- /dev/null +++ b/backend/src/types/rate-limit.ts @@ -0,0 +1,20 @@ +import type { Request } from 'express'; + +export interface RateLimitEntry { + count: number; + expiresAt: number; + resetTime: string; +} + +export type RateLimitKeyGenerator = (req: Request) => string; +export type RateLimitSkipPredicate = (req: Request) => boolean; + +export interface RateLimiterOptions { + keyPrefix?: string; + windowMs?: number; + max?: number; + message?: string; + skipFailedRequests?: boolean; + keyGenerator?: RateLimitKeyGenerator | null; + skip?: RateLimitSkipPredicate | null; +} diff --git a/backend/src/types/request.ts b/backend/src/types/request.ts new file mode 100644 index 0000000..e5e1a34 --- /dev/null +++ b/backend/src/types/request.ts @@ -0,0 +1,7 @@ +import type Joi from 'joi'; + +import type { RequestValidationPart } from './validation.ts'; + +export type RequestSchemaMap = Partial>; +export type RequestSchemaGroup = Record; +export type RequestSchemaCatalog = Record; diff --git a/backend/src/types/roles.ts b/backend/src/types/roles.ts new file mode 100644 index 0000000..2761070 --- /dev/null +++ b/backend/src/types/roles.ts @@ -0,0 +1,81 @@ +import type { Response } from 'express'; +import type { Transaction } from 'sequelize'; + +import type { + CreateOptions, + DbAssociationConfig, + DbFindByOptions, + DbRelationFilterConfig, + DeleteByIdsOptions, + EntityIdOptions, + FileUploadRequest, + PermissionRecord, + RoleWithPermissionLoader, + ServiceOptions, + UpdateOptions, +} from './index.ts'; + +export interface RoleData { + id?: string; + name?: string | null; + role_customization?: string | null; + permissions?: ReadonlyArray; +} + +export interface RoleFieldMapping { + id: string | undefined; + name: string | null; + role_customization: string | null; +} + +export type RoleAssociationConfig = DbAssociationConfig; +export type RoleRelationFilterConfig = DbRelationFilterConfig; + +export interface RoleServiceRecord extends RoleWithPermissionLoader { + id: string; + role_customization?: string | null; +} + +export interface RoleModelRecord extends RoleServiceRecord { + setPermissions( + permissions: ReadonlyArray, + options: { transaction?: Transaction | undefined }, + ): Promise; +} + +export interface RoleCsvRow { + [column: string]: string; +} + +export interface RoleBulkImportOptions { + transaction: Transaction; + ignoreDuplicates: boolean; + validate: boolean; + currentUser?: CreateOptions['currentUser']; +} + +export interface RolesDbApi { + create(options: CreateOptions): Promise; + bulkImport(rows: RoleCsvRow[], options: RoleBulkImportOptions): Promise; + findBy(options: DbFindByOptions): Promise; + findBy( + where: RoleLookup, + options?: ServiceOptions, + ): Promise; + update(options: UpdateOptions): Promise; + deleteByIds(options: DeleteByIdsOptions): Promise; + remove(options: EntityIdOptions): Promise; +} + +export interface RoleLookup { + id?: string; + name?: string; +} + +export type RoleCreateOptions = CreateOptions; +export type RoleUpdateOptions = UpdateOptions; +export type RoleDeleteByIdsOptions = DeleteByIdsOptions; +export type RoleRemoveOptions = EntityIdOptions; + +export type RoleBulkImportRequest = FileUploadRequest; +export type RoleBulkImportResponse = Response; diff --git a/backend/src/types/runtime-presentation-access.ts b/backend/src/types/runtime-presentation-access.ts new file mode 100644 index 0000000..89b1151 --- /dev/null +++ b/backend/src/types/runtime-presentation-access.ts @@ -0,0 +1,31 @@ +import type { AccessPolicyOptions, ProductionPresentationProject } from './access-policy.ts'; + +export interface PrivateProductionPresentationOption { + id: string; + label: string; + name: string; + slug: string; +} + +export interface ProductionPresentationAccessProject { + slug: string; + production_presentation_visibility: 'private'; +} + +export interface ProductionPresentationAccessGrantPlain { + project?: ProductionPresentationAccessProject | null; +} + +export interface ProductionPresentationAccessGrantRow + extends ProductionPresentationAccessGrantPlain { + get?: (options: { plain: true }) => ProductionPresentationAccessGrantPlain; +} + +export interface PrivateProductionProjectRow + extends Pick { + get?: ( + options: { plain: true }, + ) => Pick; +} + +export type RuntimePresentationAccessOptions = AccessPolicyOptions; diff --git a/backend/src/types/runtime.ts b/backend/src/types/runtime.ts new file mode 100644 index 0000000..a90397e --- /dev/null +++ b/backend/src/types/runtime.ts @@ -0,0 +1,51 @@ +export type RuntimeEnvironment = 'dev' | 'stage' | 'production'; +export type RuntimeReadableEnvironment = 'stage' | 'production'; +export type RuntimeMode = 'admin' | 'runtime'; + +export interface RuntimeContext { + mode: RuntimeMode; + projectSlug: string | null; + headerEnvironment?: RuntimeEnvironment; + headerProjectSlug?: string; +} + +export interface UnknownRuntimeContext { + mode: 'unknown'; + projectSlug: null; +} + +export type RuntimeContextInspectionResponse = + | RuntimeContext + | UnknownRuntimeContext; + +export interface RuntimeFilterOptions { + runtimeContext?: RuntimeContext | null; +} + +export type QueryWhere = Record; + +export interface RuntimeProjectInclude { + required?: boolean; + where?: QueryWhere; + [key: string]: unknown; +} + +export type RuntimePublicPathPattern = string | RegExp; +export type RuntimePublicFieldMap = Record; +export type RuntimePublicAllowedPathMap = Record< + string, + readonly RuntimePublicPathPattern[] +>; + +export interface RuntimePublicPlainRecord { + [key: string]: unknown; +} + +export interface RuntimePublicSequelizeRecord extends RuntimePublicPlainRecord { + get(options: { plain: true }): RuntimePublicPlainRecord; +} + +export interface RuntimePublicListResponse { + rows: unknown[]; + [key: string]: unknown; +} diff --git a/backend/src/types/search.ts b/backend/src/types/search.ts new file mode 100644 index 0000000..a4bc4c0 --- /dev/null +++ b/backend/src/types/search.ts @@ -0,0 +1,14 @@ +export interface SearchRequestBody { + searchQuery: string; +} + +export type SearchFieldValue = string | number | boolean | Date | null; + +export interface SearchResultRow { + id?: string; + matchAttribute: string[]; + tableName: string; + [field: string]: SearchFieldValue | SearchFieldValue[] | undefined; +} + +export type SearchResult = SearchResultRow[]; diff --git a/backend/src/types/sequelize-models.ts b/backend/src/types/sequelize-models.ts new file mode 100644 index 0000000..757092d --- /dev/null +++ b/backend/src/types/sequelize-models.ts @@ -0,0 +1,37 @@ +import type { DataTypes, Model, ModelStatic, Sequelize } from 'sequelize'; + +export type SequelizeDataTypes = typeof DataTypes; + +export interface SequelizeModel extends ModelStatic { + associate?: (db: SequelizeModelRegistry) => void; +} + +export interface SequelizeModelRegistry { + [modelName: string]: unknown; + access_logs: SequelizeModel; + users: SequelizeModel; + projects: SequelizeModel; + assets: SequelizeModel; + asset_variants: SequelizeModel; + element_type_defaults: SequelizeModel; + permissions: SequelizeModel; + publish_events: SequelizeModel; + roles: SequelizeModel; + file: SequelizeModel; + global_transition_defaults: SequelizeModel; + global_ui_control_defaults: SequelizeModel; + project_audio_tracks: SequelizeModel; + project_element_defaults: SequelizeModel; + project_memberships: SequelizeModel; + project_transition_settings: SequelizeModel; + project_ui_control_settings: SequelizeModel; + presigned_url_requests: SequelizeModel; + production_presentation_access: SequelizeModel; + pwa_caches: SequelizeModel; + tour_pages: SequelizeModel; +} + +export type SequelizeModelFactory = ( + sequelize: Sequelize, + DataTypes: SequelizeDataTypes, +) => SequelizeModel; diff --git a/backend/src/types/service-options.ts b/backend/src/types/service-options.ts new file mode 100644 index 0000000..45b864a --- /dev/null +++ b/backend/src/types/service-options.ts @@ -0,0 +1,45 @@ +import type { Transaction } from 'sequelize'; + +import type { CurrentUser } from './auth.ts'; +import type { RuntimeContext } from './runtime.ts'; + +export interface ServiceContext { + currentUser?: CurrentUser | null; + runtimeContext?: RuntimeContext; +} + +export interface TransactionalOptions { + transaction?: Transaction; +} + +export interface ServiceOptions extends ServiceContext, TransactionalOptions {} + +export interface CreateOptions extends ServiceOptions { + data: TData; + sendInvitationEmails?: boolean; + host?: string; +} + +export interface UpdateOptions extends ServiceOptions { + id: string; + data: TData; +} + +export interface EntityIdOptions extends ServiceOptions { + id: string; +} + +export interface DeleteByIdsOptions extends ServiceOptions { + ids: string[]; +} + +export interface AutocompleteOptions extends ServiceOptions { + query?: string; + limit?: number; + offset?: number; +} + +export interface BulkImportOptions extends ServiceOptions { + ignoreDuplicates?: boolean; + validate?: boolean; +} diff --git a/backend/src/types/sql.ts b/backend/src/types/sql.ts new file mode 100644 index 0000000..fc46ef2 --- /dev/null +++ b/backend/src/types/sql.ts @@ -0,0 +1,43 @@ +export interface ReadOnlySqlValidationOptions { + maxLength?: number; +} + +export interface ExecuteSqlRequestBody { + sql: string; +} + +export type SqlQueryScalar = string | number | boolean | Date | Buffer | null; +export type SqlQueryValue = + | SqlQueryScalar + | ReadonlyArray + | Readonly>; + +export type SqlQueryRow = Record; + +export interface ExecuteSqlResponseMeta { + maxRows: number; + statementTimeoutMs: number; +} + +export interface ExecuteSqlSuccessResponse { + rows: SqlQueryRow[]; + meta: ExecuteSqlResponseMeta; +} + +export interface ExecuteSqlErrorResponse { + error: string; +} + +export interface InvalidSqlValidationResult { + valid: false; + error: string; +} + +export interface ValidSqlValidationResult { + valid: true; + normalized: string; +} + +export type SqlValidationResult = + | InvalidSqlValidationResult + | ValidSqlValidationResult; diff --git a/backend/src/types/tour-pages.ts b/backend/src/types/tour-pages.ts new file mode 100644 index 0000000..38abde3 --- /dev/null +++ b/backend/src/types/tour-pages.ts @@ -0,0 +1,374 @@ +import type { Request } from 'express'; +import type { ParamsDictionary, Query } from 'express-serve-static-core'; + +import type { CurrentUser } from './auth.ts'; +import type { EntityDataRequestBody } from './http.ts'; +import type { PaginatedResult } from './pagination.ts'; +import type { + QueryWhere, + RuntimeContext, + RuntimeEnvironment, + RuntimeProjectInclude, +} from './runtime.ts'; +import type { Transaction } from 'sequelize'; +import type { + CreateOptions, + AutocompleteOptions, + DbFindAllOptions, + DbFindByOptions, + DeleteByIdsOptions, + EntityDbApi, + EntityIdOptions, + ServiceOptions, + UpdateOptions, + ProjectCloneGenericPayload, + PublishCloneSource, +} from './index.ts'; + +export type TourPageUiSchema = Record | string | null; + +export interface TourPageNestedUiItem { + id?: string; + [key: string]: unknown; +} + +export interface TourPageInfoPanelSection extends TourPageNestedUiItem { + spans?: TourPageNestedUiItem[]; + images?: TourPageNestedUiItem[]; +} + +export interface TourPageElement { + id?: string; + type?: string; + navType?: string; + navigationTargetMode?: string; + targetPageSlug?: string | null; + targetPageId?: string | null; + transitionVideoUrl?: string | null; + transitionReverseMode?: string | null; + reverseVideoUrl?: string | null; + galleryCards?: TourPageNestedUiItem[]; + galleryInfoSpans?: TourPageNestedUiItem[]; + carouselSlides?: TourPageNestedUiItem[]; + infoPanelSections?: TourPageInfoPanelSection[]; + [key: string]: unknown; +} + +export interface TourPageStructuredUiSchema { + elements?: TourPageElement[]; + [key: string]: unknown; +} + +export interface TourPagePlainRecord extends TourPageRecord { + get?: undefined; +} + +export interface TourPageSequelizeRecord extends TourPageRecord { + get(options: { plain: true }): TourPageRecord; +} + +export type TourPageMaybeSequelizeRecord = + | TourPageRecord + | TourPageSequelizeRecord; + +export interface TourPageProjectRef { + id: string; + name?: string; + slug?: string; +} + +export interface TourPageData { + id?: string; + project?: string | TourPageProjectRef | null | undefined; + projectId?: string | null | undefined; + project_id?: string | null | undefined; + environment?: RuntimeEnvironment | undefined; + source_key?: string | null | undefined; + name?: string | null | undefined; + slug?: string | null | undefined; + sort_order?: number | null | undefined; + background_image_url?: string | null | undefined; + background_video_url?: string | null | undefined; + background_embed_url?: string | null | undefined; + background_audio_url?: string | null | undefined; + background_audio_autoplay?: boolean | undefined; + background_audio_loop?: boolean | undefined; + background_audio_start_time?: number | null | undefined; + background_audio_end_time?: number | null | undefined; + background_loop?: boolean | undefined; + background_video_autoplay?: boolean | undefined; + background_video_loop?: boolean | undefined; + background_video_muted?: boolean | undefined; + background_video_start_time?: number | null | undefined; + background_video_end_time?: number | null | undefined; + design_width?: number | null | undefined; + design_height?: number | null | undefined; + requires_auth?: boolean | undefined; + ui_schema_json?: TourPageUiSchema; + global_ui_controls_settings_json?: TourPageUiSchema; + importHash?: string | null | undefined; +} + +export interface TourPageRecord extends TourPageData { + id: string; + projectId?: string | null; + createdAt?: Date | string; + updatedAt?: Date | string; + deletedAt?: Date | string | null; + createdById?: string | null; + updatedById?: string | null; +} + +export type TourPageCreateBody = EntityDataRequestBody; + +export interface TourPageUpdateBody + extends EntityDataRequestBody { + id?: string; +} + +export interface TourPageReorderData { + project?: string; + projectId?: string; + environment?: 'dev'; + orderedPageIds: string[]; +} + +export type TourPageReorderBody = EntityDataRequestBody; + +export interface TourPageDeleteByIdsBody { + data: string[]; +} + +export interface TourPageDuplicateData { + project?: string; + projectId?: string; + environment?: 'dev'; + name?: string; + slug?: string; +} + +export interface TourPageDuplicateBody { + data?: TourPageDuplicateData; +} + +export interface TourPageReverseVideoStatusBody { + storageKeys: string[]; +} + +export interface TourPageReverseVideoStatus { + ready: Record; + pending: string[]; + allReady: boolean; +} + +export interface TourPageVideoMetadata { + widthPx: number | null; + heightPx: number | null; + durationSec: number | null; + frameRate: number | null; +} + +export interface TourPageDecodedBytesEstimateInput { + widthPx: number | null; + heightPx: number | null; + durationSec: number | null; + fps: number | null; +} + +export interface TourPageBuildDuplicateSlugOptions { + projectId: string; + environment: RuntimeEnvironment; + preferredSlug?: string | null | undefined; + sourceSlug?: string | null | undefined; + transaction: Transaction; +} + +export interface TourPageProcessReverseOptions { + _skipHistoryModeCheck?: boolean; +} + +export interface TourPageReverseGenerationTask { + projectId?: string | null | undefined; + storageKey?: string | null | undefined; + currentUser?: CurrentUser | null | undefined; + pageId?: string | null | undefined; +} + +export interface TourPagesDbApi + extends EntityDbApi< + TourPageRecord, + TourPageData, + TourPageData, + TourPageListQuery +> { + readonly CSV_FIELDS: readonly string[]; + findBy(options: DbFindByOptions): Promise; + findBy( + where: { id: string }, + options?: ServiceOptions, + ): Promise; + findAll(options: DbFindAllOptions): Promise; + findAll( + filter?: TourPageListQuery, + options?: TourPageFindAllOptions, + ): Promise; + findAllAutocomplete(options: TourPageAutocompleteOptions): Promise; + findAllAutocomplete(options: AutocompleteOptions): Promise; +} + +export type TourPageCreateOptions = CreateOptions; +export type TourPageUpdateOptions = UpdateOptions; +export type TourPageDeleteByIdsOptions = DeleteByIdsOptions; +export type TourPageRemoveOptions = EntityIdOptions; + +export interface TourPageListQuery extends Query { + filetype?: string; + project?: string; + projectId?: string; + id?: string; + environment?: RuntimeEnvironment; + field?: string; + sort?: string; + page?: string; + limit?: string; +} + +export interface TourPageFindAllOptions { + countOnly?: boolean; + currentUser?: CurrentUser | null; + runtimeContext?: RuntimeContext; + transaction?: Transaction; +} + +export interface TourPageAutocompleteOptions { + query?: Query[string]; + limit?: Query[string]; + offset?: Query[string]; +} + +export interface TourPageRouteParams extends ParamsDictionary { + id: string; +} + +export type TourPageListResult = PaginatedResult; + +export interface TourPageFieldMapping { + id: string | undefined; + environment: RuntimeEnvironment | null; + source_key: string | null; + name: string | null; + slug: string | null; + sort_order: number | null; + background_image_url: string | null; + background_video_url: string | null; + background_embed_url: string | null; + background_audio_url: string | null; + background_audio_autoplay: boolean; + background_audio_loop: boolean; + background_audio_start_time: number | null; + background_audio_end_time: number | null; + background_loop: boolean; + background_video_autoplay: boolean; + background_video_loop: boolean; + background_video_muted: boolean; + background_video_start_time: number | null; + background_video_end_time: number | null; + design_width: number | null; + design_height: number | null; + requires_auth: boolean; + ui_schema_json: TourPageUiSchema; + global_ui_controls_settings_json: TourPageUiSchema; +} + +export interface TourPageCreatePayload extends TourPageFieldMapping { + projectId: string | null; + importHash: string | null; + createdById: string | null; + updatedById: string | null; +} + +export interface TourPageModelRecord extends TourPageRecord { + get(options: { plain: true }): TourPageRecord; + setProject( + projectId: string | null, + options: { transaction?: Transaction | undefined }, + ): Promise; +} + +export interface TourPageModelApi { + create( + data: TourPageCreatePayload, + options: { + transaction?: Transaction | undefined; + }, + ): Promise; + create( + data: ProjectCloneGenericPayload, + options: { + transaction: Transaction; + }, + ): Promise; + findOne(options: { + where: QueryWhere; + transaction?: Transaction | undefined; + include?: RuntimeProjectInclude[]; + }): Promise; + findAndCountAll(options: { + where: QueryWhere; + include: RuntimeProjectInclude[]; + distinct: true; + order: string[][]; + transaction?: Transaction | undefined; + limit?: number | undefined; + offset?: number | undefined; + }): Promise>; +} + +export type TourPageRangeValue = string | number | Date; +export type TourPageRangeFilter = readonly [ + TourPageRangeValue | null | undefined, + TourPageRangeValue | null | undefined, +]; + +export type TourPageListRequest = Request< + ParamsDictionary, + TourPageListResult | string, + unknown, + TourPageListQuery +>; + +export type TourPageCreateRequest = Request< + ParamsDictionary, + TourPageRecord, + TourPageCreateBody +>; + +export type TourPageUpdateRequest = Request< + TourPageRouteParams, + boolean, + TourPageUpdateBody +>; + +export type TourPageReorderRequest = Request< + ParamsDictionary, + TourPageRecord[], + TourPageReorderBody +>; + +export type TourPageDuplicateRequest = Request< + TourPageRouteParams, + TourPageRecord, + TourPageDuplicateBody +>; + +export type TourPageDeleteByIdsRequest = Request< + ParamsDictionary, + boolean, + TourPageDeleteByIdsBody +>; + +export type TourPageReverseVideoStatusRequest = Request< + ParamsDictionary, + TourPageReverseVideoStatus, + TourPageReverseVideoStatusBody +>; diff --git a/backend/src/types/upload.ts b/backend/src/types/upload.ts new file mode 100644 index 0000000..ba05c44 --- /dev/null +++ b/backend/src/types/upload.ts @@ -0,0 +1,6 @@ +import type { Request, Response } from 'express'; + +export type FileUploadProcessor = ( + req: Request, + res: Response, +) => Promise; diff --git a/backend/src/types/users.ts b/backend/src/types/users.ts new file mode 100644 index 0000000..24a2ee1 --- /dev/null +++ b/backend/src/types/users.ts @@ -0,0 +1,368 @@ +import type { RequestHandler } from 'express'; +import type { Transaction } from 'sequelize'; + +import type { PermissionRecord, RoleRecord } from './auth.ts'; +import type { AutocompleteOptions, DeleteByIdsOptions } from './service-options.ts'; +import type { DbFindAllOptions, DbFindByOptions, EntityDbApi } from './db-api.ts'; +import type { EntityDataRequestBody } from './http.ts'; +import type { PaginatedResult } from './pagination.ts'; +import type { QueryWhere } from './runtime.ts'; +import type { + CreateOptions, + EntityIdOptions, + ServiceOptions, + UpdateOptions, +} from './service-options.ts'; + +export interface UserFileRecord { + id?: string; + name?: string; + sizeInBytes?: number; + privateUrl?: string; + publicUrl?: string; + new?: boolean; +} + +export interface PrivateProductionProjectOption { + id: string; + label: string; + name: string; + slug: string; +} + +export interface UserData { + id?: string; + firstName?: string | null; + lastName?: string | null; + phoneNumber?: string | null; + email?: string | null; + disabled?: boolean; + password?: string | null | undefined; + emailVerified?: boolean; + emailVerificationToken?: string | null; + emailVerificationTokenExpiresAt?: Date | string | null; + passwordResetToken?: string | null; + passwordResetTokenExpiresAt?: Date | string | null; + provider?: string | null; + importHash?: string | null; + app_role?: string | RoleRecord | { id?: string; value?: string } | null; + custom_permissions?: Array< + string | PermissionRecord | { id?: string; value?: string } + >; + allowed_private_production_project_ids?: Array< + string | PrivateProductionProjectOption | { id?: string; value?: string } + >; + avatar?: UserFileRecord | UserFileRecord[] | null; +} + +export interface UserCreatePayload { + id?: string | undefined; + firstName: string | null; + lastName: string | null; + phoneNumber: string | null; + email: string | null; + disabled: boolean; + password: string; + emailVerified?: boolean | undefined; + emailVerificationToken: string | null; + emailVerificationTokenExpiresAt: Date | string | null; + passwordResetToken: string | null; + passwordResetTokenExpiresAt: Date | string | null; + provider: string | null; + importHash: string | null; + createdById?: string | null | undefined; + updatedById?: string | null | undefined; + createdAt?: Date | undefined; +} + +export interface UserUpdatePayload { + firstName?: string | null | undefined; + lastName?: string | null | undefined; + phoneNumber?: string | null | undefined; + email?: string | null | undefined; + disabled?: boolean | undefined; + password?: string | undefined; + emailVerified?: boolean | undefined; + emailVerificationToken?: string | null | undefined; + emailVerificationTokenExpiresAt?: Date | string | null | undefined; + passwordResetToken?: string | null | undefined; + passwordResetTokenExpiresAt?: Date | string | null | undefined; + provider?: string | null | undefined; + authenticationUid?: string | undefined; + updatedById?: string | null | undefined; +} + +export interface UserAuthCreatePayload { + email?: string | null | undefined; + firstName?: string | null | undefined; + authenticationUid?: string | null | undefined; + password?: string | null | undefined; +} + +export interface UserRecord extends UserData { + id: string; + email: string | null; + password?: string | null | undefined; + authenticationUid?: string | null; + app_roleId?: string | null; + app_role?: RoleRecord | null; + custom_permissions?: PermissionRecord[]; + app_role_permissions?: ReadonlyArray; + allowed_private_production_project_ids?: PrivateProductionProjectOption[]; + createdAt?: Date | string; + updatedAt?: Date | string; + deletedAt?: Date | string | null; + createdById?: string | null; + updatedById?: string | null; +} + +export type UserFindByWhere = QueryWhere & { + id?: string; + email?: string; +}; + +export type UserRangeValue = string | number | Date; +export type UserRangeFilter = readonly [ + UserRangeValue | null | undefined, + UserRangeValue | null | undefined, +]; + +export interface UserListFilter { + [key: string]: unknown; + id?: string; + firstName?: string; + lastName?: string; + phoneNumber?: string; + email?: string; + password?: string; + emailVerificationToken?: string; + passwordResetToken?: string; + provider?: string; + app_role?: string; + custom_permissions?: string; + active?: string | boolean; + disabled?: string | boolean; + emailVerified?: string | boolean; + emailVerificationTokenExpiresAtRange?: UserRangeFilter; + passwordResetTokenExpiresAtRange?: UserRangeFilter; + createdAtRange?: UserRangeFilter; + field?: string; + sort?: string; + limit?: string | number; + page?: string | number; +} + +export interface UserFindAllOptions extends ServiceOptions { + countOnly?: boolean; +} + +export type UserSelectableIdInput = + | string + | { id?: string | null; value?: string | null } + | null + | undefined; + +export type UserSelectableIdArrayInput = + | Array + | null + | undefined; + +export interface UserAccessMutationOptions { + user: Pick | null | undefined; + data: UserData; + currentUser: Pick | null | undefined; + transaction: Transaction; +} + +export interface UserPublicRole { + id: string; + name: string; +} + +export interface UserPrivateProject { + id: string; +} + +export interface UserProductionPresentationAccessPayload { + projectId: string; + userId: string; + createdById: string | null; + updatedById: string | null; + createdAt: Date; + updatedAt: Date; +} + +export interface UserRestorableRecord extends UserRecord { + deletedAt?: Date | string | null; + restore(options: { transaction: Transaction }): Promise; +} + +export interface UserTokenFields { + emailVerificationToken?: string | undefined; + emailVerificationTokenExpiresAt?: number | undefined; + passwordResetToken?: string | undefined; + passwordResetTokenExpiresAt?: number | undefined; +} + +export interface UserModelRecord extends UserRecord { + get(options: { plain: true }): UserRecord; + update( + data: UserUpdatePayload | UserTokenFields | { deletedBy: string | null }, + options: { transaction?: Transaction | undefined }, + ): Promise; + destroy(options: { transaction?: Transaction | undefined }): Promise; + setApp_role( + role: string | RoleRecord | UserPublicRole | null, + options: { transaction?: Transaction | undefined }, + ): Promise; + setCustom_permissions( + permissions: ReadonlyArray, + options: { transaction?: Transaction | undefined }, + ): Promise; + restore(options: { transaction: Transaction }): Promise; +} + +export interface UserProductionPresentationAccessPlain { + project?: { + id?: string; + name?: string; + slug?: string; + }; +} + +export interface UserProductionPresentationAccessRecord { + get(options: { plain: true }): UserProductionPresentationAccessPlain; +} + +export interface UserFindAndCountAllOptions { + attributes: { exclude: readonly string[] }; + where: QueryWhere; + include: unknown[]; + distinct: true; + order: string[][]; + transaction?: Transaction | undefined; + limit?: number | undefined; + offset?: number | undefined; +} + +export interface UserAutocompleteOption { + id: string; + label: string | null | undefined; +} + +export interface UserModelApi { + rawAttributes?: Record; + getTableName(): string; + create( + data: UserCreatePayload | UserAuthCreatePayload, + options: { transaction?: Transaction | undefined }, + ): Promise; + bulkCreate( + data: UserCreatePayload[], + options: { transaction?: Transaction | undefined }, + ): Promise; + findByPk( + id: string, + options: { transaction?: Transaction | undefined }, + ): Promise; + findOne(options: { + where: UserFindByWhere | QueryWhere; + transaction?: Transaction | undefined; + include?: unknown[]; + attributes?: readonly string[]; + paranoid?: boolean; + }): Promise; + findAll(options: { + where: QueryWhere; + transaction?: Transaction | undefined; + attributes?: readonly string[]; + limit?: number | undefined; + offset?: number | undefined; + order?: string[][]; + orderBy?: string[][]; + include?: unknown[]; + }): Promise; + findAndCountAll( + options: UserFindAndCountAllOptions, + ): Promise>; +} + +export interface UsersDbApi + extends EntityDbApi< + UserRecord, + UserData, + UserData, + UserListFilter, + UserAutocompleteOption + > { + create(options: CreateOptions): Promise; + bulkImport( + data: UserData[], + options?: ServiceOptions, + ): Promise; + update(options: UpdateOptions): Promise; + deleteByIds(options: DeleteByIdsOptions): Promise; + remove(options: EntityIdOptions): Promise; + findBy(options: DbFindByOptions): Promise; + findBy( + where: UserFindByWhere, + options?: ServiceOptions, + ): Promise; + findByForAuth( + where: UserFindByWhere, + options?: ServiceOptions, + ): Promise; + findAll(options: DbFindAllOptions): Promise>; + findAll( + filter?: UserListFilter, + options?: UserFindAllOptions, + ): Promise>; + findAllAutocomplete( + options: AutocompleteOptions, + queryOptions?: ServiceOptions, + ): Promise; + createFromAuth( + data: UserAuthCreatePayload, + options?: ServiceOptions, + ): Promise; + findByEmailVerificationToken( + token: string, + options?: ServiceOptions, + ): Promise; + findByPasswordResetToken( + token: string, + options?: ServiceOptions, + ): Promise; + generateEmailVerificationToken( + email: string, + options?: ServiceOptions, + ): Promise; + generatePasswordResetToken( + email: string, + options?: ServiceOptions, + ): Promise; + markEmailVerified(id: string, options?: ServiceOptions): Promise; + updatePassword( + id: string, + password: string, + options?: ServiceOptions, + ): Promise; +} + +export type UserCreateOptions = CreateOptions; +export type UserUpdateOptions = UpdateOptions; +export type UserRemoveOptions = EntityIdOptions; + +export type UserCreateBody = EntityDataRequestBody; + +export interface MutableRouteLayer { + route: { + path: string; + methods: { + get?: boolean; + }; + stack: Array<{ + handle: RequestHandler<{ id: string }>; + }>; + }; +} diff --git a/backend/src/types/validation.ts b/backend/src/types/validation.ts new file mode 100644 index 0000000..da847be --- /dev/null +++ b/backend/src/types/validation.ts @@ -0,0 +1,18 @@ +export type RequestValidationPart = 'params' | 'query' | 'body'; + +export interface RequestValidationDetail { + path: string; + message: string; + type?: string; +} + +export interface RequestValidationErrorPayload { + isRequestValidation: true; + details: RequestValidationDetail[]; +} + +export interface ValidatedRequestParts { + params: TParams; + query: TQuery; + body: TBody; +} diff --git a/backend/src/types/video-processing.ts b/backend/src/types/video-processing.ts new file mode 100644 index 0000000..7a40416 --- /dev/null +++ b/backend/src/types/video-processing.ts @@ -0,0 +1,8 @@ +export interface MediaMetadata { + durationSec: number | null; + widthPx: number | null; + heightPx: number | null; + frameRate: number | null; +} + +export type FfmpegJobRunner = () => Promise; diff --git a/backend/src/utils/env-validation.js b/backend/src/utils/env-validation.js deleted file mode 100644 index c43ca4d..0000000 --- a/backend/src/utils/env-validation.js +++ /dev/null @@ -1,69 +0,0 @@ -const Joi = require('joi'); - -const envSchema = Joi.object({ - NODE_ENV: Joi.string() - .valid('development', 'test', 'production', 'dev_stage') - .default('development'), - - PORT: Joi.number().default(8080), - - DB_HOST: Joi.string().default('localhost'), - DB_PORT: Joi.number().default(5432), - DB_NAME: Joi.string().default('db_tour_builder_platform'), - DB_USER: Joi.string().default('postgres'), - DB_PASS: Joi.string().allow('').default(''), - - SECRET_KEY: Joi.string() - .min(16) - .default('88dbeaf8-e906-405e-9e41-c3baadeda5c6'), - - ADMIN_PASS: Joi.string().default('88dbeaf8'), - USER_PASS: Joi.string().default('c3baadeda5c6'), - ADMIN_EMAIL: Joi.string().email().default('admin@flatlogic.com'), - - GOOGLE_CLIENT_ID: Joi.string().allow('').default(''), - GOOGLE_CLIENT_SECRET: Joi.string().allow('').default(''), - MS_CLIENT_ID: Joi.string().allow('').default(''), - MS_CLIENT_SECRET: Joi.string().allow('').default(''), - - AWS_ACCESS_KEY_ID: Joi.string().allow('').default(''), - AWS_SECRET_ACCESS_KEY: Joi.string().allow('').default(''), - AWS_S3_BUCKET: Joi.string().allow('').default(''), - AWS_S3_REGION: Joi.string().default('us-east-1'), - AWS_S3_PREFIX: Joi.string().default('afeefb9d49f5b7977577876b99532ac7'), - - EMAIL_USER: Joi.string().allow('').default(''), - EMAIL_PASS: Joi.string().allow('').default(''), - EMAIL_TLS_REJECT_UNAUTHORIZED: Joi.string() - .valid('true', 'false') - .default('true'), - - GPT_KEY: Joi.string().allow('').default(''), - PEXELS_KEY: Joi.string().allow('').default(''), - - LOG_LEVEL: Joi.string() - .valid('fatal', 'error', 'warn', 'info', 'debug', 'trace') - .default('info'), -}).unknown(true); - -function validateEnv() { - const { error, value } = envSchema.validate(process.env, { - abortEarly: false, - stripUnknown: false, - }); - - if (error) { - const messages = error.details.map((d) => ` - ${d.message}`).join('\n'); - console.error('Environment validation failed:\n' + messages); - - if (process.env.NODE_ENV === 'production') { - process.exit(1); - } else { - console.warn('Continuing with default values in non-production mode'); - } - } - - return value; -} - -module.exports = { validateEnv, envSchema }; diff --git a/backend/src/utils/env-validation.ts b/backend/src/utils/env-validation.ts new file mode 100644 index 0000000..a4032f3 --- /dev/null +++ b/backend/src/utils/env-validation.ts @@ -0,0 +1,169 @@ +import Joi from 'joi'; + +import type { NodeEnvironment, ValidatedEnvironment } from '../types/index.ts'; + +type EnvBooleanString = 'true' | 'false'; +type EnvLogLevel = ValidatedEnvironment['LOG_LEVEL']; + +const envSchema = Joi.object({ + NODE_ENV: Joi.string() + .valid('development', 'test', 'production', 'dev_stage') + .default('development'), + + PORT: Joi.number().default(8080), + + DB_HOST: Joi.string().default('localhost'), + DB_PORT: Joi.number().default(5432), + DB_NAME: Joi.string().default('db_tour_builder_platform'), + DB_USER: Joi.string().default('postgres'), + DB_PASS: Joi.string().allow('').default(''), + + SECRET_KEY: Joi.string() + .min(16) + .default('88dbeaf8-e906-405e-9e41-c3baadeda5c6'), + + ADMIN_PASS: Joi.string().default('88dbeaf8'), + USER_PASS: Joi.string().default('c3baadeda5c6'), + ADMIN_EMAIL: Joi.string().email().default('admin@flatlogic.com'), + + GOOGLE_CLIENT_ID: Joi.string().allow('').default(''), + GOOGLE_CLIENT_SECRET: Joi.string().allow('').default(''), + MS_CLIENT_ID: Joi.string().allow('').default(''), + MS_CLIENT_SECRET: Joi.string().allow('').default(''), + + AWS_ACCESS_KEY_ID: Joi.string().allow('').default(''), + AWS_SECRET_ACCESS_KEY: Joi.string().allow('').default(''), + AWS_S3_BUCKET: Joi.string().allow('').default(''), + AWS_S3_REGION: Joi.string().default('us-east-1'), + AWS_S3_PREFIX: Joi.string().default('afeefb9d49f5b7977577876b99532ac7'), + + EMAIL_USER: Joi.string().allow('').default(''), + EMAIL_PASS: Joi.string().allow('').default(''), + EMAIL_TLS_REJECT_UNAUTHORIZED: Joi.string() + .valid('true', 'false') + .default('true'), + + PEXELS_KEY: Joi.string().allow('').default(''), + + LOG_LEVEL: Joi.string() + .valid('fatal', 'error', 'warn', 'info', 'debug', 'trace') + .default('info'), +}).unknown(true); + +function isNodeEnvironment(value: unknown): value is NodeEnvironment { + return ( + value === 'development' || + value === 'test' || + value === 'production' || + value === 'dev_stage' + ); +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function isEnvBooleanString(value: unknown): value is EnvBooleanString { + return value === 'true' || value === 'false'; +} + +function isEnvLogLevel(value: unknown): value is EnvLogLevel { + return ( + value === 'fatal' || + value === 'error' || + value === 'warn' || + value === 'info' || + value === 'debug' || + value === 'trace' + ); +} + +function readString( + values: Record, + key: keyof ValidatedEnvironment, + fallback: string, +): string { + const value = values[key]; + return typeof value === 'string' ? value : fallback; +} + +function readNumber( + values: Record, + key: keyof ValidatedEnvironment, + fallback: number, +): number { + const value = values[key]; + return typeof value === 'number' ? value : fallback; +} + +function toValidatedEnvironment( + values: Record, +): ValidatedEnvironment { + const nodeEnv = values.NODE_ENV; + const tlsRejectUnauthorized = values.EMAIL_TLS_REJECT_UNAUTHORIZED; + const logLevel = values.LOG_LEVEL; + + return { + NODE_ENV: isNodeEnvironment(nodeEnv) ? nodeEnv : 'development', + PORT: readNumber(values, 'PORT', 8080), + DB_HOST: readString(values, 'DB_HOST', 'localhost'), + DB_PORT: readNumber(values, 'DB_PORT', 5432), + DB_NAME: readString(values, 'DB_NAME', 'db_tour_builder_platform'), + DB_USER: readString(values, 'DB_USER', 'postgres'), + DB_PASS: readString(values, 'DB_PASS', ''), + SECRET_KEY: readString( + values, + 'SECRET_KEY', + '88dbeaf8-e906-405e-9e41-c3baadeda5c6', + ), + ADMIN_PASS: readString(values, 'ADMIN_PASS', '88dbeaf8'), + USER_PASS: readString(values, 'USER_PASS', 'c3baadeda5c6'), + ADMIN_EMAIL: readString(values, 'ADMIN_EMAIL', 'admin@flatlogic.com'), + GOOGLE_CLIENT_ID: readString(values, 'GOOGLE_CLIENT_ID', ''), + GOOGLE_CLIENT_SECRET: readString(values, 'GOOGLE_CLIENT_SECRET', ''), + MS_CLIENT_ID: readString(values, 'MS_CLIENT_ID', ''), + MS_CLIENT_SECRET: readString(values, 'MS_CLIENT_SECRET', ''), + AWS_ACCESS_KEY_ID: readString(values, 'AWS_ACCESS_KEY_ID', ''), + AWS_SECRET_ACCESS_KEY: readString(values, 'AWS_SECRET_ACCESS_KEY', ''), + AWS_S3_BUCKET: readString(values, 'AWS_S3_BUCKET', ''), + AWS_S3_REGION: readString(values, 'AWS_S3_REGION', 'us-east-1'), + AWS_S3_PREFIX: readString( + values, + 'AWS_S3_PREFIX', + 'afeefb9d49f5b7977577876b99532ac7', + ), + EMAIL_USER: readString(values, 'EMAIL_USER', ''), + EMAIL_PASS: readString(values, 'EMAIL_PASS', ''), + EMAIL_TLS_REJECT_UNAUTHORIZED: isEnvBooleanString(tlsRejectUnauthorized) + ? tlsRejectUnauthorized + : 'true', + PEXELS_KEY: readString(values, 'PEXELS_KEY', ''), + LOG_LEVEL: isEnvLogLevel(logLevel) ? logLevel : 'info', + }; +} + +function validateEnv(): ValidatedEnvironment { + const result: Joi.ValidationResult> = + envSchema.validate(process.env, { + abortEarly: false, + stripUnknown: false, + }); + + if (result.error) { + const messages = result.error.details.map( + (detail) => ` - ${detail.message}`, + ); + console.error(`Environment validation failed:\n${messages.join('\n')}`); + + if (process.env.NODE_ENV === 'production') { + process.exit(1); + } else { + console.warn('Continuing with default values in non-production mode'); + } + } + + const resultValue: unknown = result.value; + return toValidatedEnvironment(isRecord(resultValue) ? resultValue : {}); +} + +export { envSchema, validateEnv }; diff --git a/backend/src/utils/errors.js b/backend/src/utils/errors.ts similarity index 75% rename from backend/src/utils/errors.js rename to backend/src/utils/errors.ts index 97da127..fd1278c 100644 --- a/backend/src/utils/errors.js +++ b/backend/src/utils/errors.ts @@ -1,5 +1,11 @@ +import type { ErrorDetails } from '../types/index.ts'; + class AppError extends Error { - constructor(message, statusCode = 500, details = null) { + statusCode: number; + details: ErrorDetails; + isOperational: true; + + constructor(message: string, statusCode = 500, details: ErrorDetails = null) { super(message); this.statusCode = statusCode; this.details = details; @@ -15,7 +21,7 @@ class NotFoundError extends AppError { } class ValidationError extends AppError { - constructor(message, details = null) { + constructor(message: string, details: ErrorDetails = null) { super(message, 400, details); } } @@ -38,11 +44,11 @@ class ConflictError extends AppError { } } -module.exports = { +export { AppError, - NotFoundError, - ValidationError, - ForbiddenError, - UnauthorizedError, ConflictError, + ForbiddenError, + NotFoundError, + UnauthorizedError, + ValidationError, }; diff --git a/backend/src/utils/global-defaults.ts b/backend/src/utils/global-defaults.ts new file mode 100644 index 0000000..3795d10 --- /dev/null +++ b/backend/src/utils/global-defaults.ts @@ -0,0 +1,62 @@ +import type { + GlobalTransitionDefaultsData, + GlobalUiControlDefaultsData, +} from '../types/index.ts'; + +const transitionTypes = new Set([ + 'fade', + 'slide-left', + 'slide-right', + 'zoom', + 'none', +]); + +const transitionEasings = new Set([ + 'ease-in-out', + 'ease-in', + 'ease-out', + 'linear', +]); + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function isOptionalString(value: unknown): boolean { + return value === undefined || typeof value === 'string'; +} + +function isOptionalNumber(value: unknown): boolean { + return value === undefined || typeof value === 'number'; +} + +function isOptionalRecord(value: unknown): boolean { + return value === undefined || isRecord(value); +} + +function isGlobalTransitionDefaultsData( + value: unknown, +): value is GlobalTransitionDefaultsData { + return ( + isRecord(value) && + (value.transition_type === undefined || + (typeof value.transition_type === 'string' && + transitionTypes.has(value.transition_type))) && + isOptionalNumber(value.duration_ms) && + (value.easing === undefined || + (typeof value.easing === 'string' && transitionEasings.has(value.easing))) && + isOptionalString(value.overlay_color) + ); +} + +function isGlobalUiControlDefaultsData( + value: unknown, +): value is GlobalUiControlDefaultsData { + return ( + isRecord(value) && + isOptionalRecord(value.settings_json) && + isOptionalRecord(value.settings) + ); +} + +export { isGlobalTransitionDefaultsData, isGlobalUiControlDefaultsData }; diff --git a/backend/src/utils/index.js b/backend/src/utils/index.js deleted file mode 100644 index 051340b..0000000 --- a/backend/src/utils/index.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - ...require('./errors'), - ...require('./logger'), - envValidation: require('./env-validation'), -}; diff --git a/backend/src/utils/index.ts b/backend/src/utils/index.ts new file mode 100644 index 0000000..7b3cd75 --- /dev/null +++ b/backend/src/utils/index.ts @@ -0,0 +1,10 @@ +export { + AppError, + ConflictError, + ForbiddenError, + NotFoundError, + UnauthorizedError, + ValidationError, +} from './errors.ts'; +export { logger, requestLogger } from './logger.ts'; +export { envSchema, validateEnv } from './env-validation.ts'; diff --git a/backend/src/utils/logger.js b/backend/src/utils/logger.js deleted file mode 100644 index e77047c..0000000 --- a/backend/src/utils/logger.js +++ /dev/null @@ -1,55 +0,0 @@ -const pino = require('pino'); -const crypto = require('crypto'); - -const shouldPrettyPrint = - process.env.LOG_PRETTY === 'true' || process.env.NODE_ENV === 'development'; - -const logger = pino({ - level: process.env.LOG_LEVEL || 'info', - transport: shouldPrettyPrint - ? { - target: 'pino-pretty', - options: { - colorize: true, - ignore: 'pid,hostname', - messageFormat: '{service} | {msg}', - translateTime: 'SYS:yyyy-mm-dd HH:MM:ss.l', - }, - } - : undefined, - base: { - service: 'tour-builder-api', - env: process.env.NODE_ENV || 'development', - }, -}); - -function requestLogger(req, res, next) { - const requestId = req.headers['x-request-id'] || crypto.randomUUID(); - req.log = logger.child({ requestId }); - req.requestId = requestId; - res.setHeader('X-Request-Id', requestId); - - const start = Date.now(); - res.on('finish', () => { - const duration = Date.now() - start; - const logData = { - method: req.method, - url: req.originalUrl || req.url, - status: res.statusCode, - durationMs: duration, - userAgent: req.headers['user-agent'], - }; - - if (res.statusCode >= 500) { - req.log.error(logData, 'Request completed with server error'); - } else if (res.statusCode >= 400) { - req.log.warn(logData, 'Request completed with client error'); - } else { - req.log.info(logData, 'Request completed'); - } - }); - - next(); -} - -module.exports = { logger, requestLogger }; diff --git a/backend/src/utils/logger.ts b/backend/src/utils/logger.ts new file mode 100644 index 0000000..ebcfe44 --- /dev/null +++ b/backend/src/utils/logger.ts @@ -0,0 +1,75 @@ +import crypto from 'crypto'; + +import type { RequestHandler } from 'express'; +import type { LoggerOptions } from 'pino'; +import pino from 'pino'; + +import { + getRequestLogger, + setRequestId, + setRequestLogger, +} from './request-context.ts'; + +const shouldPrettyPrint = + process.env.LOG_PRETTY === 'true' || process.env.NODE_ENV === 'development'; + +const baseLoggerOptions: LoggerOptions = { + level: process.env.LOG_LEVEL ?? 'info', + base: { + service: 'tour-builder-api', + env: process.env.NODE_ENV ?? 'development', + }, +}; + +const logger = pino( + shouldPrettyPrint + ? { + ...baseLoggerOptions, + transport: { + target: 'pino-pretty', + options: { + colorize: true, + ignore: 'pid,hostname', + messageFormat: '{service} | {msg}', + translateTime: 'SYS:yyyy-mm-dd HH:MM:ss.l', + }, + }, + } + : baseLoggerOptions, +); + +const requestLogger: RequestHandler = (req, res, next) => { + const headerRequestId = req.headers['x-request-id']; + const requestId = + typeof headerRequestId === 'string' ? headerRequestId : crypto.randomUUID(); + + setRequestLogger(req, logger.child({ requestId })); + setRequestId(req, requestId); + res.setHeader('X-Request-Id', requestId); + + const start = Date.now(); + res.on('finish', () => { + const duration = Date.now() - start; + const logData = { + method: req.method, + url: req.originalUrl || req.url, + status: res.statusCode, + durationMs: duration, + userAgent: req.headers['user-agent'], + }; + + const requestLog = getRequestLogger(req) ?? logger; + + if (res.statusCode >= 500) { + requestLog.error(logData, 'Request completed with server error'); + } else if (res.statusCode >= 400) { + requestLog.warn(logData, 'Request completed with client error'); + } else { + requestLog.info(logData, 'Request completed'); + } + }); + + next(); +}; + +export { logger, requestLogger }; diff --git a/backend/src/utils/project-settings-query.ts b/backend/src/utils/project-settings-query.ts new file mode 100644 index 0000000..a241649 --- /dev/null +++ b/backend/src/utils/project-settings-query.ts @@ -0,0 +1,114 @@ +import type { + ProjectSettingsListFilter, + ProjectTransitionSettingsListFilter, +} from '../types/index.ts'; + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function readQueryString(query: unknown, key: string): string | undefined { + if (!isRecord(query)) { + return undefined; + } + + const value = query[key]; + + if (typeof value === 'string') { + return value; + } + + if (Array.isArray(value)) { + return value.find((item) => typeof item === 'string'); + } + + return undefined; +} + +function getProjectSettingsListFilter(query: unknown): ProjectSettingsListFilter { + const filter: ProjectSettingsListFilter = {}; + const id = readQueryString(query, 'id'); + const project = readQueryString(query, 'project'); + const sourceKey = readQueryString(query, 'source_key'); + const environment = readQueryString(query, 'environment'); + const limit = readQueryString(query, 'limit'); + const page = readQueryString(query, 'page'); + const field = readQueryString(query, 'field'); + const sort = readQueryString(query, 'sort'); + + if (id !== undefined) filter.id = id; + if (project !== undefined) filter.project = project; + if (sourceKey !== undefined) filter.source_key = sourceKey; + if ( + environment === 'dev' || + environment === 'stage' || + environment === 'production' + ) { + filter.environment = environment; + } + if (limit !== undefined) filter.limit = limit; + if (page !== undefined) filter.page = page; + if (field !== undefined) filter.field = field; + if (sort !== undefined) filter.sort = sort; + + return filter; +} + +function readQueryRange( + query: unknown, + key: string, +): readonly [string | undefined, string | undefined] | undefined { + if (!isRecord(query)) { + return undefined; + } + + const value = query[key]; + + if (Array.isArray(value)) { + const first = value.find((item) => typeof item === 'string'); + const second = value + .slice(1) + .find((item) => typeof item === 'string'); + return [first, second]; + } + + return undefined; +} + +function getProjectTransitionSettingsListFilter( + query: unknown, +): ProjectTransitionSettingsListFilter { + const filter: ProjectTransitionSettingsListFilter = { + ...getProjectSettingsListFilter(query), + }; + const transitionType = readQueryString(query, 'transition_type'); + const easing = readQueryString(query, 'easing'); + const overlayColor = readQueryString(query, 'overlay_color'); + const active = readQueryString(query, 'active'); + const durationMsRange = readQueryRange(query, 'duration_msRange'); + const createdAtRange = readQueryRange(query, 'createdAtRange'); + + if ( + transitionType === 'fade' || + transitionType === 'none' || + transitionType === 'video' + ) { + filter.transition_type = transitionType; + } + if ( + easing === 'ease-in-out' || + easing === 'ease-in' || + easing === 'ease-out' || + easing === 'linear' + ) { + filter.easing = easing; + } + if (overlayColor !== undefined) filter.overlay_color = overlayColor; + if (active !== undefined) filter.active = active; + if (durationMsRange !== undefined) filter.duration_msRange = durationMsRange; + if (createdAtRange !== undefined) filter.createdAtRange = createdAtRange; + + return filter; +} + +export { getProjectSettingsListFilter, getProjectTransitionSettingsListFilter }; diff --git a/backend/src/utils/project-transition-settings.ts b/backend/src/utils/project-transition-settings.ts new file mode 100644 index 0000000..ea4ec13 --- /dev/null +++ b/backend/src/utils/project-transition-settings.ts @@ -0,0 +1,45 @@ +import type { ProjectTransitionSettingsData } from '../types/index.ts'; + +const transitionTypes = new Set(['fade', 'none', 'video']); +const transitionEasings = new Set([ + 'ease-in-out', + 'ease-in', + 'ease-out', + 'linear', +]); + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function isOptionalString(value: unknown): boolean { + return value === undefined || typeof value === 'string'; +} + +function isOptionalNullableString(value: unknown): boolean { + return value === undefined || value === null || typeof value === 'string'; +} + +function isOptionalNumber(value: unknown): boolean { + return value === undefined || typeof value === 'number'; +} + +function isProjectTransitionSettingsData( + value: unknown, +): value is ProjectTransitionSettingsData { + return ( + isRecord(value) && + isOptionalString(value.id) && + isOptionalNullableString(value.source_key) && + (value.transition_type === undefined || + (typeof value.transition_type === 'string' && + transitionTypes.has(value.transition_type))) && + isOptionalNumber(value.duration_ms) && + (value.easing === undefined || + (typeof value.easing === 'string' && + transitionEasings.has(value.easing))) && + isOptionalString(value.overlay_color) + ); +} + +export { isProjectTransitionSettingsData }; diff --git a/backend/src/utils/project-ui-control-settings.ts b/backend/src/utils/project-ui-control-settings.ts new file mode 100644 index 0000000..4f1ee2e --- /dev/null +++ b/backend/src/utils/project-ui-control-settings.ts @@ -0,0 +1,24 @@ +import type { ProjectUiControlSettingsData } from '../types/index.ts'; + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function isOptionalRecord(value: unknown): boolean { + return value === undefined || isRecord(value); +} + +function isProjectUiControlSettingsData( + value: unknown, +): value is ProjectUiControlSettingsData { + return ( + isRecord(value) && + (value.source_key === undefined || + value.source_key === null || + typeof value.source_key === 'string') && + isOptionalRecord(value.settings_json) && + isOptionalRecord(value.settings) + ); +} + +export { isProjectUiControlSettingsData }; diff --git a/backend/src/utils/request-body.ts b/backend/src/utils/request-body.ts new file mode 100644 index 0000000..7859cbe --- /dev/null +++ b/backend/src/utils/request-body.ts @@ -0,0 +1,31 @@ +import ValidationError from '../services/notifications/errors/validation.ts'; +import type { EntityDataRequestBody } from '../types/index.ts'; + +function hasStringId(value: unknown): value is { id: string } { + return ( + value !== null && + typeof value === 'object' && + 'id' in value && + typeof value.id === 'string' + ); +} + +function isEntityDataRequestBody( + body: unknown, +): body is EntityDataRequestBody { + return body !== null && typeof body === 'object' && 'data' in body; +} + +function assertBodyIdMatchesRouteId(routeId: string, body: unknown): void { + const bodyId = hasStringId(body) + ? body.id + : isEntityDataRequestBody(body) && hasStringId(body.data) + ? body.data.id + : undefined; + + if (bodyId && bodyId !== routeId) { + throw new ValidationError('Request body id does not match route id'); + } +} + +export { assertBodyIdMatchesRouteId, isEntityDataRequestBody }; diff --git a/backend/src/utils/request-context.ts b/backend/src/utils/request-context.ts new file mode 100644 index 0000000..62b46fd --- /dev/null +++ b/backend/src/utils/request-context.ts @@ -0,0 +1,102 @@ +import type { Request } from 'express'; +import type { Logger } from 'pino'; + +import type { CurrentUser, RuntimeContext } from '../types/index.ts'; + +export interface AppRequestContext { + currentUser?: CurrentUser; + runtimeContext?: RuntimeContext; + isRuntimePublicRequest?: boolean; + permissionNameOverride?: string; + log?: Logger; + requestId?: string; + socialAuthToken?: string; +} + +export type AppRequest = Request; +export type AuthenticatedRequest = AppRequest; +export type RuntimeRequest = AppRequest; + +const requestContexts = new WeakMap(); + +function getOrCreateRequestContext(req: Request): AppRequestContext { + const existingContext = requestContexts.get(req); + if (existingContext) return existingContext; + + const context: AppRequestContext = {}; + requestContexts.set(req, context); + return context; +} + +export function getRequestContext(req: Request): AppRequestContext { + return requestContexts.get(req) ?? {}; +} + +export function setCurrentUser(req: Request, currentUser: CurrentUser): void { + getOrCreateRequestContext(req).currentUser = currentUser; +} + +export function getCurrentUser(req: Request): CurrentUser | undefined { + return getRequestContext(req).currentUser; +} + +export function setRuntimeContext( + req: Request, + runtimeContext: RuntimeContext, +): void { + getOrCreateRequestContext(req).runtimeContext = runtimeContext; +} + +export function getRuntimeContext(req: Request): RuntimeContext | undefined { + return getRequestContext(req).runtimeContext; +} + +export function setRuntimePublicRequest( + req: Request, + isRuntimePublicRequest: boolean, +): void { + getOrCreateRequestContext(req).isRuntimePublicRequest = + isRuntimePublicRequest; +} + +export function isRuntimePublicRequest(req: Request): boolean { + return getRequestContext(req).isRuntimePublicRequest === true; +} + +export function setPermissionNameOverride( + req: Request, + permissionNameOverride: string, +): void { + getOrCreateRequestContext(req).permissionNameOverride = + permissionNameOverride; +} + +export function getPermissionNameOverride( + req: Request, +): string | undefined { + return getRequestContext(req).permissionNameOverride; +} + +export function setRequestLogger(req: Request, log: Logger): void { + getOrCreateRequestContext(req).log = log; +} + +export function getRequestLogger(req: Request): Logger | undefined { + return getRequestContext(req).log; +} + +export function setRequestId(req: Request, requestId: string): void { + getOrCreateRequestContext(req).requestId = requestId; +} + +export function getRequestId(req: Request): string | undefined { + return getRequestContext(req).requestId; +} + +export function setSocialAuthToken(req: Request, token: string): void { + getOrCreateRequestContext(req).socialAuthToken = token; +} + +export function getSocialAuthToken(req: Request): string | undefined { + return getRequestContext(req).socialAuthToken; +} diff --git a/backend/src/utils/route-context.ts b/backend/src/utils/route-context.ts new file mode 100644 index 0000000..edc7979 --- /dev/null +++ b/backend/src/utils/route-context.ts @@ -0,0 +1,22 @@ +import type { Request } from 'express'; + +import type { ServiceContext } from '../types/index.ts'; +import { getCurrentUser, getRuntimeContext } from './request-context.ts'; + +function getRouteServiceContext(req: Request): ServiceContext { + const context: ServiceContext = {}; + + const currentUser = getCurrentUser(req); + if (currentUser !== undefined) { + context.currentUser = currentUser; + } + + const runtimeContext = getRuntimeContext(req); + if (runtimeContext !== undefined) { + context.runtimeContext = runtimeContext; + } + + return context; +} + +export { getRouteServiceContext }; diff --git a/backend/src/utils/sqlValidator.js b/backend/src/utils/sqlValidator.ts similarity index 59% rename from backend/src/utils/sqlValidator.js rename to backend/src/utils/sqlValidator.ts index d62d19b..74cb9ef 100644 --- a/backend/src/utils/sqlValidator.js +++ b/backend/src/utils/sqlValidator.ts @@ -1,22 +1,16 @@ -/** - * SQL Validator - * - * Shared validation for read-only SQL queries (widgets, admin SQL executor). - * Ensures queries are SELECT-only and don't contain dangerous patterns. - */ +import type { + ReadOnlySqlValidationOptions, + SqlValidationResult, +} from '../types/index.ts'; const DEFAULT_MAX_LENGTH = 5000; const RESTRICTED_FUNCTIONS = /\b(pg_sleep|set_config|copy)\b/i; -/** - * Validate a SQL query for read-only execution - * @param {string} sql - The SQL query to validate - * @param {object} options - Validation options - * @param {number} [options.maxLength=5000] - Maximum allowed query length - * @returns {{ valid: boolean, error?: string, normalized?: string }} - */ -function validateReadOnlySql(sql, options = {}) { - const maxLength = options.maxLength || DEFAULT_MAX_LENGTH; +function validateReadOnlySql( + sql: unknown, + options: ReadOnlySqlValidationOptions = {}, +): SqlValidationResult { + const maxLength = options.maxLength ?? DEFAULT_MAX_LENGTH; if (typeof sql !== 'string' || !sql.trim()) { return { valid: false, error: 'SQL query must be a non-empty string' }; @@ -50,4 +44,4 @@ function validateReadOnlySql(sql, options = {}) { return { valid: true, normalized }; } -module.exports = { validateReadOnlySql }; +export { validateReadOnlySql }; diff --git a/backend/src/validators/request-schemas.js b/backend/src/validators/request-schemas.ts similarity index 50% rename from backend/src/validators/request-schemas.js rename to backend/src/validators/request-schemas.ts index ace3f34..216f3db 100644 --- a/backend/src/validators/request-schemas.js +++ b/backend/src/validators/request-schemas.ts @@ -1,4 +1,10 @@ -const Joi = require('joi'); +import Joi from 'joi'; + +import type { RequestSchemaMap } from '../types/index.ts'; + +interface EnvelopeForDataOptions { + requireData?: boolean; +} const uuid = Joi.string().guid({ version: ['uuidv4'] }); const idParam = Joi.object({ id: uuid.required() }).required(); @@ -67,6 +73,19 @@ const optionalSlug = Joi.string() .pattern(/^[a-z0-9_-]+$/i); const fileUrl = Joi.string().trim().min(1).max(4096); +const relationFile = Joi.object({ + id: uuid.optional(), + name: Joi.string().allow('', null).max(4096), + sizeInBytes: Joi.number().integer().min(0), + privateUrl: Joi.string().allow('', null).max(4096), + publicUrl: Joi.string().allow('', null).max(4096), + new: Joi.boolean(), +}).unknown(false); +const relationFileInput = Joi.alternatives().try( + relationFile, + Joi.array().items(relationFile), + Joi.valid(null), +); const userData = Joi.object({ id: uuid.optional(), @@ -86,7 +105,7 @@ const userData = Joi.object({ app_role: entityId.allow(null), custom_permissions: Joi.array().items(entityId), allowed_private_production_project_ids: Joi.array().items(entityId), - avatar: Joi.any(), + avatar: relationFileInput, }).unknown(true); const userCreateData = userData.keys({ @@ -161,7 +180,12 @@ const tourPageCreateData = tourPageData }) .or('project', 'projectId', 'project_id'); -function envelopeForData(dataSchema, { requireData = true } = {}) { +function envelopeForData( + dataSchema: Joi.Schema, + options: EnvelopeForDataOptions = {}, +): Joi.ObjectSchema { + const requireData = options.requireData ?? true; + return Joi.object({ id: uuid.optional(), data: requireData ? dataSchema.required() : dataSchema, @@ -170,188 +194,193 @@ function envelopeForData(dataSchema, { requireData = true } = {}) { .required(); } -module.exports = { - auth: { - signinLocal: { - body: Joi.object({ - email: Joi.string().trim().email().required(), - password: Joi.string().required(), - }) - .unknown(false) - .required(), - }, - passwordReset: { - body: Joi.object({ - token: Joi.string().trim().required(), - password: Joi.string().required(), - }) - .unknown(false) - .required(), - }, - passwordUpdate: { - body: Joi.object({ - currentPassword: Joi.string().required(), - newPassword: Joi.string().required(), - }) - .unknown(false) - .required(), - }, - sendPasswordResetEmail: { - body: Joi.object({ - email: Joi.string().trim().email().required(), - }) - .unknown(false) - .required(), - }, - profile: { - body: Joi.object({ - profile: Joi.object().required().unknown(true), - }) - .unknown(false) - .required(), - }, - verifyEmail: { - body: Joi.object({ - token: Joi.string().trim().required(), - }) - .unknown(false) - .required(), - }, - socialSignin: { - query: Joi.object({ - app: Joi.string().allow('').max(255), - }) - .unknown(false) - .required(), - }, +const auth = { + signinLocal: { + body: Joi.object({ + email: Joi.string().trim().email().required(), + password: Joi.string().required(), + }) + .unknown(false) + .required(), }, - crud: { - create: { body: dataEnvelope }, - update: { params: idParam, body: updateDataEnvelope }, - remove: { params: idParam }, - deleteByIds: { body: deleteByIdsBody }, - list: { query: listQuery }, - count: { query: countQuery }, - autocomplete: { query: autocompleteQuery }, - findOne: { params: idParam }, + passwordReset: { + body: Joi.object({ + token: Joi.string().trim().required(), + password: Joi.string().required(), + }) + .unknown(false) + .required(), }, - file: { - presign: { - body: Joi.object({ - urls: Joi.array().items(fileUrl.required()).min(1).max(50).required(), - }) - .unknown(false) - .required(), - }, - upload: { - params: Joi.object({ - table: Joi.string() - .trim() - .max(128) - .pattern(/^[A-Za-z0-9_-]+$/) - .required(), - field: Joi.string() - .trim() - .max(128) - .pattern(/^[A-Za-z0-9_-]+$/) - .required(), - }).required(), - }, - initUploadSession: { - body: Joi.object({ - folder: Joi.string().trim().min(1).max(512).required(), - filename: Joi.string().trim().min(1).max(255).required(), - totalChunks: Joi.number().integer().min(1).max(10000).required(), - size: Joi.number().min(0).required(), - contentType: Joi.string().trim().allow('').max(255), - }) - .unknown(false) - .required(), - }, - session: { - params: Joi.object({ - sessionId: uuid.required(), - }).required(), - }, - chunk: { - params: Joi.object({ - sessionId: uuid.required(), - chunkIndex: Joi.number().integer().min(0).max(9999).required(), - }).required(), - }, + passwordUpdate: { + body: Joi.object({ + currentPassword: Joi.string().required(), + newPassword: Joi.string().required(), + }) + .unknown(false) + .required(), }, + sendPasswordResetEmail: { + body: Joi.object({ + email: Joi.string().trim().email().required(), + }) + .unknown(false) + .required(), + }, + profile: { + body: Joi.object({ + profile: Joi.object().required().unknown(true), + }) + .unknown(false) + .required(), + }, + verifyEmail: { + body: Joi.object({ + token: Joi.string().trim().required(), + }) + .unknown(false) + .required(), + }, + socialSignin: { + query: Joi.object({ + app: Joi.string().allow('').max(255), + }) + .unknown(false) + .required(), + }, +} satisfies Record; + +const crud = { + create: { body: dataEnvelope }, + update: { params: idParam, body: updateDataEnvelope }, + remove: { params: idParam }, + deleteByIds: { body: deleteByIdsBody }, + list: { query: listQuery }, + count: { query: countQuery }, + autocomplete: { query: autocompleteQuery }, + findOne: { params: idParam }, +} satisfies Record; + +const file = { + presign: { + body: Joi.object({ + urls: Joi.array().items(fileUrl.required()).min(1).max(50).required(), + }) + .unknown(false) + .required(), + }, + upload: { + params: Joi.object({ + table: Joi.string() + .trim() + .max(128) + .pattern(/^[A-Za-z0-9_-]+$/) + .required(), + field: Joi.string() + .trim() + .max(128) + .pattern(/^[A-Za-z0-9_-]+$/) + .required(), + }).required(), + }, + initUploadSession: { + body: Joi.object({ + folder: Joi.string().trim().min(1).max(512).required(), + filename: Joi.string().trim().min(1).max(255).required(), + totalChunks: Joi.number().integer().min(1).max(10000).required(), + size: Joi.number().min(0).required(), + contentType: Joi.string().trim().allow('').max(255), + }) + .unknown(false) + .required(), + }, + session: { + params: Joi.object({ + sessionId: uuid.required(), + }).required(), + }, + chunk: { + params: Joi.object({ + sessionId: uuid.required(), + chunkIndex: Joi.number().integer().min(0).max(9999).required(), + }).required(), + }, +} satisfies Record; + +const publish = { publish: { - publish: { - body: Joi.object({ - projectId: uuid.required(), - title: Joi.string().allow('', null).max(255), - description: Joi.string().allow('', null).max(2000), - }) - .unknown(false) - .required(), - }, - saveToStage: { - body: Joi.object({ - projectId: uuid.required(), - }) - .unknown(false) - .required(), - }, + body: Joi.object({ + projectId: uuid.required(), + title: Joi.string().allow('', null).max(255), + description: Joi.string().allow('', null).max(2000), + }) + .unknown(false) + .required(), }, - projects: { - create: { body: envelopeForData(projectCreateData) }, - update: { params: idParam, body: envelopeForData(projectData) }, - clone: { params: idParam }, + saveToStage: { + body: Joi.object({ + projectId: uuid.required(), + }) + .unknown(false) + .required(), }, - tourPages: { - create: { body: envelopeForData(tourPageCreateData) }, - update: { params: idParam, body: envelopeForData(tourPageData) }, - reorder: { - body: Joi.object({ - data: Joi.object({ - project: uuid.optional(), - projectId: uuid.optional(), - environment: Joi.string().valid('dev').default('dev'), - orderedPageIds: Joi.array().items(uuid.required()).min(1).required(), - }) - .or('project', 'projectId') - .required() - .unknown(false), +} satisfies Record; + +const projects = { + create: { body: envelopeForData(projectCreateData) }, + update: { params: idParam, body: envelopeForData(projectData) }, + clone: { params: idParam }, +} satisfies Record; + +const tourPages = { + create: { body: envelopeForData(tourPageCreateData) }, + update: { params: idParam, body: envelopeForData(tourPageData) }, + reorder: { + body: Joi.object({ + data: Joi.object({ + project: uuid.optional(), + projectId: uuid.optional(), + environment: Joi.string().valid('dev').default('dev'), + orderedPageIds: Joi.array().items(uuid.required()).min(1).required(), + }) + .or('project', 'projectId') + .required() + .unknown(false), + }) + .unknown(false) + .required(), + }, + duplicate: { + params: idParam, + body: Joi.object({ + data: Joi.object({ + project: uuid.optional(), + projectId: uuid.optional(), + environment: Joi.string().valid('dev').default('dev'), + name: Joi.string().trim().allow('').max(255), + slug: optionalSlug, }) .unknown(false) - .required(), - }, - duplicate: { - params: idParam, - body: Joi.object({ - data: Joi.object({ - project: uuid.optional(), - projectId: uuid.optional(), - environment: Joi.string().valid('dev').default('dev'), - name: Joi.string().trim().allow('').max(255), - slug: optionalSlug, - }) - .unknown(false) - .default({}), - }) - .unknown(false) - .required(), - }, - reverseVideoStatus: { - body: Joi.object({ - storageKeys: Joi.array() - .items(fileUrl.required()) - .min(1) - .max(200) - .required(), - }) - .unknown(false) - .required(), - }, + .default({}), + }) + .unknown(false) + .required(), }, - users: { - create: { body: envelopeForData(userCreateData) }, - update: { params: idParam, body: envelopeForData(userData) }, + reverseVideoStatus: { + body: Joi.object({ + storageKeys: Joi.array() + .items(fileUrl.required()) + .min(1) + .max(200) + .required(), + }) + .unknown(false) + .required(), }, - emptyQuery, -}; +} satisfies Record; + +const users = { + create: { body: envelopeForData(userCreateData) }, + update: { params: idParam, body: envelopeForData(userData) }, +} satisfies Record; + +export { auth, crud, emptyQuery, file, projects, publish, tourPages, users }; diff --git a/backend/tests/access-policy.test.js b/backend/tests/access-policy.test.ts similarity index 55% rename from backend/tests/access-policy.test.js rename to backend/tests/access-policy.test.ts index 8c41ff4..f6a5008 100644 --- a/backend/tests/access-policy.test.js +++ b/backend/tests/access-policy.test.ts @@ -1,10 +1,15 @@ -const assert = require('node:assert/strict'); -const test = require('node:test'); +import assert from 'node:assert/strict'; +import test from 'node:test'; -const AccessPolicy = require('../src/services/access-policy'); +import AccessPolicy from '../src/services/access-policy.ts'; +import type { CurrentUser, RoleRecord } from '../src/types/index.ts'; -test('effective permissions combine role permissions and custom permissions', async () => { - const user = { +function userWithRole(id: string, appRole: RoleRecord): CurrentUser { + return { id, app_role: appRole }; +} + +void test('effective permissions combine role permissions and custom permissions', async () => { + const user: CurrentUser = { id: 'user-1', app_role: { name: 'Tour Designer', @@ -18,8 +23,8 @@ test('effective permissions combine role permissions and custom permissions', as assert.equal(await AccessPolicy.hasPermission(user, 'DELETE_USERS'), false); }); -test('public users cannot use admin api even if stale permissions exist', async () => { - const user = { +void test('public users cannot use admin api even if stale permissions exist', async () => { + const user: CurrentUser = { id: 'public-1', app_role: { name: 'Public', @@ -33,17 +38,17 @@ test('public users cannot use admin api even if stale permissions exist', async assert.equal(AccessPolicy.canUseAdminApi(user), false); }); -test('public role permissions are ignored for fallback permission checks', async () => { +void test('public role permissions are ignored for fallback permission checks', async () => { const permissions = await AccessPolicy.getRolePermissionNames({ name: 'Public', permissions: [{ name: 'READ_PROJECTS' }], }); - assert.equal(permissions.size, 0); + assert.equal(permissions?.size, 0); }); -test('internal users with permissions can use admin api', () => { - const user = { +void test('internal users with permissions can use admin api', () => { + const user: CurrentUser = { id: 'staff-1', app_role: { name: 'Content Reviewer', @@ -56,17 +61,13 @@ test('internal users with permissions can use admin api', () => { assert.equal(AccessPolicy.canUseAdminApi(user), true); }); -test('platform-wide roles are explicit', () => { +void test('platform-wide roles are explicit', () => { assert.equal( - AccessPolicy.isPlatformWideRole({ - app_role: { name: 'Administrator' }, - }), + AccessPolicy.isPlatformWideRole(userWithRole('admin-1', { name: 'Administrator' })), true, ); assert.equal( - AccessPolicy.isPlatformWideRole({ - app_role: { name: 'Tour Designer' }, - }), + AccessPolicy.isPlatformWideRole(userWithRole('designer-1', { name: 'Tour Designer' })), false, ); }); diff --git a/backend/tests/check-permissions.test.js b/backend/tests/check-permissions.test.js deleted file mode 100644 index cd41111..0000000 --- a/backend/tests/check-permissions.test.js +++ /dev/null @@ -1,77 +0,0 @@ -const assert = require('node:assert/strict'); -const test = require('node:test'); - -const RolesDBApi = require('../src/db/api/roles'); -const AccessPolicy = require('../src/services/access-policy'); - -const originalFindBy = RolesDBApi.findBy; -RolesDBApi.findBy = async () => ({ - id: 'public-role', - name: 'Public', - permissions: [], -}); - -const { checkCrudPermissions } = require('../src/middlewares/check-permissions'); - -test.after(() => { - RolesDBApi.findBy = originalFindBy; -}); - -test('checkCrudPermissions honors explicit permission override', async () => { - const originalHasPermission = AccessPolicy.hasPermission; - const seenPermissions = []; - - AccessPolicy.hasPermission = async (_user, permission) => { - seenPermissions.push(permission); - return permission === 'UPDATE_PAGE_ELEMENTS'; - }; - - try { - const req = { - method: 'DELETE', - path: '/project/project-id/env/dev', - currentUser: { id: 'user-1' }, - permissionNameOverride: 'UPDATE_PAGE_ELEMENTS', - }; - - await new Promise((resolve, reject) => { - checkCrudPermissions('page_elements')(req, {}, (error) => { - if (error) reject(error); - else resolve(); - }); - }); - - assert.deepEqual(seenPermissions, ['UPDATE_PAGE_ELEMENTS']); - } finally { - AccessPolicy.hasPermission = originalHasPermission; - } -}); - -test('checkCrudPermissions keeps default method-derived permission without override', async () => { - const originalHasPermission = AccessPolicy.hasPermission; - const seenPermissions = []; - - AccessPolicy.hasPermission = async (_user, permission) => { - seenPermissions.push(permission); - return permission === 'DELETE_PAGE_ELEMENTS'; - }; - - try { - const req = { - method: 'DELETE', - path: '/project/project-id/env/dev', - currentUser: { id: 'user-1' }, - }; - - await new Promise((resolve, reject) => { - checkCrudPermissions('page_elements')(req, {}, (error) => { - if (error) reject(error); - else resolve(); - }); - }); - - assert.deepEqual(seenPermissions, ['DELETE_PAGE_ELEMENTS']); - } finally { - AccessPolicy.hasPermission = originalHasPermission; - } -}); diff --git a/backend/tests/check-permissions.test.ts b/backend/tests/check-permissions.test.ts new file mode 100644 index 0000000..5ea07f5 --- /dev/null +++ b/backend/tests/check-permissions.test.ts @@ -0,0 +1,24 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + getCrudPermissionName, +} from '../src/middlewares/check-permissions.ts'; + +void test('getCrudPermissionName honors explicit permission override', () => { + assert.equal( + getCrudPermissionName( + 'DELETE', + 'page_elements', + 'UPDATE_PAGE_ELEMENTS', + ), + 'UPDATE_PAGE_ELEMENTS', + ); +}); + +void test('getCrudPermissionName keeps default method-derived permission without override', () => { + assert.equal( + getCrudPermissionName('DELETE', 'page_elements'), + 'DELETE_PAGE_ELEMENTS', + ); +}); diff --git a/backend/tests/helpers.test.js b/backend/tests/helpers.test.ts similarity index 59% rename from backend/tests/helpers.test.js rename to backend/tests/helpers.test.ts index 239f897..7d95b54 100644 --- a/backend/tests/helpers.test.js +++ b/backend/tests/helpers.test.ts @@ -1,9 +1,9 @@ -const assert = require('node:assert/strict'); -const test = require('node:test'); +import assert from 'node:assert/strict'; +import test from 'node:test'; -const { assertRouteIdMatchesBody } = require('../src/helpers'); +import { assertRouteIdMatchesBody } from '../src/helpers.ts'; -test('assertRouteIdMatchesBody allows matching top-level body id', () => { +void test('assertRouteIdMatchesBody allows matching top-level body id', () => { assert.doesNotThrow(() => assertRouteIdMatchesBody({ params: { id: 'route-id' }, @@ -12,7 +12,7 @@ test('assertRouteIdMatchesBody allows matching top-level body id', () => { ); }); -test('assertRouteIdMatchesBody allows matching data body id', () => { +void test('assertRouteIdMatchesBody allows matching data body id', () => { assert.doesNotThrow(() => assertRouteIdMatchesBody({ params: { id: 'route-id' }, @@ -21,7 +21,7 @@ test('assertRouteIdMatchesBody allows matching data body id', () => { ); }); -test('assertRouteIdMatchesBody rejects mismatched body id', () => { +void test('assertRouteIdMatchesBody rejects mismatched body id', () => { assert.throws( () => assertRouteIdMatchesBody({ diff --git a/backend/tests/integration/access-policy.test.js b/backend/tests/integration/access-policy.test.ts similarity index 58% rename from backend/tests/integration/access-policy.test.js rename to backend/tests/integration/access-policy.test.ts index c2e8b6a..e32c53c 100644 --- a/backend/tests/integration/access-policy.test.js +++ b/backend/tests/integration/access-policy.test.ts @@ -1,19 +1,32 @@ -const assert = require('node:assert/strict'); -const test = require('node:test'); +import assert from 'node:assert/strict'; +import test from 'node:test'; +import type { TestContext } from 'node:test'; +import type { Transaction } from 'sequelize'; -const db = require('../../src/db/models'); -const AccessPolicy = require('../../src/services/access-policy'); -const AccessPolicyAuditService = require('../../src/services/access-policy-audit'); +import db from '../../src/db/models/index.ts'; +import AccessPolicy from '../../src/services/access-policy.ts'; +import AccessPolicyAuditService from '../../src/services/access-policy-audit.ts'; +import type { + CurrentUser, + PermissionRecord, + ProjectCreatePayload, + ProjectModelRecord, + ProductionPresentationVisibility, + RoleModelRecord, + UserModelRecord, + UserProductionPresentationAccessPayload, + UserRecord, +} from '../../src/types/index.ts'; const suffix = `${Date.now()}-${process.pid}`; -test.after(async () => { +void test.after(async () => { await db.sequelize.close(); }); -async function authenticateWithTimeout(timeoutMs = 1500) { - let timeoutId; - const timeout = new Promise((_, reject) => { +async function authenticateWithTimeout(timeoutMs = 1500): Promise { + let timeoutId: NodeJS.Timeout | undefined; + const timeout = new Promise((_, reject) => { timeoutId = setTimeout( () => reject(new Error(`Database unavailable after ${timeoutMs}ms`)), timeoutMs, @@ -23,15 +36,21 @@ async function authenticateWithTimeout(timeoutMs = 1500) { try { await Promise.race([db.sequelize.authenticate(), timeout]); } finally { - clearTimeout(timeoutId); + if (timeoutId !== undefined) { + clearTimeout(timeoutId); + } } } -async function withTransaction(t, callback) { +async function withTransaction( + t: TestContext, + callback: (transaction: Transaction) => Promise, +): Promise { try { await authenticateWithTimeout(); } catch (error) { - t.skip(`Database unavailable: ${error.message}`); + const message = error instanceof Error ? error.message : 'unknown error'; + t.skip(`Database unavailable: ${message}`); return; } @@ -43,15 +62,29 @@ async function withTransaction(t, callback) { } } -async function createRole(name, transaction) { +async function createRole( + name: string, + transaction: Transaction, +): Promise { return db.roles.create({ name }, { transaction }); } -async function createPermission(name, transaction) { +function createPermission( + name: string, + transaction: Transaction, +): Promise { return db.permissions.create({ name }, { transaction }); } -async function createUser({ email, role }, transaction) { +interface CreateUserOptions { + email: string; + role: RoleModelRecord; +} + +async function createUser( + { email, role }: CreateUserOptions, + transaction: Transaction, +): Promise { const user = await db.users.create( { email, @@ -64,18 +97,83 @@ async function createUser({ email, role }, transaction) { return user; } -async function createProject({ slug, visibility }, transaction) { - return db.projects.create( - { - name: `Test ${slug}`, - slug, - production_presentation_visibility: visibility, - }, - { transaction }, - ); +interface CreateProjectOptions { + slug: string; + visibility: ProductionPresentationVisibility; } -test('guest can view public production presentation only', async (t) => { +async function createProject( + { slug, visibility }: CreateProjectOptions, + transaction: Transaction, +): Promise { + const data: ProjectCreatePayload = { + id: undefined, + name: `Test ${slug}`, + slug, + description: undefined, + logo_url: undefined, + favicon_url: undefined, + og_image_url: undefined, + design_width: undefined, + design_height: undefined, + production_presentation_visibility: visibility, + importHash: null, + createdById: null, + updatedById: null, + }; + + return db.projects.create(data, { transaction }); +} + +function requireUserModelRecord( + user: UserModelRecord | null, +): UserModelRecord { + if (!user) { + throw new Error('Expected test user to exist.'); + } + + return user; +} + +function toCurrentUser(user: UserRecord): CurrentUser { + const currentUser: CurrentUser = { id: user.id }; + + if (typeof user.email === 'string') { + currentUser.email = user.email; + } + + if (user.app_role !== undefined) { + currentUser.app_role = user.app_role; + } + + if (user.custom_permissions !== undefined) { + currentUser.custom_permissions = user.custom_permissions; + } + + if (user.app_role_permissions !== undefined) { + currentUser.app_role_permissions = user.app_role_permissions; + } + + return currentUser; +} + +function createProductionAccessPayload( + userId: string, + projectId: string, +): UserProductionPresentationAccessPayload { + const now = new Date(); + + return { + userId, + projectId, + createdAt: now, + updatedAt: now, + createdById: null, + updatedById: null, + }; +} + +void test('guest can view public production presentation only', async (t) => { await withTransaction(t, async (transaction) => { const publicProject = await createProject( { @@ -111,7 +209,7 @@ test('guest can view public production presentation only', async (t) => { }); }); -test('public user can view granted private production presentation', async (t) => { +void test('public user can view granted private production presentation', async (t) => { await withTransaction(t, async (transaction) => { const publicRole = await createRole('Public', transaction); const publicUser = await createUser( @@ -130,10 +228,7 @@ test('public user can view granted private production presentation', async (t) = ); await db.production_presentation_access.create( - { - userId: publicUser.id, - projectId: privateProject.id, - }, + createProductionAccessPayload(publicUser.id, privateProject.id), { transaction }, ); @@ -146,9 +241,11 @@ test('public user can view granted private production presentation', async (t) = transaction, }); + const authUserRecord = requireUserModelRecord(authUser); + assert.equal( await AccessPolicy.canViewProductionPresentation( - authUser.get({ plain: true }), + toCurrentUser(authUserRecord.get({ plain: true })), privateProject.slug, { transaction }, ), @@ -157,7 +254,7 @@ test('public user can view granted private production presentation', async (t) = }); }); -test('internal user with permission can use admin api and view private presentation', async (t) => { +void test('internal user with permission can use admin api and view private presentation', async (t) => { await withTransaction(t, async (transaction) => { const role = await createRole('Content Reviewer', transaction); const permission = await createPermission( @@ -189,7 +286,9 @@ test('internal user with permission can use admin api and view private presentat ], transaction, }); - const plainUser = authUser.get({ plain: true }); + const plainUser = toCurrentUser( + requireUserModelRecord(authUser).get({ plain: true }), + ); assert.equal(AccessPolicy.canUseAdminApi(plainUser), true); assert.equal( @@ -203,7 +302,7 @@ test('internal user with permission can use admin api and view private presentat }); }); -test('audit finds and cleanup removes stale Public grants', async (t) => { +void test('audit finds and cleanup removes stale Public grants', async (t) => { await withTransaction(t, async (transaction) => { const publicRole = await createRole('Public', transaction); const internalRole = await createRole('Tour Designer', transaction); @@ -237,10 +336,7 @@ test('audit finds and cleanup removes stale Public grants', async (t) => { transaction, ); await db.production_presentation_access.create( - { - userId: internalUser.id, - projectId: privateProject.id, - }, + createProductionAccessPayload(internalUser.id, privateProject.id), { transaction }, ); diff --git a/backend/tests/request-validation.test.js b/backend/tests/request-validation.test.js deleted file mode 100644 index dbb11b7..0000000 --- a/backend/tests/request-validation.test.js +++ /dev/null @@ -1,129 +0,0 @@ -const test = require('node:test'); -const assert = require('node:assert/strict'); -const Joi = require('joi'); - -const { validateRequest } = require('../src/middlewares/validate-request'); -const { commonErrorHandler } = require('../src/helpers'); -const { - crud, - projects, - tourPages, - users, -} = require('../src/validators/request-schemas'); - -function runMiddleware(middleware, req) { - return new Promise((resolve) => { - middleware(req, {}, (error) => resolve(error || null)); - }); -} - -test('validateRequest applies converted sanitized values to request parts', async () => { - const req = { - query: { - limit: '25', - page: '2', - field: 'createdAt', - sort: 'DESC', - name: 'Lobby', - }, - }; - - const error = await runMiddleware(validateRequest(crud.list), req); - - assert.equal(error, null); - assert.equal(req.query.limit, 25); - assert.equal(req.query.page, 2); - assert.equal(req.query.name, 'Lobby'); -}); - -test('validateRequest returns structured request validation error details', async () => { - const req = { - body: { - data: ['not-object'], - }, - }; - - const error = await runMiddleware(validateRequest(crud.create), req); - - assert.equal(error.code, 400); - assert.equal(error.isRequestValidation, true); - assert.equal(error.details[0].path, 'body.data'); -}); - -test('commonErrorHandler sends request validation errors as JSON', () => { - const error = { - code: 400, - message: 'Invalid request', - isRequestValidation: true, - details: [{ path: 'body.email', message: 'email is required' }], - }; - const res = { - statusCode: null, - payload: null, - status(code) { - this.statusCode = code; - return this; - }, - send(payload) { - this.payload = payload; - return this; - }, - }; - - commonErrorHandler(error, {}, res, () => {}); - - assert.equal(res.statusCode, 400); - assert.deepEqual(res.payload, { - error: 'Invalid request', - details: [{ path: 'body.email', message: 'email is required' }], - }); -}); - -test('user update schema preserves omitted permissions fields', async () => { - const req = { - params: { id: '094121b9-c567-469a-b256-ba221b7fd5d6' }, - body: { - data: { - firstName: 'Admin', - }, - }, - }; - - const error = await runMiddleware(validateRequest(users.update), req); - - assert.equal(error, null); - assert.equal( - Object.prototype.hasOwnProperty.call(req.body.data, 'custom_permissions'), - false, - ); - assert.equal( - Object.prototype.hasOwnProperty.call( - req.body.data, - 'allowed_private_production_project_ids', - ), - false, - ); -}); - -test('create schemas reject missing fields required before service layer', async () => { - const userError = await runMiddleware(validateRequest(users.create), { - body: { data: { firstName: 'No Email' } }, - }); - const projectError = await runMiddleware(validateRequest(projects.create), { - body: { data: { name: 'No Slug' } }, - }); - const tourPageError = await runMiddleware(validateRequest(tourPages.create), { - body: { data: { name: 'No Project', slug: 'no-project' } }, - }); - - assert.equal(userError.isRequestValidation, true); - assert.equal(projectError.isRequestValidation, true); - assert.equal(tourPageError.isRequestValidation, true); -}); - -test('validateRequest rejects invalid schema maps during route setup', () => { - assert.throws( - () => validateRequest({ body: Joi.object(), cookies: Joi.object() }), - /Unsupported request validation part/, - ); -}); diff --git a/backend/tests/request-validation.test.ts b/backend/tests/request-validation.test.ts new file mode 100644 index 0000000..65d039c --- /dev/null +++ b/backend/tests/request-validation.test.ts @@ -0,0 +1,147 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import type { NextFunction, Request, RequestHandler } from 'express'; +import Joi from 'joi'; +import { createRequest, createResponse } from 'node-mocks-http'; + +import { commonErrorHandler } from '../src/helpers.ts'; +import { validateRequest } from '../src/middlewares/validate-request.ts'; +import { + crud, + projects, + tourPages, + users, +} from '../src/validators/request-schemas.ts'; +import type { RouteError } from '../src/types/index.ts'; + +interface UserUpdateTestBody { + data: { + firstName: string; + custom_permissions?: unknown; + allowed_private_production_project_ids?: unknown; + }; +} + +function runMiddleware( + middleware: RequestHandler, + req: Request, +): Promise { + const res = createResponse(); + + return new Promise((resolve) => { + const next: NextFunction = (error?: unknown) => { + resolve(error instanceof Error ? error : null); + }; + + middleware(req, res, next); + }); +} + +void test('validateRequest applies converted sanitized values to request parts', async () => { + const req = createRequest({ + query: { + limit: '25', + page: '2', + field: 'createdAt', + sort: 'DESC', + name: 'Lobby', + }, + }); + + const error = await runMiddleware(validateRequest(crud.list), req); + + assert.equal(error, null); + assert.equal(req.query.limit, 25); + assert.equal(req.query.page, 2); + assert.equal(req.query.name, 'Lobby'); +}); + +void test('validateRequest returns structured request validation error details', async () => { + const req = createRequest({ + body: { + data: ['not-object'], + }, + }); + + const error = await runMiddleware(validateRequest(crud.create), req); + + assert.equal(error?.code, 400); + assert.equal(error?.isRequestValidation, true); + assert.equal(error?.details?.[0]?.path, 'body.data'); +}); + +void test('commonErrorHandler sends request validation errors as JSON', () => { + const error: RouteError = new Error('Invalid request'); + error.code = 400; + error.isRequestValidation = true; + error.details = [{ path: 'body.email', message: 'email is required' }]; + + const req = createRequest(); + const res = createResponse(); + + commonErrorHandler(error, req, res, () => {}); + + assert.equal(res.statusCode, 400); + assert.deepEqual(res._getData(), { + error: 'Invalid request', + details: [{ path: 'body.email', message: 'email is required' }], + }); +}); + +void test('user update schema preserves omitted permissions fields', async () => { + const req = createRequest>({ + params: { id: '094121b9-c567-469a-b256-ba221b7fd5d6' }, + body: { + data: { + firstName: 'Admin', + }, + }, + }); + + const error = await runMiddleware(validateRequest(users.update), req); + + assert.equal(error, null); + assert.equal( + Object.prototype.hasOwnProperty.call(req.body.data, 'custom_permissions'), + false, + ); + assert.equal( + Object.prototype.hasOwnProperty.call( + req.body.data, + 'allowed_private_production_project_ids', + ), + false, + ); +}); + +void test('create schemas reject missing fields required before service layer', async () => { + const userError = await runMiddleware( + validateRequest(users.create), + createRequest({ body: { data: { firstName: 'No Email' } } }), + ); + const projectError = await runMiddleware( + validateRequest(projects.create), + createRequest({ body: { data: { name: 'No Slug' } } }), + ); + const tourPageError = await runMiddleware( + validateRequest(tourPages.create), + createRequest({ body: { data: { name: 'No Project', slug: 'no-project' } } }), + ); + + assert.equal(userError?.isRequestValidation, true); + assert.equal(projectError?.isRequestValidation, true); + assert.equal(tourPageError?.isRequestValidation, true); +}); + +void test('validateRequest rejects invalid schema maps during route setup', () => { + const invalidSchemas = { body: Joi.object() }; + Object.defineProperty(invalidSchemas, 'cookies', { + enumerable: true, + value: Joi.object(), + }); + + assert.throws( + () => validateRequest(invalidSchemas), + /Unsupported request validation part/, + ); +}); diff --git a/backend/tests/update-contracts.test.js b/backend/tests/update-contracts.test.js deleted file mode 100644 index 2e798ef..0000000 --- a/backend/tests/update-contracts.test.js +++ /dev/null @@ -1,486 +0,0 @@ -const assert = require('node:assert/strict'); -const test = require('node:test'); - -const db = require('../src/db/models'); -const GenericDBApi = require('../src/db/api/base.api'); -const { createEntityService } = require('../src/factories/service.factory'); - -test('GenericDBApi.update uses object signature and forwards update context', async () => { - const calls = {}; - const transaction = { id: 'tx-db-api' }; - const currentUser = { id: 'user-1' }; - const record = { - async update(payload, options) { - calls.update = { payload, options }; - }, - async setTags(value, options) { - calls.setTags = { value, options }; - }, - }; - - class TestDBApi extends GenericDBApi { - static get MODEL() { - return { - rawAttributes: {}, - getTableName: () => 'test_records', - async findByPk(id, options) { - calls.findByPk = { id, options }; - return record; - }, - }; - } - - static get ASSOCIATIONS() { - return [{ field: 'tags', setter: 'setTags', isArray: true }]; - } - } - - const result = await TestDBApi.update({ - id: 'record-1', - data: { name: 'Updated', tags: ['a', 'b'], skipped: undefined }, - currentUser, - transaction, - }); - - assert.equal(result, record); - assert.deepEqual(calls.findByPk, { - id: 'record-1', - options: { transaction }, - }); - assert.deepEqual(calls.update, { - payload: { - updatedById: 'user-1', - name: 'Updated', - tags: ['a', 'b'], - }, - options: { transaction }, - }); - assert.deepEqual(calls.setTags, { - value: ['a', 'b'], - options: { transaction }, - }); -}); - -test('GenericDBApi.update rejects positional signature', async () => { - class TestDBApi extends GenericDBApi { - static get MODEL() { - return { - rawAttributes: {}, - getTableName: () => 'test_records', - }; - } - } - - await assert.rejects( - () => TestDBApi.update('record-1', { name: 'Updated' }, {}), - /DBApi\.update expects an options object/, - ); -}); - -test('GenericDBApi.create uses object signature and forwards create context', async () => { - const calls = {}; - const transaction = { id: 'tx-create' }; - const currentUser = { id: 'user-1' }; - const record = { - id: 'record-1', - async setTags(value, options) { - calls.setTags = { value, options }; - }, - }; - - class TestDBApi extends GenericDBApi { - static get MODEL() { - return { - rawAttributes: {}, - getTableName: () => 'test_records', - async create(payload, options) { - calls.create = { payload, options }; - return record; - }, - }; - } - - static get ASSOCIATIONS() { - return [{ field: 'tags', setter: 'setTags', isArray: true }]; - } - } - - const result = await TestDBApi.create({ - data: { name: 'Created', tags: ['a', 'b'] }, - currentUser, - transaction, - }); - - assert.equal(result, record); - assert.deepEqual(calls.create, { - payload: { - name: 'Created', - tags: ['a', 'b'], - importHash: null, - createdById: 'user-1', - updatedById: 'user-1', - }, - options: { transaction }, - }); - assert.deepEqual(calls.setTags, { - value: ['a', 'b'], - options: { transaction }, - }); -}); - -test('GenericDBApi.create rejects positional signature', async () => { - class TestDBApi extends GenericDBApi { - static get MODEL() { - return { - rawAttributes: {}, - getTableName: () => 'test_records', - }; - } - } - - await assert.rejects( - () => TestDBApi.create({ name: 'Created' }, {}), - /DBApi\.create requires \{ data \}/, - ); -}); - -test('GenericDBApi.partialUpdate uses object signature and only updates defined fields', async () => { - const calls = {}; - const transaction = { id: 'tx-partial' }; - const currentUser = { id: 'user-1' }; - const record = { - async update(payload, options) { - calls.update = { payload, options }; - }, - }; - - class TestDBApi extends GenericDBApi { - static get MODEL() { - return { - rawAttributes: {}, - getTableName: () => 'test_records', - async findByPk(id, options) { - calls.findByPk = { id, options }; - return record; - }, - }; - } - } - - const result = await TestDBApi.partialUpdate({ - id: 'record-1', - data: { name: 'Updated', skipped: undefined }, - currentUser, - transaction, - }); - - assert.equal(result, record); - assert.deepEqual(calls.findByPk, { - id: 'record-1', - options: { transaction }, - }); - assert.deepEqual(calls.update, { - payload: { - updatedById: 'user-1', - name: 'Updated', - }, - options: { transaction }, - }); -}); - -test('GenericDBApi.partialUpdate rejects positional signature', async () => { - class TestDBApi extends GenericDBApi { - static get MODEL() { - return { - rawAttributes: {}, - getTableName: () => 'test_records', - }; - } - } - - await assert.rejects( - () => TestDBApi.partialUpdate('record-1', { name: 'Updated' }, {}), - /DBApi\.update expects an options object/, - ); -}); - -test('createEntityService update uses object signature and manages own transaction', async () => { - const originalTransaction = db.sequelize.transaction; - const calls = {}; - const transaction = { - async commit() { - calls.committed = true; - }, - async rollback() { - calls.rolledBack = true; - }, - }; - - db.sequelize.transaction = async () => transaction; - - const DBApi = { - async findBy(where, options) { - calls.findBy = { where, options }; - return { id: where.id }; - }, - async update(options) { - calls.update = options; - return { id: options.id, ...options.data }; - }, - }; - - try { - const Service = createEntityService(DBApi, { entityName: 'TestEntity' }); - const currentUser = { id: 'user-1' }; - const runtimeContext = { environment: 'dev' }; - - const result = await Service.update({ - id: 'record-1', - data: { name: 'Updated' }, - currentUser, - runtimeContext, - }); - - assert.deepEqual(result, { id: 'record-1', name: 'Updated' }); - assert.deepEqual(calls.findBy, { - where: { id: 'record-1' }, - options: { transaction, runtimeContext }, - }); - assert.deepEqual(calls.update, { - id: 'record-1', - data: { name: 'Updated' }, - currentUser, - transaction, - runtimeContext, - }); - assert.equal(calls.committed, true); - assert.equal(calls.rolledBack, undefined); - } finally { - db.sequelize.transaction = originalTransaction; - } -}); - -test('createEntityService update rejects positional signature', async () => { - const Service = createEntityService( - { - async findBy() { - throw new Error('should not be called'); - }, - async update() { - throw new Error('should not be called'); - }, - }, - { entityName: 'TestEntity' }, - ); - - await assert.rejects( - () => Service.update({ name: 'Updated' }, 'record-1', { id: 'user-1' }), - /Service\.update requires \{ id, data \}/, - ); -}); - -test('createEntityService create, deleteByIds, and remove use object signatures', async () => { - const originalTransaction = db.sequelize.transaction; - const calls = {}; - const transaction = { - async commit() { - calls.commits = (calls.commits || 0) + 1; - }, - async rollback() { - calls.rollbacks = (calls.rollbacks || 0) + 1; - }, - }; - db.sequelize.transaction = async () => transaction; - - const DBApi = { - async create(options) { - calls.create = options; - return { id: 'created-1', ...options.data }; - }, - async deleteByIds(options) { - calls.deleteByIds = options; - }, - async remove(options) { - calls.remove = options; - }, - }; - - try { - const Service = createEntityService(DBApi, { entityName: 'TestEntity' }); - const currentUser = { id: 'user-1' }; - const runtimeContext = { environment: 'stage' }; - - const created = await Service.create({ - data: { name: 'Created' }, - currentUser, - runtimeContext, - }); - await Service.deleteByIds({ - ids: ['record-1', 'record-2'], - currentUser, - runtimeContext, - }); - await Service.remove({ - id: 'record-1', - currentUser, - runtimeContext, - }); - - assert.deepEqual(created, { id: 'created-1', name: 'Created' }); - assert.deepEqual(calls.create, { - data: { name: 'Created' }, - currentUser, - transaction, - runtimeContext, - }); - assert.deepEqual(calls.deleteByIds, { - ids: ['record-1', 'record-2'], - currentUser, - transaction, - runtimeContext, - }); - assert.deepEqual(calls.remove, { - id: 'record-1', - currentUser, - transaction, - runtimeContext, - }); - assert.equal(calls.commits, 3); - assert.equal(calls.rollbacks, undefined); - } finally { - db.sequelize.transaction = originalTransaction; - } -}); - -test('createEntityService create, deleteByIds, and remove reject positional signatures', async () => { - const Service = createEntityService( - { - async create() { - throw new Error('should not be called'); - }, - async deleteByIds() { - throw new Error('should not be called'); - }, - async remove() { - throw new Error('should not be called'); - }, - }, - { entityName: 'TestEntity' }, - ); - - await assert.rejects( - () => Service.create({ name: 'Created' }, { id: 'user-1' }), - /Service\.create requires \{ data \}/, - ); - await assert.rejects( - () => Service.deleteByIds(['record-1'], { id: 'user-1' }), - /Service\.deleteByIds expects an options object/, - ); - await assert.rejects( - () => Service.remove('record-1', { id: 'user-1' }), - /Service\.remove expects an options object/, - ); -}); - -test('GenericDBApi deleteByIds, remove, and findAllAutocomplete use object signatures', async () => { - const calls = {}; - const transaction = { id: 'tx-db-api' }; - const currentUser = { id: 'user-1' }; - const deletedRecords = [ - { - id: 'record-1', - async update(payload, options) { - calls.deletedUpdate = { payload, options }; - }, - async destroy(options) { - calls.deletedDestroy = options; - }, - }, - ]; - const removedRecord = { - id: 'record-2', - async update(payload, options) { - calls.removedUpdate = { payload, options }; - }, - async destroy(options) { - calls.removedDestroy = options; - }, - }; - const autocompleteRecords = [{ id: 'record-3', name: 'Alpha' }]; - - class TestDBApi extends GenericDBApi { - static get MODEL() { - return { - rawAttributes: {}, - getTableName: () => 'test_records', - async findAll(options) { - calls.findAll = options; - return options.attributes ? autocompleteRecords : deletedRecords; - }, - async findByPk(id, options) { - calls.findByPk = { id, options }; - return removedRecord; - }, - }; - } - } - - const deleted = await TestDBApi.deleteByIds({ - ids: ['record-1'], - currentUser, - transaction, - }); - const removed = await TestDBApi.remove({ - id: 'record-2', - currentUser, - transaction, - }); - const autocomplete = await TestDBApi.findAllAutocomplete( - { query: 'Alpha', limit: 5, offset: 0 }, - { transaction }, - ); - - assert.equal(deleted, deletedRecords); - assert.equal(removed, removedRecord); - assert.deepEqual(calls.deletedUpdate, { - payload: { deletedBy: 'user-1' }, - options: { transaction }, - }); - assert.deepEqual(calls.deletedDestroy, { transaction }); - assert.deepEqual(calls.findByPk, { - id: 'record-2', - options: { transaction }, - }); - assert.deepEqual(calls.removedUpdate, { - payload: { deletedBy: 'user-1' }, - options: { transaction }, - }); - assert.deepEqual(calls.removedDestroy, { transaction }); - assert.deepEqual(autocomplete, [{ id: 'record-3', label: 'Alpha' }]); - assert.equal(calls.findAll.limit, 5); - assert.equal(calls.findAll.transaction, transaction); -}); - -test('GenericDBApi deleteByIds, remove, and findAllAutocomplete reject positional signatures', async () => { - class TestDBApi extends GenericDBApi { - static get MODEL() { - return { - rawAttributes: {}, - getTableName: () => 'test_records', - }; - } - } - - await assert.rejects( - () => TestDBApi.deleteByIds(['record-1'], {}), - /DBApi\.deleteByIds expects an options object/, - ); - await assert.rejects( - () => TestDBApi.remove('record-1', {}), - /DBApi\.remove expects an options object/, - ); - await assert.rejects( - () => TestDBApi.findAllAutocomplete('Alpha', 5, 0), - /DBApi\.findAllAutocomplete expects an options object/, - ); -}); diff --git a/backend/tests/update-contracts.test.ts b/backend/tests/update-contracts.test.ts new file mode 100644 index 0000000..59e1884 --- /dev/null +++ b/backend/tests/update-contracts.test.ts @@ -0,0 +1,582 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import db from '../src/db/models/index.ts'; +import GenericDBApi from '../src/db/api/base.api.ts'; +import { createEntityService } from '../src/factories/service.factory.ts'; +import type { + CurrentUser, + DbData, + EntityRecord, + EntityServiceDbApi, + RuntimeContext, + RuntimeEnvironment, +} from '../src/types/index.ts'; + +interface TestTransactionOptions { + transaction?: unknown; +} + +interface TestManagedTransaction { + id: string; + commit(): Promise; + rollback(): Promise; +} + +interface TestAssociationRecord { + id?: string; + name?: string; + update(payload: DbData, options: TestTransactionOptions): Promise; + setTags?(value: unknown, options: TestTransactionOptions): Promise; + destroy?(options: TestTransactionOptions): Promise; +} + +interface TestModelOptions extends TestTransactionOptions { + attributes?: string[]; + limit?: number; +} + +interface UpdateContractCalls { + findByPk?: { id: string; options: TestTransactionOptions }; + findAll?: TestModelOptions; + update?: { payload: DbData; options: TestTransactionOptions }; + setTags?: { value: unknown; options: TestTransactionOptions }; + create?: { payload: DbData; options: TestTransactionOptions }; + deletedUpdate?: { payload: DbData; options: TestTransactionOptions }; + deletedDestroy?: TestTransactionOptions; + removedUpdate?: { payload: DbData; options: TestTransactionOptions }; + removedDestroy?: TestTransactionOptions; + findBy?: { where: { id: string }; options: TestTransactionOptions }; + serviceUpdate?: unknown; + serviceCreate?: unknown; + serviceDeleteByIds?: unknown; + serviceRemove?: unknown; + committed?: true; + rolledBack?: true; + commits?: number; + rollbacks?: number; +} + +interface TestEntity extends EntityRecord { + name?: string; +} + +interface TestEntityData { + name: string; +} + +function createCurrentUser(): CurrentUser { + return { id: 'user-1' }; +} + +function createRuntimeContext(environment: RuntimeEnvironment): RuntimeContext { + return { + mode: 'admin', + projectSlug: null, + headerEnvironment: environment, + }; +} + +function isTestEntityData(data: unknown): data is TestEntityData { + return data !== null && typeof data === 'object' && 'name' in data; +} + +function replaceSequelizeTransaction( + transaction: TestManagedTransaction, +): () => void { + const originalTransaction = db.sequelize.transaction.bind(db.sequelize); + + Object.defineProperty(db.sequelize, 'transaction', { + configurable: true, + value: () => Promise.resolve(transaction), + }); + + return () => { + Object.defineProperty(db.sequelize, 'transaction', { + configurable: true, + value: originalTransaction, + }); + }; +} + +function createServiceDbApi( + calls: UpdateContractCalls, +): EntityServiceDbApi { + return { + create(options) { + calls.serviceCreate = options; + const data = isTestEntityData(options.data) ? options.data : { name: '' }; + return Promise.resolve({ id: 'created-1', ...data }); + }, + bulkImport() { + return Promise.resolve([]); + }, + findBy(where, options) { + calls.findBy = { where, options }; + return Promise.resolve({ id: where.id }); + }, + update(options) { + calls.serviceUpdate = options; + const data = isTestEntityData(options.data) ? options.data : { name: '' }; + return Promise.resolve({ id: options.id, ...data }); + }, + deleteByIds(options) { + calls.serviceDeleteByIds = options; + return Promise.resolve([]); + }, + remove(options) { + calls.serviceRemove = options; + return Promise.resolve({ id: options.id }); + }, + }; +} + +void test('GenericDBApi.update uses object signature and forwards update context', async () => { + const calls: UpdateContractCalls = {}; + const currentUser = createCurrentUser(); + const record: TestAssociationRecord = { + update(payload, options) { + calls.update = { payload, options }; + return Promise.resolve(); + }, + setTags(value, options) { + calls.setTags = { value, options }; + return Promise.resolve(); + }, + }; + + class TestDBApi extends GenericDBApi { + static override get MODEL(): unknown { + return { + rawAttributes: {}, + getTableName: () => 'test_records', + findByPk(id: string, options: TestTransactionOptions) { + calls.findByPk = { id, options }; + return Promise.resolve(record); + }, + }; + } + + static override get ASSOCIATIONS() { + return [{ field: 'tags', setter: 'setTags', isArray: true }]; + } + } + + const result = await TestDBApi.update({ + id: 'record-1', + data: { name: 'Updated', tags: ['a', 'b'], skipped: undefined }, + currentUser, + }); + + assert.equal(result, record); + assert.deepEqual(calls.findByPk, { + id: 'record-1', + options: {}, + }); + assert.deepEqual(calls.update, { + payload: { + updatedById: 'user-1', + name: 'Updated', + tags: ['a', 'b'], + }, + options: {}, + }); + assert.deepEqual(calls.setTags, { + value: ['a', 'b'], + options: {}, + }); +}); + +void test('GenericDBApi.update rejects positional signature', async () => { + class TestDBApi extends GenericDBApi { + static override get MODEL(): unknown { + return { + rawAttributes: {}, + getTableName: () => 'test_records', + }; + } + } + + await assert.rejects( + () => TestDBApi.update('record-1'), + /DBApi\.update expects an options object/, + ); +}); + +void test('GenericDBApi.create uses object signature and forwards create context', async () => { + const calls: UpdateContractCalls = {}; + const currentUser = createCurrentUser(); + const record: TestAssociationRecord = { + id: 'record-1', + update() { + return Promise.resolve(); + }, + setTags(value, options) { + calls.setTags = { value, options }; + return Promise.resolve(); + }, + }; + + class TestDBApi extends GenericDBApi { + static override get MODEL(): unknown { + return { + rawAttributes: {}, + getTableName: () => 'test_records', + create(payload: DbData, options: TestTransactionOptions) { + calls.create = { payload, options }; + return Promise.resolve(record); + }, + }; + } + + static override get ASSOCIATIONS() { + return [{ field: 'tags', setter: 'setTags', isArray: true }]; + } + } + + const result = await TestDBApi.create({ + data: { name: 'Created', tags: ['a', 'b'] }, + currentUser, + }); + + assert.equal(result, record); + assert.deepEqual(calls.create, { + payload: { + name: 'Created', + tags: ['a', 'b'], + importHash: null, + createdById: 'user-1', + updatedById: 'user-1', + }, + options: {}, + }); + assert.deepEqual(calls.setTags, { + value: ['a', 'b'], + options: {}, + }); +}); + +void test('GenericDBApi.create rejects positional signature', async () => { + class TestDBApi extends GenericDBApi { + static override get MODEL(): unknown { + return { + rawAttributes: {}, + getTableName: () => 'test_records', + }; + } + } + + await assert.rejects( + () => TestDBApi.create({ name: 'Created' }), + /DBApi\.create requires \{ data \}/, + ); +}); + +void test('GenericDBApi.partialUpdate uses object signature and only updates defined fields', async () => { + const calls: UpdateContractCalls = {}; + const currentUser = createCurrentUser(); + const record: TestAssociationRecord = { + update(payload, options) { + calls.update = { payload, options }; + return Promise.resolve(); + }, + }; + + class TestDBApi extends GenericDBApi { + static override get MODEL(): unknown { + return { + rawAttributes: {}, + getTableName: () => 'test_records', + findByPk(id: string, options: TestTransactionOptions) { + calls.findByPk = { id, options }; + return Promise.resolve(record); + }, + }; + } + } + + const result = await TestDBApi.partialUpdate({ + id: 'record-1', + data: { name: 'Updated', skipped: undefined }, + currentUser, + }); + + assert.equal(result, record); + assert.deepEqual(calls.findByPk, { + id: 'record-1', + options: {}, + }); + assert.deepEqual(calls.update, { + payload: { + updatedById: 'user-1', + name: 'Updated', + }, + options: {}, + }); +}); + +void test('GenericDBApi.partialUpdate rejects positional signature', async () => { + class TestDBApi extends GenericDBApi { + static override get MODEL(): unknown { + return { + rawAttributes: {}, + getTableName: () => 'test_records', + }; + } + } + + await assert.rejects( + () => TestDBApi.partialUpdate('record-1'), + /DBApi\.update expects an options object/, + ); +}); + +void test('createEntityService update uses object signature and manages own transaction', async () => { + const calls: UpdateContractCalls = {}; + const transaction: TestManagedTransaction = { + id: 'tx-service-update', + commit() { + calls.committed = true; + return Promise.resolve(); + }, + rollback() { + calls.rolledBack = true; + return Promise.resolve(); + }, + }; + const restoreTransaction = replaceSequelizeTransaction(transaction); + + try { + const Service = createEntityService(createServiceDbApi(calls), { + entityName: 'TestEntity', + }); + const currentUser = createCurrentUser(); + const runtimeContext = createRuntimeContext('dev'); + + const result = await Service.update({ + id: 'record-1', + data: { name: 'Updated' }, + currentUser, + runtimeContext, + }); + + assert.deepEqual(result, { id: 'record-1', name: 'Updated' }); + assert.deepEqual(calls.findBy, { + where: { id: 'record-1' }, + options: { transaction, runtimeContext }, + }); + assert.deepEqual(calls.serviceUpdate, { + id: 'record-1', + data: { name: 'Updated' }, + currentUser, + transaction, + runtimeContext, + }); + assert.equal(calls.committed, true); + assert.equal(calls.rolledBack, undefined); + } finally { + restoreTransaction(); + } +}); + +void test('createEntityService update rejects positional signature', async () => { + const Service = createEntityService(createServiceDbApi({}), { + entityName: 'TestEntity', + }); + + await assert.rejects( + () => Service.update({ name: 'Updated' }), + /Service\.update requires \{ id, data \}/, + ); +}); + +void test('createEntityService create, deleteByIds, and remove use object signatures', async () => { + const calls: UpdateContractCalls = {}; + const transaction: TestManagedTransaction = { + id: 'tx-service-write', + commit() { + calls.commits = (calls.commits ?? 0) + 1; + return Promise.resolve(); + }, + rollback() { + calls.rollbacks = (calls.rollbacks ?? 0) + 1; + return Promise.resolve(); + }, + }; + const restoreTransaction = replaceSequelizeTransaction(transaction); + + try { + const Service = createEntityService(createServiceDbApi(calls), { + entityName: 'TestEntity', + }); + const currentUser = createCurrentUser(); + const runtimeContext = createRuntimeContext('stage'); + + const created = await Service.create({ + data: { name: 'Created' }, + currentUser, + runtimeContext, + }); + await Service.deleteByIds({ + ids: ['record-1', 'record-2'], + currentUser, + runtimeContext, + }); + await Service.remove({ + id: 'record-1', + currentUser, + runtimeContext, + }); + + assert.deepEqual(created, { id: 'created-1', name: 'Created' }); + assert.deepEqual(calls.serviceCreate, { + data: { name: 'Created' }, + currentUser, + transaction, + runtimeContext, + }); + assert.deepEqual(calls.serviceDeleteByIds, { + ids: ['record-1', 'record-2'], + currentUser, + transaction, + runtimeContext, + }); + assert.deepEqual(calls.serviceRemove, { + id: 'record-1', + currentUser, + transaction, + runtimeContext, + }); + assert.equal(calls.commits, 3); + assert.equal(calls.rollbacks, undefined); + } finally { + restoreTransaction(); + } +}); + +void test('createEntityService create, deleteByIds, and remove reject positional signatures', async () => { + const Service = createEntityService(createServiceDbApi({}), { + entityName: 'TestEntity', + }); + + await assert.rejects( + () => Service.create({ name: 'Created' }), + /Service\.create requires \{ data \}/, + ); + await assert.rejects( + () => Service.deleteByIds(['record-1']), + /Service\.deleteByIds expects an options object/, + ); + await assert.rejects( + () => Service.remove('record-1'), + /Service\.remove expects an options object/, + ); +}); + +void test('GenericDBApi deleteByIds, remove, and findAllAutocomplete use object signatures', async () => { + const calls: UpdateContractCalls = {}; + const currentUser = createCurrentUser(); + const deletedRecords: TestAssociationRecord[] = [ + { + id: 'record-1', + update(payload, options) { + calls.deletedUpdate = { payload, options }; + return Promise.resolve(); + }, + destroy(options) { + calls.deletedDestroy = options; + return Promise.resolve(); + }, + }, + ]; + const removedRecord: TestAssociationRecord = { + id: 'record-2', + update(payload, options) { + calls.removedUpdate = { payload, options }; + return Promise.resolve(); + }, + destroy(options) { + calls.removedDestroy = options; + return Promise.resolve(); + }, + }; + const autocompleteRecords: TestAssociationRecord[] = [ + { id: 'record-3', name: 'Alpha', update: () => Promise.resolve() }, + ]; + + class TestDBApi extends GenericDBApi { + static override get MODEL(): unknown { + return { + rawAttributes: {}, + getTableName: () => 'test_records', + findAll(options: TestModelOptions) { + calls.findAll = options; + return Promise.resolve( + options.attributes ? autocompleteRecords : deletedRecords, + ); + }, + findByPk(id: string, options: TestTransactionOptions) { + calls.findByPk = { id, options }; + return Promise.resolve(removedRecord); + }, + }; + } + } + + const deleted = await TestDBApi.deleteByIds({ + ids: ['record-1'], + currentUser, + }); + const removed = await TestDBApi.remove({ + id: 'record-2', + currentUser, + }); + const autocomplete = await TestDBApi.findAllAutocomplete({ + query: 'Alpha', + limit: 5, + offset: 0, + }); + + assert.equal(deleted, deletedRecords); + assert.equal(removed, removedRecord); + assert.deepEqual(calls.deletedUpdate, { + payload: { deletedBy: 'user-1' }, + options: {}, + }); + assert.deepEqual(calls.deletedDestroy, {}); + assert.deepEqual(calls.findByPk, { + id: 'record-2', + options: {}, + }); + assert.deepEqual(calls.removedUpdate, { + payload: { deletedBy: 'user-1' }, + options: {}, + }); + assert.deepEqual(calls.removedDestroy, {}); + assert.deepEqual(autocomplete, [{ id: 'record-3', label: 'Alpha' }]); + assert.equal(calls.findAll?.limit, 5); +}); + +void test('GenericDBApi deleteByIds, remove, and findAllAutocomplete reject positional signatures', async () => { + class TestDBApi extends GenericDBApi { + static override get MODEL(): unknown { + return { + rawAttributes: {}, + getTableName: () => 'test_records', + }; + } + } + + await assert.rejects( + () => TestDBApi.deleteByIds(['record-1']), + /DBApi\.deleteByIds expects an options object/, + ); + await assert.rejects( + () => TestDBApi.remove('record-1'), + /DBApi\.remove expects an options object/, + ); + await assert.rejects( + () => TestDBApi.findAllAutocomplete('Alpha'), + /DBApi\.findAllAutocomplete expects an options object/, + ); +}); diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..3ef59ee --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2024", + "lib": ["ES2024"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "rootDir": ".", + "outDir": "dist", + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "allowJs": false, + "checkJs": false, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noEmitOnError": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "exactOptionalPropertyTypes": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "rewriteRelativeImportExtensions": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.d.ts", + "tests/**/*.ts", + "scripts/**/*.ts", + "watcher.ts" + ], + "exclude": ["dist", "node_modules"] +} diff --git a/backend/watcher.js b/backend/watcher.ts similarity index 52% rename from backend/watcher.js rename to backend/watcher.ts index b85561a..da6e3bf 100644 --- a/backend/watcher.js +++ b/backend/watcher.ts @@ -1,15 +1,38 @@ -const chokidar = require('chokidar'); -const { exec } = require('child_process'); -const nodemon = require('nodemon'); -const { logger } = require('./src/utils/logger'); +import chokidar from 'chokidar'; +import { exec } from 'node:child_process'; +import type { ExecException } from 'node:child_process'; +import nodemon from 'nodemon'; + +import { logger } from './src/utils/logger.ts'; const nodeEnv = process.env.NODE_ENV || 'dev_stage'; -const childEnv = { ...process.env, NODE_ENV: nodeEnv }; +const childEnv = buildChildEnv(process.env, nodeEnv); const log = logger.child({ module: 'watcher' }); -function logCommandResult(error, stdout, stderr, successMessage) { - const output = stdout && stdout.trim(); - const errorOutput = stderr && stderr.trim(); +function buildChildEnv( + env: NodeJS.ProcessEnv, + fallbackNodeEnv: string, +): Record { + const child: Record = {}; + + for (const [key, value] of Object.entries(env)) { + if (value !== undefined) { + child[key] = value; + } + } + + child.NODE_ENV = fallbackNodeEnv; + return child; +} + +function logCommandResult( + error: ExecException | null, + stdout: string, + stderr: string, + successMessage: string, +): void { + const output = stdout.trim(); + const errorOutput = stderr.trim(); if (output) { log.info({ output }, successMessage); @@ -28,6 +51,11 @@ function logCommandResult(error, stdout, stderr, successMessage) { const migrationsWatcher = chokidar.watch('./src/db/migrations', { persistent: true, ignoreInitial: true, + usePolling: true, + interval: 1000, +}); +migrationsWatcher.on('error', (error) => { + log.error({ err: error }, 'Migrations watcher failed'); }); migrationsWatcher.on('add', (filePath) => { log.info({ filePath }, 'New migration file detected'); @@ -39,6 +67,11 @@ migrationsWatcher.on('add', (filePath) => { const seedersWatcher = chokidar.watch('./src/db/seeders', { persistent: true, ignoreInitial: true, + usePolling: true, + interval: 1000, +}); +seedersWatcher.on('error', (error) => { + log.error({ err: error }, 'Seeders watcher failed'); }); seedersWatcher.on('add', (filePath) => { log.info({ filePath }, 'New seed file detected'); @@ -48,10 +81,18 @@ seedersWatcher.on('add', (filePath) => { }); nodemon({ - script: './src/index.js', + script: './src/index.ts', + exec: 'node', env: childEnv, - ignore: ['./src/db/migrations', './src/db/seeders'], - delay: '500', + watch: ['./src'], + ext: 'js,ts,json', + ignore: [ + './src/db/migrations', + './src/db/seeders', + './dist', + './node_modules', + ], + delay: 500, }); nodemon.on('start', () => { diff --git a/backend/yarn.lock b/backend/yarn.lock deleted file mode 100644 index 9cac3a1..0000000 --- a/backend/yarn.lock +++ /dev/null @@ -1,6038 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@apidevtools/json-schema-ref-parser@^9.0.6": - version "9.1.2" - resolved "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz" - integrity sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg== - dependencies: - "@jsdevtools/ono" "^7.1.3" - "@types/json-schema" "^7.0.6" - call-me-maybe "^1.0.1" - js-yaml "^4.1.0" - -"@apidevtools/openapi-schemas@^2.0.4": - version "2.1.0" - resolved "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz" - integrity sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ== - -"@apidevtools/swagger-methods@^3.0.2": - version "3.0.2" - resolved "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz" - integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg== - -"@apidevtools/swagger-parser@10.0.3": - version "10.0.3" - resolved "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz" - integrity sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g== - dependencies: - "@apidevtools/json-schema-ref-parser" "^9.0.6" - "@apidevtools/openapi-schemas" "^2.0.4" - "@apidevtools/swagger-methods" "^3.0.2" - "@jsdevtools/ono" "^7.1.3" - call-me-maybe "^1.0.1" - z-schema "^5.0.1" - -"@aws-crypto/crc32@5.2.0": - version "5.2.0" - resolved "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz" - integrity sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg== - dependencies: - "@aws-crypto/util" "^5.2.0" - "@aws-sdk/types" "^3.222.0" - tslib "^2.6.2" - -"@aws-crypto/crc32c@5.2.0": - version "5.2.0" - resolved "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz" - integrity sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag== - dependencies: - "@aws-crypto/util" "^5.2.0" - "@aws-sdk/types" "^3.222.0" - tslib "^2.6.2" - -"@aws-crypto/sha1-browser@5.2.0": - version "5.2.0" - resolved "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz" - integrity sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg== - dependencies: - "@aws-crypto/supports-web-crypto" "^5.2.0" - "@aws-crypto/util" "^5.2.0" - "@aws-sdk/types" "^3.222.0" - "@aws-sdk/util-locate-window" "^3.0.0" - "@smithy/util-utf8" "^2.0.0" - tslib "^2.6.2" - -"@aws-crypto/sha256-browser@5.2.0": - version "5.2.0" - resolved "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz" - integrity sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw== - dependencies: - "@aws-crypto/sha256-js" "^5.2.0" - "@aws-crypto/supports-web-crypto" "^5.2.0" - "@aws-crypto/util" "^5.2.0" - "@aws-sdk/types" "^3.222.0" - "@aws-sdk/util-locate-window" "^3.0.0" - "@smithy/util-utf8" "^2.0.0" - tslib "^2.6.2" - -"@aws-crypto/sha256-js@^5.2.0", "@aws-crypto/sha256-js@5.2.0": - version "5.2.0" - resolved "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz" - integrity sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA== - dependencies: - "@aws-crypto/util" "^5.2.0" - "@aws-sdk/types" "^3.222.0" - tslib "^2.6.2" - -"@aws-crypto/supports-web-crypto@^5.2.0": - version "5.2.0" - resolved "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz" - integrity sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg== - dependencies: - tslib "^2.6.2" - -"@aws-crypto/util@^5.2.0", "@aws-crypto/util@5.2.0": - version "5.2.0" - resolved "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz" - integrity sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ== - dependencies: - "@aws-sdk/types" "^3.222.0" - "@smithy/util-utf8" "^2.0.0" - tslib "^2.6.2" - -"@aws-sdk/client-s3@^3.1011.0": - version "3.1045.0" - resolved "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1045.0.tgz" - integrity sha512-fsuO3Y6t+3Ro9Bsg41DKj4Sfy53CGSrhnMldNplWmG8Tx0UbYk+YDa4RD1hVlJpERw4JBmPkl0+J9qlxMh1pcA== - dependencies: - "@aws-crypto/sha1-browser" "5.2.0" - "@aws-crypto/sha256-browser" "5.2.0" - "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "^3.974.8" - "@aws-sdk/credential-provider-node" "^3.972.39" - "@aws-sdk/middleware-bucket-endpoint" "^3.972.10" - "@aws-sdk/middleware-expect-continue" "^3.972.10" - "@aws-sdk/middleware-flexible-checksums" "^3.974.16" - "@aws-sdk/middleware-host-header" "^3.972.10" - "@aws-sdk/middleware-location-constraint" "^3.972.10" - "@aws-sdk/middleware-logger" "^3.972.10" - "@aws-sdk/middleware-recursion-detection" "^3.972.11" - "@aws-sdk/middleware-sdk-s3" "^3.972.37" - "@aws-sdk/middleware-ssec" "^3.972.10" - "@aws-sdk/middleware-user-agent" "^3.972.38" - "@aws-sdk/region-config-resolver" "^3.972.13" - "@aws-sdk/signature-v4-multi-region" "^3.996.25" - "@aws-sdk/types" "^3.973.8" - "@aws-sdk/util-endpoints" "^3.996.8" - "@aws-sdk/util-user-agent-browser" "^3.972.10" - "@aws-sdk/util-user-agent-node" "^3.973.24" - "@smithy/config-resolver" "^4.4.17" - "@smithy/core" "^3.23.17" - "@smithy/eventstream-serde-browser" "^4.2.14" - "@smithy/eventstream-serde-config-resolver" "^4.3.14" - "@smithy/eventstream-serde-node" "^4.2.14" - "@smithy/fetch-http-handler" "^5.3.17" - "@smithy/hash-blob-browser" "^4.2.15" - "@smithy/hash-node" "^4.2.14" - "@smithy/hash-stream-node" "^4.2.14" - "@smithy/invalid-dependency" "^4.2.14" - "@smithy/md5-js" "^4.2.14" - "@smithy/middleware-content-length" "^4.2.14" - "@smithy/middleware-endpoint" "^4.4.32" - "@smithy/middleware-retry" "^4.5.7" - "@smithy/middleware-serde" "^4.2.20" - "@smithy/middleware-stack" "^4.2.14" - "@smithy/node-config-provider" "^4.3.14" - "@smithy/node-http-handler" "^4.6.1" - "@smithy/protocol-http" "^5.3.14" - "@smithy/smithy-client" "^4.12.13" - "@smithy/types" "^4.14.1" - "@smithy/url-parser" "^4.2.14" - "@smithy/util-base64" "^4.3.2" - "@smithy/util-body-length-browser" "^4.2.2" - "@smithy/util-body-length-node" "^4.2.3" - "@smithy/util-defaults-mode-browser" "^4.3.49" - "@smithy/util-defaults-mode-node" "^4.2.54" - "@smithy/util-endpoints" "^3.4.2" - "@smithy/util-middleware" "^4.2.14" - "@smithy/util-retry" "^4.3.6" - "@smithy/util-stream" "^4.5.25" - "@smithy/util-utf8" "^4.2.2" - "@smithy/util-waiter" "^4.3.0" - tslib "^2.6.2" - -"@aws-sdk/core@^3.974.8": - version "3.974.8" - resolved "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.8.tgz" - integrity sha512-njR2qoG6ZuB0kvAS2FyICsFZJ6gmCcf2X/7JcD14sUvGDm26wiZ5BrA6LOiUxKFEF+IVe7kdroxyE00YlkiYsw== - dependencies: - "@aws-sdk/types" "^3.973.8" - "@aws-sdk/xml-builder" "^3.972.22" - "@smithy/core" "^3.23.17" - "@smithy/node-config-provider" "^4.3.14" - "@smithy/property-provider" "^4.2.14" - "@smithy/protocol-http" "^5.3.14" - "@smithy/signature-v4" "^5.3.14" - "@smithy/smithy-client" "^4.12.13" - "@smithy/types" "^4.14.1" - "@smithy/util-base64" "^4.3.2" - "@smithy/util-middleware" "^4.2.14" - "@smithy/util-retry" "^4.3.6" - "@smithy/util-utf8" "^4.2.2" - tslib "^2.6.2" - -"@aws-sdk/crc64-nvme@^3.972.7": - version "3.972.7" - resolved "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.7.tgz" - integrity sha512-QUagVVBbC8gODCF6e1aV0mE2TXWB9Opz4k8EJFdNrujUVQm5R4AjJa1mpOqzwOuROBzqJU9zawzig7M96L8Ejg== - dependencies: - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@aws-sdk/credential-provider-env@^3.972.34": - version "3.972.34" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.34.tgz" - integrity sha512-XT0jtf8Fw9JE6ppsQeoNnZRiG+jqRixMT1v1ZR17G60UvVdsQmTG8nbEyHuEPfMxDXEhfdARaM/XiEhca4lGHQ== - dependencies: - "@aws-sdk/core" "^3.974.8" - "@aws-sdk/types" "^3.973.8" - "@smithy/property-provider" "^4.2.14" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@aws-sdk/credential-provider-http@^3.972.36": - version "3.972.36" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.36.tgz" - integrity sha512-DPoGWfy7J7RKxvbf5kOKIGQkD2ek3dbKgzKIGrnLuvZBz5myU+Im/H6pmc14QcnFbqHMqxvtWSgRDSJW3qXLQg== - dependencies: - "@aws-sdk/core" "^3.974.8" - "@aws-sdk/types" "^3.973.8" - "@smithy/fetch-http-handler" "^5.3.17" - "@smithy/node-http-handler" "^4.6.1" - "@smithy/property-provider" "^4.2.14" - "@smithy/protocol-http" "^5.3.14" - "@smithy/smithy-client" "^4.12.13" - "@smithy/types" "^4.14.1" - "@smithy/util-stream" "^4.5.25" - tslib "^2.6.2" - -"@aws-sdk/credential-provider-ini@^3.972.38": - version "3.972.38" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.38.tgz" - integrity sha512-oDzUBu2MGJFgoar05sPMCwSrhw44ASyccrHzj66vO69OZqi7I6hZZxXfuPLC8OCzW7C+sU+bI73XHij41yekgQ== - dependencies: - "@aws-sdk/core" "^3.974.8" - "@aws-sdk/credential-provider-env" "^3.972.34" - "@aws-sdk/credential-provider-http" "^3.972.36" - "@aws-sdk/credential-provider-login" "^3.972.38" - "@aws-sdk/credential-provider-process" "^3.972.34" - "@aws-sdk/credential-provider-sso" "^3.972.38" - "@aws-sdk/credential-provider-web-identity" "^3.972.38" - "@aws-sdk/nested-clients" "^3.997.6" - "@aws-sdk/types" "^3.973.8" - "@smithy/credential-provider-imds" "^4.2.14" - "@smithy/property-provider" "^4.2.14" - "@smithy/shared-ini-file-loader" "^4.4.9" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@aws-sdk/credential-provider-login@^3.972.38": - version "3.972.38" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.38.tgz" - integrity sha512-g1NosS8qe4OF++G2UFCM5ovSkgipC7YYor5KCWatG0UoMSO5YFj9C8muePlyVmOBV/WTI16Jo3/s1NUo/o1Bww== - dependencies: - "@aws-sdk/core" "^3.974.8" - "@aws-sdk/nested-clients" "^3.997.6" - "@aws-sdk/types" "^3.973.8" - "@smithy/property-provider" "^4.2.14" - "@smithy/protocol-http" "^5.3.14" - "@smithy/shared-ini-file-loader" "^4.4.9" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@aws-sdk/credential-provider-node@^3.972.39": - version "3.972.39" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.39.tgz" - integrity sha512-HEswDQyxUtadoZ/bJsPPENHg7R0Lzym5LuMksJeHvqhCOpP+rtkDLKI4/ZChH4w3cf5kG8n6bZuI8PzajoiqMg== - dependencies: - "@aws-sdk/credential-provider-env" "^3.972.34" - "@aws-sdk/credential-provider-http" "^3.972.36" - "@aws-sdk/credential-provider-ini" "^3.972.38" - "@aws-sdk/credential-provider-process" "^3.972.34" - "@aws-sdk/credential-provider-sso" "^3.972.38" - "@aws-sdk/credential-provider-web-identity" "^3.972.38" - "@aws-sdk/types" "^3.973.8" - "@smithy/credential-provider-imds" "^4.2.14" - "@smithy/property-provider" "^4.2.14" - "@smithy/shared-ini-file-loader" "^4.4.9" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@aws-sdk/credential-provider-process@^3.972.34": - version "3.972.34" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.34.tgz" - integrity sha512-T3IFs4EVmVi1dVN5RciFnklCANSzvrQd/VuHY9ThHSQmYkTogjcGkoJEr+oNUPQZnso52183088NqysMPji1/Q== - dependencies: - "@aws-sdk/core" "^3.974.8" - "@aws-sdk/types" "^3.973.8" - "@smithy/property-provider" "^4.2.14" - "@smithy/shared-ini-file-loader" "^4.4.9" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@aws-sdk/credential-provider-sso@^3.972.38": - version "3.972.38" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.38.tgz" - integrity sha512-5ZxG+t0+3Q3QPh8KEjX6syskhgNf7I0MN7oGioTf6Lm1NTjfP7sIcYGNsthXC2qR8vcD3edNZwCr2ovfSSWuRA== - dependencies: - "@aws-sdk/core" "^3.974.8" - "@aws-sdk/nested-clients" "^3.997.6" - "@aws-sdk/token-providers" "3.1041.0" - "@aws-sdk/types" "^3.973.8" - "@smithy/property-provider" "^4.2.14" - "@smithy/shared-ini-file-loader" "^4.4.9" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@aws-sdk/credential-provider-web-identity@^3.972.38": - version "3.972.38" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.38.tgz" - integrity sha512-lYHFF30DGI20jZcYX8cm6Ns0V7f1dDN6g/MBDLTyD/5iw+bXs3yBr2iAiHDkx4RFU5JgsnZvCHYKiRVPRdmOgw== - dependencies: - "@aws-sdk/core" "^3.974.8" - "@aws-sdk/nested-clients" "^3.997.6" - "@aws-sdk/types" "^3.973.8" - "@smithy/property-provider" "^4.2.14" - "@smithy/shared-ini-file-loader" "^4.4.9" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@aws-sdk/middleware-bucket-endpoint@^3.972.10": - version "3.972.10" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.10.tgz" - integrity sha512-Vbc2frZH7wXlMNd+ZZSXUEs/l1Sv8Jj4zUnIfwrYF5lwaLdXHZ9xx4U3rjUcaye3HRhFVc+E5DbBxpRAbB16BA== - dependencies: - "@aws-sdk/types" "^3.973.8" - "@aws-sdk/util-arn-parser" "^3.972.3" - "@smithy/node-config-provider" "^4.3.14" - "@smithy/protocol-http" "^5.3.14" - "@smithy/types" "^4.14.1" - "@smithy/util-config-provider" "^4.2.2" - tslib "^2.6.2" - -"@aws-sdk/middleware-expect-continue@^3.972.10": - version "3.972.10" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.10.tgz" - integrity sha512-2Yn0f1Qiq/DjxYR3wfI3LokXnjOhFM7Ssn4LTdFDIxRMCE6I32MAsVnhPX1cUZsuVA9tiZtwwhlSLAtFGxAZlQ== - dependencies: - "@aws-sdk/types" "^3.973.8" - "@smithy/protocol-http" "^5.3.14" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@aws-sdk/middleware-flexible-checksums@^3.974.16": - version "3.974.16" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.16.tgz" - integrity sha512-6ru8doI0/XzszqLIPXf0E/V7HhAw1Pu94010XCKYtBUfD0LxF0BuOzrUf8OQGR6j2o6wgKTHUniOmndQycHwCA== - dependencies: - "@aws-crypto/crc32" "5.2.0" - "@aws-crypto/crc32c" "5.2.0" - "@aws-crypto/util" "5.2.0" - "@aws-sdk/core" "^3.974.8" - "@aws-sdk/crc64-nvme" "^3.972.7" - "@aws-sdk/types" "^3.973.8" - "@smithy/is-array-buffer" "^4.2.2" - "@smithy/node-config-provider" "^4.3.14" - "@smithy/protocol-http" "^5.3.14" - "@smithy/types" "^4.14.1" - "@smithy/util-middleware" "^4.2.14" - "@smithy/util-stream" "^4.5.25" - "@smithy/util-utf8" "^4.2.2" - tslib "^2.6.2" - -"@aws-sdk/middleware-host-header@^3.972.10": - version "3.972.10" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.10.tgz" - integrity sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg== - dependencies: - "@aws-sdk/types" "^3.973.8" - "@smithy/protocol-http" "^5.3.14" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@aws-sdk/middleware-location-constraint@^3.972.10": - version "3.972.10" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.10.tgz" - integrity sha512-rI3NZvJcEvjoD0+0PI0iUAwlPw2IlSlhyvgBK/3WkKJQE/YiKFedd9dMN2lVacdNxPNhxL/jzQaKQdrGtQagjQ== - dependencies: - "@aws-sdk/types" "^3.973.8" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@aws-sdk/middleware-logger@^3.972.10": - version "3.972.10" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz" - integrity sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ== - dependencies: - "@aws-sdk/types" "^3.973.8" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@aws-sdk/middleware-recursion-detection@^3.972.11": - version "3.972.11" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.11.tgz" - integrity sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ== - dependencies: - "@aws-sdk/types" "^3.973.8" - "@aws/lambda-invoke-store" "^0.2.2" - "@smithy/protocol-http" "^5.3.14" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@aws-sdk/middleware-sdk-s3@^3.972.37": - version "3.972.37" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.37.tgz" - integrity sha512-Km7M+i8DrLArVzrid1gfxeGhYHBd3uxvE77g0s5a52zPSVosxzQBnJ0gwWb6NIp/DOk8gsBMhi7V+cpJG0ndTA== - dependencies: - "@aws-sdk/core" "^3.974.8" - "@aws-sdk/types" "^3.973.8" - "@aws-sdk/util-arn-parser" "^3.972.3" - "@smithy/core" "^3.23.17" - "@smithy/node-config-provider" "^4.3.14" - "@smithy/protocol-http" "^5.3.14" - "@smithy/signature-v4" "^5.3.14" - "@smithy/smithy-client" "^4.12.13" - "@smithy/types" "^4.14.1" - "@smithy/util-config-provider" "^4.2.2" - "@smithy/util-middleware" "^4.2.14" - "@smithy/util-stream" "^4.5.25" - "@smithy/util-utf8" "^4.2.2" - tslib "^2.6.2" - -"@aws-sdk/middleware-ssec@^3.972.10": - version "3.972.10" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.10.tgz" - integrity sha512-Gli9A0u8EVVb+5bFDGS/QbSVg28w/wpEidg1ggVcSj65BDTdGR6punsOcVjqdiu1i42WHWo51MCvARPIIz9juw== - dependencies: - "@aws-sdk/types" "^3.973.8" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@aws-sdk/middleware-user-agent@^3.972.38": - version "3.972.38" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.38.tgz" - integrity sha512-iz+B29TXcAZsJpwB+AwG/TTGA5l/VnmMZ2UxtiySOZjI6gCdmviXPwdgzcmuazMy16rXoPY4mYCGe7zdNKfx5A== - dependencies: - "@aws-sdk/core" "^3.974.8" - "@aws-sdk/types" "^3.973.8" - "@aws-sdk/util-endpoints" "^3.996.8" - "@smithy/core" "^3.23.17" - "@smithy/protocol-http" "^5.3.14" - "@smithy/types" "^4.14.1" - "@smithy/util-retry" "^4.3.6" - tslib "^2.6.2" - -"@aws-sdk/nested-clients@^3.997.6": - version "3.997.6" - resolved "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.6.tgz" - integrity sha512-WBDnqatJl+kGObpfmfSxqnXeYTu3Me8wx8WCtvoxX3pfWrrTv8I4WTMSSs7PZqcRcVh8WeUKMgGFjMG+52SR1w== - dependencies: - "@aws-crypto/sha256-browser" "5.2.0" - "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "^3.974.8" - "@aws-sdk/middleware-host-header" "^3.972.10" - "@aws-sdk/middleware-logger" "^3.972.10" - "@aws-sdk/middleware-recursion-detection" "^3.972.11" - "@aws-sdk/middleware-user-agent" "^3.972.38" - "@aws-sdk/region-config-resolver" "^3.972.13" - "@aws-sdk/signature-v4-multi-region" "^3.996.25" - "@aws-sdk/types" "^3.973.8" - "@aws-sdk/util-endpoints" "^3.996.8" - "@aws-sdk/util-user-agent-browser" "^3.972.10" - "@aws-sdk/util-user-agent-node" "^3.973.24" - "@smithy/config-resolver" "^4.4.17" - "@smithy/core" "^3.23.17" - "@smithy/fetch-http-handler" "^5.3.17" - "@smithy/hash-node" "^4.2.14" - "@smithy/invalid-dependency" "^4.2.14" - "@smithy/middleware-content-length" "^4.2.14" - "@smithy/middleware-endpoint" "^4.4.32" - "@smithy/middleware-retry" "^4.5.7" - "@smithy/middleware-serde" "^4.2.20" - "@smithy/middleware-stack" "^4.2.14" - "@smithy/node-config-provider" "^4.3.14" - "@smithy/node-http-handler" "^4.6.1" - "@smithy/protocol-http" "^5.3.14" - "@smithy/smithy-client" "^4.12.13" - "@smithy/types" "^4.14.1" - "@smithy/url-parser" "^4.2.14" - "@smithy/util-base64" "^4.3.2" - "@smithy/util-body-length-browser" "^4.2.2" - "@smithy/util-body-length-node" "^4.2.3" - "@smithy/util-defaults-mode-browser" "^4.3.49" - "@smithy/util-defaults-mode-node" "^4.2.54" - "@smithy/util-endpoints" "^3.4.2" - "@smithy/util-middleware" "^4.2.14" - "@smithy/util-retry" "^4.3.6" - "@smithy/util-utf8" "^4.2.2" - tslib "^2.6.2" - -"@aws-sdk/region-config-resolver@^3.972.13": - version "3.972.13" - resolved "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.13.tgz" - integrity sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A== - dependencies: - "@aws-sdk/types" "^3.973.8" - "@smithy/config-resolver" "^4.4.17" - "@smithy/node-config-provider" "^4.3.14" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@aws-sdk/s3-request-presigner@^3.1016.0": - version "3.1045.0" - resolved "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1045.0.tgz" - integrity sha512-VDRF8GIuUPX+K4DUYrvcODj/h54LOmdJ7DhpLQ0wrYrdxzIiJEpi0n9jZ1bbjT2UxhwTbOorse5EGo+gnOK2aA== - dependencies: - "@aws-sdk/signature-v4-multi-region" "^3.996.25" - "@aws-sdk/types" "^3.973.8" - "@aws-sdk/util-format-url" "^3.972.10" - "@smithy/middleware-endpoint" "^4.4.32" - "@smithy/protocol-http" "^5.3.14" - "@smithy/smithy-client" "^4.12.13" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@aws-sdk/signature-v4-multi-region@^3.996.25": - version "3.996.25" - resolved "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.25.tgz" - integrity sha512-+CMIt3e1VzlklAECmG+DtP1sV8iKq25FuA0OKpnJ4KA0kxUtd7CgClY7/RU6VzJBQwbN4EJ9Ue6plvqx1qGadw== - dependencies: - "@aws-sdk/middleware-sdk-s3" "^3.972.37" - "@aws-sdk/types" "^3.973.8" - "@smithy/protocol-http" "^5.3.14" - "@smithy/signature-v4" "^5.3.14" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@aws-sdk/token-providers@3.1041.0": - version "3.1041.0" - resolved "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1041.0.tgz" - integrity sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw== - dependencies: - "@aws-sdk/core" "^3.974.8" - "@aws-sdk/nested-clients" "^3.997.6" - "@aws-sdk/types" "^3.973.8" - "@smithy/property-provider" "^4.2.14" - "@smithy/shared-ini-file-loader" "^4.4.9" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@aws-sdk/types@^3.222.0", "@aws-sdk/types@^3.973.8": - version "3.973.8" - resolved "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz" - integrity sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw== - dependencies: - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@aws-sdk/util-arn-parser@^3.972.3": - version "3.972.3" - resolved "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz" - integrity sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA== - dependencies: - tslib "^2.6.2" - -"@aws-sdk/util-endpoints@^3.996.8": - version "3.996.8" - resolved "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.8.tgz" - integrity sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g== - dependencies: - "@aws-sdk/types" "^3.973.8" - "@smithy/types" "^4.14.1" - "@smithy/url-parser" "^4.2.14" - "@smithy/util-endpoints" "^3.4.2" - tslib "^2.6.2" - -"@aws-sdk/util-format-url@^3.972.10": - version "3.972.10" - resolved "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.10.tgz" - integrity sha512-DEKiHNJVtNxdyTeQspzY+15Po/kHm6sF0Cs4HV9Q2+lplB63+DrvdeiSoOSdWEWAoO2RcY1veoXVDz2tWxWCgQ== - dependencies: - "@aws-sdk/types" "^3.973.8" - "@smithy/querystring-builder" "^4.2.14" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@aws-sdk/util-locate-window@^3.0.0": - version "3.965.5" - resolved "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz" - integrity sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ== - dependencies: - tslib "^2.6.2" - -"@aws-sdk/util-user-agent-browser@^3.972.10": - version "3.972.10" - resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.10.tgz" - integrity sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g== - dependencies: - "@aws-sdk/types" "^3.973.8" - "@smithy/types" "^4.14.1" - bowser "^2.11.0" - tslib "^2.6.2" - -"@aws-sdk/util-user-agent-node@^3.973.24": - version "3.973.24" - resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.24.tgz" - integrity sha512-ZWwlkjcIp7cEL8ZfTpTAPNkwx25p7xol0xlKoWVVf22+nsjwmLcHYtTPjIV1cSpmB/b6DaK4cb1fSkvCXHgRdw== - dependencies: - "@aws-sdk/middleware-user-agent" "^3.972.38" - "@aws-sdk/types" "^3.973.8" - "@smithy/node-config-provider" "^4.3.14" - "@smithy/types" "^4.14.1" - "@smithy/util-config-provider" "^4.2.2" - tslib "^2.6.2" - -"@aws-sdk/xml-builder@^3.972.22": - version "3.972.22" - resolved "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.22.tgz" - integrity sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA== - dependencies: - "@nodable/entities" "2.1.0" - "@smithy/types" "^4.14.1" - fast-xml-parser "5.7.2" - tslib "^2.6.2" - -"@aws/lambda-invoke-store@^0.2.2": - version "0.2.4" - resolved "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz" - integrity sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ== - -"@azure/abort-controller@^1.0.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz" - integrity sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw== - dependencies: - tslib "^2.2.0" - -"@azure/abort-controller@^2.0.0": - version "2.1.2" - resolved "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz" - integrity sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA== - dependencies: - tslib "^2.6.2" - -"@azure/core-auth@^1.3.0", "@azure/core-auth@^1.4.0", "@azure/core-auth@^1.5.0", "@azure/core-auth@^1.7.2": - version "1.7.2" - resolved "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.2.tgz" - integrity sha512-Igm/S3fDYmnMq1uKS38Ae1/m37B3zigdlZw+kocwEhh5GjyKjPrXKO2J6rzpC1wAxrNil/jX9BJRqBshyjnF3g== - dependencies: - "@azure/abort-controller" "^2.0.0" - "@azure/core-util" "^1.1.0" - tslib "^2.6.2" - -"@azure/core-client@^1.3.0", "@azure/core-client@^1.5.0", "@azure/core-client@^1.9.2": - version "1.9.2" - resolved "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz" - integrity sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w== - dependencies: - "@azure/abort-controller" "^2.0.0" - "@azure/core-auth" "^1.4.0" - "@azure/core-rest-pipeline" "^1.9.1" - "@azure/core-tracing" "^1.0.0" - "@azure/core-util" "^1.6.1" - "@azure/logger" "^1.0.0" - tslib "^2.6.2" - -"@azure/core-http-compat@^2.0.1": - version "2.1.2" - resolved "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.1.2.tgz" - integrity sha512-5MnV1yqzZwgNLLjlizsU3QqOeQChkIXw781Fwh1xdAqJR5AA32IUaq6xv1BICJvfbHoa+JYcaij2HFkhLbNTJQ== - dependencies: - "@azure/abort-controller" "^2.0.0" - "@azure/core-client" "^1.3.0" - "@azure/core-rest-pipeline" "^1.3.0" - -"@azure/core-lro@^2.2.0": - version "2.7.2" - resolved "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz" - integrity sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw== - dependencies: - "@azure/abort-controller" "^2.0.0" - "@azure/core-util" "^1.2.0" - "@azure/logger" "^1.0.0" - tslib "^2.6.2" - -"@azure/core-paging@^1.1.1": - version "1.6.2" - resolved "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz" - integrity sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA== - dependencies: - tslib "^2.6.2" - -"@azure/core-rest-pipeline@^1.1.0", "@azure/core-rest-pipeline@^1.3.0", "@azure/core-rest-pipeline@^1.8.1", "@azure/core-rest-pipeline@^1.9.1": - version "1.16.2" - resolved "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.16.2.tgz" - integrity sha512-Hnhm/PG9/SQ07JJyLDv3l9Qr8V3xgAe1hFoBYzt6LaalMxfL/ZqFaZf/bz5VN3pMcleCPwl8ivlS2Fjxq/iC8Q== - dependencies: - "@azure/abort-controller" "^2.0.0" - "@azure/core-auth" "^1.4.0" - "@azure/core-tracing" "^1.0.1" - "@azure/core-util" "^1.9.0" - "@azure/logger" "^1.0.0" - http-proxy-agent "^7.0.0" - https-proxy-agent "^7.0.0" - tslib "^2.6.2" - -"@azure/core-tracing@^1.0.0", "@azure/core-tracing@^1.0.1": - version "1.1.2" - resolved "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.1.2.tgz" - integrity sha512-dawW9ifvWAWmUm9/h+/UQ2jrdvjCJ7VJEuCJ6XVNudzcOwm53BFZH4Q845vjfgoUAM8ZxokvVNxNxAITc502YA== - dependencies: - tslib "^2.6.2" - -"@azure/core-util@^1.0.0", "@azure/core-util@^1.1.0", "@azure/core-util@^1.2.0", "@azure/core-util@^1.3.0", "@azure/core-util@^1.6.1", "@azure/core-util@^1.9.0": - version "1.9.1" - resolved "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.1.tgz" - integrity sha512-OLsq0etbHO1MA7j6FouXFghuHrAFGk+5C1imcpQ2e+0oZhYF07WLA+NW2Vqs70R7d+zOAWiWM3tbE1sXcDN66g== - dependencies: - "@azure/abort-controller" "^2.0.0" - tslib "^2.6.2" - -"@azure/identity@^4.2.1": - version "4.4.0" - resolved "https://registry.npmjs.org/@azure/identity/-/identity-4.4.0.tgz" - integrity sha512-oG6oFNMxUuoivYg/ElyZWVSZfw42JQyHbrp+lR7VJ1BYWsGzt34NwyDw3miPp1QI7Qm5+4KAd76wGsbHQmkpkg== - dependencies: - "@azure/abort-controller" "^1.0.0" - "@azure/core-auth" "^1.5.0" - "@azure/core-client" "^1.9.2" - "@azure/core-rest-pipeline" "^1.1.0" - "@azure/core-tracing" "^1.0.0" - "@azure/core-util" "^1.3.0" - "@azure/logger" "^1.0.0" - "@azure/msal-browser" "^3.14.0" - "@azure/msal-node" "^2.9.2" - events "^3.0.0" - jws "^4.0.0" - open "^8.0.0" - stoppable "^1.1.0" - tslib "^2.2.0" - -"@azure/keyvault-keys@^4.4.0": - version "4.8.0" - resolved "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.8.0.tgz" - integrity sha512-jkuYxgkw0aaRfk40OQhFqDIupqblIOIlYESWB6DKCVDxQet1pyv86Tfk9M+5uFM0+mCs6+MUHU+Hxh3joiUn4Q== - dependencies: - "@azure/abort-controller" "^1.0.0" - "@azure/core-auth" "^1.3.0" - "@azure/core-client" "^1.5.0" - "@azure/core-http-compat" "^2.0.1" - "@azure/core-lro" "^2.2.0" - "@azure/core-paging" "^1.1.1" - "@azure/core-rest-pipeline" "^1.8.1" - "@azure/core-tracing" "^1.0.0" - "@azure/core-util" "^1.0.0" - "@azure/logger" "^1.0.0" - tslib "^2.2.0" - -"@azure/logger@^1.0.0": - version "1.1.3" - resolved "https://registry.npmjs.org/@azure/logger/-/logger-1.1.3.tgz" - integrity sha512-J8/cIKNQB1Fc9fuYqBVnrppiUtW+5WWJPCj/tAokC5LdSTwkWWttN+jsRgw9BLYD7JDBx7PceiqOBxJJ1tQz3Q== - dependencies: - tslib "^2.6.2" - -"@azure/msal-browser@^3.14.0": - version "3.19.1" - resolved "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.19.1.tgz" - integrity sha512-pqYP2gK0GCEa4OxtOqlS+EdFQqhXV6ZuESgSTYWq2ABXyxBVVdd5KNuqgR5SU0OwI2V1YWdFVvLDe1487dyQ0g== - dependencies: - "@azure/msal-common" "14.13.1" - -"@azure/msal-common@14.13.1": - version "14.13.1" - resolved "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.13.1.tgz" - integrity sha512-iUp3BYrsRZ4X3EiaZ2fDjNFjmtYMv9rEQd6c1op6ULn0HWk4ACvDmosL6NaBgWOhl1BAblIbd9vmB5/ilF8d4A== - -"@azure/msal-node@^2.9.2": - version "2.11.1" - resolved "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.11.1.tgz" - integrity sha512-8ECtug4RL+zsgh20VL8KYHjrRO3MJOeAKEPRXT2lwtiu5U3BdyIdBb50+QZthEkIi60K6pc/pdOx/k5Jp4sLng== - dependencies: - "@azure/msal-common" "14.13.1" - jsonwebtoken "^9.0.0" - uuid "^8.3.0" - -"@derhuerst/http-basic@^8.2.0": - version "8.2.4" - resolved "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz" - integrity sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw== - dependencies: - caseless "^0.12.0" - concat-stream "^2.0.0" - http-response-object "^3.0.1" - parse-cache-control "^1.0.1" - -"@eslint-community/eslint-utils@^4.2.0": - version "4.9.1" - resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz" - integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== - dependencies: - eslint-visitor-keys "^3.4.3" - -"@eslint-community/regexpp@^4.6.1": - version "4.12.2" - resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz" - integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== - -"@eslint/eslintrc@^2.1.4": - version "2.1.4" - resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz" - integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^9.6.0" - globals "^13.19.0" - ignore "^5.2.0" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.1.2" - strip-json-comments "^3.1.1" - -"@eslint/js@8.57.1": - version "8.57.1" - resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz" - integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== - -"@google-cloud/paginator@^5.0.0": - version "5.0.2" - resolved "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz" - integrity sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg== - dependencies: - arrify "^2.0.0" - extend "^3.0.2" - -"@google-cloud/projectify@^4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz" - integrity sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA== - -"@google-cloud/promisify@<4.1.0": - version "2.0.4" - resolved "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.4.tgz" - integrity sha512-j8yRSSqswWi1QqUGKVEKOG03Q7qOoZP6/h2zN2YO+F5h2+DHU0bSrHCK9Y7lo2DI9fBd8qGAw795sf+3Jva4yA== - -"@google-cloud/storage@^7.0.0": - version "7.19.0" - resolved "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.19.0.tgz" - integrity sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ== - dependencies: - "@google-cloud/paginator" "^5.0.0" - "@google-cloud/projectify" "^4.0.0" - "@google-cloud/promisify" "<4.1.0" - abort-controller "^3.0.0" - async-retry "^1.3.3" - duplexify "^4.1.3" - fast-xml-parser "^5.3.4" - gaxios "^6.0.2" - google-auth-library "^9.6.3" - html-entities "^2.5.2" - mime "^3.0.0" - p-limit "^3.0.1" - retry-request "^7.0.0" - teeny-request "^9.0.0" - uuid "^8.0.0" - -"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": - version "9.3.0" - resolved "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz" - integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== - -"@hapi/topo@^5.1.0": - version "5.1.0" - resolved "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz" - integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== - dependencies: - "@hapi/hoek" "^9.0.0" - -"@humanwhocodes/config-array@^0.13.0": - version "0.13.0" - resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz" - integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== - dependencies: - "@humanwhocodes/object-schema" "^2.0.3" - debug "^4.3.1" - minimatch "^3.0.5" - -"@humanwhocodes/module-importer@^1.0.1": - version "1.0.1" - resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" - integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== - -"@humanwhocodes/object-schema@^2.0.3": - version "2.0.3" - resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz" - integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== - -"@isaacs/cliui@^8.0.2": - version "8.0.2" - resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" - integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== - dependencies: - string-width "^5.1.2" - string-width-cjs "npm:string-width@^4.2.0" - strip-ansi "^7.0.1" - strip-ansi-cjs "npm:strip-ansi@^6.0.1" - wrap-ansi "^8.1.0" - wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" - -"@js-joda/core@^5.6.1": - version "5.6.3" - resolved "https://registry.npmjs.org/@js-joda/core/-/core-5.6.3.tgz" - integrity sha512-T1rRxzdqkEXcou0ZprN1q9yDRlvzCPLqmlNt5IIsGBzoEVgLCCYrKEwc84+TvsXuAc95VAZwtWD2zVsKPY4bcA== - -"@jsdevtools/ono@^7.1.3": - version "7.1.3" - resolved "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz" - integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== - -"@nodable/entities@^2.1.0", "@nodable/entities@2.1.0": - version "2.1.0" - resolved "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz" - integrity sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA== - -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5": - version "2.0.5" - resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.8": - version "1.2.8" - resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@one-ini/wasm@0.1.1": - version "0.1.1" - resolved "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz" - integrity sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw== - -"@pinojs/redact@^0.4.0": - version "0.4.0" - resolved "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz" - integrity sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg== - -"@pkgjs/parseargs@^0.11.0": - version "0.11.0" - resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" - integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== - -"@rtsao/scc@^1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz" - integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== - -"@sideway/address@^4.1.5": - version "4.1.5" - resolved "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz" - integrity sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q== - dependencies: - "@hapi/hoek" "^9.0.0" - -"@sideway/formula@^3.0.1": - version "3.0.1" - resolved "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz" - integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== - -"@sideway/pinpoint@^2.0.0": - version "2.0.0" - resolved "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz" - integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== - -"@smithy/chunked-blob-reader-native@^4.2.3": - version "4.2.3" - resolved "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.3.tgz" - integrity sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw== - dependencies: - "@smithy/util-base64" "^4.3.2" - tslib "^2.6.2" - -"@smithy/chunked-blob-reader@^5.2.2": - version "5.2.2" - resolved "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz" - integrity sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw== - dependencies: - tslib "^2.6.2" - -"@smithy/config-resolver@^4.4.17": - version "4.4.17" - resolved "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.17.tgz" - integrity sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ== - dependencies: - "@smithy/node-config-provider" "^4.3.14" - "@smithy/types" "^4.14.1" - "@smithy/util-config-provider" "^4.2.2" - "@smithy/util-endpoints" "^3.4.2" - "@smithy/util-middleware" "^4.2.14" - tslib "^2.6.2" - -"@smithy/core@^3.23.17": - version "3.23.17" - resolved "https://registry.npmjs.org/@smithy/core/-/core-3.23.17.tgz" - integrity sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ== - dependencies: - "@smithy/protocol-http" "^5.3.14" - "@smithy/types" "^4.14.1" - "@smithy/url-parser" "^4.2.14" - "@smithy/util-base64" "^4.3.2" - "@smithy/util-body-length-browser" "^4.2.2" - "@smithy/util-middleware" "^4.2.14" - "@smithy/util-stream" "^4.5.25" - "@smithy/util-utf8" "^4.2.2" - "@smithy/uuid" "^1.1.2" - tslib "^2.6.2" - -"@smithy/credential-provider-imds@^4.2.14": - version "4.2.14" - resolved "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.14.tgz" - integrity sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg== - dependencies: - "@smithy/node-config-provider" "^4.3.14" - "@smithy/property-provider" "^4.2.14" - "@smithy/types" "^4.14.1" - "@smithy/url-parser" "^4.2.14" - tslib "^2.6.2" - -"@smithy/eventstream-codec@^4.2.14": - version "4.2.14" - resolved "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.14.tgz" - integrity sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw== - dependencies: - "@aws-crypto/crc32" "5.2.0" - "@smithy/types" "^4.14.1" - "@smithy/util-hex-encoding" "^4.2.2" - tslib "^2.6.2" - -"@smithy/eventstream-serde-browser@^4.2.14": - version "4.2.14" - resolved "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.14.tgz" - integrity sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ== - dependencies: - "@smithy/eventstream-serde-universal" "^4.2.14" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/eventstream-serde-config-resolver@^4.3.14": - version "4.3.14" - resolved "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.14.tgz" - integrity sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA== - dependencies: - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/eventstream-serde-node@^4.2.14": - version "4.2.14" - resolved "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.14.tgz" - integrity sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw== - dependencies: - "@smithy/eventstream-serde-universal" "^4.2.14" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/eventstream-serde-universal@^4.2.14": - version "4.2.14" - resolved "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.14.tgz" - integrity sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg== - dependencies: - "@smithy/eventstream-codec" "^4.2.14" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/fetch-http-handler@^5.3.17": - version "5.3.17" - resolved "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.17.tgz" - integrity sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw== - dependencies: - "@smithy/protocol-http" "^5.3.14" - "@smithy/querystring-builder" "^4.2.14" - "@smithy/types" "^4.14.1" - "@smithy/util-base64" "^4.3.2" - tslib "^2.6.2" - -"@smithy/hash-blob-browser@^4.2.15": - version "4.2.15" - resolved "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.15.tgz" - integrity sha512-0PJ4Al3fg2nM4qKrAIxyNcApgqHAXcBkN8FeizOz69z0rb26uZ6lMESYtxegaTlXB5Hj84JfwMPavMrwDMjucA== - dependencies: - "@smithy/chunked-blob-reader" "^5.2.2" - "@smithy/chunked-blob-reader-native" "^4.2.3" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/hash-node@^4.2.14": - version "4.2.14" - resolved "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.14.tgz" - integrity sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g== - dependencies: - "@smithy/types" "^4.14.1" - "@smithy/util-buffer-from" "^4.2.2" - "@smithy/util-utf8" "^4.2.2" - tslib "^2.6.2" - -"@smithy/hash-stream-node@^4.2.14": - version "4.2.14" - resolved "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.14.tgz" - integrity sha512-tw4GANWkZPb6+BdD4Fgucqzey2+r73Z/GRo9zklsCdwrnxxumUV83ZIaBDdudV4Ylazw3EPTiJZhpX42105ruQ== - dependencies: - "@smithy/types" "^4.14.1" - "@smithy/util-utf8" "^4.2.2" - tslib "^2.6.2" - -"@smithy/invalid-dependency@^4.2.14": - version "4.2.14" - resolved "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.14.tgz" - integrity sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw== - dependencies: - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/is-array-buffer@^2.2.0": - version "2.2.0" - resolved "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz" - integrity sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA== - dependencies: - tslib "^2.6.2" - -"@smithy/is-array-buffer@^4.2.2": - version "4.2.2" - resolved "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz" - integrity sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow== - dependencies: - tslib "^2.6.2" - -"@smithy/md5-js@^4.2.14": - version "4.2.14" - resolved "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.14.tgz" - integrity sha512-V2v0vx+h0iUSNG1Alt+GNBMSLGCrl9iVsdd+Ap67HPM9PN479x12V8LkuMoKImNZxn3MXeuyUjls+/7ZACZghA== - dependencies: - "@smithy/types" "^4.14.1" - "@smithy/util-utf8" "^4.2.2" - tslib "^2.6.2" - -"@smithy/middleware-content-length@^4.2.14": - version "4.2.14" - resolved "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.14.tgz" - integrity sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw== - dependencies: - "@smithy/protocol-http" "^5.3.14" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/middleware-endpoint@^4.4.32": - version "4.4.32" - resolved "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.32.tgz" - integrity sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q== - dependencies: - "@smithy/core" "^3.23.17" - "@smithy/middleware-serde" "^4.2.20" - "@smithy/node-config-provider" "^4.3.14" - "@smithy/shared-ini-file-loader" "^4.4.9" - "@smithy/types" "^4.14.1" - "@smithy/url-parser" "^4.2.14" - "@smithy/util-middleware" "^4.2.14" - tslib "^2.6.2" - -"@smithy/middleware-retry@^4.5.7": - version "4.5.7" - resolved "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.7.tgz" - integrity sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg== - dependencies: - "@smithy/core" "^3.23.17" - "@smithy/node-config-provider" "^4.3.14" - "@smithy/protocol-http" "^5.3.14" - "@smithy/service-error-classification" "^4.3.1" - "@smithy/smithy-client" "^4.12.13" - "@smithy/types" "^4.14.1" - "@smithy/util-middleware" "^4.2.14" - "@smithy/util-retry" "^4.3.6" - "@smithy/uuid" "^1.1.2" - tslib "^2.6.2" - -"@smithy/middleware-serde@^4.2.20": - version "4.2.20" - resolved "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.20.tgz" - integrity sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ== - dependencies: - "@smithy/core" "^3.23.17" - "@smithy/protocol-http" "^5.3.14" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/middleware-stack@^4.2.14": - version "4.2.14" - resolved "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.14.tgz" - integrity sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA== - dependencies: - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/node-config-provider@^4.3.14": - version "4.3.14" - resolved "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.14.tgz" - integrity sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg== - dependencies: - "@smithy/property-provider" "^4.2.14" - "@smithy/shared-ini-file-loader" "^4.4.9" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/node-http-handler@^4.6.1": - version "4.6.1" - resolved "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.6.1.tgz" - integrity sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg== - dependencies: - "@smithy/protocol-http" "^5.3.14" - "@smithy/querystring-builder" "^4.2.14" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/property-provider@^4.2.14": - version "4.2.14" - resolved "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.14.tgz" - integrity sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ== - dependencies: - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/protocol-http@^5.3.14": - version "5.3.14" - resolved "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.14.tgz" - integrity sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ== - dependencies: - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/querystring-builder@^4.2.14": - version "4.2.14" - resolved "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.14.tgz" - integrity sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A== - dependencies: - "@smithy/types" "^4.14.1" - "@smithy/util-uri-escape" "^4.2.2" - tslib "^2.6.2" - -"@smithy/querystring-parser@^4.2.14": - version "4.2.14" - resolved "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.14.tgz" - integrity sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw== - dependencies: - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/service-error-classification@^4.3.1": - version "4.3.1" - resolved "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.1.tgz" - integrity sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw== - dependencies: - "@smithy/types" "^4.14.1" - -"@smithy/shared-ini-file-loader@^4.4.9": - version "4.4.9" - resolved "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.9.tgz" - integrity sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ== - dependencies: - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/signature-v4@^5.3.14": - version "5.3.14" - resolved "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.14.tgz" - integrity sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA== - dependencies: - "@smithy/is-array-buffer" "^4.2.2" - "@smithy/protocol-http" "^5.3.14" - "@smithy/types" "^4.14.1" - "@smithy/util-hex-encoding" "^4.2.2" - "@smithy/util-middleware" "^4.2.14" - "@smithy/util-uri-escape" "^4.2.2" - "@smithy/util-utf8" "^4.2.2" - tslib "^2.6.2" - -"@smithy/smithy-client@^4.12.13": - version "4.12.13" - resolved "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.13.tgz" - integrity sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA== - dependencies: - "@smithy/core" "^3.23.17" - "@smithy/middleware-endpoint" "^4.4.32" - "@smithy/middleware-stack" "^4.2.14" - "@smithy/protocol-http" "^5.3.14" - "@smithy/types" "^4.14.1" - "@smithy/util-stream" "^4.5.25" - tslib "^2.6.2" - -"@smithy/types@^4.14.1": - version "4.14.1" - resolved "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz" - integrity sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg== - dependencies: - tslib "^2.6.2" - -"@smithy/url-parser@^4.2.14": - version "4.2.14" - resolved "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.14.tgz" - integrity sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ== - dependencies: - "@smithy/querystring-parser" "^4.2.14" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/util-base64@^4.3.2": - version "4.3.2" - resolved "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz" - integrity sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ== - dependencies: - "@smithy/util-buffer-from" "^4.2.2" - "@smithy/util-utf8" "^4.2.2" - tslib "^2.6.2" - -"@smithy/util-body-length-browser@^4.2.2": - version "4.2.2" - resolved "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz" - integrity sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ== - dependencies: - tslib "^2.6.2" - -"@smithy/util-body-length-node@^4.2.3": - version "4.2.3" - resolved "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz" - integrity sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g== - dependencies: - tslib "^2.6.2" - -"@smithy/util-buffer-from@^2.2.0": - version "2.2.0" - resolved "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz" - integrity sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA== - dependencies: - "@smithy/is-array-buffer" "^2.2.0" - tslib "^2.6.2" - -"@smithy/util-buffer-from@^4.2.2": - version "4.2.2" - resolved "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz" - integrity sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q== - dependencies: - "@smithy/is-array-buffer" "^4.2.2" - tslib "^2.6.2" - -"@smithy/util-config-provider@^4.2.2": - version "4.2.2" - resolved "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz" - integrity sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ== - dependencies: - tslib "^2.6.2" - -"@smithy/util-defaults-mode-browser@^4.3.49": - version "4.3.49" - resolved "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.49.tgz" - integrity sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw== - dependencies: - "@smithy/property-provider" "^4.2.14" - "@smithy/smithy-client" "^4.12.13" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/util-defaults-mode-node@^4.2.54": - version "4.2.54" - resolved "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.54.tgz" - integrity sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw== - dependencies: - "@smithy/config-resolver" "^4.4.17" - "@smithy/credential-provider-imds" "^4.2.14" - "@smithy/node-config-provider" "^4.3.14" - "@smithy/property-provider" "^4.2.14" - "@smithy/smithy-client" "^4.12.13" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/util-endpoints@^3.4.2": - version "3.4.2" - resolved "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.2.tgz" - integrity sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg== - dependencies: - "@smithy/node-config-provider" "^4.3.14" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/util-hex-encoding@^4.2.2": - version "4.2.2" - resolved "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz" - integrity sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg== - dependencies: - tslib "^2.6.2" - -"@smithy/util-middleware@^4.2.14": - version "4.2.14" - resolved "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.14.tgz" - integrity sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw== - dependencies: - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/util-retry@^4.3.6": - version "4.3.8" - resolved "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.8.tgz" - integrity sha512-LUIxbTBi+OpvXpg91poGA6BdyoleMDLnfXjVDqyi2RvZmTveY5loE/FgYUBCR5LU2BThW2SoZRh8dTIIy38IPw== - dependencies: - "@smithy/service-error-classification" "^4.3.1" - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/util-stream@^4.5.25": - version "4.5.25" - resolved "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.25.tgz" - integrity sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA== - dependencies: - "@smithy/fetch-http-handler" "^5.3.17" - "@smithy/node-http-handler" "^4.6.1" - "@smithy/types" "^4.14.1" - "@smithy/util-base64" "^4.3.2" - "@smithy/util-buffer-from" "^4.2.2" - "@smithy/util-hex-encoding" "^4.2.2" - "@smithy/util-utf8" "^4.2.2" - tslib "^2.6.2" - -"@smithy/util-uri-escape@^4.2.2": - version "4.2.2" - resolved "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz" - integrity sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw== - dependencies: - tslib "^2.6.2" - -"@smithy/util-utf8@^2.0.0": - version "2.3.0" - resolved "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz" - integrity sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A== - dependencies: - "@smithy/util-buffer-from" "^2.2.0" - tslib "^2.6.2" - -"@smithy/util-utf8@^4.2.2": - version "4.2.2" - resolved "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz" - integrity sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw== - dependencies: - "@smithy/util-buffer-from" "^4.2.2" - tslib "^2.6.2" - -"@smithy/util-waiter@^4.3.0": - version "4.3.0" - resolved "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.3.0.tgz" - integrity sha512-JyjYmLAfS+pdxF92o4yLgEoy0zhayKTw73FU1aofLWwLcJw7iSqIY2exGmMTrl/lmZugP5p/zxdFSippJDfKWA== - dependencies: - "@smithy/types" "^4.14.1" - tslib "^2.6.2" - -"@smithy/uuid@^1.1.2": - version "1.1.2" - resolved "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz" - integrity sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g== - dependencies: - tslib "^2.6.2" - -"@tootallnate/once@2": - version "2.0.1" - resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz" - integrity sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ== - -"@types/caseless@*": - version "0.12.5" - resolved "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz" - integrity sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg== - -"@types/debug@^4.1.8": - version "4.1.12" - resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz" - integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== - dependencies: - "@types/ms" "*" - -"@types/json-schema@^7.0.6": - version "7.0.15" - resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" - integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== - -"@types/json5@^0.0.29": - version "0.0.29" - resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" - integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== - -"@types/ms@*": - version "0.7.34" - resolved "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz" - integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== - -"@types/node@*", "@types/node@>=18": - version "20.14.11" - resolved "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz" - integrity sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA== - dependencies: - undici-types "~5.26.4" - -"@types/node@^10.0.3": - version "10.17.60" - resolved "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz" - integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== - -"@types/readable-stream@^4.0.0": - version "4.0.15" - resolved "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.15.tgz" - integrity sha512-oAZ3kw+kJFkEqyh7xORZOku1YAKvsFTogRY8kVl4vHpEKiDkfnSA/My8haRE7fvmix5Zyy+1pwzOi7yycGLBJw== - dependencies: - "@types/node" "*" - safe-buffer "~5.1.1" - -"@types/request@^2.48.8": - version "2.48.13" - resolved "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz" - integrity sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg== - dependencies: - "@types/caseless" "*" - "@types/node" "*" - "@types/tough-cookie" "*" - form-data "^2.5.5" - -"@types/tough-cookie@*": - version "4.0.5" - resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz" - integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== - -"@types/validator@^13.7.17": - version "13.12.0" - resolved "https://registry.npmjs.org/@types/validator/-/validator-13.12.0.tgz" - integrity sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag== - -"@ungap/structured-clone@^1.2.0": - version "1.3.0" - resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz" - integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== - -abbrev@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz" - integrity sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ== - -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - -accepts@^1.3.7, accepts@~1.3.8: - version "1.3.8" - resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" - integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== - dependencies: - mime-types "~2.1.34" - negotiator "0.6.3" - -acorn-jsx@^5.3.2: - version "5.3.2" - resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.9.0: - version "8.15.0" - -agent-base@^7.1.0, agent-base@^7.1.2: - version "7.1.4" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz" - integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== - -agent-base@6: - version "6.0.2" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - -ajv@^6.12.4: - version "6.12.6" - 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" - -ansi-colors@^4.1.3: - version "4.1.3" - resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz" - integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-regex@^6.2.2: - version "6.2.2" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz" - integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg== - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -ansi-styles@^6.1.0: - version "6.2.3" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz" - integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== - -anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -append-field@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz" - integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -array-buffer-byte-length@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz" - integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== - dependencies: - call-bind "^1.0.5" - is-array-buffer "^3.0.4" - -array-buffer-byte-length@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz" - integrity sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw== - dependencies: - call-bound "^1.0.3" - is-array-buffer "^3.0.5" - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" - integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== - -array-includes@^3.1.9: - version "3.1.9" - resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz" - integrity sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.4" - define-properties "^1.2.1" - es-abstract "^1.24.0" - es-object-atoms "^1.1.1" - get-intrinsic "^1.3.0" - is-string "^1.1.1" - math-intrinsics "^1.1.0" - -array.prototype.findlastindex@^1.2.6: - version "1.2.6" - resolved "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz" - integrity sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.4" - define-properties "^1.2.1" - es-abstract "^1.23.9" - es-errors "^1.3.0" - es-object-atoms "^1.1.1" - es-shim-unscopables "^1.1.0" - -array.prototype.flat@^1.3.3: - version "1.3.3" - resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz" - integrity sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg== - dependencies: - call-bind "^1.0.8" - define-properties "^1.2.1" - es-abstract "^1.23.5" - es-shim-unscopables "^1.0.2" - -array.prototype.flatmap@^1.3.3: - version "1.3.3" - resolved "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz" - integrity sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg== - dependencies: - call-bind "^1.0.8" - define-properties "^1.2.1" - es-abstract "^1.23.5" - es-shim-unscopables "^1.0.2" - -arraybuffer.prototype.slice@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz" - integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== - dependencies: - array-buffer-byte-length "^1.0.1" - call-bind "^1.0.5" - define-properties "^1.2.1" - es-abstract "^1.22.3" - es-errors "^1.2.1" - get-intrinsic "^1.2.3" - is-array-buffer "^3.0.4" - is-shared-array-buffer "^1.0.2" - -arraybuffer.prototype.slice@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz" - integrity sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ== - dependencies: - array-buffer-byte-length "^1.0.1" - call-bind "^1.0.8" - define-properties "^1.2.1" - es-abstract "^1.23.5" - es-errors "^1.3.0" - get-intrinsic "^1.2.6" - is-array-buffer "^3.0.4" - -arrify@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz" - integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== - -async-function@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz" - integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA== - -async-retry@^1.3.3: - version "1.3.3" - resolved "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz" - integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== - dependencies: - retry "0.13.1" - -async@^0.2.9: - version "0.2.10" - resolved "https://registry.npmjs.org/async/-/async-0.2.10.tgz" - integrity sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ== - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - -atomic-sleep@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz" - integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== - -available-typed-arrays@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz" - integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== - dependencies: - possible-typed-array-names "^1.0.0" - -axios@^1.13.0: - version "1.16.0" - resolved "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz" - integrity sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w== - dependencies: - follow-redirects "^1.16.0" - form-data "^4.0.5" - proxy-from-env "^2.1.0" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -balanced-match@^4.0.2: - version "4.0.4" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz" - integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA== - -base64-js@^1.3.0, base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -base64url@3.x.x: - version "3.0.1" - resolved "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz" - integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== - -bcrypt@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz" - integrity sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg== - dependencies: - node-addon-api "^8.3.0" - node-gyp-build "^4.8.4" - -bignumber.js@^9.0.0: - version "9.3.1" - resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz" - integrity sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ== - -binary-extensions@^2.0.0: - version "2.3.0" - resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz" - integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== - -bl@^6.0.11: - version "6.0.14" - resolved "https://registry.npmjs.org/bl/-/bl-6.0.14.tgz" - integrity sha512-TJfbvGdL7KFGxTsEbsED7avqpFdY56q9IW0/aiytyheJzxST/+Io6cx/4Qx0K2/u0BPRDs65mjaQzYvMZeNocQ== - dependencies: - "@types/readable-stream" "^4.0.0" - buffer "^6.0.3" - inherits "^2.0.4" - readable-stream "^4.2.0" - -bluebird@^3.7.2: - version "3.7.2" - resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - -body-parser@1.20.1: - version "1.20.1" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz" - integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== - dependencies: - bytes "3.1.2" - content-type "~1.0.4" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.11.0" - raw-body "2.5.1" - type-is "~1.6.18" - unpipe "1.0.0" - -bowser@^2.11.0: - version "2.14.1" - resolved "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz" - integrity sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -brace-expansion@^2.0.1: - version "2.1.0" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz" - integrity sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w== - dependencies: - balanced-match "^1.0.0" - -brace-expansion@^2.0.2: - version "2.1.0" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz" - integrity sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w== - dependencies: - balanced-match "^1.0.0" - -brace-expansion@^5.0.5: - version "5.0.6" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz" - integrity sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g== - dependencies: - balanced-match "^4.0.2" - -braces@~3.0.2: - version "3.0.3" - resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - -browser-stdout@^1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz" - integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== - -buffer-equal-constant-time@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz" - integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -buffer@^6.0.3: - version "6.0.3" - resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - -busboy@^1.6.0: - version "1.6.0" - resolved "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz" - integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== - dependencies: - streamsearch "^1.1.0" - -bytes@3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - -call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: - 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== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - -call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz" - integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - set-function-length "^1.2.1" - -call-bind@^1.0.8: - version "1.0.8" - resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz" - integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== - dependencies: - call-bind-apply-helpers "^1.0.0" - es-define-property "^1.0.0" - get-intrinsic "^1.2.4" - set-function-length "^1.2.2" - -call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz" - integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== - dependencies: - call-bind-apply-helpers "^1.0.2" - get-intrinsic "^1.3.0" - -call-me-maybe@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz" - integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ== - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camelcase@^6.0.0: - version "6.3.0" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" - integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== - -caseless@^0.12.0: - version "0.12.0" - resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" - integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== - -chalk@^4.0.0, chalk@^4.1.0: - version "4.1.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chokidar@^3.5.2: - version "3.6.0" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" - integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chokidar@^3.5.3: - version "3.6.0" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" - integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chokidar@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz" - integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== - dependencies: - readdirp "^4.0.1" - -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -colorette@^2.0.7: - version "2.0.20" - resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz" - integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== - -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -commander@^10.0.0: - version "10.0.1" - resolved "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz" - integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== - -commander@^6.1.0: - version "6.2.1" - resolved "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz" - integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== - -commander@6.2.0: - version "6.2.0" - resolved "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz" - integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -concat-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz" - integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.0.2" - typedarray "^0.0.6" - -config-chain@^1.1.13: - version "1.1.13" - resolved "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz" - integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== - dependencies: - ini "^1.3.4" - proto-list "~1.2.1" - -content-disposition@^0.5.3, content-disposition@0.5.4: - version "0.5.4" - resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" - integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== - dependencies: - safe-buffer "5.2.1" - -content-type@~1.0.4: - version "1.0.5" - resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz" - integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" - integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== - -cookie@0.5.0: - version "0.5.0" - resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== - -cors@^2.8.6: - version "2.8.6" - resolved "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz" - integrity sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw== - dependencies: - object-assign "^4" - vary "^1" - -cross-env@^7.0.3: - version "7.0.3" - resolved "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz" - integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== - dependencies: - cross-spawn "^7.0.1" - -cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.6: - version "7.0.6" - resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz" - integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -csv-parser@^3.2.0: - version "3.2.1" - resolved "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.1.tgz" - integrity sha512-v8RPMSglouR9od735SnwSxLBbCJqEPSbgm1R5qfr8yIiMUCEFjox56kRZid0SvgHJEkxeIEu3+a9QS3YRh7CuA== - -data-view-buffer@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz" - integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== - dependencies: - call-bind "^1.0.6" - es-errors "^1.3.0" - is-data-view "^1.0.1" - -data-view-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz" - integrity sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ== - dependencies: - call-bound "^1.0.3" - es-errors "^1.3.0" - is-data-view "^1.0.2" - -data-view-byte-length@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz" - integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== - dependencies: - call-bind "^1.0.7" - es-errors "^1.3.0" - is-data-view "^1.0.1" - -data-view-byte-length@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz" - integrity sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ== - dependencies: - call-bound "^1.0.3" - es-errors "^1.3.0" - is-data-view "^1.0.2" - -data-view-byte-offset@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz" - integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== - dependencies: - call-bind "^1.0.6" - es-errors "^1.3.0" - is-data-view "^1.0.1" - -data-view-byte-offset@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz" - integrity sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ== - dependencies: - call-bound "^1.0.2" - es-errors "^1.3.0" - is-data-view "^1.0.1" - -dateformat@^4.6.3: - version "4.6.3" - resolved "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz" - integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== - -debug@^3.2.7: - version "3.2.7" - resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -debug@^4, debug@^4.3.4, debug@^4.3.5, debug@4: - version "4.3.5" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz" - integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== - dependencies: - ms "2.1.2" - -debug@^4.3.1: - version "4.4.3" - resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" - integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== - dependencies: - ms "^2.1.3" - -debug@^4.3.2: - version "4.4.3" - resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" - integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== - dependencies: - ms "^2.1.3" - -debug@2.6.9: - version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -decamelize@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz" - integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== - -deep-is@^0.1.3: - 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== - -define-data-property@^1.0.1, define-data-property@^1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" - -define-lazy-prop@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" - integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== - -define-properties@^1.2.0, define-properties@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz" - integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== - dependencies: - define-data-property "^1.0.1" - has-property-descriptors "^1.0.0" - object-keys "^1.1.1" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - -denque@^1.4.1: - version "1.5.1" - resolved "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz" - integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== - -depd@^1.1.0: - version "1.1.2" - resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" - integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== - -depd@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -destroy@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" - integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== - -diff@^5.2.0: - version "5.2.2" - resolved "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz" - integrity sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A== - -doctrine@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz" - integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== - dependencies: - esutils "^2.0.2" - -doctrine@^3.0.0, doctrine@3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -dotenv@^16.4.0: - version "16.6.1" - resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz" - integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== - -dottie@^2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz" - integrity sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA== - -dunder-proto@^1.0.0, dunder-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz" - integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== - dependencies: - call-bind-apply-helpers "^1.0.1" - es-errors "^1.3.0" - gopd "^1.2.0" - -duplexify@^4.1.3: - version "4.1.3" - resolved "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz" - integrity sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA== - dependencies: - end-of-stream "^1.4.1" - inherits "^2.0.3" - readable-stream "^3.1.1" - stream-shift "^1.0.2" - -eastasianwidth@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" - integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== - -ecdsa-sig-formatter@^1.0.11, ecdsa-sig-formatter@1.0.11: - version "1.0.11" - resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - -editorconfig@^1.0.4: - version "1.0.7" - resolved "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz" - integrity sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw== - dependencies: - "@one-ini/wasm" "0.1.1" - commander "^10.0.0" - minimatch "^9.0.1" - semver "^7.5.3" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" - integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -emoji-regex@^9.2.2: - version "9.2.2" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" - integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" - integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== - -end-of-stream@^1.1.0, end-of-stream@^1.4.1: - 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== - dependencies: - once "^1.4.0" - -env-paths@^2.2.0: - version "2.2.1" - resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz" - integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== - -es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.2: - version "1.23.3" - resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz" - integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== - dependencies: - array-buffer-byte-length "^1.0.1" - arraybuffer.prototype.slice "^1.0.3" - available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - data-view-buffer "^1.0.1" - data-view-byte-length "^1.0.1" - data-view-byte-offset "^1.0.0" - es-define-property "^1.0.0" - es-errors "^1.3.0" - es-object-atoms "^1.0.0" - es-set-tostringtag "^2.0.3" - es-to-primitive "^1.2.1" - function.prototype.name "^1.1.6" - get-intrinsic "^1.2.4" - get-symbol-description "^1.0.2" - globalthis "^1.0.3" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - has-proto "^1.0.3" - has-symbols "^1.0.3" - hasown "^2.0.2" - internal-slot "^1.0.7" - is-array-buffer "^3.0.4" - is-callable "^1.2.7" - is-data-view "^1.0.1" - is-negative-zero "^2.0.3" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.3" - is-string "^1.0.7" - is-typed-array "^1.1.13" - is-weakref "^1.0.2" - object-inspect "^1.13.1" - object-keys "^1.1.1" - object.assign "^4.1.5" - regexp.prototype.flags "^1.5.2" - safe-array-concat "^1.1.2" - safe-regex-test "^1.0.3" - string.prototype.trim "^1.2.9" - string.prototype.trimend "^1.0.8" - string.prototype.trimstart "^1.0.8" - typed-array-buffer "^1.0.2" - typed-array-byte-length "^1.0.1" - typed-array-byte-offset "^1.0.2" - typed-array-length "^1.0.6" - unbox-primitive "^1.0.2" - which-typed-array "^1.1.15" - -es-abstract@^1.23.5, es-abstract@^1.23.9, es-abstract@^1.24.0: - version "1.24.1" - resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz" - integrity sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw== - dependencies: - array-buffer-byte-length "^1.0.2" - arraybuffer.prototype.slice "^1.0.4" - available-typed-arrays "^1.0.7" - call-bind "^1.0.8" - call-bound "^1.0.4" - data-view-buffer "^1.0.2" - data-view-byte-length "^1.0.2" - data-view-byte-offset "^1.0.1" - es-define-property "^1.0.1" - es-errors "^1.3.0" - es-object-atoms "^1.1.1" - es-set-tostringtag "^2.1.0" - es-to-primitive "^1.3.0" - function.prototype.name "^1.1.8" - get-intrinsic "^1.3.0" - get-proto "^1.0.1" - get-symbol-description "^1.1.0" - globalthis "^1.0.4" - gopd "^1.2.0" - has-property-descriptors "^1.0.2" - has-proto "^1.2.0" - has-symbols "^1.1.0" - hasown "^2.0.2" - internal-slot "^1.1.0" - is-array-buffer "^3.0.5" - is-callable "^1.2.7" - is-data-view "^1.0.2" - is-negative-zero "^2.0.3" - is-regex "^1.2.1" - is-set "^2.0.3" - is-shared-array-buffer "^1.0.4" - is-string "^1.1.1" - is-typed-array "^1.1.15" - is-weakref "^1.1.1" - math-intrinsics "^1.1.0" - object-inspect "^1.13.4" - object-keys "^1.1.1" - object.assign "^4.1.7" - own-keys "^1.0.1" - regexp.prototype.flags "^1.5.4" - safe-array-concat "^1.1.3" - safe-push-apply "^1.0.0" - safe-regex-test "^1.1.0" - set-proto "^1.0.0" - stop-iteration-iterator "^1.1.0" - string.prototype.trim "^1.2.10" - string.prototype.trimend "^1.0.9" - string.prototype.trimstart "^1.0.8" - typed-array-buffer "^1.0.3" - typed-array-byte-length "^1.0.3" - typed-array-byte-offset "^1.0.4" - typed-array-length "^1.0.7" - unbox-primitive "^1.1.0" - which-typed-array "^1.1.19" - -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" - -es-define-property@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz" - integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== - -es-errors@^1.2.1, es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -es-object-atoms@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz" - integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== - dependencies: - es-errors "^1.3.0" - -es-object-atoms@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz" - integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== - dependencies: - es-errors "^1.3.0" - -es-set-tostringtag@^2.0.3, es-set-tostringtag@^2.1.0: - 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== - dependencies: - es-errors "^1.3.0" - get-intrinsic "^1.2.6" - has-tostringtag "^1.0.2" - hasown "^2.0.2" - -es-shim-unscopables@^1.0.2, es-shim-unscopables@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz" - integrity sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw== - dependencies: - hasown "^2.0.2" - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -es-to-primitive@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz" - integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g== - dependencies: - is-callable "^1.2.7" - is-date-object "^1.0.5" - is-symbol "^1.0.4" - -escalade@^3.1.1: - version "3.1.2" - resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz" - integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" - integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -eslint-import-resolver-node@^0.3.9: - version "0.3.9" - resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz" - integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== - dependencies: - debug "^3.2.7" - is-core-module "^2.13.0" - resolve "^1.22.4" - -eslint-module-utils@^2.12.1: - version "2.12.1" - resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz" - integrity sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw== - dependencies: - debug "^3.2.7" - -eslint-plugin-import@^2.29.1: - version "2.32.0" - resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz" - integrity sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA== - dependencies: - "@rtsao/scc" "^1.1.0" - array-includes "^3.1.9" - array.prototype.findlastindex "^1.2.6" - array.prototype.flat "^1.3.3" - array.prototype.flatmap "^1.3.3" - debug "^3.2.7" - doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.9" - eslint-module-utils "^2.12.1" - hasown "^2.0.2" - is-core-module "^2.16.1" - is-glob "^4.0.3" - minimatch "^3.1.2" - object.fromentries "^2.0.8" - object.groupby "^1.0.3" - object.values "^1.2.1" - semver "^6.3.1" - string.prototype.trimend "^1.0.9" - tsconfig-paths "^3.15.0" - -eslint-scope@^7.2.2: - version "7.2.2" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz" - integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - -eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: - version "3.4.3" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" - integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== - -"eslint@^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", eslint@^8.57.0: - version "8.57.1" - resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz" - integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.6.1" - "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.57.1" - "@humanwhocodes/config-array" "^0.13.0" - "@humanwhocodes/module-importer" "^1.0.1" - "@nodelib/fs.walk" "^1.2.8" - "@ungap/structured-clone" "^1.2.0" - ajv "^6.12.4" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - escape-string-regexp "^4.0.0" - eslint-scope "^7.2.2" - eslint-visitor-keys "^3.4.3" - espree "^9.6.1" - esquery "^1.4.2" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - find-up "^5.0.0" - glob-parent "^6.0.2" - globals "^13.19.0" - graphemer "^1.4.0" - ignore "^5.2.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" - natural-compare "^1.4.0" - optionator "^0.9.3" - strip-ansi "^6.0.1" - text-table "^0.2.0" - -espree@^9.6.0, espree@^9.6.1: - version "9.6.1" - resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz" - integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== - dependencies: - acorn "^8.9.0" - acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.4.1" - -esquery@^1.4.2: - version "1.7.0" - resolved "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz" - integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^5.1.0, estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" - integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== - -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - -events@^3.0.0, events@^3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - -express-validator@^7.0.0: - version "7.3.2" - resolved "https://registry.npmjs.org/express-validator/-/express-validator-7.3.2.tgz" - integrity sha512-ctLw1Vl6dXVH62dIQMDdTAQkrh480mkFuG6/SGXOaVlwPNukhRAe7EgJIMJ2TSAni8iwHBRp530zAZE5ZPF2IA== - dependencies: - lodash "^4.18.1" - validator "~13.15.23" - -"express@>=4.0.0 || >=5.0.0-beta", express@4.18.2: - version "4.18.2" - resolved "https://registry.npmjs.org/express/-/express-4.18.2.tgz" - integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.1" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.5.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "2.0.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.2.0" - fresh "0.5.2" - http-errors "2.0.0" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "2.4.1" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.7" - qs "6.11.0" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" - setprototypeof "1.2.0" - statuses "2.0.1" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -extend@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -fast-copy@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz" - integrity sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ== - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-json-stable-stringify@^2.0.0: - 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== - -fast-levenshtein@^2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" - integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== - -fast-safe-stringify@^2.1.1: - 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== - -fast-xml-builder@^1.1.5: - 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== - dependencies: - path-expression-matcher "^1.5.0" - xml-naming "^0.1.0" - -fast-xml-parser@^5.3.4, fast-xml-parser@5.7.2: - version "5.7.2" - resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz" - integrity sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w== - dependencies: - "@nodable/entities" "^2.1.0" - fast-xml-builder "^1.1.5" - path-expression-matcher "^1.5.0" - strnum "^2.2.3" - -fastq@^1.6.0: - version "1.20.1" - resolved "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz" - integrity sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw== - dependencies: - reusify "^1.0.4" - -ffmpeg-static@^5.2.0: - version "5.3.0" - resolved "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.3.0.tgz" - integrity sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg== - dependencies: - "@derhuerst/http-basic" "^8.2.0" - env-paths "^2.2.0" - https-proxy-agent "^5.0.0" - progress "^2.0.3" - -ffprobe-static@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/ffprobe-static/-/ffprobe-static-3.1.0.tgz" - integrity sha512-Dvpa9uhVMOYivhHKWLGDoa512J751qN1WZAIO+Xw4L/mrUSPxS4DApzSUDbCFE/LUq2+xYnznEahTd63AqBSpA== - -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== - dependencies: - flat-cache "^3.0.4" - -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - -finalhandler@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz" - integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "2.4.1" - parseurl "~1.3.3" - statuses "2.0.1" - unpipe "~1.0.0" - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -flat-cache@^3.0.4: - version "3.2.0" - resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz" - integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== - dependencies: - flatted "^3.2.9" - keyv "^4.5.3" - rimraf "^3.0.2" - -flat@^5.0.2: - version "5.0.2" - resolved "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz" - integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== - -flatted@^3.2.9: - version "3.3.3" - -fluent-ffmpeg@^2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz" - integrity sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q== - dependencies: - async "^0.2.9" - which "^1.1.1" - -follow-redirects@^1.16.0: - 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== - -for-each@^0.3.3: - version "0.3.3" - resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== - dependencies: - is-callable "^1.1.3" - -for-each@^0.3.5: - version "0.3.5" - resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz" - integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== - dependencies: - is-callable "^1.2.7" - -foreground-child@^3.1.0: - version "3.3.1" - resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz" - integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== - dependencies: - cross-spawn "^7.0.6" - signal-exit "^4.0.1" - -form-data@^2.5.5: - version "2.5.5" - resolved "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz" - integrity sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - es-set-tostringtag "^2.1.0" - hasown "^2.0.2" - mime-types "^2.1.35" - safe-buffer "^5.2.1" - -form-data@^4.0.5: - version "4.0.5" - resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz" - integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== - 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" - -formidable@1.2.2: - version "1.2.2" - resolved "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz" - integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== - -forwarded@0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" - integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== - -fresh@^0.5.2, fresh@0.5.2: - version "0.5.2" - resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" - integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== - -fs-extra@^9.1.0: - version "9.1.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -function.prototype.name@^1.1.6: - version "1.1.6" - resolved "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz" - integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - functions-have-names "^1.2.3" - -function.prototype.name@^1.1.8: - version "1.1.8" - resolved "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz" - integrity sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.3" - define-properties "^1.2.1" - functions-have-names "^1.2.3" - hasown "^2.0.2" - is-callable "^1.2.7" - -functions-have-names@^1.2.3: - version "1.2.3" - resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz" - integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== - -gaxios@^6.0.0, gaxios@^6.0.2, gaxios@^6.1.1: - version "6.7.1" - resolved "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz" - integrity sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ== - dependencies: - extend "^3.0.2" - https-proxy-agent "^7.0.1" - is-stream "^2.0.0" - node-fetch "^2.6.9" - uuid "^9.0.1" - -gcp-metadata@^6.1.0: - version "6.1.1" - resolved "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz" - integrity sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A== - dependencies: - gaxios "^6.1.1" - google-logging-utils "^0.0.2" - json-bigint "^1.0.0" - -generate-function@^2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz" - integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== - dependencies: - is-property "^1.0.2" - -generator-function@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz" - integrity sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g== - -get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" - -get-intrinsic@^1.2.1, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" - -get-intrinsic@^1.2.3: - version "1.2.4" - resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" - -get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz" - integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== - 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" - -get-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz" - integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== - dependencies: - dunder-proto "^1.0.1" - es-object-atoms "^1.0.0" - -get-symbol-description@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz" - integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== - dependencies: - call-bind "^1.0.5" - es-errors "^1.3.0" - get-intrinsic "^1.2.4" - -get-symbol-description@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz" - integrity sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg== - dependencies: - call-bound "^1.0.3" - es-errors "^1.3.0" - get-intrinsic "^1.2.6" - -glob-parent@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob@^10.4.2: - version "10.5.0" - resolved "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz" - integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg== - dependencies: - foreground-child "^3.1.0" - jackspeak "^3.1.2" - minimatch "^9.0.4" - minipass "^7.1.2" - package-json-from-dist "^1.0.0" - path-scurry "^1.11.1" - -glob@^7.1.3: - version "7.2.3" - resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^8.1.0: - version "8.1.0" - resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - -glob@7.1.6: - version "7.1.6" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globals@^13.19.0: - version "13.24.0" - resolved "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz" - integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== - dependencies: - type-fest "^0.20.2" - -globalthis@^1.0.3, globalthis@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz" - integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== - dependencies: - define-properties "^1.2.1" - gopd "^1.0.1" - -google-auth-library@^9.6.3: - version "9.15.1" - resolved "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz" - integrity sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng== - dependencies: - base64-js "^1.3.0" - ecdsa-sig-formatter "^1.0.11" - gaxios "^6.1.1" - gcp-metadata "^6.1.0" - gtoken "^7.0.0" - jws "^4.0.0" - -google-logging-utils@^0.0.2: - version "0.0.2" - resolved "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz" - integrity sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ== - -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" - -gopd@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" - integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== - -graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.11" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - -graphemer@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz" - integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== - -gtoken@^7.0.0: - version "7.1.0" - resolved "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz" - integrity sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw== - dependencies: - gaxios "^6.0.0" - jws "^4.0.0" - -has-bigints@^1.0.1, has-bigints@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz" - integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" - -has-proto@^1.0.1, has-proto@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz" - integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== - -has-proto@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz" - integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ== - dependencies: - dunder-proto "^1.0.0" - -has-symbols@^1.0.2, has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz" - integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== - -has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz" - integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== - dependencies: - has-symbols "^1.0.3" - -hasown@^2.0.0, hasown@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - -he@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -helmet@^8.0.0: - version "8.1.0" - resolved "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz" - integrity sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg== - -help-me@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz" - integrity sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg== - -html-entities@^2.5.2: - 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== - -http-errors@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" - integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== - dependencies: - depd "2.0.0" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses "2.0.1" - toidentifier "1.0.1" - -http-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz" - integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== - dependencies: - "@tootallnate/once" "2" - agent-base "6" - debug "4" - -http-proxy-agent@^7.0.0: - version "7.0.2" - resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz" - integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== - dependencies: - agent-base "^7.1.0" - debug "^4.3.4" - -http-response-object@^3.0.1: - version "3.0.2" - resolved "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz" - integrity sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA== - dependencies: - "@types/node" "^10.0.3" - -https-proxy-agent@^5.0.0: - 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== - dependencies: - agent-base "6" - debug "4" - -https-proxy-agent@^7.0.0, https-proxy-agent@^7.0.1: - version "7.0.6" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz" - integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== - dependencies: - agent-base "^7.1.2" - debug "4" - -iconv-lite@^0.6.2: - version "0.6.3" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -iconv-lite@^0.6.3: - version "0.6.3" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -ieee754@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -ignore-by-default@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz" - integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== - -ignore@^5.2.0: - version "5.3.2" - resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" - integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== - -import-fresh@^3.2.1: - version "3.3.1" - resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz" - integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -inflection@^1.13.4: - version "1.13.4" - resolved "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz" - integrity sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@^2.0.3, inherits@^2.0.4, inherits@2, inherits@2.0.4: - version "2.0.4" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ini@^1.3.4: - version "1.3.8" - resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -internal-slot@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz" - integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== - dependencies: - es-errors "^1.3.0" - hasown "^2.0.0" - side-channel "^1.0.4" - -internal-slot@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz" - integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== - dependencies: - es-errors "^1.3.0" - hasown "^2.0.2" - side-channel "^1.1.0" - -ipaddr.js@1.9.1: - 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== - -is-array-buffer@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz" - integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.2.1" - -is-array-buffer@^3.0.5: - version "3.0.5" - resolved "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz" - integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.3" - get-intrinsic "^1.2.6" - -is-async-function@^2.0.0: - version "2.1.1" - resolved "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz" - integrity sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ== - dependencies: - async-function "^1.0.0" - call-bound "^1.0.3" - get-proto "^1.0.1" - has-tostringtag "^1.0.2" - safe-regex-test "^1.1.0" - -is-bigint@^1.0.1: - version "1.0.4" - resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz" - integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== - dependencies: - has-bigints "^1.0.1" - -is-bigint@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz" - integrity sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ== - dependencies: - has-bigints "^1.0.2" - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-boolean-object@^1.2.1: - version "1.2.2" - resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz" - integrity sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A== - dependencies: - call-bound "^1.0.3" - has-tostringtag "^1.0.2" - -is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: - version "1.2.7" - resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== - -is-core-module@^2.13.0: - version "2.15.0" - resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz" - integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA== - dependencies: - hasown "^2.0.2" - -is-core-module@^2.16.1: - version "2.16.1" - resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz" - integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== - dependencies: - hasown "^2.0.2" - -is-data-view@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz" - integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== - dependencies: - is-typed-array "^1.1.13" - -is-data-view@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz" - integrity sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw== - dependencies: - call-bound "^1.0.2" - get-intrinsic "^1.2.6" - is-typed-array "^1.1.13" - -is-date-object@^1.0.1: - version "1.0.5" - resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== - dependencies: - has-tostringtag "^1.0.0" - -is-date-object@^1.0.5, is-date-object@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz" - integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== - dependencies: - call-bound "^1.0.2" - has-tostringtag "^1.0.2" - -is-docker@^2.0.0, is-docker@^2.1.1: - version "2.2.1" - resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" - integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-finalizationregistry@^1.1.0: - version "1.1.1" - resolved "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz" - integrity sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg== - dependencies: - call-bound "^1.0.3" - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-generator-function@^1.0.10: - version "1.1.2" - resolved "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz" - integrity sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA== - dependencies: - call-bound "^1.0.4" - generator-function "^2.0.0" - get-proto "^1.0.1" - has-tostringtag "^1.0.2" - safe-regex-test "^1.1.0" - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-map@^2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz" - integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== - -is-negative-zero@^2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz" - integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== - -is-number-object@^1.0.4: - version "1.0.7" - resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz" - integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== - dependencies: - has-tostringtag "^1.0.0" - -is-number-object@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz" - integrity sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw== - dependencies: - call-bound "^1.0.3" - has-tostringtag "^1.0.2" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-path-inside@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - -is-plain-obj@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz" - integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== - -is-property@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz" - integrity sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g== - -is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-regex@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz" - integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== - dependencies: - call-bound "^1.0.2" - gopd "^1.2.0" - has-tostringtag "^1.0.2" - hasown "^2.0.2" - -is-set@^2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz" - integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== - -is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz" - integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== - dependencies: - call-bind "^1.0.7" - -is-shared-array-buffer@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz" - integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A== - dependencies: - call-bound "^1.0.3" - -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -is-string@^1.0.5, is-string@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== - dependencies: - has-tostringtag "^1.0.0" - -is-string@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz" - integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA== - dependencies: - call-bound "^1.0.3" - has-tostringtag "^1.0.2" - -is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.4" - resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== - dependencies: - has-symbols "^1.0.2" - -is-symbol@^1.0.4: - version "1.1.1" - resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz" - integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== - dependencies: - call-bound "^1.0.2" - has-symbols "^1.1.0" - safe-regex-test "^1.1.0" - -is-symbol@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz" - integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== - dependencies: - call-bound "^1.0.2" - has-symbols "^1.1.0" - safe-regex-test "^1.1.0" - -is-typed-array@^1.1.13: - version "1.1.13" - resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz" - integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== - dependencies: - which-typed-array "^1.1.14" - -is-typed-array@^1.1.14, is-typed-array@^1.1.15: - version "1.1.15" - resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz" - integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== - dependencies: - which-typed-array "^1.1.16" - -is-unicode-supported@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz" - integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== - -is-weakmap@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz" - integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== - -is-weakref@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz" - integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== - dependencies: - call-bind "^1.0.2" - -is-weakref@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz" - integrity sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew== - dependencies: - call-bound "^1.0.3" - -is-weakset@^2.0.3: - version "2.0.4" - resolved "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz" - integrity sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ== - dependencies: - call-bound "^1.0.3" - get-intrinsic "^1.2.6" - -is-wsl@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== - dependencies: - is-docker "^2.0.0" - -isarray@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -jackspeak@^3.1.2: - version "3.4.3" - resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz" - integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== - dependencies: - "@isaacs/cliui" "^8.0.2" - optionalDependencies: - "@pkgjs/parseargs" "^0.11.0" - -joi@^17.13.0: - version "17.13.3" - resolved "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz" - integrity sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA== - dependencies: - "@hapi/hoek" "^9.3.0" - "@hapi/topo" "^5.1.0" - "@sideway/address" "^4.1.5" - "@sideway/formula" "^3.0.1" - "@sideway/pinpoint" "^2.0.0" - -joycon@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz" - integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== - -js-beautify@1.15.4: - version "1.15.4" - resolved "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz" - integrity sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA== - dependencies: - config-chain "^1.1.13" - editorconfig "^1.0.4" - glob "^10.4.2" - js-cookie "^3.0.5" - nopt "^7.2.1" - -js-cookie@^3.0.5: - version "3.0.5" - resolved "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz" - integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== - -js-md4@^0.3.2: - version "0.3.2" - resolved "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz" - integrity sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA== - -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -json-bigint@^1.0.0: - 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== - dependencies: - bignumber.js "^9.0.0" - -json-buffer@3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" - integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" - integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== - -json2csv@^5.0.7: - version "5.0.7" - resolved "https://registry.npmjs.org/json2csv/-/json2csv-5.0.7.tgz" - integrity sha512-YRZbUnyaJZLZUJSRi2G/MqahCyRv9n/ds+4oIetjDF3jWQA7AG7iSeKTiZiCNqtMZM7HDyt0e/W6lEnoGEmMGA== - dependencies: - commander "^6.1.0" - jsonparse "^1.3.1" - lodash.get "^4.4.2" - -json5@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz" - integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== - dependencies: - minimist "^1.2.0" - -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - -jsonparse@^1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz" - integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== - -jsonwebtoken@^9.0.0: - version "9.0.3" - resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz" - integrity sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g== - dependencies: - jws "^4.0.1" - lodash.includes "^4.3.0" - lodash.isboolean "^3.0.3" - lodash.isinteger "^4.0.4" - lodash.isnumber "^3.0.3" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.once "^4.0.0" - ms "^2.1.1" - semver "^7.5.4" - -jwa@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz" - integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg== - dependencies: - buffer-equal-constant-time "^1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^4.0.0, jws@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz" - integrity sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA== - dependencies: - jwa "^2.0.1" - safe-buffer "^5.0.1" - -keyv@^4.5.3: - version "4.5.4" - resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" - integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== - dependencies: - json-buffer "3.0.1" - -levn@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" - integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== - dependencies: - prelude-ls "^1.2.1" - type-check "~0.4.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -lodash.get@^4.4.2: - version "4.4.2" - resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz" - integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== - -lodash.includes@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz" - integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== - -lodash.isboolean@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz" - integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== - -lodash.isequal@^4.5.0: - version "4.5.0" - resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz" - integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== - -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz" - integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== - -lodash.isnumber@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz" - integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz" - integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz" - integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== - -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -lodash.mergewith@^4.6.2: - version "4.6.2" - resolved "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz" - integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== - -lodash.once@^4.0.0: - version "4.1.1" - resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz" - integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== - -lodash@^4.17.21, lodash@^4.17.23, lodash@^4.18.1: - version "4.18.1" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz" - integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== - -log-symbols@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz" - integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== - dependencies: - chalk "^4.1.0" - is-unicode-supported "^0.1.0" - -long@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/long/-/long-4.0.0.tgz" - integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== - -lru-cache@^10.2.0: - version "10.4.3" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz" - integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -lru-cache@^7.14.1: - version "7.18.3" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz" - integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== - -math-intrinsics@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz" - integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" - integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== - -merge-descriptors@^1.0.1: - version "1.0.3" - resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz" - integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" - integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== - -methods@^1.1.2, methods@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" - integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== - -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12, mime-types@^2.1.35, mime-types@~2.1.24, mime-types@~2.1.34: - version "2.1.35" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime@^1.3.4, mime@1.6.0: - version "1.6.0" - resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mime@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz" - integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== - -minimatch@^10.2.1: - version "10.2.5" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz" - integrity sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg== - dependencies: - brace-expansion "^5.0.5" - -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^5.0.1, minimatch@^5.1.6: - version "5.1.9" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz" - integrity sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^9.0.1: - version "9.0.9" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz" - integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg== - dependencies: - brace-expansion "^2.0.2" - -minimatch@^9.0.4: - version "9.0.9" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz" - integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg== - dependencies: - brace-expansion "^2.0.2" - -minimist@^1.2.0, minimist@^1.2.6: - version "1.2.8" - resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: - version "7.1.3" - resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz" - integrity sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A== - -mocha@^10.0.0: - version "10.8.2" - resolved "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz" - integrity sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg== - dependencies: - ansi-colors "^4.1.3" - browser-stdout "^1.3.1" - chokidar "^3.5.3" - debug "^4.3.5" - diff "^5.2.0" - escape-string-regexp "^4.0.0" - find-up "^5.0.0" - glob "^8.1.0" - he "^1.2.0" - js-yaml "^4.1.0" - log-symbols "^4.1.0" - minimatch "^5.1.6" - ms "^2.1.3" - serialize-javascript "^6.0.2" - strip-json-comments "^3.1.1" - supports-color "^8.1.1" - workerpool "^6.5.1" - yargs "^16.2.0" - yargs-parser "^20.2.9" - yargs-unparser "^2.0.0" - -moment-timezone@^0.5.43: - version "0.5.45" - resolved "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz" - integrity sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ== - dependencies: - moment "^2.29.4" - -moment@^2.29.4, moment@2.30.1: - version "2.30.1" - resolved "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz" - integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== - -ms@^2.1.1, ms@^2.1.3, ms@2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -multer@^2.0.0: - version "2.1.1" - resolved "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz" - integrity sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A== - dependencies: - append-field "^1.0.0" - busboy "^1.6.0" - concat-stream "^2.0.0" - type-is "^1.6.18" - -mysql2@2.2.5: - version "2.2.5" - resolved "https://registry.npmjs.org/mysql2/-/mysql2-2.2.5.tgz" - integrity sha512-XRqPNxcZTpmFdXbJqb+/CtYVLCx14x1RTeNMD4954L331APu75IC74GDqnZMEt1kwaXy6TySo55rF2F3YJS78g== - dependencies: - denque "^1.4.1" - generate-function "^2.3.1" - iconv-lite "^0.6.2" - long "^4.0.0" - lru-cache "^6.0.0" - named-placeholders "^1.1.2" - seq-queue "^0.0.5" - sqlstring "^2.3.2" - -named-placeholders@^1.1.2: - version "1.1.3" - resolved "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz" - integrity sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w== - dependencies: - lru-cache "^7.14.1" - -native-duplexpair@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz" - integrity sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" - integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== - -negotiator@0.6.3: - version "0.6.3" - resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" - integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== - -node-addon-api@^8.3.0: - version "8.7.0" - resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz" - integrity sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA== - -node-fetch@^2.6.9: - version "2.7.0" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" - integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== - dependencies: - whatwg-url "^5.0.0" - -node-gyp-build@^4.8.4: - version "4.8.4" - resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz" - integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ== - -node-mocks-http@^1.17.0: - version "1.17.2" - resolved "https://registry.npmjs.org/node-mocks-http/-/node-mocks-http-1.17.2.tgz" - integrity sha512-HVxSnjNzE9NzoWMx9T9z4MLqwMpLwVvA0oVZ+L+gXskYXEJ6tFn3Kx4LargoB6ie7ZlCLplv7QbWO6N+MysWGA== - dependencies: - accepts "^1.3.7" - content-disposition "^0.5.3" - depd "^1.1.0" - fresh "^0.5.2" - merge-descriptors "^1.0.1" - methods "^1.1.2" - mime "^1.3.4" - parseurl "^1.3.3" - range-parser "^1.2.0" - type-is "^1.6.18" - -nodemailer@6.9.9: - version "6.9.9" - resolved "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.9.tgz" - integrity sha512-dexTll8zqQoVJEZPwQAKzxxtFn0qTnjdQTchoU6Re9BUUGBJiOy3YMn/0ShTW6J5M0dfQ1NeDeRTTl4oIWgQMA== - -nodemon@^3.0.0: - version "3.1.14" - resolved "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz" - integrity sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw== - dependencies: - chokidar "^3.5.2" - debug "^4" - ignore-by-default "^1.0.1" - minimatch "^10.2.1" - pstree.remy "^1.1.8" - semver "^7.5.3" - simple-update-notifier "^2.0.0" - supports-color "^5.5.0" - touch "^3.1.0" - undefsafe "^2.0.5" - -nopt@^7.2.1: - version "7.2.1" - resolved "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz" - integrity sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w== - dependencies: - abbrev "^2.0.0" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -oauth@0.10.x: - version "0.10.0" - resolved "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz" - integrity sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q== - -object-assign@^4: - version "4.1.1" - resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-inspect@^1.13.1: - version "1.13.2" - resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz" - integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== - -object-inspect@^1.13.3, object-inspect@^1.13.4: - version "1.13.4" - resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz" - integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== - -object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.5: - version "4.1.5" - resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz" - integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== - dependencies: - call-bind "^1.0.5" - define-properties "^1.2.1" - has-symbols "^1.0.3" - object-keys "^1.1.1" - -object.assign@^4.1.7: - version "4.1.7" - resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz" - integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.3" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - has-symbols "^1.1.0" - object-keys "^1.1.1" - -object.fromentries@^2.0.8: - version "2.0.8" - resolved "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz" - integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-object-atoms "^1.0.0" - -object.groupby@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz" - integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.2" - -object.values@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz" - integrity sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.3" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - -on-exit-leak-free@^2.1.0: - 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== - -on-finished@2.4.1: - version "2.4.1" - resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" - integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== - dependencies: - ee-first "1.1.1" - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -open@^8.0.0: - version "8.4.2" - resolved "https://registry.npmjs.org/open/-/open-8.4.2.tgz" - integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== - dependencies: - define-lazy-prop "^2.0.0" - is-docker "^2.1.1" - is-wsl "^2.2.0" - -openapi-types@>=7: - version "12.1.3" - resolved "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz" - integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw== - -optionator@^0.9.3: - version "0.9.4" - resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz" - integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== - 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" - -own-keys@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz" - integrity sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg== - dependencies: - get-intrinsic "^1.2.6" - object-keys "^1.1.1" - safe-push-apply "^1.0.0" - -p-limit@^3.0.1, p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-locate@^5.0.0: - 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== - dependencies: - p-limit "^3.0.2" - -package-json-from-dist@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz" - integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-cache-control@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz" - integrity sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg== - -parseurl@^1.3.3, parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -passport-google-oauth2@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/passport-google-oauth2/-/passport-google-oauth2-0.2.0.tgz" - integrity sha512-62EdPtbfVdc55nIXi0p1WOa/fFMM8v/M8uQGnbcXA4OexZWCnfsEi3wo2buag+Is5oqpuHzOtI64JpHk0Xi5RQ== - dependencies: - passport-oauth2 "^1.1.2" - -passport-jwt@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz" - integrity sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ== - dependencies: - jsonwebtoken "^9.0.0" - passport-strategy "^1.0.0" - -passport-microsoft@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/passport-microsoft/-/passport-microsoft-2.1.0.tgz" - integrity sha512-7bOcjEmZCHg5qD55iHaMD/mgBxPtXLbqAwmKox5IsqOSEU50WJk5nQKK4lxKdBHLZ0hf+gzrFgDsTybJP18/JA== - dependencies: - passport-oauth2 "1.8.0" - -passport-oauth2@^1.1.2, passport-oauth2@1.8.0: - version "1.8.0" - resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz" - integrity sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA== - dependencies: - base64url "3.x.x" - oauth "0.10.x" - passport-strategy "1.x.x" - uid2 "0.0.x" - utils-merge "1.x.x" - -passport-strategy@^1.0.0, passport-strategy@1.x.x: - version "1.0.0" - resolved "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz" - integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== - -passport@^0.7.0: - version "0.7.0" - resolved "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz" - integrity sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ== - dependencies: - passport-strategy "1.x.x" - pause "0.0.1" - utils-merge "^1.0.1" - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-expression-matcher@^1.5.0: - 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== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-scurry@^1.11.1: - version "1.11.1" - resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz" - integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== - dependencies: - lru-cache "^10.2.0" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== - -pause@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz" - integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== - -pg-cloudflare@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz" - integrity sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ== - -pg-connection-string@^2.12.0, pg-connection-string@^2.6.1: - version "2.12.0" - resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz" - integrity sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ== - -pg-hstore@2.3.4: - version "2.3.4" - resolved "https://registry.npmjs.org/pg-hstore/-/pg-hstore-2.3.4.tgz" - integrity sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA== - dependencies: - underscore "^1.13.1" - -pg-int8@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz" - integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== - -pg-pool@^3.13.0: - version "3.13.0" - resolved "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz" - integrity sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA== - -pg-protocol@^1.13.0: - version "1.13.0" - resolved "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz" - integrity sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w== - -pg-types@2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz" - integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== - 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" - -pg@^8.20.0, pg@>=8.0: - version "8.20.0" - resolved "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz" - integrity sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA== - dependencies: - pg-connection-string "^2.12.0" - pg-pool "^3.13.0" - pg-protocol "^1.13.0" - pg-types "2.2.0" - pgpass "1.0.5" - optionalDependencies: - pg-cloudflare "^1.3.0" - -pgpass@1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz" - integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== - dependencies: - split2 "^4.1.0" - -picocolors@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" - integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== - -picomatch@^2.0.4, picomatch@^2.2.1: - version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -pino-abstract-transport@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz" - integrity sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw== - dependencies: - split2 "^4.0.0" - -pino-pretty@^11.0.0: - version "11.3.0" - resolved "https://registry.npmjs.org/pino-pretty/-/pino-pretty-11.3.0.tgz" - integrity sha512-oXwn7ICywaZPHmu3epHGU2oJX4nPmKvHvB/bwrJHlGcbEWaVcotkpyVHMKLKmiVryWYByNp0jpgAcXpFJDXJzA== - dependencies: - colorette "^2.0.7" - dateformat "^4.6.3" - fast-copy "^3.0.2" - 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 "^2.0.0" - pump "^3.0.0" - readable-stream "^4.0.0" - secure-json-parse "^2.4.0" - sonic-boom "^4.0.1" - strip-json-comments "^3.1.1" - -pino-std-serializers@^7.0.0: - version "7.1.0" - resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz" - integrity sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw== - -pino@^9.0.0: - version "9.14.0" - resolved "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz" - integrity sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w== - dependencies: - "@pinojs/redact" "^0.4.0" - atomic-sleep "^1.0.0" - on-exit-leak-free "^2.1.0" - pino-abstract-transport "^2.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 "^3.0.0" - -possible-typed-array-names@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz" - integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== - -postgres-array@~2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz" - integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== - -postgres-bytea@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz" - integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w== - -postgres-date@~1.0.4: - 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== - -postgres-interval@^1.1.0: - version "1.2.0" - resolved "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz" - integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== - dependencies: - xtend "^4.0.0" - -prelude-ls@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" - integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== - -process-warning@^5.0.0: - 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== - -process@^0.11.10: - version "0.11.10" - resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" - integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== - -progress@^2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - -proto-list@~1.2.1: - version "1.2.4" - resolved "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz" - integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== - -proxy-addr@~2.0.7: - 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== - dependencies: - forwarded "0.2.0" - ipaddr.js "1.9.1" - -proxy-from-env@^2.1.0: - 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== - -pstree.remy@^1.1.8: - version "1.1.8" - resolved "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz" - integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== - -pump@^3.0.0: - version "3.0.4" - resolved "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz" - integrity sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -punycode@^2.1.0: - version "2.3.1" - resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" - integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== - -qs@6.11.0: - version "6.11.0" - resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== - dependencies: - side-channel "^1.0.4" - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -quick-format-unescaped@^4.0.3: - 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== - -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -range-parser@^1.2.0, range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.5.1: - version "2.5.1" - resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz" - integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - -readable-stream@^3.0.2, readable-stream@^3.1.1: - version "3.6.2" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@^4.0.0: - version "4.7.0" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz" - integrity sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg== - dependencies: - abort-controller "^3.0.0" - buffer "^6.0.3" - events "^3.3.0" - process "^0.11.10" - string_decoder "^1.3.0" - -readable-stream@^4.2.0: - version "4.5.2" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz" - integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== - dependencies: - abort-controller "^3.0.0" - buffer "^6.0.3" - events "^3.3.0" - process "^0.11.10" - string_decoder "^1.3.0" - -readdirp@^4.0.1: - version "4.1.2" - resolved "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz" - integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -real-require@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz" - integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== - -reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: - version "1.0.10" - resolved "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz" - integrity sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw== - dependencies: - call-bind "^1.0.8" - define-properties "^1.2.1" - es-abstract "^1.23.9" - es-errors "^1.3.0" - es-object-atoms "^1.0.0" - get-intrinsic "^1.2.7" - get-proto "^1.0.1" - which-builtin-type "^1.2.1" - -regexp.prototype.flags@^1.5.2: - version "1.5.2" - resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz" - integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== - dependencies: - call-bind "^1.0.6" - define-properties "^1.2.1" - es-errors "^1.3.0" - set-function-name "^2.0.1" - -regexp.prototype.flags@^1.5.4: - version "1.5.4" - resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz" - integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA== - dependencies: - call-bind "^1.0.8" - define-properties "^1.2.1" - es-errors "^1.3.0" - get-proto "^1.0.1" - gopd "^1.2.0" - set-function-name "^2.0.2" - -require-directory@^2.1.1: - 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== - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve@^1.22.1: - version "1.22.8" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== - dependencies: - is-core-module "^2.13.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -resolve@^1.22.4: - version "1.22.11" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz" - integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== - dependencies: - is-core-module "^2.16.1" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -retry-as-promised@^7.0.4: - version "7.0.4" - resolved "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz" - integrity sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA== - -retry-request@^7.0.0: - version "7.0.2" - resolved "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz" - integrity sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w== - dependencies: - "@types/request" "^2.48.8" - extend "^3.0.2" - teeny-request "^9.0.0" - -retry@0.13.1: - version "0.13.1" - resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz" - integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== - -reusify@^1.0.4: - version "1.1.0" - resolved "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz" - integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -safe-array-concat@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz" - integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== - dependencies: - call-bind "^1.0.7" - get-intrinsic "^1.2.4" - has-symbols "^1.0.3" - isarray "^2.0.5" - -safe-array-concat@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz" - integrity sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.2" - get-intrinsic "^1.2.6" - has-symbols "^1.1.0" - isarray "^2.0.5" - -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0, safe-buffer@5.2.1: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-push-apply@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz" - integrity sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA== - dependencies: - es-errors "^1.3.0" - isarray "^2.0.5" - -safe-regex-test@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz" - integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== - dependencies: - call-bind "^1.0.6" - es-errors "^1.3.0" - is-regex "^1.1.4" - -safe-regex-test@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz" - integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== - dependencies: - call-bound "^1.0.2" - es-errors "^1.3.0" - is-regex "^1.2.1" - -safe-stable-stringify@^2.3.1: - version "2.5.0" - resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz" - integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== - -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": - version "2.1.2" - resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -secure-json-parse@^2.4.0: - version "2.7.0" - resolved "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz" - integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw== - -semver@^6.3.1: - version "6.3.1" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^7.5.3: - version "7.8.0" - resolved "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz" - integrity sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA== - -semver@^7.5.4: - version "7.8.0" - resolved "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz" - integrity sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA== - -send@0.18.0: - version "0.18.0" - resolved "https://registry.npmjs.org/send/-/send-0.18.0.tgz" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== - dependencies: - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "2.0.0" - mime "1.6.0" - ms "2.1.3" - on-finished "2.4.1" - range-parser "~1.2.1" - statuses "2.0.1" - -seq-queue@^0.0.5: - version "0.0.5" - resolved "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz" - integrity sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q== - -sequelize-cli@^6.6.5: - version "6.6.5" - resolved "https://registry.npmjs.org/sequelize-cli/-/sequelize-cli-6.6.5.tgz" - integrity sha512-DqyISCULOaEbTM+rRQH4YvcUWeOC1XDiSKcjsC6TfAnT7W837mNkChJhtB/Z4FdCFHRCojmiP7zsrA4pARmacA== - dependencies: - fs-extra "^9.1.0" - js-beautify "1.15.4" - lodash "^4.17.21" - picocolors "^1.1.1" - resolve "^1.22.1" - umzug "^2.3.0" - yargs "^16.2.0" - -sequelize-json-schema@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/sequelize-json-schema/-/sequelize-json-schema-2.1.1.tgz" - integrity sha512-yCGaHnmQQeL6MQ/fOxhkR5C2aOGZyTD6OrgjP4yw1rbuujuIUVdzWN3AsC6r6AvlGZ3EUBBbCJHKl8OIFFES4Q== - -sequelize-pool@^7.1.0: - version "7.1.0" - resolved "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz" - integrity sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg== - -sequelize@^6.37.0, "sequelize@>= 4": - version "6.37.8" - resolved "https://registry.npmjs.org/sequelize/-/sequelize-6.37.8.tgz" - integrity sha512-HJ0IQFqcTsTiqbEgiuioYFMSD00TP6Cz7zoTti+zVVBwVe9fEhev9cH6WnM3XU31+ABS356durAb99ZuOthnKw== - dependencies: - "@types/debug" "^4.1.8" - "@types/validator" "^13.7.17" - debug "^4.3.4" - dottie "^2.0.6" - inflection "^1.13.4" - lodash "^4.17.21" - moment "^2.29.4" - moment-timezone "^0.5.43" - pg-connection-string "^2.6.1" - retry-as-promised "^7.0.4" - semver "^7.5.4" - sequelize-pool "^7.1.0" - toposort-class "^1.0.1" - uuid "^8.3.2" - validator "^13.9.0" - wkx "^0.5.0" - -serialize-javascript@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz" - integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== - dependencies: - randombytes "^2.1.0" - -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.18.0" - -set-function-length@^1.2.1, set-function-length@^1.2.2: - version "1.2.2" - resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz" - integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - -set-function-name@^2.0.1, set-function-name@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz" - integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - functions-have-names "^1.2.3" - has-property-descriptors "^1.0.2" - -set-proto@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz" - integrity sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw== - dependencies: - dunder-proto "^1.0.1" - es-errors "^1.3.0" - es-object-atoms "^1.0.0" - -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -side-channel-list@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz" - integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== - dependencies: - es-errors "^1.3.0" - object-inspect "^1.13.3" - -side-channel-map@^1.0.1: - 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== - dependencies: - call-bound "^1.0.2" - es-errors "^1.3.0" - get-intrinsic "^1.2.5" - object-inspect "^1.13.3" - -side-channel-weakmap@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz" - integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== - 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" - -side-channel@^1.0.4: - version "1.0.6" - resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz" - integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== - dependencies: - call-bind "^1.0.7" - es-errors "^1.3.0" - get-intrinsic "^1.2.4" - object-inspect "^1.13.1" - -side-channel@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz" - integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== - 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" - -signal-exit@^4.0.1: - version "4.1.0" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" - integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== - -simple-update-notifier@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz" - integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w== - dependencies: - semver "^7.5.3" - -sonic-boom@^4.0.1: - version "4.2.1" - resolved "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz" - integrity sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q== - dependencies: - atomic-sleep "^1.0.0" - -split2@^4.0.0, split2@^4.1.0: - version "4.2.0" - resolved "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz" - integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== - -sprintf-js@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz" - integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== - -sqlite@4.0.15: - version "4.0.15" - resolved "https://registry.npmjs.org/sqlite/-/sqlite-4.0.15.tgz" - integrity sha512-irPPTrbVoDvwzRGpe0v8vxpNwMl+q0tXQzffQTcCUnaJzQFO0hfLLvFwGDKxd6vYBuvEr3uvPkObVoGOvVsmzA== - -sqlstring@^2.3.2: - version "2.3.3" - resolved "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz" - integrity sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg== - -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== - -stop-iteration-iterator@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz" - integrity sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ== - dependencies: - es-errors "^1.3.0" - internal-slot "^1.1.0" - -stoppable@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz" - integrity sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw== - -stream-events@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz" - integrity sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg== - dependencies: - stubs "^3.0.0" - -stream-shift@^1.0.2: - version "1.0.3" - resolved "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz" - integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== - -streamsearch@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz" - integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== - -string_decoder@^1.1.1, string_decoder@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^5.0.1, string-width@^5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" - integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== - dependencies: - eastasianwidth "^0.2.0" - emoji-regex "^9.2.2" - strip-ansi "^7.0.1" - -string.prototype.trim@^1.2.10: - version "1.2.10" - resolved "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz" - integrity sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.2" - define-data-property "^1.1.4" - define-properties "^1.2.1" - es-abstract "^1.23.5" - es-object-atoms "^1.0.0" - has-property-descriptors "^1.0.2" - -string.prototype.trim@^1.2.9: - version "1.2.9" - resolved "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz" - integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.0" - es-object-atoms "^1.0.0" - -string.prototype.trimend@^1.0.8: - version "1.0.8" - resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz" - integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - -string.prototype.trimend@^1.0.9: - version "1.0.9" - resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz" - integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.2" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - -string.prototype.trimstart@^1.0.8: - version "1.0.8" - resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz" - integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^7.0.1: - version "7.2.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz" - integrity sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w== - dependencies: - ansi-regex "^6.2.2" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" - integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== - -strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -strnum@^2.2.3: - version "2.3.0" - resolved "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz" - integrity sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q== - -stubs@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz" - integrity sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw== - -supports-color@^5.5.0: - version "5.5.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-color@^8.1.1: - version "8.1.1" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -swagger-jsdoc@^6.2.8: - version "6.2.8" - resolved "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz" - integrity sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ== - dependencies: - commander "6.2.0" - doctrine "3.0.0" - glob "7.1.6" - lodash.mergewith "^4.6.2" - swagger-parser "^10.0.3" - yaml "2.0.0-1" - -swagger-parser@^10.0.3: - version "10.0.3" - resolved "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz" - integrity sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg== - dependencies: - "@apidevtools/swagger-parser" "10.0.3" - -swagger-ui-dist@>=5.0.0: - version "5.17.14" - resolved "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz" - integrity sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw== - -swagger-ui-express@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz" - integrity sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA== - dependencies: - swagger-ui-dist ">=5.0.0" - -tedious@^18.6.0: - version "18.6.2" - resolved "https://registry.npmjs.org/tedious/-/tedious-18.6.2.tgz" - integrity sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg== - dependencies: - "@azure/core-auth" "^1.7.2" - "@azure/identity" "^4.2.1" - "@azure/keyvault-keys" "^4.4.0" - "@js-joda/core" "^5.6.1" - "@types/node" ">=18" - bl "^6.0.11" - iconv-lite "^0.6.3" - js-md4 "^0.3.2" - native-duplexpair "^1.0.0" - sprintf-js "^1.1.3" - -teeny-request@^9.0.0: - version "9.0.0" - resolved "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz" - integrity sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g== - dependencies: - http-proxy-agent "^5.0.0" - https-proxy-agent "^5.0.0" - node-fetch "^2.6.9" - stream-events "^1.0.5" - uuid "^9.0.0" - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== - -thread-stream@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz" - integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A== - dependencies: - real-require "^0.2.0" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -toidentifier@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== - -toposort-class@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz" - integrity sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg== - -touch@^3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz" - integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA== - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - -tsconfig-paths@^3.15.0: - version "3.15.0" - resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz" - integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== - dependencies: - "@types/json5" "^0.0.29" - json5 "^1.0.2" - minimist "^1.2.6" - strip-bom "^3.0.0" - -tslib@^2.2.0, tslib@^2.6.2: - version "2.6.3" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz" - integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== - -type-check@^0.4.0, type-check@~0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" - integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== - dependencies: - prelude-ls "^1.2.1" - -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - -type-is@^1.6.18, type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -typed-array-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz" - integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== - dependencies: - call-bind "^1.0.7" - es-errors "^1.3.0" - is-typed-array "^1.1.13" - -typed-array-buffer@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz" - integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== - dependencies: - call-bound "^1.0.3" - es-errors "^1.3.0" - is-typed-array "^1.1.14" - -typed-array-byte-length@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz" - integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== - dependencies: - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-proto "^1.0.3" - is-typed-array "^1.1.13" - -typed-array-byte-length@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz" - integrity sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg== - dependencies: - call-bind "^1.0.8" - for-each "^0.3.3" - gopd "^1.2.0" - has-proto "^1.2.0" - is-typed-array "^1.1.14" - -typed-array-byte-offset@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz" - integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-proto "^1.0.3" - is-typed-array "^1.1.13" - -typed-array-byte-offset@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz" - integrity sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.8" - for-each "^0.3.3" - gopd "^1.2.0" - has-proto "^1.2.0" - is-typed-array "^1.1.15" - reflect.getprototypeof "^1.0.9" - -typed-array-length@^1.0.6: - version "1.0.6" - resolved "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz" - integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== - dependencies: - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-proto "^1.0.3" - is-typed-array "^1.1.13" - possible-typed-array-names "^1.0.0" - -typed-array-length@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz" - integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg== - dependencies: - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - is-typed-array "^1.1.13" - possible-typed-array-names "^1.0.0" - reflect.getprototypeof "^1.0.6" - -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" - integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== - -uid2@0.0.x: - version "0.0.4" - resolved "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz" - integrity sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA== - -umzug@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz" - integrity sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw== - dependencies: - bluebird "^3.7.2" - -unbox-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz" - integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== - dependencies: - call-bind "^1.0.2" - has-bigints "^1.0.2" - has-symbols "^1.0.3" - which-boxed-primitive "^1.0.2" - -unbox-primitive@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz" - integrity sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw== - dependencies: - call-bound "^1.0.3" - has-bigints "^1.0.2" - has-symbols "^1.1.0" - which-boxed-primitive "^1.1.1" - -undefsafe@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz" - integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== - -underscore@^1.13.1: - version "1.13.6" - resolved "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz" - integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== - -undici-types@~5.26.4: - version "5.26.5" - resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz" - integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== - -universalify@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" - integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== - -unpipe@~1.0.0, unpipe@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" - integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -util-deprecate@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -utils-merge@^1.0.1, utils-merge@1.0.1, utils-merge@1.x.x: - version "1.0.1" - resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" - integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== - -uuid@^8.0.0, uuid@^8.3.0, uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -uuid@^9.0.0: - version "9.0.1" - resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" - integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== - -uuid@^9.0.1: - version "9.0.1" - resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" - integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== - -validator@^13.7.0, validator@^13.9.0, validator@~13.15.23: - version "13.15.35" - resolved "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz" - integrity sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw== - -vary@^1, vary@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" - integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== - dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" - -which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz" - integrity sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA== - dependencies: - is-bigint "^1.1.0" - is-boolean-object "^1.2.1" - is-number-object "^1.1.1" - is-string "^1.1.1" - is-symbol "^1.1.1" - -which-builtin-type@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz" - integrity sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q== - dependencies: - call-bound "^1.0.2" - function.prototype.name "^1.1.6" - has-tostringtag "^1.0.2" - is-async-function "^2.0.0" - is-date-object "^1.1.0" - is-finalizationregistry "^1.1.0" - is-generator-function "^1.0.10" - is-regex "^1.2.1" - is-weakref "^1.0.2" - isarray "^2.0.5" - which-boxed-primitive "^1.1.0" - which-collection "^1.0.2" - which-typed-array "^1.1.16" - -which-collection@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz" - integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== - dependencies: - is-map "^2.0.3" - is-set "^2.0.3" - is-weakmap "^2.0.2" - is-weakset "^2.0.3" - -which-typed-array@^1.1.14: - version "1.1.15" - resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz" - integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.2" - -which-typed-array@^1.1.15: - version "1.1.15" - resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz" - integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.2" - -which-typed-array@^1.1.16, which-typed-array@^1.1.19: - version "1.1.20" - resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz" - integrity sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.8" - call-bound "^1.0.4" - for-each "^0.3.5" - get-proto "^1.0.1" - gopd "^1.2.0" - has-tostringtag "^1.0.2" - -which@^1.1.1: - version "1.3.1" - resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -wkx@^0.5.0: - version "0.5.0" - resolved "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz" - integrity sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg== - dependencies: - "@types/node" "*" - -word-wrap@^1.2.5: - version "1.2.5" - resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" - integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== - -workerpool@^6.5.1: - version "6.5.1" - resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz" - integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== - -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^8.1.0: - version "8.1.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" - integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== - dependencies: - ansi-styles "^6.1.0" - string-width "^5.0.1" - strip-ansi "^7.0.1" - -wrappy@1: - version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -xml-naming@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz" - integrity sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw== - -xtend@^4.0.0: - version "4.0.2" - resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yaml@2.0.0-1: - version "2.0.0-1" - resolved "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz" - integrity sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ== - -yargs-parser@^20.2.2, yargs-parser@^20.2.9: - version "20.2.9" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - -yargs-unparser@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz" - integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== - dependencies: - camelcase "^6.0.0" - decamelize "^4.0.0" - flat "^5.0.2" - is-plain-obj "^2.1.0" - -yargs@^16.2.0: - version "16.2.0" - resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -z-schema@^5.0.1: - version "5.0.6" - resolved "https://registry.npmjs.org/z-schema/-/z-schema-5.0.6.tgz" - integrity sha512-+XR1GhnWklYdfr8YaZv/iu+vY+ux7V5DS5zH1DQf6bO5ufrt/5cgNhVO5qyhsjFXvsqQb/f08DWE9b6uPscyAg== - dependencies: - lodash.get "^4.4.2" - lodash.isequal "^4.5.0" - validator "^13.7.0" - optionalDependencies: - commander "^10.0.0" diff --git a/docker/README.md b/docker/README.md index 69d1021..4eaa9e7 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,18 +1,15 @@ ## Description: - The project contains the **docker folder** and the `Dockerfile`. + The project contains the **docker folder** and Dockerfiles for local and image-based runs. The `Dockerfile` is used to Deploy the project to Google Cloud. The **docker folder** contains a couple of helper scripts: - `docker-compose.yml` (all our services: web, backend, db are described here) -- `start-backend.sh` (starts backend, but only after the database) +- `start-backend.sh` (starts backend with the current npm/Umzug flow, but only after the database) - `wait-for-it.sh` (imported from https://github.com/vishnubob/wait-for-it) - > To avoid breaking the application, we recommend you don't edit the following files: everything that includes the **docker folder** and `Dokerfile`. - - ## Run services: 1. Install docker compose (https://docs.docker.com/compose/install/) @@ -37,10 +34,13 @@ 7.2. With a stored (from previus runs) database data `docker-compose up` - 8. Check http://localhost:3000 + 8. Check the services: + + - Frontend: http://localhost:3001 + - Backend API: http://localhost:3000/api + - Swagger: http://localhost:3000/api-docs 9. Stop services: 9.1. Just press `Ctr+C` - diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index e03e333..abaef5e 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,12 +1,14 @@ -version: "3.9" services: web: image: frontend build: ../frontend stdin_open: true # docker run -i tty: true # docker run -t + environment: + - FRONT_PORT=3001 + - NEXT_PUBLIC_BACK_API=http://localhost:3000 ports: - - "3000:3000" + - "3001:3001" logging: driver: json-file options: @@ -35,9 +37,15 @@ services: - ./wait-for-it.sh:/usr/src/app/wait-for-it.sh - ./start-backend.sh:/usr/src/app/start-backend.sh environment: + - NODE_ENV=dev_stage - DB_HOST=db + - DB_PORT=5432 + - DB_NAME=db_tour_builder_platform + - DB_USER=postgres + - DB_PASS= + - PORT=3000 ports: - - "8080:8080" + - "3000:3000" logging: driver: json-file options: diff --git a/docker/start-backend.sh b/docker/start-backend.sh index fb353bf..59bcce2 100644 --- a/docker/start-backend.sh +++ b/docker/start-backend.sh @@ -1,2 +1,4 @@ #!/usr/bin/env bash -yarn start +set -euo pipefail + +npm run start diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..932a221 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,7 @@ +node_modules +build +.next +.env.local +.DS_Store +tsconfig.tsbuildinfo +npm-debug.log* diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 56e10d0..dba5ed0 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.15.1-alpine +FROM node:24-alpine # Create app directory WORKDIR /usr/src/app @@ -8,12 +8,10 @@ WORKDIR /usr/src/app # where available (npm@5+) COPY package*.json ./ -RUN yarn install -# If you are building your code for production -# RUN npm ci --only=production +RUN npm ci # Bundle app source COPY . . -EXPOSE 3000 -CMD [ "yarn", "dev" ] \ No newline at end of file +EXPOSE 3001 +CMD [ "npm", "run", "dev" ] diff --git a/frontend/README.md b/frontend/README.md index 8f8e0d9..cd05092 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -6,21 +6,19 @@ Next.js 15 application with React 19, TypeScript, Redux Toolkit, and Tailwind CS - **Framework**: Next.js 15 with Turbopack - **UI Library**: React 19 -- **Language**: TypeScript 5.4 +- **Language**: TypeScript 5.9 - **State Management**: Redux Toolkit - **Styling**: Tailwind CSS 3.4 + MUI 6 - **Forms**: Formik - **Data Grid**: MUI X Data Grid v7 -- **Charts**: ApexCharts, Chart.js -- **Drag & Drop**: react-dnd - **PWA**: Serwist (Service Worker) - **i18n**: i18next - **Offline Storage**: Dexie (IndexedDB) ## Prerequisites -- Node.js 18+ -- npm or yarn +- Node.js 24.x +- npm ## Quick Start @@ -28,7 +26,7 @@ Next.js 15 application with React 19, TypeScript, Redux Toolkit, and Tailwind CS # Install dependencies npm install -# Start development server (port 3000) +# Start development server (port 3001) npm run dev # Production build @@ -36,7 +34,7 @@ npm run build npm run start ``` -The app runs on **port 3000** by default (configurable via `FRONT_PORT` env var). +The app runs on **port 3001** by default (configurable via `FRONT_PORT` env var). ## Available Commands @@ -342,11 +340,11 @@ The `X-Runtime-Environment` header tells the backend which environment to query. ## Environment Variables ```env -# Backend API URL (defaults to localhost:8080) -NEXT_PUBLIC_BACK_API=http://localhost:8080 +# Backend API URL (defaults to localhost:3000) +NEXT_PUBLIC_BACK_API=http://localhost:3000 -# Frontend port (optional, default 3000) -FRONT_PORT=3000 +# Frontend port (optional, default 3001) +FRONT_PORT=3001 ``` ## API Integration diff --git a/frontend/package.json b/frontend/package.json index 8cb2155..20154ab 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,12 +1,17 @@ { "private": true, "scripts": { - "dev": "cross-env PORT=${FRONT_PORT:-3000} next dev --turbopack", + "dev": "next dev --turbopack -p ${FRONT_PORT:-3001}", "build": "next build", "start": "next start -H 0.0.0.0 -p ${FRONT_PORT:-3001}", + "typecheck": "tsc --noEmit", "lint": "eslint . --ext .ts,.tsx", + "verify": "npm run typecheck && npm run lint && npm run build", "format": "prettier '{components,pages,src,interfaces,hooks}/**/*.{tsx,ts,js}' --write" }, + "overrides": { + "postcss": "^8.5.16" + }, "dependencies": { "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", @@ -19,35 +24,22 @@ "@serwist/next": "^9.5.7", "@tailwindcss/typography": "^0.5.13", "@tanstack/react-query": "^5.96.2", - "apexcharts": "^5.0.0", "axios": "^1.8.4", - "chart.js": "^4.4.1", - "chroma-js": "^2.4.2", "dayjs": "^1.11.10", "dexie": "^4.3.0", "file-saver": "^2.0.5", "formik": "^2.4.5", - "html2canvas": "^1.4.1", "i18next": "^25.1.2", "i18next-browser-languagedetector": "^8.1.0", "i18next-http-backend": "^3.0.2", "intro.js": "^7.2.0", "intro.js-react": "^1.0.0", - "jsonwebtoken": "^9.0.2", "jwt-decode": "^4.0.0", "lodash": "^4.17.21", - "moment": "^2.30.1", "next": "^15.3.1", "next-i18next": "^15.4.2", - "numeral": "^2.0.6", - "query-string": "^8.1.0", "react": "^19.0.0", - "react-apexcharts": "^2.0.0", - "react-big-calendar": "^1.19.0", - "react-chartjs-2": "^5.0.0", "react-datepicker": "^7.0.0", - "react-dnd": "^16.0.1", - "react-dnd-html5-backend": "^16.0.1", "react-dom": "^19.0.0", "react-i18next": "^15.5.1", "react-redux": "^9.0.0", @@ -55,26 +47,21 @@ "react-select-async-paginate": "^0.7.11", "react-switch": "^7.0.0", "react-toastify": "^11.0.2", - "uuid": "^9.0.0", + "uuid": "^14.0.1", "zod": "^4.3.6" }, "devDependencies": { "@tailwindcss/forms": "^0.5.7", - "@tailwindcss/line-clamp": "^0.4.4", "@types/node": "18.7.16", - "@types/numeral": "^2.0.2", - "@types/react-big-calendar": "^1.8.8", - "@types/react-redux": "^7.1.24", - "@typescript-eslint/eslint-plugin": "^5.37.0", - "@typescript-eslint/parser": "^5.37.0", + "@typescript-eslint/eslint-plugin": "^8.62.1", + "@typescript-eslint/parser": "^8.62.1", "autoprefixer": "^10.4.0", - "cross-env": "^7.0.3", - "eslint": "^8.23.1", - "eslint-config-next": "^13.0.4", - "eslint-config-prettier": "^8.5.0", - "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-import": "^2.29.1", - "postcss": "^8.4.4", + "eslint": "^8.57.1", + "eslint-config-next": "^15.5.19", + "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.5", + "eslint-plugin-import": "^2.32.0", + "postcss": "^8.5.16", "postcss-import": "^14.1.0", "prettier": "^3.2.4", "serwist": "^9.5.7", diff --git a/frontend/src/components/SmartWidget/SmartWidget.tsx b/frontend/src/components/SmartWidget/SmartWidget.tsx deleted file mode 100644 index ef76c98..0000000 --- a/frontend/src/components/SmartWidget/SmartWidget.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React from 'react'; -import BaseButton from '../BaseButton'; -import BaseIcon from '../BaseIcon'; -import * as icons from '@mdi/js'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; - -import { fetchWidgets, removeWidget } from '../../stores/roles/rolesSlice'; -import { WidgetChartType, WidgetType } from './models/widget.model'; -import { BarChart } from './components/BarChart'; -import { PieChart } from './components/PieChart'; -import { AreaChart } from './components/AreaChart'; -import { LineChart } from './components/LineChart'; - -export const SmartWidget = ({ widget, userId, admin, roleId }) => { - const dispatch = useAppDispatch(); - const corners = useAppSelector((state) => state.style.corners); - const cardsStyle = useAppSelector((state) => state.style.cardsStyle); - - const deleteWidget = async () => { - await dispatch( - removeWidget({ id: userId, widgetId: widget.widget_id, roleId }), - ); - await dispatch(fetchWidgets(roleId)); - }; - - return ( -
-
-
-
- {widget.label} -
- - {admin && ( - - )} -
- -
-
- {widget.value ? ( - widget.widget_type === WidgetType.chart ? ( - widget.chart_type === WidgetChartType.bar ? ( - - ) : widget.chart_type === WidgetChartType.line ? ( - - ) : widget.chart_type === WidgetChartType.pie ? ( - - ) : widget.chart_type === WidgetChartType.area ? ( - - ) : widget.chart_type === WidgetChartType.funnel ? ( - - ) : null - ) : ( -
- {widget.value} -
- ) - ) : ( -
- Something went wrong, please try again or use a different query. -
- )} -
- - {widget.type === WidgetType.scalar && widget.mdiIcon && ( -
- -
- )} -
-
-
- ); -}; diff --git a/frontend/src/components/SmartWidget/components/AreaChart.tsx b/frontend/src/components/SmartWidget/components/AreaChart.tsx deleted file mode 100644 index 50bd5c7..0000000 --- a/frontend/src/components/SmartWidget/components/AreaChart.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { WidgetLibName } from '../models/widget.model'; -import { ApexAreaChart } from './AreaChart/ApexAreaChart'; -import { ChartJSAreaChart } from './AreaChart/ChartJSAreaChart'; - -export const AreaChart = ({ widget }) => { - return ( - <> - {!widget.lib_name && } - {widget.lib_name === WidgetLibName.chartjs && ( - - )} - {widget.lib_name === WidgetLibName.apex && ( - - )} - - ); -}; diff --git a/frontend/src/components/SmartWidget/components/AreaChart/ApexAreaChart.tsx b/frontend/src/components/SmartWidget/components/AreaChart/ApexAreaChart.tsx deleted file mode 100644 index d05d17e..0000000 --- a/frontend/src/components/SmartWidget/components/AreaChart/ApexAreaChart.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import React from 'react'; -import dynamic from 'next/dynamic'; -import { humanize } from '../../../../helpers/humanize'; -import type { - ChartComponentProps, - ChartValueArray, -} from '../../../../types/charts'; - -const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); -type ValueType = { [key: string]: string | number }[]; - -export const ApexAreaChart = ({ widget }: ChartComponentProps) => { - const dataForLineChart = (value: ChartValueArray) => { - if (!value?.length || value?.length > 10000) - return [{ name: '', data: [] }]; - - const valueKey = Object.keys(value[0])[1]; - const data = value.map((el) => +el[valueKey]); - - return [{ name: humanize(valueKey), data }]; - }; - - const optionsForLineChart = ( - value: ValueType, - chartColor: string[], - currency: boolean, - ) => { - const chartColors = Array.isArray(chartColor) - ? chartColor - : [chartColor || '#3751FF']; - const defaultOptions = { - xaxis: {}, - chart: { - toolbar: { - show: true, - offsetX: 0, - offsetY: 0, - tools: { - download: true, - selection: true, - zoom: true, - zoomin: true, - zoomout: true, - pan: true, - }, - export: { - csv: { - filename: undefined, - columnDelimiter: ',', - headerCategory: 'category', - headerValue: 'value', - }, - svg: { - filename: undefined, - }, - png: { - filename: undefined, - }, - }, - }, - }, - plotOptions: { - bar: { - distributed: false, - }, - }, - colors: [], - }; - - if (!value?.length || value?.length > 10000) return defaultOptions; - - const key = Object.keys(value[0])[0]; - const categories = value - .map((el) => el[key]) - .map((item) => - typeof item === 'string' && item?.length > 7 - ? item?.slice(0, 7) - : item || '', - ); - - if (categories.length <= 3) { - defaultOptions.plotOptions = { - bar: { - distributed: true, - }, - }; - } - - const colors = []; - for (let i = 0; i < categories.length; i++) { - colors.push(chartColors[i % chartColors.length]); - } - - return { - ...defaultOptions, - yaxis: { - labels: { - formatter: function (value) { - if (currency) { - return '$' + value; - } else { - return value; - } - }, - }, - }, - dataLabels: { - formatter: (val) => { - if (currency) { - return '$' + val; - } else { - return val; - } - }, - }, - legend: { - show: false, - }, - xaxis: { - categories, - }, - colors, - }; - }; - - return ( - - ); -}; diff --git a/frontend/src/components/SmartWidget/components/AreaChart/ChartJSAreaChart.tsx b/frontend/src/components/SmartWidget/components/AreaChart/ChartJSAreaChart.tsx deleted file mode 100644 index e6d0d0d..0000000 --- a/frontend/src/components/SmartWidget/components/AreaChart/ChartJSAreaChart.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React from 'react'; -import { Line } from 'react-chartjs-2'; -import chroma from 'chroma-js'; -import { humanize } from '../../../../helpers/humanize'; -import { collectOtherData, findFirstNumericKey } from '../../widgetHelpers'; -import type { - ChartComponentProps, - ChartValueArray, -} from '../../../../types/charts'; -import { - Chart as ChartJS, - CategoryScale, - LinearScale, - PointElement, - LineElement, - Title, - Tooltip, - Filler, - Legend, - ChartData, -} from 'chart.js'; - -ChartJS.register( - CategoryScale, - LinearScale, - PointElement, - LineElement, - Title, - Tooltip, - Filler, - Legend, -); - -export const ChartJSAreaChart = ({ widget }: ChartComponentProps) => { - const options = { - responsive: true, - maintainAspectRatio: false, - scales: { - y: { - display: true, - }, - x: { - display: true, - }, - }, - plugins: { - legend: { - display: true, - }, - }, - }; - - const dataForBarChart = ( - value: ChartValueArray, - chartColors: string[], - ): ChartData<'line', number[], string> => { - if (!value?.length) return { labels: [''], datasets: [{ data: [] }] }; - const initColors = Array.isArray(chartColors) - ? chartColors - : [chartColors || '#3751FF']; - - const valueKey = findFirstNumericKey(value[0]); - const label = humanize(valueKey); - const data = value.map((el) => +el[valueKey]); - const labels = value.map((el) => - Object.keys(el).length <= 2 - ? humanize(String(el[Object.keys(el)[0]])) - : collectOtherData(el, valueKey), - ); - - const backgroundColor = - labels.length > initColors.length - ? chroma - .scale([ - chroma(initColors[0]).brighten(), - chroma(initColors.slice(-1)[0]).darken(), - ]) - .colors(labels.length) - : initColors; - - return { - labels, - datasets: [ - { - label, - data, - backgroundColor, - }, - ], - }; - }; - - return ( - - ); -}; diff --git a/frontend/src/components/SmartWidget/components/BarChart.tsx b/frontend/src/components/SmartWidget/components/BarChart.tsx deleted file mode 100644 index bf7a79b..0000000 --- a/frontend/src/components/SmartWidget/components/BarChart.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { ChartJSBarChart } from './BarChart/ChartJSBarChart'; -import { ApexBarChart } from './BarChart/ApexBarChart'; -import { WidgetLibName } from '../models/widget.model'; - -export const BarChart = ({ widget }) => { - return ( - <> - {!widget.lib_name && } - {widget.lib_name === WidgetLibName.chartjs && ( - - )} - {widget.lib_name === WidgetLibName.apex && ( - - )} - - ); -}; diff --git a/frontend/src/components/SmartWidget/components/BarChart/ApexBarChart.tsx b/frontend/src/components/SmartWidget/components/BarChart/ApexBarChart.tsx deleted file mode 100644 index fa0efa7..0000000 --- a/frontend/src/components/SmartWidget/components/BarChart/ApexBarChart.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import React from 'react'; -import dynamic from 'next/dynamic'; -import { humanize } from '../../../../helpers/humanize'; -import type { - ChartComponentProps, - ChartValueArray, -} from '../../../../types/charts'; - -const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); -type ValueType = { [key: string]: string | number }[]; - -export const ApexBarChart = ({ widget }: ChartComponentProps) => { - const dataForBarChart = (value: ChartValueArray) => { - if (!value?.length || value?.length > 10000) - return [{ name: '', data: [] }]; - - const valueKey = Object.keys(value[0])[1]; - const data = value.map((el) => +el[valueKey]); - - return [{ name: humanize(valueKey), data }]; - }; - const optionsForBarChart = ( - value: ValueType, - chartColor: string[], - currency: boolean, - ) => { - const chartColors = Array.isArray(chartColor) - ? chartColor - : [chartColor || '#3751FF']; - const defaultOptions = { - xaxis: {}, - chart: { - toolbar: { - show: true, - offsetX: 0, - offsetY: 0, - tools: { - download: true, - selection: true, - zoom: true, - }, - export: { - csv: { - filename: undefined, - columnDelimiter: ',', - headerCategory: 'category', - headerValue: 'value', - }, - svg: { - filename: undefined, - }, - png: { - filename: undefined, - }, - }, - }, - }, - plotOptions: { - bar: { - distributed: false, - }, - }, - colors: [], - }; - - if (!value?.length || value?.length > 10000) return defaultOptions; - - const key = Object.keys(value[0])[0]; - const categories = value - .map((el) => el[key]) - .map((item) => - typeof item === 'string' && item?.length > 20 - ? item?.slice(0, 15) - : item || '', - ); - - if (categories.length <= 3) { - defaultOptions.plotOptions = { - bar: { - distributed: true, - }, - }; - } - - const colors = []; - for (let i = 0; i < categories.length; i++) { - colors.push(chartColors[i % chartColors.length]); - } - - return { - ...defaultOptions, - yaxis: { - labels: { - formatter: function (value) { - if (currency) { - return '$' + value; - } else { - return value; - } - }, - }, - }, - dataLabels: { - formatter: (val) => { - if (currency) { - return '$' + val; - } else { - return val; - } - }, - }, - legend: { - show: true, - }, - xaxis: { - categories, - }, - colors, - }; - }; - - return ( - - ); -}; diff --git a/frontend/src/components/SmartWidget/components/BarChart/ChartJSBarChart.tsx b/frontend/src/components/SmartWidget/components/BarChart/ChartJSBarChart.tsx deleted file mode 100644 index 9df0d4d..0000000 --- a/frontend/src/components/SmartWidget/components/BarChart/ChartJSBarChart.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; -import { humanize } from '../../../../helpers/humanize'; -import { Bar } from 'react-chartjs-2'; -import { collectOtherData, findFirstNumericKey } from '../../widgetHelpers'; -import { - BarElement, - CategoryScale, - Chart as ChartJS, - ChartData, - Legend, - LinearScale, - Title, - Tooltip, -} from 'chart.js'; -import chroma from 'chroma-js'; -import { logger } from '../../../../lib/logger'; -import type { - ChartComponentProps, - ChartValueArray, -} from '../../../../types/charts'; - -ChartJS.register( - CategoryScale, - LinearScale, - BarElement, - Title, - Tooltip, - Legend, -); - -export const ChartJSBarChart = ({ widget }: ChartComponentProps) => { - logger.debug('ChartJSBarChart widget:', { ...widget }); - const options = () => { - return { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: 'top' as const, - }, - title: { - display: true, - text: widget.label, - }, - }, - }; - }; - - const dataForBarChart = ( - value: ChartValueArray, - chartColors: string[], - ): ChartData<'bar', number[], string> => { - if (!value?.length) return { labels: [''], datasets: [{ data: [] }] }; - - const initColors = Array.isArray(chartColors) - ? chartColors - : [chartColors || '#3751FF']; - - const valueKey = findFirstNumericKey(value[0]); - const label = humanize(valueKey); - const data = value.map((el) => +el[valueKey]); - const labels = value.map((el) => - Object.keys(el).length <= 2 - ? humanize(String(el[Object.keys(el)[0]])) - : collectOtherData(el, valueKey), - ); - - const backgroundColor = - labels.length > initColors.length - ? chroma - .scale([ - chroma(initColors[0]).brighten(), - chroma(initColors.slice(-1)[0]).darken(), - ]) - .colors(labels.length) - : initColors; - - return { - labels, - datasets: [ - { - label, - data, - backgroundColor, - }, - ], - }; - }; - - return ( - - ); -}; diff --git a/frontend/src/components/SmartWidget/components/FunnelChart.tsx b/frontend/src/components/SmartWidget/components/FunnelChart.tsx deleted file mode 100644 index b47e6eb..0000000 --- a/frontend/src/components/SmartWidget/components/FunnelChart.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import React from 'react'; -import dynamic from 'next/dynamic'; -import { humanize } from '../../../helpers/humanize'; -import type { - ChartComponentProps, - ChartValueArray, -} from '../../../types/charts'; - -const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); -type ValueType = { [key: string]: string | number }[]; - -export const FunnelChart = ({ widget }: ChartComponentProps) => { - const dataForBarChart = (value: ChartValueArray) => { - if (!value?.length || value?.length > 10000) - return [{ name: '', data: [] }]; - const valueKey = Object.keys(value[0])[1]; - const data = value.map((el) => +el[valueKey]); - - return [{ name: humanize(valueKey), data }]; - }; - const optionsForBarChart = ( - value: ValueType, - chartColor: string[], - currency: boolean, - ) => { - const chartColors = Array.isArray(chartColor) - ? chartColor - : [chartColor || '#3751FF']; - const defaultOptions = { - xaxis: {}, - chart: { - toolbar: { - show: true, - offsetX: 0, - offsetY: 0, - tools: { - download: true, - selection: true, - zoom: true, - }, - export: { - csv: { - filename: undefined, - columnDelimiter: ',', - headerCategory: 'category', - headerValue: 'value', - }, - svg: { - filename: undefined, - }, - png: { - filename: undefined, - }, - }, - }, - }, - plotOptions: { - bar: { - distributed: false, - horizontal: true, - isFunnel: true, - }, - }, - colors: [], - }; - - if (!value?.length || value?.length > 10000) return defaultOptions; - - const key = Object.keys(value[0])[0]; - const categories = value - .map((el) => el[key]) - .map((item) => - typeof item === 'string' && item?.length > 20 - ? item?.slice(0, 15) - : item || '', - ); - - if (categories.length <= 3) { - defaultOptions.plotOptions = { - bar: { - distributed: true, - horizontal: true, - isFunnel: true, - }, - }; - } - - const colors = []; - for (let i = 0; i < categories.length; i++) { - colors.push(chartColors[i % chartColors.length]); - } - - return { - ...defaultOptions, - yaxis: { - labels: { - formatter: function (value) { - if (currency) { - return '$' + value; - } else { - return value; - } - }, - }, - }, - dataLabels: { - formatter: (val) => { - if (currency) { - return '$' + val; - } else { - return val; - } - }, - }, - legend: { - show: true, - }, - xaxis: { - categories, - }, - colors, - }; - }; - - return ( - - ); -}; diff --git a/frontend/src/components/SmartWidget/components/LineChart.tsx b/frontend/src/components/SmartWidget/components/LineChart.tsx deleted file mode 100644 index 3b9e7b0..0000000 --- a/frontend/src/components/SmartWidget/components/LineChart.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { WidgetLibName } from '../models/widget.model'; -import { ApexLineChart } from './LineChart/ApexLineChart'; -import { ChartJSLineChart } from './LineChart/ChartJSLineChart'; - -export const LineChart = ({ widget }) => { - return ( - <> - {!widget.lib_name && } - {widget.lib_name === WidgetLibName.chartjs && ( - - )} - {widget.lib_name === WidgetLibName.apex && ( - - )} - - ); -}; diff --git a/frontend/src/components/SmartWidget/components/LineChart/ApexLineChart.tsx b/frontend/src/components/SmartWidget/components/LineChart/ApexLineChart.tsx deleted file mode 100644 index 5aa5d43..0000000 --- a/frontend/src/components/SmartWidget/components/LineChart/ApexLineChart.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import React from 'react'; -import dynamic from 'next/dynamic'; -import { humanize } from '../../../../helpers/humanize'; -import type { - ChartComponentProps, - ChartValueArray, -} from '../../../../types/charts'; - -const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); -type ValueType = { [key: string]: string | number }[]; - -export const ApexLineChart = ({ widget }: ChartComponentProps) => { - const dataForLineChart = (value: ChartValueArray) => { - if (!value?.length || value?.length > 10000) - return [{ name: '', data: [] }]; - - const valueKey = Object.keys(value[0])[1]; - const data = value.map((el) => +el[valueKey]); - - return [{ name: humanize(valueKey), data }]; - }; - - const optionsForLineChart = ( - value: ValueType, - chartColor: string[], - currency: boolean, - ) => { - const chartColors = Array.isArray(chartColor) - ? chartColor - : [chartColor || '#3751FF']; - const defaultOptions = { - xaxis: {}, - chart: { - toolbar: { - show: true, - offsetX: 0, - offsetY: 0, - tools: { - download: true, - selection: true, - zoom: true, - zoomin: true, - zoomout: true, - pan: true, - }, - export: { - csv: { - filename: undefined, - columnDelimiter: ',', - headerCategory: 'category', - headerValue: 'value', - }, - svg: { - filename: undefined, - }, - png: { - filename: undefined, - }, - }, - }, - }, - plotOptions: { - bar: { - distributed: false, - }, - }, - colors: [], - }; - - if (!value?.length || value?.length > 10000) return defaultOptions; - - const key = Object.keys(value[0])[0]; - const categories = value - .map((el) => el[key]) - .map((item) => - typeof item === 'string' && item?.length > 7 - ? item?.slice(0, 7) - : item || '', - ); - - if (categories.length <= 3) { - defaultOptions.plotOptions = { - bar: { - distributed: true, - }, - }; - } - - const colors = []; - for (let i = 0; i < categories.length; i++) { - colors.push(chartColors[i % chartColors.length]); - } - - return { - ...defaultOptions, - yaxis: { - labels: { - formatter: function (value) { - if (currency) { - return '$' + value; - } else { - return value; - } - }, - }, - }, - dataLabels: { - formatter: (val) => { - if (currency) { - return '$' + val; - } else { - return val; - } - }, - }, - legend: { - show: false, - }, - xaxis: { - categories, - }, - colors, - }; - }; - - return ( - - ); -}; diff --git a/frontend/src/components/SmartWidget/components/LineChart/ChartJSLineChart.tsx b/frontend/src/components/SmartWidget/components/LineChart/ChartJSLineChart.tsx deleted file mode 100644 index cf7b552..0000000 --- a/frontend/src/components/SmartWidget/components/LineChart/ChartJSLineChart.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React from 'react'; -import { humanize } from '../../../../helpers/humanize'; -import { Line } from 'react-chartjs-2'; -import chroma from 'chroma-js'; -import { collectOtherData, findFirstNumericKey } from '../../widgetHelpers'; -import { - Chart, - LineElement, - PointElement, - LineController, - LinearScale, - CategoryScale, - Tooltip, - ChartData, -} from 'chart.js'; -import type { - ChartComponentProps, - ChartValueArray, -} from '../../../../types/charts'; - -Chart.register( - LineElement, - PointElement, - LineController, - LinearScale, - CategoryScale, - Tooltip, -); - -export const ChartJSLineChart = (props: ChartComponentProps) => { - const options = { - responsive: true, - maintainAspectRatio: false, - scales: { - y: { - display: true, - }, - x: { - display: true, - }, - }, - plugins: { - legend: { - display: true, - }, - }, - }; - - const dataForBarChart = ( - value: ChartValueArray, - chartColors: string[], - ): ChartData<'line', number[], string> => { - if (!value?.length) return { labels: [''], datasets: [{ data: [] }] }; - const initColors = Array.isArray(chartColors) - ? chartColors - : [chartColors || '#3751FF']; - - const valueKey = findFirstNumericKey(value[0]); - const label = humanize(valueKey); - const data = value.map((el) => +el[valueKey]); - const labels = value.map((el) => - Object.keys(el).length <= 2 - ? humanize(String(el[Object.keys(el)[0]])) - : collectOtherData(el, valueKey), - ); - - const backgroundColor = - labels.length > initColors.length - ? chroma - .scale([ - chroma(initColors[0]).brighten(), - chroma(initColors.slice(-1)[0]).darken(), - ]) - .colors(labels.length) - : initColors; - - return { - labels, - datasets: [ - { - label, - data, - backgroundColor, - }, - ], - }; - }; - - return ( - - ); -}; diff --git a/frontend/src/components/SmartWidget/components/PieChart.tsx b/frontend/src/components/SmartWidget/components/PieChart.tsx deleted file mode 100644 index f6f0fd3..0000000 --- a/frontend/src/components/SmartWidget/components/PieChart.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { WidgetLibName } from '../models/widget.model'; -import { ApexPieChart } from './PieChart/ApexPieChart'; -import { ChartJSPieChart } from './PieChart/ChartJSPieChart'; - -export const PieChart = ({ widget }) => { - return ( - <> - {!widget.lib_name && } - {widget.lib_name === WidgetLibName.chartjs && ( - - )} - {widget.lib_name === WidgetLibName.apex && ( - - )} - - ); -}; diff --git a/frontend/src/components/SmartWidget/components/PieChart/ApexPieChart.tsx b/frontend/src/components/SmartWidget/components/PieChart/ApexPieChart.tsx deleted file mode 100644 index a5f17be..0000000 --- a/frontend/src/components/SmartWidget/components/PieChart/ApexPieChart.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React from 'react'; -import dynamic from 'next/dynamic'; -import chroma from 'chroma-js'; -import type { - ChartComponentProps, - ChartValueArray, -} from '../../../../types/charts'; - -const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); -type ValueType = { [key: string]: string | number }[]; - -export const ApexPieChart = ({ widget }: ChartComponentProps) => { - const optionsForPieChart = ( - value: ValueType, - chartColor?: string | string[], - ) => { - const chartColors = Array.isArray(chartColor) - ? chartColor - : [chartColor || '#3751FF']; - const defaultOptions = { - xaxis: {}, - toolbar: { - show: true, - offsetX: 0, - offsetY: 0, - tools: { - download: true, - selection: true, - zoom: true, - zoomin: true, - zoomout: true, - pan: true, - customIcons: [], - }, - export: { - csv: { - filename: undefined, - columnDelimiter: ',', - headerCategory: 'category', - headerValue: 'value', - }, - svg: { - filename: undefined, - }, - png: { - filename: undefined, - }, - }, - autoSelected: 'zoom', - }, - colors: [], - }; - - if (!value?.length || value?.length > 10000) return defaultOptions; - - if ( - !isNaN(Number(value[0][Object.keys(value[0])[1]])) && - isFinite(Number(value[0][Object.keys(value[0])[1]])) - ) { - const labels = value - .map((el) => String(el[Object.keys(value[0])[0]])) - .reverse(); - - let colors: string[] | (string & any[]); - if (labels.length > chartColors.length) { - colors = chroma - .scale([ - chroma(chartColors.at(0)).brighten(), - chroma(chartColors.at(-1)).darken(), - ]) - .colors(labels.length); - } else { - colors = chartColors; - } - - return { - ...defaultOptions, - colors, - labels, - }; - } - const key = Object.keys(value[0])[1]; - const categories = value.map((el) => String(el[key])).reverse(); - - return { - ...defaultOptions, - labels: categories, - }; - }; - const dataForPieChart = (value: ChartValueArray) => { - if (!value?.length || value?.length > 10000) - return [{ name: '', data: [] }]; - - const secondKeyValue = value[0][Object.keys(value[0])[1]]; - if ( - !isNaN(parseFloat(String(secondKeyValue))) && - isFinite(Number(secondKeyValue)) - ) { - return value.map((el) => +el[Object.keys(value[0])[1]]).reverse(); - } - const valueKey = Object.keys(value[0])[0]; - return value.map((el) => +el[valueKey]).reverse(); - }; - - return ( - - ); -}; diff --git a/frontend/src/components/SmartWidget/components/PieChart/ChartJSPieChart.tsx b/frontend/src/components/SmartWidget/components/PieChart/ChartJSPieChart.tsx deleted file mode 100644 index 9f30a51..0000000 --- a/frontend/src/components/SmartWidget/components/PieChart/ChartJSPieChart.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react'; -import { humanize } from '../../../../helpers/humanize'; -import { Pie } from 'react-chartjs-2'; -import chroma from 'chroma-js'; -import { collectOtherData, findFirstNumericKey } from '../../widgetHelpers'; -import type { - ChartComponentProps, - ChartValueArray, -} from '../../../../types/charts'; - -import { - Chart as ChartJS, - ArcElement, - Tooltip, - Legend, - ChartData, -} from 'chart.js'; - -ChartJS.register(ArcElement, Tooltip, Legend); - -export const ChartJSPieChart = ({ widget }: ChartComponentProps) => { - const options = () => { - return { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: 'right' as const, - }, - title: { - display: true, - text: widget.label, - }, - }, - }; - }; - - const dataForBarChart = ( - value: ChartValueArray, - chartColors: string[], - ): ChartData<'pie', number[], string> => { - if (!value?.length) return { labels: [''], datasets: [{ data: [] }] }; - const initColors = Array.isArray(chartColors) - ? chartColors - : [chartColors || '#3751FF']; - - const valueKey = findFirstNumericKey(value[0]); - const label = humanize(valueKey); - const data = value.map((el) => +el[valueKey]); - const labels = value.map((el) => - Object.keys(el).length <= 2 - ? humanize(String(el[Object.keys(el)[0]])) - : collectOtherData(el, valueKey), - ); - - const backgroundColor = - labels.length > initColors.length - ? chroma - .scale([ - chroma(initColors[0]).brighten(), - chroma(initColors.slice(-1)[0]).darken(), - ]) - .colors(labels.length) - : initColors; - - return { - labels, - datasets: [ - { - label, - data, - backgroundColor, - }, - ], - }; - }; - - return ( - - ); -}; diff --git a/frontend/src/components/SmartWidget/models/widget.model.ts b/frontend/src/components/SmartWidget/models/widget.model.ts deleted file mode 100644 index d26b4b7..0000000 --- a/frontend/src/components/SmartWidget/models/widget.model.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { ChartValueArray } from '../../../types/charts'; - -export enum WidgetLibName { - apex = 'apex', - chartjs = 'chartjs', -} - -export enum WidgetChartType { - scalar = 'scalar', - bar = 'bar', - line = 'line', - pie = 'pie', - area = 'area', - funnel = 'funnel', -} - -export enum WidgetType { - chart = 'chart', - scalar = 'scalar', -} - -export interface Widget { - type: WidgetType; - chartType: WidgetChartType; - query: string; - mdiIcon: string; - iconColor: string; - label: string; - id: string; - lib?: WidgetLibName; - value: ChartValueArray; - chartColors: string[]; - options?: Record; - prompt: string; - color: string; - color_array: string[]; -} diff --git a/frontend/src/components/SmartWidget/widgetHelpers.tsx b/frontend/src/components/SmartWidget/widgetHelpers.tsx deleted file mode 100644 index 0bb6400..0000000 --- a/frontend/src/components/SmartWidget/widgetHelpers.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { humanize } from '../../helpers/humanize'; -import type { ChartDataPoint } from '../../types/charts'; - -export const findFirstNumericKey = ( - obj: ChartDataPoint, -): string | undefined => { - for (const [key, value] of Object.entries(obj)) { - if (typeof value === 'string') { - const trimmedValue = value.trim(); - - // Only allow numbers, and optionally a single decimal point - const isNumeric = /^-?\d+(\.\d+)?$/.test(trimmedValue); - - if (isNumeric) { - // Check if the number is the same as the trimmed value - // This is to avoid cases like '1.0' being treated as a number - const numberValue = parseFloat(trimmedValue); - if (numberValue.toString() === trimmedValue) { - return key; - } - } - } - } - return undefined; -}; - -export const collectOtherData = ( - obj: ChartDataPoint, - excludeKey: string, -): string => { - return Object.entries(obj) - .filter(([key, _]) => key !== excludeKey) - .map(([_, value]) => humanize(String(value))) - .join(' / '); -}; diff --git a/frontend/src/components/WidgetCreator/RoleSelect.tsx b/frontend/src/components/WidgetCreator/RoleSelect.tsx deleted file mode 100644 index 607dbd9..0000000 --- a/frontend/src/components/WidgetCreator/RoleSelect.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { useEffect, useId, useState } from 'react'; -import { AsyncPaginate } from 'react-select-async-paginate'; -import axios from 'axios'; - -export const RoleSelect = ({ - options, - field, - form, - itemRef, - disabled, - currentUser, -}) => { - const [value, setValue] = useState(null); - const PAGE_SIZE = 50; - - React.useEffect(() => { - if (currentUser.app_role.id) { - setValue({ - value: currentUser.app_role.id, - label: currentUser.app_role.name, - }); - } - }, [currentUser]); - - useEffect(() => { - if (options?.value && options?.label) { - setValue({ value: options.value, label: options.label }); - } - }, [options?.id, field?.value?.id]); - - const mapResponseToValuesAndLabels = (data) => ({ - value: data.id, - label: data.label, - }); - const handleChange = (option) => { - form.setFieldValue(field.name, option); - setValue(option); - }; - - async function callApi( - inputValue: string, - loadedOptions: Array<{ value: string; label: string }>, - ) { - const path = `/${itemRef}/autocomplete?limit=${PAGE_SIZE}&offset=${loadedOptions.length}${inputValue ? `&query=${inputValue}` : ''}`; - const { data } = await axios(path); - return { - options: data.map(mapResponseToValuesAndLabels), - hasMore: data.length === PAGE_SIZE, - }; - } - return ( - 'px-1 py-2', - }} - classNamePrefix={'react-select'} - instanceId={useId()} - value={value} - debounceTimeout={1000} - loadOptions={callApi} - onChange={handleChange} - defaultOptions - isDisabled={disabled} - /> - ); -}; diff --git a/frontend/src/components/WidgetCreator/WidgetCreator.tsx b/frontend/src/components/WidgetCreator/WidgetCreator.tsx deleted file mode 100644 index 33587af..0000000 --- a/frontend/src/components/WidgetCreator/WidgetCreator.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import CardBox from '../CardBox'; -import { mdiCog } from '@mdi/js'; -import { Field, Form, Formik } from 'formik'; -import { ToastContainer, toast } from 'react-toastify'; -import FormField from '../FormField'; -import React from 'react'; -import { - aiPrompt, - setErrorNotification, - resetNotify, -} from '../../stores/openAiSlice'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; - -import { fetchWidgets } from '../../stores/roles/rolesSlice'; - -import BaseButton from '../BaseButton'; -import CardBoxModal from '../CardBoxModal'; -import { RoleSelect } from './RoleSelect'; - -export const WidgetCreator = ({ - currentUser, - isFetchingQuery, - setWidgetsRole, - widgetsRole, -}) => { - const dispatch = useAppDispatch(); - const [isModalOpen, setIsModalOpen] = React.useState(false); - const { notify: openAiNotify } = useAppSelector((state) => state.openAi); - - const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); - React.useEffect(() => { - if (openAiNotify.showNotification) { - notify(openAiNotify.typeNotification, openAiNotify.textNotification); - dispatch(resetNotify()); - } - }, [openAiNotify.showNotification]); - - const openModal = (): void => { - setIsModalOpen(true); - }; - - const handleCloseModal = (value = {}) => { - setWidgetsRole(value); - setIsModalOpen(false); - }; - - const getWidgets = async () => { - await dispatch(fetchWidgets(widgetsRole?.role?.value || '')); - }; - - const smartSearch = async ( - values: { description: string }, - resetForm: (nextState?: { values: { description: string } }) => void, - ) => { - const description = values.description; - const projectId = ''; - - const payload = { - roleId: widgetsRole?.role?.value, - description, - projectId, - userId: currentUser?.id, - }; - const result = await dispatch(aiPrompt(payload)); - const responcePayload = result.payload as - | { data?: { error?: { message?: string } } } - | undefined; - const error = - 'error' in result - ? (result.error as { message?: string } | undefined) - : undefined; - - await getWidgets().then(); - - resetForm({ values: { description: '' } }); - if (responcePayload.data?.error || error) { - const errorMessage = - responcePayload.data?.error?.message || error?.message; - await dispatch( - setErrorNotification(errorMessage || 'Error with widget creation'), - ); - } - }; - - return ( - <> - - - smartSearch(values, resetForm)} - > -
- - - -
-
-
- handleCloseModal(values)} - > - {({ submitForm }) => ( - setIsModalOpen(false)} - > -

What role are we showing and creating widgets for?

- -
- - - -
-
- )} -
- - - ); -}; diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 906af83..0b8983f 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -4,7 +4,7 @@ export const hostApi = : ''; export const portApi = process.env.NODE_ENV === 'development' && !process.env.NEXT_PUBLIC_BACK_API - ? 8080 + ? 3000 : ''; export const baseURLApi = `${hostApi}${portApi ? `:${portApi}` : ``}/api`; diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index df33692..4640726 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -1,5 +1,4 @@ import React, { ReactNode, useEffect, useRef, useState } from 'react'; -import jwt from 'jsonwebtoken'; import axios from 'axios'; import { toast } from 'react-toastify'; import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'; @@ -15,6 +14,7 @@ import Search from '../components/Search'; import { useRouter } from 'next/router'; import { findMe, logoutUser } from '../stores/authSlice'; import { logger } from '../lib/logger'; +import { isAuthTokenValid } from '../lib/authToken'; import { hasPermission } from '../helpers/userPermissions'; @@ -60,30 +60,13 @@ export default function LayoutAuthenticated({ return sessionStorage.getItem('token') || localStorage.getItem('token'); }; - const isTokenValid = (tokenToCheck?: string | null) => { - if (!tokenToCheck) return false; - - try { - const decoded = jwt.decode(tokenToCheck); - if (!decoded || typeof decoded !== 'object') return false; - if (typeof decoded.exp !== 'number') return true; - return Date.now() / 1000 < decoded.exp; - } catch (error) { - logger.error( - 'Failed to decode auth token:', - error instanceof Error ? error : { error }, - ); - return false; - } - }; - useEffect(() => { if (!router.isReady) return; const storedToken = getStoredToken(); const authToken = token || storedToken; - if (!authToken || !isTokenValid(authToken)) { + if (!authToken || !isAuthTokenValid(authToken)) { dispatch(logoutUser()); if (router.pathname !== '/login') { router.replace('/login'); diff --git a/frontend/src/lib/authToken.ts b/frontend/src/lib/authToken.ts new file mode 100644 index 0000000..f84c4d3 --- /dev/null +++ b/frontend/src/lib/authToken.ts @@ -0,0 +1,34 @@ +import { jwtDecode } from 'jwt-decode'; +import type { FrontendAuthTokenPayload } from '../types/auth'; +import { logger } from './logger'; + +export function decodeAuthToken( + token: string, +): FrontendAuthTokenPayload | null { + try { + return jwtDecode(token); + } catch (error) { + logger.error( + 'Failed to decode auth token:', + error instanceof Error ? error : { error }, + ); + return null; + } +} + +export function isAuthTokenValid(token?: string | null): boolean { + if (!token) { + return false; + } + + const decoded = decodeAuthToken(token); + if (!decoded) { + return false; + } + + if (typeof decoded.exp !== 'number') { + return true; + } + + return Date.now() / 1000 < decoded.exp; +} diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index a28b5b9..b987881 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -10,16 +10,12 @@ import { getPageTitle } from '../config'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import { hasPermission } from '../helpers/userPermissions'; -import { fetchWidgets } from '../stores/roles/rolesSlice'; -import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator'; -import { SmartWidget } from '../components/SmartWidget/SmartWidget'; import { useDashboardCounts, EntityCountValue, } from '../hooks/useDashboardCounts'; -import { useAppDispatch, useAppSelector } from '../stores/hooks'; +import { useAppSelector } from '../stores/hooks'; /** * Dashboard card component for entity counts @@ -75,49 +71,15 @@ const DashboardCard = ({ }; const Dashboard = () => { - const dispatch = useAppDispatch(); const router = useRouter(); const iconsColor = useAppSelector((state) => state.style.iconsColor); const corners = useAppSelector((state) => state.style.corners); const cardsStyle = useAppSelector((state) => state.style.cardsStyle); const { currentUser } = useAppSelector((state) => state.auth); - const { isFetchingQuery } = useAppSelector((state) => state.openAi); // Use the centralized dashboard counts hook const { getCount, getVisibleEntities } = useDashboardCounts(currentUser); - const [widgetsRole, setWidgetsRole] = React.useState({ - role: { value: '', label: '' }, - }); - - const { rolesWidgets, loading } = useAppSelector((state) => state.roles) as { - rolesWidgets: Array<{ - id?: string; - widget_id?: string; - [key: string]: unknown; - }>; - loading: boolean; - }; - - async function getWidgets(roleId: string) { - await dispatch(fetchWidgets(roleId)); - } - - React.useEffect(() => { - if (!currentUser) return; - setWidgetsRole({ - role: { - value: currentUser?.app_role?.id, - label: currentUser?.app_role?.name, - }, - }); - }, [currentUser]); - - React.useEffect(() => { - if (!currentUser || !widgetsRole?.role?.value) return; - getWidgets(widgetsRole?.role?.value || '').then(); - }, [widgetsRole?.role?.value]); - // Get entities visible to current user const visibleEntities = getVisibleEntities(); @@ -143,51 +105,6 @@ const Dashboard = () => { {''} - {hasPermission(currentUser, 'CREATE_ROLES') && ( - - )} - {!!rolesWidgets.length && - hasPermission(currentUser, 'CREATE_ROLES') && ( -

- {`${widgetsRole?.role?.label || 'Users'}'s widgets`} -

- )} - -
- {(isFetchingQuery || loading) && ( -
- {' '} - Loading widgets... -
- )} - - {rolesWidgets && - rolesWidgets.map((widget, index) => ( - - ))} -
- - {!!rolesWidgets.length &&
} -
{ const token = action.payload; - const user = jwt.decode(token); + const user = decodeAuthToken(token); state.errorMessage = ''; state.isFetching = false; state.token = token; sessionStorage.setItem('token', token); - sessionStorage.setItem('user', JSON.stringify(user)); localStorage.setItem('token', token); - localStorage.setItem('user', JSON.stringify(user)); + if (user) { + sessionStorage.setItem('user', JSON.stringify(user)); + localStorage.setItem('user', JSON.stringify(user)); + } else { + sessionStorage.removeItem('user'); + localStorage.removeItem('user'); + } axios.defaults.headers.common['Authorization'] = 'Bearer ' + token; }); @@ -104,7 +108,7 @@ export const authSlice = createSlice({ state.isFetching = false; }); - builder.addCase(passwordReset.fulfilled, (state, action) => { + builder.addCase(passwordReset.fulfilled, (state) => { state.notify.showNotification = true; state.notify.textNotification = 'Password has been reset successfully'; }); diff --git a/frontend/src/stores/introSteps.ts b/frontend/src/stores/introSteps.ts index e650213..1a8a128 100644 --- a/frontend/src/stores/introSteps.ts +++ b/frontend/src/stores/introSteps.ts @@ -41,13 +41,6 @@ export const appSteps: IntroStep[] = [ position: 'auto', disableInteraction: true, }, - { - element: '#widgetCreator', - intro: - 'Use Text-to-Chart and Text-to-Widget to create charts or widgets from text descriptions. Type what you need, like "Orders by Month," and customize your dashboard.', - position: 'auto', - disableInteraction: true, - }, { element: '#dashboard', intro: diff --git a/frontend/src/stores/openAiSlice.ts b/frontend/src/stores/openAiSlice.ts deleted file mode 100644 index d5fa568..0000000 --- a/frontend/src/stores/openAiSlice.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; -import axios from 'axios'; -import type { - SmartWidget, - OpenAIState, - CreateWidgetRequest, - CreateWidgetResponse, - AIRequestPayload, -} from '../types/openai'; -const initialState: OpenAIState = { - isFetchingQuery: false, - errorMessage: '', - smartWidgets: [], - gptResponse: null, - aiResponse: null, - isAskingQuestion: false, - isAskingResponse: false, - notify: { - showNotification: false, - textNotification: '', - typeNotification: 'warn', - }, -}; - -type NotificationType = 'success' | 'error' | 'warn' | 'info' | ''; - -const fulfilledNotify = ( - state: OpenAIState, - msg: string, - type?: NotificationType, -) => { - state.notify.textNotification = msg; - state.notify.typeNotification = type || 'success'; - state.notify.showNotification = true; -}; - -export const aiPrompt = createAsyncThunk< - CreateWidgetResponse, - CreateWidgetRequest ->('openai/aiPrompt', async (data, { rejectWithValue }) => { - try { - const response = await axios.post( - '/openai/create_widget', - data, - ); - return response.data; - } catch (error) { - if (!error.response) { - throw error; - } - return rejectWithValue(error.response.data); - } -}); - -export const askGpt = createAsyncThunk( - 'openai/askGpt', - async (prompt: string, { rejectWithValue }) => { - try { - const response = await axios.post('/openai/ask-gpt', { prompt }); - return response.data; - } catch (error) { - if (!error.response) { - throw error; - } - return rejectWithValue(error.response.data); - } - }, -); - -export const aiResponse = createAsyncThunk( - 'openai/aiResponse', - async (payload: AIRequestPayload, { rejectWithValue }) => { - try { - const response = await axios.post('/ai/response', payload); - return response.data; - } catch (error) { - if (!error.response) { - throw error; - } - return rejectWithValue(error.response.data); - } - }, -); - -export const openAiSlice = createSlice({ - name: 'openAiSlice', - initialState, - reducers: { - resetNotify: (state) => { - state.notify.showNotification = false; - state.notify.typeNotification = ''; - state.notify.textNotification = ''; - }, - setErrorNotification: (state, action) => { - fulfilledNotify(state, action.payload, 'error'); - }, - }, - extraReducers: (builder) => { - builder.addCase(aiPrompt.pending, (state) => { - state.isFetchingQuery = true; - }); - builder.addCase(aiPrompt.fulfilled, (state, action) => { - state.isFetchingQuery = false; - state.errorMessage = ''; - if (action.payload?.data) { - state.smartWidgets.unshift(action.payload.data as SmartWidget); - } - }); - - builder.addCase(aiPrompt.rejected, (state) => { - state.errorMessage = 'Something was wrong. Try again'; - state.isFetchingQuery = false; - state.smartWidgets = []; - }); - - builder.addCase(askGpt.pending, (state) => { - state.isAskingQuestion = true; - state.gptResponse = null; - state.errorMessage = ''; - }); - builder.addCase(askGpt.fulfilled, (state, action) => { - state.isAskingQuestion = false; - state.gptResponse = action.payload.data; - state.errorMessage = ''; - }); - builder.addCase(askGpt.rejected, (state) => { - state.isAskingQuestion = false; - state.gptResponse = null; - state.errorMessage = - 'Failed to get response from ChatGPT. Please try again.'; - fulfilledNotify(state, 'Failed to get response from ChatGPT', 'error'); - }); - - builder.addCase(aiResponse.pending, (state) => { - state.isAskingResponse = true; - state.aiResponse = null; - state.errorMessage = ''; - }); - builder.addCase(aiResponse.fulfilled, (state, action) => { - state.isAskingResponse = false; - state.aiResponse = action.payload; - state.errorMessage = ''; - }); - builder.addCase(aiResponse.rejected, (state) => { - state.isAskingResponse = false; - state.aiResponse = null; - state.errorMessage = - 'Failed to get response from AI proxy. Please try again.'; - fulfilledNotify(state, 'Failed to get response from AI proxy', 'error'); - }); - }, -}); - -// Action creators are generated for each case reducer function -export const { resetNotify, setErrorNotification } = openAiSlice.actions; - -export default openAiSlice.reducer; diff --git a/frontend/src/stores/roles/rolesSlice.ts b/frontend/src/stores/roles/rolesSlice.ts index 284b01e..320df8f 100644 --- a/frontend/src/stores/roles/rolesSlice.ts +++ b/frontend/src/stores/roles/rolesSlice.ts @@ -1,86 +1,13 @@ -/** - * Roles Redux Slice - * - * Extends the base entity slice with custom widget management actions. - */ - -import { createAsyncThunk, createReducer } from '@reduxjs/toolkit'; -import axios from 'axios'; import { createEntitySlice } from '../createEntitySlice'; import type { Role } from '../../types/entities'; -import type { EntitySliceState } from '../../types/redux'; - -// Extended state type for roles with widget support -interface RolesSliceState extends EntitySliceState { - rolesWidgets: Array<{ id: string; [key: string]: unknown }>; -} // Create base entity slice with standard CRUD operations -const { - slice, - actions, - reducer: baseReducer, -} = createEntitySlice({ +const { slice, actions, reducer } = createEntitySlice({ name: 'roles', endpoint: 'roles', singularName: 'Role', }); -// Custom thunks specific to roles -export const removeWidget = createAsyncThunk( - 'openai/removeWidget', - async (payload: { id: string; roleId: string; widgetId: string }) => { - const result = await axios.delete(`openai/roles-info/${payload.id}`, { - params: { - roleId: payload.roleId, - infoId: payload.widgetId, - key: 'widgets', - }, - }); - return result.data; - }, -); - -export const fetchWidgets = createAsyncThunk( - 'openai/fetchWidgets', - async (roleId: string) => { - const result = await axios.get( - `openai/info-by-key?key=widgets&roleId=${roleId}`, - ); - return result.data; - }, -); - -// Initial state with widgets -const initialWidgetsState: { - rolesWidgets: Array<{ id: string; [key: string]: unknown }>; -} = { - rolesWidgets: [], -}; - -// Combined reducer that extends base reducer with widget handling -const combinedReducer = createReducer( - { - ...baseReducer(undefined, { type: '' }), - ...initialWidgetsState, - } as RolesSliceState, - (builder) => { - // Handle fetchWidgets.fulfilled - builder.addCase(fetchWidgets.fulfilled, (state, action) => { - state.rolesWidgets = action.payload || []; - }); - // Handle removeWidget.fulfilled - refresh will be triggered by component - builder.addCase(removeWidget.fulfilled, () => { - // Widget removed, component will refetch - }); - // Pass all other actions to base reducer - builder.addDefaultCase((state, action) => { - const baseResult = baseReducer(state, action); - return { ...baseResult, rolesWidgets: state.rolesWidgets }; - }); - }, -); - // Export standard CRUD actions export const { fetch, @@ -93,4 +20,4 @@ export const { } = actions; export const rolesSlice = slice; -export default combinedReducer; +export default reducer; diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts index 358703a..19503da 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -2,7 +2,6 @@ import { configureStore } from '@reduxjs/toolkit'; import styleReducer from './styleSlice'; import mainReducer from './mainSlice'; import authSlice from './authSlice'; -import openAiSlice from './openAiSlice'; import constructorReducer from './constructor/constructorSlice'; import usersSlice from './users/usersSlice'; @@ -28,7 +27,6 @@ export const store = configureStore({ style: styleReducer, main: mainReducer, auth: authSlice, - openAi: openAiSlice, constructorUI: constructorReducer, users: usersSlice, diff --git a/frontend/src/sw.ts b/frontend/src/sw.ts index 2cd4e00..675a5bf 100644 --- a/frontend/src/sw.ts +++ b/frontend/src/sw.ts @@ -112,7 +112,7 @@ const isAudioRequest = (request: Request): boolean => { * Extract storage path from various URL formats. * Handles: * - Presigned S3 URLs: https://s3.../bucket/assets/project/file.mp4?X-Amz-Signature=... - * - Backend proxy URLs: http://localhost:8080/api/file/download?privateUrl=assets%2F... + * - Backend proxy URLs: http://localhost:3000/api/file/download?privateUrl=assets%2F... * - Relative paths: assets/project/file.mp4 */ const extractStoragePathFromUrl = (url: string): string | null => { diff --git a/frontend/src/types/auth.ts b/frontend/src/types/auth.ts new file mode 100644 index 0000000..673af16 --- /dev/null +++ b/frontend/src/types/auth.ts @@ -0,0 +1,4 @@ +import type { JwtPayload } from 'jwt-decode'; +import type { User } from './entities'; + +export type FrontendAuthTokenPayload = JwtPayload & Partial; diff --git a/frontend/src/types/charts.ts b/frontend/src/types/charts.ts index 2c00aef..2b93198 100644 --- a/frontend/src/types/charts.ts +++ b/frontend/src/types/charts.ts @@ -1,5 +1,5 @@ /** - * Chart/Widget Data Types + * Chart Data Types */ /** @@ -35,18 +35,18 @@ export type ApexChartType = | 'treemap'; /** - * Widget result structure containing chart data + * Chart result structure containing chart data */ -export interface WidgetResult { +export interface ChartResult { value: ChartValueArray; label?: string; options?: Record; } /** - * Chart widget data structure used by SmartWidget components + * Chart data structure */ -export interface ChartWidget { +export interface ChartData { value: ChartValueArray; color_array?: string[]; currency?: boolean; @@ -58,5 +58,5 @@ export interface ChartWidget { * Props for chart components */ export interface ChartComponentProps { - widget: ChartWidget; + chart: ChartData; } diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 2259fba..9ef8e4f 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -15,7 +15,6 @@ export * from './constructor'; export * from './presentation'; export * from './menu'; export * from './ui'; -export * from './openai'; export * from './components'; export * from './charts'; export * from './transition'; diff --git a/frontend/src/types/openai.ts b/frontend/src/types/openai.ts deleted file mode 100644 index 3dbc57f..0000000 --- a/frontend/src/types/openai.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * OpenAI/AI Feature Types - */ - -import type { NotificationState } from './redux'; - -/** - * Smart widget data structure for AI-generated visualizations - */ -export interface SmartWidget { - id: string; - name: string; - description?: string; - query?: string; - result?: Record; -} - -/** - * AI response data structure - */ -export interface AIResponseData { - content: string; - role: 'assistant' | 'user' | 'system'; - model?: string; -} - -/** - * OpenAI Redux slice state - */ -export interface OpenAIState { - isFetchingQuery: boolean; - errorMessage: string; - smartWidgets: SmartWidget[]; - gptResponse: string | null; - aiResponse: AIResponseData | null; - isAskingQuestion: boolean; - isAskingResponse: boolean; - notify: NotificationState; -} - -/** - * Request payload for creating a smart widget - */ -export interface CreateWidgetRequest { - description: string; - roleId?: string; - projectId?: string; - userId?: string; -} - -/** - * Response from widget creation endpoint - */ -export interface CreateWidgetResponse { - data?: SmartWidget; - error?: { message: string }; -} - -/** - * Single AI message in a conversation - */ -export interface AIMessage { - role: 'assistant' | 'user' | 'system'; - content: string; -} - -/** - * Request payload for AI response endpoint - */ -export interface AIRequestPayload { - input: AIMessage[]; - options?: Record; -} diff --git a/frontend/yarn.lock b/frontend/yarn.lock deleted file mode 100644 index 1ccbb15..0000000 --- a/frontend/yarn.lock +++ /dev/null @@ -1,4515 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@alloc/quick-lru@^5.2.0": - version "5.2.0" - resolved "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz" - integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== - -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.2": - version "7.26.2" - resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz" - integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== - dependencies: - "@babel/helper-validator-identifier" "^7.25.9" - js-tokens "^4.0.0" - picocolors "^1.0.0" - -"@babel/generator@^7.26.3": - version "7.26.3" - resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz" - integrity sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ== - dependencies: - "@babel/parser" "^7.26.3" - "@babel/types" "^7.26.3" - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.25" - jsesc "^3.0.2" - -"@babel/helper-module-imports@^7.16.7": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz" - integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== - dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" - -"@babel/helper-string-parser@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz" - integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== - -"@babel/helper-validator-identifier@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz" - integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== - -"@babel/parser@^7.25.9", "@babel/parser@^7.26.3": - version "7.26.3" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz" - integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA== - dependencies: - "@babel/types" "^7.26.3" - -"@babel/runtime-corejs3@^7.10.2": - version "7.19.0" - resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.19.0.tgz" - integrity sha512-JyXXoCu1N8GLuKc2ii8y5RGma5FMpFeO2nAQIe0Yzrbq+rQnN+sFj47auLblR5ka6aHNGPDgv8G/iI2Grb0ldQ== - dependencies: - core-js-pure "^3.20.2" - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.9", "@babel/runtime@^7.20.7", "@babel/runtime@^7.23.2", "@babel/runtime@^7.25.7", "@babel/runtime@^7.26.0", "@babel/runtime@^7.27.6", "@babel/runtime@^7.29.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.29.2" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz" - integrity sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g== - -"@babel/template@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz" - integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg== - dependencies: - "@babel/code-frame" "^7.25.9" - "@babel/parser" "^7.25.9" - "@babel/types" "^7.25.9" - -"@babel/traverse@^7.25.9": - version "7.26.4" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz" - integrity sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w== - dependencies: - "@babel/code-frame" "^7.26.2" - "@babel/generator" "^7.26.3" - "@babel/parser" "^7.26.3" - "@babel/template" "^7.25.9" - "@babel/types" "^7.26.3" - debug "^4.3.1" - globals "^11.1.0" - -"@babel/types@^7.25.9", "@babel/types@^7.26.3": - version "7.26.3" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz" - integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA== - dependencies: - "@babel/helper-string-parser" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" - -"@emotion/babel-plugin@^11.13.5": - version "11.13.5" - resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz" - integrity sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ== - dependencies: - "@babel/helper-module-imports" "^7.16.7" - "@babel/runtime" "^7.18.3" - "@emotion/hash" "^0.9.2" - "@emotion/memoize" "^0.9.0" - "@emotion/serialize" "^1.3.3" - babel-plugin-macros "^3.1.0" - convert-source-map "^1.5.0" - escape-string-regexp "^4.0.0" - find-root "^1.1.0" - source-map "^0.5.7" - stylis "4.2.0" - -"@emotion/cache@^11.13.5", "@emotion/cache@^11.14.0", "@emotion/cache@^11.4.0": - version "11.14.0" - resolved "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz" - integrity sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA== - dependencies: - "@emotion/memoize" "^0.9.0" - "@emotion/sheet" "^1.4.0" - "@emotion/utils" "^1.4.2" - "@emotion/weak-memoize" "^0.4.0" - stylis "4.2.0" - -"@emotion/hash@^0.9.2": - version "0.9.2" - resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz" - integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g== - -"@emotion/is-prop-valid@^1.3.0": - version "1.3.1" - resolved "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz" - integrity sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw== - dependencies: - "@emotion/memoize" "^0.9.0" - -"@emotion/memoize@^0.9.0": - version "0.9.0" - resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz" - integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== - -"@emotion/react@^11.0.0-rc.0", "@emotion/react@^11.11.3", "@emotion/react@^11.4.1", "@emotion/react@^11.5.0", "@emotion/react@^11.8.1", "@emotion/react@^11.9.0": - version "11.14.0" - resolved "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz" - integrity sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA== - dependencies: - "@babel/runtime" "^7.18.3" - "@emotion/babel-plugin" "^11.13.5" - "@emotion/cache" "^11.14.0" - "@emotion/serialize" "^1.3.3" - "@emotion/use-insertion-effect-with-fallbacks" "^1.2.0" - "@emotion/utils" "^1.4.2" - "@emotion/weak-memoize" "^0.4.0" - hoist-non-react-statics "^3.3.1" - -"@emotion/serialize@^1.3.3": - version "1.3.3" - resolved "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz" - integrity sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA== - dependencies: - "@emotion/hash" "^0.9.2" - "@emotion/memoize" "^0.9.0" - "@emotion/unitless" "^0.10.0" - "@emotion/utils" "^1.4.2" - csstype "^3.0.2" - -"@emotion/sheet@^1.4.0": - version "1.4.0" - resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz" - integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== - -"@emotion/styled@^11.11.0", "@emotion/styled@^11.3.0", "@emotion/styled@^11.8.1": - version "11.14.0" - resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz" - integrity sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA== - dependencies: - "@babel/runtime" "^7.18.3" - "@emotion/babel-plugin" "^11.13.5" - "@emotion/is-prop-valid" "^1.3.0" - "@emotion/serialize" "^1.3.3" - "@emotion/use-insertion-effect-with-fallbacks" "^1.2.0" - "@emotion/utils" "^1.4.2" - -"@emotion/unitless@^0.10.0": - version "0.10.0" - resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz" - integrity sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg== - -"@emotion/use-insertion-effect-with-fallbacks@^1.2.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz" - integrity sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg== - -"@emotion/utils@^1.4.2": - version "1.4.2" - resolved "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz" - integrity sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA== - -"@emotion/weak-memoize@^0.4.0": - version "0.4.0" - resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz" - integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== - -"@eslint/eslintrc@^1.3.2": - version "1.3.2" - resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz" - integrity sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^9.4.0" - globals "^13.15.0" - ignore "^5.2.0" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.1.2" - strip-json-comments "^3.1.1" - -"@floating-ui/core@^1.7.5": - version "1.7.5" - resolved "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz" - integrity sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ== - dependencies: - "@floating-ui/utils" "^0.2.11" - -"@floating-ui/dom@^1.0.1", "@floating-ui/dom@^1.7.6": - version "1.7.6" - resolved "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz" - integrity sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ== - dependencies: - "@floating-ui/core" "^1.7.5" - "@floating-ui/utils" "^0.2.11" - -"@floating-ui/react-dom@^2.1.8": - version "2.1.8" - resolved "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz" - integrity sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A== - dependencies: - "@floating-ui/dom" "^1.7.6" - -"@floating-ui/react@^0.27.0": - version "0.27.19" - resolved "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.19.tgz" - integrity sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog== - dependencies: - "@floating-ui/react-dom" "^2.1.8" - "@floating-ui/utils" "^0.2.11" - tabbable "^6.0.0" - -"@floating-ui/utils@^0.2.11": - version "0.2.11" - resolved "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz" - integrity sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg== - -"@fontsource-variable/instrument-sans@^5.2.8": - version "5.2.8" - resolved "https://registry.npmjs.org/@fontsource-variable/instrument-sans/-/instrument-sans-5.2.8.tgz" - integrity sha512-mTCaukbdIjjoipj2E3Q5XoZM3ZxJWdzyHevf/LG/0PHlfF9Q85pxOM7B7A9MerFyxmRzz5kVlumgIvgDSG4CPg== - -"@humanwhocodes/config-array@^0.10.4": - version "0.10.4" - resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz" - integrity sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw== - dependencies: - "@humanwhocodes/object-schema" "^1.2.1" - debug "^4.1.1" - minimatch "^3.0.4" - -"@humanwhocodes/gitignore-to-minimatch@^1.0.2": - version "1.0.2" - resolved "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz" - integrity sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA== - -"@humanwhocodes/module-importer@^1.0.1": - version "1.0.1" - resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" - integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== - -"@humanwhocodes/object-schema@^1.2.1": - version "1.2.1" - resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== - -"@img/colour@^1.0.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz" - integrity sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ== - -"@img/sharp-darwin-arm64@0.34.5": - version "0.34.5" - resolved "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz" - integrity sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w== - optionalDependencies: - "@img/sharp-libvips-darwin-arm64" "1.2.4" - -"@img/sharp-libvips-darwin-arm64@1.2.4": - version "1.2.4" - resolved "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz" - integrity sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g== - -"@isaacs/cliui@^8.0.2": - version "8.0.2" - resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" - integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== - dependencies: - string-width "^5.1.2" - string-width-cjs "npm:string-width@^4.2.0" - strip-ansi "^7.0.1" - strip-ansi-cjs "npm:strip-ansi@^6.0.1" - wrap-ansi "^8.1.0" - wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" - -"@jridgewell/gen-mapping@^0.3.2", "@jridgewell/gen-mapping@^0.3.5": - version "0.3.8" - resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz" - integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA== - dependencies: - "@jridgewell/set-array" "^1.2.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.24" - -"@jridgewell/resolve-uri@^3.1.0": - version "3.1.2" - resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" - integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== - -"@jridgewell/set-array@^1.2.1": - version "1.2.1" - resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz" - integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== - -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": - version "1.5.0" - resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz" - integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== - -"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": - version "0.3.25" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz" - integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" - -"@kurkle/color@^0.3.0": - version "0.3.4" - resolved "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz" - integrity sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w== - -"@mdi/js@^7.4.47": - version "7.4.47" - resolved "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz" - integrity sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ== - -"@mdi/react@^1.6.1": - version "1.6.1" - resolved "https://registry.npmjs.org/@mdi/react/-/react-1.6.1.tgz" - integrity sha512-4qZeDcluDFGFTWkHs86VOlHkm6gnKaMql13/gpIcUQ8kzxHgpj31NuCkD8abECVfbULJ3shc7Yt4HJ6Wu6SN4w== - dependencies: - prop-types "^15.7.2" - -"@mui/core-downloads-tracker@^6.5.0": - version "6.5.0" - resolved "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.5.0.tgz" - integrity sha512-LGb8t8i6M2ZtS3Drn3GbTI1DVhDY6FJ9crEey2lZ0aN2EMZo8IZBZj9wRf4vqbZHaWjsYgtbOnJw5V8UWbmK2Q== - -"@mui/material@^5.15.14 || ^6.0.0 || ^7.0.0", "@mui/material@^6.3.0": - version "6.5.0" - resolved "https://registry.npmjs.org/@mui/material/-/material-6.5.0.tgz" - integrity sha512-yjvtXoFcrPLGtgKRxFaH6OQPtcLPhkloC0BML6rBG5UeldR0nPULR/2E2BfXdo5JNV7j7lOzrrLX2Qf/iSidow== - dependencies: - "@babel/runtime" "^7.26.0" - "@mui/core-downloads-tracker" "^6.5.0" - "@mui/system" "^6.5.0" - "@mui/types" "~7.2.24" - "@mui/utils" "^6.4.9" - "@popperjs/core" "^2.11.8" - "@types/react-transition-group" "^4.4.12" - clsx "^2.1.1" - csstype "^3.1.3" - prop-types "^15.8.1" - react-is "^19.0.0" - react-transition-group "^4.4.5" - -"@mui/private-theming@^6.4.9": - version "6.4.9" - resolved "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.9.tgz" - integrity sha512-LktcVmI5X17/Q5SkwjCcdOLBzt1hXuc14jYa7NPShog0GBDCDvKtcnP0V7a2s6EiVRlv7BzbWEJzH6+l/zaCxw== - dependencies: - "@babel/runtime" "^7.26.0" - "@mui/utils" "^6.4.9" - prop-types "^15.8.1" - -"@mui/styled-engine@^6.5.0": - version "6.5.0" - resolved "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.5.0.tgz" - integrity sha512-8woC2zAqF4qUDSPIBZ8v3sakj+WgweolpyM/FXf8jAx6FMls+IE4Y8VDZc+zS805J7PRz31vz73n2SovKGaYgw== - dependencies: - "@babel/runtime" "^7.26.0" - "@emotion/cache" "^11.13.5" - "@emotion/serialize" "^1.3.3" - "@emotion/sheet" "^1.4.0" - csstype "^3.1.3" - prop-types "^15.8.1" - -"@mui/system@^5.15.14 || ^6.0.0 || ^7.0.0", "@mui/system@^6.5.0": - version "6.5.0" - resolved "https://registry.npmjs.org/@mui/system/-/system-6.5.0.tgz" - integrity sha512-XcbBYxDS+h/lgsoGe78ExXFZXtuIlSBpn/KsZq8PtZcIkUNJInkuDqcLd2rVBQrDC1u+rvVovdaWPf2FHKJf3w== - dependencies: - "@babel/runtime" "^7.26.0" - "@mui/private-theming" "^6.4.9" - "@mui/styled-engine" "^6.5.0" - "@mui/types" "~7.2.24" - "@mui/utils" "^6.4.9" - clsx "^2.1.1" - csstype "^3.1.3" - prop-types "^15.8.1" - -"@mui/types@~7.2.24": - version "7.2.24" - resolved "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz" - integrity sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw== - -"@mui/utils@^5.16.6 || ^6.0.0 || ^7.0.0", "@mui/utils@^6.4.9": - version "6.4.9" - resolved "https://registry.npmjs.org/@mui/utils/-/utils-6.4.9.tgz" - integrity sha512-Y12Q9hbK9g+ZY0T3Rxrx9m2m10gaphDuUMgWxyV5kNJevVxXYCLclYUCC9vXaIk1/NdNDTcW2Yfr2OGvNFNmHg== - dependencies: - "@babel/runtime" "^7.26.0" - "@mui/types" "~7.2.24" - "@types/prop-types" "^15.7.14" - clsx "^2.1.1" - prop-types "^15.8.1" - react-is "^19.0.0" - -"@mui/x-data-grid@^7.0.0": - version "7.29.13" - resolved "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.29.13.tgz" - integrity sha512-XHrZTvpa61eqSgUIevDzXYfYCbI7cbN/aRxSCaw2cZzQZ/USdpECYbnTEhgw5XgUlvkAK0mItKZmgKM4X7zT+Q== - dependencies: - "@babel/runtime" "^7.25.7" - "@mui/utils" "^5.16.6 || ^6.0.0 || ^7.0.0" - "@mui/x-internals" "7.29.0" - clsx "^2.1.1" - prop-types "^15.8.1" - reselect "^5.1.1" - use-sync-external-store "^1.0.0" - -"@mui/x-internals@7.29.0": - version "7.29.0" - resolved "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.29.0.tgz" - integrity sha512-+Gk6VTZIFD70XreWvdXBwKd8GZ2FlSCuecQFzm6znwqXg1ZsndavrhG9tkxpxo2fM1Zf7Tk8+HcOO0hCbhTQFA== - dependencies: - "@babel/runtime" "^7.25.7" - "@mui/utils" "^5.16.6 || ^6.0.0 || ^7.0.0" - -"@next/env@15.5.18": - version "15.5.18" - resolved "https://registry.npmjs.org/@next/env/-/env-15.5.18.tgz" - integrity sha512-hAV85Ckd9QR6RvH04MEKwsfLTksvFpO47j9xwtoIuvuPnlwecpSi+uZTtm8HirVbtlI2Fnz//xpcSTjFdyJk+g== - -"@next/eslint-plugin-next@13.0.4": - version "13.0.4" - resolved "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.0.4.tgz" - integrity sha512-jZ4urKT+aO9QHm3ttihrIQscQISDSKK8isAom750+EySn9o3LCSkTdPGBtvBqY7Yku+NqhfQempR5J58DqTaVg== - dependencies: - glob "7.1.7" - -"@next/swc-darwin-arm64@15.5.18": - version "15.5.18" - resolved "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.18.tgz" - integrity sha512-w0WvQf1n+txiwns/9pwIQteCJpZTbxzO2SE0FLcwuD4v0WEh1JPOjdyxWL21XwJsdpx8cFRjyzxzCS/siP7HcQ== - -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": - version "2.0.5" - resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@nolyfill/is-core-module@1.0.39": - version "1.0.39" - resolved "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz" - integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA== - -"@pkgjs/parseargs@^0.11.0": - version "0.11.0" - resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" - integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== - -"@popperjs/core@^2.11.6", "@popperjs/core@^2.11.8": - version "2.11.8" - resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz" - integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== - -"@react-dnd/asap@^5.0.1": - version "5.0.2" - resolved "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz" - integrity sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A== - -"@react-dnd/invariant@^4.0.1": - version "4.0.2" - resolved "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz" - integrity sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw== - -"@react-dnd/shallowequal@^4.0.1": - version "4.0.2" - resolved "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz" - integrity sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA== - -"@reduxjs/toolkit@^2.1.0": - version "2.5.0" - resolved "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz" - integrity sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg== - dependencies: - immer "^10.0.3" - redux "^5.0.1" - redux-thunk "^3.1.0" - reselect "^5.1.0" - -"@restart/hooks@^0.4.7": - version "0.4.16" - resolved "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz" - integrity sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w== - dependencies: - dequal "^2.0.3" - -"@rtsao/scc@^1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz" - integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== - -"@rushstack/eslint-patch@^1.1.3": - version "1.1.4" - resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.4.tgz" - integrity sha512-LwzQKA4vzIct1zNZzBmRKI9QuNpLgTQMEjsQLf3BXuGYb3QPTP4Yjf6mkdX+X1mYttZ808QpOwAzZjv28kq7DA== - -"@serwist/build@9.5.11": - version "9.5.11" - resolved "https://registry.npmjs.org/@serwist/build/-/build-9.5.11.tgz" - integrity sha512-PQfW+LhADYFOOp0PhEnjlgJCyKor6cYa06d3rID1OpiKzkmCApJV1WYfdTBB96jXaWv6OWcWSbSV4tqDLxvaVA== - dependencies: - "@serwist/utils" "9.5.11" - common-tags "1.8.2" - glob "13.0.6" - pretty-bytes "6.1.1" - source-map "0.8.0-beta.0" - type-fest "5.6.0" - zod "4.4.1" - -"@serwist/next@^9.5.7": - version "9.5.11" - resolved "https://registry.npmjs.org/@serwist/next/-/next-9.5.11.tgz" - integrity sha512-omT32H7U21ihCymSvOG9QeRJBuOEomJx4JdzKhUoqOW3DR10tH3m84VOHj3BvK0OcA7av3qj5FsyNFBB+f0n8A== - dependencies: - "@serwist/build" "9.5.11" - "@serwist/utils" "9.5.11" - "@serwist/webpack-plugin" "9.5.11" - "@serwist/window" "9.5.11" - browserslist "4.28.2" - glob "13.0.6" - kolorist "1.8.0" - semver "7.7.4" - serwist "9.5.11" - zod "4.4.1" - -"@serwist/utils@9.5.11": - version "9.5.11" - resolved "https://registry.npmjs.org/@serwist/utils/-/utils-9.5.11.tgz" - integrity sha512-zqxmwuHqWA3OwN82Wo8gFZ9QBemygJP3cap5JWAOG4UyJZgUZfmBXAXj+IMaD4eKZ/6pqrxHHDZ9uSWZmJ1mXA== - -"@serwist/webpack-plugin@9.5.11": - version "9.5.11" - resolved "https://registry.npmjs.org/@serwist/webpack-plugin/-/webpack-plugin-9.5.11.tgz" - integrity sha512-SlvO3A1UMcc1htCzMtLCtPQK6yISCO7B859ixLv7EiY/yayXjVxGm9vHqkJYpQ768PWyjEZXRY/X6EGRMA6wJQ== - dependencies: - "@serwist/build" "9.5.11" - "@serwist/utils" "9.5.11" - pretty-bytes "6.1.1" - zod "4.4.1" - -"@serwist/window@9.5.11": - version "9.5.11" - resolved "https://registry.npmjs.org/@serwist/window/-/window-9.5.11.tgz" - integrity sha512-OrH9srhmifUvY36NuukHSZby24XTEk4pHh3pfY0GBQzA9ouU1fYh+ORWhKxH7/wkVHRr3sc4YAhjtpfL14PjjQ== - dependencies: - "@types/trusted-types" "2.0.7" - serwist "9.5.11" - -"@seznam/compose-react-refs@^1.0.6": - version "1.0.6" - resolved "https://registry.npmjs.org/@seznam/compose-react-refs/-/compose-react-refs-1.0.6.tgz" - integrity sha512-izzOXQfeQLonzrIQb8u6LQ8dk+ymz3WXTIXjvOlTXHq6sbzROg3NWU+9TTAOpEoK9Bth24/6F/XrfHJ5yR5n6Q== - -"@swc/helpers@0.5.15": - version "0.5.15" - resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz" - integrity sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g== - dependencies: - tslib "^2.8.0" - -"@tailwindcss/forms@^0.5.7": - version "0.5.9" - resolved "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.9.tgz" - integrity sha512-tM4XVr2+UVTxXJzey9Twx48c1gcxFStqn1pQz0tRsX8o3DvxhN5oY5pvyAbUx7VTaZxpej4Zzvc6h+1RJBzpIg== - dependencies: - mini-svg-data-uri "^1.2.3" - -"@tailwindcss/line-clamp@^0.4.4": - version "0.4.4" - resolved "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.4.tgz" - integrity sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g== - -"@tailwindcss/typography@^0.5.13": - version "0.5.15" - resolved "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.15.tgz" - integrity sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA== - dependencies: - lodash.castarray "^4.4.0" - lodash.isplainobject "^4.0.6" - lodash.merge "^4.6.2" - postcss-selector-parser "6.0.10" - -"@tanstack/query-core@5.100.9": - version "5.100.9" - resolved "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.9.tgz" - integrity sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ== - -"@tanstack/react-query@^5.96.2": - version "5.100.9" - resolved "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.9.tgz" - integrity sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A== - dependencies: - "@tanstack/query-core" "5.100.9" - -"@types/date-arithmetic@*": - version "4.1.4" - resolved "https://registry.npmjs.org/@types/date-arithmetic/-/date-arithmetic-4.1.4.tgz" - integrity sha512-p9eZ2X9B80iKiTW4ukVj8B4K6q9/+xFtQ5MGYA5HWToY9nL4EkhV9+6ftT2VHpVMEZb5Tv00Iel516bVdO+yRw== - -"@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1", "@types/hoist-non-react-statics@^3.3.6", "@types/hoist-non-react-statics@>= 3.3.1": - version "3.3.7" - resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz" - integrity sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g== - dependencies: - hoist-non-react-statics "^3.3.0" - -"@types/json-schema@^7.0.9": - version "7.0.11" - resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" - integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== - -"@types/json5@^0.0.29": - version "0.0.29" - resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" - integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== - -"@types/node@>= 12", "@types/node@18.7.16": - version "18.7.16" - resolved "https://registry.npmjs.org/@types/node/-/node-18.7.16.tgz" - integrity sha512-EQHhixfu+mkqHMZl1R2Ovuvn47PUw18azMJOTwSZr9/fhzHNGXAJ0ma0dayRVchprpCj0Kc1K1xKoWaATWF1qg== - -"@types/numeral@^2.0.2": - version "2.0.2" - resolved "https://registry.npmjs.org/@types/numeral/-/numeral-2.0.2.tgz" - integrity sha512-A8F30k2gYJ/6e07spSCPpkuZu79LCnkPTvqmIWQzNGcrzwFKpVOydG41lNt5wZXjSI149qjyzC2L1+F2PD/NUA== - -"@types/parse-json@^4.0.0": - version "4.0.2" - resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz" - integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== - -"@types/prop-types@*", "@types/prop-types@^15.7.14": - version "15.7.15" - resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz" - integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw== - -"@types/react-big-calendar@^1.8.8": - version "1.16.0" - resolved "https://registry.npmjs.org/@types/react-big-calendar/-/react-big-calendar-1.16.0.tgz" - integrity sha512-1w2GXAJWlGmaPZOd9J9cyWA/XBNOGRZ4MmRNypEQhwEMIIL9cfd1UdcvzSrQsnBm0qYF/scqmsISNbUzPBE1vg== - dependencies: - "@types/date-arithmetic" "*" - "@types/prop-types" "*" - "@types/react" "*" - -"@types/react-redux@^7.1.24": - version "7.1.24" - resolved "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.24.tgz" - integrity sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ== - dependencies: - "@types/hoist-non-react-statics" "^3.3.0" - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - redux "^4.0.0" - -"@types/react-transition-group@^4.4.0", "@types/react-transition-group@^4.4.12": - version "4.4.12" - resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz" - integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== - -"@types/react@*", "@types/react@^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react@^18.2.25 || ^19", "@types/react@>= 16", "@types/react@>=16.9.11": - version "19.2.14" - resolved "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz" - integrity sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w== - dependencies: - csstype "^3.2.2" - -"@types/trusted-types@2.0.7": - version "2.0.7" - resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz" - integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== - -"@types/use-sync-external-store@^0.0.6": - version "0.0.6" - resolved "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz" - integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg== - -"@types/warning@^3.0.0": - version "3.0.3" - resolved "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz" - integrity sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q== - -"@typescript-eslint/eslint-plugin@^5.37.0": - version "5.37.0" - resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.37.0.tgz" - integrity sha512-Fde6W0IafXktz1UlnhGkrrmnnGpAo1kyX7dnyHHVrmwJOn72Oqm3eYtddrpOwwel2W8PAK9F3pIL5S+lfoM0og== - dependencies: - "@typescript-eslint/scope-manager" "5.37.0" - "@typescript-eslint/type-utils" "5.37.0" - "@typescript-eslint/utils" "5.37.0" - debug "^4.3.4" - functional-red-black-tree "^1.0.1" - ignore "^5.2.0" - regexpp "^3.2.0" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/parser@^5.0.0", "@typescript-eslint/parser@^5.37.0", "@typescript-eslint/parser@^5.42.0": - version "5.43.0" - resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.43.0.tgz" - integrity sha512-2iHUK2Lh7PwNUlhFxxLI2haSDNyXvebBO9izhjhMoDC+S3XI9qt2DGFUsiJ89m2k7gGYch2aEpYqV5F/+nwZug== - dependencies: - "@typescript-eslint/scope-manager" "5.43.0" - "@typescript-eslint/types" "5.43.0" - "@typescript-eslint/typescript-estree" "5.43.0" - debug "^4.3.4" - -"@typescript-eslint/scope-manager@5.37.0": - version "5.37.0" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.37.0.tgz" - integrity sha512-F67MqrmSXGd/eZnujjtkPgBQzgespu/iCZ+54Ok9X5tALb9L2v3G+QBSoWkXG0p3lcTJsL+iXz5eLUEdSiJU9Q== - dependencies: - "@typescript-eslint/types" "5.37.0" - "@typescript-eslint/visitor-keys" "5.37.0" - -"@typescript-eslint/scope-manager@5.43.0": - version "5.43.0" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.43.0.tgz" - integrity sha512-XNWnGaqAtTJsUiZaoiGIrdJYHsUOd3BZ3Qj5zKp9w6km6HsrjPk/TGZv0qMTWyWj0+1QOqpHQ2gZOLXaGA9Ekw== - dependencies: - "@typescript-eslint/types" "5.43.0" - "@typescript-eslint/visitor-keys" "5.43.0" - -"@typescript-eslint/type-utils@5.37.0": - version "5.37.0" - resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.37.0.tgz" - integrity sha512-BSx/O0Z0SXOF5tY0bNTBcDEKz2Ec20GVYvq/H/XNKiUorUFilH7NPbFUuiiyzWaSdN3PA8JV0OvYx0gH/5aFAQ== - dependencies: - "@typescript-eslint/typescript-estree" "5.37.0" - "@typescript-eslint/utils" "5.37.0" - debug "^4.3.4" - tsutils "^3.21.0" - -"@typescript-eslint/types@5.37.0": - version "5.37.0" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.37.0.tgz" - integrity sha512-3frIJiTa5+tCb2iqR/bf7XwU20lnU05r/sgPJnRpwvfZaqCJBrl8Q/mw9vr3NrNdB/XtVyMA0eppRMMBqdJ1bA== - -"@typescript-eslint/types@5.43.0": - version "5.43.0" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.43.0.tgz" - integrity sha512-jpsbcD0x6AUvV7tyOlyvon0aUsQpF8W+7TpJntfCUWU1qaIKu2K34pMwQKSzQH8ORgUrGYY6pVIh1Pi8TNeteg== - -"@typescript-eslint/typescript-estree@5.37.0": - version "5.37.0" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.37.0.tgz" - integrity sha512-JkFoFIt/cx59iqEDSgIGnQpCTRv96MQnXCYvJi7QhBC24uyuzbD8wVbajMB1b9x4I0octYFJ3OwjAwNqk1AjDA== - dependencies: - "@typescript-eslint/types" "5.37.0" - "@typescript-eslint/visitor-keys" "5.37.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/typescript-estree@5.43.0": - version "5.43.0" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.43.0.tgz" - integrity sha512-BZ1WVe+QQ+igWal2tDbNg1j2HWUkAa+CVqdU79L4HP9izQY6CNhXfkNwd1SS4+sSZAP/EthI1uiCSY/+H0pROg== - dependencies: - "@typescript-eslint/types" "5.43.0" - "@typescript-eslint/visitor-keys" "5.43.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/utils@5.37.0": - version "5.37.0" - resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.37.0.tgz" - integrity sha512-jUEJoQrWbZhmikbcWSMDuUSxEE7ID2W/QCV/uz10WtQqfOuKZUqFGjqLJ+qhDd17rjgp+QJPqTdPIBWwoob2NQ== - dependencies: - "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.37.0" - "@typescript-eslint/types" "5.37.0" - "@typescript-eslint/typescript-estree" "5.37.0" - eslint-scope "^5.1.1" - eslint-utils "^3.0.0" - -"@typescript-eslint/visitor-keys@5.37.0": - version "5.37.0" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.37.0.tgz" - integrity sha512-Hp7rT4cENBPIzMwrlehLW/28EVCOcE9U1Z1BQTc8EA8v5qpr7GRGuG+U58V5tTY48zvUOA3KHvw3rA8tY9fbdA== - dependencies: - "@typescript-eslint/types" "5.37.0" - eslint-visitor-keys "^3.3.0" - -"@typescript-eslint/visitor-keys@5.43.0": - version "5.43.0" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.43.0.tgz" - integrity sha512-icl1jNH/d18OVHLfcwdL3bWUKsBeIiKYTGxMJCoGe7xFht+E4QgzOqoWYrU8XSLJWhVw8nTacbm03v23J/hFTg== - dependencies: - "@typescript-eslint/types" "5.43.0" - eslint-visitor-keys "^3.3.0" - -"@unrs/resolver-binding-darwin-arm64@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz" - integrity sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g== - -"@vtaits/use-lazy-ref@^0.1.4": - version "0.1.4" - resolved "https://registry.npmjs.org/@vtaits/use-lazy-ref/-/use-lazy-ref-0.1.4.tgz" - integrity sha512-pdHe8k2WLIm8ccVfNw3HzeTCkifKKjVQ3hpiM7/rMynCp8nev715wrY2RCYnbeowNvekWqpGdHtrWKfCDocC6g== - -acorn-jsx@^5.3.2: - version "5.3.2" - resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.8.0: - version "8.8.0" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz" - integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== - -ajv@^6.10.0, ajv@^6.12.4: - version "6.12.6" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - 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" - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-regex@^6.0.1: - version "6.1.0" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz" - integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -ansi-styles@^6.1.0: - version "6.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== - -any-promise@^1.0.0: - version "1.3.0" - resolved "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz" - integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== - -anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -apexcharts@^5.0.0, apexcharts@>=5.10.1: - version "5.11.0" - resolved "https://registry.npmjs.org/apexcharts/-/apexcharts-5.11.0.tgz" - integrity sha512-iIXnZ1DhndVgY4G5fmAbyUtGBTR5j4LlMEb561eAd4+wkJB7wJXXKLMr9u+4V5aQEPXeVYWgkAbuw9Y1z6MIrg== - -arg@^5.0.2: - version "5.0.2" - resolved "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz" - integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -aria-query@^4.2.2: - version "4.2.2" - resolved "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz" - integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== - dependencies: - "@babel/runtime" "^7.10.2" - "@babel/runtime-corejs3" "^7.10.2" - -array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz" - integrity sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw== - dependencies: - call-bound "^1.0.3" - is-array-buffer "^3.0.5" - -array-includes@^3.1.5, array-includes@^3.1.9: - version "3.1.9" - resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz" - integrity sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.4" - define-properties "^1.2.1" - es-abstract "^1.24.0" - es-object-atoms "^1.1.1" - get-intrinsic "^1.3.0" - is-string "^1.1.1" - math-intrinsics "^1.1.0" - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -array.prototype.findlastindex@^1.2.6: - version "1.2.6" - resolved "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz" - integrity sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.4" - define-properties "^1.2.1" - es-abstract "^1.23.9" - es-errors "^1.3.0" - es-object-atoms "^1.1.1" - es-shim-unscopables "^1.1.0" - -array.prototype.flat@^1.3.3: - version "1.3.3" - resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz" - integrity sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg== - dependencies: - call-bind "^1.0.8" - define-properties "^1.2.1" - es-abstract "^1.23.5" - es-shim-unscopables "^1.0.2" - -array.prototype.flatmap@^1.3.0, array.prototype.flatmap@^1.3.3: - version "1.3.3" - resolved "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz" - integrity sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg== - dependencies: - call-bind "^1.0.8" - define-properties "^1.2.1" - es-abstract "^1.23.5" - es-shim-unscopables "^1.0.2" - -arraybuffer.prototype.slice@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz" - integrity sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ== - dependencies: - array-buffer-byte-length "^1.0.1" - call-bind "^1.0.8" - define-properties "^1.2.1" - es-abstract "^1.23.5" - es-errors "^1.3.0" - get-intrinsic "^1.2.6" - is-array-buffer "^3.0.4" - -ast-types-flow@^0.0.7: - version "0.0.7" - resolved "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz" - integrity sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag== - -async-function@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz" - integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA== - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - -autoprefixer@^10.4.0: - version "10.4.9" - resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.9.tgz" - integrity sha512-Uu67eduPEmOeA0vyJby5ghu1AAELCCNSsLAjK+lz6kYzNM5sqnBO36MqfsjhPjQF/BaJM5U/UuFYyl7PavY/wQ== - dependencies: - browserslist "^4.21.3" - caniuse-lite "^1.0.30001394" - fraction.js "^4.2.0" - normalize-range "^0.1.2" - picocolors "^1.0.0" - postcss-value-parser "^4.2.0" - -available-typed-arrays@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz" - integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== - dependencies: - possible-typed-array-names "^1.0.0" - -axe-core@^4.4.3: - version "4.4.3" - resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.4.3.tgz" - integrity sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w== - -axios@^1.8.4: - version "1.16.0" - resolved "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz" - integrity sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w== - dependencies: - follow-redirects "^1.16.0" - form-data "^4.0.5" - proxy-from-env "^2.1.0" - -axobject-query@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz" - integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== - -babel-plugin-macros@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz" - integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== - dependencies: - "@babel/runtime" "^7.12.5" - cosmiconfig "^7.0.0" - resolve "^1.19.0" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -balanced-match@^4.0.2: - version "4.0.4" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz" - integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA== - -base64-arraybuffer@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz" - integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== - -baseline-browser-mapping@^2.10.12: - version "2.10.29" - resolved "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz" - integrity sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ== - -binary-extensions@^2.0.0: - version "2.3.0" - resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz" - integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -brace-expansion@^2.0.2: - version "2.1.0" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz" - integrity sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w== - dependencies: - balanced-match "^1.0.0" - -brace-expansion@^5.0.5: - version "5.0.6" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz" - integrity sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g== - dependencies: - balanced-match "^4.0.2" - -braces@^3.0.3, braces@~3.0.2: - version "3.0.3" - resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - -browserslist@^4.21.3, "browserslist@>= 4.21.0", browserslist@>=4, browserslist@4.28.2: - version "4.28.2" - resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz" - integrity sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg== - 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" - -buffer-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz" - integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== - -call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: - 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== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - -call-bind@^1.0.2, call-bind@^1.0.7, call-bind@^1.0.8: - version "1.0.8" - resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz" - integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== - dependencies: - call-bind-apply-helpers "^1.0.0" - es-define-property "^1.0.0" - get-intrinsic "^1.2.4" - set-function-length "^1.2.2" - -call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz" - integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== - dependencies: - call-bind-apply-helpers "^1.0.2" - get-intrinsic "^1.3.0" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camelcase-css@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz" - integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== - -caniuse-lite@^1.0.30001394, caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001782: - version "1.0.30001792" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz" - integrity sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw== - -chalk@^4.0.0: - version "4.1.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chart.js@^4.1.1, chart.js@^4.4.1: - version "4.4.7" - resolved "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz" - integrity sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw== - dependencies: - "@kurkle/color" "^0.3.0" - -chokidar@^3.6.0: - version "3.6.0" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" - integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chroma-js@^2.4.2: - version "2.6.0" - resolved "https://registry.npmjs.org/chroma-js/-/chroma-js-2.6.0.tgz" - integrity sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A== - -client-only@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz" - integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== - -clsx@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz" - integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== - -clsx@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz" - integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -commander@^4.0.0: - version "4.1.1" - resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz" - integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== - -common-tags@1.8.2: - version "1.8.2" - resolved "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz" - integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -convert-source-map@^1.5.0: - version "1.9.0" - resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz" - integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== - -core-js-pure@^3.20.2: - version "3.25.1" - resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.25.1.tgz" - integrity sha512-7Fr74bliUDdeJCBMxkkIuQ4xfxn/SwrVg+HkJUAoNEXVqYLv55l6Af0dJ5Lq2YBUW9yKqSkLXaS5SYPK6MGa/A== - -core-js@^3: - version "3.49.0" - resolved "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz" - integrity sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg== - -cosmiconfig@^7.0.0: - version "7.1.0" - resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz" - integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.2.1" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.10.0" - -cross-env@^7.0.3: - version "7.0.3" - resolved "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz" - integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== - dependencies: - cross-spawn "^7.0.1" - -cross-fetch@4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz" - integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g== - dependencies: - node-fetch "^2.6.12" - -cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2: - version "7.0.3" - resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -css-line-break@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz" - integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w== - dependencies: - utrie "^1.0.2" - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -csstype@^3.0.2, csstype@^3.1.3, csstype@^3.2.2: - version "3.2.3" - resolved "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz" - integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== - -damerau-levenshtein@^1.0.8: - version "1.0.8" - resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" - integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== - -data-view-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz" - integrity sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ== - dependencies: - call-bound "^1.0.3" - es-errors "^1.3.0" - is-data-view "^1.0.2" - -data-view-byte-length@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz" - integrity sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ== - dependencies: - call-bound "^1.0.3" - es-errors "^1.3.0" - is-data-view "^1.0.2" - -data-view-byte-offset@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz" - integrity sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ== - dependencies: - call-bound "^1.0.2" - es-errors "^1.3.0" - is-data-view "^1.0.1" - -date-arithmetic@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz" - integrity sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg== - -date-fns@^3.6.0: - version "3.6.0" - resolved "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz" - integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== - -dayjs@^1.11.10, dayjs@^1.11.7: - version "1.11.13" - resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz" - integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== - -debug@^3.2.7: - version "3.2.7" - resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - -debug@^4.4.0: - version "4.4.3" - resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" - integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== - dependencies: - ms "^2.1.3" - -decode-uri-component@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz" - integrity sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ== - -deep-is@^0.1.3: - 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== - -deepmerge@^2.1.1: - version "2.2.1" - resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz" - integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== - -define-data-property@^1.0.1, define-data-property@^1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" - -define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz" - integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== - dependencies: - define-data-property "^1.0.1" - has-property-descriptors "^1.0.0" - object-keys "^1.1.1" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - -dequal@^2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" - integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== - -detect-libc@^2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz" - integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== - -dexie@^4.3.0: - version "4.4.2" - resolved "https://registry.npmjs.org/dexie/-/dexie-4.4.2.tgz" - integrity sha512-zMtV8q79EFE5U8FKZvt0Y/77PCU/Hr/RDxv1EDeo228L+m/HTbeN2AjoQm674rhQCX8n3ljK87lajt7UQuZfvw== - -didyoumean@^1.2.2: - version "1.2.2" - resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz" - integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -dlv@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz" - integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== - -dnd-core@^16.0.1: - version "16.0.1" - resolved "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz" - integrity sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng== - dependencies: - "@react-dnd/asap" "^5.0.1" - "@react-dnd/invariant" "^4.0.1" - redux "^4.2.0" - -doctrine@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz" - integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== - dependencies: - esutils "^2.0.2" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -dom-helpers@^5.0.1, dom-helpers@^5.2.0, dom-helpers@^5.2.1: - version "5.2.1" - resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz" - integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== - dependencies: - "@babel/runtime" "^7.8.7" - csstype "^3.0.2" - -dunder-proto@^1.0.0, dunder-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz" - integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== - dependencies: - call-bind-apply-helpers "^1.0.1" - es-errors "^1.3.0" - gopd "^1.2.0" - -eastasianwidth@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" - integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== - -ecdsa-sig-formatter@1.0.11: - version "1.0.11" - resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - -electron-to-chromium@^1.5.328: - version "1.5.353" - resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz" - integrity sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -emoji-regex@^9.2.2: - version "9.2.2" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" - integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-abstract@^1.19.1, es-abstract@^1.19.5, es-abstract@^1.23.2, es-abstract@^1.23.5, es-abstract@^1.23.9, es-abstract@^1.24.0: - version "1.24.1" - resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz" - integrity sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw== - dependencies: - array-buffer-byte-length "^1.0.2" - arraybuffer.prototype.slice "^1.0.4" - available-typed-arrays "^1.0.7" - call-bind "^1.0.8" - call-bound "^1.0.4" - data-view-buffer "^1.0.2" - data-view-byte-length "^1.0.2" - data-view-byte-offset "^1.0.1" - es-define-property "^1.0.1" - es-errors "^1.3.0" - es-object-atoms "^1.1.1" - es-set-tostringtag "^2.1.0" - es-to-primitive "^1.3.0" - function.prototype.name "^1.1.8" - get-intrinsic "^1.3.0" - get-proto "^1.0.1" - get-symbol-description "^1.1.0" - globalthis "^1.0.4" - gopd "^1.2.0" - has-property-descriptors "^1.0.2" - has-proto "^1.2.0" - has-symbols "^1.1.0" - hasown "^2.0.2" - internal-slot "^1.1.0" - is-array-buffer "^3.0.5" - is-callable "^1.2.7" - is-data-view "^1.0.2" - is-negative-zero "^2.0.3" - is-regex "^1.2.1" - is-set "^2.0.3" - is-shared-array-buffer "^1.0.4" - is-string "^1.1.1" - is-typed-array "^1.1.15" - is-weakref "^1.1.1" - math-intrinsics "^1.1.0" - object-inspect "^1.13.4" - object-keys "^1.1.1" - object.assign "^4.1.7" - own-keys "^1.0.1" - regexp.prototype.flags "^1.5.4" - safe-array-concat "^1.1.3" - safe-push-apply "^1.0.0" - safe-regex-test "^1.1.0" - set-proto "^1.0.0" - stop-iteration-iterator "^1.1.0" - string.prototype.trim "^1.2.10" - string.prototype.trimend "^1.0.9" - string.prototype.trimstart "^1.0.8" - typed-array-buffer "^1.0.3" - typed-array-byte-length "^1.0.3" - typed-array-byte-offset "^1.0.4" - typed-array-length "^1.0.7" - unbox-primitive "^1.1.0" - which-typed-array "^1.1.19" - -es-define-property@^1.0.0, es-define-property@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz" - integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== - -es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz" - integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== - dependencies: - es-errors "^1.3.0" - -es-set-tostringtag@^2.1.0: - 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== - dependencies: - es-errors "^1.3.0" - get-intrinsic "^1.2.6" - has-tostringtag "^1.0.2" - hasown "^2.0.2" - -es-shim-unscopables@^1.0.2, es-shim-unscopables@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz" - integrity sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw== - dependencies: - hasown "^2.0.2" - -es-to-primitive@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz" - integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g== - dependencies: - is-callable "^1.2.7" - is-date-object "^1.0.5" - is-symbol "^1.0.4" - -escalade@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" - integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -eslint-config-next@^13.0.4: - version "13.0.4" - resolved "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.0.4.tgz" - integrity sha512-moEC7BW2TK7JKq3QfnaauqRjWzVcEf71gp5DbClpFPHM6QXE0u0uVvSTiHlmOgtCe1vyWAO+AhF87ZITd8mIDw== - dependencies: - "@next/eslint-plugin-next" "13.0.4" - "@rushstack/eslint-patch" "^1.1.3" - "@typescript-eslint/parser" "^5.42.0" - eslint-import-resolver-node "^0.3.6" - eslint-import-resolver-typescript "^3.5.2" - eslint-plugin-import "^2.26.0" - eslint-plugin-jsx-a11y "^6.5.1" - eslint-plugin-react "^7.31.7" - eslint-plugin-react-hooks "^4.5.0" - -eslint-config-prettier@^8.5.0: - version "8.5.0" - resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz" - integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== - -eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9: - version "0.3.10" - resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz" - integrity sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ== - dependencies: - debug "^3.2.7" - is-core-module "^2.16.1" - resolve "^2.0.0-next.6" - -eslint-import-resolver-typescript@^3.5.2, eslint-import-resolver-typescript@^3.6.1: - version "3.10.1" - resolved "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz" - integrity sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ== - dependencies: - "@nolyfill/is-core-module" "1.0.39" - debug "^4.4.0" - get-tsconfig "^4.10.0" - is-bun-module "^2.0.0" - stable-hash "^0.0.5" - tinyglobby "^0.2.13" - unrs-resolver "^1.6.2" - -eslint-module-utils@^2.12.1: - version "2.12.1" - resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz" - integrity sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw== - dependencies: - debug "^3.2.7" - -eslint-plugin-import@*, eslint-plugin-import@^2.26.0, eslint-plugin-import@^2.29.1: - version "2.32.0" - resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz" - integrity sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA== - dependencies: - "@rtsao/scc" "^1.1.0" - array-includes "^3.1.9" - array.prototype.findlastindex "^1.2.6" - array.prototype.flat "^1.3.3" - array.prototype.flatmap "^1.3.3" - debug "^3.2.7" - doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.9" - eslint-module-utils "^2.12.1" - hasown "^2.0.2" - is-core-module "^2.16.1" - is-glob "^4.0.3" - minimatch "^3.1.2" - object.fromentries "^2.0.8" - object.groupby "^1.0.3" - object.values "^1.2.1" - semver "^6.3.1" - string.prototype.trimend "^1.0.9" - tsconfig-paths "^3.15.0" - -eslint-plugin-jsx-a11y@^6.5.1: - version "6.6.1" - resolved "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz" - integrity sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q== - dependencies: - "@babel/runtime" "^7.18.9" - aria-query "^4.2.2" - array-includes "^3.1.5" - ast-types-flow "^0.0.7" - axe-core "^4.4.3" - axobject-query "^2.2.0" - damerau-levenshtein "^1.0.8" - emoji-regex "^9.2.2" - has "^1.0.3" - jsx-ast-utils "^3.3.2" - language-tags "^1.0.5" - minimatch "^3.1.2" - semver "^6.3.0" - -eslint-plugin-react-hooks@^4.5.0: - version "4.6.0" - resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz" - integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== - -eslint-plugin-react@^7.31.7: - version "7.31.8" - resolved "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.8.tgz" - integrity sha512-5lBTZmgQmARLLSYiwI71tiGVTLUuqXantZM6vlSY39OaDSV0M7+32K5DnLkmFrwTe+Ksz0ffuLUC91RUviVZfw== - dependencies: - array-includes "^3.1.5" - array.prototype.flatmap "^1.3.0" - doctrine "^2.1.0" - estraverse "^5.3.0" - jsx-ast-utils "^2.4.1 || ^3.0.0" - minimatch "^3.1.2" - object.entries "^1.1.5" - object.fromentries "^2.0.5" - object.hasown "^1.1.1" - object.values "^1.1.5" - prop-types "^15.8.1" - resolve "^2.0.0-next.3" - semver "^6.3.0" - string.prototype.matchall "^4.0.7" - -eslint-scope@^5.1.1: - version "5.1.1" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== - dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" - -eslint-scope@^7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz" - integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - -eslint-visitor-keys@^3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" - integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== - -eslint@*, "eslint@^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9", "eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8", "eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "eslint@^6.0.0 || ^7.0.0 || ^8.0.0", "eslint@^7.23.0 || ^8.0.0", eslint@^8.23.1, eslint@>=5, eslint@>=7.0.0: - version "8.23.1" - resolved "https://registry.npmjs.org/eslint/-/eslint-8.23.1.tgz" - integrity sha512-w7C1IXCc6fNqjpuYd0yPlcTKKmHlHHktRkzmBPZ+7cvNBQuiNjx0xaMTjAJGCafJhQkrFJooREv0CtrVzmHwqg== - dependencies: - "@eslint/eslintrc" "^1.3.2" - "@humanwhocodes/config-array" "^0.10.4" - "@humanwhocodes/gitignore-to-minimatch" "^1.0.2" - "@humanwhocodes/module-importer" "^1.0.1" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - escape-string-regexp "^4.0.0" - eslint-scope "^7.1.1" - eslint-utils "^3.0.0" - eslint-visitor-keys "^3.3.0" - espree "^9.4.0" - esquery "^1.4.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - find-up "^5.0.0" - glob-parent "^6.0.1" - globals "^13.15.0" - globby "^11.1.0" - grapheme-splitter "^1.0.4" - ignore "^5.2.0" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - js-sdsl "^4.1.4" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" - natural-compare "^1.4.0" - optionator "^0.9.1" - regexpp "^3.2.0" - strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" - text-table "^0.2.0" - -espree@^9.4.0: - version "9.4.0" - resolved "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz" - integrity sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw== - dependencies: - acorn "^8.8.0" - acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.3.0" - -esquery@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: - version "5.3.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-glob@^3.2.9, fast-glob@^3.3.2: - version "3.3.2" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz" - integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-json-stable-stringify@^2.0.0: - 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== - -fast-levenshtein@^2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" - integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== - -fastq@^1.6.0: - version "1.13.0" - resolved "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz" - integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== - dependencies: - reusify "^1.0.4" - -fdir@^6.5.0: - version "6.5.0" - resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz" - integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== - -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== - dependencies: - flat-cache "^3.0.4" - -file-saver@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz" - integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== - -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - -filter-obj@^5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz" - integrity sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng== - -find-root@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz" - integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== - dependencies: - flatted "^3.1.0" - rimraf "^3.0.2" - -flatted@^3.1.0: - version "3.2.7" - resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz" - integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== - -follow-redirects@^1.16.0: - 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== - -for-each@^0.3.3, for-each@^0.3.5: - version "0.3.5" - resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz" - integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== - dependencies: - is-callable "^1.2.7" - -foreground-child@^3.1.0: - version "3.3.0" - resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz" - integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== - dependencies: - cross-spawn "^7.0.0" - signal-exit "^4.0.1" - -form-data@^4.0.5: - version "4.0.5" - resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz" - integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== - 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" - -formik@^2.4.5: - version "2.4.6" - resolved "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz" - integrity sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g== - dependencies: - "@types/hoist-non-react-statics" "^3.3.1" - deepmerge "^2.1.1" - hoist-non-react-statics "^3.3.0" - lodash "^4.17.21" - lodash-es "^4.17.21" - react-fast-compare "^2.0.1" - tiny-warning "^1.0.2" - tslib "^2.0.0" - -fraction.js@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz" - integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -function-bind@^1.1.1, function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -function.prototype.name@^1.1.6, function.prototype.name@^1.1.8: - version "1.1.8" - resolved "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz" - integrity sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.3" - define-properties "^1.2.1" - functions-have-names "^1.2.3" - hasown "^2.0.2" - is-callable "^1.2.7" - -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" - integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== - -functions-have-names@^1.2.3: - version "1.2.3" - resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz" - integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== - -generator-function@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz" - integrity sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g== - -get-intrinsic@^1.1.1, get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz" - integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== - 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" - -get-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz" - integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== - dependencies: - dunder-proto "^1.0.1" - es-object-atoms "^1.0.0" - -get-symbol-description@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz" - integrity sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg== - dependencies: - call-bound "^1.0.3" - es-errors "^1.3.0" - get-intrinsic "^1.2.6" - -get-tsconfig@^4.10.0: - version "4.13.6" - resolved "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz" - integrity sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw== - dependencies: - resolve-pkg-maps "^1.0.0" - -glob-parent@^5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-parent@^6.0.1, glob-parent@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob@^10.3.10: - version "10.4.5" - resolved "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz" - integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== - dependencies: - foreground-child "^3.1.0" - jackspeak "^3.1.2" - minimatch "^9.0.4" - minipass "^7.1.2" - package-json-from-dist "^1.0.0" - path-scurry "^1.11.1" - -glob@^7.1.3: - version "7.1.7" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@13.0.6: - version "13.0.6" - resolved "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz" - integrity sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw== - dependencies: - minimatch "^10.2.2" - minipass "^7.1.3" - path-scurry "^2.0.2" - -glob@7.1.7: - version "7.1.7" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globalize@^0.1.1: - version "0.1.1" - resolved "https://registry.npmjs.org/globalize/-/globalize-0.1.1.tgz" - integrity sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA== - -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globals@^13.15.0: - version "13.17.0" - resolved "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz" - integrity sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw== - dependencies: - type-fest "^0.20.2" - -globalthis@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz" - integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== - dependencies: - define-properties "^1.2.1" - gopd "^1.0.1" - -globby@^11.1.0: - version "11.1.0" - resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - -gopd@^1.0.1, gopd@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" - integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== - -grapheme-splitter@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz" - integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== - -has-bigints@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz" - integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" - -has-proto@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz" - integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ== - dependencies: - dunder-proto "^1.0.0" - -has-symbols@^1.0.3, has-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz" - integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== - -has-tostringtag@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz" - integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== - dependencies: - has-symbols "^1.0.3" - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hasown@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - -hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: - version "3.3.2" - resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" - integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== - dependencies: - react-is "^16.7.0" - -html-parse-stringify@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz" - integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== - dependencies: - void-elements "3.1.0" - -html2canvas@^1.4.1: - version "1.4.1" - resolved "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz" - integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA== - dependencies: - css-line-break "^2.1.0" - text-segmentation "^1.0.3" - -i18next-browser-languagedetector@^8.1.0: - version "8.2.1" - resolved "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz" - integrity sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw== - dependencies: - "@babel/runtime" "^7.23.2" - -i18next-fs-backend@^2.6.0: - version "2.6.1" - resolved "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.6.1.tgz" - integrity sha512-eYWTX7QT7kJ0sZyCPK6x1q+R63zvNKv2D6UdbMf15A8vNb2ZLyw4NNNZxPFwXlIv/U+oUtg8SakW6ZgJZcoqHQ== - -i18next-http-backend@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz" - integrity sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g== - dependencies: - cross-fetch "4.0.0" - -i18next@^25.1.2, "i18next@>= 23.4.0", "i18next@>= 23.7.13": - version "25.10.10" - resolved "https://registry.npmjs.org/i18next/-/i18next-25.10.10.tgz" - integrity sha512-cqUW2Z3EkRx7NqSyywjkgCLK7KLCL6IFVFcONG7nVYIJ3ekZ1/N5jUsihHV6Bq37NfhgtczxJcxduELtjTwkuQ== - dependencies: - "@babel/runtime" "^7.29.2" - -idb@8.0.3: - version "8.0.3" - resolved "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz" - integrity sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg== - -ignore@^5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz" - integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== - -immer@^10.0.3: - version "10.1.1" - resolved "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz" - integrity sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw== - -import-fresh@^3.0.0, import-fresh@^3.2.1: - version "3.3.0" - resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.4" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -internal-slot@^1.0.3, internal-slot@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz" - integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== - dependencies: - es-errors "^1.3.0" - hasown "^2.0.2" - side-channel "^1.1.0" - -intro.js-react@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/intro.js-react/-/intro.js-react-1.0.0.tgz" - integrity sha512-zR8pbTyX20RnCZpJMc0nuHBpsjcr1wFkj3ZookV6Ly4eE/LGpFTQwPsaA61Cryzwiy/tTFsusf4hPU9NpI9UOg== - -intro.js@^7.2.0, intro.js@>=2.5.0: - version "7.2.0" - resolved "https://registry.npmjs.org/intro.js/-/intro.js-7.2.0.tgz" - integrity sha512-qbMfaB70rOXVBceIWNYnYTpVTiZsvQh/MIkfdQbpA9di9VBfj1GigUPfcCv3aOfsbrtPcri8vTLTA4FcEDcHSQ== - -invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - -is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: - version "3.0.5" - resolved "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz" - integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.3" - get-intrinsic "^1.2.6" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" - integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== - -is-async-function@^2.0.0: - version "2.1.1" - resolved "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz" - integrity sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ== - dependencies: - async-function "^1.0.0" - call-bound "^1.0.3" - get-proto "^1.0.1" - has-tostringtag "^1.0.2" - safe-regex-test "^1.1.0" - -is-bigint@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz" - integrity sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ== - dependencies: - has-bigints "^1.0.2" - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-boolean-object@^1.2.1: - version "1.2.2" - resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz" - integrity sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A== - dependencies: - call-bound "^1.0.3" - has-tostringtag "^1.0.2" - -is-bun-module@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz" - integrity sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ== - dependencies: - semver "^7.7.1" - -is-callable@^1.2.7: - version "1.2.7" - resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== - -is-core-module@^2.16.0, is-core-module@^2.16.1, is-core-module@^2.9.0: - version "2.16.1" - resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz" - integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== - dependencies: - hasown "^2.0.2" - -is-data-view@^1.0.1, is-data-view@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz" - integrity sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw== - dependencies: - call-bound "^1.0.2" - get-intrinsic "^1.2.6" - is-typed-array "^1.1.13" - -is-date-object@^1.0.5, is-date-object@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz" - integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== - dependencies: - call-bound "^1.0.2" - has-tostringtag "^1.0.2" - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-finalizationregistry@^1.1.0: - version "1.1.1" - resolved "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz" - integrity sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg== - dependencies: - call-bound "^1.0.3" - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-generator-function@^1.0.10: - version "1.1.2" - resolved "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz" - integrity sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA== - dependencies: - call-bound "^1.0.4" - generator-function "^2.0.0" - get-proto "^1.0.1" - has-tostringtag "^1.0.2" - safe-regex-test "^1.1.0" - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-map@^2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz" - integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== - -is-negative-zero@^2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz" - integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== - -is-number-object@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz" - integrity sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw== - dependencies: - call-bound "^1.0.3" - has-tostringtag "^1.0.2" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-regex@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz" - integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== - dependencies: - call-bound "^1.0.2" - gopd "^1.2.0" - has-tostringtag "^1.0.2" - hasown "^2.0.2" - -is-set@^2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz" - integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== - -is-shared-array-buffer@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz" - integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A== - dependencies: - call-bound "^1.0.3" - -is-string@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz" - integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA== - dependencies: - call-bound "^1.0.3" - has-tostringtag "^1.0.2" - -is-symbol@^1.0.4, is-symbol@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz" - integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== - dependencies: - call-bound "^1.0.2" - has-symbols "^1.1.0" - safe-regex-test "^1.1.0" - -is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15: - version "1.1.15" - resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz" - integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== - dependencies: - which-typed-array "^1.1.16" - -is-weakmap@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz" - integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== - -is-weakref@^1.0.2, is-weakref@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz" - integrity sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew== - dependencies: - call-bound "^1.0.3" - -is-weakset@^2.0.3: - version "2.0.4" - resolved "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz" - integrity sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ== - dependencies: - call-bound "^1.0.3" - get-intrinsic "^1.2.6" - -isarray@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -jackspeak@^3.1.2: - version "3.4.3" - resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz" - integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== - dependencies: - "@isaacs/cliui" "^8.0.2" - optionalDependencies: - "@pkgjs/parseargs" "^0.11.0" - -jiti@^1.21.6: - version "1.21.7" - resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz" - integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A== - -js-sdsl@^4.1.4: - version "4.1.4" - resolved "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.4.tgz" - integrity sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw== - -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -jsesc@^3.0.2: - version "3.1.0" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz" - integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== - -json-parse-even-better-errors@^2.3.0: - version "2.3.1" - resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" - integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== - -json5@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz" - integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== - dependencies: - minimist "^1.2.0" - -jsonwebtoken@^9.0.2: - version "9.0.2" - resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz" - integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== - dependencies: - jws "^3.2.2" - lodash.includes "^4.3.0" - lodash.isboolean "^3.0.3" - lodash.isinteger "^4.0.4" - lodash.isnumber "^3.0.3" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.once "^4.0.0" - ms "^2.1.1" - semver "^7.5.4" - -"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.2: - version "3.3.3" - resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz" - integrity sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw== - dependencies: - array-includes "^3.1.5" - object.assign "^4.1.3" - -jwa@^1.4.1: - version "1.4.1" - resolved "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz" - integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^3.2.2: - version "3.2.2" - resolved "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz" - integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== - dependencies: - jwa "^1.4.1" - safe-buffer "^5.0.1" - -jwt-decode@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz" - integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA== - -kolorist@1.8.0: - version "1.8.0" - resolved "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz" - integrity sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ== - -krustykrab@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/krustykrab/-/krustykrab-1.1.0.tgz" - integrity sha512-xpX9MPbw+nJseewe6who9Oq46RQwrBfps+dO/N4fSjJhsf2+y4XWC2kz46oBGX8yzMHyYJj35ug0X5s5yxB6tA== - -language-subtag-registry@~0.3.2: - version "0.3.22" - resolved "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz" - integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== - -language-tags@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz" - integrity sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ== - dependencies: - language-subtag-registry "~0.3.2" - -levn@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" - integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== - dependencies: - prelude-ls "^1.2.1" - type-check "~0.4.0" - -lilconfig@^3.0.0, lilconfig@^3.1.3: - version "3.1.3" - resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz" - integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== - -lines-and-columns@^1.1.6: - version "1.2.4" - resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" - integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -lodash-es@^4.17.21: - version "4.17.21" - resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz" - integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== - -lodash.castarray@^4.4.0: - version "4.4.0" - resolved "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz" - integrity sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q== - -lodash.includes@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz" - integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== - -lodash.isboolean@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz" - integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== - -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz" - integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== - -lodash.isnumber@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz" - integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz" - integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz" - integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== - -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -lodash.once@^4.0.0: - version "4.1.1" - resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz" - integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== - -lodash.sortby@^4.7.0: - version "4.7.0" - resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz" - integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== - -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -loose-envify@^1.0.0, loose-envify@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -lru-cache@^10.2.0: - version "10.4.3" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz" - integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== - -lru-cache@^11.0.0: - version "11.3.6" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz" - integrity sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A== - -luxon@^3.2.1: - version "3.5.0" - resolved "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz" - integrity sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ== - -math-intrinsics@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz" - integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== - -memoize-one@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz" - integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== - -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -micromatch@^4.0.4, micromatch@^4.0.8: - version "4.0.8" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" - integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== - dependencies: - braces "^3.0.3" - picomatch "^2.3.1" - -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12: - version "2.1.35" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mini-svg-data-uri@^1.2.3: - version "1.4.4" - resolved "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz" - integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg== - -minimatch@^10.2.2: - version "10.2.5" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz" - integrity sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg== - dependencies: - brace-expansion "^5.0.5" - -minimatch@^3.0.4, minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^9.0.4: - version "9.0.9" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz" - integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg== - dependencies: - brace-expansion "^2.0.2" - -minimist@^1.2.0, minimist@^1.2.6: - version "1.2.6" - resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== - -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2, minipass@^7.1.3: - version "7.1.3" - resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz" - integrity sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A== - -moment-timezone@^0.5.40: - version "0.5.46" - resolved "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.46.tgz" - integrity sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw== - dependencies: - moment "^2.29.4" - -moment@^2.29.4, moment@^2.30.1: - version "2.30.1" - resolved "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz" - integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== - -ms@^2.1.1, ms@2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@^2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -mz@^2.7.0: - version "2.7.0" - resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz" - integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== - dependencies: - any-promise "^1.0.0" - object-assign "^4.0.1" - thenify-all "^1.0.0" - -nanoid@^3.3.6, nanoid@^3.3.7: - version "3.3.8" - resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz" - integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== - -napi-postinstall@^0.3.0: - version "0.3.4" - resolved "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz" - integrity sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" - integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== - -next-i18next@^15.4.2: - version "15.4.3" - resolved "https://registry.npmjs.org/next-i18next/-/next-i18next-15.4.3.tgz" - integrity sha512-ZRmiz72o1Jvh2ZghCUQX1Ua5F/f2W1/Ila/L1ZeKVuSWiH7J4zfUedfDxNBEhj9lajREC7aoJuPXMFtKi2bdIg== - dependencies: - "@babel/runtime" "^7.23.2" - "@types/hoist-non-react-statics" "^3.3.6" - core-js "^3" - hoist-non-react-statics "^3.3.2" - i18next-fs-backend "^2.6.0" - -next@^15.3.1, "next@>= 12.0.0", next@>=14.0.0: - version "15.5.18" - resolved "https://registry.npmjs.org/next/-/next-15.5.18.tgz" - integrity sha512-eKL8zUJkX9Y5lE+RX/2YJoItVdGlIscyVyboeD9wSpp0PaGqjoA4tTpT2qPqz9ax+5IzGESyLSeZ/RCwbSZ2uQ== - dependencies: - "@next/env" "15.5.18" - "@swc/helpers" "0.5.15" - caniuse-lite "^1.0.30001579" - postcss "8.4.31" - styled-jsx "5.1.6" - optionalDependencies: - "@next/swc-darwin-arm64" "15.5.18" - "@next/swc-darwin-x64" "15.5.18" - "@next/swc-linux-arm64-gnu" "15.5.18" - "@next/swc-linux-arm64-musl" "15.5.18" - "@next/swc-linux-x64-gnu" "15.5.18" - "@next/swc-linux-x64-musl" "15.5.18" - "@next/swc-win32-arm64-msvc" "15.5.18" - "@next/swc-win32-x64-msvc" "15.5.18" - sharp "^0.34.3" - -node-exports-info@^1.6.0: - version "1.6.0" - resolved "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz" - integrity sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw== - dependencies: - array.prototype.flatmap "^1.3.3" - es-errors "^1.3.0" - object.entries "^1.1.9" - semver "^6.3.1" - -node-fetch@^2.6.12: - version "2.7.0" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" - integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== - dependencies: - whatwg-url "^5.0.0" - -node-releases@^2.0.36: - version "2.0.38" - resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz" - integrity sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw== - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" - integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== - -numeral@^2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz" - integrity sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA== - -object-assign@^4.0.1, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-hash@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz" - integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== - -object-inspect@^1.13.3, object-inspect@^1.13.4: - version "1.13.4" - resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz" - integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== - -object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.3, object.assign@^4.1.7: - version "4.1.7" - resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz" - integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.3" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - has-symbols "^1.1.0" - object-keys "^1.1.1" - -object.entries@^1.1.5, object.entries@^1.1.9: - version "1.1.9" - resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz" - integrity sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.4" - define-properties "^1.2.1" - es-object-atoms "^1.1.1" - -object.fromentries@^2.0.5, object.fromentries@^2.0.8: - version "2.0.8" - resolved "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz" - integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-object-atoms "^1.0.0" - -object.groupby@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz" - integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.2" - -object.hasown@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.1.tgz" - integrity sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A== - dependencies: - define-properties "^1.1.4" - es-abstract "^1.19.5" - -object.values@^1.1.5, object.values@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz" - integrity sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.3" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== - 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.3" - -own-keys@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz" - integrity sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg== - dependencies: - get-intrinsic "^1.2.6" - object-keys "^1.1.1" - safe-push-apply "^1.0.0" - -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-locate@^5.0.0: - 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== - dependencies: - p-limit "^3.0.2" - -package-json-from-dist@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz" - integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-json@^5.0.0: - version "5.2.0" - resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-scurry@^1.11.1: - version "1.11.1" - resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz" - integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== - dependencies: - lru-cache "^10.2.0" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - -path-scurry@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz" - integrity sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg== - dependencies: - lru-cache "^11.0.0" - minipass "^7.1.2" - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -picocolors@^1.0.0, picocolors@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" - integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== - -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -"picomatch@^3 || ^4", picomatch@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz" - integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== - -pify@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" - integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== - -pirates@^4.0.1: - version "4.0.6" - resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz" - integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== - -possible-typed-array-names@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz" - integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== - -postcss-import@^14.1.0: - version "14.1.0" - resolved "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz" - integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw== - dependencies: - postcss-value-parser "^4.0.0" - read-cache "^1.0.0" - resolve "^1.1.7" - -postcss-import@^15.1.0: - version "15.1.0" - resolved "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz" - integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== - dependencies: - postcss-value-parser "^4.0.0" - read-cache "^1.0.0" - resolve "^1.1.7" - -postcss-js@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz" - integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw== - dependencies: - camelcase-css "^2.0.1" - -postcss-load-config@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz" - integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ== - dependencies: - lilconfig "^3.0.0" - yaml "^2.3.4" - -postcss-nested@^6.2.0: - version "6.2.0" - resolved "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz" - integrity sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ== - dependencies: - postcss-selector-parser "^6.1.1" - -postcss-selector-parser@^6.1.1: - version "6.1.2" - resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz" - integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-selector-parser@^6.1.2: - version "6.1.2" - resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz" - integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-selector-parser@6.0.10: - version "6.0.10" - resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz" - integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" - integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== - -postcss@^8.0.0, postcss@^8.1.0, postcss@^8.2.14, postcss@^8.4.21, postcss@^8.4.4, postcss@^8.4.47, postcss@>=8.0.9: - version "8.4.49" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz" - integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA== - dependencies: - nanoid "^3.3.7" - picocolors "^1.1.1" - source-map-js "^1.2.1" - -postcss@8.4.31: - version "8.4.31" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz" - integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== - dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" - -prelude-ls@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" - integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== - -prettier@^3.2.4: - version "3.4.2" - resolved "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz" - integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ== - -pretty-bytes@6.1.1: - version "6.1.1" - resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz" - integrity sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ== - -prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: - version "15.8.1" - resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" - integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.13.1" - -proxy-from-env@^2.1.0: - 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== - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -query-string@^8.1.0: - version "8.2.0" - resolved "https://registry.npmjs.org/query-string/-/query-string-8.2.0.tgz" - integrity sha512-tUZIw8J0CawM5wyGBiDOAp7ObdRQh4uBor/fUR9ZjmbZVvw95OD9If4w3MQxr99rg0DJZ/9CIORcpEqU5hQG7g== - dependencies: - decode-uri-component "^0.4.1" - filter-obj "^5.1.0" - split-on-first "^3.0.0" - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -react-apexcharts@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-2.1.0.tgz" - integrity sha512-xrmeTKRKHh3cvvLc8SasqFjlOgIqGpyHc81qjnRtcjUM0Fu7qEjgVRWGPokGFjqhwRZVgEym8zmuEyYr5LMYIg== - dependencies: - prop-types "^15.8.1" - -react-big-calendar@^1.19.0: - version "1.19.4" - resolved "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.19.4.tgz" - integrity sha512-FrvbDx2LF6JAWFD96LU1jjloppC5OgIvMYUYIPzAw5Aq+ArYFPxAjLqXc4DyxfsQDN0TJTMuS/BIbcSB7Pg0YA== - dependencies: - "@babel/runtime" "^7.20.7" - clsx "^1.2.1" - date-arithmetic "^4.1.0" - dayjs "^1.11.7" - dom-helpers "^5.2.1" - globalize "^0.1.1" - invariant "^2.2.4" - lodash "^4.17.21" - lodash-es "^4.17.21" - luxon "^3.2.1" - memoize-one "^6.0.0" - moment "^2.29.4" - moment-timezone "^0.5.40" - prop-types "^15.8.1" - react-overlays "^5.2.1" - uncontrollable "^7.2.1" - -react-chartjs-2@^5.0.0: - version "5.3.1" - resolved "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz" - integrity sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A== - -react-datepicker@^7.0.0: - version "7.6.0" - resolved "https://registry.npmjs.org/react-datepicker/-/react-datepicker-7.6.0.tgz" - integrity sha512-9cQH6Z/qa4LrGhzdc3XoHbhrxNcMi9MKjZmYgF/1MNNaJwvdSjv3Xd+jjvrEEbKEf71ZgCA3n7fQbdwd70qCRw== - dependencies: - "@floating-ui/react" "^0.27.0" - clsx "^2.1.1" - date-fns "^3.6.0" - -react-dnd-html5-backend@^16.0.1: - version "16.0.1" - resolved "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz" - integrity sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw== - dependencies: - dnd-core "^16.0.1" - -react-dnd@^16.0.1: - version "16.0.1" - resolved "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz" - integrity sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q== - dependencies: - "@react-dnd/invariant" "^4.0.1" - "@react-dnd/shallowequal" "^4.0.1" - dnd-core "^16.0.1" - fast-deep-equal "^3.1.3" - hoist-non-react-statics "^3.3.2" - -"react-dom@^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^16.14.0 || ^17 || ^18 || ^19", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom@^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^18 || ^19", "react-dom@^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", react-dom@^19.0.0, react-dom@>=16.3.0, react-dom@>=16.6.0, react-dom@>=16.8.0, react-dom@>=17.0.0: - version "19.0.0" - resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz" - integrity sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ== - dependencies: - scheduler "^0.25.0" - -react-fast-compare@^2.0.1: - version "2.0.4" - resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz" - integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== - -react-i18next@^15.5.1, "react-i18next@>= 13.5.0": - version "15.7.4" - resolved "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.4.tgz" - integrity sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw== - dependencies: - "@babel/runtime" "^7.27.6" - html-parse-stringify "^3.0.1" - -react-is@^16.13.1: - version "16.13.1" - resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react-is@^16.7.0: - version "16.13.1" - resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react-is@^19.0.0: - version "19.2.6" - resolved "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz" - integrity sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw== - -react-lifecycles-compat@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz" - integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== - -react-overlays@^5.2.1: - version "5.2.1" - resolved "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz" - integrity sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA== - dependencies: - "@babel/runtime" "^7.13.8" - "@popperjs/core" "^2.11.6" - "@restart/hooks" "^0.4.7" - "@types/warning" "^3.0.0" - dom-helpers "^5.2.0" - prop-types "^15.7.2" - uncontrollable "^7.2.1" - warning "^4.0.3" - -"react-redux@^7.2.1 || ^8.1.3 || ^9.0.0", react-redux@^9.0.0: - version "9.2.0" - resolved "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz" - integrity sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g== - dependencies: - "@types/use-sync-external-store" "^0.0.6" - use-sync-external-store "^1.4.0" - -react-select-async-paginate@^0.7.11: - version "0.7.11" - resolved "https://registry.npmjs.org/react-select-async-paginate/-/react-select-async-paginate-0.7.11.tgz" - integrity sha512-AjtCLPMk5DLNgygwQprEPC0gfVIjkou+QYvXM+2gm/LeRpY1Gv5KNT79EYB37H1uMCrwA+HL9BY7OtlaNWtYNg== - dependencies: - "@seznam/compose-react-refs" "^1.0.6" - "@vtaits/use-lazy-ref" "^0.1.4" - krustykrab "^1.1.0" - sleep-promise "^9.1.0" - use-is-mounted-ref "^1.5.0" - use-latest "^1.3.0" - -react-select@^5.0.0, react-select@^5.7.0: - version "5.9.0" - resolved "https://registry.npmjs.org/react-select/-/react-select-5.9.0.tgz" - integrity sha512-nwRKGanVHGjdccsnzhFte/PULziueZxGD8LL2WojON78Mvnq7LdAMEtu2frrwld1fr3geixg3iiMBIc/LLAZpw== - dependencies: - "@babel/runtime" "^7.12.0" - "@emotion/cache" "^11.4.0" - "@emotion/react" "^11.8.1" - "@floating-ui/dom" "^1.0.1" - "@types/react-transition-group" "^4.4.0" - memoize-one "^6.0.0" - prop-types "^15.6.0" - react-transition-group "^4.3.0" - use-isomorphic-layout-effect "^1.2.0" - -react-switch@^7.0.0: - version "7.1.0" - resolved "https://registry.npmjs.org/react-switch/-/react-switch-7.1.0.tgz" - integrity sha512-4xVeyImZE8QOTDw2FmhWz0iqo2psoRiS7XzdjaZBCIP8Dzo3rT0esHUjLee5WsAPSFXWWl1eVA5arp9n2C6yQA== - dependencies: - prop-types "^15.7.2" - -react-toastify@^11.0.2: - version "11.0.5" - resolved "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz" - integrity sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA== - dependencies: - clsx "^2.1.1" - -react-transition-group@^4.3.0, react-transition-group@^4.4.5: - version "4.4.5" - resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz" - integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== - dependencies: - "@babel/runtime" "^7.5.5" - dom-helpers "^5.0.1" - loose-envify "^1.4.0" - prop-types "^15.6.2" - -"react@^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.14.0 || ^17 || ^18 || ^19", "react@^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react@^16.9.0 || ^17.0.0 || ^18 || ^19", "react@^17.0.0 || ^18.0.0 || ^19.0.0", "react@^18 || ^19", "react@^18.0 || ^19", "react@^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", react@^19.0.0, "react@>= 16.14", "react@>= 16.8.0", "react@>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0", "react@>= 17.0.2", react@>=0.14.0, react@>=15.0.0, react@>=16.0.0, react@>=16.3.0, react@>=16.6.0, react@>=16.8.0, react@>=17.0.0, react@>=18.0.0: - version "19.0.0" - resolved "https://registry.npmjs.org/react/-/react-19.0.0.tgz" - integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ== - -read-cache@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz" - integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== - dependencies: - pify "^2.3.0" - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -redux-thunk@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz" - integrity sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw== - -redux@^4.0.0: - version "4.2.0" - resolved "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz" - integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA== - dependencies: - "@babel/runtime" "^7.9.2" - -redux@^4.2.0: - version "4.2.1" - resolved "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz" - integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== - dependencies: - "@babel/runtime" "^7.9.2" - -redux@^5.0.0, redux@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz" - integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== - -reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: - version "1.0.10" - resolved "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz" - integrity sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw== - dependencies: - call-bind "^1.0.8" - define-properties "^1.2.1" - es-abstract "^1.23.9" - es-errors "^1.3.0" - es-object-atoms "^1.0.0" - get-intrinsic "^1.2.7" - get-proto "^1.0.1" - which-builtin-type "^1.2.1" - -regenerator-runtime@^0.13.4: - version "0.13.9" - resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz" - integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== - -regexp.prototype.flags@^1.4.1, regexp.prototype.flags@^1.5.4: - version "1.5.4" - resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz" - integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA== - dependencies: - call-bind "^1.0.8" - define-properties "^1.2.1" - es-errors "^1.3.0" - get-proto "^1.0.1" - gopd "^1.2.0" - set-function-name "^2.0.2" - -regexpp@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== - -reselect@^5.1.0, reselect@^5.1.1: - version "5.1.1" - resolved "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz" - integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w== - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-pkg-maps@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz" - integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== - -resolve@^1.1.7, resolve@^1.19.0, resolve@^1.22.8: - version "1.22.10" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz" - integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== - dependencies: - is-core-module "^2.16.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -resolve@^2.0.0-next.3: - version "2.0.0-next.4" - resolved "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz" - integrity sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ== - dependencies: - is-core-module "^2.9.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -resolve@^2.0.0-next.6: - version "2.0.0-next.6" - resolved "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz" - integrity sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA== - dependencies: - es-errors "^1.3.0" - is-core-module "^2.16.1" - node-exports-info "^1.6.0" - object-keys "^1.1.1" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -safe-array-concat@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz" - integrity sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.2" - get-intrinsic "^1.2.6" - has-symbols "^1.1.0" - isarray "^2.0.5" - -safe-buffer@^5.0.1: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-push-apply@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz" - integrity sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA== - dependencies: - es-errors "^1.3.0" - isarray "^2.0.5" - -safe-regex-test@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz" - integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== - dependencies: - call-bound "^1.0.2" - es-errors "^1.3.0" - is-regex "^1.2.1" - -scheduler@^0.25.0: - version "0.25.0" - resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz" - integrity sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA== - -semver@^6.3.0: - version "6.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^6.3.1: - version "6.3.1" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^7.3.7, semver@^7.5.4, semver@^7.7.1, semver@^7.7.3, semver@7.7.4: - version "7.7.4" - resolved "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz" - integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== - -serwist@^9.5.7, serwist@9.5.11: - version "9.5.11" - resolved "https://registry.npmjs.org/serwist/-/serwist-9.5.11.tgz" - integrity sha512-Bq6uwJFd4ET60BWI77v3VbazKHv6k7lECOiiCFwKyBu/slaCn0GHJ5L5RfsuJUKrnbD9lYUCDo6sqaKRM5M2vA== - dependencies: - "@serwist/utils" "9.5.11" - idb "8.0.3" - -set-function-length@^1.2.2: - version "1.2.2" - resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz" - integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - -set-function-name@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz" - integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - functions-have-names "^1.2.3" - has-property-descriptors "^1.0.2" - -set-proto@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz" - integrity sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw== - dependencies: - dunder-proto "^1.0.1" - es-errors "^1.3.0" - es-object-atoms "^1.0.0" - -sharp@^0.34.3: - version "0.34.5" - resolved "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz" - integrity sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg== - dependencies: - "@img/colour" "^1.0.0" - detect-libc "^2.1.2" - semver "^7.7.3" - optionalDependencies: - "@img/sharp-darwin-arm64" "0.34.5" - "@img/sharp-darwin-x64" "0.34.5" - "@img/sharp-libvips-darwin-arm64" "1.2.4" - "@img/sharp-libvips-darwin-x64" "1.2.4" - "@img/sharp-libvips-linux-arm" "1.2.4" - "@img/sharp-libvips-linux-arm64" "1.2.4" - "@img/sharp-libvips-linux-ppc64" "1.2.4" - "@img/sharp-libvips-linux-riscv64" "1.2.4" - "@img/sharp-libvips-linux-s390x" "1.2.4" - "@img/sharp-libvips-linux-x64" "1.2.4" - "@img/sharp-libvips-linuxmusl-arm64" "1.2.4" - "@img/sharp-libvips-linuxmusl-x64" "1.2.4" - "@img/sharp-linux-arm" "0.34.5" - "@img/sharp-linux-arm64" "0.34.5" - "@img/sharp-linux-ppc64" "0.34.5" - "@img/sharp-linux-riscv64" "0.34.5" - "@img/sharp-linux-s390x" "0.34.5" - "@img/sharp-linux-x64" "0.34.5" - "@img/sharp-linuxmusl-arm64" "0.34.5" - "@img/sharp-linuxmusl-x64" "0.34.5" - "@img/sharp-wasm32" "0.34.5" - "@img/sharp-win32-arm64" "0.34.5" - "@img/sharp-win32-ia32" "0.34.5" - "@img/sharp-win32-x64" "0.34.5" - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -side-channel-list@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz" - integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== - dependencies: - es-errors "^1.3.0" - object-inspect "^1.13.3" - -side-channel-map@^1.0.1: - 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== - dependencies: - call-bound "^1.0.2" - es-errors "^1.3.0" - get-intrinsic "^1.2.5" - object-inspect "^1.13.3" - -side-channel-weakmap@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz" - integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== - 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" - -side-channel@^1.0.4, side-channel@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz" - integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== - 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" - -signal-exit@^4.0.1: - version "4.1.0" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" - integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -sleep-promise@^9.1.0: - version "9.1.0" - resolved "https://registry.npmjs.org/sleep-promise/-/sleep-promise-9.1.0.tgz" - integrity sha512-UHYzVpz9Xn8b+jikYSD6bqvf754xL2uBUzDFwiU6NcdZeifPr6UfgU43xpkPu67VMS88+TI2PSI7Eohgqf2fKA== - -source-map-js@^1.0.2, source-map-js@^1.2.1: - 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== - -source-map@^0.5.7: - version "0.5.7" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" - integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== - -source-map@0.8.0-beta.0: - version "0.8.0-beta.0" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz" - integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== - dependencies: - whatwg-url "^7.0.0" - -split-on-first@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz" - integrity sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA== - -stable-hash@^0.0.5: - version "0.0.5" - resolved "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz" - integrity sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA== - -stop-iteration-iterator@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz" - integrity sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ== - dependencies: - es-errors "^1.3.0" - internal-slot "^1.1.0" - -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0: - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^5.0.1, string-width@^5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" - integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== - dependencies: - eastasianwidth "^0.2.0" - emoji-regex "^9.2.2" - strip-ansi "^7.0.1" - -string.prototype.matchall@^4.0.7: - version "4.0.7" - resolved "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz" - integrity sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - get-intrinsic "^1.1.1" - has-symbols "^1.0.3" - internal-slot "^1.0.3" - regexp.prototype.flags "^1.4.1" - side-channel "^1.0.4" - -string.prototype.trim@^1.2.10: - version "1.2.10" - resolved "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz" - integrity sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.2" - define-data-property "^1.1.4" - define-properties "^1.2.1" - es-abstract "^1.23.5" - es-object-atoms "^1.0.0" - has-property-descriptors "^1.0.2" - -string.prototype.trimend@^1.0.9: - version "1.0.9" - resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz" - integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.2" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - -string.prototype.trimstart@^1.0.8: - version "1.0.8" - resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz" - integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^7.0.1: - version "7.1.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" - integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== - dependencies: - ansi-regex "^6.0.1" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" - integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== - -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -styled-jsx@5.1.6: - version "5.1.6" - resolved "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz" - integrity sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA== - dependencies: - client-only "0.0.1" - -stylis@4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz" - integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== - -sucrase@^3.35.0: - version "3.35.0" - resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz" - integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA== - dependencies: - "@jridgewell/gen-mapping" "^0.3.2" - commander "^4.0.0" - glob "^10.3.10" - lines-and-columns "^1.1.6" - mz "^2.7.0" - pirates "^4.0.1" - ts-interface-checker "^0.1.9" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -tabbable@^6.0.0: - version "6.4.0" - resolved "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz" - integrity sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg== - -tagged-tag@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz" - integrity sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng== - -tailwindcss@^3.4.1, "tailwindcss@>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1", "tailwindcss@>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20", "tailwindcss@>=3.0.0 || insiders || >=4.0.0-alpha.20": - version "3.4.17" - resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz" - integrity sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og== - dependencies: - "@alloc/quick-lru" "^5.2.0" - arg "^5.0.2" - chokidar "^3.6.0" - didyoumean "^1.2.2" - dlv "^1.1.3" - fast-glob "^3.3.2" - glob-parent "^6.0.2" - is-glob "^4.0.3" - jiti "^1.21.6" - lilconfig "^3.1.3" - micromatch "^4.0.8" - normalize-path "^3.0.0" - object-hash "^3.0.0" - picocolors "^1.1.1" - postcss "^8.4.47" - postcss-import "^15.1.0" - postcss-js "^4.0.1" - postcss-load-config "^4.0.2" - postcss-nested "^6.2.0" - postcss-selector-parser "^6.1.2" - resolve "^1.22.8" - sucrase "^3.35.0" - -text-segmentation@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz" - integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw== - dependencies: - utrie "^1.0.2" - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== - -thenify-all@^1.0.0: - version "1.6.0" - resolved "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz" - integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== - dependencies: - thenify ">= 3.1.0 < 4" - -"thenify@>= 3.1.0 < 4": - version "3.3.1" - resolved "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz" - integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== - dependencies: - any-promise "^1.0.0" - -tiny-warning@^1.0.2: - version "1.0.3" - resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" - integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== - -tinyglobby@^0.2.13: - version "0.2.15" - resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz" - integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== - dependencies: - fdir "^6.5.0" - picomatch "^4.0.3" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -tr46@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz" - integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA== - dependencies: - punycode "^2.1.0" - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - -ts-interface-checker@^0.1.9: - version "0.1.13" - resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz" - integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== - -tsconfig-paths@^3.15.0: - version "3.15.0" - resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz" - integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== - dependencies: - "@types/json5" "^0.0.29" - json5 "^1.0.2" - minimist "^1.2.6" - strip-bom "^3.0.0" - -tslib@^1.8.1: - version "1.14.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tslib@^2.0.0, tslib@^2.4.0: - version "2.4.0" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz" - integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== - -tslib@^2.8.0: - version "2.8.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" - integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== - -tsutils@^3.21.0: - version "3.21.0" - resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" - integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== - dependencies: - tslib "^1.8.1" - -type-check@^0.4.0, type-check@~0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" - integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== - dependencies: - prelude-ls "^1.2.1" - -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - -type-fest@5.6.0: - version "5.6.0" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz" - integrity sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA== - dependencies: - tagged-tag "^1.0.0" - -typed-array-buffer@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz" - integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== - dependencies: - call-bound "^1.0.3" - es-errors "^1.3.0" - is-typed-array "^1.1.14" - -typed-array-byte-length@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz" - integrity sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg== - dependencies: - call-bind "^1.0.8" - for-each "^0.3.3" - gopd "^1.2.0" - has-proto "^1.2.0" - is-typed-array "^1.1.14" - -typed-array-byte-offset@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz" - integrity sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.8" - for-each "^0.3.3" - gopd "^1.2.0" - has-proto "^1.2.0" - is-typed-array "^1.1.15" - reflect.getprototypeof "^1.0.9" - -typed-array-length@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz" - integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg== - dependencies: - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - is-typed-array "^1.1.13" - possible-typed-array-names "^1.0.0" - reflect.getprototypeof "^1.0.6" - -typescript@^5, "typescript@^5 || ^6", typescript@^5.4.5, "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta", typescript@>=3.3.1, typescript@>=5.0.0: - version "5.9.3" - resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz" - integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== - -unbox-primitive@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz" - integrity sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw== - dependencies: - call-bound "^1.0.3" - has-bigints "^1.0.2" - has-symbols "^1.1.0" - which-boxed-primitive "^1.1.1" - -uncontrollable@^7.2.1: - version "7.2.1" - resolved "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz" - integrity sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ== - dependencies: - "@babel/runtime" "^7.6.3" - "@types/react" ">=16.9.11" - invariant "^2.2.4" - react-lifecycles-compat "^3.0.4" - -unrs-resolver@^1.6.2: - version "1.11.1" - resolved "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz" - integrity sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg== - dependencies: - napi-postinstall "^0.3.0" - optionalDependencies: - "@unrs/resolver-binding-android-arm-eabi" "1.11.1" - "@unrs/resolver-binding-android-arm64" "1.11.1" - "@unrs/resolver-binding-darwin-arm64" "1.11.1" - "@unrs/resolver-binding-darwin-x64" "1.11.1" - "@unrs/resolver-binding-freebsd-x64" "1.11.1" - "@unrs/resolver-binding-linux-arm-gnueabihf" "1.11.1" - "@unrs/resolver-binding-linux-arm-musleabihf" "1.11.1" - "@unrs/resolver-binding-linux-arm64-gnu" "1.11.1" - "@unrs/resolver-binding-linux-arm64-musl" "1.11.1" - "@unrs/resolver-binding-linux-ppc64-gnu" "1.11.1" - "@unrs/resolver-binding-linux-riscv64-gnu" "1.11.1" - "@unrs/resolver-binding-linux-riscv64-musl" "1.11.1" - "@unrs/resolver-binding-linux-s390x-gnu" "1.11.1" - "@unrs/resolver-binding-linux-x64-gnu" "1.11.1" - "@unrs/resolver-binding-linux-x64-musl" "1.11.1" - "@unrs/resolver-binding-wasm32-wasi" "1.11.1" - "@unrs/resolver-binding-win32-arm64-msvc" "1.11.1" - "@unrs/resolver-binding-win32-ia32-msvc" "1.11.1" - "@unrs/resolver-binding-win32-x64-msvc" "1.11.1" - -update-browserslist-db@^1.2.3: - 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== - dependencies: - escalade "^3.2.0" - picocolors "^1.1.1" - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -use-is-mounted-ref@^1.5.0: - version "1.5.0" - resolved "https://registry.npmjs.org/use-is-mounted-ref/-/use-is-mounted-ref-1.5.0.tgz" - integrity sha512-p5FksHf/ospZUr5KU9ese6u3jp9fzvZ3wuSb50i0y6fdONaHWgmOqQtxR/PUcwi6hnhQDbNxWSg3eTK3N6m+dg== - -use-isomorphic-layout-effect@^1.1.1, use-isomorphic-layout-effect@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.0.tgz" - integrity sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w== - -use-latest@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz" - integrity sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ== - dependencies: - use-isomorphic-layout-effect "^1.1.1" - -use-sync-external-store@^1.0.0, use-sync-external-store@^1.4.0: - version "1.6.0" - resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz" - integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== - -util-deprecate@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -utrie@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz" - integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw== - dependencies: - base64-arraybuffer "^1.0.2" - -uuid@^9.0.0: - version "9.0.1" - resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" - integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== - -void-elements@3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz" - integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== - -warning@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz" - integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== - dependencies: - loose-envify "^1.0.0" - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -webidl-conversions@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz" - integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -whatwg-url@^7.0.0: - version "7.1.0" - resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz" - integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== - dependencies: - lodash.sortby "^4.7.0" - tr46 "^1.0.1" - webidl-conversions "^4.0.2" - -which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz" - integrity sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA== - dependencies: - is-bigint "^1.1.0" - is-boolean-object "^1.2.1" - is-number-object "^1.1.1" - is-string "^1.1.1" - is-symbol "^1.1.1" - -which-builtin-type@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz" - integrity sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q== - dependencies: - call-bound "^1.0.2" - function.prototype.name "^1.1.6" - has-tostringtag "^1.0.2" - is-async-function "^2.0.0" - is-date-object "^1.1.0" - is-finalizationregistry "^1.1.0" - is-generator-function "^1.0.10" - is-regex "^1.2.1" - is-weakref "^1.0.2" - isarray "^2.0.5" - which-boxed-primitive "^1.1.0" - which-collection "^1.0.2" - which-typed-array "^1.1.16" - -which-collection@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz" - integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== - dependencies: - is-map "^2.0.3" - is-set "^2.0.3" - is-weakmap "^2.0.2" - is-weakset "^2.0.3" - -which-typed-array@^1.1.16, which-typed-array@^1.1.19: - version "1.1.20" - resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz" - integrity sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.8" - call-bound "^1.0.4" - for-each "^0.3.5" - get-proto "^1.0.1" - gopd "^1.2.0" - has-tostringtag "^1.0.2" - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -word-wrap@^1.2.3: - version "1.2.3" - resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^8.1.0: - version "8.1.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" - integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== - dependencies: - ansi-styles "^6.1.0" - string-width "^5.0.1" - strip-ansi "^7.0.1" - -wrappy@1: - version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -yaml@^1.10.0: - version "1.10.2" - resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - -yaml@^2.3.4: - version "2.7.0" - resolved "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz" - integrity sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA== - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -zod@^4.3.6: - version "4.4.3" - resolved "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz" - integrity sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ== - -zod@4.4.1: - version "4.4.1" - resolved "https://registry.npmjs.org/zod/-/zod-4.4.1.tgz" - integrity sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q== diff --git a/package.json b/package.json index de78912..43c1445 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "app", "version": "0.0.1", "scripts": { - "build:production": "cd ./frontend && yarn install && yarn run build && cd ../backend && yarn install", - "start:production": "cd ./backend && NODE_ENV=production yarn start" + "build:production": "cd ./frontend && npm ci && npm run build && cd ../backend && npm ci && npm run build", + "start:production": "cd ./backend && npm run start" } }