From 8cf57d7878bdb8c0d7093489137e243a631bf1ef Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Sun, 19 Jul 2026 13:09:04 +0200 Subject: [PATCH] Initial commit --- .dockerignore | 14 + .editorconfig | 12 + .env.example | 37 + .gitignore | 10 + .nvmrc | 1 + .prettierignore | 5 + .prettierrc | 6 + AGENTS.md | 27 + Dockerfile | 30 + README.md | 122 + apps/backend/.prettierrc | 4 + apps/backend/README.md | 40 + apps/backend/eslint.config.mjs | 35 + apps/backend/nest-cli.json | 9 + apps/backend/package.json | 43 + apps/backend/src/app.module.ts | 101 + apps/backend/src/audit/audit.controller.ts | 15 + apps/backend/src/audit/audit.module.ts | 14 + apps/backend/src/audit/audit.service.ts | 31 + .../src/audit/entities/audit-log.entity.ts | 49 + .../audit/repositories/audit.repository.ts | 24 + apps/backend/src/auth/auth.controller.ts | 69 + apps/backend/src/auth/auth.module.ts | 27 + apps/backend/src/auth/auth.service.spec.ts | 50 + apps/backend/src/auth/auth.service.ts | 328 + .../backend/src/auth/authenticated-request.ts | 12 + .../auth/entities/oidc-login-state.entity.ts | 19 + apps/backend/src/auth/guards/csrf.guard.ts | 49 + .../src/auth/guards/permissions.guard.ts | 68 + .../src/auth/guards/public.decorator.ts | 4 + .../guards/require-permissions.decorator.ts | 6 + apps/backend/src/auth/oidc.types.ts | 23 + apps/backend/src/common/dto/pagination.dto.ts | 28 + apps/backend/src/common/errors/api-error.ts | 27 + .../src/common/errors/api-exception.filter.ts | 66 + apps/backend/src/common/errors/error-codes.ts | 29 + .../src/common/http/external-http-client.ts | 55 + .../sensitive-rate-limit.decorator.ts | 6 + .../common/request-context/request-context.ts | 12 + .../request-context/request-id.middleware.ts | 15 + .../src/common/security/validation.pipe.ts | 42 + apps/backend/src/config/config.module.ts | 20 + .../backend/src/config/config.service.spec.ts | 104 + apps/backend/src/config/config.service.ts | 102 + apps/backend/src/config/config.types.ts | 46 + apps/backend/src/config/env.ts | 157 + .../src/dashboard/dashboard.controller.ts | 15 + .../backend/src/dashboard/dashboard.module.ts | 9 + .../src/dashboard/dashboard.service.ts | 32 + apps/backend/src/database/database.module.ts | 8 + apps/backend/src/database/entities.ts | 21 + .../src/database/migration-health.service.ts | 41 + .../migrations/1720000000000-InitialSchema.ts | 145 + .../1720000001000-AddNotifications.ts | 31 + .../1720000002000-AddRoleDescription.ts | 16 + apps/backend/src/database/run-migrations.ts | 13 + .../src/database/typeorm-cli.datasource.ts | 29 + apps/backend/src/database/typeorm-options.ts | 30 + apps/backend/src/health/health.controller.ts | 21 + apps/backend/src/health/health.module.ts | 9 + apps/backend/src/items/dto/item.dto.ts | 58 + .../backend/src/items/entities/item.entity.ts | 49 + apps/backend/src/items/items.controller.ts | 53 + apps/backend/src/items/items.module.ts | 19 + apps/backend/src/items/items.service.ts | 118 + .../items/repositories/items.repository.ts | 45 + .../src/items/tests/items.service.spec.ts | 63 + apps/backend/src/main.ts | 99 + .../src/notifications/dto/notification.dto.ts | 78 + .../entities/notification.entity.ts | 62 + .../src/notifications/notification-types.ts | 15 + .../notifications/notifications.controller.ts | 95 + .../src/notifications/notifications.module.ts | 20 + .../notifications/notifications.service.ts | 287 + .../repositories/notifications.repository.ts | 79 + .../tests/notification-integrations.spec.ts | 124 + .../tests/notifications.service.spec.ts | 271 + .../src/roles/admin-roles.controller.ts | 71 + apps/backend/src/roles/dto/role.dto.ts | 19 + .../src/roles/entities/permission.entity.ts | 11 + .../backend/src/roles/entities/role.entity.ts | 45 + apps/backend/src/roles/permissions.ts | 29 + .../roles/repositories/roles.repository.ts | 42 + apps/backend/src/roles/roles.controller.ts | 46 + apps/backend/src/roles/roles.module.ts | 20 + apps/backend/src/roles/roles.service.ts | 311 + apps/backend/src/sessions/dto/session.dto.ts | 9 + .../src/sessions/entities/session.entity.ts | 69 + .../repositories/sessions.repository.ts | 78 + .../src/sessions/session-crypto.service.ts | 54 + .../src/sessions/sessions.controller.ts | 61 + apps/backend/src/sessions/sessions.module.ts | 16 + apps/backend/src/sessions/sessions.service.ts | 209 + .../tests/session-crypto.service.spec.ts | 27 + apps/backend/src/users/dto/user.dto.ts | 69 + .../users/entities/user-settings.entity.ts | 24 + .../backend/src/users/entities/user.entity.ts | 70 + .../users/repositories/users.repository.ts | 112 + .../src/users/tests/users.service.spec.ts | 45 + apps/backend/src/users/users.controller.ts | 165 + apps/backend/src/users/users.module.ts | 25 + apps/backend/src/users/users.service.ts | 416 + apps/backend/tsconfig.build.json | 4 + apps/backend/tsconfig.json | 18 + apps/backend/vitest.config.ts | 12 + apps/frontend/.editorconfig | 17 + apps/frontend/.gitignore | 44 + apps/frontend/.prettierrc | 12 + apps/frontend/.vscode/extensions.json | 4 + apps/frontend/.vscode/launch.json | 20 + apps/frontend/.vscode/tasks.json | 42 + apps/frontend/README.md | 35 + apps/frontend/angular.json | 83 + apps/frontend/package.json | 24 + apps/frontend/proxy.conf.json | 12 + apps/frontend/public/favicon.ico | Bin 0 -> 15086 bytes apps/frontend/src/app/app.config.ts | 17 + apps/frontend/src/app/app.html | 1 + apps/frontend/src/app/app.routes.ts | 143 + apps/frontend/src/app/app.scss | 0 apps/frontend/src/app/app.spec.ts | 18 + apps/frontend/src/app/app.ts | 9 + .../src/app/core/auth.service.spec.ts | 35 + apps/frontend/src/app/core/auth.service.ts | 43 + .../frontend/src/app/core/csrf.interceptor.ts | 17 + apps/frontend/src/app/core/dev-only.guard.ts | 8 + apps/frontend/src/app/core/dev-routes.prod.ts | 3 + apps/frontend/src/app/core/dev-routes.ts | 12 + .../src/app/core/notification.store.spec.ts | 139 + .../src/app/core/notification.store.ts | 213 + .../frontend/src/app/core/permission.guard.ts | 21 + apps/frontend/src/app/core/title.strategy.ts | 15 + .../account/account-security.page.spec.ts | 197 + .../features/account/account-security.page.ts | 324 + .../app/features/admin/admin-permissions.ts | 57 + .../admin/admin-role-detail.page.spec.ts | 34 + .../features/admin/admin-role-detail.page.ts | 274 + .../app/features/admin/admin-roles.page.ts | 154 + .../features/admin/admin-user-detail.page.ts | 368 + .../features/admin/admin-users.page.spec.ts | 46 + .../app/features/admin/admin-users.page.ts | 270 + .../src/app/features/audit/audit.page.ts | 39 + .../app/features/dashboard/dashboard.page.ts | 36 + .../app/features/dev/design-system.page.ts | 207 + .../src/app/features/errors/error.page.ts | 15 + .../src/app/features/errors/forbidden.page.ts | 15 + .../src/app/features/errors/not-found.page.ts | 15 + .../src/app/features/items/items.page.spec.ts | 71 + .../src/app/features/items/items.page.ts | 169 + .../notifications/notifications.page.spec.ts | 54 + .../notifications/notifications.page.ts | 168 + .../src/app/features/profile/profile.page.ts | 83 + .../src/app/features/roles/roles.page.ts | 149 + .../app/features/sessions/sessions.page.ts | 58 + .../src/app/features/users/users.page.ts | 98 + apps/frontend/src/app/layout/app-shell.html | 36 + apps/frontend/src/app/layout/app-shell.scss | 98 + .../frontend/src/app/layout/app-shell.spec.ts | 102 + apps/frontend/src/app/layout/app-shell.ts | 296 + .../src/app/layout/notification-panel.spec.ts | 79 + .../src/app/layout/notification-panel.ts | 169 + .../app/shared/ui/button/button.component.ts | 38 + .../confirm-dialog.component.ts | 146 + .../ui/empty-state/empty-state.component.ts | 53 + .../ui/form-field/form-field.component.ts | 32 + .../ui/icon-button/icon-button.component.ts | 41 + .../src/app/shared/ui/icon/icon.component.ts | 90 + apps/frontend/src/app/shared/ui/index.ts | 12 + .../loading-state/loading-state.component.ts | 31 + .../ui/page-header/page-header.component.ts | 27 + .../ui/pagination/pagination.component.ts | 54 + .../ui/status-badge/status-badge.component.ts | 30 + .../shared/ui/toast/toast-host.component.ts | 79 + .../src/app/shared/ui/toast/toast.service.ts | 29 + .../src/app/shared/ui/ui-components.spec.ts | 135 + apps/frontend/src/index.html | 13 + apps/frontend/src/main.ts | 5 + apps/frontend/src/styles.scss | 9 + apps/frontend/src/styles/_animations.scss | 25 + apps/frontend/src/styles/_buttons.scss | 90 + apps/frontend/src/styles/_forms.scss | 65 + apps/frontend/src/styles/_layout.scss | 121 + apps/frontend/src/styles/_reset.scss | 70 + apps/frontend/src/styles/_tables.scss | 34 + apps/frontend/src/styles/_tokens.scss | 76 + apps/frontend/src/styles/_typography.scss | 44 + apps/frontend/src/styles/_utilities.scss | 72 + apps/frontend/src/test-setup.ts | 8 + apps/frontend/tsconfig.app.json | 10 + apps/frontend/tsconfig.json | 28 + apps/frontend/tsconfig.spec.json | 10 + apps/frontend/vitest.config.ts | 13 + compose.yml | 29 + docs/admin.md | 121 + docs/architecture.md | 129 + docs/configuration.md | 118 + docs/deployment.md | 139 + docs/design-system.md | 114 + docs/development.md | 119 + docs/getting-started.md | 93 + docs/nginx-example.conf | 32 + docs/notifications.md | 146 + docs/security.md | 135 + docs/using-as-template.md | 97 + eslint.config.mjs | 52 + package-lock.json | 15734 ++++++++++++++++ package.json | 63 + packages/api-client/package.json | 23 + packages/api-client/src/api-client.service.ts | 343 + packages/api-client/src/models.ts | 154 + packages/api-client/src/public-api.ts | 2 + packages/api-client/tsconfig.json | 12 + scripts/copy-frontend.mjs | 10 + scripts/generate-api-client.mjs | 546 + tsconfig.base.json | 23 + 215 files changed, 30417 insertions(+) create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .nvmrc create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 AGENTS.md create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 apps/backend/.prettierrc create mode 100644 apps/backend/README.md create mode 100644 apps/backend/eslint.config.mjs create mode 100644 apps/backend/nest-cli.json create mode 100644 apps/backend/package.json create mode 100644 apps/backend/src/app.module.ts create mode 100644 apps/backend/src/audit/audit.controller.ts create mode 100644 apps/backend/src/audit/audit.module.ts create mode 100644 apps/backend/src/audit/audit.service.ts create mode 100644 apps/backend/src/audit/entities/audit-log.entity.ts create mode 100644 apps/backend/src/audit/repositories/audit.repository.ts create mode 100644 apps/backend/src/auth/auth.controller.ts create mode 100644 apps/backend/src/auth/auth.module.ts create mode 100644 apps/backend/src/auth/auth.service.spec.ts create mode 100644 apps/backend/src/auth/auth.service.ts create mode 100644 apps/backend/src/auth/authenticated-request.ts create mode 100644 apps/backend/src/auth/entities/oidc-login-state.entity.ts create mode 100644 apps/backend/src/auth/guards/csrf.guard.ts create mode 100644 apps/backend/src/auth/guards/permissions.guard.ts create mode 100644 apps/backend/src/auth/guards/public.decorator.ts create mode 100644 apps/backend/src/auth/guards/require-permissions.decorator.ts create mode 100644 apps/backend/src/auth/oidc.types.ts create mode 100644 apps/backend/src/common/dto/pagination.dto.ts create mode 100644 apps/backend/src/common/errors/api-error.ts create mode 100644 apps/backend/src/common/errors/api-exception.filter.ts create mode 100644 apps/backend/src/common/errors/error-codes.ts create mode 100644 apps/backend/src/common/http/external-http-client.ts create mode 100644 apps/backend/src/common/rate-limit/sensitive-rate-limit.decorator.ts create mode 100644 apps/backend/src/common/request-context/request-context.ts create mode 100644 apps/backend/src/common/request-context/request-id.middleware.ts create mode 100644 apps/backend/src/common/security/validation.pipe.ts create mode 100644 apps/backend/src/config/config.module.ts create mode 100644 apps/backend/src/config/config.service.spec.ts create mode 100644 apps/backend/src/config/config.service.ts create mode 100644 apps/backend/src/config/config.types.ts create mode 100644 apps/backend/src/config/env.ts create mode 100644 apps/backend/src/dashboard/dashboard.controller.ts create mode 100644 apps/backend/src/dashboard/dashboard.module.ts create mode 100644 apps/backend/src/dashboard/dashboard.service.ts create mode 100644 apps/backend/src/database/database.module.ts create mode 100644 apps/backend/src/database/entities.ts create mode 100644 apps/backend/src/database/migration-health.service.ts create mode 100644 apps/backend/src/database/migrations/1720000000000-InitialSchema.ts create mode 100644 apps/backend/src/database/migrations/1720000001000-AddNotifications.ts create mode 100644 apps/backend/src/database/migrations/1720000002000-AddRoleDescription.ts create mode 100644 apps/backend/src/database/run-migrations.ts create mode 100644 apps/backend/src/database/typeorm-cli.datasource.ts create mode 100644 apps/backend/src/database/typeorm-options.ts create mode 100644 apps/backend/src/health/health.controller.ts create mode 100644 apps/backend/src/health/health.module.ts create mode 100644 apps/backend/src/items/dto/item.dto.ts create mode 100644 apps/backend/src/items/entities/item.entity.ts create mode 100644 apps/backend/src/items/items.controller.ts create mode 100644 apps/backend/src/items/items.module.ts create mode 100644 apps/backend/src/items/items.service.ts create mode 100644 apps/backend/src/items/repositories/items.repository.ts create mode 100644 apps/backend/src/items/tests/items.service.spec.ts create mode 100644 apps/backend/src/main.ts create mode 100644 apps/backend/src/notifications/dto/notification.dto.ts create mode 100644 apps/backend/src/notifications/entities/notification.entity.ts create mode 100644 apps/backend/src/notifications/notification-types.ts create mode 100644 apps/backend/src/notifications/notifications.controller.ts create mode 100644 apps/backend/src/notifications/notifications.module.ts create mode 100644 apps/backend/src/notifications/notifications.service.ts create mode 100644 apps/backend/src/notifications/repositories/notifications.repository.ts create mode 100644 apps/backend/src/notifications/tests/notification-integrations.spec.ts create mode 100644 apps/backend/src/notifications/tests/notifications.service.spec.ts create mode 100644 apps/backend/src/roles/admin-roles.controller.ts create mode 100644 apps/backend/src/roles/dto/role.dto.ts create mode 100644 apps/backend/src/roles/entities/permission.entity.ts create mode 100644 apps/backend/src/roles/entities/role.entity.ts create mode 100644 apps/backend/src/roles/permissions.ts create mode 100644 apps/backend/src/roles/repositories/roles.repository.ts create mode 100644 apps/backend/src/roles/roles.controller.ts create mode 100644 apps/backend/src/roles/roles.module.ts create mode 100644 apps/backend/src/roles/roles.service.ts create mode 100644 apps/backend/src/sessions/dto/session.dto.ts create mode 100644 apps/backend/src/sessions/entities/session.entity.ts create mode 100644 apps/backend/src/sessions/repositories/sessions.repository.ts create mode 100644 apps/backend/src/sessions/session-crypto.service.ts create mode 100644 apps/backend/src/sessions/sessions.controller.ts create mode 100644 apps/backend/src/sessions/sessions.module.ts create mode 100644 apps/backend/src/sessions/sessions.service.ts create mode 100644 apps/backend/src/sessions/tests/session-crypto.service.spec.ts create mode 100644 apps/backend/src/users/dto/user.dto.ts create mode 100644 apps/backend/src/users/entities/user-settings.entity.ts create mode 100644 apps/backend/src/users/entities/user.entity.ts create mode 100644 apps/backend/src/users/repositories/users.repository.ts create mode 100644 apps/backend/src/users/tests/users.service.spec.ts create mode 100644 apps/backend/src/users/users.controller.ts create mode 100644 apps/backend/src/users/users.module.ts create mode 100644 apps/backend/src/users/users.service.ts create mode 100644 apps/backend/tsconfig.build.json create mode 100644 apps/backend/tsconfig.json create mode 100644 apps/backend/vitest.config.ts create mode 100644 apps/frontend/.editorconfig create mode 100644 apps/frontend/.gitignore create mode 100644 apps/frontend/.prettierrc create mode 100644 apps/frontend/.vscode/extensions.json create mode 100644 apps/frontend/.vscode/launch.json create mode 100644 apps/frontend/.vscode/tasks.json create mode 100644 apps/frontend/README.md create mode 100644 apps/frontend/angular.json create mode 100644 apps/frontend/package.json create mode 100644 apps/frontend/proxy.conf.json create mode 100644 apps/frontend/public/favicon.ico create mode 100644 apps/frontend/src/app/app.config.ts create mode 100644 apps/frontend/src/app/app.html create mode 100644 apps/frontend/src/app/app.routes.ts create mode 100644 apps/frontend/src/app/app.scss create mode 100644 apps/frontend/src/app/app.spec.ts create mode 100644 apps/frontend/src/app/app.ts create mode 100644 apps/frontend/src/app/core/auth.service.spec.ts create mode 100644 apps/frontend/src/app/core/auth.service.ts create mode 100644 apps/frontend/src/app/core/csrf.interceptor.ts create mode 100644 apps/frontend/src/app/core/dev-only.guard.ts create mode 100644 apps/frontend/src/app/core/dev-routes.prod.ts create mode 100644 apps/frontend/src/app/core/dev-routes.ts create mode 100644 apps/frontend/src/app/core/notification.store.spec.ts create mode 100644 apps/frontend/src/app/core/notification.store.ts create mode 100644 apps/frontend/src/app/core/permission.guard.ts create mode 100644 apps/frontend/src/app/core/title.strategy.ts create mode 100644 apps/frontend/src/app/features/account/account-security.page.spec.ts create mode 100644 apps/frontend/src/app/features/account/account-security.page.ts create mode 100644 apps/frontend/src/app/features/admin/admin-permissions.ts create mode 100644 apps/frontend/src/app/features/admin/admin-role-detail.page.spec.ts create mode 100644 apps/frontend/src/app/features/admin/admin-role-detail.page.ts create mode 100644 apps/frontend/src/app/features/admin/admin-roles.page.ts create mode 100644 apps/frontend/src/app/features/admin/admin-user-detail.page.ts create mode 100644 apps/frontend/src/app/features/admin/admin-users.page.spec.ts create mode 100644 apps/frontend/src/app/features/admin/admin-users.page.ts create mode 100644 apps/frontend/src/app/features/audit/audit.page.ts create mode 100644 apps/frontend/src/app/features/dashboard/dashboard.page.ts create mode 100644 apps/frontend/src/app/features/dev/design-system.page.ts create mode 100644 apps/frontend/src/app/features/errors/error.page.ts create mode 100644 apps/frontend/src/app/features/errors/forbidden.page.ts create mode 100644 apps/frontend/src/app/features/errors/not-found.page.ts create mode 100644 apps/frontend/src/app/features/items/items.page.spec.ts create mode 100644 apps/frontend/src/app/features/items/items.page.ts create mode 100644 apps/frontend/src/app/features/notifications/notifications.page.spec.ts create mode 100644 apps/frontend/src/app/features/notifications/notifications.page.ts create mode 100644 apps/frontend/src/app/features/profile/profile.page.ts create mode 100644 apps/frontend/src/app/features/roles/roles.page.ts create mode 100644 apps/frontend/src/app/features/sessions/sessions.page.ts create mode 100644 apps/frontend/src/app/features/users/users.page.ts create mode 100644 apps/frontend/src/app/layout/app-shell.html create mode 100644 apps/frontend/src/app/layout/app-shell.scss create mode 100644 apps/frontend/src/app/layout/app-shell.spec.ts create mode 100644 apps/frontend/src/app/layout/app-shell.ts create mode 100644 apps/frontend/src/app/layout/notification-panel.spec.ts create mode 100644 apps/frontend/src/app/layout/notification-panel.ts create mode 100644 apps/frontend/src/app/shared/ui/button/button.component.ts create mode 100644 apps/frontend/src/app/shared/ui/confirm-dialog/confirm-dialog.component.ts create mode 100644 apps/frontend/src/app/shared/ui/empty-state/empty-state.component.ts create mode 100644 apps/frontend/src/app/shared/ui/form-field/form-field.component.ts create mode 100644 apps/frontend/src/app/shared/ui/icon-button/icon-button.component.ts create mode 100644 apps/frontend/src/app/shared/ui/icon/icon.component.ts create mode 100644 apps/frontend/src/app/shared/ui/index.ts create mode 100644 apps/frontend/src/app/shared/ui/loading-state/loading-state.component.ts create mode 100644 apps/frontend/src/app/shared/ui/page-header/page-header.component.ts create mode 100644 apps/frontend/src/app/shared/ui/pagination/pagination.component.ts create mode 100644 apps/frontend/src/app/shared/ui/status-badge/status-badge.component.ts create mode 100644 apps/frontend/src/app/shared/ui/toast/toast-host.component.ts create mode 100644 apps/frontend/src/app/shared/ui/toast/toast.service.ts create mode 100644 apps/frontend/src/app/shared/ui/ui-components.spec.ts create mode 100644 apps/frontend/src/index.html create mode 100644 apps/frontend/src/main.ts create mode 100644 apps/frontend/src/styles.scss create mode 100644 apps/frontend/src/styles/_animations.scss create mode 100644 apps/frontend/src/styles/_buttons.scss create mode 100644 apps/frontend/src/styles/_forms.scss create mode 100644 apps/frontend/src/styles/_layout.scss create mode 100644 apps/frontend/src/styles/_reset.scss create mode 100644 apps/frontend/src/styles/_tables.scss create mode 100644 apps/frontend/src/styles/_tokens.scss create mode 100644 apps/frontend/src/styles/_typography.scss create mode 100644 apps/frontend/src/styles/_utilities.scss create mode 100644 apps/frontend/src/test-setup.ts create mode 100644 apps/frontend/tsconfig.app.json create mode 100644 apps/frontend/tsconfig.json create mode 100644 apps/frontend/tsconfig.spec.json create mode 100644 apps/frontend/vitest.config.ts create mode 100644 compose.yml create mode 100644 docs/admin.md create mode 100644 docs/architecture.md create mode 100644 docs/configuration.md create mode 100644 docs/deployment.md create mode 100644 docs/design-system.md create mode 100644 docs/development.md create mode 100644 docs/getting-started.md create mode 100644 docs/nginx-example.conf create mode 100644 docs/notifications.md create mode 100644 docs/security.md create mode 100644 docs/using-as-template.md create mode 100644 eslint.config.mjs create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 packages/api-client/package.json create mode 100644 packages/api-client/src/api-client.service.ts create mode 100644 packages/api-client/src/models.ts create mode 100644 packages/api-client/src/public-api.ts create mode 100644 packages/api-client/tsconfig.json create mode 100644 scripts/copy-frontend.mjs create mode 100644 scripts/generate-api-client.mjs create mode 100644 tsconfig.base.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..868b834 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git +node_modules +**/node_modules +dist +**/dist +apps/backend/public +coverage +**/coverage +.angular +**/.angular +.env +.env.* +!.env.example +npm-debug.log* diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8c52ff9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5331aa6 --- /dev/null +++ b/.env.example @@ -0,0 +1,37 @@ +NODE_ENV=development +PORT=3000 +APP_BASE_URL=http://localhost:3000 +FRONTEND_BASE_URL=http://localhost:4200 +TRUST_PROXY=false + +DATABASE_HOST=mysql.example.internal +DATABASE_PORT=3306 +DATABASE_NAME=business_app +DATABASE_USER=business_app +DATABASE_PASSWORD=change-me +DATABASE_SSL=false + +OIDC_ISSUER=https://idp.example.com/realms/internal +OIDC_CLIENT_ID=business-app +OIDC_CLIENT_SECRET=change-me +OIDC_SCOPES=openid profile email +OIDC_LOGOUT_URL= +OIDC_ALLOWED_ALGORITHMS=RS256 +OIDC_HTTP_TIMEOUT_MS=5000 + +SESSION_COOKIE_NAME=app_session +SESSION_IDLE_TIMEOUT_SECONDS=28800 +SESSION_ABSOLUTE_TIMEOUT_SECONDS=604800 +SESSION_SECRET=replace-with-at-least-32-random-bytes +SESSION_ENCRYPTION_KEY=replace-with-32-byte-base64url-key + +CORS_ORIGINS=http://localhost:4200,http://localhost:3000 +CSRF_HEADER_NAME=X-CSRF-Token + +LOG_LEVEL=info +SWAGGER_ENABLED=true + +RATE_LIMIT_WINDOW_SECONDS=60 +RATE_LIMIT_MAX_REQUESTS=300 +RATE_LIMIT_SENSITIVE_WINDOW_SECONDS=60 +RATE_LIMIT_SENSITIVE_MAX_REQUESTS=10 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..91b6278 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +dist/ +apps/backend/public/ +coverage/ +.angular/ +.env +.env.* +!.env.example +npm-debug.log* +*.local diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..ca5c350 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24.18.0 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..a8c4708 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +dist +coverage +node_modules +package-lock.json +apps/frontend/.angular diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..521e271 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "semi": true, + "printWidth": 100, + "trailingComma": "all" +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ca0622b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,27 @@ +# Arbeitsregeln fuer Codex + +- Das Repository ist ein npm-Workspace-Monorepo mit `apps/frontend`, `apps/backend` und `packages/api-client`; keine Nx-Einfuehrung. +- TypeScript bleibt strikt. Kein `any`, keine unnoetigen Non-Null-Assertions. +- Backend-Controller verwenden niemals TypeORM-Repositories direkt, sondern Services. +- Services kapseln Fachlogik; Datenbankzugriffe laufen ueber Repository-Klassen oder klar benannte Persistence-Services. +- Keine UI-Library einsetzen. Angular bleibt mobile-first, mit eigenem HTML und SCSS. +- Neue UI muss die Design Tokens aus `apps/frontend/src/styles/_tokens.scss` verwenden. +- Keine direkten Hex-Farben in Feature-Komponenten; Farben werden semantisch ueber CSS Custom Properties genutzt. +- Bestehende UI-Komponenten unter `apps/frontend/src/app/shared/ui` wiederverwenden, bevor neue Abstraktionen entstehen. +- Interaktive UI muss per Tastatur bedienbar sein; Fokuszustaende duerfen nicht entfernt werden. +- Informationen duerfen nicht ausschliesslich ueber Farbe vermittelt werden. +- Kein `::ng-deep`, keine unkontrollierten `!important`-Regeln und keine unnoetigen Utility-Klassen. +- OIDC-Tokens bleiben ausschliesslich im Backend. Der Browser erhaelt nur Session-Cookie und CSRF-Token. +- Sessions liegen serverseitig in MySQL. Session-Cookies enthalten keine Tokens oder sensiblen Daten. +- Permissions sind im Code definiert. Benutzer haben keine direkten Permissions, sondern Rollen. +- Admin-Aktionen duerfen den letzten aktiven Administrator nicht entfernen; entsprechende Benutzer- und Rollen-Aenderungen muessen transaktional abgesichert bleiben. +- Admin-Controller verwenden `@RequirePermissions(...)` und delegieren an Services; Benutzer, Rollen und Sessions werden nicht direkt aus Controllern ueber Repositories veraendert. +- Benachrichtigungen sind benutzerbezogene Daten. Zugriffe muessen serverseitig ueber den aktuellen Session-Benutzer eingeschraenkt werden; keine normalen Endpunkte mit frei uebergebener `userId`. +- Benachrichtigungen werden ueber `NotificationsService` erzeugt, nicht direkt ueber TypeORM-Repositories in Controllern oder fremden Modulen. +- Das Backend ist fuer Authentifizierung, Autorisierung und CSRF verbindlich; Angular nutzt Permissions nur zur Darstellung. +- Migrationen werden niemals automatisch beim normalen App-Start ausgefuehrt. Der Start prueft nur auf fehlende Migrationen. +- Secrets, Session-IDs und Tokens duerfen nicht geloggt oder ins Repository aufgenommen werden. +- Tests muessen Verhalten pruefen; keine trivialen Tests, die nur Existenz testen. +- Keine Auth- oder CSRF-Umgehung fuer Entwicklung oder Tests in Production-Code einbauen. +- Keine WebSockets, Queues, Redis, E-Mail-, SMS- oder Push-Versand fuer In-App-Benachrichtigungen einfuehren, solange dies nicht explizit architektonisch entschieden wurde. +- Vor Abschluss muessen `npm run lint`, `npm run format:check`, `npm run typecheck`, `npm test`, `npm run build` und `docker build .` bestehen. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7604069 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM node:24.18.0-alpine AS build +WORKDIR /app + +COPY package.json package-lock.json ./ +COPY apps/frontend/package.json apps/frontend/package.json +COPY apps/backend/package.json apps/backend/package.json +COPY packages/api-client/package.json packages/api-client/package.json +RUN npm ci + +COPY . . +RUN npm run build +RUN mkdir -p apps/backend/dist/public \ + && cp -R apps/frontend/dist/frontend/browser/* apps/backend/dist/public/ \ + && npm prune --omit=dev --workspaces --include-workspace-root + +FROM node:24.18.0-alpine AS runtime +ENV NODE_ENV=production +WORKDIR /app + +RUN addgroup -S app && adduser -S app -G app + +COPY --from=build --chown=app:app /app/package.json /app/package-lock.json ./ +COPY --from=build --chown=app:app /app/node_modules ./node_modules +COPY --from=build --chown=app:app /app/apps/backend/package.json ./apps/backend/package.json +COPY --from=build --chown=app:app /app/apps/backend/dist ./apps/backend/dist + +USER app +EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD node -e "fetch('http://127.0.0.1:3000/health/ready').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" +CMD ["node", "apps/backend/dist/main.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..c217d46 --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +# Angular/NestJS Business Boilerplate + +Production-ready npm-Workspace-Monorepo fuer interne Business-Anwendungen mit +Angular 22, NestJS 11, Node.js 24.18 LTS, TypeORM, MySQL 8, OIDC, serverseitigen +Sessions und Vitest. + +Dieses Repository ist als Startpunkt fuer eigene Projekte gedacht. Es bringt die +technischen Grundentscheidungen mit, die in internen Business-Anwendungen haeufig +spaet und teuer nachgezogen werden: Backend-for-Frontend Authentifizierung, +rollenbasierte Autorisierung, CSRF-Schutz, serverseitige Sessions, Migrationen, +Docker-Auslieferung, Healthchecks und eine einfache Angular-Verwaltungsoberflaeche. + +## Was enthalten ist + +- npm Workspaces ohne Nx: `apps/frontend`, `apps/backend`, `packages/api-client` +- Angular Standalone Components, mobile-first SCSS und keine UI-Library +- NestJS API mit modularen Features, Services, Repositories, Guards und DTOs +- OIDC Authorization Code Flow mit PKCE; Tokens bleiben ausschliesslich im Backend +- HttpOnly Session-Cookie plus CSRF-Cookie/Header fuer schreibende Requests +- konfigurierbares In-Memory Rate Limiting pro IP mit strengeren sensiblen Endpunkten +- MySQL-8-Persistenz mit TypeORM, Migrationen und Startpruefung auf fehlende Migrationen +- Code-definierte Permissions, Rollenverwaltung, Benutzerverwaltung, Audit-Log und Sessions +- In-App-Benachrichtigungen mit Polling, Badge, eigener Seite und Admin-Erzeugung +- Generierter API-Client fuer das Angular-Frontend +- Dockerfile und Compose-Beispiel fuer eine einzelne auslieferbare App + +## Schnellstart + +```bash +npm ci +cp .env.example .env +npm run dev +``` + +Angular laeuft lokal auf `http://localhost:4200` und proxyt `/api` an NestJS auf +`http://localhost:3000`. Eine MySQL-8-Datenbank und ein OIDC Provider muessen +konfiguriert sein; Compose startet bewusst keine lokale Datenbank. + +## Wichtige Befehle + +```bash +npm run dev # Frontend und Backend parallel starten +npm run lint # ESLint fuer alle Workspaces +npm run format:check # Prettier-Pruefung +npm run typecheck # TypeScript-Pruefung aller Workspaces +npm test # Backend- und Frontend-Tests +npm run build # API-Client, Backend, Frontend und Bundle bauen +npm run migration:status # offene TypeORM-Migrationen anzeigen +npm run migration:run # Migrationen ausfuehren +npm run docker:build # Docker-Image bauen +``` + +Vor einem Merge oder Release muessen `npm run lint`, `npm run format:check`, +`npm run typecheck`, `npm test`, `npm run build` und `docker build .` erfolgreich +laufen. + +## Dokumentation + +- [Getting Started](docs/getting-started.md): lokaler Start, Konfiguration und erster Login +- [Als Projektvorlage verwenden](docs/using-as-template.md): Umbenennen, Entfernen von Demo-Code und erste fachliche Erweiterung +- [Architektur](docs/architecture.md): Monorepo, Backend, Frontend, API-Client und Modulgrenzen +- [Entwicklung](docs/development.md): Workflows fuer Features, Migrationen, Tests und API-Client +- [Security-Modell](docs/security.md): OIDC, Sessions, CSRF, Rollen, Permissions und Logging +- [Designsystem](docs/design-system.md): Tokens, UI-Komponenten, responsive Regeln und Accessibility +- [Adminbereich](docs/admin.md): Benutzer, Rollen, Sessions, Audit und letzter-Admin-Schutz +- [Benachrichtigungen](docs/notifications.md): Datenmodell, API, Permissions, Polling und Erweiterung +- [Deployment und Betrieb](docs/deployment.md): Build, Docker, Migrationen, Runtime-Konfiguration und Healthchecks +- [Konfiguration](docs/configuration.md): Umgebungsvariablen und Produktionshinweise +- [Nginx-Beispiel](docs/nginx-example.conf): Reverse Proxy mit HTTPS-Terminierung + +## Repository-Struktur + +```text +apps/ + backend/ NestJS API, Auth, Sessions, Rollen, Fachmodule, Migrationen + frontend/ Angular SPA, Shell, Feature Pages, Guards, Interceptor +packages/ + api-client/ generierter Angular API-Client +scripts/ Build- und Generator-Hilfsskripte +docs/ Betriebs-, Architektur- und Starter-Dokumentation +``` + +## Auslieferungsmodell + +Der Produktionsbuild erzeugt eine einzelne NestJS-Anwendung. Das Angular-Frontend +wird nach `apps/backend/dist/public` kopiert und vom Backend unter `/` +ausgeliefert. Die API liegt unter `/api`, Swagger optional unter `/api/docs` und +Healthchecks unter `/health/live` sowie `/health/ready`. + +Ein typisches Release: + +```bash +npm ci +npm run lint +npm run format:check +npm run typecheck +npm test +npm run build +docker build -t registry.example.com/business-app: . +docker push registry.example.com/business-app: +``` + +Auf der Zielumgebung werden Migrationen einmalig mit demselben Image ausgefuehrt, +danach wird der App-Container gestartet. Details stehen in +[Deployment und Betrieb](docs/deployment.md). + +## Sicherheitsgrundsaetze + +- OIDC-Tokens werden nur serverseitig gespeichert und verschluesselt. +- Der Browser erhaelt keine Access-, Refresh- oder ID-Tokens. +- Session-Cookies enthalten nur eine signierte Session-ID. +- Schreibende Requests brauchen ein gueltiges CSRF-Token. +- Permissions sind im Code definiert; Benutzer erhalten Rechte nur ueber Rollen. +- Migrationen laufen nie automatisch beim normalen App-Start. +- Secrets, Tokens, Cookies und Session-IDs duerfen nicht geloggt werden. + +## Lizenzierung fuer eigene Projekte + +Das Root-`package.json` ist aktuell `private` und `UNLICENSED`. Wenn dieses +Boilerplate als Open-Source-Startpunkt veroeffentlicht werden soll, muessen vor +der Veroeffentlichung eine passende Lizenzdatei, Paketnamen, Repository-Links und +Projektmetadaten bewusst gesetzt werden. diff --git a/apps/backend/.prettierrc b/apps/backend/.prettierrc new file mode 100644 index 0000000..a20502b --- /dev/null +++ b/apps/backend/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} diff --git a/apps/backend/README.md b/apps/backend/README.md new file mode 100644 index 0000000..f17c584 --- /dev/null +++ b/apps/backend/README.md @@ -0,0 +1,40 @@ +# Backend Workspace + +NestJS Backend fuer das Boilerplate. Der Workspace wird normalerweise ueber die +Root-Scripts gesteuert. + +## Befehle + +```bash +npm --workspace apps/backend run start:dev +npm --workspace apps/backend run build +npm --workspace apps/backend run typecheck +npm --workspace apps/backend run test +npm --workspace apps/backend run migration:status +npm --workspace apps/backend run migration:run +npm --workspace apps/backend run migration:generate +``` + +Root-Aliase fuer die wichtigsten Befehle stehen in `../../package.json`. + +## Struktur + +- `src/main.ts`: Bootstrap, Security Header, CORS, Swagger, Static Assets +- `src/app.module.ts`: globale Guards, Filter, Rate Limiting und Feature-Module +- `src/auth`: OIDC Login, Callback, Public Decorator und Guards +- `src/sessions`: serverseitige Sessions und CSRF +- `src/roles`: Rollen und Code-definierte Permissions +- `src/users`: lokale Benutzer und Einstellungen +- `src/items`: Beispiel-Fachmodul +- `src/database`: TypeORM-Konfiguration, Entities, Migrationen und Healthcheck +- `src/common`: Fehlerformat, Validierung, Request ID und Hilfsdienste + +## Entwicklungsregeln + +Controller verwenden keine TypeORM-Repositories direkt. Fachlogik gehoert in +Services, Datenbankzugriff in Repository-Klassen oder klar benannte +Persistence-Services. Neue Endpunkte muessen mit Permissions geschuetzt werden, +sofern sie nicht explizit public sind. + +Migrationen werden nicht automatisch beim normalen App-Start ausgefuehrt. Nutze +`npm run migration:run` aus dem Root oder den Workspace-Befehl oben. diff --git a/apps/backend/eslint.config.mjs b/apps/backend/eslint.config.mjs new file mode 100644 index 0000000..d45aa03 --- /dev/null +++ b/apps/backend/eslint.config.mjs @@ -0,0 +1,35 @@ +// @ts-check +import eslint from '@eslint/js'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: ['eslint.config.mjs'], + }, + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + eslintPluginPrettierRecommended, + { + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + }, + sourceType: 'commonjs', + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-floating-promises': 'warn', + '@typescript-eslint/no-unsafe-argument': 'warn', + 'prettier/prettier': ['error', { endOfLine: 'auto' }], + }, + }, +); diff --git a/apps/backend/nest-cli.json b/apps/backend/nest-cli.json new file mode 100644 index 0000000..2f068a1 --- /dev/null +++ b/apps/backend/nest-cli.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true, + "tsConfigPath": "tsconfig.build.json" + } +} diff --git a/apps/backend/package.json b/apps/backend/package.json new file mode 100644 index 0000000..2b57366 --- /dev/null +++ b/apps/backend/package.json @@ -0,0 +1,43 @@ +{ + "name": "@boilerplate/backend", + "version": "1.0.0", + "private": true, + "type": "commonjs", + "scripts": { + "build": "nest build", + "start:dev": "nest start --watch", + "start:prod": "node dist/main.js", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "vitest run --config vitest.config.ts", + "migration:status": "typeorm-ts-node-commonjs -d src/database/typeorm-cli.datasource.ts migration:show", + "migration:run": "typeorm-ts-node-commonjs -d src/database/typeorm-cli.datasource.ts migration:run", + "migration:generate": "typeorm-ts-node-commonjs -d src/database/typeorm-cli.datasource.ts migration:generate src/database/migrations/GeneratedMigration" + }, + "dependencies": { + "@nestjs/common": "11.1.28", + "@nestjs/config": "4.0.4", + "@nestjs/core": "11.1.28", + "@nestjs/platform-express": "11.1.28", + "@nestjs/swagger": "11.2.3", + "@nestjs/throttler": "6.4.0", + "@nestjs/typeorm": "11.0.0", + "class-transformer": "0.5.1", + "class-validator": "0.14.2", + "cookie-parser": "1.4.7", + "dotenv": "17.4.2", + "helmet": "8.1.0", + "jose": "6.1.2", + "mysql2": "3.15.3", + "openid-client": "6.8.1", + "pino": "10.1.0", + "pino-http": "11.0.0", + "pino-pretty": "13.1.2", + "reflect-metadata": "0.2.2", + "rxjs": "7.8.2", + "typeorm": "0.3.27", + "zod": "4.1.13" + }, + "devDependencies": { + "typeorm-ts-node-commonjs": "0.3.20" + } +} diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts new file mode 100644 index 0000000..de091a3 --- /dev/null +++ b/apps/backend/src/app.module.ts @@ -0,0 +1,101 @@ +import { createHash } from 'node:crypto'; +import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; +import { APP_FILTER, APP_GUARD, Reflector } from '@nestjs/core'; +import { + ThrottlerGuard, + ThrottlerModule, + type ThrottlerGenerateKeyFunction, + type ThrottlerGetTrackerFunction, +} from '@nestjs/throttler'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuditModule } from './audit/audit.module'; +import { AuthModule } from './auth/auth.module'; +import { CsrfGuard } from './auth/guards/csrf.guard'; +import { PermissionsGuard } from './auth/guards/permissions.guard'; +import { AppConfigModule } from './config/config.module'; +import { AppConfigService } from './config/config.service'; +import { ApiExceptionFilter } from './common/errors/api-exception.filter'; +import { RequestIdMiddleware } from './common/request-context/request-id.middleware'; +import { SENSITIVE_RATE_LIMIT_KEY } from './common/rate-limit/sensitive-rate-limit.decorator'; +import { typeOrmOptionsFactory } from './database/typeorm-options'; +import { DatabaseModule } from './database/database.module'; +import { DashboardModule } from './dashboard/dashboard.module'; +import { HealthModule } from './health/health.module'; +import { ItemsModule } from './items/items.module'; +import { NotificationsModule } from './notifications/notifications.module'; +import { RolesModule } from './roles/roles.module'; +import { SessionsModule } from './sessions/sessions.module'; +import { UsersModule } from './users/users.module'; + +const getIpTracker: ThrottlerGetTrackerFunction = (req) => { + const ip = typeof req['ip'] === 'string' ? req['ip'] : undefined; + const socket = req['socket'] as { remoteAddress?: unknown } | undefined; + const remoteAddress = + typeof socket?.remoteAddress === 'string' + ? socket.remoteAddress + : undefined; + return ip ?? remoteAddress ?? 'unknown'; +}; + +const generateGlobalKey: ThrottlerGenerateKeyFunction = ( + _context, + tracker, + throttlerName, +) => createHash('sha256').update(`${throttlerName}:${tracker}`).digest('hex'); + +@Module({ + imports: [ + AppConfigModule, + TypeOrmModule.forRootAsync({ + imports: [AppConfigModule], + inject: [AppConfigService], + useFactory: typeOrmOptionsFactory, + }), + DatabaseModule, + DashboardModule, + ThrottlerModule.forRootAsync({ + imports: [AppConfigModule], + inject: [AppConfigService, Reflector], + useFactory: (config: AppConfigService, reflector: Reflector) => ({ + getTracker: getIpTracker, + generateKey: generateGlobalKey, + throttlers: [ + { + name: 'default', + ttl: config.rateLimit.global.windowSeconds * 1000, + limit: config.rateLimit.global.maxRequests, + }, + { + name: 'sensitive', + ttl: config.rateLimit.sensitive.windowSeconds * 1000, + limit: config.rateLimit.sensitive.maxRequests, + skipIf: (context) => + !reflector.getAllAndOverride(SENSITIVE_RATE_LIMIT_KEY, [ + context.getHandler(), + context.getClass(), + ]), + }, + ], + }), + }), + AuthModule, + UsersModule, + RolesModule, + SessionsModule, + NotificationsModule, + AuditModule, + ItemsModule, + HealthModule, + ], + providers: [ + { provide: APP_FILTER, useClass: ApiExceptionFilter }, + { provide: APP_GUARD, useClass: ThrottlerGuard }, + { provide: APP_GUARD, useClass: CsrfGuard }, + { provide: APP_GUARD, useClass: PermissionsGuard }, + ], +}) +export class AppModule implements NestModule { + configure(consumer: MiddlewareConsumer): void { + consumer.apply(RequestIdMiddleware).forRoutes('*'); + } +} diff --git a/apps/backend/src/audit/audit.controller.ts b/apps/backend/src/audit/audit.controller.ts new file mode 100644 index 0000000..8e9da15 --- /dev/null +++ b/apps/backend/src/audit/audit.controller.ts @@ -0,0 +1,15 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { RequirePermissions } from '../auth/guards/require-permissions.decorator'; +import { Permission } from '../roles/permissions'; +import { AuditService } from './audit.service'; + +@Controller('audit-log') +export class AuditController { + constructor(private readonly audit: AuditService) {} + + @Get() + @RequirePermissions(Permission.AuditRead) + list(@Query('page') page?: string, @Query('pageSize') pageSize?: string) { + return this.audit.list(Number(page ?? 1), Number(pageSize ?? 20)); + } +} diff --git a/apps/backend/src/audit/audit.module.ts b/apps/backend/src/audit/audit.module.ts new file mode 100644 index 0000000..0a43d63 --- /dev/null +++ b/apps/backend/src/audit/audit.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuditLogEntity } from './entities/audit-log.entity'; +import { AuditController } from './audit.controller'; +import { AuditRepository } from './repositories/audit.repository'; +import { AuditService } from './audit.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([AuditLogEntity])], + controllers: [AuditController], + providers: [AuditRepository, AuditService], + exports: [AuditService], +}) +export class AuditModule {} diff --git a/apps/backend/src/audit/audit.service.ts b/apps/backend/src/audit/audit.service.ts new file mode 100644 index 0000000..16060be --- /dev/null +++ b/apps/backend/src/audit/audit.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { getRequestId } from '../common/request-context/request-context'; +import { AuditAction, AuditLogEntity } from './entities/audit-log.entity'; +import { AuditRepository } from './repositories/audit.repository'; + +@Injectable() +export class AuditService { + constructor(private readonly audit: AuditRepository) {} + + async record( + actorUserId: string | null, + action: AuditAction, + targetType: string, + targetId: string, + metadata: Record | null = null, + ): Promise { + const entry = new AuditLogEntity(); + entry.actorUserId = actorUserId; + entry.action = action; + entry.targetType = targetType; + entry.targetId = targetId; + entry.metadata = metadata; + entry.requestId = getRequestId(); + await this.audit.save(entry); + } + + async list(page = 1, pageSize = 20) { + const [items, total] = await this.audit.list(page, pageSize); + return { items, total, page, pageSize }; + } +} diff --git a/apps/backend/src/audit/entities/audit-log.entity.ts b/apps/backend/src/audit/entities/audit-log.entity.ts new file mode 100644 index 0000000..ce09ec0 --- /dev/null +++ b/apps/backend/src/audit/entities/audit-log.entity.ts @@ -0,0 +1,49 @@ +import { + Column, + CreateDateColumn, + Entity, + Index, + PrimaryGeneratedColumn, +} from 'typeorm'; + +export enum AuditAction { + UserActivated = 'USER_ACTIVATED', + UserDeactivated = 'USER_DEACTIVATED', + UserRoleAssigned = 'USER_ROLE_ASSIGNED', + UserRoleRemoved = 'USER_ROLE_REMOVED', + RoleCreated = 'ROLE_CREATED', + RoleUpdated = 'ROLE_UPDATED', + RoleDeleted = 'ROLE_DELETED', + RolePermissionsUpdated = 'ROLE_PERMISSIONS_UPDATED', + SessionRevoked = 'SESSION_REVOKED', + AllUserSessionsRevoked = 'ALL_USER_SESSIONS_REVOKED', + NotificationCreated = 'NOTIFICATION_CREATED', +} + +@Entity('audit_logs') +export class AuditLogEntity { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Index('idx_audit_created_at') + @CreateDateColumn({ name: 'created_at', type: 'datetime', precision: 3 }) + createdAt!: Date; + + @Column({ name: 'actor_user_id', type: 'char', length: 36, nullable: true }) + actorUserId!: string | null; + + @Column({ type: 'varchar', length: 80 }) + action!: AuditAction; + + @Column({ name: 'target_type', type: 'varchar', length: 80 }) + targetType!: string; + + @Column({ name: 'target_id', type: 'varchar', length: 120 }) + targetId!: string; + + @Column({ type: 'json', nullable: true }) + metadata!: Record | null; + + @Column({ name: 'request_id', type: 'varchar', length: 128 }) + requestId!: string; +} diff --git a/apps/backend/src/audit/repositories/audit.repository.ts b/apps/backend/src/audit/repositories/audit.repository.ts new file mode 100644 index 0000000..ed41615 --- /dev/null +++ b/apps/backend/src/audit/repositories/audit.repository.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { AuditLogEntity } from '../entities/audit-log.entity'; + +@Injectable() +export class AuditRepository { + constructor( + @InjectRepository(AuditLogEntity) + private readonly repo: Repository, + ) {} + + save(entry: AuditLogEntity): Promise { + return this.repo.save(entry); + } + + list(page: number, pageSize: number): Promise<[AuditLogEntity[], number]> { + return this.repo.findAndCount({ + order: { createdAt: 'DESC' }, + skip: (page - 1) * pageSize, + take: pageSize, + }); + } +} diff --git a/apps/backend/src/auth/auth.controller.ts b/apps/backend/src/auth/auth.controller.ts new file mode 100644 index 0000000..7335f4d --- /dev/null +++ b/apps/backend/src/auth/auth.controller.ts @@ -0,0 +1,69 @@ +import { Controller, Get, Query, Redirect, Req, Res } from '@nestjs/common'; +import type { Response } from 'express'; +import type { AuthenticatedRequest } from './authenticated-request'; +import { Public } from './guards/public.decorator'; +import { AppConfigService } from '../config/config.service'; +import { SensitiveRateLimit } from '../common/rate-limit/sensitive-rate-limit.decorator'; +import { AuthService } from './auth.service'; + +@Controller('auth') +export class AuthController { + constructor( + private readonly auth: AuthService, + private readonly config: AppConfigService, + ) {} + + @Get('login') + @Public() + @SensitiveRateLimit() + @Redirect() + async login() { + return { url: await this.auth.createLoginUrl() }; + } + + @Get('callback') + @Public() + @SensitiveRateLimit() + async callback( + @Query('code') code: string, + @Query('state') state: string, + @Req() req: AuthenticatedRequest, + @Res() res: Response, + ) { + const { session, csrfToken } = await this.auth.completeLogin( + code, + state, + req.get('user-agent'), + req.ip, + ); + res.cookie(this.config.session.cookieName, session.id, { + httpOnly: true, + signed: true, + secure: this.config.isProduction, + sameSite: 'lax', + path: '/', + expires: session.absoluteExpiresAt, + }); + res.cookie('csrf_token', csrfToken, { + httpOnly: false, + secure: this.config.isProduction, + sameSite: 'lax', + path: '/', + expires: session.absoluteExpiresAt, + }); + res.redirect(this.config.frontendBaseUrl); + } + + @Get('logout') + @Public() + @SensitiveRateLimit() + async logout(@Req() req: AuthenticatedRequest, @Res() res: Response) { + const sessionId = req.signedCookies?.[this.config.session.cookieName] as + | string + | undefined; + const logoutUrl = await this.auth.logout(sessionId); + res.clearCookie(this.config.session.cookieName, { path: '/' }); + res.clearCookie('csrf_token', { path: '/' }); + res.redirect(logoutUrl); + } +} diff --git a/apps/backend/src/auth/auth.module.ts b/apps/backend/src/auth/auth.module.ts new file mode 100644 index 0000000..86393cf --- /dev/null +++ b/apps/backend/src/auth/auth.module.ts @@ -0,0 +1,27 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ExternalHttpClient } from '../common/http/external-http-client'; +import { RolesModule } from '../roles/roles.module'; +import { SessionsModule } from '../sessions/sessions.module'; +import { UserSettingsEntity } from '../users/entities/user-settings.entity'; +import { UserEntity } from '../users/entities/user.entity'; +import { UsersRepository } from '../users/repositories/users.repository'; +import { OidcLoginStateEntity } from './entities/oidc-login-state.entity'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + OidcLoginStateEntity, + UserEntity, + UserSettingsEntity, + ]), + RolesModule, + SessionsModule, + ], + controllers: [AuthController], + providers: [AuthService, ExternalHttpClient, UsersRepository], + exports: [AuthService], +}) +export class AuthModule {} diff --git a/apps/backend/src/auth/auth.service.spec.ts b/apps/backend/src/auth/auth.service.spec.ts new file mode 100644 index 0000000..904a0cf --- /dev/null +++ b/apps/backend/src/auth/auth.service.spec.ts @@ -0,0 +1,50 @@ +import { describe, expect, it, vi } from 'vitest'; +import type { DataSource, Repository } from 'typeorm'; +import type { ExternalHttpClient } from '../common/http/external-http-client'; +import type { AppConfigService } from '../config/config.service'; +import type { RolesService } from '../roles/roles.service'; +import type { SessionsService } from '../sessions/sessions.service'; +import type { UsersRepository } from '../users/repositories/users.repository'; +import type { OidcLoginStateEntity } from './entities/oidc-login-state.entity'; +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + it('revokes the local session and redirects to the OIDC logout endpoint', async () => { + const revoke = vi.fn<() => Promise>(() => Promise.resolve(1)); + const getIdTokenForLogout = vi.fn<() => Promise>(() => + Promise.resolve('id-token'), + ); + const service = new AuthService( + { + frontendBaseUrl: 'https://app.example.test', + appBaseUrl: 'https://app.example.test', + oidc: { + issuer: 'https://idp.example.test', + clientId: 'business-app', + clientSecret: 'secret', + scopes: 'openid profile email', + allowedAlgorithms: ['RS256'], + httpTimeoutMs: 5000, + logoutUrl: 'https://idp.example.test/logout', + }, + } as AppConfigService, + {} as ExternalHttpClient, + {} as RolesService, + {} as UsersRepository, + { revoke, getIdTokenForLogout } as unknown as SessionsService, + {} as DataSource, + {} as Repository, + ); + + const url = new URL(await service.logout('session-1')); + + expect(revoke).toHaveBeenCalledWith('session-1'); + expect(getIdTokenForLogout).toHaveBeenCalledWith('session-1'); + expect(url.origin + url.pathname).toBe('https://idp.example.test/logout'); + expect(url.searchParams.get('client_id')).toBe('business-app'); + expect(url.searchParams.get('post_logout_redirect_uri')).toBe( + 'https://app.example.test', + ); + expect(url.searchParams.get('id_token_hint')).toBe('id-token'); + }); +}); diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts new file mode 100644 index 0000000..4196e4f --- /dev/null +++ b/apps/backend/src/auth/auth.service.ts @@ -0,0 +1,328 @@ +import { createHash, randomBytes } from 'node:crypto'; +import { Injectable } from '@nestjs/common'; +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { DataSource, EntityManager, LessThan, Repository } from 'typeorm'; +import { ExternalHttpClient } from '../common/http/external-http-client'; +import { ApiError } from '../common/errors/api-error'; +import { ErrorCode } from '../common/errors/error-codes'; +import { AppConfigService } from '../config/config.service'; +import { RolesService } from '../roles/roles.service'; +import { UserSettingsEntity } from '../users/entities/user-settings.entity'; +import { UserEntity } from '../users/entities/user.entity'; +import { UsersRepository } from '../users/repositories/users.repository'; +import { SessionsService } from '../sessions/sessions.service'; +import { OidcLoginStateEntity } from './entities/oidc-login-state.entity'; +import type { + OidcDiscovery, + OidcTokenResponse, + OidcUserInfo, +} from './oidc.types'; + +@Injectable() +export class AuthService { + constructor( + private readonly config: AppConfigService, + private readonly http: ExternalHttpClient, + private readonly roles: RolesService, + private readonly users: UsersRepository, + private readonly sessions: SessionsService, + @InjectDataSource() private readonly dataSource: DataSource, + @InjectRepository(OidcLoginStateEntity) + private readonly loginStates: Repository, + ) {} + + async createLoginUrl(): Promise { + const discovery = await this.discovery(); + const state = randomBytes(32).toString('hex'); + const nonce = randomBytes(32).toString('base64url'); + const codeVerifier = randomBytes(48).toString('base64url'); + const challenge = createHash('sha256') + .update(codeVerifier) + .digest('base64url'); + + await this.loginStates.delete({ expiresAt: LessThan(new Date()) }); + const loginState = new OidcLoginStateEntity(); + loginState.state = state; + loginState.codeVerifier = codeVerifier; + loginState.nonce = nonce; + loginState.expiresAt = new Date(Date.now() + 10 * 60 * 1000); + await this.loginStates.save(loginState); + + const url = new URL(discovery.authorization_endpoint); + url.searchParams.set('response_type', 'code'); + url.searchParams.set('client_id', this.config.oidc.clientId); + url.searchParams.set('redirect_uri', this.callbackUrl); + url.searchParams.set('scope', this.config.oidc.scopes); + url.searchParams.set('state', state); + url.searchParams.set('nonce', nonce); + url.searchParams.set('code_challenge', challenge); + url.searchParams.set('code_challenge_method', 'S256'); + return url.toString(); + } + + async completeLogin( + code: string, + state: string, + userAgent: string | undefined, + ip: string | undefined, + ) { + const loginState = await this.loginStates.findOneBy({ state }); + if (!loginState || loginState.expiresAt <= new Date()) { + throw new ApiError( + ErrorCode.Unauthorized, + 'Die Anmeldung ist abgelaufen.', + 401, + ); + } + await this.loginStates.delete({ state }); + + const discovery = await this.discovery(); + const tokens = await this.exchangeCode( + discovery, + code, + loginState.codeVerifier, + ); + const profile = await this.verifyAndLoadProfile( + discovery, + tokens, + loginState.nonce, + ); + const user = await this.upsertLocalUser(discovery.issuer, profile); + if (!user.active) { + throw new ApiError( + ErrorCode.UserDisabled, + 'Dieser Benutzer ist deaktiviert.', + 403, + ); + } + + return this.sessions.createSession( + user, + { + accessToken: tokens.access_token, + ...(tokens.refresh_token ? { refreshToken: tokens.refresh_token } : {}), + idToken: tokens.id_token, + accessTokenExpiresAt: new Date( + Date.now() + (tokens.expires_in ?? 3600) * 1000, + ), + }, + userAgent, + ip, + ); + } + + async logout(sessionId: string | undefined): Promise { + let idToken: string | undefined; + if (sessionId) { + idToken = await this.readLogoutIdToken(sessionId); + await this.sessions.revoke(sessionId); + } + return this.createLogoutUrl(idToken); + } + + private async upsertLocalUser( + issuer: string, + profile: OidcUserInfo, + ): Promise { + return this.dataSource.transaction(async (manager) => { + await manager.query("SELECT GET_LOCK('business_app_first_admin', 10)"); + try { + const { admin, user: userRole } = + await this.roles.ensureSystemRoles(manager); + let user = await manager.getRepository(UserEntity).findOne({ + where: { issuer, subject: profile.sub }, + relations: { roles: true, settings: true }, + }); + if (!user) { + user = new UserEntity(); + user.issuer = issuer; + user.subject = profile.sub; + user.active = true; + user.roles = [userRole]; + const userCount = await manager.getRepository(UserEntity).count(); + if (userCount === 0) { + user.roles = [userRole, admin]; + } + } + user.name = profile.name ?? profile.email ?? profile.sub; + user.email = profile.email ?? null; + user.lastLoginAt = new Date(); + const savedUser = await manager.getRepository(UserEntity).save(user); + savedUser.settings = await this.ensureUserSettings(manager, savedUser); + return savedUser; + } finally { + await manager.query("SELECT RELEASE_LOCK('business_app_first_admin')"); + } + }); + } + + private async ensureUserSettings( + manager: EntityManager, + user: UserEntity, + ): Promise { + const settingsRepository = manager.getRepository(UserSettingsEntity); + const existingSettings = await settingsRepository.findOne({ + where: { user: { id: user.id } }, + }); + if (existingSettings) { + return existingSettings; + } + + const settings = new UserSettingsEntity(); + settings.user = user; + return settingsRepository.save(settings); + } + + private async discovery(): Promise { + const discoveryUrl = new URL( + '/.well-known/openid-configuration', + this.config.oidc.issuer, + ); + const discovery = await this.http.requestJson( + discoveryUrl.toString(), + ); + if (discovery.issuer !== this.config.oidc.issuer) { + throw new ApiError( + ErrorCode.Unauthorized, + 'OIDC-Issuer ist ungueltig.', + 401, + ); + } + return discovery; + } + + private async readLogoutIdToken( + sessionId: string, + ): Promise { + try { + return await this.sessions.getIdTokenForLogout(sessionId); + } catch { + return undefined; + } + } + + private async createLogoutUrl(idToken: string | undefined): Promise { + const endpoint = await this.resolveLogoutEndpoint(); + if (!endpoint) { + return this.config.frontendBaseUrl; + } + + const url = new URL(endpoint); + url.searchParams.set('client_id', this.config.oidc.clientId); + url.searchParams.set( + 'post_logout_redirect_uri', + this.config.frontendBaseUrl, + ); + if (idToken) { + url.searchParams.set('id_token_hint', idToken); + } + return url.toString(); + } + + private async resolveLogoutEndpoint(): Promise { + if (this.config.oidc.logoutUrl) { + return this.config.oidc.logoutUrl; + } + try { + return (await this.discovery()).end_session_endpoint; + } catch { + return undefined; + } + } + + private async exchangeCode( + discovery: OidcDiscovery, + code: string, + codeVerifier: string, + ): Promise { + const body = new URLSearchParams({ + grant_type: 'authorization_code', + code, + redirect_uri: this.callbackUrl, + client_id: this.config.oidc.clientId, + code_verifier: codeVerifier, + }); + const basic = Buffer.from( + `${this.config.oidc.clientId}:${this.config.oidc.clientSecret}`, + 'utf8', + ).toString('base64'); + return this.http.requestJson(discovery.token_endpoint, { + method: 'POST', + headers: { + Authorization: `Basic ${basic}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body, + }); + } + + private async verifyAndLoadProfile( + discovery: OidcDiscovery, + tokens: OidcTokenResponse, + nonce: string, + ): Promise { + const { createRemoteJWKSet, decodeProtectedHeader, jwtVerify } = + await import('jose'); + const protectedHeader = decodeProtectedHeader(tokens.id_token); + if (!this.isAllowedOidcAlgorithm(protectedHeader.alg)) { + throw new ApiError( + ErrorCode.Unauthorized, + 'OIDC-Signaturalgorithmus ist ungueltig.', + 401, + ); + } + + const { payload } = await jwtVerify( + tokens.id_token, + createRemoteJWKSet(new URL(discovery.jwks_uri)), + { + issuer: discovery.issuer, + audience: this.config.oidc.clientId, + algorithms: this.config.oidc.allowedAlgorithms, + }, + ); + if (payload['nonce'] !== nonce || typeof payload.sub !== 'string') { + throw new ApiError( + ErrorCode.Unauthorized, + 'OIDC-Token ist ungueltig.', + 401, + ); + } + if (!discovery.userinfo_endpoint) { + const fallback: OidcUserInfo = { sub: payload.sub }; + if (typeof payload['name'] === 'string') { + fallback.name = payload['name']; + } + if (typeof payload['email'] === 'string') { + fallback.email = payload['email']; + } + return fallback; + } + const userInfo = await this.http.requestJson( + discovery.userinfo_endpoint, + { + headers: { Authorization: `Bearer ${tokens.access_token}` }, + }, + ); + if (userInfo.sub !== payload.sub) { + throw new ApiError( + ErrorCode.Unauthorized, + 'OIDC-UserInfo ist ungueltig.', + 401, + ); + } + return userInfo; + } + + private isAllowedOidcAlgorithm(algorithm: string | undefined): boolean { + return ( + typeof algorithm === 'string' && + algorithm.toLowerCase() !== 'none' && + this.config.oidc.allowedAlgorithms.includes(algorithm) + ); + } + + private get callbackUrl(): string { + return new URL('/api/auth/callback', this.config.appBaseUrl).toString(); + } +} diff --git a/apps/backend/src/auth/authenticated-request.ts b/apps/backend/src/auth/authenticated-request.ts new file mode 100644 index 0000000..ed0d334 --- /dev/null +++ b/apps/backend/src/auth/authenticated-request.ts @@ -0,0 +1,12 @@ +import type { Request } from 'express'; +import type { Permission } from '../roles/permissions'; + +export interface AuthenticatedUser { + id: string; + sessionId: string; + permissions: Permission[]; +} + +export interface AuthenticatedRequest extends Request { + user?: AuthenticatedUser; +} diff --git a/apps/backend/src/auth/entities/oidc-login-state.entity.ts b/apps/backend/src/auth/entities/oidc-login-state.entity.ts new file mode 100644 index 0000000..06300cf --- /dev/null +++ b/apps/backend/src/auth/entities/oidc-login-state.entity.ts @@ -0,0 +1,19 @@ +import { Column, CreateDateColumn, Entity, PrimaryColumn } from 'typeorm'; + +@Entity('oidc_login_states') +export class OidcLoginStateEntity { + @PrimaryColumn({ type: 'char', length: 64 }) + state!: string; + + @Column({ name: 'code_verifier', type: 'varchar', length: 160 }) + codeVerifier!: string; + + @Column({ type: 'varchar', length: 160 }) + nonce!: string; + + @Column({ name: 'expires_at', type: 'datetime', precision: 3 }) + expiresAt!: Date; + + @CreateDateColumn({ name: 'created_at', type: 'datetime', precision: 3 }) + createdAt!: Date; +} diff --git a/apps/backend/src/auth/guards/csrf.guard.ts b/apps/backend/src/auth/guards/csrf.guard.ts new file mode 100644 index 0000000..61ea4ff --- /dev/null +++ b/apps/backend/src/auth/guards/csrf.guard.ts @@ -0,0 +1,49 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { ApiError } from '../../common/errors/api-error'; +import { ErrorCode } from '../../common/errors/error-codes'; +import { AppConfigService } from '../../config/config.service'; +import { SessionsService } from '../../sessions/sessions.service'; +import { IS_PUBLIC_KEY } from './public.decorator'; +import type { AuthenticatedRequest } from '../authenticated-request'; + +const unsafeMethods = new Set(['POST', 'PUT', 'PATCH', 'DELETE']); + +@Injectable() +export class CsrfGuard implements CanActivate { + constructor( + private readonly reflector: Reflector, + private readonly sessions: SessionsService, + private readonly config: AppConfigService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + if (!unsafeMethods.has(request.method)) { + return true; + } + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (isPublic) { + return true; + } + const sessionId = request.signedCookies?.[ + this.config.session.cookieName + ] as string | undefined; + const csrfToken = request.header(this.config.csrfHeaderName); + if ( + !sessionId || + !csrfToken || + !(await this.sessions.verifyCsrfToken(sessionId, csrfToken)) + ) { + throw new ApiError( + ErrorCode.CsrfInvalid, + 'Das Sicherheits-Token ist ungueltig.', + 403, + ); + } + return true; + } +} diff --git a/apps/backend/src/auth/guards/permissions.guard.ts b/apps/backend/src/auth/guards/permissions.guard.ts new file mode 100644 index 0000000..307c849 --- /dev/null +++ b/apps/backend/src/auth/guards/permissions.guard.ts @@ -0,0 +1,68 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { ApiError } from '../../common/errors/api-error'; +import { ErrorCode } from '../../common/errors/error-codes'; +import { AppConfigService } from '../../config/config.service'; +import { SessionsService } from '../../sessions/sessions.service'; +import type { Permission } from '../../roles/permissions'; +import { IS_PUBLIC_KEY } from './public.decorator'; +import { REQUIRED_PERMISSIONS_KEY } from './require-permissions.decorator'; +import type { AuthenticatedRequest } from '../authenticated-request'; + +@Injectable() +export class PermissionsGuard implements CanActivate { + constructor( + private readonly reflector: Reflector, + private readonly sessions: SessionsService, + private readonly config: AppConfigService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + const required = this.reflector.getAllAndOverride( + REQUIRED_PERMISSIONS_KEY, + [context.getHandler(), context.getClass()], + ); + if (isPublic || !required?.length) { + return true; + } + + const request = context.switchToHttp().getRequest(); + const sessionId = request.signedCookies?.[ + this.config.session.cookieName + ] as string | undefined; + if (!sessionId) { + throw new ApiError( + ErrorCode.Unauthorized, + 'Bitte melden Sie sich an.', + 401, + ); + } + + const session = await this.sessions.resolveSession( + sessionId, + request.ip, + request.get('user-agent'), + ); + request.user = { + id: session.user.id, + sessionId, + permissions: session.permissions, + }; + + const allowed = required.every((permission) => + session.permissions.includes(permission), + ); + if (!allowed) { + throw new ApiError( + ErrorCode.PermissionDenied, + 'Keine Berechtigung fuer diese Aktion.', + 403, + ); + } + return true; + } +} diff --git a/apps/backend/src/auth/guards/public.decorator.ts b/apps/backend/src/auth/guards/public.decorator.ts new file mode 100644 index 0000000..b3845e1 --- /dev/null +++ b/apps/backend/src/auth/guards/public.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'isPublic'; +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); diff --git a/apps/backend/src/auth/guards/require-permissions.decorator.ts b/apps/backend/src/auth/guards/require-permissions.decorator.ts new file mode 100644 index 0000000..549df44 --- /dev/null +++ b/apps/backend/src/auth/guards/require-permissions.decorator.ts @@ -0,0 +1,6 @@ +import { SetMetadata } from '@nestjs/common'; +import type { Permission } from '../../roles/permissions'; + +export const REQUIRED_PERMISSIONS_KEY = 'requiredPermissions'; +export const RequirePermissions = (...permissions: Permission[]) => + SetMetadata(REQUIRED_PERMISSIONS_KEY, permissions); diff --git a/apps/backend/src/auth/oidc.types.ts b/apps/backend/src/auth/oidc.types.ts new file mode 100644 index 0000000..9e37451 --- /dev/null +++ b/apps/backend/src/auth/oidc.types.ts @@ -0,0 +1,23 @@ +export interface OidcDiscovery { + issuer: string; + authorization_endpoint: string; + token_endpoint: string; + userinfo_endpoint?: string; + jwks_uri: string; + revocation_endpoint?: string; + end_session_endpoint?: string; +} + +export interface OidcTokenResponse { + access_token: string; + refresh_token?: string; + id_token: string; + expires_in?: number; + token_type: string; +} + +export interface OidcUserInfo { + sub: string; + name?: string; + email?: string; +} diff --git a/apps/backend/src/common/dto/pagination.dto.ts b/apps/backend/src/common/dto/pagination.dto.ts new file mode 100644 index 0000000..1542737 --- /dev/null +++ b/apps/backend/src/common/dto/pagination.dto.ts @@ -0,0 +1,28 @@ +import { Type } from 'class-transformer'; +import { IsInt, IsOptional, IsString, Max, Min } from 'class-validator'; + +export class PaginationQueryDto { + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page = 1; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + pageSize = 20; + + @IsOptional() + @IsString() + search?: string; +} + +export interface PageDto { + items: T[]; + total: number; + page: number; + pageSize: number; +} diff --git a/apps/backend/src/common/errors/api-error.ts b/apps/backend/src/common/errors/api-error.ts new file mode 100644 index 0000000..bdadb9b --- /dev/null +++ b/apps/backend/src/common/errors/api-error.ts @@ -0,0 +1,27 @@ +import { HttpException } from '@nestjs/common'; +import type { HttpStatus } from '@nestjs/common'; +import type { ErrorCode } from './error-codes'; + +export interface ValidationErrorDetail { + field: string; + messages: string[]; +} + +export interface ApiErrorBody { + status: number; + code: ErrorCode; + message: string; + requestId: string; + validation?: ValidationErrorDetail[]; +} + +export class ApiError extends HttpException { + constructor( + public readonly code: ErrorCode, + message: string, + status: HttpStatus, + public readonly validation?: ValidationErrorDetail[], + ) { + super(message, status); + } +} diff --git a/apps/backend/src/common/errors/api-exception.filter.ts b/apps/backend/src/common/errors/api-exception.filter.ts new file mode 100644 index 0000000..bb4610f --- /dev/null +++ b/apps/backend/src/common/errors/api-exception.filter.ts @@ -0,0 +1,66 @@ +import { + ArgumentsHost, + Catch, + ExceptionFilter, + HttpException, + HttpStatus, + Logger, +} from '@nestjs/common'; +import type { Response } from 'express'; +import { QueryFailedError } from 'typeorm'; +import { getRequestId } from '../request-context/request-context'; +import { ApiError, type ApiErrorBody } from './api-error'; +import { ErrorCode } from './error-codes'; + +@Catch() +export class ApiExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger(ApiExceptionFilter.name); + + catch(exception: unknown, host: ArgumentsHost): void { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const requestId = getRequestId(); + + let status: number = HttpStatus.INTERNAL_SERVER_ERROR; + let code = ErrorCode.InternalError; + let message = 'Ein unerwarteter Fehler ist aufgetreten.'; + let validation: ApiErrorBody['validation']; + + if (exception instanceof ApiError) { + status = exception.getStatus(); + code = exception.code; + message = exception.message; + validation = exception.validation; + } else if (exception instanceof HttpException) { + status = exception.getStatus(); + code = + status === 401 + ? ErrorCode.Unauthorized + : status === 429 + ? ErrorCode.RateLimitExceeded + : ErrorCode.InternalError; + message = + status === 429 + ? 'Zu viele Anfragen. Bitte versuchen Sie es spaeter erneut.' + : status >= 500 + ? message + : 'Die Anfrage konnte nicht verarbeitet werden.'; + } else if (exception instanceof QueryFailedError) { + status = HttpStatus.CONFLICT; + code = ErrorCode.Conflict; + message = 'Die Aenderung steht im Konflikt mit bestehenden Daten.'; + } + + if (status >= 500) { + this.logger.error({ requestId, exception }, 'Unhandled API error'); + } + + response.status(status).json({ + status, + code, + message, + requestId, + ...(validation ? { validation } : {}), + } satisfies ApiErrorBody); + } +} diff --git a/apps/backend/src/common/errors/error-codes.ts b/apps/backend/src/common/errors/error-codes.ts new file mode 100644 index 0000000..77ab47d --- /dev/null +++ b/apps/backend/src/common/errors/error-codes.ts @@ -0,0 +1,29 @@ +export enum ErrorCode { + PermissionDenied = 'PERMISSION_DENIED', + Unauthorized = 'UNAUTHORIZED', + ValidationFailed = 'VALIDATION_FAILED', + NotFound = 'NOT_FOUND', + Conflict = 'CONFLICT', + CsrfInvalid = 'CSRF_INVALID', + UserDisabled = 'USER_DISABLED', + LastAdminRequired = 'LAST_ACTIVE_ADMIN_REQUIRED', + MigrationMissing = 'MIGRATION_MISSING', + RateLimitExceeded = 'RATE_LIMIT_EXCEEDED', + NotificationNotFound = 'NOTIFICATION_NOT_FOUND', + NotificationAccessDenied = 'NOTIFICATION_ACCESS_DENIED', + NotificationTypeInvalid = 'NOTIFICATION_TYPE_INVALID', + NotificationLinkInvalid = 'NOTIFICATION_LINK_INVALID', + NotificationMetadataTooLarge = 'NOTIFICATION_METADATA_TOO_LARGE', + UserNotFound = 'USER_NOT_FOUND', + UserAlreadyActive = 'USER_ALREADY_ACTIVE', + UserAlreadyInactive = 'USER_ALREADY_INACTIVE', + RoleNotFound = 'ROLE_NOT_FOUND', + RoleAlreadyAssigned = 'ROLE_ALREADY_ASSIGNED', + RoleNotAssigned = 'ROLE_NOT_ASSIGNED', + RoleNameAlreadyExists = 'ROLE_NAME_ALREADY_EXISTS', + RoleStillAssigned = 'ROLE_STILL_ASSIGNED', + SystemRoleProtected = 'SYSTEM_ROLE_PROTECTED', + UnknownPermission = 'UNKNOWN_PERMISSION', + SessionNotFound = 'SESSION_NOT_FOUND', + InternalError = 'INTERNAL_ERROR', +} diff --git a/apps/backend/src/common/http/external-http-client.ts b/apps/backend/src/common/http/external-http-client.ts new file mode 100644 index 0000000..fc3a3b3 --- /dev/null +++ b/apps/backend/src/common/http/external-http-client.ts @@ -0,0 +1,55 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { AppConfigService } from '../../config/config.service'; +import { getRequestId } from '../request-context/request-context'; + +export interface ExternalHttpOptions { + method?: 'GET' | 'POST'; + headers?: Record; + body?: URLSearchParams | string; + timeoutMs?: number; +} + +@Injectable() +export class ExternalHttpClient { + private readonly logger = new Logger(ExternalHttpClient.name); + + constructor(private readonly config: AppConfigService) {} + + async requestJson( + url: string, + options: ExternalHttpOptions = {}, + ): Promise { + const controller = new AbortController(); + const timeout = setTimeout( + () => controller.abort(), + options.timeoutMs ?? this.config.oidc.httpTimeoutMs, + ); + try { + const init: RequestInit = { + method: options.method ?? 'GET', + headers: { + Accept: 'application/json', + 'X-Request-ID': getRequestId(), + ...(options.headers ?? {}), + }, + signal: controller.signal, + }; + if (options.body !== undefined) { + init.body = options.body; + } + const response = await fetch(url, init); + + if (!response.ok) { + this.logger.warn( + { url, status: response.status }, + 'External HTTP request failed', + ); + throw new Error(`External HTTP request failed with ${response.status}`); + } + + return (await response.json()) as T; + } finally { + clearTimeout(timeout); + } + } +} diff --git a/apps/backend/src/common/rate-limit/sensitive-rate-limit.decorator.ts b/apps/backend/src/common/rate-limit/sensitive-rate-limit.decorator.ts new file mode 100644 index 0000000..9bd0347 --- /dev/null +++ b/apps/backend/src/common/rate-limit/sensitive-rate-limit.decorator.ts @@ -0,0 +1,6 @@ +import { SetMetadata } from '@nestjs/common'; + +export const SENSITIVE_RATE_LIMIT_KEY = Symbol('SENSITIVE_RATE_LIMIT'); + +export const SensitiveRateLimit = () => + SetMetadata(SENSITIVE_RATE_LIMIT_KEY, true); diff --git a/apps/backend/src/common/request-context/request-context.ts b/apps/backend/src/common/request-context/request-context.ts new file mode 100644 index 0000000..96fb282 --- /dev/null +++ b/apps/backend/src/common/request-context/request-context.ts @@ -0,0 +1,12 @@ +import { AsyncLocalStorage } from 'node:async_hooks'; + +export interface RequestContext { + requestId: string; + userId?: string; +} + +export const requestContextStorage = new AsyncLocalStorage(); + +export function getRequestId(): string { + return requestContextStorage.getStore()?.requestId ?? 'unknown'; +} diff --git a/apps/backend/src/common/request-context/request-id.middleware.ts b/apps/backend/src/common/request-context/request-id.middleware.ts new file mode 100644 index 0000000..a76c74d --- /dev/null +++ b/apps/backend/src/common/request-context/request-id.middleware.ts @@ -0,0 +1,15 @@ +import { randomUUID } from 'node:crypto'; +import type { NextFunction, Request, Response } from 'express'; +import { requestContextStorage } from './request-context'; + +export const requestIdHeader = 'x-request-id'; + +export class RequestIdMiddleware { + use(req: Request, res: Response, next: NextFunction): void { + const incoming = req.header(requestIdHeader); + const requestId = + incoming && incoming.length <= 128 ? incoming : randomUUID(); + res.setHeader('X-Request-ID', requestId); + requestContextStorage.run({ requestId }, () => next()); + } +} diff --git a/apps/backend/src/common/security/validation.pipe.ts b/apps/backend/src/common/security/validation.pipe.ts new file mode 100644 index 0000000..4fbba7c --- /dev/null +++ b/apps/backend/src/common/security/validation.pipe.ts @@ -0,0 +1,42 @@ +import { ValidationPipe } from '@nestjs/common'; +import type { ValidationError } from '@nestjs/common'; +import { ErrorCode } from '../errors/error-codes'; +import { ApiError, type ValidationErrorDetail } from '../errors/api-error'; + +function flattenValidation( + errors: ValidationError[], + parent = '', +): ValidationErrorDetail[] { + return errors.flatMap((error) => { + const field = parent ? `${parent}.${error.property}` : error.property; + const own = error.constraints + ? [ + { + field, + messages: Object.values(error.constraints).map( + () => 'Ungueltiger Wert.', + ), + }, + ] + : []; + return [...own, ...flattenValidation(error.children ?? [], field)]; + }); +} + +export function createValidationPipe(): ValidationPipe { + return new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + transformOptions: { enableImplicitConversion: false }, + exceptionFactory: (errors) => { + const validation = flattenValidation(errors); + return new ApiError( + ErrorCode.ValidationFailed, + 'Bitte pruefen Sie die markierten Felder.', + 400, + validation, + ); + }, + }); +} diff --git a/apps/backend/src/config/config.module.ts b/apps/backend/src/config/config.module.ts new file mode 100644 index 0000000..66f19c2 --- /dev/null +++ b/apps/backend/src/config/config.module.ts @@ -0,0 +1,20 @@ +import { Global, Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { envFilePaths, loadConfigFromEnv } from './env'; +import { AppConfigService } from './config.service'; + +@Global() +@Module({ + imports: [ + ConfigModule.forRoot({ + cache: true, + envFilePath: envFilePaths, + expandVariables: false, + isGlobal: true, + validate: loadConfigFromEnv, + }), + ], + providers: [AppConfigService], + exports: [AppConfigService], +}) +export class AppConfigModule {} diff --git a/apps/backend/src/config/config.service.spec.ts b/apps/backend/src/config/config.service.spec.ts new file mode 100644 index 0000000..e36faac --- /dev/null +++ b/apps/backend/src/config/config.service.spec.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from 'vitest'; +import { loadConfigFromEnv } from './env'; + +const validEnv = { + NODE_ENV: 'test', + PORT: '3000', + APP_BASE_URL: 'http://localhost:3000', + DATABASE_HOST: 'localhost', + DATABASE_PORT: '3306', + DATABASE_NAME: 'business_app_test', + DATABASE_USER: 'test', + DATABASE_PASSWORD: 'test', + OIDC_ISSUER: 'https://idp.example.test', + OIDC_CLIENT_ID: 'client', + OIDC_CLIENT_SECRET: '12345678901234567890123456789012', + OIDC_ALLOWED_ALGORITHMS: 'RS256', + SESSION_SECRET: '12345678901234567890123456789012', + SESSION_ENCRYPTION_KEY: 'abcdefghijklmnopqrstuvwxyz123456', + CORS_ORIGINS: 'http://localhost:4200', +}; + +describe('loadConfigFromEnv', () => { + it('validates required configuration centrally', () => { + const config = loadConfigFromEnv(validEnv); + + expect(config.database.name).toBe('business_app_test'); + expect(config.frontendBaseUrl).toBe('http://localhost:3000'); + expect(config.oidc.allowedAlgorithms).toEqual(['RS256']); + expect(config.rateLimit.global).toEqual({ + windowSeconds: 60, + maxRequests: 300, + }); + expect(config.rateLimit.sensitive).toEqual({ + windowSeconds: 60, + maxRequests: 10, + }); + }); + + it('allows all rate limit buckets to be configured independently', () => { + const config = loadConfigFromEnv({ + ...validEnv, + RATE_LIMIT_WINDOW_SECONDS: '30', + RATE_LIMIT_MAX_REQUESTS: '200', + RATE_LIMIT_SENSITIVE_WINDOW_SECONDS: '90', + RATE_LIMIT_SENSITIVE_MAX_REQUESTS: '5', + }); + + expect(config.rateLimit).toEqual({ + global: { windowSeconds: 30, maxRequests: 200 }, + sensitive: { windowSeconds: 90, maxRequests: 5 }, + }); + }); + + it('uses a separate frontend base URL when configured', () => { + const config = loadConfigFromEnv({ + ...validEnv, + FRONTEND_BASE_URL: 'http://localhost:4200', + }); + + expect(config.frontendBaseUrl).toBe('http://localhost:4200'); + }); + + it('accepts an optional OIDC logout URL', () => { + const config = loadConfigFromEnv({ + ...validEnv, + OIDC_LOGOUT_URL: 'https://idp.example.test/logout', + }); + const withoutLogout = loadConfigFromEnv({ + ...validEnv, + OIDC_LOGOUT_URL: '', + }); + + expect(config.oidc.logoutUrl).toBe('https://idp.example.test/logout'); + expect(withoutLogout.oidc.logoutUrl).toBeUndefined(); + }); + + it('rejects unsafe production secret placeholders', () => { + expect(() => + loadConfigFromEnv({ + ...validEnv, + NODE_ENV: 'production', + OIDC_CLIENT_SECRET: 'change-me-change-me-change-me-change-me', + }), + ).toThrow(/Production-Secrets/); + }); + + it('accepts comma or whitespace separated OIDC signing algorithms', () => { + const config = loadConfigFromEnv({ + ...validEnv, + OIDC_ALLOWED_ALGORITHMS: 'RS256 PS256,ES256', + }); + + expect(config.oidc.allowedAlgorithms).toEqual(['RS256', 'PS256', 'ES256']); + }); + + it('rejects alg none in OIDC signing algorithms', () => { + expect(() => + loadConfigFromEnv({ + ...validEnv, + OIDC_ALLOWED_ALGORITHMS: 'RS256,none', + }), + ).toThrow(/OIDC_ALLOWED_ALGORITHMS/); + }); +}); diff --git a/apps/backend/src/config/config.service.ts b/apps/backend/src/config/config.service.ts new file mode 100644 index 0000000..b93c32e --- /dev/null +++ b/apps/backend/src/config/config.service.ts @@ -0,0 +1,102 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService as NestConfigService } from '@nestjs/config'; +import type { AppConfig, NodeEnv } from './config.types'; + +@Injectable() +export class AppConfigService { + private readonly config: AppConfig; + + constructor(private readonly nestConfig: NestConfigService) { + this.config = { + nodeEnv: this.nestConfig.getOrThrow('nodeEnv', { infer: true }), + port: this.nestConfig.getOrThrow('port', { infer: true }), + appBaseUrl: this.nestConfig.getOrThrow('appBaseUrl', { infer: true }), + frontendBaseUrl: this.nestConfig.getOrThrow('frontendBaseUrl', { + infer: true, + }), + trustProxy: this.nestConfig.getOrThrow('trustProxy', { infer: true }), + database: this.nestConfig.getOrThrow('database', { infer: true }), + oidc: this.nestConfig.getOrThrow('oidc', { infer: true }), + session: this.nestConfig.getOrThrow('session', { infer: true }), + corsOrigins: this.nestConfig.getOrThrow('corsOrigins', { infer: true }), + csrfHeaderName: this.nestConfig.getOrThrow('csrfHeaderName', { + infer: true, + }), + logLevel: this.nestConfig.getOrThrow('logLevel', { infer: true }), + swaggerEnabled: this.nestConfig.getOrThrow('swaggerEnabled', { + infer: true, + }), + rateLimit: this.nestConfig.getOrThrow('rateLimit', { infer: true }), + }; + } + + get nodeEnv(): NodeEnv { + return this.config.nodeEnv; + } + + get isProduction(): boolean { + return this.config.nodeEnv === 'production'; + } + + get port(): number { + return this.config.port; + } + + get appBaseUrl(): string { + return this.config.appBaseUrl; + } + + get frontendBaseUrl(): string { + return this.config.frontendBaseUrl; + } + + get trustProxy(): boolean { + return this.config.trustProxy; + } + + get database(): AppConfig['database'] { + return this.config.database; + } + + get oidc(): AppConfig['oidc'] { + return this.config.oidc; + } + + get session(): AppConfig['session'] { + return this.config.session; + } + + get corsOrigins(): string[] { + return this.config.corsOrigins; + } + + get csrfHeaderName(): string { + return this.config.csrfHeaderName; + } + + get logLevel(): string { + return this.config.logLevel; + } + + get swaggerEnabled(): boolean { + return this.config.swaggerEnabled; + } + + get rateLimit(): AppConfig['rateLimit'] { + return this.config.rateLimit; + } + + /** + * @deprecated Use rateLimit.global.windowSeconds. + */ + get rateLimitWindowSeconds(): number { + return this.config.rateLimit.global.windowSeconds; + } + + /** + * @deprecated Use rateLimit.global.maxRequests. + */ + get rateLimitMaxRequests(): number { + return this.config.rateLimit.global.maxRequests; + } +} diff --git a/apps/backend/src/config/config.types.ts b/apps/backend/src/config/config.types.ts new file mode 100644 index 0000000..91c8e45 --- /dev/null +++ b/apps/backend/src/config/config.types.ts @@ -0,0 +1,46 @@ +export type NodeEnv = 'development' | 'test' | 'production'; + +export interface RateLimitRuleConfig { + windowSeconds: number; + maxRequests: number; +} + +export interface AppConfig { + nodeEnv: NodeEnv; + port: number; + appBaseUrl: string; + frontendBaseUrl: string; + trustProxy: boolean; + database: { + host: string; + port: number; + name: string; + user: string; + password: string; + ssl: boolean; + }; + oidc: { + issuer: string; + clientId: string; + clientSecret: string; + scopes: string; + allowedAlgorithms: string[]; + httpTimeoutMs: number; + logoutUrl?: string; + }; + session: { + cookieName: string; + idleTimeoutSeconds: number; + absoluteTimeoutSeconds: number; + secret: string; + encryptionKey: string; + }; + corsOrigins: string[]; + csrfHeaderName: string; + logLevel: string; + swaggerEnabled: boolean; + rateLimit: { + global: RateLimitRuleConfig; + sensitive: RateLimitRuleConfig; + }; +} diff --git a/apps/backend/src/config/env.ts b/apps/backend/src/config/env.ts new file mode 100644 index 0000000..eae66d1 --- /dev/null +++ b/apps/backend/src/config/env.ts @@ -0,0 +1,157 @@ +import { config as loadDotenv } from 'dotenv'; +import { z } from 'zod'; +import type { AppConfig } from './config.types'; + +export const envFilePaths = ['.env', '../../.env']; + +const booleanFromString = z + .string() + .transform((value) => value.toLowerCase()) + .pipe(z.enum(['true', 'false'])) + .transform((value) => value === 'true'); + +const envSchema = z.object({ + NODE_ENV: z + .enum(['development', 'test', 'production']) + .default('development'), + PORT: z.coerce.number().int().min(1).max(65535).default(3000), + APP_BASE_URL: z.url(), + FRONTEND_BASE_URL: z.url().optional(), + TRUST_PROXY: booleanFromString.default(false), + DATABASE_HOST: z.string().min(1), + DATABASE_PORT: z.coerce.number().int().min(1).max(65535).default(3306), + DATABASE_NAME: z.string().min(1), + DATABASE_USER: z.string().min(1), + DATABASE_PASSWORD: z.string().min(1), + DATABASE_SSL: booleanFromString.default(false), + OIDC_ISSUER: z.url(), + OIDC_CLIENT_ID: z.string().min(1), + OIDC_CLIENT_SECRET: z.string().min(1), + OIDC_SCOPES: z.string().min(1).default('openid profile email'), + OIDC_LOGOUT_URL: z.preprocess( + (value) => (value === '' ? undefined : value), + z.url().optional(), + ), + OIDC_ALLOWED_ALGORITHMS: z + .string() + .min(1) + .transform((value) => + value + .split(/[\s,]+/) + .map((entry) => entry.trim()) + .filter(Boolean), + ) + .refine( + (algorithms) => + algorithms.length > 0 && + algorithms.every((algorithm) => algorithm.toLowerCase() !== 'none'), + 'OIDC_ALLOWED_ALGORITHMS darf none nicht erlauben.', + ), + OIDC_HTTP_TIMEOUT_MS: z.coerce + .number() + .int() + .min(1000) + .max(30000) + .default(5000), + SESSION_COOKIE_NAME: z.string().min(1).default('app_session'), + SESSION_IDLE_TIMEOUT_SECONDS: z.coerce.number().int().min(300).default(28800), + SESSION_ABSOLUTE_TIMEOUT_SECONDS: z.coerce + .number() + .int() + .min(3600) + .default(604800), + SESSION_SECRET: z.string().min(32), + SESSION_ENCRYPTION_KEY: z.string().min(32), + CORS_ORIGINS: z.string().transform((value) => + value + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean), + ), + CSRF_HEADER_NAME: z.string().min(1).default('X-CSRF-Token'), + LOG_LEVEL: z.string().min(1).default('info'), + SWAGGER_ENABLED: booleanFromString.default(false), + RATE_LIMIT_WINDOW_SECONDS: z.coerce.number().int().min(1).default(60), + RATE_LIMIT_MAX_REQUESTS: z.coerce.number().int().min(1).default(300), + RATE_LIMIT_SENSITIVE_WINDOW_SECONDS: z.coerce + .number() + .int() + .min(1) + .default(60), + RATE_LIMIT_SENSITIVE_MAX_REQUESTS: z.coerce.number().int().min(1).default(10), +}); + +export function loadConfigFromEnv(env: Record): AppConfig { + const parsed = envSchema.safeParse(env); + if (!parsed.success) { + const issues = parsed.error.issues.map( + (issue) => `${issue.path.join('.')}: ${issue.message}`, + ); + throw new Error(`Ungueltige Konfiguration:\n${issues.join('\n')}`); + } + + const value = parsed.data; + if (value.NODE_ENV === 'production') { + const insecure = [ + value.SESSION_SECRET.includes('change'), + value.SESSION_ENCRYPTION_KEY.includes('change'), + value.OIDC_CLIENT_SECRET.includes('change'), + ]; + if (insecure.some(Boolean)) { + throw new Error( + 'Production-Secrets muessen explizit und sicher gesetzt werden.', + ); + } + } + + return { + nodeEnv: value.NODE_ENV, + port: value.PORT, + appBaseUrl: value.APP_BASE_URL, + frontendBaseUrl: value.FRONTEND_BASE_URL ?? value.APP_BASE_URL, + trustProxy: value.TRUST_PROXY, + database: { + host: value.DATABASE_HOST, + port: value.DATABASE_PORT, + name: value.DATABASE_NAME, + user: value.DATABASE_USER, + password: value.DATABASE_PASSWORD, + ssl: value.DATABASE_SSL, + }, + oidc: { + issuer: value.OIDC_ISSUER, + clientId: value.OIDC_CLIENT_ID, + clientSecret: value.OIDC_CLIENT_SECRET, + scopes: value.OIDC_SCOPES, + allowedAlgorithms: value.OIDC_ALLOWED_ALGORITHMS, + httpTimeoutMs: value.OIDC_HTTP_TIMEOUT_MS, + ...(value.OIDC_LOGOUT_URL ? { logoutUrl: value.OIDC_LOGOUT_URL } : {}), + }, + session: { + cookieName: value.SESSION_COOKIE_NAME, + idleTimeoutSeconds: value.SESSION_IDLE_TIMEOUT_SECONDS, + absoluteTimeoutSeconds: value.SESSION_ABSOLUTE_TIMEOUT_SECONDS, + secret: value.SESSION_SECRET, + encryptionKey: value.SESSION_ENCRYPTION_KEY, + }, + corsOrigins: value.CORS_ORIGINS, + csrfHeaderName: value.CSRF_HEADER_NAME, + logLevel: value.LOG_LEVEL, + swaggerEnabled: value.SWAGGER_ENABLED, + rateLimit: { + global: { + windowSeconds: value.RATE_LIMIT_WINDOW_SECONDS, + maxRequests: value.RATE_LIMIT_MAX_REQUESTS, + }, + sensitive: { + windowSeconds: value.RATE_LIMIT_SENSITIVE_WINDOW_SECONDS, + maxRequests: value.RATE_LIMIT_SENSITIVE_MAX_REQUESTS, + }, + }, + }; +} + +export function loadConfigForCli(): AppConfig { + loadDotenv({ path: envFilePaths, override: false, quiet: true }); + return loadConfigFromEnv(process.env); +} diff --git a/apps/backend/src/dashboard/dashboard.controller.ts b/apps/backend/src/dashboard/dashboard.controller.ts new file mode 100644 index 0000000..03727fa --- /dev/null +++ b/apps/backend/src/dashboard/dashboard.controller.ts @@ -0,0 +1,15 @@ +import { Controller, Get } from '@nestjs/common'; +import { RequirePermissions } from '../auth/guards/require-permissions.decorator'; +import { Permission } from '../roles/permissions'; +import { DashboardService } from './dashboard.service'; + +@Controller('dashboard') +export class DashboardController { + constructor(private readonly dashboard: DashboardService) {} + + @Get() + @RequirePermissions(Permission.ItemsRead) + summary() { + return this.dashboard.summary(); + } +} diff --git a/apps/backend/src/dashboard/dashboard.module.ts b/apps/backend/src/dashboard/dashboard.module.ts new file mode 100644 index 0000000..c4a4a45 --- /dev/null +++ b/apps/backend/src/dashboard/dashboard.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { DashboardController } from './dashboard.controller'; +import { DashboardService } from './dashboard.service'; + +@Module({ + controllers: [DashboardController], + providers: [DashboardService], +}) +export class DashboardModule {} diff --git a/apps/backend/src/dashboard/dashboard.service.ts b/apps/backend/src/dashboard/dashboard.service.ts new file mode 100644 index 0000000..f44fa2d --- /dev/null +++ b/apps/backend/src/dashboard/dashboard.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; +import { IsNull } from 'typeorm'; +import { ItemEntity } from '../items/entities/item.entity'; +import { RoleEntity } from '../roles/entities/role.entity'; +import { SessionEntity } from '../sessions/entities/session.entity'; +import { UserEntity } from '../users/entities/user.entity'; + +@Injectable() +export class DashboardService { + constructor(@InjectDataSource() private readonly dataSource: DataSource) {} + + async summary(): Promise<{ + userCount: number; + activeSessions: number; + roleCount: number; + itemCount: number; + }> { + const [userCount, activeSessions, roleCount, itemCount] = await Promise.all( + [ + this.dataSource.getRepository(UserEntity).count(), + this.dataSource + .getRepository(SessionEntity) + .count({ where: { revokedAt: IsNull() } }), + this.dataSource.getRepository(RoleEntity).count(), + this.dataSource.getRepository(ItemEntity).count(), + ], + ); + return { userCount, activeSessions, roleCount, itemCount }; + } +} diff --git a/apps/backend/src/database/database.module.ts b/apps/backend/src/database/database.module.ts new file mode 100644 index 0000000..1c58473 --- /dev/null +++ b/apps/backend/src/database/database.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { MigrationHealthService } from './migration-health.service'; + +@Module({ + providers: [MigrationHealthService], + exports: [MigrationHealthService], +}) +export class DatabaseModule {} diff --git a/apps/backend/src/database/entities.ts b/apps/backend/src/database/entities.ts new file mode 100644 index 0000000..c9dbebf --- /dev/null +++ b/apps/backend/src/database/entities.ts @@ -0,0 +1,21 @@ +import { AuditLogEntity } from '../audit/entities/audit-log.entity'; +import { OidcLoginStateEntity } from '../auth/entities/oidc-login-state.entity'; +import { ItemEntity } from '../items/entities/item.entity'; +import { NotificationEntity } from '../notifications/entities/notification.entity'; +import { RoleEntity } from '../roles/entities/role.entity'; +import { PermissionEntity } from '../roles/entities/permission.entity'; +import { SessionEntity } from '../sessions/entities/session.entity'; +import { UserSettingsEntity } from '../users/entities/user-settings.entity'; +import { UserEntity } from '../users/entities/user.entity'; + +export const entities = [ + AuditLogEntity, + OidcLoginStateEntity, + ItemEntity, + NotificationEntity, + RoleEntity, + PermissionEntity, + SessionEntity, + UserSettingsEntity, + UserEntity, +]; diff --git a/apps/backend/src/database/migration-health.service.ts b/apps/backend/src/database/migration-health.service.ts new file mode 100644 index 0000000..19011dc --- /dev/null +++ b/apps/backend/src/database/migration-health.service.ts @@ -0,0 +1,41 @@ +import { Injectable, ServiceUnavailableException } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; +import { ErrorCode } from '../common/errors/error-codes'; +import { ApiError } from '../common/errors/api-error'; + +@Injectable() +export class MigrationHealthService { + private initialized = false; + private missingMigrations = false; + + constructor(@InjectDataSource() private readonly dataSource: DataSource) {} + + async assertNoPendingMigrations(): Promise { + const pending = await this.dataSource.showMigrations(); + this.initialized = true; + this.missingMigrations = pending; + if (pending) { + throw new ApiError( + ErrorCode.MigrationMissing, + 'Es fehlen Datenbankmigrationen. Fuehren Sie npm run migration:run aus.', + 503, + ); + } + } + + isReady(): boolean { + return ( + this.initialized && + !this.missingMigrations && + this.dataSource.isInitialized + ); + } + + async ping(): Promise { + if (!this.isReady()) { + throw new ServiceUnavailableException(); + } + await this.dataSource.query('SELECT 1'); + } +} diff --git a/apps/backend/src/database/migrations/1720000000000-InitialSchema.ts b/apps/backend/src/database/migrations/1720000000000-InitialSchema.ts new file mode 100644 index 0000000..3bee2bb --- /dev/null +++ b/apps/backend/src/database/migrations/1720000000000-InitialSchema.ts @@ -0,0 +1,145 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class InitialSchema1720000000000 implements MigrationInterface { + name = 'InitialSchema1720000000000'; + + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE permissions ( + id varchar(80) NOT NULL, + description varchar(160) NOT NULL, + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `); + await queryRunner.query(` + CREATE TABLE roles ( + id char(36) NOT NULL, + name varchar(80) NOT NULL, + description varchar(255) NOT NULL DEFAULT '', + protected tinyint NOT NULL DEFAULT 0, + created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + UNIQUE KEY uq_roles_name (name), + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `); + await queryRunner.query(` + CREATE TABLE role_permissions ( + role_id char(36) NOT NULL, + permission_id varchar(80) NOT NULL, + PRIMARY KEY (role_id, permission_id), + CONSTRAINT fk_role_permissions_role FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, + CONSTRAINT fk_role_permissions_permission FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `); + await queryRunner.query(` + CREATE TABLE users ( + id char(36) NOT NULL, + issuer varchar(255) NOT NULL, + subject varchar(255) NOT NULL, + name varchar(255) NOT NULL, + email varchar(320) NULL, + active tinyint NOT NULL DEFAULT 1, + last_login_at datetime(3) NULL, + created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + UNIQUE KEY uq_users_issuer_subject (issuer, subject), + KEY idx_users_issuer (issuer), + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `); + await queryRunner.query(` + CREATE TABLE user_roles ( + user_id char(36) NOT NULL, + role_id char(36) NOT NULL, + PRIMARY KEY (user_id, role_id), + CONSTRAINT fk_user_roles_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + CONSTRAINT fk_user_roles_role FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `); + await queryRunner.query(` + CREATE TABLE user_settings ( + id char(36) NOT NULL, + user_id char(36) NOT NULL, + table_page_size int NOT NULL DEFAULT 20, + sidebar_expanded tinyint NOT NULL DEFAULT 1, + UNIQUE KEY uq_user_settings_user (user_id), + CONSTRAINT fk_user_settings_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `); + await queryRunner.query(` + CREATE TABLE sessions ( + id char(64) NOT NULL, + user_id char(36) NOT NULL, + access_token_encrypted text NOT NULL, + refresh_token_encrypted text NULL, + id_token_encrypted text NULL, + csrf_token_hash char(64) NOT NULL, + access_token_expires_at datetime(3) NOT NULL, + expires_at datetime(3) NOT NULL, + absolute_expires_at datetime(3) NOT NULL, + last_activity_at datetime(3) NOT NULL, + user_agent varchar(512) NULL, + last_ip varchar(80) NULL, + revoked_at datetime(3) NULL, + created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + KEY idx_sessions_user_id (user_id), + CONSTRAINT fk_sessions_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `); + await queryRunner.query(` + CREATE TABLE oidc_login_states ( + state char(64) NOT NULL, + code_verifier varchar(160) NOT NULL, + nonce varchar(160) NOT NULL, + expires_at datetime(3) NOT NULL, + created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (state) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `); + await queryRunner.query(` + CREATE TABLE audit_logs ( + id char(36) NOT NULL, + created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + actor_user_id char(36) NULL, + action varchar(80) NOT NULL, + target_type varchar(80) NOT NULL, + target_id varchar(120) NOT NULL, + metadata json NULL, + request_id varchar(128) NOT NULL, + KEY idx_audit_created_at (created_at), + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `); + await queryRunner.query(` + CREATE TABLE items ( + id char(36) NOT NULL, + name varchar(160) NOT NULL, + description text NULL, + status varchar(30) NOT NULL DEFAULT 'draft', + version int NOT NULL DEFAULT 1, + created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + deleted_at datetime(3) NULL, + KEY idx_items_name (name), + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `); + } + + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('DROP TABLE items'); + await queryRunner.query('DROP TABLE audit_logs'); + await queryRunner.query('DROP TABLE oidc_login_states'); + await queryRunner.query('DROP TABLE sessions'); + await queryRunner.query('DROP TABLE user_settings'); + await queryRunner.query('DROP TABLE user_roles'); + await queryRunner.query('DROP TABLE users'); + await queryRunner.query('DROP TABLE role_permissions'); + await queryRunner.query('DROP TABLE roles'); + await queryRunner.query('DROP TABLE permissions'); + } +} diff --git a/apps/backend/src/database/migrations/1720000001000-AddNotifications.ts b/apps/backend/src/database/migrations/1720000001000-AddNotifications.ts new file mode 100644 index 0000000..18b7b32 --- /dev/null +++ b/apps/backend/src/database/migrations/1720000001000-AddNotifications.ts @@ -0,0 +1,31 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddNotifications1720000001000 implements MigrationInterface { + name = 'AddNotifications1720000001000'; + + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE notifications ( + id char(36) NOT NULL, + user_id char(36) NOT NULL, + type varchar(80) NOT NULL, + title varchar(150) NOT NULL, + message varchar(1000) NOT NULL, + link varchar(500) NULL, + metadata json NULL, + read_at datetime(3) NULL, + created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + deleted_at datetime(3) NULL, + KEY idx_notifications_user_created_at (user_id, created_at), + KEY idx_notifications_user_read_at (user_id, read_at), + KEY idx_notifications_user_deleted_at (user_id, deleted_at), + CONSTRAINT fk_notifications_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `); + } + + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('DROP TABLE notifications'); + } +} diff --git a/apps/backend/src/database/migrations/1720000002000-AddRoleDescription.ts b/apps/backend/src/database/migrations/1720000002000-AddRoleDescription.ts new file mode 100644 index 0000000..cd1ffcb --- /dev/null +++ b/apps/backend/src/database/migrations/1720000002000-AddRoleDescription.ts @@ -0,0 +1,16 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddRoleDescription1720000002000 implements MigrationInterface { + name = 'AddRoleDescription1720000002000'; + + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE roles + ADD description varchar(255) NOT NULL DEFAULT '' + `); + } + + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE roles DROP COLUMN description'); + } +} diff --git a/apps/backend/src/database/run-migrations.ts b/apps/backend/src/database/run-migrations.ts new file mode 100644 index 0000000..4155009 --- /dev/null +++ b/apps/backend/src/database/run-migrations.ts @@ -0,0 +1,13 @@ +import 'reflect-metadata'; +import dataSource from './typeorm-cli.datasource'; + +async function run(): Promise { + await dataSource.initialize(); + try { + await dataSource.runMigrations({ transaction: 'all' }); + } finally { + await dataSource.destroy(); + } +} + +void run(); diff --git a/apps/backend/src/database/typeorm-cli.datasource.ts b/apps/backend/src/database/typeorm-cli.datasource.ts new file mode 100644 index 0000000..c6dd351 --- /dev/null +++ b/apps/backend/src/database/typeorm-cli.datasource.ts @@ -0,0 +1,29 @@ +import 'reflect-metadata'; +import { DataSource } from 'typeorm'; +import { loadConfigForCli } from '../config/env'; +import { entities } from './entities'; +import { InitialSchema1720000000000 } from './migrations/1720000000000-InitialSchema'; +import { AddNotifications1720000001000 } from './migrations/1720000001000-AddNotifications'; +import { AddRoleDescription1720000002000 } from './migrations/1720000002000-AddRoleDescription'; + +const config = loadConfigForCli(); + +export default new DataSource({ + type: 'mysql', + host: config.database.host, + port: config.database.port, + username: config.database.user, + password: config.database.password, + database: config.database.name, + charset: 'utf8mb4_unicode_ci', + timezone: 'Z', + ssl: config.database.ssl ? { rejectUnauthorized: true } : undefined, + synchronize: false, + migrationsRun: false, + entities, + migrations: [ + InitialSchema1720000000000, + AddNotifications1720000001000, + AddRoleDescription1720000002000, + ], +}); diff --git a/apps/backend/src/database/typeorm-options.ts b/apps/backend/src/database/typeorm-options.ts new file mode 100644 index 0000000..ecfb86c --- /dev/null +++ b/apps/backend/src/database/typeorm-options.ts @@ -0,0 +1,30 @@ +import type { TypeOrmModuleOptions } from '@nestjs/typeorm'; +import type { AppConfigService } from '../config/config.service'; +import { entities } from './entities'; +import { InitialSchema1720000000000 } from './migrations/1720000000000-InitialSchema'; +import { AddNotifications1720000001000 } from './migrations/1720000001000-AddNotifications'; +import { AddRoleDescription1720000002000 } from './migrations/1720000002000-AddRoleDescription'; + +export function typeOrmOptionsFactory( + config: AppConfigService, +): TypeOrmModuleOptions { + return { + type: 'mysql', + host: config.database.host, + port: config.database.port, + username: config.database.user, + password: config.database.password, + database: config.database.name, + charset: 'utf8mb4_unicode_ci', + timezone: 'Z', + ssl: config.database.ssl ? { rejectUnauthorized: true } : undefined, + synchronize: false, + migrationsRun: false, + entities, + migrations: [ + InitialSchema1720000000000, + AddNotifications1720000001000, + AddRoleDescription1720000002000, + ], + }; +} diff --git a/apps/backend/src/health/health.controller.ts b/apps/backend/src/health/health.controller.ts new file mode 100644 index 0000000..4dc0c79 --- /dev/null +++ b/apps/backend/src/health/health.controller.ts @@ -0,0 +1,21 @@ +import { Controller, Get } from '@nestjs/common'; +import { Public } from '../auth/guards/public.decorator'; +import { MigrationHealthService } from '../database/migration-health.service'; + +@Controller('health') +export class HealthController { + constructor(private readonly migrations: MigrationHealthService) {} + + @Get('live') + @Public() + live() { + return { status: 'ok' }; + } + + @Get('ready') + @Public() + async ready() { + await this.migrations.ping(); + return { status: 'ok' }; + } +} diff --git a/apps/backend/src/health/health.module.ts b/apps/backend/src/health/health.module.ts new file mode 100644 index 0000000..3fd050b --- /dev/null +++ b/apps/backend/src/health/health.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { DatabaseModule } from '../database/database.module'; +import { HealthController } from './health.controller'; + +@Module({ + imports: [DatabaseModule], + controllers: [HealthController], +}) +export class HealthModule {} diff --git a/apps/backend/src/items/dto/item.dto.ts b/apps/backend/src/items/dto/item.dto.ts new file mode 100644 index 0000000..0a2cb27 --- /dev/null +++ b/apps/backend/src/items/dto/item.dto.ts @@ -0,0 +1,58 @@ +import { + IsEnum, + IsInt, + IsOptional, + IsString, + Length, + Max, + Min, +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { ItemStatus } from '../entities/item.entity'; + +export class ItemListQueryDto { + @IsOptional() + @IsString() + search?: string; + + @IsOptional() + @IsString() + sort?: 'name' | 'status' | 'createdAt' | 'updatedAt'; + + @IsOptional() + @IsString() + direction?: 'ASC' | 'DESC'; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page = 1; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + pageSize = 20; +} + +export class CreateItemDto { + @IsString() + @Length(1, 160) + name!: string; + + @IsOptional() + @IsString() + @Length(0, 4000) + description?: string; + + @IsEnum(ItemStatus) + status!: ItemStatus; +} + +export class UpdateItemDto extends CreateItemDto { + @IsInt() + @Min(1) + version!: number; +} diff --git a/apps/backend/src/items/entities/item.entity.ts b/apps/backend/src/items/entities/item.entity.ts new file mode 100644 index 0000000..2430899 --- /dev/null +++ b/apps/backend/src/items/entities/item.entity.ts @@ -0,0 +1,49 @@ +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + Index, + PrimaryGeneratedColumn, + UpdateDateColumn, + VersionColumn, +} from 'typeorm'; + +export enum ItemStatus { + Draft = 'draft', + Active = 'active', + Archived = 'archived', +} + +@Entity('items') +export class ItemEntity { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Index('idx_items_name') + @Column({ type: 'varchar', length: 160 }) + name!: string; + + @Column({ type: 'text', nullable: true }) + description!: string | null; + + @Column({ type: 'varchar', length: 30, default: ItemStatus.Draft }) + status!: ItemStatus; + + @VersionColumn({ type: 'int' }) + version!: number; + + @CreateDateColumn({ name: 'created_at', type: 'datetime', precision: 3 }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'datetime', precision: 3 }) + updatedAt!: Date; + + @DeleteDateColumn({ + name: 'deleted_at', + type: 'datetime', + precision: 3, + nullable: true, + }) + deletedAt!: Date | null; +} diff --git a/apps/backend/src/items/items.controller.ts b/apps/backend/src/items/items.controller.ts new file mode 100644 index 0000000..79506ea --- /dev/null +++ b/apps/backend/src/items/items.controller.ts @@ -0,0 +1,53 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + Query, + Req, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import type { AuthenticatedRequest } from '../auth/authenticated-request'; +import { RequirePermissions } from '../auth/guards/require-permissions.decorator'; +import { Permission } from '../roles/permissions'; +import { CreateItemDto, ItemListQueryDto, UpdateItemDto } from './dto/item.dto'; +import { ItemsService } from './items.service'; + +@ApiTags('items') +@Controller('items') +export class ItemsController { + constructor(private readonly items: ItemsService) {} + + @Get() + @RequirePermissions(Permission.ItemsRead) + list(@Query() query: ItemListQueryDto) { + return this.items.list(query); + } + + @Get(':id') + @RequirePermissions(Permission.ItemsRead) + get(@Param('id') id: string) { + return this.items.get(id); + } + + @Post() + @RequirePermissions(Permission.ItemsCreate) + create(@Req() req: AuthenticatedRequest, @Body() dto: CreateItemDto) { + return this.items.create(dto, req.user?.id); + } + + @Put(':id') + @RequirePermissions(Permission.ItemsUpdate) + update(@Param('id') id: string, @Body() dto: UpdateItemDto) { + return this.items.update(id, dto); + } + + @Delete(':id') + @RequirePermissions(Permission.ItemsDelete) + delete(@Param('id') id: string, @Query('version') version: number) { + return this.items.delete(id, Number(version)); + } +} diff --git a/apps/backend/src/items/items.module.ts b/apps/backend/src/items/items.module.ts new file mode 100644 index 0000000..869ad57 --- /dev/null +++ b/apps/backend/src/items/items.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { NotificationsModule } from '../notifications/notifications.module'; +import { UserEntity } from '../users/entities/user.entity'; +import { UsersRepository } from '../users/repositories/users.repository'; +import { ItemEntity } from './entities/item.entity'; +import { ItemsController } from './items.controller'; +import { ItemsService } from './items.service'; +import { ItemsRepository } from './repositories/items.repository'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ItemEntity, UserEntity]), + NotificationsModule, + ], + controllers: [ItemsController], + providers: [ItemsService, ItemsRepository, UsersRepository], +}) +export class ItemsModule {} diff --git a/apps/backend/src/items/items.service.ts b/apps/backend/src/items/items.service.ts new file mode 100644 index 0000000..3ac89f3 --- /dev/null +++ b/apps/backend/src/items/items.service.ts @@ -0,0 +1,118 @@ +import { Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; +import { ApiError } from '../common/errors/api-error'; +import { ErrorCode } from '../common/errors/error-codes'; +import type { PageDto } from '../common/dto/pagination.dto'; +import { NotificationType } from '../notifications/notification-types'; +import { NotificationsService } from '../notifications/notifications.service'; +import { UsersRepository } from '../users/repositories/users.repository'; +import { ItemEntity } from './entities/item.entity'; +import type { + CreateItemDto, + ItemListQueryDto, + UpdateItemDto, +} from './dto/item.dto'; +import { + ItemsRepository, + type ItemSortField, +} from './repositories/items.repository'; + +@Injectable() +export class ItemsService { + private readonly logger = new Logger(ItemsService.name); + + constructor( + private readonly items: ItemsRepository, + private readonly notifications: NotificationsService, + private readonly users: UsersRepository, + ) {} + + async list(query: ItemListQueryDto): Promise> { + const page = query.page ?? 1; + const pageSize = query.pageSize ?? 20; + const sort: ItemSortField = query.sort ?? 'updatedAt'; + const direction = query.direction ?? 'DESC'; + const [items, total] = await this.items.list( + query.search, + page, + pageSize, + sort, + direction, + ); + return { items, total, page, pageSize }; + } + + async get(id: string): Promise { + const item = await this.items.findById(id); + if (!item) { + throw new ApiError( + ErrorCode.NotFound, + 'Der Eintrag wurde nicht gefunden.', + 404, + ); + } + return item; + } + + async create(dto: CreateItemDto, actorUserId?: string): Promise { + const item = new ItemEntity(); + item.name = dto.name; + item.description = dto.description ?? null; + item.status = dto.status; + const saved = await this.items.save(item); + await this.notifyFirstAdminAboutCreatedItem(saved, actorUserId); + return saved; + } + + async update(id: string, dto: UpdateItemDto): Promise { + const item = await this.get(id); + if (item.version !== dto.version) { + throw new ApiError( + ErrorCode.Conflict, + 'Der Eintrag wurde zwischenzeitlich geaendert. Bitte laden Sie ihn neu.', + 409, + ); + } + item.name = dto.name; + item.description = dto.description ?? null; + item.status = dto.status; + return this.items.save(item); + } + + async delete(id: string, version: number): Promise { + const item = await this.get(id); + if (item.version !== version) { + throw new ApiError( + ErrorCode.Conflict, + 'Der Eintrag wurde zwischenzeitlich geaendert. Bitte laden Sie ihn neu.', + 409, + ); + } + await this.items.softDelete(item); + } + + private async notifyFirstAdminAboutCreatedItem( + item: ItemEntity, + actorUserId: string | undefined, + ): Promise { + try { + const admin = await this.users.findFirstActiveAdmin(actorUserId); + if (!admin) { + return; + } + await this.notifications.createForUser({ + userId: admin.id, + type: NotificationType.ItemCreated, + title: 'Neuer Eintrag', + message: `Der Eintrag "${item.name}" wurde erstellt.`, + link: `/items/${item.id}`, + metadata: { itemId: item.id }, + }); + } catch (error) { + this.logger.error( + { itemId: item.id, error }, + 'Failed to create item notification', + ); + } + } +} diff --git a/apps/backend/src/items/repositories/items.repository.ts b/apps/backend/src/items/repositories/items.repository.ts new file mode 100644 index 0000000..3fdea65 --- /dev/null +++ b/apps/backend/src/items/repositories/items.repository.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ItemEntity } from '../entities/item.entity'; + +export type ItemSortField = 'name' | 'status' | 'createdAt' | 'updatedAt'; + +@Injectable() +export class ItemsRepository { + constructor( + @InjectRepository(ItemEntity) private readonly repo: Repository, + ) {} + + list( + search: string | undefined, + page: number, + pageSize: number, + sort: ItemSortField, + direction: 'ASC' | 'DESC', + ): Promise<[ItemEntity[], number]> { + const qb = this.repo.createQueryBuilder('item'); + if (search) { + qb.where('item.name LIKE :search OR item.description LIKE :search', { + search: `%${search}%`, + }); + } + return qb + .orderBy(`item.${sort}`, direction) + .skip((page - 1) * pageSize) + .take(pageSize) + .getManyAndCount(); + } + + findById(id: string): Promise { + return this.repo.findOne({ where: { id } }); + } + + save(item: ItemEntity): Promise { + return this.repo.save(item); + } + + async softDelete(item: ItemEntity): Promise { + await this.repo.softRemove(item); + } +} diff --git a/apps/backend/src/items/tests/items.service.spec.ts b/apps/backend/src/items/tests/items.service.spec.ts new file mode 100644 index 0000000..952a1fa --- /dev/null +++ b/apps/backend/src/items/tests/items.service.spec.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from 'vitest'; +import { ErrorCode } from '../../common/errors/error-codes'; +import { ItemEntity, ItemStatus } from '../entities/item.entity'; +import { ItemsService } from '../items.service'; +import type { NotificationsService } from '../../notifications/notifications.service'; +import type { UsersRepository } from '../../users/repositories/users.repository'; +import type { ItemsRepository } from '../repositories/items.repository'; + +function item(version = 2): ItemEntity { + const entity = new ItemEntity(); + entity.id = 'item-1'; + entity.name = 'Alt'; + entity.description = null; + entity.status = ItemStatus.Draft; + entity.version = version; + entity.createdAt = new Date(); + entity.updatedAt = new Date(); + entity.deletedAt = null; + return entity; +} + +describe('ItemsService', () => { + it('answers stale updates with HTTP 409 conflict semantics', async () => { + const repo: Pick = { + findById: () => Promise.resolve(item(3)), + save: (entity) => Promise.resolve(entity), + }; + const service = new ItemsService( + repo as ItemsRepository, + {} as NotificationsService, + {} as UsersRepository, + ); + + await expect( + service.update('item-1', { + name: 'Neu', + description: 'Beschreibung', + status: ItemStatus.Active, + version: 2, + }), + ).rejects.toMatchObject({ code: ErrorCode.Conflict, status: 409 }); + }); + + it('soft deletes only when the submitted version is current', async () => { + let deleted = false; + const repo: Pick = { + findById: () => Promise.resolve(item(4)), + softDelete: () => { + deleted = true; + return Promise.resolve(); + }, + }; + const service = new ItemsService( + repo as ItemsRepository, + {} as NotificationsService, + {} as UsersRepository, + ); + + await service.delete('item-1', 4); + + expect(deleted).toBe(true); + }); +}); diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts new file mode 100644 index 0000000..c1dfcb7 --- /dev/null +++ b/apps/backend/src/main.ts @@ -0,0 +1,99 @@ +import { join } from 'node:path'; +import cookieParser from 'cookie-parser'; +import type { NextFunction, Request, Response } from 'express'; +import helmet from 'helmet'; +import pinoHttp from 'pino-http'; +import { NestFactory } from '@nestjs/core'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import type { NestExpressApplication } from '@nestjs/platform-express'; +import { AppModule } from './app.module'; +import { createValidationPipe } from './common/security/validation.pipe'; +import { AppConfigService } from './config/config.service'; +import { MigrationHealthService } from './database/migration-health.service'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule, { + bufferLogs: true, + }); + const config = app.get(AppConfigService); + + app.use( + pinoHttp({ + level: config.logLevel, + ...(config.isProduction ? {} : { transport: { target: 'pino-pretty' } }), + redact: { + paths: [ + 'req.headers.authorization', + 'req.headers.cookie', + 'req.headers["x-csrf-token"]', + 'req.headers["x-session-id"]', + 'res.headers["set-cookie"]', + '*.access_token', + '*.refresh_token', + '*.id_token', + ], + censor: '[redacted]', + }, + customProps: (req) => ({ + requestId: req.headers['x-request-id'], + }), + }), + ); + app.use(cookieParser(config.session.secret)); + app.use( + helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'"], + imgSrc: ["'self'", 'data:'], + connectSrc: ["'self'"], + frameAncestors: ["'none'"], + }, + }, + }), + ); + app.set('trust proxy', config.trustProxy); + app.enableCors({ + origin: config.corsOrigins, + credentials: true, + methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', config.csrfHeaderName, 'X-Request-ID'], + }); + app.setGlobalPrefix('api', { + exclude: ['health/live', 'health/ready'], + }); + app.useGlobalPipes(createValidationPipe()); + app.enableShutdownHooks(); + + if (config.swaggerEnabled) { + const document = SwaggerModule.createDocument( + app, + new DocumentBuilder() + .setTitle('Business App API') + .setDescription('Interne Business-Anwendung') + .setVersion('1.0.0') + .build(), + ); + SwaggerModule.setup('api/docs', app, document, { + jsonDocumentUrl: '/api/docs-json', + }); + } + + app.useStaticAssets(join(__dirname, '..', 'public'), { + index: false, + fallthrough: true, + }); + app.use((req: Request, res: Response, next: NextFunction) => { + if (req.url.startsWith('/api') || req.url.startsWith('/health')) { + next(); + return; + } + res.sendFile(join(__dirname, '..', 'public', 'index.html')); + }); + + await app.get(MigrationHealthService).assertNoPendingMigrations(); + await app.listen(config.port); +} +void bootstrap(); diff --git a/apps/backend/src/notifications/dto/notification.dto.ts b/apps/backend/src/notifications/dto/notification.dto.ts new file mode 100644 index 0000000..185ac20 --- /dev/null +++ b/apps/backend/src/notifications/dto/notification.dto.ts @@ -0,0 +1,78 @@ +import { Type } from 'class-transformer'; +import { + IsIn, + IsObject, + IsOptional, + IsString, + IsUUID, + Length, + Max, + Min, +} from 'class-validator'; +import { allNotificationTypes } from '../notification-types'; +import type { NotificationMetadata } from '../entities/notification.entity'; + +export type NotificationStatusFilter = 'all' | 'read' | 'unread'; + +export class NotificationListQueryDto { + @IsOptional() + @Type(() => Number) + @Min(1) + page = 1; + + @IsOptional() + @Type(() => Number) + @Min(1) + @Max(100) + pageSize = 20; + + @IsOptional() + @IsIn(['all', 'read', 'unread']) + status: NotificationStatusFilter = 'all'; +} + +export class CreateNotificationDto { + @IsUUID() + userId!: string; + + @IsString() + @IsIn(allNotificationTypes) + type!: string; + + @IsString() + @Length(1, 150) + title!: string; + + @IsString() + @Length(1, 1000) + message!: string; + + @IsOptional() + @IsString() + @Length(1, 500) + link?: string; + + @IsOptional() + @IsObject() + metadata?: NotificationMetadata; +} + +export interface NotificationDto { + id: string; + type: string; + title: string; + message: string; + link: string | null; + metadata: NotificationMetadata | null; + read: boolean; + readAt: string | null; + createdAt: string; +} + +export interface NotificationPageDto { + items: NotificationDto[]; + total: number; + page: number; + pageSize: number; + unreadCount: number; +} diff --git a/apps/backend/src/notifications/entities/notification.entity.ts b/apps/backend/src/notifications/entities/notification.entity.ts new file mode 100644 index 0000000..bba1d46 --- /dev/null +++ b/apps/backend/src/notifications/entities/notification.entity.ts @@ -0,0 +1,62 @@ +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { UserEntity } from '../../users/entities/user.entity'; +import type { NotificationType } from '../notification-types'; + +export type NotificationMetadata = Record< + string, + string | number | boolean | null +>; + +@Entity('notifications') +@Index('idx_notifications_user_created_at', ['userId', 'createdAt']) +@Index('idx_notifications_user_read_at', ['userId', 'readAt']) +@Index('idx_notifications_user_deleted_at', ['userId', 'deletedAt']) +export class NotificationEntity { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @ManyToOne(() => UserEntity, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user!: UserEntity; + + @Column({ name: 'user_id', type: 'char', length: 36 }) + userId!: string; + + @Column({ type: 'varchar', length: 80 }) + type!: NotificationType; + + @Column({ type: 'varchar', length: 150 }) + title!: string; + + @Column({ type: 'varchar', length: 1000 }) + message!: string; + + @Column({ type: 'varchar', length: 500, nullable: true }) + link!: string | null; + + @Column({ type: 'json', nullable: true }) + metadata!: NotificationMetadata | null; + + @Column({ name: 'read_at', type: 'datetime', precision: 3, nullable: true }) + readAt!: Date | null; + + @CreateDateColumn({ name: 'created_at', type: 'datetime', precision: 3 }) + createdAt!: Date; + + @DeleteDateColumn({ + name: 'deleted_at', + type: 'datetime', + precision: 3, + nullable: true, + }) + deletedAt!: Date | null; +} diff --git a/apps/backend/src/notifications/notification-types.ts b/apps/backend/src/notifications/notification-types.ts new file mode 100644 index 0000000..abb7073 --- /dev/null +++ b/apps/backend/src/notifications/notification-types.ts @@ -0,0 +1,15 @@ +export const NotificationType = { + System: 'system', + ItemCreated: 'item.created', + ItemUpdated: 'item.updated', + UserRoleChanged: 'user.role-changed', +} as const; + +export type NotificationType = + (typeof NotificationType)[keyof typeof NotificationType]; + +export const allNotificationTypes = Object.values(NotificationType); + +export function isNotificationType(value: string): value is NotificationType { + return allNotificationTypes.includes(value as NotificationType); +} diff --git a/apps/backend/src/notifications/notifications.controller.ts b/apps/backend/src/notifications/notifications.controller.ts new file mode 100644 index 0000000..d75955b --- /dev/null +++ b/apps/backend/src/notifications/notifications.controller.ts @@ -0,0 +1,95 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + Query, + Req, +} from '@nestjs/common'; +import type { AuthenticatedRequest } from '../auth/authenticated-request'; +import { RequirePermissions } from '../auth/guards/require-permissions.decorator'; +import { ApiError } from '../common/errors/api-error'; +import { ErrorCode } from '../common/errors/error-codes'; +import { SensitiveRateLimit } from '../common/rate-limit/sensitive-rate-limit.decorator'; +import { Permission } from '../roles/permissions'; +import { + CreateNotificationDto, + NotificationListQueryDto, +} from './dto/notification.dto'; +import { NotificationsService } from './notifications.service'; + +@Controller() +export class NotificationsController { + constructor(private readonly notifications: NotificationsService) {} + + @Get('notifications') + @RequirePermissions(Permission.NotificationsReadOwn) + list( + @Req() req: AuthenticatedRequest, + @Query() query: NotificationListQueryDto, + ) { + return this.notifications.getForCurrentUser( + this.requireUser(req).id, + query, + ); + } + + @Get('notifications/unread-count') + @RequirePermissions(Permission.NotificationsReadOwn) + async unreadCount(@Req() req: AuthenticatedRequest) { + return { + count: await this.notifications.getUnreadCount(this.requireUser(req).id), + }; + } + + @Patch('notifications/:id/read') + @RequirePermissions(Permission.NotificationsUpdateOwn) + markAsRead(@Req() req: AuthenticatedRequest, @Param('id') id: string) { + return this.notifications.markAsRead(this.requireUser(req).id, id); + } + + @Patch('notifications/:id/unread') + @RequirePermissions(Permission.NotificationsUpdateOwn) + markAsUnread(@Req() req: AuthenticatedRequest, @Param('id') id: string) { + return this.notifications.markAsUnread(this.requireUser(req).id, id); + } + + @Patch('notifications/read-all') + @RequirePermissions(Permission.NotificationsUpdateOwn) + markAllAsRead(@Req() req: AuthenticatedRequest) { + return this.notifications.markAllAsRead(this.requireUser(req).id); + } + + @Delete('notifications/:id') + @RequirePermissions(Permission.NotificationsUpdateOwn) + delete(@Req() req: AuthenticatedRequest, @Param('id') id: string) { + return this.notifications.softDelete(this.requireUser(req).id, id); + } + + @Post('admin/notifications') + @RequirePermissions(Permission.NotificationsManage) + @SensitiveRateLimit() + createAdmin( + @Req() req: AuthenticatedRequest, + @Body() dto: CreateNotificationDto, + ) { + return this.notifications.createAdminNotification( + this.requireUser(req).id, + dto, + ); + } + + private requireUser(req: AuthenticatedRequest) { + if (!req.user) { + throw new ApiError( + ErrorCode.Unauthorized, + 'Bitte melden Sie sich an.', + 401, + ); + } + return req.user; + } +} diff --git a/apps/backend/src/notifications/notifications.module.ts b/apps/backend/src/notifications/notifications.module.ts new file mode 100644 index 0000000..e9b92b5 --- /dev/null +++ b/apps/backend/src/notifications/notifications.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuditModule } from '../audit/audit.module'; +import { UserEntity } from '../users/entities/user.entity'; +import { UsersRepository } from '../users/repositories/users.repository'; +import { NotificationEntity } from './entities/notification.entity'; +import { NotificationsController } from './notifications.controller'; +import { NotificationsService } from './notifications.service'; +import { NotificationsRepository } from './repositories/notifications.repository'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([NotificationEntity, UserEntity]), + AuditModule, + ], + controllers: [NotificationsController], + providers: [NotificationsRepository, NotificationsService, UsersRepository], + exports: [NotificationsService], +}) +export class NotificationsModule {} diff --git a/apps/backend/src/notifications/notifications.service.ts b/apps/backend/src/notifications/notifications.service.ts new file mode 100644 index 0000000..86bfb0c --- /dev/null +++ b/apps/backend/src/notifications/notifications.service.ts @@ -0,0 +1,287 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource, EntityManager } from 'typeorm'; +import { AuditService } from '../audit/audit.service'; +import { AuditAction } from '../audit/entities/audit-log.entity'; +import { ApiError } from '../common/errors/api-error'; +import { ErrorCode } from '../common/errors/error-codes'; +import { UserEntity } from '../users/entities/user.entity'; +import { UsersRepository } from '../users/repositories/users.repository'; +import { + NotificationEntity, + type NotificationMetadata, +} from './entities/notification.entity'; +import { + type NotificationDto, + type NotificationListQueryDto, + type NotificationPageDto, + type NotificationStatusFilter, +} from './dto/notification.dto'; +import { + isNotificationType, + type NotificationType, +} from './notification-types'; +import { NotificationsRepository } from './repositories/notifications.repository'; + +export interface CreateNotificationInput { + userId: string; + type: string; + title: string; + message: string; + link?: string | null; + metadata?: NotificationMetadata | null; +} + +const maxBulkCreate = 100; +const maxMetadataBytes = 4096; + +@Injectable() +export class NotificationsService { + private readonly logger = new Logger(NotificationsService.name); + + constructor( + private readonly notifications: NotificationsRepository, + private readonly users: UsersRepository, + private readonly audit: AuditService, + @InjectDataSource() private readonly dataSource: DataSource, + ) {} + + async createForUser( + input: CreateNotificationInput, + manager?: EntityManager, + ): Promise { + const user = await this.findActiveUser(input.userId, manager); + const notification = this.buildNotification(user.id, input); + return this.notifications.save(notification, manager); + } + + async createForUsers( + userIds: string[], + input: Omit, + ): Promise { + const uniqueUserIds = Array.from(new Set(userIds)); + if (uniqueUserIds.length > maxBulkCreate) { + this.logger.warn( + { count: uniqueUserIds.length }, + 'Large notification bulk create rejected', + ); + throw new ApiError( + ErrorCode.ValidationFailed, + 'Zu viele Zielbenutzer fuer eine Benachrichtigung.', + 400, + ); + } + return this.dataSource.transaction(async (manager) => { + const notifications: NotificationEntity[] = []; + for (const userId of uniqueUserIds) { + const user = await this.findActiveUser(userId, manager); + notifications.push( + this.buildNotification(user.id, { ...input, userId }), + ); + } + return this.notifications.saveMany(notifications, manager); + }); + } + + async getForCurrentUser( + userId: string, + query: NotificationListQueryDto, + ): Promise { + const page = query.page; + const pageSize = Math.min(query.pageSize, 100); + const status: NotificationStatusFilter = query.status ?? 'all'; + const [items, total] = await this.notifications.listForUser( + userId, + status, + page, + pageSize, + ); + const unreadCount = await this.getUnreadCount(userId); + return { + items: items.map((item) => this.toDto(item)), + total, + page, + pageSize, + unreadCount, + }; + } + + async getUnreadCount(userId: string): Promise { + return this.notifications.countUnreadForUser(userId); + } + + async markAsRead(userId: string, id: string): Promise { + const notification = await this.getOwnedNotification(userId, id); + notification.readAt ??= new Date(); + return this.toDto(await this.notifications.save(notification)); + } + + async markAsUnread(userId: string, id: string): Promise { + const notification = await this.getOwnedNotification(userId, id); + notification.readAt = null; + return this.toDto(await this.notifications.save(notification)); + } + + async markAllAsRead(userId: string): Promise<{ updated: number }> { + return { updated: await this.notifications.markAllAsRead(userId) }; + } + + async softDelete(userId: string, id: string): Promise { + const notification = await this.getOwnedNotification(userId, id); + await this.notifications.softDelete(notification); + } + + async createAdminNotification( + actorUserId: string, + input: CreateNotificationInput, + ): Promise { + const notification = await this.createForUser(input); + await this.audit.record( + actorUserId, + AuditAction.NotificationCreated, + 'notification', + notification.id, + { + targetUserId: input.userId, + notificationId: notification.id, + type: notification.type, + }, + ); + return this.toDto(notification); + } + + private async getOwnedNotification( + userId: string, + id: string, + ): Promise { + const notification = await this.notifications.findActiveForUser(id, userId); + if (!notification) { + throw new ApiError( + ErrorCode.NotificationNotFound, + 'Die Benachrichtigung wurde nicht gefunden.', + 404, + ); + } + return notification; + } + + private async findActiveUser( + userId: string, + manager?: EntityManager, + ): Promise { + const user = await this.users.findById(userId, manager); + if (!user || !user.active) { + throw new ApiError( + ErrorCode.UserNotFound, + 'Der Zielbenutzer wurde nicht gefunden.', + 404, + ); + } + return user; + } + + private buildNotification( + userId: string, + input: CreateNotificationInput, + ): NotificationEntity { + const type = this.normalizeType(input.type); + const title = this.normalizePlainText(input.title, 150); + const message = this.normalizePlainText(input.message, 1000); + const link = this.normalizeLink(input.link ?? null); + const metadata = this.normalizeMetadata(input.metadata ?? null); + + const notification = new NotificationEntity(); + notification.userId = userId; + notification.type = type; + notification.title = title; + notification.message = message; + notification.link = link; + notification.metadata = metadata; + notification.readAt = null; + return notification; + } + + private normalizeType(value: string): NotificationType { + if (!isNotificationType(value)) { + throw new ApiError( + ErrorCode.NotificationTypeInvalid, + 'Der Benachrichtigungstyp ist ungueltig.', + 400, + ); + } + return value; + } + + private normalizePlainText(value: string, maxLength: number): string { + const trimmed = value.trim(); + if ( + trimmed.length < 1 || + trimmed.length > maxLength || + /<[^>]*>|[<>]/.test(trimmed) + ) { + throw new ApiError( + ErrorCode.ValidationFailed, + 'Benachrichtigungen duerfen nur Plain Text enthalten.', + 400, + ); + } + return trimmed; + } + + private normalizeLink(value: string | null): string | null { + if (!value) { + return null; + } + const trimmed = value.trim(); + const lower = trimmed.toLowerCase(); + if ( + trimmed.length > 500 || + !trimmed.startsWith('/') || + trimmed.startsWith('//') || + trimmed.includes('\\') || + lower.includes('javascript:') || + /^[a-z][a-z0-9+.-]*:/i.test(trimmed) + ) { + throw new ApiError( + ErrorCode.NotificationLinkInvalid, + 'Der Link muss eine interne Route sein.', + 400, + ); + } + return trimmed; + } + + private normalizeMetadata( + value: NotificationMetadata | null, + ): NotificationMetadata | null { + if (!value) { + return null; + } + const serialized = JSON.stringify(value); + if ( + !serialized || + Buffer.byteLength(serialized, 'utf8') > maxMetadataBytes + ) { + throw new ApiError( + ErrorCode.NotificationMetadataTooLarge, + 'Die Metadaten sind zu gross.', + 400, + ); + } + return value; + } + + private toDto(notification: NotificationEntity): NotificationDto { + return { + id: notification.id, + type: notification.type, + title: notification.title, + message: notification.message, + link: notification.link, + metadata: notification.metadata, + read: notification.readAt !== null, + readAt: notification.readAt?.toISOString() ?? null, + createdAt: notification.createdAt.toISOString(), + }; + } +} diff --git a/apps/backend/src/notifications/repositories/notifications.repository.ts b/apps/backend/src/notifications/repositories/notifications.repository.ts new file mode 100644 index 0000000..0af483d --- /dev/null +++ b/apps/backend/src/notifications/repositories/notifications.repository.ts @@ -0,0 +1,79 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { EntityManager, IsNull, Repository } from 'typeorm'; +import { NotificationEntity } from '../entities/notification.entity'; +import type { NotificationStatusFilter } from '../dto/notification.dto'; + +@Injectable() +export class NotificationsRepository { + constructor( + @InjectRepository(NotificationEntity) + private readonly repo: Repository, + ) {} + + listForUser( + userId: string, + status: NotificationStatusFilter, + page: number, + pageSize: number, + ): Promise<[NotificationEntity[], number]> { + const qb = this.repo + .createQueryBuilder('notification') + .where('notification.userId = :userId', { userId }) + .andWhere('notification.deletedAt IS NULL'); + if (status === 'read') { + qb.andWhere('notification.readAt IS NOT NULL'); + } + if (status === 'unread') { + qb.andWhere('notification.readAt IS NULL'); + } + return qb + .orderBy('notification.createdAt', 'DESC') + .skip((page - 1) * pageSize) + .take(pageSize) + .getManyAndCount(); + } + + countUnreadForUser(userId: string): Promise { + return this.repo.count({ + where: { userId, readAt: IsNull(), deletedAt: IsNull() }, + }); + } + + findActiveForUser( + id: string, + userId: string, + ): Promise { + return this.repo.findOne({ where: { id, userId, deletedAt: IsNull() } }); + } + + save( + notification: NotificationEntity, + manager?: EntityManager, + ): Promise { + return (manager?.getRepository(NotificationEntity) ?? this.repo).save( + notification, + ); + } + + saveMany( + notifications: NotificationEntity[], + manager?: EntityManager, + ): Promise { + return (manager?.getRepository(NotificationEntity) ?? this.repo).save( + notifications, + ); + } + + async markAllAsRead(userId: string, readAt = new Date()): Promise { + const result = await this.repo.update( + { userId, readAt: IsNull(), deletedAt: IsNull() }, + { readAt }, + ); + return result.affected ?? 0; + } + + async softDelete(notification: NotificationEntity): Promise { + await this.repo.softRemove(notification); + } +} diff --git a/apps/backend/src/notifications/tests/notification-integrations.spec.ts b/apps/backend/src/notifications/tests/notification-integrations.spec.ts new file mode 100644 index 0000000..95cedd1 --- /dev/null +++ b/apps/backend/src/notifications/tests/notification-integrations.spec.ts @@ -0,0 +1,124 @@ +import { describe, expect, it } from 'vitest'; +import type { AuditService } from '../../audit/audit.service'; +import type { AuthenticatedUser } from '../../auth/authenticated-request'; +import { ItemStatus } from '../../items/entities/item.entity'; +import { ItemsService } from '../../items/items.service'; +import { NotificationType } from '../notification-types'; +import { UsersService } from '../../users/users.service'; +import type { ItemsRepository } from '../../items/repositories/items.repository'; +import type { NotificationsService } from '../notifications.service'; +import type { RolesService } from '../../roles/roles.service'; +import type { SessionsService } from '../../sessions/sessions.service'; +import type { UsersRepository } from '../../users/repositories/users.repository'; +import type { DataSource, EntityManager } from 'typeorm'; + +describe('Notification integrations', () => { + it('creates an item.created notification for the first active admin', async () => { + const created: unknown[] = []; + const items: Pick = { + save: (item) => { + item.id = 'item-1'; + item.createdAt = new Date(); + item.updatedAt = new Date(); + item.deletedAt = null; + return Promise.resolve(item); + }, + }; + const notifications: Pick = { + createForUser: (input) => { + created.push(input); + return Promise.resolve( + {} as Awaited>, + ); + }, + }; + const users: Pick = { + findFirstActiveAdmin: () => + Promise.resolve({ id: 'admin-1' } as Awaited< + ReturnType + >), + }; + const service = new ItemsService( + items as ItemsRepository, + notifications as NotificationsService, + users as UsersRepository, + ); + + await service.create( + { name: 'Beispiel', status: ItemStatus.Active }, + 'user-1', + ); + + expect(created).toEqual([ + expect.objectContaining({ + userId: 'admin-1', + type: NotificationType.ItemCreated, + link: '/items/item-1', + }), + ]); + }); + + it('creates a user.role-changed notification after role changes', async () => { + const created: unknown[] = []; + const user = { + id: 'user-1', + active: true, + roles: [{ id: 'role-user', name: 'user' }], + }; + const users: Pick = { + findByIdWithRoles: () => + Promise.resolve( + user as Awaited>, + ), + save: (entry) => Promise.resolve(entry), + }; + const roles: Pick = { + getRole: (id) => + Promise.resolve({ + id, + name: id === 'role-editor' ? 'Editor' : 'user', + } as Awaited>), + }; + const notifications: Pick = { + createForUser: (input) => { + created.push(input); + return Promise.resolve( + {} as Awaited>, + ); + }, + }; + const manager = { + query: () => Promise.resolve(), + } as unknown as EntityManager; + const dataSource = { + transaction: (action: (manager: EntityManager) => Promise) => + action(manager), + } as DataSource; + const service = new UsersService( + users as UsersRepository, + roles as RolesService, + {} as SessionsService, + { record: () => Promise.resolve() } as unknown as AuditService, + notifications as NotificationsService, + dataSource, + ); + + await service.setRoles( + { + id: 'admin-1', + sessionId: 'session-1', + permissions: [], + } satisfies AuthenticatedUser, + 'user-1', + ['role-user', 'role-editor'], + ); + + expect(created).toEqual([ + expect.objectContaining({ + userId: 'user-1', + type: NotificationType.UserRoleChanged, + message: 'Ihnen wurde die Rolle "Editor" zugewiesen.', + }), + ]); + }); +}); diff --git a/apps/backend/src/notifications/tests/notifications.service.spec.ts b/apps/backend/src/notifications/tests/notifications.service.spec.ts new file mode 100644 index 0000000..4243205 --- /dev/null +++ b/apps/backend/src/notifications/tests/notifications.service.spec.ts @@ -0,0 +1,271 @@ +import { describe, expect, it } from 'vitest'; +import type { AuditService } from '../../audit/audit.service'; +import { PermissionsGuard } from '../../auth/guards/permissions.guard'; +import { ErrorCode } from '../../common/errors/error-codes'; +import { NotificationEntity } from '../entities/notification.entity'; +import { NotificationType } from '../notification-types'; +import { NotificationsController } from '../notifications.controller'; +import { NotificationsService } from '../notifications.service'; +import type { AppConfigService } from '../../config/config.service'; +import type { SessionsService } from '../../sessions/sessions.service'; +import type { UsersRepository } from '../../users/repositories/users.repository'; +import type { NotificationsRepository } from '../repositories/notifications.repository'; +import type { DataSource } from 'typeorm'; +import { Reflector } from '@nestjs/core'; +import type { ExecutionContext } from '@nestjs/common'; + +const now = new Date('2026-07-16T08:00:00.000Z'); + +function notification( + id: string, + userId: string, + readAt: Date | null = null, +): NotificationEntity { + const entity = new NotificationEntity(); + entity.id = id; + entity.userId = userId; + entity.type = NotificationType.System; + entity.title = 'Titel'; + entity.message = 'Nachricht'; + entity.link = '/'; + entity.metadata = null; + entity.readAt = readAt; + entity.createdAt = now; + entity.deletedAt = null; + return entity; +} + +function serviceWithStore(store: NotificationEntity[]) { + const repo: Pick< + NotificationsRepository, + | 'listForUser' + | 'countUnreadForUser' + | 'findActiveForUser' + | 'save' + | 'saveMany' + | 'markAllAsRead' + | 'softDelete' + > = { + listForUser: (userId, status, page, pageSize) => { + const filtered = store.filter( + (entry) => + entry.userId === userId && + entry.deletedAt === null && + (status === 'all' || + (status === 'read' && entry.readAt !== null) || + (status === 'unread' && entry.readAt === null)), + ); + return Promise.resolve([ + filtered.slice((page - 1) * pageSize, page * pageSize), + filtered.length, + ]); + }, + countUnreadForUser: (userId) => + Promise.resolve( + store.filter( + (entry) => + entry.userId === userId && + entry.readAt === null && + entry.deletedAt === null, + ).length, + ), + findActiveForUser: (id, userId) => + Promise.resolve( + store.find( + (entry) => + entry.id === id && + entry.userId === userId && + entry.deletedAt === null, + ) ?? null, + ), + save: (entry) => { + entry.createdAt ??= now; + const index = store.findIndex((candidate) => candidate.id === entry.id); + if (index >= 0) { + store[index] = entry; + } else { + entry.id = entry.id || `notification-${store.length + 1}`; + store.push(entry); + } + return Promise.resolve(entry); + }, + saveMany: (entries) => Promise.resolve(entries), + markAllAsRead: (userId) => { + let updated = 0; + for (const entry of store) { + if ( + entry.userId === userId && + entry.readAt === null && + entry.deletedAt === null + ) { + entry.readAt = now; + updated += 1; + } + } + return Promise.resolve(updated); + }, + softDelete: (entry) => { + entry.deletedAt = now; + return Promise.resolve(); + }, + }; + const users: Pick = { + findById: (id) => + Promise.resolve({ + id, + active: id !== 'disabled-user', + } as Awaited>), + }; + const dataSource = { + transaction: async (callback: (manager: never) => Promise) => + callback(undefined as never), + } as unknown as DataSource; + const service = new NotificationsService( + repo as NotificationsRepository, + users as UsersRepository, + {} as AuditService, + dataSource, + ); + return { service, store }; +} + +describe('NotificationsService', () => { + it('returns only current user notifications with pagination and status filter', async () => { + const { service } = serviceWithStore([ + notification('own-unread', 'user-1'), + notification('own-read', 'user-1', now), + notification('other', 'user-2'), + ]); + + const result = await service.getForCurrentUser('user-1', { + page: 1, + pageSize: 10, + status: 'unread', + }); + + expect(result.items.map((entry) => entry.id)).toEqual(['own-unread']); + expect(result.total).toBe(1); + expect(result.unreadCount).toBe(1); + }); + + it('does not reveal or mutate foreign notification ids', async () => { + const { service } = serviceWithStore([notification('foreign', 'user-2')]); + + await expect(service.markAsRead('user-1', 'foreign')).rejects.toMatchObject( + { + code: ErrorCode.NotificationNotFound, + status: 404, + }, + ); + }); + + it('marks read and unread idempotently', async () => { + const { service } = serviceWithStore([notification('own', 'user-1')]); + + await service.markAsRead('user-1', 'own'); + const readAgain = await service.markAsRead('user-1', 'own'); + expect(readAgain.read).toBe(true); + + await service.markAsUnread('user-1', 'own'); + const unreadAgain = await service.markAsUnread('user-1', 'own'); + expect(unreadAgain.read).toBe(false); + }); + + it('marks all unread notifications only for the current user', async () => { + const { service, store } = serviceWithStore([ + notification('own', 'user-1'), + notification('other', 'user-2'), + ]); + + await expect(service.markAllAsRead('user-1')).resolves.toEqual({ + updated: 1, + }); + expect(store.find((entry) => entry.id === 'own')?.readAt).toBe(now); + expect(store.find((entry) => entry.id === 'other')?.readAt).toBeNull(); + }); + + it('soft deletes notifications from normal queries', async () => { + const { service } = serviceWithStore([notification('own', 'user-1')]); + + await service.softDelete('user-1', 'own'); + const result = await service.getForCurrentUser('user-1', { + page: 1, + pageSize: 20, + status: 'all', + }); + + expect(result.items).toEqual([]); + }); + + it('rejects unknown types and unsafe links', async () => { + const { service } = serviceWithStore([]); + + await expect( + service.createForUser({ + userId: 'user-1', + type: 'unknown', + title: 'Titel', + message: 'Nachricht', + }), + ).rejects.toMatchObject({ code: ErrorCode.NotificationTypeInvalid }); + + await expect( + service.createForUser({ + userId: 'user-1', + type: NotificationType.System, + title: 'Titel', + message: 'Nachricht', + link: 'https://example.com', + }), + ).rejects.toMatchObject({ code: ErrorCode.NotificationLinkInvalid }); + }); + + it('rejects disabled target users', async () => { + const { service } = serviceWithStore([]); + + await expect( + service.createForUser({ + userId: 'disabled-user', + type: NotificationType.System, + title: 'Titel', + message: 'Nachricht', + }), + ).rejects.toMatchObject({ code: ErrorCode.UserNotFound }); + }); + + it('requires notifications.manage for administrative creation', async () => { + const controller = new NotificationsController({} as NotificationsService); + const reflector = new Reflector(); + const sessions: Pick = { + resolveSession: () => + Promise.resolve({ + user: { id: 'user-1' }, + permissions: [], + } as unknown as Awaited>), + }; + const config = { + session: { cookieName: 'app_session' }, + } as AppConfigService; + const guard = new PermissionsGuard( + reflector, + sessions as SessionsService, + config, + ); + const context = { + getHandler: () => controller.createAdmin, + getClass: () => NotificationsController, + switchToHttp: () => ({ + getRequest: () => ({ + signedCookies: { app_session: 'session-id' }, + ip: '127.0.0.1', + get: () => 'test-agent', + }), + }), + } as unknown as ExecutionContext; + + await expect(guard.canActivate(context)).rejects.toMatchObject({ + code: ErrorCode.PermissionDenied, + status: 403, + }); + }); +}); diff --git a/apps/backend/src/roles/admin-roles.controller.ts b/apps/backend/src/roles/admin-roles.controller.ts new file mode 100644 index 0000000..74bb0ba --- /dev/null +++ b/apps/backend/src/roles/admin-roles.controller.ts @@ -0,0 +1,71 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + Req, +} from '@nestjs/common'; +import type { AuthenticatedRequest } from '../auth/authenticated-request'; +import { RequirePermissions } from '../auth/guards/require-permissions.decorator'; +import { ApiError } from '../common/errors/api-error'; +import { ErrorCode } from '../common/errors/error-codes'; +import { SensitiveRateLimit } from '../common/rate-limit/sensitive-rate-limit.decorator'; +import { Permission } from './permissions'; +import { CreateRoleDto, UpdateRoleDto } from './dto/role.dto'; +import { RolesService } from './roles.service'; + +@Controller('admin/roles') +export class AdminRolesController { + constructor(private readonly roles: RolesService) {} + + @Get() + @RequirePermissions(Permission.RolesRead) + list() { + return this.roles.adminList(); + } + + @Get(':id') + @RequirePermissions(Permission.RolesRead) + get(@Param('id') id: string) { + return this.roles.adminGet(id); + } + + @Post() + @RequirePermissions(Permission.RolesManage) + @SensitiveRateLimit() + create(@Req() req: AuthenticatedRequest, @Body() dto: CreateRoleDto) { + return this.roles.create(dto, this.requireUser(req)); + } + + @Put(':id') + @RequirePermissions(Permission.RolesManage) + @SensitiveRateLimit() + update( + @Req() req: AuthenticatedRequest, + @Param('id') id: string, + @Body() dto: UpdateRoleDto, + ) { + return this.roles.update(id, dto, this.requireUser(req)); + } + + @Delete(':id') + @RequirePermissions(Permission.RolesManage) + @SensitiveRateLimit() + delete(@Req() req: AuthenticatedRequest, @Param('id') id: string) { + return this.roles.delete(id, this.requireUser(req)); + } + + private requireUser(req: AuthenticatedRequest) { + if (!req.user) { + throw new ApiError( + ErrorCode.Unauthorized, + 'Bitte melden Sie sich an.', + 401, + ); + } + return req.user; + } +} diff --git a/apps/backend/src/roles/dto/role.dto.ts b/apps/backend/src/roles/dto/role.dto.ts new file mode 100644 index 0000000..12cf2dd --- /dev/null +++ b/apps/backend/src/roles/dto/role.dto.ts @@ -0,0 +1,19 @@ +import { IsArray, IsEnum, IsOptional, IsString, Length } from 'class-validator'; +import { Permission } from '../permissions'; + +export class CreateRoleDto { + @IsString() + @Length(2, 80) + name!: string; + + @IsOptional() + @IsString() + @Length(0, 255) + description = ''; + + @IsArray() + @IsEnum(Permission, { each: true }) + permissions!: Permission[]; +} + +export class UpdateRoleDto extends CreateRoleDto {} diff --git a/apps/backend/src/roles/entities/permission.entity.ts b/apps/backend/src/roles/entities/permission.entity.ts new file mode 100644 index 0000000..da78e9f --- /dev/null +++ b/apps/backend/src/roles/entities/permission.entity.ts @@ -0,0 +1,11 @@ +import { Column, Entity, PrimaryColumn } from 'typeorm'; +import { Permission } from '../permissions'; + +@Entity('permissions') +export class PermissionEntity { + @PrimaryColumn({ type: 'varchar', length: 80 }) + id!: Permission; + + @Column({ type: 'varchar', length: 160 }) + description!: string; +} diff --git a/apps/backend/src/roles/entities/role.entity.ts b/apps/backend/src/roles/entities/role.entity.ts new file mode 100644 index 0000000..19c37c1 --- /dev/null +++ b/apps/backend/src/roles/entities/role.entity.ts @@ -0,0 +1,45 @@ +import { + Column, + CreateDateColumn, + Entity, + JoinTable, + ManyToMany, + PrimaryGeneratedColumn, + Unique, + UpdateDateColumn, +} from 'typeorm'; +import { PermissionEntity } from './permission.entity'; +import { UserEntity } from '../../users/entities/user.entity'; + +@Entity('roles') +@Unique('uq_roles_name', ['name']) +export class RoleEntity { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column({ type: 'varchar', length: 80 }) + name!: string; + + @Column({ type: 'varchar', length: 255, default: '' }) + description!: string; + + @Column({ type: 'boolean', default: false }) + protected!: boolean; + + @ManyToMany(() => PermissionEntity, { eager: true }) + @JoinTable({ + name: 'role_permissions', + joinColumn: { name: 'role_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'permission_id', referencedColumnName: 'id' }, + }) + permissions!: PermissionEntity[]; + + @ManyToMany(() => UserEntity, (user) => user.roles) + users!: UserEntity[]; + + @CreateDateColumn({ name: 'created_at', type: 'datetime', precision: 3 }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'datetime', precision: 3 }) + updatedAt!: Date; +} diff --git a/apps/backend/src/roles/permissions.ts b/apps/backend/src/roles/permissions.ts new file mode 100644 index 0000000..4ae1f31 --- /dev/null +++ b/apps/backend/src/roles/permissions.ts @@ -0,0 +1,29 @@ +export enum Permission { + ItemsRead = 'items.read', + ItemsCreate = 'items.create', + ItemsUpdate = 'items.update', + ItemsDelete = 'items.delete', + UsersRead = 'users.read', + UsersManage = 'users.manage', + RolesRead = 'roles.read', + RolesManage = 'roles.manage', + AuditRead = 'audit.read', + SessionsReadOwn = 'sessions.readOwn', + SessionsRevokeOwn = 'sessions.revokeOwn', + SessionsManage = 'sessions.manage', + NotificationsReadOwn = 'notifications.readOwn', + NotificationsUpdateOwn = 'notifications.updateOwn', + NotificationsManage = 'notifications.manage', +} + +export const allPermissions = Object.values(Permission); + +export const administrativePermissions = [ + Permission.UsersRead, + Permission.UsersManage, + Permission.RolesRead, + Permission.RolesManage, + Permission.AuditRead, + Permission.SessionsManage, + Permission.NotificationsManage, +] as const; diff --git a/apps/backend/src/roles/repositories/roles.repository.ts b/apps/backend/src/roles/repositories/roles.repository.ts new file mode 100644 index 0000000..728be5d --- /dev/null +++ b/apps/backend/src/roles/repositories/roles.repository.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { EntityManager, Repository } from 'typeorm'; +import { RoleEntity } from '../entities/role.entity'; + +@Injectable() +export class RolesRepository { + constructor( + @InjectRepository(RoleEntity) private readonly repo: Repository, + ) {} + + findByName( + name: string, + manager?: EntityManager, + ): Promise { + return (manager?.getRepository(RoleEntity) ?? this.repo).findOne({ + where: { name }, + }); + } + + findById(id: string, manager?: EntityManager): Promise { + return (manager?.getRepository(RoleEntity) ?? this.repo).findOne({ + where: { id }, + relations: { users: true, permissions: true }, + }); + } + + list(): Promise { + return this.repo.find({ + order: { name: 'ASC' }, + relations: { users: true }, + }); + } + + save(role: RoleEntity, manager?: EntityManager): Promise { + return (manager?.getRepository(RoleEntity) ?? this.repo).save(role); + } + + remove(role: RoleEntity, manager?: EntityManager): Promise { + return (manager?.getRepository(RoleEntity) ?? this.repo).remove(role); + } +} diff --git a/apps/backend/src/roles/roles.controller.ts b/apps/backend/src/roles/roles.controller.ts new file mode 100644 index 0000000..ffd1362 --- /dev/null +++ b/apps/backend/src/roles/roles.controller.ts @@ -0,0 +1,46 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, +} from '@nestjs/common'; +import { RequirePermissions } from '../auth/guards/require-permissions.decorator'; +import { SensitiveRateLimit } from '../common/rate-limit/sensitive-rate-limit.decorator'; +import { Permission } from './permissions'; +import { CreateRoleDto, UpdateRoleDto } from './dto/role.dto'; +import { RolesService } from './roles.service'; + +@Controller('roles') +export class RolesController { + constructor(private readonly roles: RolesService) {} + + @Get() + @RequirePermissions(Permission.RolesRead) + list() { + return this.roles.list(); + } + + @Post() + @RequirePermissions(Permission.RolesManage) + @SensitiveRateLimit() + create(@Body() dto: CreateRoleDto) { + return this.roles.create(dto); + } + + @Put(':id') + @RequirePermissions(Permission.RolesManage) + @SensitiveRateLimit() + update(@Param('id') id: string, @Body() dto: UpdateRoleDto) { + return this.roles.update(id, dto); + } + + @Delete(':id') + @RequirePermissions(Permission.RolesManage) + @SensitiveRateLimit() + delete(@Param('id') id: string) { + return this.roles.delete(id); + } +} diff --git a/apps/backend/src/roles/roles.module.ts b/apps/backend/src/roles/roles.module.ts new file mode 100644 index 0000000..768e362 --- /dev/null +++ b/apps/backend/src/roles/roles.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuditModule } from '../audit/audit.module'; +import { PermissionEntity } from './entities/permission.entity'; +import { RoleEntity } from './entities/role.entity'; +import { AdminRolesController } from './admin-roles.controller'; +import { RolesController } from './roles.controller'; +import { RolesRepository } from './repositories/roles.repository'; +import { RolesService } from './roles.service'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([RoleEntity, PermissionEntity]), + AuditModule, + ], + controllers: [RolesController, AdminRolesController], + providers: [RolesRepository, RolesService], + exports: [RolesRepository, RolesService], +}) +export class RolesModule {} diff --git a/apps/backend/src/roles/roles.service.ts b/apps/backend/src/roles/roles.service.ts new file mode 100644 index 0000000..0c65597 --- /dev/null +++ b/apps/backend/src/roles/roles.service.ts @@ -0,0 +1,311 @@ +import { Injectable } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource, EntityManager } from 'typeorm'; +import { AuditService } from '../audit/audit.service'; +import { AuditAction } from '../audit/entities/audit-log.entity'; +import type { AuthenticatedUser } from '../auth/authenticated-request'; +import { ApiError } from '../common/errors/api-error'; +import { ErrorCode } from '../common/errors/error-codes'; +import { PermissionEntity } from './entities/permission.entity'; +import { RoleEntity } from './entities/role.entity'; +import { allPermissions, Permission } from './permissions'; +import type { CreateRoleDto, UpdateRoleDto } from './dto/role.dto'; +import { RolesRepository } from './repositories/roles.repository'; + +const adminRoleName = 'admin'; +const userRoleName = 'user'; + +@Injectable() +export class RolesService { + constructor( + private readonly roles: RolesRepository, + private readonly audit: AuditService, + @InjectDataSource() private readonly dataSource: DataSource, + ) {} + + list(): Promise { + return this.roles.list(); + } + + async adminList() { + const roles = await this.roles.list(); + return roles.map((role) => this.toAdminDto(role)); + } + + async adminGet(id: string) { + return this.toAdminDto(await this.getRole(id)); + } + + async create( + dto: CreateRoleDto, + actor?: AuthenticatedUser, + ): Promise { + const name = this.normalizeName(dto.name); + await this.assertRoleNameAvailable(name); + const role = new RoleEntity(); + role.name = name; + role.description = dto.description?.trim() ?? ''; + role.protected = false; + role.permissions = await this.loadPermissionEntities(dto.permissions); + const saved = await this.roles.save(role); + if (actor) { + await this.audit.record( + actor.id, + AuditAction.RoleCreated, + 'role', + saved.id, + { + roleName: saved.name, + }, + ); + } + return saved; + } + + async update( + id: string, + dto: UpdateRoleDto, + actor?: AuthenticatedUser, + ): Promise { + return this.dataSource.transaction(async (manager) => { + await manager.query( + "SELECT GET_LOCK('business_app_admin_integrity', 10)", + ); + try { + const role = await this.getRole(id, manager); + const normalizedName = this.normalizeName(dto.name); + if (!role.protected && normalizedName !== role.name) { + await this.assertRoleNameAvailable(normalizedName, id, manager); + role.name = normalizedName; + } + if (role.name === adminRoleName) { + this.assertAdminPermissions(dto.permissions); + } + role.description = dto.description?.trim() ?? ''; + role.permissions = await this.loadPermissionEntities( + role.name === adminRoleName ? allPermissions : dto.permissions, + manager, + ); + const saved = await this.roles.save(role, manager); + if (actor) { + await this.audit.record( + actor.id, + AuditAction.RoleUpdated, + 'role', + saved.id, + { + roleName: saved.name, + }, + ); + await this.audit.record( + actor.id, + AuditAction.RolePermissionsUpdated, + 'role', + saved.id, + { + permissions: saved.permissions + .map((permission) => permission.id) + .join(','), + }, + ); + } + return saved; + } finally { + await manager.query( + "SELECT RELEASE_LOCK('business_app_admin_integrity')", + ); + } + }); + } + + async delete(id: string, actor?: AuthenticatedUser): Promise { + await this.dataSource.transaction(async (manager) => { + await manager.query( + "SELECT GET_LOCK('business_app_admin_integrity', 10)", + ); + try { + const role = await this.getRole(id, manager); + if (role.protected) { + throw new ApiError( + ErrorCode.SystemRoleProtected, + 'Systemrollen koennen nicht geloescht werden.', + 409, + ); + } + if (role.users.length > 0) { + throw new ApiError( + ErrorCode.RoleStillAssigned, + 'Die Rolle ist noch Benutzern zugewiesen.', + 409, + ); + } + await this.roles.remove(role, manager); + if (actor) { + await this.audit.record( + actor.id, + AuditAction.RoleDeleted, + 'role', + id, + { + roleName: role.name, + }, + ); + } + } finally { + await manager.query( + "SELECT RELEASE_LOCK('business_app_admin_integrity')", + ); + } + }); + } + + async getEffectivePermissions(userId: string): Promise { + const rows = await this.dataSource.query<{ permission: Permission }[]>( + ` + SELECT DISTINCT rp.permission_id AS permission + FROM user_roles ur + INNER JOIN role_permissions rp ON rp.role_id = ur.role_id + WHERE ur.user_id = ? + `, + [userId], + ); + return rows.map((row) => row.permission); + } + + async ensureSystemRoles( + manager?: EntityManager, + ): Promise<{ admin: RoleEntity; user: RoleEntity }> { + await this.syncPermissions(manager); + const admin = await this.ensureRole( + adminRoleName, + allPermissions, + true, + manager, + ); + const user = await this.ensureRole( + userRoleName, + [ + Permission.ItemsRead, + Permission.SessionsReadOwn, + Permission.NotificationsReadOwn, + Permission.NotificationsUpdateOwn, + ], + true, + manager, + ); + return { admin, user }; + } + + async syncPermissions(manager?: EntityManager): Promise { + const repo = (manager ?? this.dataSource.manager).getRepository( + PermissionEntity, + ); + await repo.save( + allPermissions.map((permission) => ({ + id: permission, + description: permission, + })), + ); + } + + async getRole(id: string, manager?: EntityManager): Promise { + const role = await this.roles.findById(id, manager); + if (!role) { + throw new ApiError( + ErrorCode.RoleNotFound, + 'Die Rolle wurde nicht gefunden.', + 404, + ); + } + return role; + } + + private async ensureRole( + name: string, + permissions: Permission[], + protectedRole: boolean, + manager?: EntityManager, + ): Promise { + const repo = (manager ?? this.dataSource.manager).getRepository(RoleEntity); + let role = await repo.findOne({ + where: { name }, + relations: { permissions: true }, + }); + if (!role) { + role = new RoleEntity(); + role.name = name; + role.protected = protectedRole; + } + role.permissions = await this.loadPermissionEntities(permissions, manager); + role.protected = protectedRole; + return repo.save(role); + } + + private loadPermissionEntities( + permissions: Permission[], + manager?: EntityManager, + ): Promise { + return Promise.all( + permissions.map(async (permission) => { + const entity = await (manager ?? this.dataSource.manager) + .getRepository(PermissionEntity) + .findOneBy({ id: permission }); + if (!entity) { + throw new ApiError( + ErrorCode.UnknownPermission, + 'Unbekannte Permission.', + 400, + ); + } + return entity; + }), + ); + } + + private normalizeName(name: string): string { + return name.trim().toLowerCase().replace(/\s+/g, '-'); + } + + private async assertRoleNameAvailable( + name: string, + exceptRoleId?: string, + manager?: EntityManager, + ): Promise { + const existing = await this.roles.findByName(name, manager); + if (existing && existing.id !== exceptRoleId) { + throw new ApiError( + ErrorCode.RoleNameAlreadyExists, + 'Der Rollenname ist bereits vergeben.', + 409, + ); + } + } + + private assertAdminPermissions(permissions: Permission[]): void { + const submitted = new Set(permissions); + const missing = allPermissions.filter( + (permission) => !submitted.has(permission), + ); + if (missing.length > 0) { + throw new ApiError( + ErrorCode.LastAdminRequired, + 'Mindestens ein aktiver Administrator muss erhalten bleiben.', + 409, + ); + } + } + + private toAdminDto(role: RoleEntity) { + return { + id: role.id, + name: role.name, + description: role.description, + system: role.protected, + protected: role.protected, + permissions: role.permissions, + userCount: role.users?.length ?? 0, + users: role.users, + createdAt: role.createdAt.toISOString(), + }; + } +} diff --git a/apps/backend/src/sessions/dto/session.dto.ts b/apps/backend/src/sessions/dto/session.dto.ts new file mode 100644 index 0000000..7975936 --- /dev/null +++ b/apps/backend/src/sessions/dto/session.dto.ts @@ -0,0 +1,9 @@ +export interface SessionListItemDto { + id: string; + createdAt: string; + lastActivityAt: string; + userAgent: string | null; + approximateIp: string | null; + current: boolean; + revokedAt: string | null; +} diff --git a/apps/backend/src/sessions/entities/session.entity.ts b/apps/backend/src/sessions/entities/session.entity.ts new file mode 100644 index 0000000..ed0f076 --- /dev/null +++ b/apps/backend/src/sessions/entities/session.entity.ts @@ -0,0 +1,69 @@ +import { + Column, + CreateDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; +import { UserEntity } from '../../users/entities/user.entity'; + +@Entity('sessions') +export class SessionEntity { + @PrimaryColumn({ type: 'char', length: 64 }) + id!: string; + + @ManyToOne(() => UserEntity, (user) => user.sessions, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user!: UserEntity; + + @Index('idx_sessions_user_id') + @Column({ name: 'user_id', type: 'char', length: 36 }) + userId!: string; + + @Column({ name: 'access_token_encrypted', type: 'text' }) + accessTokenEncrypted!: string; + + @Column({ name: 'refresh_token_encrypted', type: 'text', nullable: true }) + refreshTokenEncrypted!: string | null; + + @Column({ name: 'id_token_encrypted', type: 'text', nullable: true }) + idTokenEncrypted!: string | null; + + @Column({ name: 'csrf_token_hash', type: 'char', length: 64 }) + csrfTokenHash!: string; + + @Column({ name: 'access_token_expires_at', type: 'datetime', precision: 3 }) + accessTokenExpiresAt!: Date; + + @Column({ name: 'expires_at', type: 'datetime', precision: 3 }) + expiresAt!: Date; + + @Column({ name: 'absolute_expires_at', type: 'datetime', precision: 3 }) + absoluteExpiresAt!: Date; + + @Column({ name: 'last_activity_at', type: 'datetime', precision: 3 }) + lastActivityAt!: Date; + + @Column({ name: 'user_agent', type: 'varchar', length: 512, nullable: true }) + userAgent!: string | null; + + @Column({ name: 'last_ip', type: 'varchar', length: 80, nullable: true }) + lastIp!: string | null; + + @Column({ + name: 'revoked_at', + type: 'datetime', + precision: 3, + nullable: true, + }) + revokedAt!: Date | null; + + @CreateDateColumn({ name: 'created_at', type: 'datetime', precision: 3 }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'datetime', precision: 3 }) + updatedAt!: Date; +} diff --git a/apps/backend/src/sessions/repositories/sessions.repository.ts b/apps/backend/src/sessions/repositories/sessions.repository.ts new file mode 100644 index 0000000..148b055 --- /dev/null +++ b/apps/backend/src/sessions/repositories/sessions.repository.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { IsNull, LessThan, Repository } from 'typeorm'; +import { SessionEntity } from '../entities/session.entity'; + +@Injectable() +export class SessionsRepository { + constructor( + @InjectRepository(SessionEntity) + private readonly repo: Repository, + ) {} + + findActiveById(id: string): Promise { + return this.repo.findOne({ + where: { id, revokedAt: IsNull() }, + relations: { user: { roles: { permissions: true }, settings: true } }, + }); + } + + listForUser(userId: string): Promise { + return this.repo.find({ where: { userId }, order: { createdAt: 'DESC' } }); + } + + listActiveForUser(userId: string): Promise { + return this.repo.find({ + where: { userId, revokedAt: IsNull() }, + order: { lastActivityAt: 'DESC' }, + }); + } + + countActiveForUser(userId: string): Promise { + return this.repo.count({ where: { userId, revokedAt: IsNull() } }); + } + + save(session: SessionEntity): Promise { + return this.repo.save(session); + } + + async revoke(sessionId: string): Promise { + const result = await this.repo.update( + { id: sessionId, revokedAt: IsNull() }, + { revokedAt: new Date() }, + ); + return result.affected ?? 0; + } + + async revokeForUser(userId: string, sessionId: string): Promise { + const result = await this.repo.update( + { id: sessionId, userId, revokedAt: IsNull() }, + { revokedAt: new Date() }, + ); + return result.affected ?? 0; + } + + async revokeAllForUser( + userId: string, + exceptSessionId?: string, + ): Promise { + const sessions = await this.repo.find({ + where: { userId, revokedAt: IsNull() }, + }); + const now = new Date(); + const targets = sessions.filter( + (session) => session.id !== exceptSessionId, + ); + await this.repo.save( + targets.map((session) => ({ ...session, revokedAt: now })), + ); + return targets.length; + } + + async cleanupExpired(now = new Date()): Promise { + await this.repo.delete([ + { expiresAt: LessThan(now) }, + { absoluteExpiresAt: LessThan(now) }, + ]); + } +} diff --git a/apps/backend/src/sessions/session-crypto.service.ts b/apps/backend/src/sessions/session-crypto.service.ts new file mode 100644 index 0000000..ab5b76f --- /dev/null +++ b/apps/backend/src/sessions/session-crypto.service.ts @@ -0,0 +1,54 @@ +import { + createCipheriv, + createDecipheriv, + createHash, + createHmac, + randomBytes, +} from 'node:crypto'; +import { Injectable } from '@nestjs/common'; +import { AppConfigService } from '../config/config.service'; + +@Injectable() +export class SessionCryptoService { + private readonly encryptionKey: Buffer; + private readonly hmacKey: Buffer; + + constructor(config: AppConfigService) { + this.encryptionKey = createHash('sha256') + .update(config.session.encryptionKey) + .digest(); + this.hmacKey = createHash('sha256').update(config.session.secret).digest(); + } + + encrypt(value: string): string { + const iv = randomBytes(12); + const cipher = createCipheriv('aes-256-gcm', this.encryptionKey, iv); + const encrypted = Buffer.concat([ + cipher.update(value, 'utf8'), + cipher.final(), + ]); + const tag = cipher.getAuthTag(); + return `${iv.toString('base64url')}.${tag.toString('base64url')}.${encrypted.toString('base64url')}`; + } + + decrypt(value: string): string { + const [iv, tag, encrypted] = value.split('.'); + if (!iv || !tag || !encrypted) { + throw new Error('Invalid encrypted payload'); + } + const decipher = createDecipheriv( + 'aes-256-gcm', + this.encryptionKey, + Buffer.from(iv, 'base64url'), + ); + decipher.setAuthTag(Buffer.from(tag, 'base64url')); + return Buffer.concat([ + decipher.update(Buffer.from(encrypted, 'base64url')), + decipher.final(), + ]).toString('utf8'); + } + + hashToken(token: string): string { + return createHmac('sha256', this.hmacKey).update(token).digest('hex'); + } +} diff --git a/apps/backend/src/sessions/sessions.controller.ts b/apps/backend/src/sessions/sessions.controller.ts new file mode 100644 index 0000000..c629cbe --- /dev/null +++ b/apps/backend/src/sessions/sessions.controller.ts @@ -0,0 +1,61 @@ +import { Controller, Delete, Get, Param, Req } from '@nestjs/common'; +import type { AuthenticatedRequest } from '../auth/authenticated-request'; +import { RequirePermissions } from '../auth/guards/require-permissions.decorator'; +import { ApiError } from '../common/errors/api-error'; +import { ErrorCode } from '../common/errors/error-codes'; +import { SensitiveRateLimit } from '../common/rate-limit/sensitive-rate-limit.decorator'; +import { Permission } from '../roles/permissions'; +import { SessionsService } from './sessions.service'; + +@Controller('sessions') +export class SessionsController { + constructor(private readonly sessions: SessionsService) {} + + @Get('own') + @RequirePermissions(Permission.SessionsReadOwn) + listOwn(@Req() req: AuthenticatedRequest) { + const user = this.requireUser(req); + return this.sessions.listOwn(user.id, user.sessionId); + } + + @Delete('own/:id') + @RequirePermissions(Permission.SessionsRevokeOwn) + @SensitiveRateLimit() + revokeOwn(@Req() req: AuthenticatedRequest, @Param('id') id: string) { + const user = this.requireUser(req); + if (user.sessionId === id) { + throw new ApiError( + ErrorCode.Conflict, + 'Die aktuelle Session bitte per Logout beenden.', + 409, + ); + } + return this.sessions.revoke(id); + } + + @Delete('own') + @RequirePermissions(Permission.SessionsRevokeOwn) + @SensitiveRateLimit() + revokeOthers(@Req() req: AuthenticatedRequest) { + const user = this.requireUser(req); + return this.sessions.revokeAllForUser(user.id, user.sessionId); + } + + @Delete('users/:userId') + @RequirePermissions(Permission.SessionsManage) + @SensitiveRateLimit() + revokeAllForUser(@Param('userId') userId: string) { + return this.sessions.revokeAllForUser(userId); + } + + private requireUser(req: AuthenticatedRequest) { + if (!req.user) { + throw new ApiError( + ErrorCode.Unauthorized, + 'Bitte melden Sie sich an.', + 401, + ); + } + return req.user; + } +} diff --git a/apps/backend/src/sessions/sessions.module.ts b/apps/backend/src/sessions/sessions.module.ts new file mode 100644 index 0000000..f226827 --- /dev/null +++ b/apps/backend/src/sessions/sessions.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserEntity } from '../users/entities/user.entity'; +import { SessionEntity } from './entities/session.entity'; +import { SessionsController } from './sessions.controller'; +import { SessionsRepository } from './repositories/sessions.repository'; +import { SessionCryptoService } from './session-crypto.service'; +import { SessionsService } from './sessions.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([SessionEntity, UserEntity])], + controllers: [SessionsController], + providers: [SessionsRepository, SessionCryptoService, SessionsService], + exports: [SessionsService, SessionsRepository, SessionCryptoService], +}) +export class SessionsModule {} diff --git a/apps/backend/src/sessions/sessions.service.ts b/apps/backend/src/sessions/sessions.service.ts new file mode 100644 index 0000000..d84c075 --- /dev/null +++ b/apps/backend/src/sessions/sessions.service.ts @@ -0,0 +1,209 @@ +import { randomBytes } from 'node:crypto'; +import { Injectable } from '@nestjs/common'; +import { AppConfigService } from '../config/config.service'; +import { ApiError } from '../common/errors/api-error'; +import { ErrorCode } from '../common/errors/error-codes'; +import { Permission } from '../roles/permissions'; +import { UserEntity } from '../users/entities/user.entity'; +import { SessionEntity } from './entities/session.entity'; +import { SessionsRepository } from './repositories/sessions.repository'; +import { SessionCryptoService } from './session-crypto.service'; +import type { SessionListItemDto } from './dto/session.dto'; + +export interface ResolvedSession { + user: UserEntity; + permissions: Permission[]; +} + +export interface AdminSessionDto { + id: string; + publicId: string; + createdAt: string; + lastActivityAt: string; + expiresAt: string; + absoluteExpiresAt: string; + userAgent: string | null; + approximateIp: string | null; + current: boolean; + revokedAt: string | null; +} + +@Injectable() +export class SessionsService { + constructor( + private readonly sessions: SessionsRepository, + private readonly crypto: SessionCryptoService, + private readonly config: AppConfigService, + ) {} + + async createSession( + user: UserEntity, + tokens: { + accessToken: string; + refreshToken?: string; + idToken?: string; + accessTokenExpiresAt: Date; + }, + userAgent: string | undefined, + ip: string | undefined, + ): Promise<{ session: SessionEntity; csrfToken: string }> { + await this.sessions.cleanupExpired(); + const now = new Date(); + const csrfToken = randomBytes(32).toString('base64url'); + const session = new SessionEntity(); + session.id = randomBytes(32).toString('hex'); + session.user = user; + session.userId = user.id; + session.accessTokenEncrypted = this.crypto.encrypt(tokens.accessToken); + session.refreshTokenEncrypted = tokens.refreshToken + ? this.crypto.encrypt(tokens.refreshToken) + : null; + session.idTokenEncrypted = tokens.idToken + ? this.crypto.encrypt(tokens.idToken) + : null; + session.csrfTokenHash = this.crypto.hashToken(csrfToken); + session.accessTokenExpiresAt = tokens.accessTokenExpiresAt; + session.expiresAt = new Date( + now.getTime() + this.config.session.idleTimeoutSeconds * 1000, + ); + session.absoluteExpiresAt = new Date( + now.getTime() + this.config.session.absoluteTimeoutSeconds * 1000, + ); + session.lastActivityAt = now; + session.userAgent = userAgent ?? null; + session.lastIp = ip ?? null; + session.revokedAt = null; + return { session: await this.sessions.save(session), csrfToken }; + } + + async resolveSession( + sessionId: string, + ip: string | undefined, + userAgent: string | undefined, + ): Promise { + const session = await this.sessions.findActiveById(sessionId); + const now = new Date(); + if ( + !session || + session.expiresAt <= now || + session.absoluteExpiresAt <= now || + session.user.active === false + ) { + if (session) { + await this.sessions.revoke(session.id); + } + throw new ApiError( + ErrorCode.Unauthorized, + 'Bitte melden Sie sich erneut an.', + 401, + ); + } + + session.expiresAt = new Date( + now.getTime() + this.config.session.idleTimeoutSeconds * 1000, + ); + session.lastActivityAt = now; + session.lastIp = ip ?? session.lastIp; + session.userAgent = userAgent ?? session.userAgent; + await this.sessions.save(session); + + return { + user: session.user, + permissions: this.permissionsFor(session.user), + }; + } + + async verifyCsrfToken(sessionId: string, token: string): Promise { + const session = await this.sessions.findActiveById(sessionId); + if (!session) { + return false; + } + return session.csrfTokenHash === this.crypto.hashToken(token); + } + + async listOwn( + userId: string, + currentSessionId: string, + ): Promise { + const sessions = await this.sessions.listForUser(userId); + return sessions.map((session) => ({ + id: session.id, + createdAt: session.createdAt.toISOString(), + lastActivityAt: session.lastActivityAt.toISOString(), + userAgent: session.userAgent, + approximateIp: this.maskIp(session.lastIp), + current: session.id === currentSessionId, + revokedAt: session.revokedAt?.toISOString() ?? null, + })); + } + + revoke(sessionId: string): Promise { + return this.sessions.revoke(sessionId); + } + + async getIdTokenForLogout(sessionId: string): Promise { + const session = await this.sessions.findActiveById(sessionId); + if (!session?.idTokenEncrypted) { + return undefined; + } + return this.crypto.decrypt(session.idTokenEncrypted); + } + + revokeAllForUser(userId: string, exceptSessionId?: string): Promise { + return this.sessions.revokeAllForUser(userId, exceptSessionId); + } + + async listForAdmin( + userId: string, + currentSessionId: string | undefined, + ): Promise { + const sessions = await this.sessions.listForUser(userId); + return sessions.map((session) => ({ + id: session.id, + publicId: `${session.id.slice(0, 8)}...${session.id.slice(-6)}`, + createdAt: session.createdAt.toISOString(), + lastActivityAt: session.lastActivityAt.toISOString(), + expiresAt: session.expiresAt.toISOString(), + absoluteExpiresAt: session.absoluteExpiresAt.toISOString(), + userAgent: session.userAgent, + approximateIp: this.maskIp(session.lastIp), + current: session.id === currentSessionId, + revokedAt: session.revokedAt?.toISOString() ?? null, + })); + } + + async revokeForAdmin(userId: string, sessionId: string): Promise { + const affected = await this.sessions.revokeForUser(userId, sessionId); + if (affected < 1) { + throw new ApiError( + ErrorCode.SessionNotFound, + 'Die Session wurde nicht gefunden.', + 404, + ); + } + } + + countActiveForUser(userId: string): Promise { + return this.sessions.countActiveForUser(userId); + } + + private permissionsFor(user: UserEntity): Permission[] { + return Array.from( + new Set( + user.roles.flatMap((role) => + role.permissions.map((permission) => permission.id), + ), + ), + ); + } + + private maskIp(ip: string | null): string | null { + if (!ip) { + return null; + } + if (ip.includes(':')) { + return `${ip.split(':').slice(0, 3).join(':')}:...`; + } + return ip.replace(/\.\d+$/, '.0'); + } +} diff --git a/apps/backend/src/sessions/tests/session-crypto.service.spec.ts b/apps/backend/src/sessions/tests/session-crypto.service.spec.ts new file mode 100644 index 0000000..78a1eb9 --- /dev/null +++ b/apps/backend/src/sessions/tests/session-crypto.service.spec.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest'; +import { SessionCryptoService } from '../session-crypto.service'; +import type { AppConfigService } from '../../config/config.service'; + +describe('SessionCryptoService', () => { + const config = { + session: { + encryptionKey: '12345678901234567890123456789012', + secret: 'abcdefghijklmnopqrstuvwxyz123456', + }, + } as AppConfigService; + + it('encrypts tokens without storing plaintext', () => { + const crypto = new SessionCryptoService(config); + const encrypted = crypto.encrypt('access-token'); + + expect(encrypted).not.toContain('access-token'); + expect(crypto.decrypt(encrypted)).toBe('access-token'); + }); + + it('creates stable CSRF token hashes', () => { + const crypto = new SessionCryptoService(config); + + expect(crypto.hashToken('csrf')).toBe(crypto.hashToken('csrf')); + expect(crypto.hashToken('csrf')).not.toBe(crypto.hashToken('other')); + }); +}); diff --git a/apps/backend/src/users/dto/user.dto.ts b/apps/backend/src/users/dto/user.dto.ts new file mode 100644 index 0000000..19e683f --- /dev/null +++ b/apps/backend/src/users/dto/user.dto.ts @@ -0,0 +1,69 @@ +import { Type } from 'class-transformer'; +import { + IsArray, + IsBoolean, + IsIn, + IsInt, + IsOptional, + IsString, + IsUUID, + Max, + Min, +} from 'class-validator'; + +export class UpdateUserRolesDto { + @IsArray() + @IsString({ each: true }) + roleIds!: string[]; +} + +export class UpdateUserActiveDto { + @IsBoolean() + active!: boolean; +} + +export class UpdateSettingsDto { + @IsOptional() + tablePageSize?: number; + + @IsOptional() + sidebarExpanded?: boolean; +} + +export type AdminUserSortField = 'name' | 'email' | 'lastLoginAt' | 'createdAt'; +export type AdminUserActiveFilter = 'all' | 'active' | 'inactive'; + +export class AdminUserListQueryDto { + @IsOptional() + @IsString() + search?: string; + + @IsOptional() + @IsIn(['all', 'active', 'inactive']) + active: AdminUserActiveFilter = 'all'; + + @IsOptional() + @IsUUID() + roleId?: string; + + @IsOptional() + @IsIn(['name', 'email', 'lastLoginAt', 'createdAt']) + sort: AdminUserSortField = 'createdAt'; + + @IsOptional() + @IsIn(['ASC', 'DESC']) + direction: 'ASC' | 'DESC' = 'DESC'; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page = 1; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + pageSize = 25; +} diff --git a/apps/backend/src/users/entities/user-settings.entity.ts b/apps/backend/src/users/entities/user-settings.entity.ts new file mode 100644 index 0000000..aa7449e --- /dev/null +++ b/apps/backend/src/users/entities/user-settings.entity.ts @@ -0,0 +1,24 @@ +import { + Column, + Entity, + JoinColumn, + OneToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { UserEntity } from './user.entity'; + +@Entity('user_settings') +export class UserSettingsEntity { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @OneToOne(() => UserEntity, (user) => user.settings, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user!: UserEntity; + + @Column({ name: 'table_page_size', type: 'int', default: 20 }) + tablePageSize!: number; + + @Column({ name: 'sidebar_expanded', type: 'boolean', default: true }) + sidebarExpanded!: boolean; +} diff --git a/apps/backend/src/users/entities/user.entity.ts b/apps/backend/src/users/entities/user.entity.ts new file mode 100644 index 0000000..21c6f0b --- /dev/null +++ b/apps/backend/src/users/entities/user.entity.ts @@ -0,0 +1,70 @@ +import { + Column, + CreateDateColumn, + Entity, + Index, + JoinTable, + ManyToMany, + OneToMany, + OneToOne, + PrimaryGeneratedColumn, + Unique, + UpdateDateColumn, +} from 'typeorm'; +import { RoleEntity } from '../../roles/entities/role.entity'; +import { SessionEntity } from '../../sessions/entities/session.entity'; +import { UserSettingsEntity } from './user-settings.entity'; + +@Entity('users') +@Unique('uq_users_issuer_subject', ['issuer', 'subject']) +export class UserEntity { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Index('idx_users_issuer') + @Column({ type: 'varchar', length: 255 }) + issuer!: string; + + @Column({ type: 'varchar', length: 255 }) + subject!: string; + + @Column({ type: 'varchar', length: 255 }) + name!: string; + + @Column({ type: 'varchar', length: 320, nullable: true }) + email!: string | null; + + @Column({ type: 'boolean', default: true }) + active!: boolean; + + @Column({ + name: 'last_login_at', + type: 'datetime', + precision: 3, + nullable: true, + }) + lastLoginAt!: Date | null; + + @ManyToMany(() => RoleEntity, (role) => role.users, { eager: true }) + @JoinTable({ + name: 'user_roles', + joinColumn: { name: 'user_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'role_id', referencedColumnName: 'id' }, + }) + roles!: RoleEntity[]; + + @OneToMany(() => SessionEntity, (session) => session.user) + sessions!: SessionEntity[]; + + @OneToOne(() => UserSettingsEntity, (settings) => settings.user, { + cascade: true, + eager: true, + }) + settings!: UserSettingsEntity; + + @CreateDateColumn({ name: 'created_at', type: 'datetime', precision: 3 }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'datetime', precision: 3 }) + updatedAt!: Date; +} diff --git a/apps/backend/src/users/repositories/users.repository.ts b/apps/backend/src/users/repositories/users.repository.ts new file mode 100644 index 0000000..416baed --- /dev/null +++ b/apps/backend/src/users/repositories/users.repository.ts @@ -0,0 +1,112 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { EntityManager, Repository } from 'typeorm'; +import type { + AdminUserActiveFilter, + AdminUserSortField, +} from '../dto/user.dto'; +import { UserEntity } from '../entities/user.entity'; + +@Injectable() +export class UsersRepository { + constructor( + @InjectRepository(UserEntity) private readonly repo: Repository, + ) {} + + findById(id: string, manager?: EntityManager): Promise { + return (manager?.getRepository(UserEntity) ?? this.repo).findOne({ + where: { id }, + }); + } + + findByIdentity(issuer: string, subject: string): Promise { + return this.repo.findOne({ where: { issuer, subject } }); + } + + async search( + query: string | undefined, + page: number, + pageSize: number, + ): Promise<[UserEntity[], number]> { + const qb = this.repo + .createQueryBuilder('user') + .leftJoinAndSelect('user.roles', 'role'); + if (query) { + qb.where('user.name LIKE :query OR user.email LIKE :query', { + query: `%${query}%`, + }); + } + return qb + .orderBy('user.createdAt', 'DESC') + .skip((page - 1) * pageSize) + .take(pageSize) + .getManyAndCount(); + } + + async adminSearch( + query: { + search?: string; + active: AdminUserActiveFilter; + roleId?: string; + sort: AdminUserSortField; + direction: 'ASC' | 'DESC'; + page: number; + pageSize: number; + }, + manager?: EntityManager, + ): Promise<[UserEntity[], number]> { + const repo = manager?.getRepository(UserEntity) ?? this.repo; + const qb = repo + .createQueryBuilder('user') + .leftJoinAndSelect('user.roles', 'role') + .leftJoinAndSelect('role.permissions', 'permission'); + if (query.search) { + qb.andWhere('user.name LIKE :search OR user.email LIKE :search', { + search: `%${query.search}%`, + }); + } + if (query.active !== 'all') { + qb.andWhere('user.active = :active', { + active: query.active === 'active', + }); + } + if (query.roleId) { + qb.andWhere( + 'EXISTS (SELECT 1 FROM user_roles ur WHERE ur.user_id = user.id AND ur.role_id = :roleId)', + { roleId: query.roleId }, + ); + } + return qb + .orderBy(`user.${query.sort}`, query.direction) + .skip((query.page - 1) * query.pageSize) + .take(query.pageSize) + .getManyAndCount(); + } + + findByIdWithRoles( + id: string, + manager?: EntityManager, + ): Promise { + return (manager?.getRepository(UserEntity) ?? this.repo).findOne({ + where: { id }, + relations: { roles: { permissions: true }, settings: true }, + }); + } + + async save(user: UserEntity, manager?: EntityManager): Promise { + return (manager?.getRepository(UserEntity) ?? this.repo).save(user); + } + + findFirstActiveAdmin(excludedUserId?: string): Promise { + const qb = this.repo + .createQueryBuilder('user') + .innerJoin('user.roles', 'role') + .where('user.active = :active', { active: true }) + .andWhere('role.name = :role', { role: 'admin' }) + .orderBy('user.createdAt', 'ASC'); + if (excludedUserId) { + qb.andWhere('user.id <> :excludedUserId', { excludedUserId }); + } + return qb.getOne(); + } +} diff --git a/apps/backend/src/users/tests/users.service.spec.ts b/apps/backend/src/users/tests/users.service.spec.ts new file mode 100644 index 0000000..802c169 --- /dev/null +++ b/apps/backend/src/users/tests/users.service.spec.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; +import { ErrorCode } from '../../common/errors/error-codes'; +import { UsersService } from '../users.service'; +import type { AuditService } from '../../audit/audit.service'; +import type { NotificationsService } from '../../notifications/notifications.service'; +import type { RolesService } from '../../roles/roles.service'; +import type { SessionsService } from '../../sessions/sessions.service'; +import type { UsersRepository } from '../repositories/users.repository'; +import type { DataSource } from 'typeorm'; + +describe('UsersService', () => { + it('blocks changes that would remove the last active admin', async () => { + const dataSource = { + manager: { + getRepository: () => ({ + createQueryBuilder: () => ({ + innerJoin: () => ({ + where: () => ({ + andWhere: () => ({ + andWhere: () => ({ + getCount: () => Promise.resolve(0), + }), + }), + }), + }), + }), + }), + }, + } as unknown as DataSource; + const service = new UsersService( + {} as UsersRepository, + {} as RolesService, + {} as SessionsService, + {} as AuditService, + {} as NotificationsService, + dataSource, + ); + + await expect( + service.assertAnotherActiveAdminRemains('user-1'), + ).rejects.toMatchObject({ + code: ErrorCode.LastAdminRequired, + }); + }); +}); diff --git a/apps/backend/src/users/users.controller.ts b/apps/backend/src/users/users.controller.ts new file mode 100644 index 0000000..315f250 --- /dev/null +++ b/apps/backend/src/users/users.controller.ts @@ -0,0 +1,165 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + Query, + Req, +} from '@nestjs/common'; +import type { AuthenticatedRequest } from '../auth/authenticated-request'; +import { RequirePermissions } from '../auth/guards/require-permissions.decorator'; +import { ApiError } from '../common/errors/api-error'; +import { ErrorCode } from '../common/errors/error-codes'; +import { SensitiveRateLimit } from '../common/rate-limit/sensitive-rate-limit.decorator'; +import { Permission } from '../roles/permissions'; +import { + AdminUserListQueryDto, + UpdateSettingsDto, + UpdateUserActiveDto, + UpdateUserRolesDto, +} from './dto/user.dto'; +import { UsersService } from './users.service'; + +@Controller() +export class UsersController { + constructor(private readonly users: UsersService) {} + + @Get('me') + @RequirePermissions(Permission.SessionsReadOwn) + me(@Req() req: AuthenticatedRequest) { + return this.users.get(req.user?.id ?? ''); + } + + @Patch('me/settings') + @RequirePermissions(Permission.SessionsReadOwn) + updateSettings( + @Req() req: AuthenticatedRequest, + @Body() dto: UpdateSettingsDto, + ) { + return this.users.updateSettings(req.user?.id ?? '', dto); + } + + @Get('users') + @RequirePermissions(Permission.UsersRead) + list( + @Query('search') search?: string, + @Query('page') page?: string, + @Query('pageSize') pageSize?: string, + ) { + return this.users.list(search, Number(page ?? 1), Number(pageSize ?? 20)); + } + + @Get('admin/users') + @RequirePermissions(Permission.UsersRead) + adminList(@Query() query: AdminUserListQueryDto) { + return this.users.adminList(query); + } + + @Get('admin/users/:id') + @RequirePermissions(Permission.UsersRead) + adminGet(@Req() req: AuthenticatedRequest, @Param('id') id: string) { + return this.users.adminGet(id, req.user?.sessionId); + } + + @Patch('admin/users/:id/deactivate') + @RequirePermissions(Permission.UsersManage) + @SensitiveRateLimit() + deactivate(@Req() req: AuthenticatedRequest, @Param('id') id: string) { + return this.users.setActive(this.requireUser(req), id, false); + } + + @Patch('admin/users/:id/activate') + @RequirePermissions(Permission.UsersManage) + @SensitiveRateLimit() + activate(@Req() req: AuthenticatedRequest, @Param('id') id: string) { + return this.users.setActive(this.requireUser(req), id, true); + } + + @Post('admin/users/:id/roles/:roleId') + @RequirePermissions(Permission.UsersManage) + @SensitiveRateLimit() + assignRole( + @Req() req: AuthenticatedRequest, + @Param('id') id: string, + @Param('roleId') roleId: string, + ) { + return this.users.assignRole(this.requireUser(req), id, roleId); + } + + @Delete('admin/users/:id/roles/:roleId') + @RequirePermissions(Permission.UsersManage) + @SensitiveRateLimit() + removeRole( + @Req() req: AuthenticatedRequest, + @Param('id') id: string, + @Param('roleId') roleId: string, + ) { + return this.users.removeRole(this.requireUser(req), id, roleId); + } + + @Get('admin/users/:id/sessions') + @RequirePermissions(Permission.SessionsManage) + adminSessions(@Req() req: AuthenticatedRequest, @Param('id') id: string) { + return this.users + .adminGet(id, req.user?.sessionId) + .then((user) => user.sessions); + } + + @Delete('admin/users/:userId/sessions/:sessionId') + @RequirePermissions(Permission.SessionsManage) + @SensitiveRateLimit() + revokeSession( + @Req() req: AuthenticatedRequest, + @Param('userId') userId: string, + @Param('sessionId') sessionId: string, + ) { + return this.users.revokeUserSession( + this.requireUser(req), + userId, + sessionId, + ); + } + + @Delete('admin/users/:id/sessions') + @RequirePermissions(Permission.SessionsManage) + @SensitiveRateLimit() + revokeSessions(@Req() req: AuthenticatedRequest, @Param('id') id: string) { + return this.users.revokeUserSessions(this.requireUser(req), id); + } + + @Patch('users/:id/active') + @RequirePermissions(Permission.UsersManage) + @SensitiveRateLimit() + setActive( + @Req() req: AuthenticatedRequest, + @Param('id') id: string, + @Body() dto: UpdateUserActiveDto, + ) { + return this.users.setActive(this.requireUser(req), id, dto.active); + } + + @Patch('users/:id/roles') + @RequirePermissions(Permission.UsersManage) + @SensitiveRateLimit() + setRoles( + @Req() req: AuthenticatedRequest, + @Param('id') id: string, + @Body() dto: UpdateUserRolesDto, + ) { + return this.users.setRoles(this.requireUser(req), id, dto.roleIds); + } + + private requireUser(req: AuthenticatedRequest) { + if (!req.user) { + throw new ApiError( + ErrorCode.Unauthorized, + 'Bitte melden Sie sich an.', + 401, + ); + } + return req.user; + } +} diff --git a/apps/backend/src/users/users.module.ts b/apps/backend/src/users/users.module.ts new file mode 100644 index 0000000..c20620c --- /dev/null +++ b/apps/backend/src/users/users.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuditModule } from '../audit/audit.module'; +import { NotificationsModule } from '../notifications/notifications.module'; +import { RolesModule } from '../roles/roles.module'; +import { SessionsModule } from '../sessions/sessions.module'; +import { UserSettingsEntity } from './entities/user-settings.entity'; +import { UserEntity } from './entities/user.entity'; +import { UsersRepository } from './repositories/users.repository'; +import { UsersController } from './users.controller'; +import { UsersService } from './users.service'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([UserEntity, UserSettingsEntity]), + RolesModule, + AuditModule, + SessionsModule, + NotificationsModule, + ], + controllers: [UsersController], + providers: [UsersRepository, UsersService], + exports: [UsersRepository, UsersService], +}) +export class UsersModule {} diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts new file mode 100644 index 0000000..77b5d94 --- /dev/null +++ b/apps/backend/src/users/users.service.ts @@ -0,0 +1,416 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource, EntityManager } from 'typeorm'; +import { AuditService } from '../audit/audit.service'; +import { AuditAction } from '../audit/entities/audit-log.entity'; +import type { AuthenticatedUser } from '../auth/authenticated-request'; +import { ApiError } from '../common/errors/api-error'; +import { ErrorCode } from '../common/errors/error-codes'; +import { NotificationType } from '../notifications/notification-types'; +import { NotificationsService } from '../notifications/notifications.service'; +import { RolesService } from '../roles/roles.service'; +import { RoleEntity } from '../roles/entities/role.entity'; +import { SessionsService } from '../sessions/sessions.service'; +import type { AdminSessionDto } from '../sessions/sessions.service'; +import { UserSettingsEntity } from './entities/user-settings.entity'; +import { UserEntity } from './entities/user.entity'; +import type { AdminUserListQueryDto } from './dto/user.dto'; +import { UsersRepository } from './repositories/users.repository'; + +interface AdminRoleSummary { + id: string; + name: string; + description: string; + system: boolean; +} + +interface AdminUserListItem { + id: string; + name: string; + email: string | null; + active: boolean; + roles: AdminRoleSummary[]; + lastLoginAt: string | null; + createdAt: string; + activeSessionCount: number; +} + +interface AdminUserDetail extends AdminUserListItem { + effectivePermissions: string[]; + sessions: AdminSessionDto[]; + settings: { tablePageSize: number; sidebarExpanded: boolean }; +} + +@Injectable() +export class UsersService { + private readonly logger = new Logger(UsersService.name); + + constructor( + private readonly users: UsersRepository, + private readonly roles: RolesService, + private readonly sessions: SessionsService, + private readonly audit: AuditService, + private readonly notifications: NotificationsService, + @InjectDataSource() private readonly dataSource: DataSource, + ) {} + + async list(search: string | undefined, page = 1, pageSize = 20) { + const [items, total] = await this.users.search(search, page, pageSize); + return { items, total, page, pageSize }; + } + + async adminList(query: AdminUserListQueryDto) { + const [users, total] = await this.users.adminSearch(query); + const items = await Promise.all( + users.map(async (user) => ({ + ...this.toAdminListItem(user), + activeSessionCount: await this.sessions.countActiveForUser(user.id), + })), + ); + return { items, total, page: query.page, pageSize: query.pageSize }; + } + + async adminGet( + id: string, + currentSessionId: string | undefined, + ): Promise { + const user = await this.getWithRoles(id); + return { + ...this.toAdminListItem(user), + activeSessionCount: await this.sessions.countActiveForUser(user.id), + effectivePermissions: this.effectivePermissions(user), + sessions: await this.sessions.listForAdmin(user.id, currentSessionId), + settings: { + tablePageSize: user.settings.tablePageSize, + sidebarExpanded: user.settings.sidebarExpanded, + }, + }; + } + + async get(id: string): Promise { + const user = await this.users.findById(id); + if (!user) { + throw new ApiError( + ErrorCode.NotFound, + 'Der Benutzer wurde nicht gefunden.', + 404, + ); + } + return user; + } + + async setActive( + actor: AuthenticatedUser, + userId: string, + active: boolean, + ): Promise { + return this.withAdminLock(async (manager) => { + const user = await this.getWithRoles(userId, manager); + if (active && user.active) { + throw new ApiError( + ErrorCode.UserAlreadyActive, + 'Der Benutzer ist bereits aktiv.', + 409, + ); + } + if (!active && !user.active) { + throw new ApiError( + ErrorCode.UserAlreadyInactive, + 'Der Benutzer ist bereits deaktiviert.', + 409, + ); + } + if (!active && this.hasRole(user, 'admin')) { + await this.assertAnotherActiveAdminRemains(userId, manager); + } + user.active = active; + const saved = await this.users.save(user, manager); + if (!active) { + await this.sessions.revokeAllForUser(userId); + } + await this.audit.record( + actor.id, + active ? AuditAction.UserActivated : AuditAction.UserDeactivated, + 'user', + userId, + ); + return saved; + }); + } + + async setRoles( + actor: AuthenticatedUser, + userId: string, + roleIds: string[], + ): Promise { + return this.replaceRoles(actor, userId, roleIds); + } + + async assignRole( + actor: AuthenticatedUser, + userId: string, + roleId: string, + ): Promise { + const user = await this.getWithRoles(userId); + if (user.roles.some((role) => role.id === roleId)) { + return user; + } + return this.replaceRoles(actor, userId, [ + ...user.roles.map((role) => role.id), + roleId, + ]); + } + + async removeRole( + actor: AuthenticatedUser, + userId: string, + roleId: string, + ): Promise { + const user = await this.getWithRoles(userId); + if (!user.roles.some((role) => role.id === roleId)) { + return user; + } + return this.replaceRoles( + actor, + userId, + user.roles.filter((role) => role.id !== roleId).map((role) => role.id), + ); + } + + async revokeUserSession( + actor: AuthenticatedUser, + userId: string, + sessionId: string, + ): Promise { + await this.get(userId); + await this.sessions.revokeForAdmin(userId, sessionId); + await this.audit.record( + actor.id, + AuditAction.SessionRevoked, + 'session', + sessionId, + { + targetUserId: userId, + }, + ); + } + + async revokeUserSessions( + actor: AuthenticatedUser, + userId: string, + ): Promise<{ revoked: number }> { + await this.get(userId); + const revoked = await this.sessions.revokeAllForUser(userId); + await this.audit.record( + actor.id, + AuditAction.AllUserSessionsRevoked, + 'user', + userId, + { revoked }, + ); + return { revoked }; + } + + async updateSettings( + userId: string, + settings: Partial< + Pick + >, + ): Promise { + const user = await this.get(userId); + user.settings.tablePageSize = + settings.tablePageSize ?? user.settings.tablePageSize; + user.settings.sidebarExpanded = + settings.sidebarExpanded ?? user.settings.sidebarExpanded; + return this.users.save(user); + } + + async assertAnotherActiveAdminRemains( + excludedUserId: string, + manager?: EntityManager, + ): Promise { + const result = await (manager ?? this.dataSource.manager) + .getRepository(UserEntity) + .createQueryBuilder('user') + .innerJoin('user.roles', 'role') + .where('user.active = :active', { active: true }) + .andWhere('user.id <> :excludedUserId', { excludedUserId }) + .andWhere('role.name = :role', { role: 'admin' }) + .getCount(); + if (result < 1) { + throw new ApiError( + ErrorCode.LastAdminRequired, + 'Mindestens ein aktiver Administrator muss erhalten bleiben.', + 409, + ); + } + } + + private async replaceRoles( + actor: AuthenticatedUser, + userId: string, + roleIds: string[], + ): Promise { + return this.withAdminLock(async (manager) => { + const user = await this.getWithRoles(userId, manager); + const previousRoleNames = new Set(user.roles.map((role) => role.name)); + const previousRoleIds = new Set(user.roles.map((role) => role.id)); + const roles = await Promise.all( + roleIds.map((id) => this.roles.getRole(id, manager)), + ); + const nextRoleNames = new Set(roles.map((role) => role.name)); + user.roles = roles; + if (previousRoleNames.has('admin') && !nextRoleNames.has('admin')) { + await this.assertAnotherActiveAdminRemains(userId, manager); + } + const saved = await this.users.save(user, manager); + await this.recordRoleAudit(actor, userId, previousRoleIds, roles); + await this.notifyRoleChanges(userId, previousRoleNames, nextRoleNames); + return saved; + }); + } + + private async recordRoleAudit( + actor: AuthenticatedUser, + userId: string, + previousRoleIds: Set, + nextRoles: RoleEntity[], + ): Promise { + const nextRoleIds = new Set(nextRoles.map((role) => role.id)); + for (const previousRoleId of previousRoleIds) { + if (!nextRoleIds.has(previousRoleId)) { + await this.audit.record( + actor.id, + AuditAction.UserRoleRemoved, + 'user', + userId, + { roleId: previousRoleId }, + ); + } + } + for (const role of nextRoles) { + if (!previousRoleIds.has(role.id)) { + await this.audit.record( + actor.id, + AuditAction.UserRoleAssigned, + 'user', + userId, + { roleId: role.id, roleName: role.name }, + ); + } + } + } + + private async getWithRoles( + userId: string, + manager?: EntityManager, + ): Promise { + const user = await this.users.findByIdWithRoles(userId, manager); + if (!user) { + throw new ApiError( + ErrorCode.UserNotFound, + 'Der Benutzer wurde nicht gefunden.', + 404, + ); + } + return user; + } + + private async withAdminLock( + action: (manager: EntityManager) => Promise, + ): Promise { + return this.dataSource.transaction(async (manager) => { + await manager.query( + "SELECT GET_LOCK('business_app_admin_integrity', 10)", + ); + try { + return await action(manager); + } finally { + await manager.query( + "SELECT RELEASE_LOCK('business_app_admin_integrity')", + ); + } + }); + } + + private toAdminListItem(user: UserEntity): AdminUserListItem { + return { + id: user.id, + name: user.name, + email: user.email, + active: user.active, + roles: user.roles.map((role) => ({ + id: role.id, + name: role.name, + description: role.description, + system: role.protected, + })), + lastLoginAt: user.lastLoginAt?.toISOString() ?? null, + createdAt: user.createdAt.toISOString(), + activeSessionCount: 0, + }; + } + + private effectivePermissions(user: UserEntity): string[] { + return Array.from( + new Set( + user.roles.flatMap((role) => + role.permissions.map((permission) => permission.id), + ), + ), + ).sort(); + } + + private hasRole(user: UserEntity, roleName: string): boolean { + return user.roles.some((role) => role.name === roleName); + } + + private async notifyRoleChanges( + userId: string, + previousRoleNames: Set, + nextRoleNames: Set, + ): Promise { + const added = [...nextRoleNames].filter( + (role) => !previousRoleNames.has(role), + ); + const removed = [...previousRoleNames].filter( + (role) => !nextRoleNames.has(role), + ); + for (const role of added) { + await this.createRoleNotification( + userId, + role, + `Ihnen wurde die Rolle "${role}" zugewiesen.`, + ); + } + for (const role of removed) { + await this.createRoleNotification( + userId, + role, + `Die Rolle "${role}" wurde Ihnen entzogen.`, + ); + } + } + + private async createRoleNotification( + userId: string, + role: string, + message: string, + ): Promise { + try { + await this.notifications.createForUser({ + userId, + type: NotificationType.UserRoleChanged, + title: 'Rollen geaendert', + message, + link: '/profil', + metadata: { role }, + }); + } catch (error) { + this.logger.error( + { userId, role, error }, + 'Failed to create role change notification', + ); + } + } +} diff --git a/apps/backend/tsconfig.build.json b/apps/backend/tsconfig.build.json new file mode 100644 index 0000000..e097075 --- /dev/null +++ b/apps/backend/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*.spec.ts", "**/*.test.ts"] +} diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json new file mode 100644 index 0000000..2038d03 --- /dev/null +++ b/apps/backend/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "Node16", + "moduleResolution": "Node16", + "target": "ES2023", + "outDir": "dist", + "rootDir": "src", + "lib": ["ES2023"], + "types": ["node", "vitest"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "isolatedModules": false, + "strictPropertyInitialization": false, + "sourceMap": true + }, + "include": ["src/**/*.ts"] +} diff --git a/apps/backend/vitest.config.ts b/apps/backend/vitest.config.ts new file mode 100644 index 0000000..31e4ac1 --- /dev/null +++ b/apps/backend/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['src/**/*.spec.ts'], + globals: true, + coverage: { + reporter: ['text', 'html'], + }, + }, +}); diff --git a/apps/frontend/.editorconfig b/apps/frontend/.editorconfig new file mode 100644 index 0000000..f166060 --- /dev/null +++ b/apps/frontend/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single +ij_typescript_use_double_quotes = false + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/apps/frontend/.gitignore b/apps/frontend/.gitignore new file mode 100644 index 0000000..854acd5 --- /dev/null +++ b/apps/frontend/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/mcp.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/apps/frontend/.prettierrc b/apps/frontend/.prettierrc new file mode 100644 index 0000000..d6c16d7 --- /dev/null +++ b/apps/frontend/.prettierrc @@ -0,0 +1,12 @@ +{ + "printWidth": 100, + "singleQuote": true, + "overrides": [ + { + "files": "*.html", + "options": { + "parser": "angular" + } + } + ] +} diff --git a/apps/frontend/.vscode/extensions.json b/apps/frontend/.vscode/extensions.json new file mode 100644 index 0000000..77b3745 --- /dev/null +++ b/apps/frontend/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/apps/frontend/.vscode/launch.json b/apps/frontend/.vscode/launch.json new file mode 100644 index 0000000..925af83 --- /dev/null +++ b/apps/frontend/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + }, + { + "name": "ng test", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: test", + "url": "http://localhost:9876/debug.html" + } + ] +} diff --git a/apps/frontend/.vscode/tasks.json b/apps/frontend/.vscode/tasks.json new file mode 100644 index 0000000..244306f --- /dev/null +++ b/apps/frontend/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "Changes detected" + }, + "endsPattern": { + "regexp": "bundle generation (complete|failed)" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "Changes detected" + }, + "endsPattern": { + "regexp": "bundle generation (complete|failed)" + } + } + } + } + ] +} diff --git a/apps/frontend/README.md b/apps/frontend/README.md new file mode 100644 index 0000000..415a099 --- /dev/null +++ b/apps/frontend/README.md @@ -0,0 +1,35 @@ +# Frontend Workspace + +Angular Frontend fuer das Boilerplate. Der Workspace wird normalerweise ueber die +Root-Scripts gesteuert. + +## Befehle + +```bash +npm --workspace apps/frontend run start +npm --workspace apps/frontend run build +npm --workspace apps/frontend run typecheck +npm --workspace apps/frontend run test +``` + +Im lokalen Start laeuft Angular auf `http://localhost:4200` und nutzt +`proxy.conf.json`, um `/api` an das Backend auf `http://localhost:3000` zu +proxyn. + +## Struktur + +- `src/app/layout/app-shell.ts`: Hauptlayout, Navigation und Login-Zustand +- `src/app/app.routes.ts`: Routen und Permission-Daten +- `src/app/core/auth.service.ts`: aktueller Benutzer und Permissions +- `src/app/core/csrf.interceptor.ts`: CSRF-Header fuer schreibende Requests +- `src/app/core/permission.guard.ts`: UI-seitige Routensperre +- `src/app/features`: fachliche Seiten + +## Entwicklungsregeln + +Das Frontend nutzt keine UI-Library. Komponenten bleiben mobile-first und +verwenden eigenes HTML und SCSS. Permissions im Frontend dienen nur Darstellung +und Navigation; die verbindliche Autorisierung findet im Backend statt. + +API-Zugriffe laufen ueber `@boilerplate/api-client`. Der Client ist generiert +und wird aus dem Root mit `npm run api:generate` aktualisiert. diff --git a/apps/frontend/angular.json b/apps/frontend/angular.json new file mode 100644 index 0000000..5d539c4 --- /dev/null +++ b/apps/frontend/angular.json @@ -0,0 +1,83 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "npm", + "analytics": false + }, + "newProjectRoot": "projects", + "projects": { + "frontend": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.scss"] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/app/core/dev-routes.ts", + "with": "src/app/core/dev-routes.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "frontend:build:production" + }, + "development": { + "buildTarget": "frontend:build:development" + } + }, + "defaultConfiguration": "development" + }, + "test": { + "builder": "@angular/build:unit-test" + } + } + } + } +} diff --git a/apps/frontend/package.json b/apps/frontend/package.json new file mode 100644 index 0000000..845c470 --- /dev/null +++ b/apps/frontend/package.json @@ -0,0 +1,24 @@ +{ + "name": "@boilerplate/frontend", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "start": "ng serve --host 0.0.0.0 --port 4200 --proxy-config proxy.conf.json", + "build": "ng build --configuration production", + "typecheck": "ng build --configuration development --no-progress", + "test": "vitest run --config vitest.config.ts" + }, + "dependencies": { + "@angular/common": "22.0.6", + "@angular/compiler": "22.0.6", + "@angular/core": "22.0.6", + "@angular/forms": "22.0.6", + "@angular/platform-browser": "22.0.6", + "@angular/platform-browser-dynamic": "22.0.6", + "@angular/router": "22.0.6", + "@boilerplate/api-client": "1.0.0", + "rxjs": "7.8.2", + "tslib": "2.8.1" + } +} diff --git a/apps/frontend/proxy.conf.json b/apps/frontend/proxy.conf.json new file mode 100644 index 0000000..5f99906 --- /dev/null +++ b/apps/frontend/proxy.conf.json @@ -0,0 +1,12 @@ +{ + "/api": { + "target": "http://localhost:3000", + "secure": false, + "changeOrigin": true + }, + "/health": { + "target": "http://localhost:3000", + "secure": false, + "changeOrigin": true + } +} diff --git a/apps/frontend/public/favicon.ico b/apps/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..57614f9c967596fad0a3989bec2b1deff33034f6 GIT binary patch literal 15086 zcmd^G33O9Omi+`8$@{|M-I6TH3wzF-p5CV8o}7f~KxR60LK+ApEFB<$bcciv%@SmA zV{n>g85YMFFeU*Uvl=i4v)C*qgnb;$GQ=3XTe9{Y%c`mO%su)noNCCQ*@t1WXn|B(hQ7i~ zrUK8|pUkD6#lNo!bt$6)jR!&C?`P5G(`e((P($RaLeq+o0Vd~f11;qB05kdbAOm?r zXv~GYr_sibQO9NGTCdT;+G(!{4Xs@4fPak8#L8PjgJwcs-Mm#nR_Z0s&u?nDX5^~@ z+A6?}g0|=4e_LoE69pPFO`yCD@BCjgKpzMH0O4Xs{Ahc?K3HC5;l=f zg>}alhBXX&);z$E-wai+9TTRtBX-bWYY@cl$@YN#gMd~tM_5lj6W%8ah4;uZ;jP@Q zVbuel1rPA?2@x9Y+u?e`l{Z4ngfG5q5BLH5QsEu4GVpt{KIp1?U)=3+KQ;%7ec8l* zdV=zZgN5>O3G(3L2fqj3;oBbZZw$Ij@`Juz@?+yy#OPw)>#wsTewVgTK9BGt5AbZ&?K&B3GVF&yu?@(Xj3fR3n+ZP0%+wo)D9_xp>Z$`A4 zfV>}NWjO#3lqumR0`gvnffd9Ka}JJMuHS&|55-*mCD#8e^anA<+sFZVaJe7{=p*oX zE_Uv?1>e~ga=seYzh{9P+n5<+7&9}&(kwqSaz;1aD|YM3HBiy<))4~QJSIryyqp| z8nGc(8>3(_nEI4n)n7j(&d4idW1tVLjZ7QbNLXg;LB ziHsS5pXHEjGJZb59KcvS~wv;uZR-+4qEqow`;JCfB*+b^UL^3!?;-^F%yt=VjU|v z39SSqKcRu_NVvz!zJzL0CceJaS6%!(eMshPv_0U5G`~!a#I$qI5Ic(>IONej@aH=f z)($TAT#1I{iCS4f{D2+ApS=$3E7}5=+y(rA9mM#;Cky%b*Gi0KfFA`ofKTzu`AV-9 znW|y@19rrZ*!N2AvDi<_ZeR3O2R{#dh1#3-d%$k${Rx42h+i&GZo5!C^dSL34*AKp z27mTd>k>?V&X;Nl%GZ(>0s`1UN~Hfyj>KPjtnc|)xM@{H_B9rNr~LuH`Gr5_am&Ep zTjZA8hljNj5H1Ipm-uD9rC}U{-vR!eay5&6x6FkfupdpT*84MVwGpdd(}ib)zZ3Ky z7C$pnjc82(W_y_F{PhYj?o!@3__UUvpX)v69aBSzYj3 zdi}YQkKs^SyXyFG2LTRz9{(w}y~!`{EuAaUr6G1M{*%c+kP1olW9z23dSH!G4_HSK zzae-DF$OGR{ofP*!$a(r^5Go>I3SObVI6FLY)N@o<*gl0&kLo-OT{Tl*7nCz>Iq=? zcigIDHtj|H;6sR?or8Wd_a4996GI*CXGU}o;D9`^FM!AT1pBY~?|4h^61BY#_yIfO zKO?E0 zJ{Pc`9rVEI&$xxXu`<5E)&+m(7zX^v0rqofLs&bnQT(1baQkAr^kEsk)15vlzAZ-l z@OO9RF<+IiJ*O@HE256gCt!bF=NM*vh|WVWmjVawcNoksRTMvR03H{p@cjwKh(CL4 z7_PB(dM=kO)!s4fW!1p0f93YN@?ZSG` z$B!JaAJCtW$B97}HNO9(x-t30&E}Mo1UPi@Av%uHj~?T|!4JLwV;KCx8xO#b9IlUW zI6+{a@Wj|<2Y=U;a@vXbxqZNngH8^}LleE_4*0&O7#3iGxfJ%Id>+sb;7{L=aIic8 z|EW|{{S)J-wr@;3PmlxRXU8!e2gm_%s|ReH!reFcY8%$Hl4M5>;6^UDUUae?kOy#h zk~6Ee_@ZAn48Bab__^bNmQ~+k=02jz)e0d9Z3>G?RGG!65?d1>9}7iG17?P*=GUV-#SbLRw)Hu{zx*azHxWkGNTWl@HeWjA?39Ia|sCi{e;!^`1Oec zb>Z|b65OM*;eC=ZLSy?_fg$&^2xI>qSLA2G*$nA3GEnp3$N-)46`|36m*sc#4%C|h zBN<2U;7k>&G_wL4=Ve5z`ubVD&*Hxi)r@{4RCDw7U_D`lbC(9&pG5C*z#W>8>HU)h z!h3g?2UL&sS!oY5$3?VlA0Me9W5e~V;2jds*fz^updz#AJ%G8w2V}AEE?E^=MK%Xt z__Bx1cr7+DQmuHmzn*|hh%~eEc9@m05@clWfpEFcr+06%0&dZJH&@8^&@*$qR@}o3 z@Tuuh2FsLz^zH+dN&T&?0G3I?MpmYJ;GP$J!EzjeM#YLJ!W$}MVNb0^HfOA>5Fe~UNn%Zk(PT@~9}1dt)1UQ zU*B5K?Dl#G74qmg|2>^>0WtLX#Jz{lO4NT`NYB*(L#D|5IpXr9v&7a@YsGp3vLR7L zHYGHZg7{ie6n~2p$6Yz>=^cEg7tEgk-1YRl%-s7^cbqFb(U7&Dp78+&ut5!Tn(hER z|Gp4Ed@CnOPeAe|N>U(dB;SZ?NU^AzoD^UAH_vamp6Ws}{|mSq`^+VP1g~2B{%N-!mWz<`)G)>V-<`9`L4?3dM%Qh6<@kba+m`JS{Ya@9Fq*m6$$ zA1%Ogc~VRH33|S9l%CNb4zM%k^EIpqY}@h{w(aBcJ9c05oiZx#SK9t->5lSI`=&l~ z+-Ic)a{FbBhXV$Xt!WRd`R#Jk-$+_Z52rS>?Vpt2IK<84|E-SBEoIw>cs=a{BlQ7O z-?{Fy_M&84&9|KM5wt~)*!~i~E=(6m8(uCO)I=)M?)&sRbzH$9Rovzd?ZEY}GqX+~ zFbEbLz`BZ49=2Yh-|<`waK-_4!7`ro@zlC|r&I4fc4oyb+m=|c8)8%tZ-z5FwhzDt zL5kB@u53`d@%nHl0Sp)Dw`(QU&>vujEn?GPEXUW!Wi<+4e%BORl&BIH+SwRcbS}X@ z01Pk|vA%OdJKAs17zSXtO55k!;%m9>1eW9LnyAX4uj7@${O6cfii`49qTNItzny5J zH&Gj`e}o}?xjQ}r?LrI%FjUd@xflT3|7LA|ka%Q3i}a8gVm<`HIWoJGH=$EGClX^C0lysQJ>UO(q&;`T#8txuoQ_{l^kEV9CAdXuU1Ghg8 zN_6hHFuy&1x24q5-(Z7;!poYdt*`UTdrQOIQ!2O7_+AHV2hgXaEz7)>$LEdG z<8vE^Tw$|YwZHZDPM!SNOAWG$?J)MdmEk{U!!$M#fp7*Wo}jJ$Q(=8>R`Ats?e|VU?Zt7Cdh%AdnfyN3MBWw{ z$OnREvPf7%z6`#2##_7id|H%Y{vV^vWXb?5d5?a_y&t3@p9t$ncHj-NBdo&X{wrfJ zamN)VMYROYh_SvjJ=Xd!Ga?PY_$;*L=SxFte!4O6%0HEh%iZ4=gvns7IWIyJHa|hT z2;1+e)`TvbNb3-0z&DD_)Jomsg-7p_Uh`wjGnU1urmv1_oVqRg#=C?e?!7DgtqojU zWoAB($&53;TsXu^@2;8M`#z{=rPy?JqgYM0CDf4v@z=ZD|ItJ&8%_7A#K?S{wjxgd z?xA6JdJojrWpB7fr2p_MSsU4(R7=XGS0+Eg#xR=j>`H@R9{XjwBmqAiOxOL` zt?XK-iTEOWV}f>Pz3H-s*>W z4~8C&Xq25UQ^xH6H9kY_RM1$ch+%YLF72AA7^b{~VNTG}Tj#qZltz5Q=qxR`&oIlW Nr__JTFzvMr^FKp4S3v*( literal 0 HcmV?d00001 diff --git a/apps/frontend/src/app/app.config.ts b/apps/frontend/src/app/app.config.ts new file mode 100644 index 0000000..7562033 --- /dev/null +++ b/apps/frontend/src/app/app.config.ts @@ -0,0 +1,17 @@ +import { provideBrowserGlobalErrorListeners } from '@angular/core'; +import type { ApplicationConfig } from '@angular/core'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; + +import { routes } from './app.routes'; +import { csrfInterceptor } from './core/csrf.interceptor'; +import { titleStrategyProvider } from './core/title.strategy'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideBrowserGlobalErrorListeners(), + provideHttpClient(withInterceptors([csrfInterceptor])), + provideRouter(routes, withComponentInputBinding()), + titleStrategyProvider, + ], +}; diff --git a/apps/frontend/src/app/app.html b/apps/frontend/src/app/app.html new file mode 100644 index 0000000..67e7bd4 --- /dev/null +++ b/apps/frontend/src/app/app.html @@ -0,0 +1 @@ + diff --git a/apps/frontend/src/app/app.routes.ts b/apps/frontend/src/app/app.routes.ts new file mode 100644 index 0000000..622462b --- /dev/null +++ b/apps/frontend/src/app/app.routes.ts @@ -0,0 +1,143 @@ +import type { Routes } from '@angular/router'; +import { permissionGuard } from './core/permission.guard'; +import { devRoutes } from './core/dev-routes'; + +export const routes: Routes = [ + { + path: '', + loadComponent: () => import('./layout/app-shell').then((m) => m.AppShellComponent), + children: [ + { + path: '', + title: 'Dashboard', + loadComponent: () => + import('./features/dashboard/dashboard.page').then((m) => m.DashboardPageComponent), + }, + { + path: 'profil', + title: 'Profil', + loadComponent: () => + import('./features/profile/profile.page').then((m) => m.ProfilePageComponent), + }, + { + path: 'account/security', + title: 'Account-Sicherheit', + loadComponent: () => + import('./features/account/account-security.page').then( + (m) => m.AccountSecurityPageComponent, + ), + }, + { + path: 'notifications', + title: 'Benachrichtigungen', + canActivate: [permissionGuard], + data: { permissions: ['notifications.readOwn'] }, + loadComponent: () => + import('./features/notifications/notifications.page').then( + (m) => m.NotificationsPageComponent, + ), + }, + { + path: 'sessions', + title: 'Eigene Sessions', + loadComponent: () => + import('./features/sessions/sessions.page').then((m) => m.SessionsPageComponent), + }, + { + path: 'items', + title: 'Items', + canActivate: [permissionGuard], + data: { permissions: ['items.read'] }, + loadComponent: () => + import('./features/items/items.page').then((m) => m.ItemsPageComponent), + }, + { + path: 'benutzer', + title: 'Benutzerverwaltung', + canActivate: [permissionGuard], + data: { permissions: ['users.read'] }, + loadComponent: () => + import('./features/users/users.page').then((m) => m.UsersPageComponent), + }, + { + path: 'admin/users', + title: 'Admin / Benutzer', + canActivate: [permissionGuard], + data: { permissions: ['users.read'] }, + loadComponent: () => + import('./features/admin/admin-users.page').then((m) => m.AdminUsersPageComponent), + }, + { + path: 'admin/users/:id', + title: 'Admin / Benutzer', + canActivate: [permissionGuard], + data: { permissions: ['users.read'] }, + loadComponent: () => + import('./features/admin/admin-user-detail.page').then( + (m) => m.AdminUserDetailPageComponent, + ), + }, + { + path: 'rollen', + title: 'Rollenverwaltung', + canActivate: [permissionGuard], + data: { permissions: ['roles.read'] }, + loadComponent: () => + import('./features/roles/roles.page').then((m) => m.RolesPageComponent), + }, + { + path: 'admin/roles', + title: 'Admin / Rollen', + canActivate: [permissionGuard], + data: { permissions: ['roles.read'] }, + loadComponent: () => + import('./features/admin/admin-roles.page').then((m) => m.AdminRolesPageComponent), + }, + { + path: 'admin/roles/:id', + title: 'Admin / Rolle', + canActivate: [permissionGuard], + data: { permissions: ['roles.read'] }, + loadComponent: () => + import('./features/admin/admin-role-detail.page').then( + (m) => m.AdminRoleDetailPageComponent, + ), + }, + { + path: 'audit-log', + title: 'Audit-Log', + canActivate: [permissionGuard], + data: { permissions: ['audit.read'] }, + loadComponent: () => + import('./features/audit/audit.page').then((m) => m.AuditPageComponent), + }, + { + path: 'admin/audit', + title: 'Admin / Audit', + canActivate: [permissionGuard], + data: { permissions: ['audit.read'] }, + loadComponent: () => + import('./features/audit/audit.page').then((m) => m.AuditPageComponent), + }, + { + path: '403', + title: 'Keine Berechtigung', + loadComponent: () => + import('./features/errors/forbidden.page').then((m) => m.ForbiddenPageComponent), + }, + { + path: 'fehler', + title: 'Fehler', + loadComponent: () => + import('./features/errors/error.page').then((m) => m.ErrorPageComponent), + }, + ...devRoutes, + { + path: '**', + title: 'Nicht gefunden', + loadComponent: () => + import('./features/errors/not-found.page').then((m) => m.NotFoundPageComponent), + }, + ], + }, +]; diff --git a/apps/frontend/src/app/app.scss b/apps/frontend/src/app/app.scss new file mode 100644 index 0000000..e69de29 diff --git a/apps/frontend/src/app/app.spec.ts b/apps/frontend/src/app/app.spec.ts new file mode 100644 index 0000000..09189ff --- /dev/null +++ b/apps/frontend/src/app/app.spec.ts @@ -0,0 +1,18 @@ +import { TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { App } from './app'; + +describe('App', () => { + it('renders the router outlet as application entry point', async () => { + await TestBed.configureTestingModule({ + imports: [App], + providers: [provideRouter([])], + }).compileComponents(); + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const element = fixture.nativeElement as HTMLElement; + expect(element.querySelector('router-outlet')).not.toBeNull(); + }); +}); diff --git a/apps/frontend/src/app/app.ts b/apps/frontend/src/app/app.ts new file mode 100644 index 0000000..2a070fc --- /dev/null +++ b/apps/frontend/src/app/app.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'app-root', + imports: [RouterOutlet], + template: '', +}) +export class App {} diff --git a/apps/frontend/src/app/core/auth.service.spec.ts b/apps/frontend/src/app/core/auth.service.spec.ts new file mode 100644 index 0000000..54971dc --- /dev/null +++ b/apps/frontend/src/app/core/auth.service.spec.ts @@ -0,0 +1,35 @@ +import { TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { ApiClientService, type UserDto } from '@boilerplate/api-client'; +import { AuthService } from './auth.service'; + +const user: UserDto = { + id: 'u1', + name: 'Ada', + email: 'ada@example.test', + active: true, + lastLoginAt: null, + settings: { tablePageSize: 20, sidebarExpanded: true }, + roles: [ + { + id: 'r1', + name: 'user', + protected: true, + permissions: [{ id: 'items.read', description: 'items.read' }], + }, + ], +}; + +describe('AuthService', () => { + it('derives permissions from roles for navigation decisions', () => { + TestBed.configureTestingModule({ + providers: [{ provide: ApiClientService, useValue: { me: () => of(user) } }], + }); + const service = TestBed.inject(AuthService); + + service.loadMe(); + + expect(service.has('items.read')).toBe(true); + expect(service.has('users.manage')).toBe(false); + }); +}); diff --git a/apps/frontend/src/app/core/auth.service.ts b/apps/frontend/src/app/core/auth.service.ts new file mode 100644 index 0000000..3f8798b --- /dev/null +++ b/apps/frontend/src/app/core/auth.service.ts @@ -0,0 +1,43 @@ +import { Injectable, computed, inject, signal } from '@angular/core'; +import type { Permission, UserDto } from '@boilerplate/api-client'; +import { ApiClientService } from '@boilerplate/api-client'; +import { catchError, finalize, of, tap } from 'rxjs'; +import type { Observable } from 'rxjs'; + +@Injectable({ providedIn: 'root' }) +export class AuthService { + private readonly api = inject(ApiClientService); + readonly user = signal(null); + readonly loaded = signal(false); + readonly permissions = computed( + () => + new Set( + (this.user()?.roles ?? []).flatMap((role) => + role.permissions.map((permission) => permission.id), + ), + ), + ); + + loadMe(): void { + this.ensureLoaded().subscribe(); + } + + ensureLoaded(): Observable { + if (this.loaded()) { + return of(this.user()); + } + + return this.api.me().pipe( + tap((user) => this.user.set(user)), + catchError(() => { + this.user.set(null); + return of(null); + }), + finalize(() => this.loaded.set(true)), + ); + } + + has(permission: Permission): boolean { + return this.permissions().has(permission); + } +} diff --git a/apps/frontend/src/app/core/csrf.interceptor.ts b/apps/frontend/src/app/core/csrf.interceptor.ts new file mode 100644 index 0000000..87c8648 --- /dev/null +++ b/apps/frontend/src/app/core/csrf.interceptor.ts @@ -0,0 +1,17 @@ +import type { HttpInterceptorFn } from '@angular/common/http'; + +const unsafeMethods = new Set(['POST', 'PUT', 'PATCH', 'DELETE']); + +export const csrfInterceptor: HttpInterceptorFn = (req, next) => { + if (!unsafeMethods.has(req.method)) { + return next(req); + } + const token = document.cookie + .split(';') + .map((entry) => entry.trim()) + .find((entry) => entry.startsWith('csrf_token=')) + ?.split('=')[1]; + return next( + token ? req.clone({ setHeaders: { 'X-CSRF-Token': decodeURIComponent(token) } }) : req, + ); +}; diff --git a/apps/frontend/src/app/core/dev-only.guard.ts b/apps/frontend/src/app/core/dev-only.guard.ts new file mode 100644 index 0000000..6ee6038 --- /dev/null +++ b/apps/frontend/src/app/core/dev-only.guard.ts @@ -0,0 +1,8 @@ +import { isDevMode } from '@angular/core'; +import type { CanMatchFn } from '@angular/router'; + +export function isDesignSystemRouteEnabled(devMode = isDevMode()): boolean { + return devMode; +} + +export const devOnlyGuard: CanMatchFn = () => isDesignSystemRouteEnabled(); diff --git a/apps/frontend/src/app/core/dev-routes.prod.ts b/apps/frontend/src/app/core/dev-routes.prod.ts new file mode 100644 index 0000000..8e84452 --- /dev/null +++ b/apps/frontend/src/app/core/dev-routes.prod.ts @@ -0,0 +1,3 @@ +import type { Routes } from '@angular/router'; + +export const devRoutes: Routes = []; diff --git a/apps/frontend/src/app/core/dev-routes.ts b/apps/frontend/src/app/core/dev-routes.ts new file mode 100644 index 0000000..7782159 --- /dev/null +++ b/apps/frontend/src/app/core/dev-routes.ts @@ -0,0 +1,12 @@ +import type { Routes } from '@angular/router'; +import { devOnlyGuard } from './dev-only.guard'; + +export const devRoutes: Routes = [ + { + path: 'dev/design-system', + title: 'Designsystem', + canMatch: [devOnlyGuard], + loadComponent: () => + import('../features/dev/design-system.page').then((m) => m.DesignSystemPageComponent), + }, +]; diff --git a/apps/frontend/src/app/core/notification.store.spec.ts b/apps/frontend/src/app/core/notification.store.spec.ts new file mode 100644 index 0000000..2f9cb1c --- /dev/null +++ b/apps/frontend/src/app/core/notification.store.spec.ts @@ -0,0 +1,139 @@ +import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { of, throwError } from 'rxjs'; +import { ApiClientService, type NotificationDto } from '@boilerplate/api-client'; +import { NOTIFICATION_POLL_INTERVAL_MS, NotificationStore } from './notification.store'; + +const unread: NotificationDto = { + id: 'n1', + type: 'system', + title: 'Wartung', + message: 'Heute Abend.', + link: '/', + metadata: null, + read: false, + readAt: null, + createdAt: '2026-07-16T08:00:00.000Z', +}; + +function setup(api: Partial = {}) { + const navigateByUrl = vi.fn(); + TestBed.configureTestingModule({ + providers: [ + NotificationStore, + { provide: NOTIFICATION_POLL_INTERVAL_MS, useValue: 60_000 }, + { provide: Router, useValue: { navigateByUrl } }, + { + provide: ApiClientService, + useValue: { + unreadNotificationCount: () => of({ count: 1 }), + notifications: () => + of({ + items: [unread], + total: 1, + page: 1, + pageSize: 20, + unreadCount: 1, + }), + markNotificationRead: () => + of({ ...unread, read: true, readAt: '2026-07-16T08:01:00.000Z' }), + markNotificationUnread: () => of({ ...unread, read: false, readAt: null }), + markAllNotificationsRead: () => of({ updated: 1 }), + deleteNotification: () => of(undefined), + ...api, + }, + }, + ], + }); + return { store: TestBed.inject(NotificationStore), navigateByUrl }; +} + +describe('NotificationStore', () => { + afterEach(() => TestBed.inject(NotificationStore).stopPolling()); + + it('updates state when a notification is marked as read', () => { + const { store } = setup(); + store.notifications.set([unread]); + store.unreadCount.set(1); + + store.markAsRead('n1'); + + expect(store.notifications()[0]?.read).toBe(true); + expect(store.unreadCount()).toBe(0); + }); + + it('deletes notifications and updates the badge count', () => { + const { store } = setup(); + store.notifications.set([unread]); + store.unreadCount.set(1); + store.total.set(1); + + store.delete('n1'); + + expect(store.notifications()).toEqual([]); + expect(store.unreadCount()).toBe(0); + expect(store.total()).toBe(0); + }); + + it('marks all notifications as read in the current state', () => { + const { store } = setup(); + store.notifications.set([unread]); + store.unreadCount.set(1); + + store.markAllAsRead(); + + expect(store.notifications()[0]?.read).toBe(true); + expect(store.unreadCount()).toBe(0); + }); + + it('navigates only internal links', () => { + const { store, navigateByUrl } = setup(); + store.openNotification({ ...unread, read: true, link: '/items/1' }); + store.openNotification({ ...unread, id: 'n2', read: true, link: 'https://example.com' }); + + expect(navigateByUrl).toHaveBeenCalledTimes(1); + expect(navigateByUrl).toHaveBeenCalledWith('/items/1'); + }); + + it('stops polling on logout and clears notification state', () => { + const count = vi.fn(() => of({ count: 3 })); + const { store } = setup({ unreadNotificationCount: count }); + store.notifications.set([unread]); + store.unreadCount.set(3); + + store.startPolling(); + store.stopPolling(); + + expect(store.notifications()).toEqual([]); + expect(store.unreadCount()).toBe(0); + }); + + it('does not poll while the tab is hidden', () => { + const count = vi.fn(() => of({ count: 1 })); + const { store } = setup({ unreadNotificationCount: count }); + Object.defineProperty(document, 'hidden', { configurable: true, value: true }); + + store.startPolling(); + document.dispatchEvent(new Event('visibilitychange')); + + expect(count).toHaveBeenCalledTimes(1); + }); + + it('stores API errors with request id', () => { + const { store } = setup({ + notifications: () => + throwError(() => ({ + error: { + status: 500, + code: 'INTERNAL_ERROR', + message: 'Fehler', + requestId: 'req-1', + }, + })), + }); + + store.load(); + + expect(store.error()?.requestId).toBe('req-1'); + }); +}); diff --git a/apps/frontend/src/app/core/notification.store.ts b/apps/frontend/src/app/core/notification.store.ts new file mode 100644 index 0000000..38242a5 --- /dev/null +++ b/apps/frontend/src/app/core/notification.store.ts @@ -0,0 +1,213 @@ +import { Injectable, InjectionToken, computed, inject, signal } from '@angular/core'; +import { Router } from '@angular/router'; +import { ApiClientService } from '@boilerplate/api-client'; +import type { + ApiErrorBody, + NotificationDto, + NotificationStatusFilter, +} from '@boilerplate/api-client'; +import type { Subscription } from 'rxjs'; +import { fromEvent, timer } from 'rxjs'; + +export const NOTIFICATION_POLL_INTERVAL_MS = new InjectionToken( + 'NOTIFICATION_POLL_INTERVAL_MS', + { factory: () => 60_000 }, +); + +@Injectable({ providedIn: 'root' }) +export class NotificationStore { + private readonly api = inject(ApiClientService); + private readonly router = inject(Router); + private readonly intervalMs = inject(NOTIFICATION_POLL_INTERVAL_MS); + private pollingSubscription: Subscription | null = null; + private visibilitySubscription: Subscription | null = null; + private unreadRequestActive = false; + private listRequestActive = false; + + readonly notifications = signal([]); + readonly unreadCount = signal(0); + readonly loading = signal(false); + readonly error = signal(null); + readonly currentFilter = signal('all'); + readonly page = signal(1); + readonly pageSize = signal(20); + readonly total = signal(0); + readonly unreadItems = computed(() => + this.notifications().filter((notification) => !notification.read), + ); + + startPolling(): void { + if (this.pollingSubscription) { + return; + } + this.refreshUnreadCount(); + this.pollingSubscription = timer(this.intervalMs, this.intervalMs).subscribe(() => { + if (!document.hidden) { + this.refreshUnreadCount(); + } + }); + this.visibilitySubscription = fromEvent(document, 'visibilitychange').subscribe(() => { + if (!document.hidden) { + this.refreshUnreadCount(); + } + }); + } + + stopPolling(): void { + this.pollingSubscription?.unsubscribe(); + this.visibilitySubscription?.unsubscribe(); + this.pollingSubscription = null; + this.visibilitySubscription = null; + this.unreadRequestActive = false; + this.listRequestActive = false; + this.notifications.set([]); + this.unreadCount.set(0); + this.error.set(null); + } + + load(filter = this.currentFilter(), page = this.page()): void { + if (this.listRequestActive) { + return; + } + this.listRequestActive = true; + this.loading.set(true); + this.error.set(null); + this.currentFilter.set(filter); + this.page.set(page); + this.api.notifications({ status: filter, page, pageSize: this.pageSize() }).subscribe({ + next: (result) => { + this.notifications.set(result.items); + this.total.set(result.total); + this.unreadCount.set(result.unreadCount); + }, + error: (error: { error?: ApiErrorBody }) => + this.error.set(error.error ?? this.genericError()), + complete: () => { + this.loading.set(false); + this.listRequestActive = false; + }, + }); + } + + refreshUnreadCount(): void { + if (this.unreadRequestActive) { + return; + } + this.unreadRequestActive = true; + this.api.unreadNotificationCount().subscribe({ + next: (result) => this.unreadCount.set(result.count), + error: () => undefined, + complete: () => { + this.unreadRequestActive = false; + }, + }); + } + + openPanel(): void { + this.refreshUnreadCount(); + this.load('all', 1); + } + + markAsRead(id: string): void { + this.api.markNotificationRead(id).subscribe({ + next: (updated) => this.replaceNotification(updated), + error: (error: { error?: ApiErrorBody }) => + this.error.set(error.error ?? this.genericError()), + }); + } + + markAsUnread(id: string): void { + this.api.markNotificationUnread(id).subscribe({ + next: (updated) => this.replaceNotification(updated), + error: (error: { error?: ApiErrorBody }) => + this.error.set(error.error ?? this.genericError()), + }); + } + + markAllAsRead(): void { + this.api.markAllNotificationsRead().subscribe({ + next: () => { + this.notifications.update((items) => + items.map((item) => ({ + ...item, + read: true, + readAt: item.readAt ?? new Date().toISOString(), + })), + ); + this.unreadCount.set(0); + }, + error: (error: { error?: ApiErrorBody }) => + this.error.set(error.error ?? this.genericError()), + }); + } + + delete(id: string): void { + this.api.deleteNotification(id).subscribe({ + next: () => { + const deleted = this.notifications().find((item) => item.id === id); + this.notifications.update((items) => items.filter((item) => item.id !== id)); + this.total.update((total) => Math.max(0, total - 1)); + if (deleted && !deleted.read) { + this.unreadCount.update((count) => Math.max(0, count - 1)); + } + }, + error: (error: { error?: ApiErrorBody }) => + this.error.set(error.error ?? this.genericError()), + }); + } + + openNotification(notification: NotificationDto): void { + if (notification.link && !this.isInternalLink(notification.link)) { + return; + } + const navigate = () => { + if (notification.link) { + void this.router.navigateByUrl(notification.link); + } + }; + if (notification.read) { + navigate(); + return; + } + this.api.markNotificationRead(notification.id).subscribe({ + next: (updated) => { + this.replaceNotification(updated); + navigate(); + }, + error: (error: { error?: ApiErrorBody }) => + this.error.set(error.error ?? this.genericError()), + }); + } + + isInternalLink(link: string | null): link is string { + if (!link) { + return false; + } + return ( + link.startsWith('/') && + !link.startsWith('//') && + !link.includes('\\') && + !/^[a-z][a-z0-9+.-]*:/i.test(link) && + !link.toLowerCase().includes('javascript:') + ); + } + + private replaceNotification(updated: NotificationDto): void { + const previous = this.notifications().find((item) => item.id === updated.id); + this.notifications.update((items) => + items.map((item) => (item.id === updated.id ? updated : item)), + ); + if (previous && previous.read !== updated.read) { + this.unreadCount.update((count) => (updated.read ? Math.max(0, count - 1) : count + 1)); + } + } + + private genericError(): ApiErrorBody { + return { + status: 0, + code: 'CLIENT_ERROR', + message: 'Benachrichtigungen konnten nicht geladen werden.', + requestId: '', + }; + } +} diff --git a/apps/frontend/src/app/core/permission.guard.ts b/apps/frontend/src/app/core/permission.guard.ts new file mode 100644 index 0000000..0c5dee2 --- /dev/null +++ b/apps/frontend/src/app/core/permission.guard.ts @@ -0,0 +1,21 @@ +import { inject } from '@angular/core'; +import { Router, type CanActivateFn } from '@angular/router'; +import type { Permission } from '@boilerplate/api-client'; +import { map } from 'rxjs'; +import { AuthService } from './auth.service'; + +export const permissionGuard: CanActivateFn = (route) => { + const auth = inject(AuthService); + const router = inject(Router); + const required = (route.data['permissions'] ?? []) as Permission[]; + return auth.ensureLoaded().pipe( + map((user) => { + if (!user) { + return router.createUrlTree(['/']); + } + return required.every((permission) => auth.has(permission)) + ? true + : router.createUrlTree(['/403']); + }), + ); +}; diff --git a/apps/frontend/src/app/core/title.strategy.ts b/apps/frontend/src/app/core/title.strategy.ts new file mode 100644 index 0000000..c1c97be --- /dev/null +++ b/apps/frontend/src/app/core/title.strategy.ts @@ -0,0 +1,15 @@ +import { Injectable, inject } from '@angular/core'; +import { Title } from '@angular/platform-browser'; +import { TitleStrategy, type RouterStateSnapshot } from '@angular/router'; + +@Injectable() +export class AppTitleStrategy extends TitleStrategy { + private readonly title = inject(Title); + + override updateTitle(snapshot: RouterStateSnapshot): void { + const title = this.buildTitle(snapshot); + this.title.setTitle(title ? `${title} | Business App` : 'Business App'); + } +} + +export const titleStrategyProvider = { provide: TitleStrategy, useClass: AppTitleStrategy }; diff --git a/apps/frontend/src/app/features/account/account-security.page.spec.ts b/apps/frontend/src/app/features/account/account-security.page.spec.ts new file mode 100644 index 0000000..d8b6710 --- /dev/null +++ b/apps/frontend/src/app/features/account/account-security.page.spec.ts @@ -0,0 +1,197 @@ +import { signal } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import type { SessionDto, UserDto } from '@boilerplate/api-client'; +import { ApiClientService } from '@boilerplate/api-client'; +import { of, throwError } from 'rxjs'; +import { AuthService } from '../../core/auth.service'; +import { AccountSecurityPageComponent } from './account-security.page'; + +const user: UserDto = { + id: 'u1', + name: 'Ada Lovelace', + email: 'ada@example.com', + active: true, + lastLoginAt: '2026-07-16T08:30:00.000Z', + settings: { tablePageSize: 20, sidebarExpanded: true }, + roles: [ + { + id: 'r1', + name: 'user', + description: 'Standardrolle', + system: true, + protected: true, + permissions: [ + { id: 'sessions.readOwn', description: 'Eigene Sessions anzeigen' }, + { id: 'items.read', description: 'Items anzeigen' }, + ], + }, + { + id: 'r2', + name: 'editor', + description: 'Editor', + system: false, + protected: false, + permissions: [ + { id: 'items.read', description: 'Items anzeigen' }, + { id: 'items.update', description: 'Items bearbeiten' }, + ], + }, + ], +}; + +const sessions: SessionDto[] = [ + { + id: 'current', + createdAt: '2026-07-16T08:00:00.000Z', + lastActivityAt: '2026-07-16T08:30:00.000Z', + userAgent: 'Firefox', + approximateIp: '192.0.2.10', + current: true, + revokedAt: null, + }, + { + id: 'other', + createdAt: '2026-07-15T08:00:00.000Z', + lastActivityAt: '2026-07-15T09:00:00.000Z', + userAgent: 'Chrome', + approximateIp: '192.0.2.11', + current: false, + revokedAt: null, + }, +]; + +describe('AccountSecurityPageComponent', () => { + it('shows account details, roles, deduplicated permissions and sessions', async () => { + await TestBed.configureTestingModule({ + imports: [AccountSecurityPageComponent], + providers: [ + { provide: AuthService, useValue: { user: signal(user) } }, + { + provide: ApiClientService, + useValue: { + sessions: () => of(sessions), + revokeSession: vi.fn(), + revokeOtherSessions: vi.fn(), + }, + }, + ], + }).compileComponents(); + + const fixture = TestBed.createComponent(AccountSecurityPageComponent); + fixture.detectChanges(); + const element = fixture.nativeElement as HTMLElement; + + expect(element.textContent).toContain('Ada Lovelace'); + expect(element.textContent).toContain('Identity Provider'); + expect(element.textContent).toContain('user - Systemrolle'); + expect(element.textContent).toContain('Aktuelle Session'); + expect(element.textContent).toContain('Weitere Session'); + expect(element.querySelectorAll('code').length).toBe(3); + }); + + it('revokes one other session and reloads sessions', async () => { + const sessionsSpy = vi.fn(() => of(sessions)); + const revokeSession = vi.fn(() => of(undefined)); + await TestBed.configureTestingModule({ + imports: [AccountSecurityPageComponent], + providers: [ + { provide: AuthService, useValue: { user: signal(user) } }, + { + provide: ApiClientService, + useValue: { + sessions: sessionsSpy, + revokeSession, + revokeOtherSessions: vi.fn(), + }, + }, + ], + }).compileComponents(); + + const fixture = TestBed.createComponent(AccountSecurityPageComponent); + fixture.detectChanges(); + + fixture.componentInstance.revoke('other'); + fixture.detectChanges(); + + expect(revokeSession).toHaveBeenCalledWith('other'); + expect(sessionsSpy).toHaveBeenCalledTimes(2); + }); + + it('revokes all other sessions and reloads sessions', async () => { + const sessionsSpy = vi.fn(() => of(sessions)); + const revokeOtherSessions = vi.fn(() => of(undefined)); + await TestBed.configureTestingModule({ + imports: [AccountSecurityPageComponent], + providers: [ + { provide: AuthService, useValue: { user: signal(user) } }, + { + provide: ApiClientService, + useValue: { + sessions: sessionsSpy, + revokeSession: vi.fn(), + revokeOtherSessions, + }, + }, + ], + }).compileComponents(); + + const fixture = TestBed.createComponent(AccountSecurityPageComponent); + fixture.detectChanges(); + + fixture.componentInstance.revokeOthers(); + fixture.detectChanges(); + + expect(revokeOtherSessions).toHaveBeenCalled(); + expect(sessionsSpy).toHaveBeenCalledTimes(2); + }); + + it('shows API errors with request id and an empty state for empty sessions', async () => { + await TestBed.configureTestingModule({ + imports: [AccountSecurityPageComponent], + providers: [ + { provide: AuthService, useValue: { user: signal(user) } }, + { + provide: ApiClientService, + useValue: { + sessions: () => + throwError(() => ({ + error: { + message: 'Sessions konnten nicht geladen werden.', + requestId: 'request-1', + }, + })), + revokeSession: vi.fn(), + revokeOtherSessions: vi.fn(), + }, + }, + ], + }).compileComponents(); + + const fixture = TestBed.createComponent(AccountSecurityPageComponent); + fixture.detectChanges(); + + expect((fixture.nativeElement as HTMLElement).textContent).toContain('request-1'); + + TestBed.resetTestingModule(); + await TestBed.configureTestingModule({ + imports: [AccountSecurityPageComponent], + providers: [ + { provide: AuthService, useValue: { user: signal(user) } }, + { + provide: ApiClientService, + useValue: { + sessions: () => of([]), + revokeSession: vi.fn(), + revokeOtherSessions: vi.fn(), + }, + }, + ], + }).compileComponents(); + + const emptyFixture = TestBed.createComponent(AccountSecurityPageComponent); + emptyFixture.detectChanges(); + expect((emptyFixture.nativeElement as HTMLElement).textContent).toContain( + 'Keine Sessions gefunden', + ); + }); +}); diff --git a/apps/frontend/src/app/features/account/account-security.page.ts b/apps/frontend/src/app/features/account/account-security.page.ts new file mode 100644 index 0000000..618c024 --- /dev/null +++ b/apps/frontend/src/app/features/account/account-security.page.ts @@ -0,0 +1,324 @@ +import { DatePipe } from '@angular/common'; +import { Component, computed, inject, signal } from '@angular/core'; +import type { ApiErrorBody, Permission, SessionDto } from '@boilerplate/api-client'; +import { ApiClientService } from '@boilerplate/api-client'; +import { AuthService } from '../../core/auth.service'; +import { adminPermissionGroups } from '../admin/admin-permissions'; +import { UiEmptyStateComponent, UiStatusBadgeComponent } from '../../shared/ui'; + +interface PermissionGroupView { + title: string; + permissions: { id: Permission; label: string }[]; +} + +interface AccountSecurityError { + message: string; + requestId: string | undefined; +} + +@Component({ + standalone: true, + imports: [DatePipe, UiEmptyStateComponent, UiStatusBadgeComponent], + template: ` +
+
+

Account-Sicherheit

+

Uebersicht ueber Ihren Account, Rollen, Berechtigungen und aktive Sessions.

+
+
+ + @if (auth.user(); as user) { +
+
+

Account

+
+
Name
+
{{ user.name }}
+
E-Mail
+
{{ user.email || 'Nicht gesetzt' }}
+
Status
+
+ +
+
Letzte Anmeldung
+
+ {{ user.lastLoginAt ? (user.lastLoginAt | date: 'short') : 'Noch nicht bekannt' }} +
+
+
+ +
+

Identity Provider

+

+ Name und E-Mail werden zentral vom Identity Provider verwaltet. Aenderungen erfolgen + nicht in dieser Anwendung. +

+

OIDC-Session mit serverseitig gespeicherten Tokens.

+
+
+ +
+

Rollen

+ @if (user.roles.length > 0) { +
+ @for (role of user.roles; track role.id) { + + {{ role.name }}{{ role.system ? ' - Systemrolle' : '' }} + + } +
+ } @else { +

Keine Rollen zugewiesen.

+ } +
+ +
+

Effektive Berechtigungen

+ @if (permissionGroups().length > 0) { +
+ @for (group of permissionGroups(); track group.title) { +
+

{{ group.title }}

+
    + @for (permission of group.permissions; track permission.id) { +
  • + {{ permission.id }} + {{ permission.label }} +
  • + } +
+
+ } +
+ } @else { +

Keine Berechtigungen ueber Rollen zugewiesen.

+ } +
+ +
+
+
+

Aktive Sessions

+

Beenden Sie nicht mehr benoetigte Browser-Sessions.

+
+ +
+ + @if (loading()) { +

Sessions werden geladen.

+ } @else if (error(); as currentError) { +

+ {{ currentError.message }} + @if (currentError.requestId) { + Request-ID: {{ currentError.requestId }} + } +

+ } @else if (sessions().length === 0) { + + } @else { +
+ @for (session of sessions(); track session.id) { +
+
+ {{ session.current ? 'Aktuelle Session' : 'Weitere Session' }} + Angemeldet: {{ session.createdAt | date: 'short' }} + Letzte Aktivitaet: {{ session.lastActivityAt | date: 'short' }} + {{ session.userAgent || 'Unbekannter Browser' }} + {{ session.approximateIp || 'IP unbekannt' }} +
+ @if (session.current) { +

Diese Session beenden Sie ueber Abmelden.

+ } @else if (!session.revokedAt) { + + } +
+ } +
+ } +
+ } + `, + styles: [ + ` + .security-summary { + margin-bottom: var(--space-5); + } + .security-summary h2, + .security-section h2, + .permission-group h3 { + margin-top: 0; + } + .security-list { + display: grid; + grid-template-columns: minmax(7rem, auto) 1fr; + gap: var(--space-3) var(--space-4); + margin: 0; + } + .security-list dt { + color: var(--color-text-muted); + } + .security-list dd { + margin: 0; + } + .security-section { + margin-top: var(--space-5); + } + .permission-groups, + .session-list { + display: grid; + gap: var(--space-4); + } + .permission-group { + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: var(--space-4); + } + .permission-group ul { + display: grid; + gap: var(--space-3); + list-style: none; + margin: 0; + padding: 0; + } + .permission-group li { + display: grid; + gap: var(--space-1); + } + code { + color: var(--color-text-primary); + font-size: var(--font-size-sm); + } + .section-heading { + display: grid; + gap: var(--space-4); + margin-bottom: var(--space-5); + } + .section-heading h2, + .section-heading p { + margin-bottom: 0; + } + .session-card { + display: grid; + gap: var(--space-4); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: var(--space-4); + } + .session-card.current { + border-left: 4px solid var(--color-primary); + background: var(--color-primary-subtle); + } + .session-card__body { + display: grid; + gap: var(--space-2); + } + .session-card__body span { + color: var(--color-text-secondary); + } + .ui-notice small { + display: block; + margin-top: var(--space-2); + } + @media (min-width: 48rem) { + .security-summary { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .section-heading, + .session-card { + grid-template-columns: 1fr auto; + align-items: start; + } + .permission-groups { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } + `, + ], +}) +export class AccountSecurityPageComponent { + readonly auth = inject(AuthService); + private readonly api = inject(ApiClientService); + readonly sessions = signal([]); + readonly loading = signal(false); + readonly error = signal(null); + readonly hasOtherSessions = computed(() => + this.sessions().some((session) => !session.current && !session.revokedAt), + ); + readonly permissionGroups = computed(() => this.buildPermissionGroups()); + + constructor() { + this.loadSessions(); + } + + loadSessions(): void { + this.loading.set(true); + this.error.set(null); + this.api.sessions().subscribe({ + next: (sessions) => this.sessions.set(sessions.filter((session) => !session.revokedAt)), + error: (err: { error?: ApiErrorBody }) => { + this.error.set({ + message: err.error?.message ?? 'Sessions konnten nicht geladen werden.', + requestId: err.error?.requestId, + }); + this.loading.set(false); + }, + complete: () => this.loading.set(false), + }); + } + + revoke(id: string): void { + this.api.revokeSession(id).subscribe({ + next: () => this.loadSessions(), + error: (err: { error?: ApiErrorBody }) => + this.error.set({ + message: err.error?.message ?? 'Session konnte nicht beendet werden.', + requestId: err.error?.requestId, + }), + }); + } + + revokeOthers(): void { + this.api.revokeOtherSessions().subscribe({ + next: () => this.loadSessions(), + error: (err: { error?: ApiErrorBody }) => + this.error.set({ + message: err.error?.message ?? 'Sessions konnten nicht beendet werden.', + requestId: err.error?.requestId, + }), + }); + } + + private buildPermissionGroups(): PermissionGroupView[] { + const userPermissions = new Set( + (this.auth.user()?.roles ?? []).flatMap((role) => + role.permissions.map((permission) => permission.id), + ), + ); + + return adminPermissionGroups + .map((group) => ({ + title: group.title, + permissions: group.permissions.filter((permission) => userPermissions.has(permission.id)), + })) + .filter((group) => group.permissions.length > 0); + } +} diff --git a/apps/frontend/src/app/features/admin/admin-permissions.ts b/apps/frontend/src/app/features/admin/admin-permissions.ts new file mode 100644 index 0000000..e7123c3 --- /dev/null +++ b/apps/frontend/src/app/features/admin/admin-permissions.ts @@ -0,0 +1,57 @@ +import type { Permission } from '@boilerplate/api-client'; + +export interface PermissionDefinition { + id: Permission; + label: string; +} + +export interface PermissionGroup { + title: string; + permissions: PermissionDefinition[]; +} + +export const adminPermissionGroups: PermissionGroup[] = [ + { + title: 'Items', + permissions: [ + { id: 'items.read', label: 'Items anzeigen' }, + { id: 'items.create', label: 'Items anlegen' }, + { id: 'items.update', label: 'Items bearbeiten' }, + { id: 'items.delete', label: 'Items loeschen' }, + ], + }, + { + title: 'Benutzer', + permissions: [ + { id: 'users.read', label: 'Benutzer anzeigen' }, + { id: 'users.manage', label: 'Benutzer aktivieren, deaktivieren und Rollen verwalten' }, + ], + }, + { + title: 'Rollen', + permissions: [ + { id: 'roles.read', label: 'Rollen und Permissions anzeigen' }, + { id: 'roles.manage', label: 'Rollen anlegen, bearbeiten und loeschen' }, + ], + }, + { + title: 'Sessions', + permissions: [ + { id: 'sessions.readOwn', label: 'Eigene Sessions anzeigen' }, + { id: 'sessions.revokeOwn', label: 'Eigene Sessions beenden' }, + { id: 'sessions.manage', label: 'Sessions anderer Benutzer verwalten' }, + ], + }, + { + title: 'Audit', + permissions: [{ id: 'audit.read', label: 'Administratives Audit-Log anzeigen' }], + }, + { + title: 'Benachrichtigungen', + permissions: [ + { id: 'notifications.readOwn', label: 'Eigene Benachrichtigungen lesen' }, + { id: 'notifications.updateOwn', label: 'Eigene Benachrichtigungen verwalten' }, + { id: 'notifications.manage', label: 'Benachrichtigungen administrativ erstellen' }, + ], + }, +]; diff --git a/apps/frontend/src/app/features/admin/admin-role-detail.page.spec.ts b/apps/frontend/src/app/features/admin/admin-role-detail.page.spec.ts new file mode 100644 index 0000000..03a994d --- /dev/null +++ b/apps/frontend/src/app/features/admin/admin-role-detail.page.spec.ts @@ -0,0 +1,34 @@ +import { TestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ApiClientService } from '@boilerplate/api-client'; +import { AdminRoleDetailPageComponent } from './admin-role-detail.page'; +import { adminPermissionGroups } from './admin-permissions'; + +describe('AdminRoleDetailPageComponent', () => { + it('groups permissions for the role editor', async () => { + await TestBed.configureTestingModule({ + imports: [AdminRoleDetailPageComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { snapshot: { paramMap: { get: () => 'new' } } }, + }, + { + provide: Router, + useValue: { navigate: vi.fn() }, + }, + { + provide: ApiClientService, + useValue: {}, + }, + ], + }).compileComponents(); + const fixture = TestBed.createComponent(AdminRoleDetailPageComponent); + fixture.detectChanges(); + const element = fixture.nativeElement as HTMLElement; + + expect(adminPermissionGroups.some((group) => group.title === 'Benutzer')).toBe(true); + expect(element.textContent).toContain('Benutzer aktivieren'); + expect(element.textContent).toContain('notifications.manage'); + }); +}); diff --git a/apps/frontend/src/app/features/admin/admin-role-detail.page.ts b/apps/frontend/src/app/features/admin/admin-role-detail.page.ts new file mode 100644 index 0000000..acfab26 --- /dev/null +++ b/apps/frontend/src/app/features/admin/admin-role-detail.page.ts @@ -0,0 +1,274 @@ +import { Component, computed, inject, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { + ApiClientService, + type ApiErrorBody, + type Permission, + type RoleDto, +} from '@boilerplate/api-client'; +import { adminPermissionGroups } from './admin-permissions'; + +@Component({ + standalone: true, + imports: [FormsModule, RouterLink], + template: ` + Zurueck zur Rollenliste + + @if (error(); as currentError) { +
+ {{ messageFor(currentError) }} + @if (currentError.requestId) { + Request-ID: {{ currentError.requestId }} + } +
+ } + + @if (!loading()) { +
+
+

{{ isNew() ? 'Rolle anlegen' : 'Rolle bearbeiten' }}

+ @if (role()?.system) { + Systemrolle + } + + +
+ +
+

Permissions

+
+ @for (group of groups; track group.title) { +
+ {{ group.title }} + @for (permission of group.permissions; track permission.id) { + + } +
+ } +
+
+ +
+ + @if (role(); as currentRole) { + @if (!currentRole.system) { + + } + } +
+
+ } @else { +
Rolle wird geladen.
+ } + `, + styles: [ + ` + a { + display: inline-flex; + margin-bottom: 16px; + color: var(--color-primary-hover); + } + .notice, + .editor { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 8px; + padding: 16px; + } + .notice.error { + border-color: var(--color-danger); + color: var(--color-danger); + margin-bottom: 16px; + } + .editor, + section, + .groups, + fieldset, + .actions { + display: grid; + gap: 14px; + } + h2, + h3 { + margin: 0; + } + label { + display: grid; + gap: 6px; + } + input, + textarea, + button { + min-height: 44px; + } + input, + textarea { + border: 1px solid var(--color-border); + border-radius: 6px; + padding: 8px 10px; + } + textarea { + min-height: 96px; + resize: vertical; + } + button { + border: 0; + border-radius: 6px; + background: var(--color-primary); + color: var(--color-surface); + padding: 0 14px; + } + button.danger { + background: var(--color-danger); + } + button:disabled { + opacity: 0.55; + } + fieldset { + border: 1px solid var(--color-border); + border-radius: 8px; + padding: 12px; + } + fieldset label { + grid-template-columns: 24px 1fr; + align-items: start; + } + code { + display: block; + margin-bottom: 2px; + } + .system { + width: fit-content; + border: 1px solid var(--color-border); + border-radius: 999px; + padding: 4px 8px; + color: var(--color-text-secondary); + } + @media (min-width: 900px) { + .groups { + grid-template-columns: 1fr 1fr; + } + .actions { + grid-template-columns: auto auto; + justify-content: start; + } + } + `, + ], +}) +export class AdminRoleDetailPageComponent { + private readonly api = inject(ApiClientService); + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + readonly role = signal(null); + readonly loading = signal(false); + readonly error = signal(null); + readonly selectedPermissions = signal(new Set()); + readonly groups = adminPermissionGroups; + readonly isNew = computed(() => this.route.snapshot.paramMap.get('id') === 'new'); + name = ''; + description = ''; + + constructor() { + if (!this.isNew()) this.load(); + } + + load(): void { + const id = this.route.snapshot.paramMap.get('id'); + if (!id) return; + this.loading.set(true); + this.error.set(null); + this.api.adminRole(id).subscribe({ + next: (role) => { + this.role.set(role); + this.name = role.name; + this.description = role.description; + this.selectedPermissions.set(new Set(role.permissions.map((permission) => permission.id))); + this.loading.set(false); + }, + error: (error: { error?: ApiErrorBody }) => { + this.error.set(error.error ?? this.genericError()); + this.loading.set(false); + }, + }); + } + + toggle(permission: Permission): void { + if (this.role()?.name === 'admin') return; + const next = new Set(this.selectedPermissions()); + if (next.has(permission)) next.delete(permission); + else next.add(permission); + this.selectedPermissions.set(next); + } + + save(): void { + const body = { + name: this.name, + description: this.description, + permissions: Array.from(this.selectedPermissions()), + }; + const request = this.isNew() + ? this.api.createAdminRole(body) + : this.api.updateAdminRole(this.role()?.id ?? '', body); + request.subscribe({ + next: (role) => { + void this.router.navigate(['/admin/roles', role.id]); + }, + error: (error: { error?: ApiErrorBody }) => + this.error.set(error.error ?? this.genericError()), + }); + } + + delete(role: RoleDto): void { + if (!confirm('Rolle wirklich loeschen?')) return; + this.api.deleteAdminRole(role.id).subscribe({ + next: () => { + void this.router.navigate(['/admin/roles']); + }, + error: (error: { error?: ApiErrorBody }) => + this.error.set(error.error ?? this.genericError()), + }); + } + + messageFor(error: ApiErrorBody): string { + if (error.code === 'ROLE_STILL_ASSIGNED') return 'Diese Rolle ist noch Benutzern zugewiesen.'; + if (error.code === 'SYSTEM_ROLE_PROTECTED') return 'Diese Systemrolle ist geschuetzt.'; + if (error.code === 'LAST_ACTIVE_ADMIN_REQUIRED') { + return 'Mindestens ein aktiver Administrator muss erhalten bleiben.'; + } + return error.message; + } + + private genericError(): ApiErrorBody { + return { + status: 0, + code: 'UNKNOWN', + message: 'Die Rolle konnte nicht verarbeitet werden.', + requestId: '', + }; + } +} diff --git a/apps/frontend/src/app/features/admin/admin-roles.page.ts b/apps/frontend/src/app/features/admin/admin-roles.page.ts new file mode 100644 index 0000000..9551a5f --- /dev/null +++ b/apps/frontend/src/app/features/admin/admin-roles.page.ts @@ -0,0 +1,154 @@ +import { Component, inject, signal } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { ApiClientService, type ApiErrorBody, type RoleDto } from '@boilerplate/api-client'; + +@Component({ + standalone: true, + imports: [RouterLink], + template: ` + + + @if (error(); as currentError) { +
+ {{ currentError.message }} + @if (currentError.requestId) { + Request-ID: {{ currentError.requestId }} + } +
+ } + + @if (loading()) { +
Rollen werden geladen.
+ } @else if (roles().length === 0) { +
Keine Rollen vorhanden.
+ } @else { +
+ @for (role of roles(); track role.id) { +
+
+ + {{ role.name }} + @if (role.system) { + Systemrolle + } + + {{ role.description || 'Keine Beschreibung' }} +
+
+
+
Benutzer
+
{{ role.userCount ?? role.users?.length ?? 0 }}
+
+
+
Permissions
+
{{ role.permissions.length }}
+
+
+ Bearbeiten +
+ } +
+ } + `, + styles: [ + ` + .toolbar { + display: flex; + justify-content: flex-end; + margin-bottom: 16px; + } + .notice, + article { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 8px; + padding: 16px; + } + .notice.error { + border-color: var(--color-danger); + color: var(--color-danger); + } + .list { + display: grid; + gap: 12px; + } + article { + display: grid; + gap: 12px; + } + .button { + min-height: 44px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 6px; + background: var(--color-primary); + color: var(--color-surface); + padding: 0 14px; + text-decoration: none; + } + .button.secondary { + background: var(--color-text-secondary); + } + dl { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + margin: 0; + } + dt, + span, + small { + color: var(--color-text-muted); + } + dd { + margin: 0; + } + small { + margin-left: 6px; + } + @media (min-width: 900px) { + article { + grid-template-columns: 1fr 220px auto; + align-items: center; + } + } + `, + ], +}) +export class AdminRolesPageComponent { + private readonly api = inject(ApiClientService); + readonly roles = signal([]); + readonly loading = signal(false); + readonly error = signal(null); + + constructor() { + this.load(); + } + + load(): void { + this.loading.set(true); + this.error.set(null); + this.api.adminRoles().subscribe({ + next: (roles) => { + this.roles.set(roles); + this.loading.set(false); + }, + error: (error: { error?: ApiErrorBody }) => { + this.error.set(error.error ?? this.genericError()); + this.loading.set(false); + }, + }); + } + + private genericError(): ApiErrorBody { + return { + status: 0, + code: 'UNKNOWN', + message: 'Rollen konnten nicht geladen werden.', + requestId: '', + }; + } +} diff --git a/apps/frontend/src/app/features/admin/admin-user-detail.page.ts b/apps/frontend/src/app/features/admin/admin-user-detail.page.ts new file mode 100644 index 0000000..8101797 --- /dev/null +++ b/apps/frontend/src/app/features/admin/admin-user-detail.page.ts @@ -0,0 +1,368 @@ +import { Component, inject, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, RouterLink } from '@angular/router'; +import { + ApiClientService, + type AdminSessionDto, + type AdminUserDetailDto, + type ApiErrorBody, + type RoleDto, +} from '@boilerplate/api-client'; + +@Component({ + standalone: true, + imports: [FormsModule, RouterLink], + template: ` + Zurueck zur Benutzerliste + + @if (error(); as currentError) { +
+ {{ messageFor(currentError) }} + @if (currentError.requestId) { + Request-ID: {{ currentError.requestId }} + } +
+ } + + @if (user(); as currentUser) { +
+
+

{{ currentUser.name }}

+

{{ currentUser.email || 'Keine E-Mail' }}

+

{{ currentUser.active ? 'Aktiv' : 'Deaktiviert' }}

+
+
+ @if (currentUser.active) { + + } @else { + + } + +
+
+ +
+
+

Rollen

+
+ @for (role of currentUser.roles; track role.id) { + + {{ role.name }} + + + } +
+
+ + +
+
+ +
+

Effektive Permissions

+
+ @for (permission of currentUser.effectivePermissions; track permission) { + {{ permission }} + } +
+
+
+ +
+

Aktive Sessions

+ @if (currentUser.sessions.length === 0) { +
Keine aktiven Sessions.
+ } @else { +
+ @for (session of currentUser.sessions; track session.id) { +
+
+ {{ session.current ? 'Aktuelle Session' : 'Session' }} + {{ session.userAgent || 'Unbekannter Browser' }} + {{ session.approximateIp || 'Keine IP' }} +
+
+
+
Aktivitaet
+
{{ session.lastActivityAt }}
+
+
+
Ablauf
+
{{ session.expiresAt }}
+
+
+ +
+ } +
+ } +
+ } @else if (loading()) { +
Benutzer wird geladen.
+ } + `, + styles: [ + ` + a { + display: inline-flex; + margin-bottom: 16px; + color: var(--color-primary-hover); + } + .notice, + .hero, + article { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 8px; + padding: 16px; + } + .notice.error { + border-color: var(--color-danger); + color: var(--color-danger); + margin-bottom: 16px; + } + .hero { + display: grid; + gap: 16px; + margin-bottom: 16px; + } + .hero.inactive { + border-left: 4px solid var(--color-border-strong); + } + h2, + h3, + p { + margin: 0; + } + h3 { + margin-bottom: 12px; + } + .actions, + .inline, + .grid, + .list, + article, + dl, + .permission-list { + display: grid; + gap: 12px; + } + button, + select { + min-height: 44px; + } + button { + border: 0; + border-radius: 6px; + background: var(--color-primary); + color: var(--color-surface); + padding: 0 14px; + } + button.secondary { + background: var(--color-text-secondary); + } + button.danger { + background: var(--color-danger); + } + button:disabled { + opacity: 0.55; + } + select { + border: 1px solid var(--color-border); + border-radius: 6px; + padding: 0 10px; + } + .chips, + .permission-list { + display: flex; + flex-wrap: wrap; + gap: 8px; + } + .chips span { + display: inline-flex; + align-items: center; + gap: 8px; + border: 1px solid var(--color-border); + border-radius: 999px; + padding: 4px 6px 4px 10px; + } + .chips button { + min-height: 34px; + background: var(--color-danger); + } + code { + border: 1px solid var(--color-border); + border-radius: 6px; + background: var(--color-background); + padding: 6px 8px; + } + dl { + margin: 0; + } + dt { + color: var(--color-text-muted); + font-size: 0.88rem; + } + dd { + margin: 0; + } + @media (min-width: 900px) { + .hero, + .list article { + grid-template-columns: 1fr auto; + align-items: center; + } + .actions, + .inline { + grid-template-columns: auto auto; + } + .grid { + grid-template-columns: 1fr 1fr; + margin-bottom: 16px; + } + .list article { + grid-template-columns: 1.2fr 1fr auto; + } + } + `, + ], +}) +export class AdminUserDetailPageComponent { + private readonly api = inject(ApiClientService); + private readonly route = inject(ActivatedRoute); + readonly user = signal(null); + readonly roles = signal([]); + readonly loading = signal(false); + readonly error = signal(null); + selectedRoleId = ''; + + constructor() { + this.api.adminRoles().subscribe((roles) => this.roles.set(roles)); + this.load(); + } + + load(): void { + const id = this.route.snapshot.paramMap.get('id'); + if (!id) return; + this.loading.set(true); + this.error.set(null); + this.api.adminUser(id).subscribe({ + next: (user) => { + this.user.set(user); + this.selectedRoleId = ''; + this.loading.set(false); + }, + error: (error: { error?: ApiErrorBody }) => { + this.error.set(error.error ?? this.genericError()); + this.loading.set(false); + }, + }); + } + + activate(id: string): void { + this.api.activateAdminUser(id).subscribe({ + next: () => this.load(), + error: (error: unknown) => this.handleError(error), + }); + } + + deactivate(id: string): void { + if (!confirm('Benutzer wirklich deaktivieren und alle Sessions beenden?')) return; + this.api.deactivateAdminUser(id).subscribe({ + next: () => this.load(), + error: (error: unknown) => this.handleError(error), + }); + } + + assignRole(userId: string): void { + if (!this.selectedRoleId) return; + this.api.assignAdminUserRole(userId, this.selectedRoleId).subscribe({ + next: () => this.load(), + error: (error: unknown) => this.handleError(error), + }); + } + + removeRole(userId: string, roleId: string, roleName: string): void { + if (roleName === 'admin' && !confirm('Adminrolle wirklich entfernen?')) return; + this.api.removeAdminUserRole(userId, roleId).subscribe({ + next: () => this.load(), + error: (error: unknown) => this.handleError(error), + }); + } + + revokeSession(userId: string, session: AdminSessionDto): void { + this.api.revokeAdminUserSession(userId, session.id).subscribe({ + next: () => this.load(), + error: (error: unknown) => this.handleError(error), + }); + } + + revokeAllSessions(userId: string): void { + if (!confirm('Alle Sessions dieses Benutzers beenden?')) return; + this.api.revokeAdminUserSessions(userId).subscribe({ + next: () => this.load(), + error: (error: unknown) => this.handleError(error), + }); + } + + availableRoles(user: AdminUserDetailDto): RoleDto[] { + const assigned = new Set(user.roles.map((role) => role.id)); + return this.roles().filter((role) => !assigned.has(role.id)); + } + + messageFor(error: ApiErrorBody): string { + if (error.code === 'LAST_ACTIVE_ADMIN_REQUIRED') { + return 'Dieser Benutzer ist der letzte aktive Administrator und kann nicht entmachtet werden.'; + } + return error.message; + } + + private handleError(error: unknown): void { + this.error.set(this.extractError(error)); + } + + private extractError(error: unknown): ApiErrorBody { + if (typeof error === 'object' && error !== null && 'error' in error) { + const body = (error as { error?: unknown }).error; + if (this.isApiErrorBody(body)) return body; + } + return this.genericError(); + } + + private isApiErrorBody(value: unknown): value is ApiErrorBody { + return ( + typeof value === 'object' && + value !== null && + 'message' in value && + 'code' in value && + 'status' in value && + 'requestId' in value + ); + } + + private genericError(): ApiErrorBody { + return { + status: 0, + code: 'UNKNOWN', + message: 'Die Aktion konnte nicht ausgefuehrt werden.', + requestId: '', + }; + } +} diff --git a/apps/frontend/src/app/features/admin/admin-users.page.spec.ts b/apps/frontend/src/app/features/admin/admin-users.page.spec.ts new file mode 100644 index 0000000..3541ffe --- /dev/null +++ b/apps/frontend/src/app/features/admin/admin-users.page.spec.ts @@ -0,0 +1,46 @@ +import { TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { of } from 'rxjs'; +import { ApiClientService } from '@boilerplate/api-client'; +import { AdminUsersPageComponent } from './admin-users.page'; + +describe('AdminUsersPageComponent', () => { + it('loads users and marks inactive accounts clearly', async () => { + await TestBed.configureTestingModule({ + imports: [AdminUsersPageComponent], + providers: [ + provideRouter([]), + { + provide: ApiClientService, + useValue: { + adminRoles: () => of([]), + adminUsers: () => + of({ + items: [ + { + id: 'user-1', + name: 'Max Mustermann', + email: 'max@example.com', + active: false, + roles: [{ id: 'role-user', name: 'user', system: true }], + lastLoginAt: null, + createdAt: '2026-07-16T08:00:00.000Z', + activeSessionCount: 0, + }, + ], + total: 1, + page: 1, + pageSize: 25, + }), + }, + }, + ], + }).compileComponents(); + const fixture = TestBed.createComponent(AdminUsersPageComponent); + fixture.detectChanges(); + const element = fixture.nativeElement as HTMLElement; + + expect(element.textContent).toContain('Max Mustermann'); + expect(element.querySelector('article.inactive')).not.toBeNull(); + }); +}); diff --git a/apps/frontend/src/app/features/admin/admin-users.page.ts b/apps/frontend/src/app/features/admin/admin-users.page.ts new file mode 100644 index 0000000..18a78e5 --- /dev/null +++ b/apps/frontend/src/app/features/admin/admin-users.page.ts @@ -0,0 +1,270 @@ +import { Component, inject, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import { + ApiClientService, + type AdminUserListItemDto, + type ApiErrorBody, + type RoleDto, +} from '@boilerplate/api-client'; + +type ActiveFilter = 'all' | 'active' | 'inactive'; +type UserSort = 'name' | 'email' | 'lastLoginAt' | 'createdAt'; + +@Component({ + standalone: true, + imports: [FormsModule, RouterLink], + template: ` +
+ + + + + +
+ + @if (error(); as currentError) { +
+ {{ currentError.message }} + @if (currentError.requestId) { + Request-ID: {{ currentError.requestId }} + } +
+ } + + @if (loading()) { +
Benutzer werden geladen.
+ } @else if (users().length === 0) { +
Keine Benutzer gefunden.
+ } @else { +
+ @for (user of users(); track user.id) { +
+
+ {{ user.name }} + {{ user.email || 'Keine E-Mail' }} + {{ user.active ? 'Aktiv' : 'Deaktiviert' }} +
+
+ @for (role of user.roles; track role.id) { + {{ role.name }} + } +
+
+
+
Sessions
+
{{ user.activeSessionCount }}
+
+
+
Letzter Login
+
{{ user.lastLoginAt || 'nie' }}
+
+
+ Details +
+ } +
+ + } + `, + styles: [ + ` + .toolbar { + display: grid; + gap: 12px; + margin-bottom: 16px; + } + label, + .summary, + dl { + display: grid; + gap: 6px; + } + input, + select, + button, + .button { + min-height: 44px; + } + input, + select { + border: 1px solid var(--color-border); + border-radius: 6px; + padding: 0 10px; + } + button, + .button { + border: 0; + border-radius: 6px; + background: var(--color-primary); + color: var(--color-surface); + padding: 0 14px; + } + .button { + display: inline-flex; + align-items: center; + justify-content: center; + text-decoration: none; + } + .notice, + article { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 8px; + padding: 16px; + } + .notice.error { + border-color: var(--color-danger); + color: var(--color-danger); + } + .list { + display: grid; + gap: 12px; + } + article { + display: grid; + gap: 12px; + } + article.inactive { + border-left: 4px solid var(--color-border-strong); + } + .chips { + display: flex; + flex-wrap: wrap; + gap: 6px; + } + .chips span { + border: 1px solid var(--color-border); + border-radius: 999px; + padding: 4px 8px; + background: var(--color-background); + } + dl { + margin: 0; + } + dt { + color: var(--color-text-muted); + font-size: 0.88rem; + } + dd { + margin: 0; + } + .pagination { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-top: 16px; + } + @media (min-width: 900px) { + .toolbar { + grid-template-columns: 2fr 1fr 1fr 1fr auto; + align-items: end; + } + article { + grid-template-columns: 1.4fr 1fr 1.2fr auto; + align-items: center; + } + } + `, + ], +}) +export class AdminUsersPageComponent { + private readonly api = inject(ApiClientService); + readonly users = signal([]); + readonly roles = signal([]); + readonly loading = signal(false); + readonly error = signal(null); + search = ''; + active: ActiveFilter = 'all'; + roleId = ''; + sort: UserSort = 'name'; + page = 1; + pageSize = 25; + total = 0; + + constructor() { + this.api.adminRoles().subscribe((roles) => this.roles.set(roles)); + this.load(); + } + + load(): void { + this.loading.set(true); + this.error.set(null); + this.api + .adminUsers({ + search: this.search, + active: this.active, + roleId: this.roleId, + sort: this.sort, + page: this.page, + pageSize: this.pageSize, + }) + .subscribe({ + next: (page) => { + this.users.set(page.items); + this.total = page.total; + this.page = page.page; + this.pageSize = page.pageSize; + this.loading.set(false); + }, + error: (error: { error?: ApiErrorBody }) => { + this.error.set(error.error ?? this.genericError()); + this.loading.set(false); + }, + }); + } + + previous(): void { + this.page -= 1; + this.load(); + } + + next(): void { + this.page += 1; + this.load(); + } + + totalPages(): number { + return Math.max(1, Math.ceil(this.total / this.pageSize)); + } + + private genericError(): ApiErrorBody { + return { + status: 0, + code: 'UNKNOWN', + message: 'Benutzer konnten nicht geladen werden.', + requestId: '', + }; + } +} diff --git a/apps/frontend/src/app/features/audit/audit.page.ts b/apps/frontend/src/app/features/audit/audit.page.ts new file mode 100644 index 0000000..3b8dab8 --- /dev/null +++ b/apps/frontend/src/app/features/audit/audit.page.ts @@ -0,0 +1,39 @@ +import { Component, inject, signal } from '@angular/core'; +import { ApiClientService, type AuditLogDto } from '@boilerplate/api-client'; + +@Component({ + standalone: true, + template: ` +
+ @for (entry of entries(); track entry.id) { +
+ {{ labels[entry.action] || entry.action }} + {{ entry.createdAt }} · Objekt: {{ entry.targetType }} {{ entry.targetId }} + Request-ID: {{ entry.requestId }} +
+ } +
+ `, +}) +export class AuditPageComponent { + private readonly api = inject(ApiClientService); + readonly entries = signal([]); + readonly labels: Record = { + USER_ACTIVATED: 'Benutzer aktiviert', + USER_DEACTIVATED: 'Benutzer deaktiviert', + USER_ROLE_ASSIGNED: 'Rolle zugewiesen', + USER_ROLE_REMOVED: 'Rolle entfernt', + ROLE_CREATED: 'Rolle erstellt', + ROLE_UPDATED: 'Rolle aktualisiert', + ROLE_DELETED: 'Rolle geloescht', + ROLE_PERMISSIONS_UPDATED: 'Permissions aktualisiert', + SESSION_REVOKED: 'Session beendet', + ALL_USER_SESSIONS_REVOKED: 'Alle Sessions beendet', + }; + + constructor() { + this.api.audit().subscribe((page) => this.entries.set(page.items)); + } +} diff --git a/apps/frontend/src/app/features/dashboard/dashboard.page.ts b/apps/frontend/src/app/features/dashboard/dashboard.page.ts new file mode 100644 index 0000000..818ff90 --- /dev/null +++ b/apps/frontend/src/app/features/dashboard/dashboard.page.ts @@ -0,0 +1,36 @@ +import { Component, inject, signal } from '@angular/core'; +import { ApiClientService } from '@boilerplate/api-client'; + +@Component({ + standalone: true, + template: ` +
+ @for (card of cards(); track card.label) { +
+ {{ card.label }} + {{ card.value }} +
+ } +
+ `, +}) +export class DashboardPageComponent { + private readonly api = inject(ApiClientService); + readonly cards = signal([ + { label: 'Benutzer', value: 0 }, + { label: 'Aktive Sessions', value: 0 }, + { label: 'Rollen', value: 0 }, + { label: 'Items', value: 0 }, + ]); + + constructor() { + this.api.dashboard().subscribe((data) => + this.cards.set([ + { label: 'Benutzer', value: data.userCount }, + { label: 'Aktive Sessions', value: data.activeSessions }, + { label: 'Rollen', value: data.roleCount }, + { label: 'Items', value: data.itemCount }, + ]), + ); + } +} diff --git a/apps/frontend/src/app/features/dev/design-system.page.ts b/apps/frontend/src/app/features/dev/design-system.page.ts new file mode 100644 index 0000000..12ba555 --- /dev/null +++ b/apps/frontend/src/app/features/dev/design-system.page.ts @@ -0,0 +1,207 @@ +import { Component, viewChild } from '@angular/core'; +import { + UiButtonComponent, + UiConfirmDialogComponent, + UiEmptyStateComponent, + UiIconButtonComponent, + UiLoadingStateComponent, + UiPaginationComponent, + UiStatusBadgeComponent, + ToastService, + UiToastHostComponent, +} from '../../shared/ui'; +import { inject } from '@angular/core'; + +const colors = [ + 'primary', + 'primary-hover', + 'primary-active', + 'primary-subtle', + 'secondary', + 'background', + 'surface', + 'surface-elevated', + 'text-primary', + 'text-secondary', + 'text-muted', + 'border', + 'border-strong', + 'focus', + 'success', + 'success-subtle', + 'warning', + 'warning-subtle', + 'danger', + 'danger-subtle', + 'info', + 'info-subtle', +]; + +@Component({ + standalone: true, + imports: [ + UiButtonComponent, + UiConfirmDialogComponent, + UiEmptyStateComponent, + UiIconButtonComponent, + UiLoadingStateComponent, + UiPaginationComponent, + UiStatusBadgeComponent, + UiToastHostComponent, + ], + template: ` +
+
+
+

Designsystem

+

Interne Referenz fuer Tokens, Komponenten und mobile Muster.

+
+
+ +
+

Farben

+
+ @for (color of colors; track color) { +
+ + --color-{{ color }} +
+ } +
+
+ +
+

Typografie

+

Seitentitel

+

Bereichstitel

+

Fliesstext mit Systemschrift und ruhiger Zeilenhoehe.

+

Hilfetext und Metainformationen

+
+ +
+

Buttons und Status

+
+ + + + + + +
+
+ + + + +
+
+ +
+

Formulare

+ + + +
+ +
+

Karten, Tabelle und Pagination

+
+
Interaktive Karte
+
Warnkarte
+
+
+ + + + + + + + + + + + + + + +
NameStatusAktion
BeispielAktivBearbeiten
+
+ +
+ +
+

Dialoge, Toasts und States

+
+ + +
+ + +
+
+ + + + `, + styles: [ + ` + .swatches { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); + gap: var(--space-4); + } + .swatches article { + display: grid; + gap: var(--space-2); + } + .swatch { + height: 3rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + } + `, + ], +}) +export class DesignSystemPageComponent { + readonly colors = colors; + private readonly toasts = inject(ToastService); + private readonly dialog = viewChild(UiConfirmDialogComponent); + + openDialog(): void { + void this.dialog()?.open({ + title: 'Aktion bestaetigen', + description: 'Dieser Dialog zeigt Fokusmanagement, Escape und Rueckgabe des Ergebnisses.', + confirmLabel: 'Bestaetigen', + }); + } + + showToast(): void { + this.toasts.show({ + tone: 'info', + title: 'Toast angezeigt', + message: 'Kurze Rueckmeldung ohne fachliche Entscheidung.', + }); + } +} diff --git a/apps/frontend/src/app/features/errors/error.page.ts b/apps/frontend/src/app/features/errors/error.page.ts new file mode 100644 index 0000000..33d4241 --- /dev/null +++ b/apps/frontend/src/app/features/errors/error.page.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { UiEmptyStateComponent } from '../../shared/ui'; + +@Component({ + standalone: true, + imports: [UiEmptyStateComponent], + template: ` + + `, +}) +export class ErrorPageComponent {} diff --git a/apps/frontend/src/app/features/errors/forbidden.page.ts b/apps/frontend/src/app/features/errors/forbidden.page.ts new file mode 100644 index 0000000..1eb6321 --- /dev/null +++ b/apps/frontend/src/app/features/errors/forbidden.page.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { UiEmptyStateComponent } from '../../shared/ui'; + +@Component({ + standalone: true, + imports: [UiEmptyStateComponent], + template: ` + + `, +}) +export class ForbiddenPageComponent {} diff --git a/apps/frontend/src/app/features/errors/not-found.page.ts b/apps/frontend/src/app/features/errors/not-found.page.ts new file mode 100644 index 0000000..f295f8a --- /dev/null +++ b/apps/frontend/src/app/features/errors/not-found.page.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { UiEmptyStateComponent } from '../../shared/ui'; + +@Component({ + standalone: true, + imports: [UiEmptyStateComponent], + template: ` + + `, +}) +export class NotFoundPageComponent {} diff --git a/apps/frontend/src/app/features/items/items.page.spec.ts b/apps/frontend/src/app/features/items/items.page.spec.ts new file mode 100644 index 0000000..098f85c --- /dev/null +++ b/apps/frontend/src/app/features/items/items.page.spec.ts @@ -0,0 +1,71 @@ +import { TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { ApiClientService } from '@boilerplate/api-client'; +import { ItemsPageComponent } from './items.page'; + +describe('ItemsPageComponent', () => { + it('keeps the save button disabled while the typed form is invalid', async () => { + await TestBed.configureTestingModule({ + imports: [ItemsPageComponent], + providers: [ + { + provide: ApiClientService, + useValue: { + items: () => of({ items: [], total: 0, page: 1, pageSize: 20 }), + }, + }, + ], + }).compileComponents(); + const fixture = TestBed.createComponent(ItemsPageComponent); + fixture.detectChanges(); + + expect(fixture.componentInstance.form.invalid).toBe(true); + fixture.componentInstance.form.controls.name.setValue('Neues Item'); + expect(fixture.componentInstance.form.valid).toBe(true); + }); + + it('loads items with pagination parameters and changes pages', async () => { + const calls: { search?: string; page?: number; pageSize?: number }[] = []; + await TestBed.configureTestingModule({ + imports: [ItemsPageComponent], + providers: [ + { + provide: ApiClientService, + useValue: { + items: (query: { search?: string; page?: number; pageSize?: number } = {}) => { + calls.push(query); + return of({ + items: [ + { + id: `item-${query.page ?? 1}`, + name: 'Item', + description: null, + status: 'active', + version: 1, + createdAt: '2026-07-16T08:00:00.000Z', + updatedAt: '2026-07-16T08:00:00.000Z', + deletedAt: null, + }, + ], + total: 30, + page: query.page ?? 1, + pageSize: query.pageSize ?? 20, + }); + }, + }, + }, + ], + }).compileComponents(); + const fixture = TestBed.createComponent(ItemsPageComponent); + fixture.detectChanges(); + + expect(calls[0]).toEqual({ search: '', page: 1, pageSize: 20 }); + + fixture.componentInstance.goToPage(2); + fixture.detectChanges(); + + expect(calls[1]).toEqual({ search: '', page: 2, pageSize: 20 }); + expect(fixture.componentInstance.page()).toBe(2); + expect((fixture.nativeElement as HTMLElement).textContent).toContain('Seite 2 von 2'); + }); +}); diff --git a/apps/frontend/src/app/features/items/items.page.ts b/apps/frontend/src/app/features/items/items.page.ts new file mode 100644 index 0000000..85568d9 --- /dev/null +++ b/apps/frontend/src/app/features/items/items.page.ts @@ -0,0 +1,169 @@ +import { Component, inject, signal } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { ApiClientService, type ApiErrorBody, type ItemDto } from '@boilerplate/api-client'; +import { UiPaginationComponent } from '../../shared/ui'; + +@Component({ + standalone: true, + imports: [ReactiveFormsModule, UiPaginationComponent], + template: ` +
+ + +
+ + @if (error()) { +

{{ error() }}

+ } + +
+ @for (item of items(); track item.id) { + + } +
+ + @if (total() > 0) { + + } + +
+

{{ selected()?.id ? 'Item bearbeiten' : 'Item erstellen' }}

+ + + +
+ + @if (selected(); as item) { + + } +
+
+ `, + styles: [ + ` + .item-toolbar, + .item-grid { + margin-bottom: var(--space-5); + } + .item-card { + text-align: left; + } + @media (min-width: 48rem) { + .item-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } + `, + ], +}) +export class ItemsPageComponent { + private readonly api = inject(ApiClientService); + readonly items = signal([]); + readonly selected = signal(null); + readonly error = signal(''); + readonly page = signal(1); + readonly total = signal(0); + readonly pageSize = 20; + readonly search = new FormControl('', { nonNullable: true }); + readonly form = new FormGroup({ + name: new FormControl('', { + nonNullable: true, + validators: [ + (control) => Validators.required(control), + (control) => Validators.maxLength(160)(control), + ], + }), + description: new FormControl('', { nonNullable: true }), + status: new FormControl('draft', { nonNullable: true }), + }); + + constructor() { + this.load(); + } + + load(): void { + this.api + .items({ + search: this.search.value, + page: this.page(), + pageSize: this.pageSize, + }) + .subscribe((page) => { + this.items.set(page.items); + this.page.set(page.page); + this.total.set(page.total); + }); + } + + searchItems(): void { + this.page.set(1); + this.load(); + } + + goToPage(page: number): void { + this.page.set(page); + this.load(); + } + + edit(item: ItemDto): void { + this.selected.set(item); + this.form.setValue({ + name: item.name, + description: item.description ?? '', + status: item.status, + }); + } + + save(): void { + this.error.set(''); + const value = this.form.getRawValue(); + const selected = this.selected(); + const request = selected + ? this.api.updateItem(selected.id, { ...value, version: selected.version }) + : this.api.createItem(value); + request.subscribe({ + next: () => { + this.selected.set(null); + this.form.reset({ name: '', description: '', status: 'draft' }); + this.load(); + }, + error: (err: { error?: ApiErrorBody }) => + this.error.set(err.error?.message ?? 'Speichern fehlgeschlagen.'), + }); + } + + delete(item: ItemDto): void { + if (confirm('Item wirklich loeschen?')) { + this.api.deleteItem(item.id, item.version).subscribe(() => this.load()); + } + } +} diff --git a/apps/frontend/src/app/features/notifications/notifications.page.spec.ts b/apps/frontend/src/app/features/notifications/notifications.page.spec.ts new file mode 100644 index 0000000..d4252c9 --- /dev/null +++ b/apps/frontend/src/app/features/notifications/notifications.page.spec.ts @@ -0,0 +1,54 @@ +import { TestBed } from '@angular/core/testing'; +import { signal } from '@angular/core'; +import type { NotificationDto } from '@boilerplate/api-client'; +import { NotificationStore } from '../../core/notification.store'; +import { NotificationsPageComponent } from './notifications.page'; + +const notification: NotificationDto = { + id: 'n1', + type: 'system', + title: 'Titel', + message: 'Nachricht', + link: '/', + metadata: null, + read: false, + readAt: null, + createdAt: '2026-07-16T08:00:00.000Z', +}; + +describe('NotificationsPageComponent', () => { + it('loads notifications and offers mobile-friendly actions', async () => { + const load = vi.fn(); + await TestBed.configureTestingModule({ + imports: [NotificationsPageComponent], + providers: [ + { + provide: NotificationStore, + useValue: { + notifications: signal([notification]), + loading: signal(false), + error: signal(null), + currentFilter: signal('all'), + page: signal(1), + pageSize: signal(20), + total: signal(1), + load, + markAllAsRead: vi.fn(), + openNotification: vi.fn(), + markAsRead: vi.fn(), + markAsUnread: vi.fn(), + delete: vi.fn(), + isInternalLink: () => true, + }, + }, + ], + }).compileComponents(); + const fixture = TestBed.createComponent(NotificationsPageComponent); + fixture.detectChanges(); + const element = fixture.nativeElement as HTMLElement; + + expect(load).toHaveBeenCalled(); + expect(element.querySelector('article.unread')).not.toBeNull(); + expect(element.textContent).toContain('Als gelesen markieren'); + }); +}); diff --git a/apps/frontend/src/app/features/notifications/notifications.page.ts b/apps/frontend/src/app/features/notifications/notifications.page.ts new file mode 100644 index 0000000..a725309 --- /dev/null +++ b/apps/frontend/src/app/features/notifications/notifications.page.ts @@ -0,0 +1,168 @@ +import { DatePipe } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import type { NotificationStatusFilter } from '@boilerplate/api-client'; +import { NotificationStore } from '../../core/notification.store'; + +@Component({ + standalone: true, + imports: [DatePipe], + template: ` +
+
+ @for (filter of filters; track filter.value) { + + } +
+ +
+ + @if (store.loading()) { +

Benachrichtigungen werden geladen.

+ } @else if (store.error(); as error) { +

+ {{ error.message }} + @if (error.requestId) { + Request-ID: {{ error.requestId }} + } +

+ } @else if (store.notifications().length === 0) { +

Keine Benachrichtigungen fuer diesen Filter.

+ } @else { +
+ @for (notification of store.notifications(); track notification.id) { +
+
+ {{ notification.title }} + +
+

{{ notification.message }}

+
+ @if (store.isInternalLink(notification.link)) { + + } + @if (notification.read) { + + } @else { + + } + +
+
+ } +
+ } + + + `, + styles: [ + ` + .toolbar, + .filters, + .actions, + .pager { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + } + .toolbar { + justify-content: space-between; + margin-bottom: 16px; + } + button { + min-height: 40px; + border: 1px solid var(--color-border-strong); + background: var(--color-surface); + color: var(--color-text-primary); + padding: 0 12px; + } + button.active { + background: var(--color-primary); + border-color: var(--color-primary); + color: var(--color-surface); + } + .list { + display: grid; + gap: 12px; + } + article, + .state { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 8px; + padding: 16px; + } + article { + display: grid; + gap: 10px; + } + article.unread { + border-left: 4px solid var(--color-primary); + background: var(--color-primary-subtle); + } + header { + display: grid; + gap: 4px; + } + p { + margin: 0; + } + time, + small, + .pager { + color: var(--color-text-muted); + } + .danger, + .error { + color: var(--color-danger); + } + .pager { + margin-top: 16px; + } + `, + ], +}) +export class NotificationsPageComponent { + readonly store = inject(NotificationStore); + readonly filters: { value: NotificationStatusFilter; label: string }[] = [ + { value: 'all', label: 'Alle' }, + { value: 'unread', label: 'Ungelesen' }, + { value: 'read', label: 'Gelesen' }, + ]; + + constructor() { + this.store.load(); + } + + previous(): void { + this.store.load(this.store.currentFilter(), Math.max(1, this.store.page() - 1)); + } + + next(): void { + this.store.load(this.store.currentFilter(), this.store.page() + 1); + } +} diff --git a/apps/frontend/src/app/features/profile/profile.page.ts b/apps/frontend/src/app/features/profile/profile.page.ts new file mode 100644 index 0000000..8d81dbc --- /dev/null +++ b/apps/frontend/src/app/features/profile/profile.page.ts @@ -0,0 +1,83 @@ +import { Component, inject } from '@angular/core'; +import { ReactiveFormsModule, FormControl, FormGroup } from '@angular/forms'; +import { AuthService } from '../../core/auth.service'; +import { ApiClientService } from '@boilerplate/api-client'; + +@Component({ + standalone: true, + imports: [ReactiveFormsModule], + template: ` + @if (auth.user(); as user) { +
+
+
Name
+
{{ user.name }}
+
E-Mail
+
{{ user.email || 'Nicht gesetzt' }}
+
Letzter Login
+
{{ user.lastLoginAt || 'Noch nicht bekannt' }}
+
+
+ + + +
+
+ } + `, + styles: [ + ` + .profile-card, + .profile-list { + gap: var(--space-5); + } + .profile-list { + display: grid; + grid-template-columns: minmax(7rem, auto) 1fr; + margin: 0; + } + dt { + color: var(--color-text-muted); + } + dd { + margin: 0; + } + .profile-form { + max-width: 28rem; + } + `, + ], +}) +export class ProfilePageComponent { + readonly auth = inject(AuthService); + private readonly api = inject(ApiClientService); + readonly form = new FormGroup({ + tablePageSize: new FormControl(20, { nonNullable: true }), + sidebarExpanded: new FormControl(true, { nonNullable: true }), + }); + + constructor() { + const user = this.auth.user(); + if (user) { + console.log(user); + // this.form.setValue(user.settings); + this.form.patchValue(user.settings); + } + } + + save(): void { + this.api.updateSettings(this.form.getRawValue()).subscribe((user) => this.auth.user.set(user)); + } +} diff --git a/apps/frontend/src/app/features/roles/roles.page.ts b/apps/frontend/src/app/features/roles/roles.page.ts new file mode 100644 index 0000000..5c82b8a --- /dev/null +++ b/apps/frontend/src/app/features/roles/roles.page.ts @@ -0,0 +1,149 @@ +import { Component, inject, signal } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { ApiClientService, type Permission, type RoleDto } from '@boilerplate/api-client'; + +const permissions: Permission[] = [ + 'items.read', + 'items.create', + 'items.update', + 'items.delete', + 'users.read', + 'users.manage', + 'roles.read', + 'roles.manage', + 'audit.read', + 'sessions.readOwn', + 'sessions.revokeOwn', + 'sessions.manage', +]; + +@Component({ + standalone: true, + imports: [ReactiveFormsModule], + template: ` +
+ @for (role of roles(); track role.id) { +
+ {{ role.name }} + @if (role.protected) { + Systemrolle + } + + {{ role.permissions.length }} Permissions · + {{ role.users?.length ?? 0 }} Benutzer +
+ } +
+
+

{{ selected()?.id ? 'Rolle bearbeiten' : 'Rolle anlegen' }}

+ +
+ Permissions + @for (permission of allPermissions; track permission) { + + } +
+ + @if (selected(); as role) { + @if (!role.protected) { + + } + } +
+ `, + styles: [ + ` + .list, + .panel, + fieldset { + display: grid; + gap: 12px; + } + article, + .panel { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 8px; + padding: 16px; + } + article { + cursor: pointer; + } + input, + button { + min-height: 44px; + } + button { + background: var(--color-primary); + color: var(--color-surface); + border: 0; + padding: 0 16px; + } + .danger { + background: var(--color-danger); + } + small { + color: var(--color-text-muted); + margin-left: 6px; + } + `, + ], +}) +export class RolesPageComponent { + private readonly api = inject(ApiClientService); + readonly roles = signal([]); + readonly selected = signal(null); + readonly selectedPermissions = signal(new Set()); + readonly allPermissions = permissions; + readonly form = new FormGroup({ + name: new FormControl('', { + nonNullable: true, + validators: [(control) => Validators.required(control)], + }), + }); + + constructor() { + this.load(); + } + + load(): void { + this.api.roles().subscribe((roles) => this.roles.set(roles)); + } + + edit(role: RoleDto): void { + this.selected.set(role); + this.form.setValue({ name: role.name }); + this.selectedPermissions.set(new Set(role.permissions.map((permission) => permission.id))); + } + + toggle(permission: Permission): void { + const next = new Set(this.selectedPermissions()); + if (next.has(permission)) next.delete(permission); + else next.add(permission); + this.selectedPermissions.set(next); + } + + save(): void { + const body = { + name: this.form.controls.name.value, + permissions: Array.from(this.selectedPermissions()), + }; + const selected = this.selected(); + const request = selected ? this.api.updateRole(selected.id, body) : this.api.createRole(body); + request.subscribe(() => this.load()); + } + + delete(id: string): void { + this.api.deleteRole(id).subscribe(() => this.load()); + } +} diff --git a/apps/frontend/src/app/features/sessions/sessions.page.ts b/apps/frontend/src/app/features/sessions/sessions.page.ts new file mode 100644 index 0000000..db2b306 --- /dev/null +++ b/apps/frontend/src/app/features/sessions/sessions.page.ts @@ -0,0 +1,58 @@ +import { Component, inject, signal } from '@angular/core'; +import { ApiClientService, type SessionDto } from '@boilerplate/api-client'; + +@Component({ + standalone: true, + template: ` + +
+ @for (session of sessions(); track session.id) { +
+
+ {{ session.current ? 'Aktuelle Session' : 'Session' }} + Angemeldet: {{ session.createdAt }} + Letzte Aktivitaet: {{ session.lastActivityAt }} + + {{ session.userAgent || 'Unbekannter Browser' }} · + {{ session.approximateIp || 'IP unbekannt' }} + +
+ @if (!session.current && !session.revokedAt) { + + } +
+ } +
+ `, + styles: [ + ` + .sessions-list { + margin-top: var(--space-5); + } + `, + ], +}) +export class SessionsPageComponent { + private readonly api = inject(ApiClientService); + readonly sessions = signal([]); + + constructor() { + this.load(); + } + + load(): void { + this.api.sessions().subscribe((sessions) => this.sessions.set(sessions)); + } + + revoke(id: string): void { + this.api.revokeSession(id).subscribe(() => this.load()); + } + + revokeOthers(): void { + this.api.revokeOtherSessions().subscribe(() => this.load()); + } +} diff --git a/apps/frontend/src/app/features/users/users.page.ts b/apps/frontend/src/app/features/users/users.page.ts new file mode 100644 index 0000000..f553d7c --- /dev/null +++ b/apps/frontend/src/app/features/users/users.page.ts @@ -0,0 +1,98 @@ +import { Component, inject, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { ApiClientService, type UserDto } from '@boilerplate/api-client'; + +@Component({ + standalone: true, + imports: [FormsModule], + template: ` +
+ + +
+
+ @for (user of users(); track user.id) { +
+
+ {{ user.name }} + {{ user.email || 'Keine E-Mail' }} + {{ user.active ? 'Aktiv' : 'Deaktiviert' }} · Letzter Login: + {{ user.lastLoginAt || 'nie' }} +
+ + +
+ } +
+ `, + styles: [ + ` + .toolbar { + display: grid; + grid-template-columns: 1fr auto; + gap: 12px; + margin-bottom: 16px; + } + input, + button { + min-height: 44px; + } + button { + border: 0; + background: var(--color-primary); + color: var(--color-surface); + padding: 0 14px; + } + .list { + display: grid; + gap: 12px; + } + article { + display: grid; + gap: 12px; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 8px; + padding: 16px; + } + article div { + display: grid; + gap: 4px; + } + span { + color: var(--color-text-muted); + } + @media (min-width: 760px) { + article { + grid-template-columns: 1fr auto auto; + align-items: center; + } + } + `, + ], +}) +export class UsersPageComponent { + private readonly api = inject(ApiClientService); + readonly users = signal([]); + search = ''; + + constructor() { + this.load(); + } + + load(): void { + this.api.users({ search: this.search }).subscribe((page) => this.users.set(page.items)); + } + + setActive(user: UserDto): void { + this.api.setUserActive(user.id, !user.active).subscribe(() => this.load()); + } + + revokeSessions(userId: string): void { + this.api.revokeUserSessions(userId).subscribe(); + } +} diff --git a/apps/frontend/src/app/layout/app-shell.html b/apps/frontend/src/app/layout/app-shell.html new file mode 100644 index 0000000..d28ebf5 --- /dev/null +++ b/apps/frontend/src/app/layout/app-shell.html @@ -0,0 +1,36 @@ +
+
+ + Business App + Abmelden +
+ + + +
+ +

{{ title() }}

+ +
+
diff --git a/apps/frontend/src/app/layout/app-shell.scss b/apps/frontend/src/app/layout/app-shell.scss new file mode 100644 index 0000000..0766bdc --- /dev/null +++ b/apps/frontend/src/app/layout/app-shell.scss @@ -0,0 +1,98 @@ +.shell { + min-height: 100vh; + background: #f6f7f9; + color: #1b2430; +} + +.topbar { + position: sticky; + top: 0; + z-index: 10; + height: 64px; + display: flex; + align-items: center; + gap: 16px; + padding: 0 16px; + background: #ffffff; + border-bottom: 1px solid #d9dee7; +} + +.icon-button { + width: 44px; + height: 44px; + border: 1px solid #c7ced9; + background: #ffffff; + display: grid; + place-content: center; + gap: 4px; +} + +.icon-button span { + display: block; + width: 18px; + height: 2px; + background: #1b2430; +} + +.logout { + margin-left: auto; + color: #184e77; +} + +.sidebar { + position: fixed; + inset: 64px auto 0 0; + width: 260px; + background: #ffffff; + border-right: 1px solid #d9dee7; + transform: translateX(-100%); + transition: transform 160ms ease; + z-index: 9; +} + +.sidebar.open { + transform: translateX(0); +} + +nav a { + display: block; + padding: 14px 18px; + color: #263445; + text-decoration: none; + min-height: 48px; +} + +nav a.active { + background: #e8f0f7; + border-left: 4px solid #26648e; +} + +.content { + padding: 20px 16px 48px; +} + +.breadcrumbs { + color: #637083; + font-size: 0.9rem; + margin-bottom: 8px; +} + +h1 { + font-size: 1.6rem; + margin: 0 0 20px; +} + +@media (min-width: 900px) { + .icon-button { + display: none; + } + + .sidebar { + transform: none; + } + + .content { + margin-left: 260px; + padding: 28px 32px; + } +} diff --git a/apps/frontend/src/app/layout/app-shell.spec.ts b/apps/frontend/src/app/layout/app-shell.spec.ts new file mode 100644 index 0000000..4007fb2 --- /dev/null +++ b/apps/frontend/src/app/layout/app-shell.spec.ts @@ -0,0 +1,102 @@ +import { TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { of } from 'rxjs'; +import { ApiClientService } from '@boilerplate/api-client'; +import { AppShellComponent } from './app-shell'; + +describe('AppShellComponent', () => { + it('hides navigation entries without matching permissions', async () => { + await TestBed.configureTestingModule({ + imports: [AppShellComponent], + providers: [ + provideRouter([]), + { + provide: ApiClientService, + useValue: { + me: () => + of({ + id: 'u1', + name: 'Ada', + email: null, + active: true, + lastLoginAt: null, + settings: { tablePageSize: 20, sidebarExpanded: true }, + roles: [ + { + id: 'r1', + name: 'user', + protected: true, + permissions: [ + { + id: 'notifications.readOwn', + description: 'notifications.readOwn', + }, + ], + }, + ], + }), + unreadNotificationCount: () => of({ count: 2 }), + notifications: () => + of({ + items: [], + total: 0, + page: 1, + pageSize: 20, + unreadCount: 2, + }), + }, + }, + ], + }).compileComponents(); + const fixture = TestBed.createComponent(AppShellComponent); + fixture.detectChanges(); + + expect( + fixture.componentInstance.visible({ + label: 'Benutzer', + path: '/benutzer', + permission: 'users.read', + }), + ).toBe(false); + expect((fixture.nativeElement as HTMLElement).querySelector('.badge')?.textContent).toContain( + '2', + ); + expect((fixture.nativeElement as HTMLElement).textContent).toContain('Sicherheit'); + }); + + it('opens the mobile drawer and closes it after navigation', async () => { + await TestBed.configureTestingModule({ + imports: [AppShellComponent], + providers: [ + provideRouter([]), + { + provide: ApiClientService, + useValue: { + me: () => + of({ + id: 'u1', + name: 'Ada', + email: null, + active: true, + lastLoginAt: null, + settings: { tablePageSize: 20, sidebarExpanded: true }, + roles: [{ id: 'r1', name: 'user', protected: true, permissions: [] }], + }), + unreadNotificationCount: () => of({ count: 0 }), + notifications: () => of({ items: [], total: 0, page: 1, pageSize: 20, unreadCount: 0 }), + }, + }, + ], + }).compileComponents(); + const fixture = TestBed.createComponent(AppShellComponent); + fixture.detectChanges(); + + fixture.componentInstance.toggleDrawer(); + fixture.detectChanges(); + expect((fixture.nativeElement as HTMLElement).querySelector('.sidebar.open')).not.toBeNull(); + + fixture.componentInstance.closeDrawerOnNavigation(); + fixture.detectChanges(); + expect((fixture.nativeElement as HTMLElement).querySelector('.sidebar.open')).toBeNull(); + }); +}); diff --git a/apps/frontend/src/app/layout/app-shell.ts b/apps/frontend/src/app/layout/app-shell.ts new file mode 100644 index 0000000..45d621a --- /dev/null +++ b/apps/frontend/src/app/layout/app-shell.ts @@ -0,0 +1,296 @@ +import { Component, HostListener, computed, effect, inject, signal } from '@angular/core'; +import { RouterLink, RouterLinkActive, RouterOutlet, Router } from '@angular/router'; +import type { Permission } from '@boilerplate/api-client'; +import { NotificationStore } from '../core/notification.store'; +import { AuthService } from '../core/auth.service'; +import { NotificationPanelComponent } from './notification-panel'; +import { UiIconButtonComponent, UiIconComponent, UiToastHostComponent } from '../shared/ui'; + +interface NavItem { + label: string; + path: string; + permission?: Permission; +} + +@Component({ + selector: 'app-shell', + standalone: true, + imports: [ + RouterOutlet, + RouterLink, + RouterLinkActive, + NotificationPanelComponent, + UiIconButtonComponent, + UiIconComponent, + UiToastHostComponent, + ], + template: ` +
+
+ @if (auth.user()) { + + } + Business App + @if (auth.user()) { + + Abmelden + } @else { + Anmelden + } + @if (auth.user() && notificationPanelOpen()) { + + } +
+ + @if (auth.loaded()) { + @if (auth.user()) { + @if (drawerOpen()) { + + } + + +
+ + +
+ } @else { +
+ +
+ } + } @else { +
+ +
+ } + +
+ `, + styles: [ + ` + .shell { + min-height: 100vh; + background: var(--color-background); + color: var(--color-text-primary); + } + .topbar { + position: sticky; + top: 0; + z-index: var(--z-header); + height: var(--header-height); + display: flex; + align-items: center; + gap: var(--space-4); + padding: 0 var(--space-5); + background: var(--color-surface); + border-bottom: 1px solid var(--color-border); + } + .brand { + white-space: nowrap; + } + .logout { + margin-left: auto; + } + .notification-button { + margin-left: auto; + } + .badge { + position: absolute; + top: var(--space-1); + right: var(--space-1); + min-width: 1.125rem; + height: 1.125rem; + border-radius: var(--radius-pill); + background: var(--color-danger); + color: var(--color-surface); + display: grid; + place-items: center; + font-size: var(--font-size-xs); + padding: 0 var(--space-2); + } + .public-content { + min-height: calc(100vh - var(--header-height)); + display: grid; + place-items: center; + margin-left: 0; + } + .login-panel { + width: min(100%, 440px); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: var(--space-6); + display: grid; + gap: var(--space-4); + } + .login-panel h1, + .login-panel p { + margin: 0; + } + .login-panel p { + color: var(--color-text-secondary); + } + .drawer-backdrop { + position: fixed; + inset: var(--header-height) 0 0; + z-index: calc(var(--z-drawer) - 1); + border: 0; + background: color-mix(in srgb, var(--color-text-primary) 36%, transparent); + } + .sidebar { + position: fixed; + inset: var(--header-height) auto 0 0; + width: var(--sidebar-width); + background: var(--color-surface); + border-right: 1px solid var(--color-border); + transform: translateX(-100%); + transition: transform var(--transition-base) ease; + z-index: var(--z-drawer); + } + .sidebar.open { + transform: translateX(0); + } + nav a { + display: block; + padding: var(--space-4) var(--space-5); + color: var(--color-text-primary); + text-decoration: none; + min-height: var(--touch-target); + } + nav a.active { + background: var(--color-primary-subtle); + border-left: 4px solid var(--color-primary); + } + .content { + min-width: 0; + padding: var(--space-5) var(--space-5) var(--space-8); + } + .breadcrumbs { + color: var(--color-text-muted); + font-size: var(--font-size-sm); + margin-bottom: var(--space-4); + } + @media (min-width: 64rem) { + ui-icon-button { + display: none; + } + .drawer-backdrop { + display: none; + } + .sidebar { + transform: none; + } + .content { + margin-left: var(--sidebar-width); + padding: var(--space-7); + } + } + `, + ], +}) +export class AppShellComponent { + private readonly router = inject(Router); + readonly auth = inject(AuthService); + readonly notifications = inject(NotificationStore); + readonly drawerOpen = signal(false); + readonly notificationPanelOpen = signal(false); + readonly title = computed( + () => this.router.routerState.snapshot.root.firstChild?.firstChild?.title ?? 'Dashboard', + ); + readonly nav: NavItem[] = [ + { label: 'Dashboard', path: '/' }, + { label: 'Profil', path: '/profil' }, + { label: 'Sicherheit', path: '/account/security' }, + { + label: 'Benachrichtigungen', + path: '/notifications', + permission: 'notifications.readOwn', + }, + { label: 'Sessions', path: '/sessions', permission: 'sessions.readOwn' }, + { label: 'Items', path: '/items', permission: 'items.read' }, + { label: 'Admin Benutzer', path: '/admin/users', permission: 'users.read' }, + { label: 'Admin Rollen', path: '/admin/roles', permission: 'roles.read' }, + { label: 'Admin Audit', path: '/admin/audit', permission: 'audit.read' }, + ]; + + constructor() { + this.auth.loadMe(); + effect(() => { + if (this.auth.user()) { + this.notifications.startPolling(); + } else { + this.notifications.stopPolling(); + this.notificationPanelOpen.set(false); + } + }); + } + + visible(item: NavItem): boolean { + return !item.permission || this.auth.has(item.permission); + } + + toggleNotifications(): void { + this.notificationPanelOpen.update((open) => !open); + if (this.notificationPanelOpen()) { + this.notifications.openPanel(); + } + } + + toggleDrawer(): void { + this.drawerOpen.update((open) => !open); + } + + closeDrawerOnNavigation(): void { + this.drawerOpen.set(false); + } + + @HostListener('document:keydown.escape') + closeOverlays(): void { + this.drawerOpen.set(false); + this.notificationPanelOpen.set(false); + } +} diff --git a/apps/frontend/src/app/layout/notification-panel.spec.ts b/apps/frontend/src/app/layout/notification-panel.spec.ts new file mode 100644 index 0000000..d376302 --- /dev/null +++ b/apps/frontend/src/app/layout/notification-panel.spec.ts @@ -0,0 +1,79 @@ +import { TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { signal } from '@angular/core'; +import type { NotificationDto } from '@boilerplate/api-client'; +import { NotificationStore } from '../core/notification.store'; +import { NotificationPanelComponent } from './notification-panel'; + +const notification: NotificationDto = { + id: 'n1', + type: 'system', + title: 'Titel', + message: 'Nachricht', + link: '/', + metadata: null, + read: false, + readAt: null, + createdAt: '2026-07-16T08:00:00.000Z', +}; + +describe('NotificationPanelComponent', () => { + it('shows unread notifications and emits close', async () => { + const closed = vi.fn(); + await TestBed.configureTestingModule({ + imports: [NotificationPanelComponent], + providers: [ + provideRouter([]), + { + provide: NotificationStore, + useValue: { + notifications: signal([notification]), + loading: signal(false), + error: signal(null), + markAllAsRead: vi.fn(), + markAsRead: vi.fn(), + markAsUnread: vi.fn(), + delete: vi.fn(), + isInternalLink: () => true, + }, + }, + ], + }).compileComponents(); + const fixture = TestBed.createComponent(NotificationPanelComponent); + fixture.componentInstance.closed.subscribe(closed); + fixture.detectChanges(); + const element = fixture.nativeElement as HTMLElement; + + expect(element.querySelector('article.unread')).not.toBeNull(); + element.querySelector('header button')?.click(); + expect(closed).toHaveBeenCalledOnce(); + }); + + it('renders empty and error states', async () => { + await TestBed.configureTestingModule({ + imports: [NotificationPanelComponent], + providers: [ + provideRouter([]), + { + provide: NotificationStore, + useValue: { + notifications: signal([]), + loading: signal(false), + error: signal({ + message: 'Fehler', + requestId: 'req-1', + status: 500, + code: 'INTERNAL_ERROR', + }), + markAllAsRead: vi.fn(), + isInternalLink: () => false, + }, + }, + ], + }).compileComponents(); + const fixture = TestBed.createComponent(NotificationPanelComponent); + fixture.detectChanges(); + + expect((fixture.nativeElement as HTMLElement).textContent).toContain('req-1'); + }); +}); diff --git a/apps/frontend/src/app/layout/notification-panel.ts b/apps/frontend/src/app/layout/notification-panel.ts new file mode 100644 index 0000000..462c943 --- /dev/null +++ b/apps/frontend/src/app/layout/notification-panel.ts @@ -0,0 +1,169 @@ +import { DatePipe } from '@angular/common'; +import { Component, EventEmitter, Output, inject } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { NotificationStore } from '../core/notification.store'; + +@Component({ + selector: 'app-notification-panel', + standalone: true, + imports: [DatePipe, RouterLink], + template: ` +
+
+ Benachrichtigungen + +
+ +
+ + Alle anzeigen +
+ + @if (store.loading()) { +

Wird geladen.

+ } @else if (store.error(); as error) { +

+ {{ error.message }} + @if (error.requestId) { + Request-ID: {{ error.requestId }} + } +

+ } @else if (store.notifications().length === 0) { +

Keine Benachrichtigungen vorhanden.

+ } @else { +
+ @for (notification of store.notifications().slice(0, 5); track notification.id) { +
+
+ {{ notification.title }} + +
+

{{ notification.message }}

+
+ @if (store.isInternalLink(notification.link)) { + + } + @if (notification.read) { + + } @else { + + } + +
+
+ } +
+ } +
+ `, + styles: [ + ` + .panel { + position: fixed; + inset: 64px 0 0; + z-index: 20; + background: var(--color-surface); + border-top: 1px solid var(--color-border); + display: grid; + align-content: start; + gap: 12px; + padding: 16px; + overflow: auto; + } + header, + .panel-actions, + .item-actions { + display: flex; + align-items: center; + gap: 8px; + } + header { + justify-content: space-between; + } + .panel-actions { + justify-content: space-between; + } + .items { + display: grid; + gap: 10px; + } + article { + border: 1px solid var(--color-border); + border-radius: 8px; + padding: 12px; + display: grid; + gap: 8px; + } + article.unread { + border-left: 4px solid var(--color-primary); + background: var(--color-primary-subtle); + } + article div:first-child { + display: grid; + gap: 4px; + } + p { + margin: 0; + } + time, + small { + color: var(--color-text-muted); + font-size: 0.85rem; + } + button, + a { + min-height: 40px; + } + button { + border: 1px solid var(--color-border-strong); + background: var(--color-surface); + color: var(--color-text-primary); + padding: 0 12px; + } + a { + display: inline-flex; + align-items: center; + color: var(--color-primary-hover); + } + .danger { + color: var(--color-danger); + } + .state { + border: 1px solid var(--color-border); + border-radius: 8px; + padding: 16px; + } + .error { + color: var(--color-danger); + } + @media (min-width: 760px) { + .panel { + inset: 72px 16px auto auto; + width: min(420px, calc(100vw - 32px)); + max-height: calc(100vh - 96px); + border: 1px solid var(--color-border); + border-radius: 8px; + box-shadow: 0 16px 40px color-mix(in srgb, var(--color-text-primary) 16%, transparent); + } + } + `, + ], +}) +export class NotificationPanelComponent { + readonly store = inject(NotificationStore); + @Output() readonly closed = new EventEmitter(); + + open(id: string): void { + const notification = this.store.notifications().find((item) => item.id === id); + if (notification) { + this.store.openNotification(notification); + this.closed.emit(); + } + } +} diff --git a/apps/frontend/src/app/shared/ui/button/button.component.ts b/apps/frontend/src/app/shared/ui/button/button.component.ts new file mode 100644 index 0000000..eef7fe8 --- /dev/null +++ b/apps/frontend/src/app/shared/ui/button/button.component.ts @@ -0,0 +1,38 @@ +import { Component, Input } from '@angular/core'; + +type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger'; +type ButtonSize = 'small' | 'medium'; +type ButtonType = 'button' | 'submit' | 'reset'; + +@Component({ + selector: 'ui-button', + standalone: true, + template: ` + + `, +}) +export class UiButtonComponent { + @Input() label = ''; + @Input() variant: ButtonVariant = 'primary'; + @Input() size: ButtonSize = 'medium'; + @Input() type: ButtonType = 'button'; + @Input() disabled = false; + @Input() loading = false; + @Input() mobileFull = false; +} diff --git a/apps/frontend/src/app/shared/ui/confirm-dialog/confirm-dialog.component.ts b/apps/frontend/src/app/shared/ui/confirm-dialog/confirm-dialog.component.ts new file mode 100644 index 0000000..f68d9d3 --- /dev/null +++ b/apps/frontend/src/app/shared/ui/confirm-dialog/confirm-dialog.component.ts @@ -0,0 +1,146 @@ +import { Component, HostListener, signal, viewChild } from '@angular/core'; +import type { ElementRef } from '@angular/core'; + +@Component({ + selector: 'ui-confirm-dialog', + standalone: true, + template: ` + @if (visible()) { + + + } + `, + styles: [ + ` + .ui-dialog-backdrop { + position: fixed; + inset: 0; + z-index: var(--z-overlay); + background: color-mix(in srgb, var(--color-text-primary) 52%, transparent); + animation: ui-fade-in var(--transition-fast) ease; + } + .ui-dialog { + position: fixed; + inset: auto var(--space-3) var(--space-3); + z-index: var(--z-dialog); + display: grid; + gap: var(--space-6); + max-height: calc(100vh - var(--space-6)); + overflow: auto; + padding: var(--space-6); + background: var(--color-surface-elevated); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-md); + animation: ui-slide-up var(--transition-base) ease; + } + .ui-actions { + justify-content: end; + } + @media (min-width: 48rem) { + .ui-dialog { + inset: 20vh auto auto 50%; + width: min(32rem, calc(100vw - var(--space-7))); + transform: translateX(-50%); + } + } + `, + ], +}) +export class UiConfirmDialogComponent { + readonly visible = signal(false); + readonly title = signal('Aktion bestaetigen'); + readonly description = signal(''); + readonly confirmLabel = signal('Bestaetigen'); + readonly cancelLabel = signal('Abbrechen'); + readonly danger = signal(false); + readonly titleId = `dialog-title-${crypto.randomUUID()}`; + readonly descriptionId = `dialog-description-${crypto.randomUUID()}`; + private readonly dialog = viewChild>('dialog'); + private resolver: ((value: boolean) => void) | null = null; + private previousFocus: HTMLElement | null = null; + + open(options: { + title: string; + description: string; + confirmLabel?: string; + cancelLabel?: string; + danger?: boolean; + }): Promise { + this.previousFocus = + document.activeElement instanceof HTMLElement ? document.activeElement : null; + this.title.set(options.title); + this.description.set(options.description); + this.confirmLabel.set(options.confirmLabel ?? 'Bestaetigen'); + this.cancelLabel.set(options.cancelLabel ?? 'Abbrechen'); + this.danger.set(options.danger ?? false); + this.visible.set(true); + queueMicrotask(() => this.dialog()?.nativeElement.focus()); + return new Promise((resolve) => { + this.resolver = resolve; + }); + } + + close(result: boolean): void { + this.visible.set(false); + this.resolver?.(result); + this.resolver = null; + this.previousFocus?.focus(); + this.previousFocus = null; + } + + @HostListener('document:keydown.escape') + onEscape(): void { + if (this.visible()) this.close(false); + } + + trapFocus(event: KeyboardEvent): void { + if (event.key !== 'Tab') return; + const root = this.dialog()?.nativeElement; + if (!root) return; + const focusable = Array.from( + root.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', + ), + ).filter((element) => !element.hasAttribute('disabled')); + if (focusable.length === 0) return; + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (!first || !last) return; + if (event.shiftKey && document.activeElement === first) { + event.preventDefault(); + last.focus(); + } else if (!event.shiftKey && document.activeElement === last) { + event.preventDefault(); + first.focus(); + } + } +} diff --git a/apps/frontend/src/app/shared/ui/empty-state/empty-state.component.ts b/apps/frontend/src/app/shared/ui/empty-state/empty-state.component.ts new file mode 100644 index 0000000..14abf7d --- /dev/null +++ b/apps/frontend/src/app/shared/ui/empty-state/empty-state.component.ts @@ -0,0 +1,53 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { UiIconComponent, type UiIconName } from '../icon/icon.component'; + +@Component({ + selector: 'ui-empty-state', + standalone: true, + imports: [UiIconComponent], + template: ` +
+ @if (iconName(); as currentIcon) { + + } +
+

{{ title }}

+ @if (description) { +

{{ description }}

+ } +
+ @if (actionLabel) { + + } +
+ `, + styles: [ + ` + .ui-empty-state { + justify-items: start; + } + ui-icon { + width: 2rem; + height: 2rem; + color: var(--color-info); + } + `, + ], +}) +export class UiEmptyStateComponent { + @Input() title = ''; + @Input() description = ''; + @Input() icon: UiIconName | '' = ''; + @Input() actionLabel = ''; + @Output() readonly action = new EventEmitter(); + + iconName(): UiIconName | null { + return this.icon === '' ? null : this.icon; + } +} diff --git a/apps/frontend/src/app/shared/ui/form-field/form-field.component.ts b/apps/frontend/src/app/shared/ui/form-field/form-field.component.ts new file mode 100644 index 0000000..d4d46c1 --- /dev/null +++ b/apps/frontend/src/app/shared/ui/form-field/form-field.component.ts @@ -0,0 +1,32 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'ui-form-field', + standalone: true, + template: ` + + `, +}) +export class UiFormFieldComponent { + @Input() label = ''; + @Input() hint = ''; + @Input() error = ''; + @Input() fieldId = `field-${crypto.randomUUID()}`; + + hintId(): string { + return `${this.fieldId}-hint`; + } + + errorId(): string { + return `${this.fieldId}-error`; + } +} diff --git a/apps/frontend/src/app/shared/ui/icon-button/icon-button.component.ts b/apps/frontend/src/app/shared/ui/icon-button/icon-button.component.ts new file mode 100644 index 0000000..cc32670 --- /dev/null +++ b/apps/frontend/src/app/shared/ui/icon-button/icon-button.component.ts @@ -0,0 +1,41 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { UiIconComponent, type UiIconName } from '../icon/icon.component'; + +type IconButtonVariant = 'default' | 'danger' | 'ghost'; + +@Component({ + selector: 'ui-icon-button', + standalone: true, + imports: [UiIconComponent], + template: ` + + `, + styles: [ + ` + .danger { + color: var(--color-danger); + } + .ghost { + background: transparent; + } + `, + ], +}) +export class UiIconButtonComponent { + @Input() icon: UiIconName = 'info'; + @Input() label = ''; + @Input() variant: IconButtonVariant = 'default'; + @Input() disabled = false; + @Output() readonly pressed = new EventEmitter(); +} diff --git a/apps/frontend/src/app/shared/ui/icon/icon.component.ts b/apps/frontend/src/app/shared/ui/icon/icon.component.ts new file mode 100644 index 0000000..9825550 --- /dev/null +++ b/apps/frontend/src/app/shared/ui/icon/icon.component.ts @@ -0,0 +1,90 @@ +import { Component, Input } from '@angular/core'; + +export type UiIconName = + | 'menu' + | 'close' + | 'user' + | 'roles' + | 'dashboard' + | 'items' + | 'audit' + | 'bell' + | 'edit' + | 'delete' + | 'activate' + | 'deactivate' + | 'search' + | 'filter' + | 'sort' + | 'back' + | 'next' + | 'check' + | 'warning' + | 'info'; + +const paths: Record = { + menu: 'M4 7h16M4 12h16M4 17h16', + close: 'M6 6l12 12M18 6L6 18', + user: 'M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm-7 8a7 7 0 0 1 14 0', + roles: + 'M7 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm10 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM2 20a5 5 0 0 1 10 0M12 20a5 5 0 0 1 10 0', + dashboard: 'M4 13h6V4H4v9Zm10 7h6V4h-6v16ZM4 20h6v-4H4v4Z', + items: 'M5 5h14v14H5V5Zm3 4h8M8 13h8', + audit: 'M7 4h10l3 3v13H7V4Zm10 0v4h4M4 8v12h12', + bell: 'M12 3a5 5 0 0 0-5 5v3.6c0 .8-.3 1.6-.9 2.2L5 15v1h14v-1l-1.1-1.2a3.2 3.2 0 0 1-.9-2.2V8a5 5 0 0 0-5-5Zm-2 15a2 2 0 0 0 4 0', + edit: 'M4 20h4l11-11-4-4L4 16v4Zm11-15 4 4', + delete: 'M5 7h14M9 7V5h6v2m-8 0 1 13h8l1-13', + activate: 'M5 12l4 4L19 6', + deactivate: 'M5 5l14 14M19 5 5 19', + search: 'M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14Zm5-2 4 4', + filter: 'M4 5h16l-6 7v6l-4 2v-8L4 5Z', + sort: 'M8 5v14m0 0-3-3m3 3 3-3m8 3V5m0 0-3 3m3-3 3 3', + back: 'M15 6 9 12l6 6', + next: 'm9 6 6 6-6 6', + check: 'M5 12l4 4L19 6', + warning: 'M12 4 3 20h18L12 4Zm0 6v4m0 3h.01', + info: 'M12 17v-6m0-4h.01M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z', +}; + +@Component({ + selector: 'ui-icon', + standalone: true, + template: ` + + + + `, + styles: [ + ` + :host { + width: 1.25rem; + height: 1.25rem; + display: inline-flex; + flex: 0 0 auto; + } + svg { + width: 100%; + height: 100%; + } + `, + ], +}) +export class UiIconComponent { + @Input() name: UiIconName = 'info'; + @Input() label = ''; + @Input() decorative = true; + + path(): string { + return paths[this.name]; + } +} diff --git a/apps/frontend/src/app/shared/ui/index.ts b/apps/frontend/src/app/shared/ui/index.ts new file mode 100644 index 0000000..92dd3b4 --- /dev/null +++ b/apps/frontend/src/app/shared/ui/index.ts @@ -0,0 +1,12 @@ +export * from './button/button.component'; +export * from './confirm-dialog/confirm-dialog.component'; +export * from './empty-state/empty-state.component'; +export * from './form-field/form-field.component'; +export * from './icon/icon.component'; +export * from './icon-button/icon-button.component'; +export * from './loading-state/loading-state.component'; +export * from './page-header/page-header.component'; +export * from './pagination/pagination.component'; +export * from './status-badge/status-badge.component'; +export * from './toast/toast-host.component'; +export * from './toast/toast.service'; diff --git a/apps/frontend/src/app/shared/ui/loading-state/loading-state.component.ts b/apps/frontend/src/app/shared/ui/loading-state/loading-state.component.ts new file mode 100644 index 0000000..245e63b --- /dev/null +++ b/apps/frontend/src/app/shared/ui/loading-state/loading-state.component.ts @@ -0,0 +1,31 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'ui-loading-state', + standalone: true, + template: ` +
+ + {{ label }} +
+ `, + styles: [ + ` + .ui-loading-state { + min-height: 8rem; + display: grid; + place-items: center; + gap: var(--space-3); + color: var(--color-text-secondary); + } + .inline { + min-height: auto; + display: inline-flex; + } + `, + ], +}) +export class UiLoadingStateComponent { + @Input() label = 'Wird geladen.'; + @Input() inline = false; +} diff --git a/apps/frontend/src/app/shared/ui/page-header/page-header.component.ts b/apps/frontend/src/app/shared/ui/page-header/page-header.component.ts new file mode 100644 index 0000000..0f78f86 --- /dev/null +++ b/apps/frontend/src/app/shared/ui/page-header/page-header.component.ts @@ -0,0 +1,27 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'ui-page-header', + standalone: true, + template: ` +
+
+ @if (breadcrumbs) { + + } +

{{ title }}

+ @if (description) { +

{{ description }}

+ } +
+
+ +
+
+ `, +}) +export class UiPageHeaderComponent { + @Input() title = ''; + @Input() description = ''; + @Input() breadcrumbs = ''; +} diff --git a/apps/frontend/src/app/shared/ui/pagination/pagination.component.ts b/apps/frontend/src/app/shared/ui/pagination/pagination.component.ts new file mode 100644 index 0000000..18d4b4c --- /dev/null +++ b/apps/frontend/src/app/shared/ui/pagination/pagination.component.ts @@ -0,0 +1,54 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + selector: 'ui-pagination', + standalone: true, + template: ` + + `, + styles: [ + ` + .ui-pagination { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + margin-top: var(--space-5); + } + `, + ], +}) +export class UiPaginationComponent { + @Input() page = 1; + @Input() total = 0; + @Input() pageSize = 20; + @Output() readonly pageChange = new EventEmitter(); + + totalPages(): number { + return Math.max(1, Math.ceil(this.total / this.pageSize)); + } + + go(page: number): void { + if (page >= 1 && page <= this.totalPages() && page !== this.page) { + this.pageChange.emit(page); + } + } +} diff --git a/apps/frontend/src/app/shared/ui/status-badge/status-badge.component.ts b/apps/frontend/src/app/shared/ui/status-badge/status-badge.component.ts new file mode 100644 index 0000000..038c3ee --- /dev/null +++ b/apps/frontend/src/app/shared/ui/status-badge/status-badge.component.ts @@ -0,0 +1,30 @@ +import { Component, Input } from '@angular/core'; + +type StatusTone = 'neutral' | 'success' | 'warning' | 'danger' | 'info'; + +@Component({ + selector: 'ui-status-badge', + standalone: true, + template: ` + + + {{ label }} + + `, +}) +export class UiStatusBadgeComponent { + @Input() label = ''; + @Input() tone: StatusTone = 'neutral'; + + toneClass(): string { + return this.tone === 'neutral' ? '' : `ui-badge--${this.tone}`; + } + + marker(): string { + if (this.tone === 'success') return '✓'; + if (this.tone === 'warning') return '!'; + if (this.tone === 'danger') return '!'; + if (this.tone === 'info') return 'i'; + return '•'; + } +} diff --git a/apps/frontend/src/app/shared/ui/toast/toast-host.component.ts b/apps/frontend/src/app/shared/ui/toast/toast-host.component.ts new file mode 100644 index 0000000..4f17f17 --- /dev/null +++ b/apps/frontend/src/app/shared/ui/toast/toast-host.component.ts @@ -0,0 +1,79 @@ +import { Component, inject } from '@angular/core'; +import { ToastService } from './toast.service'; + +@Component({ + selector: 'ui-toast-host', + standalone: true, + template: ` +
+ @for (toast of toasts.messages(); track toast.id) { +
+
+ {{ toast.title }} + @if (toast.message) { +

{{ toast.message }}

+ } + @if (toast.requestId) { + Request-ID: {{ toast.requestId }} + } +
+ +
+ } +
+ `, + styles: [ + ` + .ui-toast-region { + position: fixed; + right: var(--space-4); + bottom: var(--space-4); + z-index: var(--z-toast); + width: min(26rem, calc(100vw - var(--space-7))); + display: grid; + gap: var(--space-3); + } + .ui-toast { + display: grid; + grid-template-columns: 1fr auto; + gap: var(--space-3); + padding: var(--space-4); + background: var(--color-surface-elevated); + border: 1px solid var(--color-border); + border-left-width: 4px; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-md); + animation: ui-slide-up var(--transition-base) ease; + } + .success { + border-left-color: var(--color-success); + } + .warning { + border-left-color: var(--color-warning); + } + .danger { + border-left-color: var(--color-danger); + } + .info { + border-left-color: var(--color-info); + } + p { + margin-block: var(--space-1) 0; + color: var(--color-text-secondary); + } + small { + color: var(--color-text-muted); + } + `, + ], +}) +export class UiToastHostComponent { + readonly toasts = inject(ToastService); +} diff --git a/apps/frontend/src/app/shared/ui/toast/toast.service.ts b/apps/frontend/src/app/shared/ui/toast/toast.service.ts new file mode 100644 index 0000000..1ea175b --- /dev/null +++ b/apps/frontend/src/app/shared/ui/toast/toast.service.ts @@ -0,0 +1,29 @@ +import { Injectable, signal } from '@angular/core'; + +export type ToastTone = 'success' | 'warning' | 'danger' | 'info'; + +export interface ToastMessage { + id: string; + tone: ToastTone; + title: string; + message?: string; + requestId?: string; +} + +@Injectable({ providedIn: 'root' }) +export class ToastService { + readonly messages = signal([]); + + show(message: Omit): string { + const id = crypto.randomUUID(); + this.messages.update((messages) => [...messages.slice(-3), { ...message, id }]); + if (message.tone !== 'danger') { + window.setTimeout(() => this.close(id), 5000); + } + return id; + } + + close(id: string): void { + this.messages.update((messages) => messages.filter((message) => message.id !== id)); + } +} diff --git a/apps/frontend/src/app/shared/ui/ui-components.spec.ts b/apps/frontend/src/app/shared/ui/ui-components.spec.ts new file mode 100644 index 0000000..9862167 --- /dev/null +++ b/apps/frontend/src/app/shared/ui/ui-components.spec.ts @@ -0,0 +1,135 @@ +import { Component, ViewChild } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { + UiButtonComponent, + UiConfirmDialogComponent, + UiEmptyStateComponent, + UiFormFieldComponent, + UiIconButtonComponent, + UiPaginationComponent, + UiStatusBadgeComponent, + UiToastHostComponent, + ToastService, +} from './index'; +import { isDesignSystemRouteEnabled } from '../../core/dev-only.guard'; +import { devRoutes } from '../../core/dev-routes.prod'; + +@Component({ + standalone: true, + imports: [ + UiButtonComponent, + UiConfirmDialogComponent, + UiEmptyStateComponent, + UiFormFieldComponent, + UiIconButtonComponent, + UiPaginationComponent, + UiStatusBadgeComponent, + UiToastHostComponent, + ], + template: ` + + + + + + + + + + + + `, +}) +class UiTestHostComponent { + @ViewChild(UiConfirmDialogComponent) dialog?: UiConfirmDialogComponent; + page = 2; + emptyActionCount = 0; +} + +describe('shared UI components', () => { + it('renders button loading and disabled state', async () => { + await TestBed.configureTestingModule({ imports: [UiTestHostComponent] }).compileComponents(); + const fixture = TestBed.createComponent(UiTestHostComponent); + fixture.detectChanges(); + + const button = (fixture.nativeElement as HTMLElement).querySelector('ui-button button'); + expect(button?.hasAttribute('disabled')).toBe(true); + expect(button?.getAttribute('aria-busy')).toBe('true'); + }); + + it('requires accessible labels for icon buttons', async () => { + await TestBed.configureTestingModule({ imports: [UiTestHostComponent] }).compileComponents(); + const fixture = TestBed.createComponent(UiTestHostComponent); + fixture.detectChanges(); + + const button = (fixture.nativeElement as HTMLElement).querySelector('ui-icon-button button'); + expect(button?.getAttribute('aria-label')).toBe('Eintrag loeschen'); + }); + + it('shows form field errors and status text', async () => { + await TestBed.configureTestingModule({ imports: [UiTestHostComponent] }).compileComponents(); + const fixture = TestBed.createComponent(UiTestHostComponent); + fixture.detectChanges(); + const element = fixture.nativeElement as HTMLElement; + + expect(element.textContent).toContain('Name ist erforderlich'); + expect(element.textContent).toContain('Deaktiviert'); + }); + + it('opens and closes confirm dialog and restores focus', async () => { + await TestBed.configureTestingModule({ imports: [UiTestHostComponent] }).compileComponents(); + const fixture = TestBed.createComponent(UiTestHostComponent); + fixture.detectChanges(); + const trigger = (fixture.nativeElement as HTMLElement).querySelector( + '#trigger', + ); + trigger?.focus(); + + const promise = fixture.componentInstance.dialog?.open({ + title: 'Loeschen', + description: 'Wirklich loeschen?', + confirmLabel: 'Loeschen', + danger: true, + }); + fixture.detectChanges(); + await fixture.whenStable(); + + expect((fixture.nativeElement as HTMLElement).querySelector('[role="dialog"]')).not.toBeNull(); + fixture.componentInstance.dialog?.close(true); + fixture.detectChanges(); + + await expect(promise).resolves.toBe(true); + expect(document.activeElement).toBe(trigger); + }); + + it('shows and closes toasts', async () => { + await TestBed.configureTestingModule({ imports: [UiTestHostComponent] }).compileComponents(); + const fixture = TestBed.createComponent(UiTestHostComponent); + const service = TestBed.inject(ToastService); + const id = service.show({ tone: 'success', title: 'Gespeichert' }); + fixture.detectChanges(); + + expect((fixture.nativeElement as HTMLElement).textContent).toContain('Gespeichert'); + service.close(id); + fixture.detectChanges(); + expect((fixture.nativeElement as HTMLElement).textContent).not.toContain('Gespeichert'); + }); + + it('emits empty state and pagination actions', async () => { + await TestBed.configureTestingModule({ imports: [UiTestHostComponent] }).compileComponents(); + const fixture = TestBed.createComponent(UiTestHostComponent); + fixture.detectChanges(); + const element = fixture.nativeElement as HTMLElement; + + element.querySelector('ui-empty-state button')?.click(); + element.querySelectorAll('ui-pagination button')[0]?.click(); + + expect(fixture.componentInstance.emptyActionCount).toBe(1); + expect(fixture.componentInstance.page).toBe(1); + }); + + it('keeps the design system route disabled outside development mode', () => { + expect(isDesignSystemRouteEnabled(false)).toBe(false); + expect(devRoutes).toHaveLength(0); + }); +}); diff --git a/apps/frontend/src/index.html b/apps/frontend/src/index.html new file mode 100644 index 0000000..71cdfcc --- /dev/null +++ b/apps/frontend/src/index.html @@ -0,0 +1,13 @@ + + + + + Frontend + + + + + + + + diff --git a/apps/frontend/src/main.ts b/apps/frontend/src/main.ts new file mode 100644 index 0000000..190f341 --- /dev/null +++ b/apps/frontend/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { App } from './app/app'; + +bootstrapApplication(App, appConfig).catch((err) => console.error(err)); diff --git a/apps/frontend/src/styles.scss b/apps/frontend/src/styles.scss new file mode 100644 index 0000000..6576241 --- /dev/null +++ b/apps/frontend/src/styles.scss @@ -0,0 +1,9 @@ +@use './styles/tokens'; +@use './styles/reset'; +@use './styles/typography'; +@use './styles/layout'; +@use './styles/forms'; +@use './styles/buttons'; +@use './styles/tables'; +@use './styles/utilities'; +@use './styles/animations'; diff --git a/apps/frontend/src/styles/_animations.scss b/apps/frontend/src/styles/_animations.scss new file mode 100644 index 0000000..2b29750 --- /dev/null +++ b/apps/frontend/src/styles/_animations.scss @@ -0,0 +1,25 @@ +@keyframes ui-spin { + to { + transform: rotate(1turn); + } +} + +@keyframes ui-fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes ui-slide-up { + from { + opacity: 0; + transform: translateY(var(--space-3)); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/apps/frontend/src/styles/_buttons.scss b/apps/frontend/src/styles/_buttons.scss new file mode 100644 index 0000000..1d5fa04 --- /dev/null +++ b/apps/frontend/src/styles/_buttons.scss @@ -0,0 +1,90 @@ +.ui-button { + min-height: var(--button-height); + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-3); + border: 1px solid transparent; + border-radius: var(--radius-md); + padding: 0 var(--space-5); + font-weight: var(--font-weight-semibold); + line-height: 1; + text-decoration: none; + transition: + background-color var(--transition-fast) ease, + border-color var(--transition-fast) ease, + color var(--transition-fast) ease; +} + +.ui-button--primary { + color: var(--color-surface); + background: var(--color-primary); +} + +.ui-button--primary:hover { + background: var(--color-primary-hover); +} + +.ui-button--primary:active { + background: var(--color-primary-active); +} + +.ui-button--secondary { + color: var(--color-surface); + background: var(--color-secondary); +} + +.ui-button--ghost { + color: var(--color-primary); + background: transparent; + border-color: var(--color-border); +} + +.ui-button--danger { + color: var(--color-surface); + background: var(--color-danger); +} + +.ui-button--small { + min-height: 2.25rem; + padding-inline: var(--space-4); + font-size: var(--font-size-sm); +} + +.ui-button[disabled], +.ui-button[aria-disabled='true'] { + opacity: 0.55; +} + +.ui-icon-button { + position: relative; + width: var(--touch-target); + height: var(--touch-target); + display: inline-grid; + place-items: center; + color: var(--color-text-primary); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); +} + +.ui-icon-button:hover { + background: var(--color-primary-subtle); + border-color: var(--color-border-strong); +} + +.ui-button__spinner, +.ui-spinner { + width: 1rem; + height: 1rem; + border: 2px solid currentColor; + border-right-color: transparent; + border-radius: var(--radius-pill); + animation: ui-spin 700ms linear infinite; +} + +@media (max-width: 35.99rem) { + .ui-button--mobile-full { + width: 100%; + } +} diff --git a/apps/frontend/src/styles/_forms.scss b/apps/frontend/src/styles/_forms.scss new file mode 100644 index 0000000..cef01e7 --- /dev/null +++ b/apps/frontend/src/styles/_forms.scss @@ -0,0 +1,65 @@ +.ui-form { + display: grid; + gap: var(--space-5); +} + +.ui-form-field { + display: grid; + gap: var(--space-2); +} + +.ui-control { + width: 100%; + min-height: var(--input-height); + padding: 0 var(--space-4); + color: var(--color-text-primary); + background: var(--color-surface); + border: 1px solid var(--color-border-strong); + border-radius: var(--radius-md); + transition: + border-color var(--transition-fast) ease, + box-shadow var(--transition-fast) ease; +} + +textarea.ui-control { + min-height: 6rem; + padding-block: var(--space-3); + resize: vertical; +} + +.ui-control:focus { + border-color: var(--color-focus); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-focus) 22%, transparent); + outline: none; +} + +.ui-control:disabled, +.ui-control[readonly] { + color: var(--color-text-muted); + background: var(--color-neutral-subtle); +} + +.ui-control[aria-invalid='true'] { + border-color: var(--color-danger); +} + +.ui-field-error { + color: var(--color-danger); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); +} + +.ui-checkbox { + display: grid; + grid-template-columns: 1.25rem 1fr; + align-items: start; + gap: var(--space-3); + min-height: var(--touch-target); +} + +.ui-checkbox input { + width: 1.1rem; + height: 1.1rem; + margin-top: 0.2rem; + accent-color: var(--color-primary); +} diff --git a/apps/frontend/src/styles/_layout.scss b/apps/frontend/src/styles/_layout.scss new file mode 100644 index 0000000..d363a27 --- /dev/null +++ b/apps/frontend/src/styles/_layout.scss @@ -0,0 +1,121 @@ +.ui-page { + display: grid; + gap: var(--space-6); +} + +.ui-page-header { + display: grid; + gap: var(--space-4); + margin-bottom: var(--space-6); +} + +.ui-page-header__content { + display: grid; + gap: var(--space-2); +} + +.ui-page-header__actions { + display: flex; + flex-wrap: wrap; + gap: var(--space-3); +} + +.ui-grid { + display: grid; + gap: var(--space-4); +} + +.ui-grid--cards { + grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); +} + +.ui-card { + display: grid; + gap: var(--space-4); + min-width: 0; + padding: var(--space-5); + color: var(--color-text-primary); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); +} + +.ui-card--interactive { + transition: + border-color var(--transition-fast) ease, + box-shadow var(--transition-fast) ease; +} + +.ui-card--interactive:hover { + border-color: var(--color-border-strong); + box-shadow: var(--shadow-md); +} + +.ui-card--selected { + border-color: var(--color-primary); + background: var(--color-primary-subtle); +} + +.ui-card--warning { + border-color: var(--color-warning); + background: var(--color-warning-subtle); +} + +.ui-card--danger { + border-color: var(--color-danger); + background: var(--color-danger-subtle); +} + +.ui-toolbar { + display: grid; + gap: var(--space-4); + padding: var(--space-5); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); +} + +.ui-actions { + display: flex; + flex-wrap: wrap; + gap: var(--space-3); +} + +.ui-kpi { + display: grid; + gap: var(--space-2); +} + +.ui-kpi__value { + font-size: var(--font-size-2xl); + line-height: var(--line-height-tight); + font-weight: var(--font-weight-bold); +} + +.ui-notice { + display: grid; + gap: var(--space-2); + padding: var(--space-5); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); +} + +.ui-notice--error { + color: var(--color-danger); + background: var(--color-danger-subtle); + border-color: var(--color-danger); +} + +@media (min-width: 48rem) { + .ui-page-header { + grid-template-columns: minmax(0, 1fr) auto; + align-items: start; + } + + .ui-toolbar { + grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); + align-items: end; + } +} diff --git a/apps/frontend/src/styles/_reset.scss b/apps/frontend/src/styles/_reset.scss new file mode 100644 index 0000000..0820bf9 --- /dev/null +++ b/apps/frontend/src/styles/_reset.scss @@ -0,0 +1,70 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + min-width: 0; + min-height: 100%; + font-family: var(--font-family-base); + color: var(--color-text-primary); + background: var(--color-background); + text-size-adjust: 100%; +} + +body { + min-width: 0; + min-height: 100%; + margin: 0; + font-size: var(--font-size-md); + line-height: var(--line-height-base); +} + +img, +svg, +video { + max-width: 100%; +} + +button, +input, +textarea, +select { + font: inherit; +} + +button, +a, +input, +textarea, +select { + &:focus-visible { + outline: 3px solid var(--color-focus); + outline-offset: 2px; + } +} + +button { + cursor: pointer; +} + +button:disabled { + cursor: not-allowed; +} + +a { + color: var(--color-primary); + text-underline-offset: 0.16em; +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto; + transition-duration: 1ms; + animation-duration: 1ms; + animation-iteration-count: 1; + } +} diff --git a/apps/frontend/src/styles/_tables.scss b/apps/frontend/src/styles/_tables.scss new file mode 100644 index 0000000..e87094a --- /dev/null +++ b/apps/frontend/src/styles/_tables.scss @@ -0,0 +1,34 @@ +.ui-table-wrap { + overflow-x: auto; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); +} + +.ui-table { + width: 100%; + border-collapse: collapse; + min-width: 42rem; +} + +.ui-table th, +.ui-table td { + padding: var(--space-4) var(--space-5); + text-align: left; + border-bottom: 1px solid var(--color-border); +} + +.ui-table th { + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + background: var(--color-neutral-subtle); +} + +.ui-table tr:last-child td { + border-bottom: 0; +} + +.ui-table tbody tr:hover { + background: var(--color-primary-subtle); +} diff --git a/apps/frontend/src/styles/_tokens.scss b/apps/frontend/src/styles/_tokens.scss new file mode 100644 index 0000000..cd2680e --- /dev/null +++ b/apps/frontend/src/styles/_tokens.scss @@ -0,0 +1,76 @@ +:root { + --color-primary: #245b7d; + --color-primary-hover: #1d4f6d; + --color-primary-active: #173f58; + --color-primary-subtle: #e7f1f7; + --color-secondary: #546475; + --color-background: #f5f7fa; + --color-surface: #ffffff; + --color-surface-elevated: #ffffff; + --color-text-primary: #182331; + --color-text-secondary: #4d5d70; + --color-text-muted: #687789; + --color-border: #d9e0e8; + --color-border-strong: #aeb9c6; + --color-focus: #0b72b9; + --color-success: #23724d; + --color-success-subtle: #e7f5ee; + --color-warning: #8a5a0a; + --color-warning-subtle: #fff4d8; + --color-danger: #a13d3d; + --color-danger-subtle: #fbeaea; + --color-info: #315f94; + --color-info-subtle: #e8f1fb; + --color-neutral-subtle: #eef2f6; + + --space-0: 0; + --space-1: 0.125rem; + --space-2: 0.25rem; + --space-3: 0.5rem; + --space-4: 0.75rem; + --space-5: 1rem; + --space-6: 1.5rem; + --space-7: 2rem; + --space-8: 3rem; + --space-9: 4rem; + + --font-family-base: + ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-size-xs: 0.75rem; + --font-size-sm: 0.875rem; + --font-size-md: 1rem; + --font-size-lg: 1.125rem; + --font-size-xl: 1.375rem; + --font-size-2xl: 1.75rem; + --line-height-tight: 1.2; + --line-height-base: 1.5; + --line-height-relaxed: 1.65; + --font-weight-regular: 400; + --font-weight-medium: 500; + --font-weight-semibold: 650; + --font-weight-bold: 750; + + --radius-sm: 0.25rem; + --radius-md: 0.375rem; + --radius-lg: 0.5rem; + --radius-pill: 999px; + --shadow-sm: 0 1px 2px rgb(24 35 49 / 8%); + --shadow-md: 0 8px 24px rgb(24 35 49 / 12%); + --z-header: 20; + --z-drawer: 30; + --z-overlay: 40; + --z-dialog: 50; + --z-toast: 60; + --transition-fast: 120ms; + --transition-base: 180ms; + --container-width: 72rem; + --sidebar-width: 16.25rem; + --header-height: 4rem; + --breakpoint-small: 36rem; + --breakpoint-medium: 48rem; + --breakpoint-large: 64rem; + --breakpoint-wide: 80rem; + --touch-target: 2.75rem; + --input-height: 2.75rem; + --button-height: 2.75rem; +} diff --git a/apps/frontend/src/styles/_typography.scss b/apps/frontend/src/styles/_typography.scss new file mode 100644 index 0000000..5dd92ba --- /dev/null +++ b/apps/frontend/src/styles/_typography.scss @@ -0,0 +1,44 @@ +h1, +h2, +h3, +p { + margin-block: 0; +} + +h1, +.ui-page-title { + font-size: var(--font-size-2xl); + line-height: var(--line-height-tight); + font-weight: var(--font-weight-bold); +} + +h2, +.ui-section-title { + font-size: var(--font-size-xl); + line-height: var(--line-height-tight); + font-weight: var(--font-weight-semibold); +} + +h3, +.ui-subsection-title { + font-size: var(--font-size-lg); + line-height: var(--line-height-tight); + font-weight: var(--font-weight-semibold); +} + +.ui-text { + color: var(--color-text-primary); +} + +.ui-help-text, +.ui-meta, +.text-muted { + color: var(--color-text-muted); + font-size: var(--font-size-sm); +} + +.ui-label { + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); +} diff --git a/apps/frontend/src/styles/_utilities.scss b/apps/frontend/src/styles/_utilities.scss new file mode 100644 index 0000000..b7d2397 --- /dev/null +++ b/apps/frontend/src/styles/_utilities.scss @@ -0,0 +1,72 @@ +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.stack { + display: grid; + gap: var(--space-4); +} + +.cluster { + display: flex; + flex-wrap: wrap; + gap: var(--space-3); + align-items: center; +} + +.full-width { + width: 100%; +} + +.ui-badge { + width: fit-content; + min-height: 1.5rem; + display: inline-flex; + align-items: center; + gap: var(--space-2); + padding: 0 var(--space-3); + border: 1px solid var(--color-border); + border-radius: var(--radius-pill); + color: var(--color-text-secondary); + background: var(--color-neutral-subtle); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semibold); +} + +.ui-badge--success { + color: var(--color-success); + background: var(--color-success-subtle); + border-color: var(--color-success); +} + +.ui-badge--warning { + color: var(--color-warning); + background: var(--color-warning-subtle); + border-color: var(--color-warning); +} + +.ui-badge--danger { + color: var(--color-danger); + background: var(--color-danger-subtle); + border-color: var(--color-danger); +} + +.ui-badge--info { + color: var(--color-info); + background: var(--color-info-subtle); + border-color: var(--color-info); +} diff --git a/apps/frontend/src/test-setup.ts b/apps/frontend/src/test-setup.ts new file mode 100644 index 0000000..544680c --- /dev/null +++ b/apps/frontend/src/test-setup.ts @@ -0,0 +1,8 @@ +import '@angular/compiler'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting, +} from '@angular/platform-browser-dynamic/testing'; + +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); diff --git a/apps/frontend/tsconfig.app.json b/apps/frontend/tsconfig.app.json new file mode 100644 index 0000000..1eb42f4 --- /dev/null +++ b/apps/frontend/tsconfig.app.json @@ -0,0 +1,10 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] +} diff --git a/apps/frontend/tsconfig.json b/apps/frontend/tsconfig.json new file mode 100644 index 0000000..710f6be --- /dev/null +++ b/apps/frontend/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../../tsconfig.base.json", + "compileOnSave": false, + "compilerOptions": { + "module": "preserve", + "target": "ES2023", + "lib": ["ES2023", "DOM"], + "types": [], + "experimentalDecorators": true, + "importHelpers": true, + "strict": true + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/apps/frontend/tsconfig.spec.json b/apps/frontend/tsconfig.spec.json new file mode 100644 index 0000000..8f9324d --- /dev/null +++ b/apps/frontend/tsconfig.spec.json @@ -0,0 +1,10 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": ["vitest/globals", "node"] + }, + "include": ["src/**/*.d.ts", "src/**/*.spec.ts"] +} diff --git a/apps/frontend/vitest.config.ts b/apps/frontend/vitest.config.ts new file mode 100644 index 0000000..2cf8e8b --- /dev/null +++ b/apps/frontend/vitest.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'jsdom', + include: ['src/**/*.spec.ts'], + setupFiles: ['src/test-setup.ts'], + globals: true, + pool: 'threads', + maxWorkers: 1, + minWorkers: 1, + }, +}); diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..682a89c --- /dev/null +++ b/compose.yml @@ -0,0 +1,29 @@ +services: + app: + image: internal-business-app:latest + build: + context: . + env_file: + - .env + ports: + - '3000:3000' + restart: unless-stopped + read_only: true + tmpfs: + - /tmp + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + healthcheck: + test: + [ + 'CMD', + 'node', + '-e', + "fetch('http://127.0.0.1:3000/health/ready').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", + ] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s diff --git a/docs/admin.md b/docs/admin.md new file mode 100644 index 0000000..9af20ac --- /dev/null +++ b/docs/admin.md @@ -0,0 +1,121 @@ +# Adminbereich + +Der Adminbereich liegt im Frontend unter `/admin` und verwendet dieselbe +serverseitige Authentifizierung, CSRF-Pruefung und Permission-Logik wie die +restliche Anwendung. Angular blendet Navigation und Aktionen nur fuer passende +Permissions ein; verbindlich prueft immer das Backend. + +## Permissions + +Die administrativen Permissions sind fest in +`apps/backend/src/roles/permissions.ts` definiert: + +- `users.read`: Benutzer anzeigen, suchen und filtern +- `users.manage`: Benutzer aktivieren, deaktivieren und Rollen zuweisen +- `roles.read`: Rollen und Permissions anzeigen +- `roles.manage`: Rollen anlegen, bearbeiten und loeschen +- `sessions.manage`: Sessions anderer Benutzer anzeigen und beenden +- `audit.read`: administratives Audit-Log anzeigen + +Benutzer haben keine direkten Permissions. Effektive Permissions werden aus +allen Rollen abgeleitet. Rollen- und Benutzerverwaltung laufen ueber Services; +Controller greifen nicht direkt auf TypeORM-Repositories zu. + +## Systemrollen + +Es gibt mindestens die Systemrollen `admin` und `user`. + +- `admin` ist geschuetzt, nicht loeschbar und muss administrative Permissions + behalten. +- `user` ist geschuetzt, nicht loeschbar und Standardrolle fuer neue Benutzer. +- Eigene Rollen koennen angelegt, umbenannt, geloescht und mit bekannten + Permissions versehen werden. + +Eine Rolle kann nur geloescht werden, wenn sie keinem Benutzer mehr zugewiesen +ist. Andernfalls antwortet die API mit `ROLE_STILL_ASSIGNED`. + +## Letzter aktiver Administrator + +Die Anwendung darf nie ohne aktiven Administrator enden. Sicherheitskritische +Aktionen laufen transaktional und halten einen MySQL-Lock +`business_app_admin_integrity`: + +- Benutzer deaktivieren +- Rollen eines Benutzers aendern +- Adminrolle entfernen +- Adminrolle in ihren Permissions veraendern +- Rolle loeschen + +Wenn eine Aktion den letzten aktiven Administrator entfernen wuerde, antwortet +die API mit HTTP 409 und `LAST_ACTIVE_ADMIN_REQUIRED`. + +## Benutzerstatus und Sessions + +Benutzer werden weiterhin ausschliesslich ueber OIDC angelegt. Name und E-Mail +kommen vom Identity Provider und sind im Adminbereich nicht editierbar. + +Beim Deaktivieren wird der Benutzer sofort inaktiv gesetzt und alle aktiven +Sessions des Benutzers werden widerrufen. Neue OIDC-Logins deaktivierter Benutzer +werden trotz erfolgreicher IdP-Authentifizierung abgewiesen. Beim erneuten +Aktivieren darf sich der Benutzer wieder anmelden; alte Sessions werden nicht +wiederhergestellt. + +Admin-Session-Endpunkte geben nur eine sichere, gekuerzte Session-Referenz, +Zeitpunkte, User-Agent, IP-Annaeherung und Status zurueck. Tokens und rohe +Session-Secrets werden nie ausgegeben. + +## API-Endpunkte + +Benutzer: + +- `GET /api/admin/users` +- `GET /api/admin/users/:id` +- `PATCH /api/admin/users/:id/deactivate` +- `PATCH /api/admin/users/:id/activate` +- `POST /api/admin/users/:id/roles/:roleId` +- `DELETE /api/admin/users/:id/roles/:roleId` +- `GET /api/admin/users/:id/sessions` +- `DELETE /api/admin/users/:userId/sessions/:sessionId` +- `DELETE /api/admin/users/:id/sessions` + +Rollen: + +- `GET /api/admin/roles` +- `GET /api/admin/roles/:id` +- `POST /api/admin/roles` +- `PUT /api/admin/roles/:id` +- `DELETE /api/admin/roles/:id` + +Audit: + +- `GET /api/audit-log` + +## Audit-Log + +Administrative Aenderungen werden auditierbar protokolliert, darunter: + +- `USER_ACTIVATED` +- `USER_DEACTIVATED` +- `USER_ROLE_ASSIGNED` +- `USER_ROLE_REMOVED` +- `ROLE_CREATED` +- `ROLE_UPDATED` +- `ROLE_DELETED` +- `ROLE_PERMISSIONS_UPDATED` +- `SESSION_REVOKED` +- `ALL_USER_SESSIONS_REVOKED` + +Gespeichert werden fachliche IDs, Request-ID und minimale Metadaten. Tokens, +Secrets und vollstaendige Sessiondaten werden nicht geloggt. + +## Migration + +Die Admin-Erweiterung fuegt `roles.description` hinzu. Vor dem Deployment muss +die Migration ausgefuehrt werden: + +```bash +npm run migration:run +``` + +Der normale App-Start fuehrt Migrationen weiterhin nicht automatisch aus, +sondern meldet fehlende Migrationen ueber die Readiness-Pruefung. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..ea249d0 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,129 @@ +# Architektur + +Das Repository ist ein modularer Monolith mit getrennten Workspaces fuer +Frontend, Backend und API-Client. Ausgeliefert wird eine einzelne Node.js-App, +die die gebaute Angular-Anwendung statisch mitliefert. + +## Workspaces + +```text +apps/frontend +apps/backend +packages/api-client +``` + +Es wird bewusst kein Nx eingesetzt. Workspace-Grenzen bleiben durch npm-Scripts, +TypeScript-Projekte und klare Importpfade nachvollziehbar. + +## Backend + +Das Backend liegt in `apps/backend` und basiert auf NestJS. Es ist nach Features +geschnitten: + +- `auth`: OIDC Login, Callback, Logout und Authentifizierung +- `sessions`: serverseitige Sessions, CSRF-Token und Session-Listen +- `roles`: Code-definierte Permissions und rollenbasierte Rechteverwaltung +- `users`: lokale Benutzerprofile und Einstellungen +- `admin`: Angular-Routen unter `/admin` fuer Benutzer-, Rollen-, Session- und Audit-Verwaltung +- `items`: Beispiel-Fachmodul +- `notifications`: persoenliche In-App-Benachrichtigungen, Admin-Erzeugung und interner Service +- `audit`: Audit-Log fuer administrative Nachvollziehbarkeit +- `health`: Liveness- und Readiness-Endpunkte +- `database`: TypeORM-Konfiguration, Entities und Migrationen +- `common`: Fehlerformat, Request Context, Validierung und HTTP-Helfer + +Controller enthalten keine Datenbanklogik. Sie validieren den HTTP-Rand, +deklarieren Permissions und delegieren an Services. Services enthalten +Fachlogik. Datenbankzugriffe laufen ueber Repository-Klassen oder klar benannte +Persistence-Services. + +Globale Backend-Bausteine: + +- `ApiExceptionFilter` normalisiert Fehlerantworten. +- `ThrottlerGuard` setzt Rate Limits. +- `CsrfGuard` schuetzt schreibende Requests. +- `PermissionsGuard` prueft Rollen und Permissions. +- `RequestIdMiddleware` setzt Request-Korrelation. +- `helmet` setzt Security Header. +- `pino-http` loggt mit Redaction fuer Cookies, Tokens und Secrets. + +## Frontend + +Das Frontend liegt in `apps/frontend` und nutzt Angular Standalone Components. +Der Shell-Aufbau steckt in `src/app/layout/app-shell.ts`. Feature-Seiten liegen +unter `src/app/features`. + +Design- und Implementierungsregeln: + +- mobile-first HTML und SCSS +- keine UI-Komponentenbibliothek +- Signals fuer lokalen UI-State +- RxJS fuer HTTP- und API-Flows +- Angular Permissions nur fuer Darstellung und Navigation, nicht als + Sicherheitsgrenze + +Der `permissionGuard` verhindert unpassende Navigation im Frontend. Die +verbindliche Autorisierung findet immer im Backend statt. + +## API-Client + +`packages/api-client` enthaelt den vom Frontend genutzten Angular-Service und die +DTO-Typen. Der Code ist als generiert markiert. Er wird mit folgendem Befehl neu +geschrieben: + +```bash +npm run api:generate +``` + +Der Produktionsbuild ruft diesen Schritt vor Backend- und Frontend-Builds auf. +Bei API-Aenderungen muss der Generator synchron zur Backend-API angepasst werden. + +## Runtime-Aufbau + +Im Produktionsbuild entsteht: + +```text +apps/backend/dist/main.js +apps/backend/dist/public/index.html +apps/backend/dist/public/assets... +``` + +NestJS liefert: + +- `/api/*` als JSON API +- `/api/docs` als Swagger UI, wenn aktiviert +- `/health/live` fuer Liveness +- `/health/ready` fuer Readiness inklusive Datenbank- und Migrationspruefung +- alle anderen Pfade als Angular SPA Fallback + +## Datenmodell und Migrationen + +TypeORM laeuft mit: + +- `synchronize: false` +- `migrationsRun: false` +- expliziten Migrationen unter `apps/backend/src/database/migrations` + +Der normale App-Start fuehrt keine Migrationen aus. Stattdessen prueft +`MigrationHealthService`, ob Migrationen fehlen. Dadurch wird verhindert, dass +ein App-Rollout unbemerkt ein Schema veraendert. + +## Autorisierung + +Permissions werden in `apps/backend/src/roles/permissions.ts` definiert. Rollen +sind Datenbankdaten, Benutzer erhalten Permissions nur ueber Rollen. + +Systemrollen: + +- `admin`: alle Permissions +- `user`: Basisrechte fuer Items-Lesen und eigene Sessions + +Die Standardrolle `user` enthaelt ausserdem +`notifications.readOwn` und `notifications.updateOwn`, damit angemeldete +Benutzer ihre eigenen In-App-Benachrichtigungen verwalten koennen. + +Der erste erfolgreich angemeldete Benutzer wird automatisch Admin. Danach +erhalten neue Benutzer initial die Rolle `user`. + +Details zu Admin-Endpunkten, Systemrollen und dem transaktionalen Schutz des +letzten aktiven Administrators stehen in [Adminbereich](admin.md). diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..6ebd6ba --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,118 @@ +# Konfiguration + +Die Runtime-Konfiguration wird ueber Umgebungsvariablen geladen und mit Zod in +`apps/backend/src/config/env.ts` validiert. `.env.example` ist die Referenz fuer +lokale Entwicklung und Deployment-Vorlagen. + +## Allgemein + +| Variable | Bedeutung | +| ------------------- | -------------------------------------------------------------------------- | +| `NODE_ENV` | `development`, `test` oder `production` | +| `PORT` | HTTP-Port des Backends | +| `APP_BASE_URL` | Externe Basis-URL der App; Grundlage fuer OIDC Callback | +| `FRONTEND_BASE_URL` | Frontend-Origin fuer Redirects und CORS; faellt auf `APP_BASE_URL` zurueck | +| `TRUST_PROXY` | Express Trust Proxy, wenn hinter Reverse Proxy | + +## Datenbank + +| Variable | Bedeutung | +| ------------------- | --------------------------- | +| `DATABASE_HOST` | MySQL Host | +| `DATABASE_PORT` | MySQL Port, Standard `3306` | +| `DATABASE_NAME` | Datenbankname | +| `DATABASE_USER` | Datenbankbenutzer | +| `DATABASE_PASSWORD` | Datenbankpasswort | +| `DATABASE_SSL` | TLS fuer MySQL-Verbindung | + +Die Datenbank muss MySQL 8 und `utf8mb4` unterstuetzen. Migrationen werden +separat ausgefuehrt. + +## OIDC + +| Variable | Bedeutung | +| ------------------------- | ------------------------------------------------------- | +| `OIDC_ISSUER` | Exakter Issuer des Providers | +| `OIDC_CLIENT_ID` | Client ID | +| `OIDC_CLIENT_SECRET` | Client Secret | +| `OIDC_SCOPES` | Scopes, typischerweise `openid profile email` | +| `OIDC_LOGOUT_URL` | Optionale IdP-Logout-URL, falls Discovery keine liefert | +| `OIDC_ALLOWED_ALGORITHMS` | Erlaubte ID-Token-Signaturalgorithmen, z. B. `RS256` | +| `OIDC_HTTP_TIMEOUT_MS` | Timeout fuer IdP-HTTP-Requests | + +Der Callback ist immer: + +```text +/api/auth/callback +``` + +Beim App-Logout wird zuerst die lokale Session widerrufen. Danach leitet die App +zum OIDC `end_session_endpoint` aus Discovery weiter. Wenn der Provider diesen +Endpoint nicht publiziert, kann `OIDC_LOGOUT_URL` gesetzt werden. Die App sendet +`client_id`, `post_logout_redirect_uri` und, falls vorhanden, `id_token_hint`. + +## Sessions und CSRF + +| Variable | Bedeutung | +| ---------------------------------- | --------------------------------------------------------- | +| `SESSION_COOKIE_NAME` | Name des signierten Session-Cookies | +| `SESSION_IDLE_TIMEOUT_SECONDS` | Inaktivitaetsablauf | +| `SESSION_ABSOLUTE_TIMEOUT_SECONDS` | Absoluter Ablauf unabhaengig von Aktivitaet | +| `SESSION_SECRET` | Secret fuer Cookie-Signatur, mindestens 32 Zeichen | +| `SESSION_ENCRYPTION_KEY` | Secret fuer Token-Verschluesselung, mindestens 32 Zeichen | +| `CSRF_HEADER_NAME` | Header fuer CSRF-Token, Standard `X-CSRF-Token` | + +In Produktion duerfen diese Secrets keine Beispielwerte enthalten. Die +Konfiguration bricht bei offensichtlichen Platzhaltern ab. + +## CORS, Logging, Swagger und Rate Limits + +| Variable | Bedeutung | +| ------------------------------------- | ---------------------------------------- | +| `CORS_ORIGINS` | Kommagetrennte erlaubte Origins | +| `LOG_LEVEL` | Pino Log Level | +| `SWAGGER_ENABLED` | Swagger UI und JSON aktivieren | +| `RATE_LIMIT_WINDOW_SECONDS` | Globales IP-Zeitfenster | +| `RATE_LIMIT_MAX_REQUESTS` | Globale Requests pro IP und Zeitfenster | +| `RATE_LIMIT_SENSITIVE_WINDOW_SECONDS` | Zeitfenster fuer sensible Endpunkte | +| `RATE_LIMIT_SENSITIVE_MAX_REQUESTS` | Requests pro IP auf sensiblen Endpunkten | + +`SWAGGER_ENABLED` sollte in Produktion nur bewusst aktiviert werden. Das +eingebaute Rate Limiting ist In-Memory, pro Prozess und nicht fuer horizontale +Skalierung koordiniert. Es ist fuer genau eine Containerinstanz ausgelegt. + +## Beispiel fuer Produktion + +```env +NODE_ENV=production +PORT=3000 +APP_BASE_URL=https://business.example.com +FRONTEND_BASE_URL=https://business.example.com +TRUST_PROXY=true +DATABASE_HOST=mysql.internal +DATABASE_PORT=3306 +DATABASE_NAME=business_app +DATABASE_USER=business_app +DATABASE_PASSWORD= +DATABASE_SSL=true +OIDC_ISSUER=https://idp.example.com/realms/internal +OIDC_CLIENT_ID=business-app +OIDC_CLIENT_SECRET= +OIDC_SCOPES=openid profile email +OIDC_LOGOUT_URL=https://idp.example.com/realms/internal/protocol/openid-connect/logout +OIDC_ALLOWED_ALGORITHMS=RS256 +OIDC_HTTP_TIMEOUT_MS=5000 +SESSION_COOKIE_NAME=app_session +SESSION_IDLE_TIMEOUT_SECONDS=28800 +SESSION_ABSOLUTE_TIMEOUT_SECONDS=604800 +SESSION_SECRET= +SESSION_ENCRYPTION_KEY= +CORS_ORIGINS=https://business.example.com +CSRF_HEADER_NAME=X-CSRF-Token +LOG_LEVEL=info +SWAGGER_ENABLED=false +RATE_LIMIT_WINDOW_SECONDS=60 +RATE_LIMIT_MAX_REQUESTS=300 +RATE_LIMIT_SENSITIVE_WINDOW_SECONDS=60 +RATE_LIMIT_SENSITIVE_MAX_REQUESTS=10 +``` diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..5ac7844 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,139 @@ +# Deployment und Betrieb + +Die Anwendung wird als ein Docker-Image ausgeliefert. Das Image enthaelt das +gebaute NestJS-Backend und das gebaute Angular-Frontend. + +## Release-Build + +Vor jedem Release: + +```bash +npm ci +npm run lint +npm run format:check +npm run typecheck +npm test +npm run build +docker build -t registry.example.com/business-app: . +``` + +`npm run build` fuehrt aus: + +1. API-Client generieren +2. API-Client bauen +3. Backend bauen +4. Frontend bauen +5. Frontend nach `apps/backend/dist/public` kopieren + +Das Dockerfile baut erneut im Container und erzeugt anschliessend ein schlankes +Runtime-Image mit Non-Root-User. + +## Runtime-Konfiguration + +Konfiguration erfolgt ueber Umgebungsvariablen. Nutze `.env.example` nur als +Vorlage. Production-Secrets gehoeren in Secret Management, CI/CD Variables oder +eine geschuetzte Server-Konfiguration. + +Wichtige Produktionswerte: + +- `NODE_ENV=production` +- `APP_BASE_URL=https://app.example.com` +- `FRONTEND_BASE_URL=https://app.example.com` +- `TRUST_PROXY=true`, wenn hinter Reverse Proxy +- `DATABASE_SSL=true`, wenn die Datenbank TLS erzwingt +- `SWAGGER_ENABLED=false`, ausser bewusst anders entschieden +- starke Werte fuer `SESSION_SECRET`, `SESSION_ENCRYPTION_KEY`, `OIDC_CLIENT_SECRET` + +## Migrationen + +Migrationen werden nicht beim App-Start ausgefuehrt. Fuehre sie vor dem neuen +App-Container aus, mit demselben Image und derselben Konfiguration: + +```bash +docker run --rm --env-file .env registry.example.com/business-app: node apps/backend/dist/database/run-migrations.js +``` + +Danach den App-Container starten oder aktualisieren. Wenn Migrationen fehlen, +schlaegt der App-Start fehl beziehungsweise `/health/ready` bleibt nicht bereit. + +## Container starten + +Ein einfaches Compose-Beispiel liegt in `compose.yml`: + +```bash +docker compose up -d +``` + +Der Container: + +- laeuft als Non-Root-User +- verwendet ein read-only Root Filesystem +- nutzt `/tmp` als tmpfs +- dropt Linux Capabilities +- prueft `/health/ready` als Healthcheck + +Compose baut oder nutzt das Image `internal-business-app:latest`. Fuer echte +Deployments sollte ein versioniertes Registry-Image verwendet werden. + +## Reverse Proxy + +Nginx oder ein Load Balancer sollte TLS terminieren und alle Routen an den +App-Container weiterleiten. Die Angular-Dateien werden nicht separat durch Nginx +ausgeliefert. Ein Beispiel liegt in `docs/nginx-example.conf`. + +Wichtig: + +- WebSocket-Konfiguration ist fuer dieses Boilerplate nicht erforderlich. +- `X-Forwarded-Proto` und `X-Forwarded-For` sollten gesetzt werden. +- `APP_BASE_URL` muss zur externen HTTPS-URL passen. +- Wenn `TRUST_PROXY=true` gesetzt ist, muss der Proxy vertrauenswuerdig sein. + +## Healthchecks + +- `/health/live`: Prozess lebt. +- `/health/ready`: Datenbank erreichbar, Migration-Check initialisiert und keine + offenen Migrationen. + +Readiness ist der richtige Check fuer Rolling Deployments und Container +Orchestrierung. + +## TeamCity-Beispiel + +1. Checkout +2. `npm ci` +3. `npm run lint` +4. `npm run format:check` +5. `npm run typecheck` +6. `npm test` +7. `npm run build` +8. `docker build -t /: .` +9. `docker push /:` +10. Migration-Job mit neuem Image ausfuehren +11. App-Service auf neues Image aktualisieren +12. `/health/ready` pruefen + +## Rollback + +Ein Rollback ist nur dann einfach, wenn die Datenbankmigrationen rueckwaerts +kompatibel geplant wurden. Fuer riskante Schema-Aenderungen sollte das Expand- +Contract-Muster verwendet werden: + +1. Neue Spalten oder Tabellen hinzufuegen, alte weiter bedienen. +2. Anwendung umstellen. +3. Daten migrieren. +4. Alte Spalten oder Pfade in einem spaeteren Release entfernen. + +## Betrieb + +Beobachte mindestens: + +- HTTP-Fehlerraten und Latenzen +- Healthcheck-Status +- Datenbankverbindungen +- Login-Fehler vom OIDC Provider +- `MIGRATION_MISSING` +- Rate-Limit-Treffer, insbesondere `RATE_LIMIT_EXCEEDED` auf Login- und Admin-Endpunkten +- Audit-Log fuer administrative Aktionen + +Logs sollten zentral gesammelt werden. Request IDs helfen dabei, Frontend-Fehler, +Backend-Logs und Audit-Eintraege zusammenzufuehren. diff --git a/docs/design-system.md b/docs/design-system.md new file mode 100644 index 0000000..5e5d105 --- /dev/null +++ b/docs/design-system.md @@ -0,0 +1,114 @@ +# Designsystem + +Das Frontend verwendet ein eigenes, schlankes Designsystem ohne externe +UI-Library. Es besteht aus zentralen CSS Custom Properties, globalen +Grundklassen und wenigen Angular-UI-Komponenten unter +`apps/frontend/src/app/shared/ui`. + +## Prinzipien + +- sachliche Business-Oberflaeche statt Marketing-Optik +- mobile first, keine globale Mindestbreite +- klare Hierarchie durch Typografie, Abstand und Rahmen +- Farben immer semantisch ueber Tokens +- sichtbare Fokuszustaende und grosse Touch-Flaechen +- Komponenten nur dort, wo sie Verhalten oder Wiederverwendung bringen + +## Tokens + +Die Tokens liegen in `apps/frontend/src/styles/_tokens.scss` und werden ueber +`apps/frontend/src/styles.scss` eingebunden. + +Wichtige Gruppen: + +- Farben: `--color-primary`, `--color-danger`, `--color-surface`, + `--color-text-primary`, `--color-border`, `--color-focus` +- Abstaende: `--space-1` bis `--space-9` +- Typografie: `--font-size-xs` bis `--font-size-2xl`, `--line-height-*`, + `--font-weight-*` +- Layout: `--container-width`, `--sidebar-width`, `--header-height`, + `--touch-target`, `--input-height`, `--button-height` +- Oberflaeche: `--radius-*`, `--shadow-*`, `--z-*`, `--transition-*` + +Feature-Komponenten duerfen keine direkten Hex-Farben enthalten. Neue Farben +werden zuerst als semantische Tokens angelegt. + +## Globale Klassen + +Globale Klassen sind bewusst begrenzt: + +- Layout: `.ui-page`, `.ui-page-header`, `.ui-grid`, `.ui-card`, + `.ui-toolbar`, `.ui-actions` +- Formulare: `.ui-form`, `.ui-form-field`, `.ui-control`, `.ui-checkbox`, + `.ui-field-error` +- Buttons: `.ui-button`, `.ui-icon-button` +- Tabellen: `.ui-table-wrap`, `.ui-table` +- Status: `.ui-badge`, `.ui-badge--success`, `.ui-badge--warning`, + `.ui-badge--danger`, `.ui-badge--info` +- Utilities: `.visually-hidden`, `.truncate`, `.stack`, `.cluster`, + `.full-width`, `.text-muted` + +Keine neuen Utility-Klassen einfuehren, wenn eine lokale Klasse oder bestehende +UI-Komponente ausreicht. + +## Angular-Komponenten + +Wiederverwendbare UI-Bausteine: + +- `UiButtonComponent` +- `UiIconComponent` +- `UiIconButtonComponent` +- `UiFormFieldComponent` +- `UiStatusBadgeComponent` +- `UiConfirmDialogComponent` +- `ToastService` und `UiToastHostComponent` +- `UiEmptyStateComponent` +- `UiLoadingStateComponent` +- `UiPaginationComponent` +- `UiPageHeaderComponent` + +Neue Feature-Seiten sollen diese Bausteine bevorzugen, wenn sie Button-, Badge-, +Dialog-, Toast-, Empty-, Loading- oder Pagination-Verhalten brauchen. + +## Responsive Regeln + +- Mobile Layouts sind einspaltig. +- Aktionen duerfen mobil untereinander stehen und volle Breite nutzen. +- Business-Listen werden mobil als Karten dargestellt. +- Tabellen liegen in `.ui-table-wrap`, wenn eine echte Tabelle sinnvoll bleibt. +- Touch-Ziele orientieren sich an `--touch-target`. +- Breakpoints werden in rem formuliert und nicht nach Geraetetyp benannt. + +## Accessibility + +- native HTML-Elemente vor ARIA verwenden +- interaktive Elemente sind Buttons oder Links +- sichtbare Fokuszustaende nicht entfernen +- Labels ersetzen Placeholder nicht +- Fehlertexte stehen direkt am Feld +- Status ist nicht nur Farbe, sondern auch Text/Marker +- Dialoge setzen Fokus, schliessen per Escape und geben Fokus zurueck +- Navigation und Drawer sind per Tastatur bedienbar +- Animationen respektieren `prefers-reduced-motion` + +## Entwicklungsseite + +Die interne Referenzseite liegt unter: + +```text +/dev/design-system +``` + +Sie ist mit `devOnlyGuard` geschuetzt und im Production-Modus nicht matchbar. +Sie ersetzt kein Storybook, sondern zeigt die vorhandenen Tokens und Komponenten +innerhalb der echten Anwendung. + +## Regeln fuer neue UI + +1. Bestehende UI-Komponenten oder globale Klassen wiederverwenden. +2. Keine direkte Hex-Farbe in Feature-Komponenten. +3. Keine neue UI-Library einfuehren. +4. Keine tiefen Selektoren, kein `::ng-deep`, kein unkontrolliertes + `!important`. +5. Keine klickbaren `div`-Elemente als Ersatz fuer Buttons oder Links. +6. Verhalten mit Vitest testen, insbesondere Accessibility-relevante Zustaende. diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..ebb837c --- /dev/null +++ b/docs/development.md @@ -0,0 +1,119 @@ +# Entwicklung + +Diese Datei beschreibt die normalen Entwicklungsablaeufe im Monorepo. + +## Installation + +```bash +npm ci +``` + +Abhaengigkeiten werden nur im Root installiert. Workspace-spezifische Befehle +werden ueber Root-Scripts oder `npm --workspace ...` ausgefuehrt. + +## Lokaler Start + +```bash +npm run dev +``` + +Alternativ getrennt: + +```bash +npm run dev:frontend +npm run dev:backend +``` + +Das Frontend nutzt den Proxy in `apps/frontend/proxy.conf.json`, damit `/api` +lokal an das Backend weitergereicht wird. + +## Feature-Workflow + +1. Backend-DTOs und Entities modellieren. +2. Repository fuer Datenzugriff erstellen oder erweitern. +3. Service mit Fachlogik implementieren. +4. Controller nur als HTTP-Rand und Permission-Deklaration verwenden. +5. Permission in `roles/permissions.ts` ergaenzen, falls noetig. +6. Migration erzeugen und kontrollieren. +7. API-Client-Generator aktualisieren. +8. Frontend-Route, Navigation und Page bauen. +9. Tests fuer Verhalten schreiben. +10. Qualitaetsbefehle ausfuehren. + +## Migrationen + +Status anzeigen: + +```bash +npm run migration:status +``` + +Migration erzeugen: + +```bash +npm run migration:generate +``` + +Migration ausfuehren: + +```bash +npm run migration:run +``` + +Generierte Migrationen muessen reviewed werden. Sie duerfen keine +versehentlichen Datenverluste, falsche Defaults oder umgebungsspezifische Namen +enthalten. + +## API-Client + +Nach API-Aenderungen: + +```bash +npm run api:generate +``` + +Der Client unter `packages/api-client/src` ist generiert. Aendere stattdessen den +Generator oder ersetze ihn spaeter bewusst durch einen OpenAPI-basierten +Generator. + +## Tests + +```bash +npm test +npm run test:backend +npm run test:frontend +``` + +Tests sollen Verhalten pruefen, nicht nur Existenz. Backend-Tests fuer Services +sollen fachliche Regeln, Fehlerfaelle, Permissions und Datenbankinteraktionen +abdecken. Frontend-Tests sollen sichtbares Verhalten, Guards, Interaktionen und +API-Fehlerpfade abdecken. + +Integrationstests gegen MySQL muessen eine separate Testdatenbank verwenden, +deren Name eindeutig `test` enthaelt. Tests duerfen niemals gegen +Produktionsdatenbanken laufen. + +## Qualitaet vor Abschluss + +```bash +npm run lint +npm run format:check +npm run typecheck +npm test +npm run build +docker build . +``` + +Diese Befehle sind die Mindestpruefung fuer Merge und Release. + +## Fehlerformat + +Backend-Fehler werden ueber `ApiExceptionFilter` in ein konsistentes Format +gebracht. Neue fachliche Fehler sollten `ApiError` und `ErrorCode` verwenden, +damit Frontend und Logs stabil bleiben. + +## Logging + +`pino-http` redigiert sensible Header und Token-Felder. Neue Logs duerfen keine +Secrets, Session-IDs, Cookies, Access Tokens, Refresh Tokens oder ID Tokens +enthalten. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..02faa04 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,93 @@ +# Getting Started + +Diese Anleitung bringt eine lokale Entwicklungsumgebung fuer das Boilerplate zum +Laufen. Sie setzt voraus, dass MySQL 8 und ein OIDC Provider bereits verfuegbar +sind. + +## Voraussetzungen + +- Node.js `24.18.0` aus `.nvmrc` +- npm `11.x` +- Docker fuer Image-Builds und spaetere Auslieferung +- MySQL 8 mit `utf8mb4` +- OIDC Client mit Authorization Code Flow, PKCE und Discovery Endpoint + +Das Repository ist ein npm-Workspace-Monorepo. Abhaengigkeiten werden immer aus +dem Root installiert. + +```bash +npm ci +cp .env.example .env +``` + +## Lokale Konfiguration + +Trage in `.env` mindestens folgende Werte ein: + +- `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_NAME`, `DATABASE_USER`, `DATABASE_PASSWORD` +- `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET` +- `SESSION_SECRET` mit mindestens 32 zufaelligen Zeichen +- `SESSION_ENCRYPTION_KEY` mit mindestens 32 zufaelligen Zeichen +- `APP_BASE_URL=http://localhost:3000` +- `FRONTEND_BASE_URL=http://localhost:4200` +- `CORS_ORIGINS=http://localhost:4200,http://localhost:3000` + +Der OIDC Provider muss als Redirect URI diese URL erlauben: + +```text +http://localhost:3000/api/auth/callback +``` + +Falls der Provider RP-Initiated Logout validiert, muss ausserdem +`http://localhost:4200` beziehungsweise die konfigurierte `FRONTEND_BASE_URL` als +Post-Logout-Redirect erlaubt sein. Wenn Discovery keinen `end_session_endpoint` +liefert, setze `OIDC_LOGOUT_URL`. + +## Datenbank vorbereiten + +Die Anwendung fuehrt Migrationen beim normalen Start nicht automatisch aus. +Fuehre sie bewusst aus: + +```bash +npm run migration:status +npm run migration:run +``` + +Wenn Migrationen fehlen, verweigert das Backend den Start beziehungsweise +`/health/ready` bleibt nicht bereit. + +## Entwicklung starten + +```bash +npm run dev +``` + +Das startet: + +- Frontend: `http://localhost:4200` +- Backend: `http://localhost:3000` +- API: `http://localhost:3000/api` +- Health: `http://localhost:3000/health/live` und `/health/ready` +- Swagger, falls `SWAGGER_ENABLED=true`: `http://localhost:3000/api/docs` + +Das Frontend proxyt `/api` ueber `apps/frontend/proxy.conf.json` an das Backend. +Dadurch kann lokal mit Cookie-basierter Authentifizierung gearbeitet werden. + +## Erster Login + +1. Oeffne `http://localhost:4200`. +2. Melde dich ueber den OIDC Provider an. +3. Der erste lokal angelegte Benutzer erhaelt automatisch die Rollen `user` und + `admin`. +4. Weitere Benutzer erhalten initial die Rolle `user`. + +Systemrollen und Permissions werden beim Login synchronisiert. Permissions sind +im Code definiert und werden nicht frei in der UI angelegt. + +## Haefige Probleme + +- `Ungueltige Konfiguration`: `.env` verletzt das Schema in `apps/backend/src/config/env.ts`. +- `MIGRATION_MISSING`: `npm run migration:run` ausfuehren. +- `UNAUTHORIZED`: Session abgelaufen, Benutzer deaktiviert oder OIDC-Konfiguration falsch. +- `CSRF_INVALID`: Schreibender Request ohne `X-CSRF-Token`; im Angular-Client erledigt das der Interceptor. +- OIDC Callback schlaegt fehl: Redirect URI, Issuer, Client Secret und erlaubte Algorithmen pruefen. diff --git a/docs/nginx-example.conf b/docs/nginx-example.conf new file mode 100644 index 0000000..659265a --- /dev/null +++ b/docs/nginx-example.conf @@ -0,0 +1,32 @@ +server { + listen 443 ssl http2; + server_name app.example.com; + + ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem; + + client_max_body_size 1m; + proxy_connect_timeout 5s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + add_header X-Content-Type-Options nosniff always; + add_header Referrer-Policy strict-origin-when-cross-origin always; + + location / { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Request-ID $request_id; + } +} + +server { + listen 80; + server_name app.example.com; + return 301 https://$host$request_uri; +} diff --git a/docs/notifications.md b/docs/notifications.md new file mode 100644 index 0000000..eaea5d2 --- /dev/null +++ b/docs/notifications.md @@ -0,0 +1,146 @@ +# Benachrichtigungen + +Das Modul `apps/backend/src/notifications` stellt persoenliche In-App- +Benachrichtigungen bereit. Es gibt bewusst keinen E-Mail-Versand, keine Push +Notifications, keine WebSockets, keine Server-Sent Events, keine Queue, kein +Redis und keine Hintergrundjobs. Neue Benachrichtigungen werden beim Laden der +Anwendung und per Frontend-Polling abgerufen. + +## Datenmodell + +Die Entity `NotificationEntity` wird in der Tabelle `notifications` gespeichert. +Jede Benachrichtigung gehoert genau einem Benutzer. + +Felder: + +- `id`: UUID +- `userId`: Foreign Key auf `users.id` +- `type`: technischer Typ aus `NotificationType` +- `title`: kurzer Plain-Text-Titel, maximal 150 Zeichen +- `message`: Plain-Text-Nachricht, maximal 1000 Zeichen +- `link`: optionale interne Angular-Route, maximal 500 Zeichen +- `metadata`: optionale JSON-Metadaten, maximal 4096 Bytes serialisiert +- `readAt`: `null`, solange ungelesen +- `createdAt`: UTC-Erstellungszeit +- `deletedAt`: Soft Delete + +Indizes: + +- `user_id, created_at` +- `user_id, read_at` +- `user_id, deleted_at` + +Die Migration liegt unter +`apps/backend/src/database/migrations/1720000001000-AddNotifications.ts` und wird +nicht automatisch beim App-Start ausgefuehrt. + +## Typen + +Benachrichtigungstypen sind Code, keine Datenbankdaten: + +- `system` +- `item.created` +- `item.updated` +- `user.role-changed` + +Neue Module erweitern `apps/backend/src/notifications/notification-types.ts` +und nutzen anschliessend `NotificationsService`. + +## Permissions + +- `notifications.readOwn`: eigene Benachrichtigungen lesen +- `notifications.updateOwn`: eigene Benachrichtigungen markieren oder loeschen +- `notifications.manage`: administrativ Benachrichtigungen erzeugen + +Die Systemrolle `user` erhaelt `notifications.readOwn` und +`notifications.updateOwn`. Die Rolle `admin` erhaelt ueber `allPermissions` +zusaetzlich `notifications.manage`. + +## API + +Alle Endpunkte liegen unter dem bestehenden API-Praefix `/api`. + +- `GET /notifications`: eigene Benachrichtigungen, mit `page`, `pageSize` und + `status=all|read|unread` +- `GET /notifications/unread-count`: Anzahl ungelesener eigener + Benachrichtigungen +- `PATCH /notifications/:id/read`: idempotent als gelesen markieren +- `PATCH /notifications/:id/unread`: idempotent als ungelesen markieren +- `PATCH /notifications/read-all`: alle eigenen Benachrichtigungen als gelesen + markieren +- `DELETE /notifications/:id`: Soft Delete einer eigenen Benachrichtigung +- `POST /admin/notifications`: administrative Erzeugung mit + `notifications.manage` + +Normale Benutzer koennen keine fremde `userId` uebergeben. Der aktuelle Benutzer +wird serverseitig aus der Session bestimmt. + +## Interner Service + +Andere Backend-Module erzeugen Benachrichtigungen ueber +`NotificationsService`, nicht direkt ueber ein Repository: + +```ts +await notifications.createForUser({ + userId, + type: NotificationType.ItemCreated, + title: 'Neuer Eintrag', + message: 'Der Eintrag "Beispiel" wurde erstellt.', + link: '/items/123', + metadata: { itemId: '123' }, +}); +``` + +Verfuegbare Methoden: + +- `createForUser` +- `createForUsers` +- `getForCurrentUser` +- `getUnreadCount` +- `markAsRead` +- `markAsUnread` +- `markAllAsRead` +- `softDelete` + +`createForUsers` begrenzt Bulk-Erzeugung auf 100 Zielbenutzer. + +## Integrationen + +`ItemsService` benachrichtigt beim Erstellen eines Items den ersten aktiven +Administrator, sofern dieser nicht der Ersteller ist. Das ist eine bewusst kleine +Beispielregel und keine vollstaendige fachliche Eskalationslogik. + +`UsersService` benachrichtigt betroffene Benutzer nach erfolgreicher +Rollenaenderung. Fehler beim Erzeugen werden geloggt; die bereits erfolgreiche +sicherheitsrelevante Rollenaenderung wird dadurch nicht unkontrolliert +zurueckgerollt. + +Administrative Erzeugung wird im Audit-Log als `NOTIFICATION_CREATED` +protokolliert. Geloggt werden Actor, Zielbenutzer, Notification-ID, Typ und +Request-ID, nicht die vollstaendige Nachricht oder Metadata. + +## Frontend + +`NotificationStore` verwendet Angular Signals fuer lokalen State und RxJS fuer +HTTP und Polling. Header-Panel und Seite `/notifications` verwenden denselben +Store, damit keine doppelten Requests fuer dieselben Daten entstehen. + +Polling: + +- Standardintervall: 60 Sekunden ueber `NOTIFICATION_POLL_INTERVAL_MS` +- nur bei angemeldetem Benutzer +- pausiert bei unsichtbarem Tab +- aktualisiert sofort beim Sichtbarwerden +- verhindert ueberlappende Count-Requests +- stoppt und leert State beim Logout + +Links werden im Frontend nur navigiert, wenn sie interne relative Routen sind. +Titel und Nachricht werden normal interpoliert und nicht per `innerHTML` +gerendert. + +## Spaetere Echtzeitkommunikation + +Wenn spaeter echte Echtzeitkommunikation noetig wird, sollte das als separate +Architekturentscheidung erfolgen. Dann waeren Transport, Skalierung, +Authentifizierung, Backpressure und Betrieb gemeinsam zu entscheiden, statt +WebSockets oder Queues nebenbei in das In-App-Modul einzubauen. diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..2b764ec --- /dev/null +++ b/docs/security.md @@ -0,0 +1,135 @@ +# Security-Modell + +Das Boilerplate trennt Browser, Backend und Identity Provider strikt. Der Browser +bekommt keine OIDC-Tokens. Das Backend ist fuer Authentifizierung, +Autorisierung, CSRF und Session-Verwaltung verbindlich. + +## OIDC + +Der Login nutzt Authorization Code Flow mit PKCE: + +1. Browser ruft `/api/auth/login` auf. +2. Backend erzeugt `state`, `nonce`, `code_verifier` und leitet zum IdP weiter. +3. IdP ruft `/api/auth/callback` mit `code` und `state` auf. +4. Backend tauscht den Code gegen Tokens. +5. Backend validiert ID Token, Issuer, Audience, Nonce und Algorithmus. +6. Backend laedt optional UserInfo. +7. Backend legt oder aktualisiert den lokalen Benutzer. +8. Backend erzeugt eine serverseitige Session. + +Erlaubte Signaturalgorithmen werden ueber `OIDC_ALLOWED_ALGORITHMS` gesetzt. +`none` ist explizit verboten. + +Beim Logout wird zuerst die lokale Session widerrufen und das Session-/CSRF-Cookie +geloescht. Anschliessend redirectet das Backend zum OIDC +`end_session_endpoint` aus Discovery oder zur optionalen `OIDC_LOGOUT_URL`. Wenn +die Session ein ID-Token enthaelt, wird es nur als `id_token_hint` an den IdP +gegeben und nicht an das Frontend ausgeliefert. + +## Sessions + +Sessions liegen in MySQL. Das Session-Cookie enthaelt keine Tokens oder +Benutzerdaten, sondern nur eine signierte Session-ID. + +Gespeichert werden unter anderem: + +- verschluesselte OIDC-Tokens +- Hash des CSRF-Tokens +- Ablaufzeiten fuer Idle Timeout und absolutes Timeout +- User-Agent und IP fuer Anzeige und Audit-Kontext +- Revocation-Zeitpunkt + +Die Token-Verschluesselung nutzt `SESSION_ENCRYPTION_KEY`. Cookie-Signaturen +nutzen `SESSION_SECRET`. Beide Werte muessen in Produktion echte Secrets sein. + +## CSRF + +Schreibende Methoden `POST`, `PUT`, `PATCH` und `DELETE` brauchen ein gueltiges +CSRF-Token. Das Frontend liest das CSRF-Cookie und sendet es als +`X-CSRF-Token`. Das Backend vergleicht nur den Hash gegen die Session. + +Public Routes sind vom CSRF-Guard ausgenommen, sofern sie mit `@Public()` +markiert sind. + +## Rollen und Permissions + +Permissions werden in `apps/backend/src/roles/permissions.ts` definiert. Rollen +sind Datenbankdaten und referenzieren diese Permissions. Benutzer haben keine +direkten Permissions. + +Backend-Controller schuetzen Endpunkte mit: + +```ts +@RequirePermissions(Permission.ItemsRead) +``` + +Angular nutzt Permissions nur fuer Navigation und Darstellung. Eine versteckte +Schaltflaeche ist keine Sicherheitsgrenze. + +Administrative Benutzer-, Rollen- und Session-Aktionen sind unter `/api/admin/*` +mit `users.read`, `users.manage`, `roles.read`, `roles.manage`, +`sessions.manage` und `audit.read` geschuetzt. Aktionen, die den letzten aktiven +Administrator entfernen koennten, laufen transaktional und antworten bei Verstoss +mit `LAST_ACTIVE_ADMIN_REQUIRED`. + +Benachrichtigungen sind benutzerbezogene Daten. Normale Notification-Endpunkte +bestimmen den Benutzer ausschliesslich aus der serverseitig aufgeloesten Session. +Benutzer-IDs werden fuer eigene Benachrichtigungen nicht als Query-Parameter oder +Body-Feld akzeptiert. Fremde oder geloeschte Notification-IDs werden als nicht +gefunden behandelt. + +## Benutzerstatus + +Deaktivierte Benutzer werden trotz erfolgreichem IdP-Login abgewiesen. Aktive +Sessions deaktivierter Benutzer werden beim Session-Resolve unbrauchbar gemacht. + +## Cookies und Proxy + +In Produktion sollte TLS vor der Anwendung terminiert werden, zum Beispiel mit +Nginx. Setze `TRUST_PROXY=true`, wenn die Anwendung hinter einem vertrauenswuerdigen +Reverse Proxy laeuft und korrekte Forwarded Header benoetigt. + +`APP_BASE_URL` muss die externe URL der Anwendung enthalten, weil daraus die OIDC +Callback URL gebaut wird. + +## Logging und Redaction + +Das Backend redigiert sensible Werte in Standard-HTTP-Logs: + +- `Authorization` +- `Cookie` +- `X-CSRF-Token` +- `Set-Cookie` +- Access-, Refresh- und ID-Tokens + +Neue Logs duerfen keine Secrets, Tokens, Session-IDs oder vollstaendige Cookies +enthalten. Fuer Auditing sollten fachliche IDs und Request IDs verwendet werden. + +## Swagger + +`SWAGGER_ENABLED` ist in Produktion standardmaessig `false`. Wenn Swagger in +Produktion aktiviert wird, sollte der Zugriff ueber Netzwerkregeln oder Reverse +Proxy zusaetzlich eingeschraenkt werden. + +## Rate Limiting + +Das konfigurierte Rate Limiting ist fuer eine einzelne Containerinstanz ohne +Redis ausgelegt. Die Zaehler liegen im Prozessspeicher. Bei horizontaler +Skalierung muss ein gemeinsamer Store eingefuehrt oder der Schutz an den Reverse +Proxy verlagert werden. + +Es gibt zwei fruehe Ebenen: + +- Globales IP-Limit fuer alle Requests, bevor Guards und Controller laufen. +- Strengeres IP-Limit fuer sensible Endpunkte, ebenfalls bevor Session-Resolve + und fachliche Datenbankzugriffe laufen. + +Sensible Endpunkte werden mit `@SensitiveRateLimit()` markiert. Dazu gehoeren +Login, OIDC Callback, Logout, Session-Revoke sowie Benutzer- und +Rollenverwaltung. Bei Ueberschreitung antwortet die API mit HTTP 429, +`RATE_LIMIT_EXCEEDED`, `Retry-After` und `X-RateLimit-*` Headern. + +Ein zusaetzliches Benutzer-Limit waere technisch moeglich, wuerde in diesem +Projekt aber erst nach dem serverseitigen Session-Resolve greifen. Fuer die +geforderte fruehe Lastreduktion ist deshalb das globale IP-Limit die verbindliche +Schutzschicht. diff --git a/docs/using-as-template.md b/docs/using-as-template.md new file mode 100644 index 0000000..bbdb900 --- /dev/null +++ b/docs/using-as-template.md @@ -0,0 +1,97 @@ +# Als Projektvorlage Verwenden + +Dieses Boilerplate ist bewusst nah an einer realen internen Business-Anwendung. +Beim Start eines neuen Projekts solltest du zuerst Namen, Branding, +Runtime-Konfiguration und Demo-Fachlichkeit ersetzen, aber die Sicherheits- und +Betriebsentscheidungen beibehalten. + +## Empfohlener Start + +1. Repository kopieren oder als Template verwenden. +2. Paketnamen und Beschreibungen in `package.json`, `apps/*/package.json` und + `packages/api-client/package.json` anpassen. +3. UI-Name `Business App` in `apps/frontend/src/app/layout/app-shell.ts` ersetzen. +4. Swagger-Titel und Beschreibung in `apps/backend/src/main.ts` ersetzen. +5. Docker-Image-Namen in README, CI und Compose auf den Projektnamen aendern. +6. `.env.example` auf die Zielumgebung zuschneiden, aber keine Secrets eintragen. +7. Lizenz, Repository-URL, Ownership und Open-Source-Metadaten bewusst setzen. + +## Was du behalten solltest + +- Backend-for-Frontend Authentifizierung: OIDC-Tokens bleiben im Backend. +- Server-seitige Sessions in MySQL. +- CSRF-Schutz fuer alle schreibenden Requests. +- TypeORM-Migrationen statt `synchronize`. +- Controller delegieren an Services; Datenbankzugriff liegt in Repositories oder + klar benannten Persistence-Services. +- Permissions sind Code, Rollen sind Daten. +- Keine UI-Library als versteckte Projektbindung. +- Striktes TypeScript ohne `any`. + +## Demo-Fachlichkeit entfernen oder ersetzen + +Das Modul `items` ist ein Beispiel fuer ein einfaches fachliches CRUD-Modul mit +optimistischer Versionierung und Soft Delete. Fuer ein neues Projekt gibt es zwei +saubere Optionen: + +- Behalten und in das erste echte Fachmodul umbenennen. +- Entfernen und anhand der Struktur ein neues Modul erstellen. + +Wenn du `items` entfernst, pruefe mindestens: + +- Backend-Import in `apps/backend/src/app.module.ts` +- Entity-Export in `apps/backend/src/database/entities.ts` +- Initiale Migration und neue Migration fuer Schema-Aenderungen +- Permissions in `apps/backend/src/roles/permissions.ts` +- Routen in `apps/frontend/src/app/app.routes.ts` +- Navigation in `apps/frontend/src/app/layout/app-shell.ts` +- API-Client-Generator in `scripts/generate-api-client.mjs` +- Frontend-Pages und Tests unter `apps/frontend/src/app/features/items` + +## Ein neues Feature anlegen + +Backend: + +1. Feature-Ordner unter `apps/backend/src/` anlegen. +2. Entity, DTOs, Repository, Service, Controller und Module erstellen. +3. Controller nur mit Services verdrahten. +4. Datenbankzugriff im Repository kapseln. +5. Permissions in `roles/permissions.ts` ergaenzen. +6. Controller-Methoden mit `@RequirePermissions(...)` schuetzen. +7. Entity in `database/entities.ts` aufnehmen. +8. Migration erzeugen und pruefen. +9. Verhaltenstests fuer Service oder Controller schreiben. + +Frontend: + +1. Page unter `apps/frontend/src/app/features/` erstellen. +2. Route in `app.routes.ts` anlegen. +3. Navigation in `app-shell.ts` ergaenzen. +4. Permission im Route-`data` und in der Navigation konsistent setzen. +5. API-Client ueber `packages/api-client` verwenden. +6. Verhaltenstests fuer relevante UI-Logik schreiben. + +API-Client: + +Aktuell schreibt `scripts/generate-api-client.mjs` den Client reproduzierbar aus +einer gepflegten Vorlage. Wenn die API waechst, muss der Generator erweitert oder +durch eine echte OpenAPI-Codegenerierung ersetzt werden. Der Client selbst ist +generierter Code und sollte nicht manuell editiert werden. + +## Open-Source-Veroeffentlichung + +Vor einer Veroeffentlichung als Open-Source-Projekt: + +- `license` in `package.json` bewusst setzen. +- `LICENSE` und optional `NOTICE` hinzufuegen. +- Namen, Screenshots, Domaenen, Beispiel-Registry und interne Hinweise entfernen. +- Beispiel-Secrets in `.env.example` nur als Platzhalter belassen. +- Pruefen, ob alle Dependencies und generierten Artefakte lizenzkompatibel sind. +- Sicherheitsmodell in README und `docs/security.md` aktuell halten. + +## Projektgrenzen + +Dieses Boilerplate ist fuer eine einzelne deploybare Business-Anwendung gebaut. +Es ist kein Microservice-Framework, kein Nx-Workspace und keine Multi-Tenant +Plattform. Wenn ein Projekt diese Grenzen braucht, sollte die Architektur zuerst +bewusst erweitert werden, statt sie indirekt ueber Feature-Code einzuschleppen. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..441292f --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,52 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; +import prettier from 'eslint-config-prettier'; + +export default tseslint.config( + { + ignores: [ + 'dist/**', + '**/dist/**', + 'coverage/**', + 'node_modules/**', + 'package-lock.json', + 'eslint.config.mjs', + 'apps/frontend/.angular/**', + 'apps/backend/public/**', + 'packages/api-client/dist/**', + 'apps/**/eslint.config.mjs', + '**/vitest.config.ts', + 'scripts/**', + ], + }, + js.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + prettier, + { + files: ['**/*.ts'], + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + globals: { + ...globals.node, + ...globals.browser, + }, + }, + rules: { + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-non-null-assertion': 'error', + '@typescript-eslint/consistent-type-imports': 'error', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-misused-promises': 'error', + }, + }, + { + files: ['**/*.spec.ts', '**/*.test.ts'], + rules: { + '@typescript-eslint/unbound-method': 'off', + }, + }, +); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..300f111 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,15734 @@ +{ + "name": "internal-business-app-boilerplate", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "internal-business-app-boilerplate", + "version": "1.0.0", + "license": "UNLICENSED", + "workspaces": [ + "apps/frontend", + "apps/backend", + "packages/api-client" + ], + "devDependencies": { + "@angular/build": "22.0.6", + "@angular/cli": "22.0.6", + "@angular/compiler-cli": "22.0.6", + "@eslint/js": "9.39.1", + "@nestjs/cli": "11.0.24", + "@nestjs/schematics": "11.0.9", + "@nestjs/testing": "11.1.28", + "@testing-library/angular": "19.4.1", + "@types/cookie-parser": "1.4.10", + "@types/express": "5.0.5", + "@types/node": "24.10.1", + "@types/supertest": "6.0.3", + "concurrently": "9.2.1", + "eslint": "9.39.1", + "eslint-config-prettier": "10.1.8", + "globals": "16.5.0", + "jsdom": "27.2.0", + "prettier": "3.6.2", + "supertest": "7.1.4", + "ts-node": "^10.9.2", + "typescript": "6.0.3", + "typescript-eslint": "8.64.0", + "vitest": "4.0.8" + }, + "engines": { + "node": "24.18.x", + "npm": "11.x" + } + }, + "apps/backend": { + "name": "@boilerplate/backend", + "version": "1.0.0", + "dependencies": { + "@nestjs/common": "11.1.28", + "@nestjs/config": "4.0.4", + "@nestjs/core": "11.1.28", + "@nestjs/platform-express": "11.1.28", + "@nestjs/swagger": "11.2.3", + "@nestjs/throttler": "6.4.0", + "@nestjs/typeorm": "11.0.0", + "class-transformer": "0.5.1", + "class-validator": "0.14.2", + "cookie-parser": "1.4.7", + "dotenv": "17.4.2", + "helmet": "8.1.0", + "jose": "6.1.2", + "mysql2": "3.15.3", + "openid-client": "6.8.1", + "pino": "10.1.0", + "pino-http": "11.0.0", + "pino-pretty": "13.1.2", + "reflect-metadata": "0.2.2", + "rxjs": "7.8.2", + "typeorm": "0.3.27", + "zod": "4.1.13" + }, + "devDependencies": { + "typeorm-ts-node-commonjs": "0.3.20" + } + }, + "apps/backend/node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "apps/backend/node_modules/zod": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "apps/frontend": { + "name": "@boilerplate/frontend", + "version": "1.0.0", + "dependencies": { + "@angular/common": "22.0.6", + "@angular/compiler": "22.0.6", + "@angular/core": "22.0.6", + "@angular/forms": "22.0.6", + "@angular/platform-browser": "22.0.6", + "@angular/platform-browser-dynamic": "22.0.6", + "@angular/router": "22.0.6", + "@boilerplate/api-client": "1.0.0", + "rxjs": "7.8.2", + "tslib": "2.8.1" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@algolia/abtesting": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.18.0.tgz", + "integrity": "sha512-8siuLG+FIns1AjZ/g2SDVwHz9S+ObacDQISEJvS8XsNei1zl3FXqfqQrBpmrG7ACWCyesXHbicMJtvRbg00FEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.52.0.tgz", + "integrity": "sha512-wtwPgyPmO7b7sQPVgoK29c1VpfS08DnnJCmxX/oU1pV2DlMRJCzQcLN7JSloYpodyKHwM8+9wOzlAM0co3TDmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.52.0.tgz", + "integrity": "sha512-9KY36bRl4AH7RjqSeDDOKnjsz4IxQFBEOB8/fWmEbdQe+Isbs5jGzVJu9NEPQ1Tgwxlf8Uf07Swj3jZyMNUZ2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.52.0.tgz", + "integrity": "sha512-3a/qM3dzJqqfTx7Yrw7uGQ98I3Q0rDfb4Vkv0wEzko96l7YQMxfBVz/VbLq2N+c59GweYv6Vhp8mPeqnWJSITw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.52.0.tgz", + "integrity": "sha512-Rki7ACbMcvbQW0BuM84x9dkGHY47ABmv4jU6tYssat2k02p3mIUms2YOLUAMeknhmnFsj6lb6ZzOXdMWMyc1sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.52.0.tgz", + "integrity": "sha512-96s4Uzc3kk+/f4jJXIVVGWP5XlngOGNQ1x6hW9AT59pOixHlOs5tqJg+ZUS/GQ6h/iYP0ceQcmxDQeLyCLTaDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.52.0.tgz", + "integrity": "sha512-lqeycNpSPe5Qa0OUWpejVvYQjQWV5nQuLT0a4aq7XzRAvCxprV/6Lf841EygdD2nrFnuS58ok7Au1uOtXzpnkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.52.0.tgz", + "integrity": "sha512-ly1wETVGRo30cx61O7fetESN+ElL9c9K+bD/AVgnT1ar4c6v+/Yqjrhdtu6Fm4D0s4NZP081Isf6tunH1wUXHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.52.0.tgz", + "integrity": "sha512-U4EeTvgmluRjj39ykZSAd5X+a6LD5m7/mcOWDmB7hqm1R6QY0yT8jLxpNVEjYhzgEN5hcDGW6X67EWQY8KiYGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.52.0.tgz", + "integrity": "sha512-FCPnDcILfpTE94u7BVlV4DmnSV5wE3+j25EEF+3dYPrVzkVCSoAHs318oWDGxnxsAgiL4HpL12Jc4XHmw9shpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.52.0.tgz", + "integrity": "sha512-br3DO7n4N8CXwTRbZS0MnB4WQ9YHfNjCwkCEzVR/wek/qNTDQKDb0nROmkFaNZ8ucUqUVKZi074dbwMwRDlK8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.52.0.tgz", + "integrity": "sha512-b0T/Ca2c9KyEslKsVrGZvbe1UrrKKSdfXhBZ2pbpKahFUzJfziRZ0urbOm7V65O0tO/jwU+Lo/+bIiiyhzGt8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.52.0.tgz", + "integrity": "sha512-ozBT8J/mtD4H4IAojw8QPirlcL2gHrI1BGuZ4/ZXXO/rTE1yQ4VIPJj4mTTbwo4FbkS1MoJsD/DsrqLzhnc4/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.52.0.tgz", + "integrity": "sha512-gyyWcLD22tnabmoit4iukCXuoRc5HYJuUjPSEa8a0D/f/NlRafpWi52AlAaa4Uu/rsl7saHsJFTNjTptWbu2+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.2200.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2200.6.tgz", + "integrity": "sha512-wTjUfiT5iEMc9+9z2zo1QGtzLpXk5Xx0bV6/zTO4n6+BCC0qReuYnIYt0iJCh649Q0vHq5++0zmZZK0KyXVMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "22.0.6", + "rxjs": "7.8.2" + }, + "bin": { + "architect": "bin/cli.js" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "22.0.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-22.0.6.tgz", + "integrity": "sha512-uAZ7TPn+8sDZcFtiM5nQpmqaR9lspLAf7RY7t7TXYP794001khRf+F/AhmONxmgFPWY01+eeJSvBx+FEnHCb0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.20.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "22.0.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-22.0.6.tgz", + "integrity": "sha512-7bSiye4UrxvhYvYp1YIpVSVOFQZ75EXP3EE5/ShM3cPNtUTdXJiwHkD8SiLHNhPTB6Mzrq5tp1YlAXXRg/WgLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "22.0.6", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.21", + "ora": "9.4.0", + "rxjs": "7.8.2" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.2.27.tgz", + "integrity": "sha512-wHYH6SVXVykhLzovUHtYor3Nl4SpIiITi7r9DQDaKYUD4hpRBx25W6N9eGuakT9Vd5tV/x6wmvQFWQZQwFB7eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.27", + "@angular-devkit/schematics": "19.2.27", + "@inquirer/prompts": "7.3.2", + "ansi-colors": "4.1.3", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@angular-devkit/core": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.27.tgz", + "integrity": "sha512-3amNzoCVSKd7ah6l6lBQL4onwwJvqvam7FMoQBILrxtW5LB5ezh8gMSPuA4zJjKjoRzf9uoWdlzqv/84I52xZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@angular-devkit/schematics": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.27.tgz", + "integrity": "sha512-/PZmyAlb2NGWPikRRuiWLdfHQd8Wrx6lX4HqvTcaDhlU43M3T0ud4PH2T3QDp7BzHYY92xtD8iPxX2asg67G1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.27", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/prompts": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz", + "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.2", + "@inquirer/confirm": "^5.1.6", + "@inquirer/editor": "^4.2.7", + "@inquirer/expand": "^4.0.9", + "@inquirer/input": "^4.1.6", + "@inquirer/number": "^3.0.9", + "@inquirer/password": "^4.0.9", + "@inquirer/rawlist": "^4.0.9", + "@inquirer/search": "^3.0.9", + "@inquirer/select": "^4.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@angular-devkit/schematics-cli/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/@angular-devkit/schematics-cli/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@angular/build": { + "version": "22.0.6", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-22.0.6.tgz", + "integrity": "sha512-mIp1snH1haw4pKiJwUmKyp9f31NRUTeUwqrl9W59X3Iv54egOnD1EdCc0jsW7WJVr/JHRZYVt/ooFelUB8uV8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.2200.6", + "@babel/core": "7.29.0", + "@babel/helper-annotate-as-pure": "7.27.3", + "@babel/helper-split-export-declaration": "7.24.7", + "@inquirer/confirm": "6.0.12", + "@vitejs/plugin-basic-ssl": "2.3.0", + "beasties": "0.4.2", + "browserslist": "^4.26.0", + "esbuild": "0.28.1", + "https-proxy-agent": "9.0.0", + "jsonc-parser": "3.3.1", + "listr2": "10.2.1", + "magic-string": "0.30.21", + "mrmime": "2.0.1", + "parse5-html-rewriting-stream": "8.0.1", + "picomatch": "4.0.4", + "piscina": "5.2.0", + "rollup": "4.60.2", + "sass": "1.99.0", + "semver": "7.7.4", + "source-map-support": "0.5.21", + "tinyglobby": "0.2.16", + "vite": "7.3.5", + "watchpack": "2.5.1" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "lmdb": "3.5.4" + }, + "peerDependencies": { + "@angular/compiler": "^22.0.0", + "@angular/compiler-cli": "^22.0.0", + "@angular/core": "^22.0.0", + "@angular/localize": "^22.0.0", + "@angular/platform-browser": "^22.0.0", + "@angular/platform-server": "^22.0.0", + "@angular/service-worker": "^22.0.0", + "@angular/ssr": "^22.0.6", + "istanbul-lib-instrument": "^6.0.0", + "karma": "^6.4.0", + "less": "^4.2.0", + "ng-packagr": "^22.0.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "tslib": "^2.3.0", + "typescript": ">=6.0 <6.1", + "vitest": "^4.0.8" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, + "@angular/localize": { + "optional": true + }, + "@angular/platform-browser": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "istanbul-lib-instrument": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@angular/cli": { + "version": "22.0.6", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-22.0.6.tgz", + "integrity": "sha512-/sEN1hVcywSML+yWrr/T7gbuGfIrZwKC+WhiOv5GksSQVUrSujWnMX4meRJ5ocG/Ie8UN48LJ7LVDsm32NBt1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.2200.6", + "@angular-devkit/core": "22.0.6", + "@angular-devkit/schematics": "22.0.6", + "@inquirer/prompts": "8.4.2", + "@listr2/prompt-adapter-inquirer": "4.2.3", + "@modelcontextprotocol/sdk": "1.29.0", + "@schematics/angular": "22.0.6", + "@yarnpkg/lockfile": "1.1.0", + "algoliasearch": "5.52.0", + "ini": "6.0.0", + "jsonc-parser": "3.3.1", + "listr2": "10.2.1", + "npm-package-arg": "13.0.2", + "pacote": "21.5.1", + "parse5-html-rewriting-stream": "8.0.1", + "semver": "7.7.4", + "yargs": "18.0.0", + "zod": "4.4.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "22.0.6", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-22.0.6.tgz", + "integrity": "sha512-+sC0W0FWGeZ/HKUhueCBX8kXdFQDlStbUSK6WF/32igfNYR/PdUrp2L5bU7q654yjxnMzFWDAB+Uvzv6YI/cQg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/core": "22.0.6", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "22.0.6", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-22.0.6.tgz", + "integrity": "sha512-iFHsXOA14TsxK4jowmGfogI9Jb0ApL3z8EGTGvKMialqtw088HYjwJP1LT4OlO/wgmjSTq2peZ19jLPuhevNvw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@angular/compiler-cli": { + "version": "22.0.6", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-22.0.6.tgz", + "integrity": "sha512-HPiceCIEyIcTmOuIFiyqu04kbLoJhSUavcOy/kzJkJ2OPXtPvGKaimC4lgs0C86uFy4P6rj6PDITBe3AVHkMTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.29.7", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^5.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^18.0.0" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/compiler": "22.0.6", + "typescript": ">=6.0 <6.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/core": { + "version": "22.0.6", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-22.0.6.tgz", + "integrity": "sha512-9AYGxmZlJhjLSepZaF/cn1LTC9xRHpUPtXlyyJdauTb4f77X/P0kVdmADztgF1Qy5oMd1V9UKebAe81gmqOH2g==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/compiler": "22.0.6", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0 || ~0.16.0" + }, + "peerDependenciesMeta": { + "@angular/compiler": { + "optional": true + }, + "zone.js": { + "optional": true + } + } + }, + "node_modules/@angular/forms": { + "version": "22.0.6", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-22.0.6.tgz", + "integrity": "sha512-pZGMk1TvT+m8z2ZGm90W0sBlo6fhtMAA2baZbMt3HbGRmY23vZv1Orv9npX3Z6a9CCNDY3XBMBvkuj8g7vde9Q==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "tslib": "^2.3.0", + "zod": "^4.0.10" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/common": "22.0.6", + "@angular/core": "22.0.6", + "@angular/platform-browser": "22.0.6", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "22.0.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-22.0.6.tgz", + "integrity": "sha512-DyyYRx4jXlfaenf0mwsp9jcIak/c17QCFgJKA7OauSkQnk3dL+voMYEwdigfnTIUdw+2Y9wYDywLeuM6NychNQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/animations": "22.0.6", + "@angular/common": "22.0.6", + "@angular/core": "22.0.6" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "22.0.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-22.0.6.tgz", + "integrity": "sha512-VKf8mg+BviBp3wlElZYxWFuXtMLQqd8rfm6jEzwP1Emaz2MCx69Y54dXopD81KJCqRwTmJUVxOjoubqUOlJzRw==", + "deprecated": "@angular/platform-browser-dynamic is deprecated. Use `@angular/platform-browser` instead.", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/common": "22.0.6", + "@angular/compiler": "22.0.6", + "@angular/core": "22.0.6", + "@angular/platform-browser": "22.0.6" + } + }, + "node_modules/@angular/router": { + "version": "22.0.6", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-22.0.6.tgz", + "integrity": "sha512-s0BErBBadCXlvilob8aWke4H1FRlXDbnDfyYkjW6RkUaGQ4jzeEAzH56KsG2hf8NtBtYwkRlsLfOX9s5sPwm6w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/common": "22.0.6", + "@angular/core": "22.0.6", + "@angular/platform-browser": "22.0.6", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", + "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.0.0", + "@csstools/css-color-parser": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.5" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@boilerplate/api-client": { + "resolved": "packages/api-client", + "link": true + }, + "node_modules/@boilerplate/backend": { + "resolved": "apps/backend", + "link": true + }, + "node_modules/@boilerplate/frontend": { + "resolved": "apps/frontend", + "link": true + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", + "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", + "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/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==", + "dev": true, + "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/@eslint/eslintrc/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/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@gar/promise-retry": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", + "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@harperfast/extended-iterable": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz", + "integrity": "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.7.tgz", + "integrity": "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.1.tgz", + "integrity": "sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.12.tgz", + "integrity": "sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.9", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.2.1.tgz", + "integrity": "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.2.2.tgz", + "integrity": "sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/external-editor": "^3.0.3", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.1.tgz", + "integrity": "sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.7.tgz", + "integrity": "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.2.tgz", + "integrity": "sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.1.1.tgz", + "integrity": "sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.1.1.tgz", + "integrity": "sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.4.2.tgz", + "integrity": "sha512-XJmn/wY4AX56l1BRU+ZjDrFtg9+2uBEi4JvJQj82kwJDQKiPgSn4CEsbfGGygS4Gw6rkL4W18oATjfVfaqub2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.1.4", + "@inquirer/confirm": "^6.0.12", + "@inquirer/editor": "^5.1.1", + "@inquirer/expand": "^5.0.13", + "@inquirer/input": "^5.0.12", + "@inquirer/number": "^4.0.12", + "@inquirer/password": "^5.0.12", + "@inquirer/rawlist": "^5.2.8", + "@inquirer/search": "^4.1.8", + "@inquirer/select": "^5.1.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.1.tgz", + "integrity": "sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.2.1.tgz", + "integrity": "sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.1.tgz", + "integrity": "sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", + "integrity": "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "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" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-4.2.3.tgz", + "integrity": "sha512-Co9U3AJ3LW0J8XBHjVoNKA79dMAyFt8EZH3OaKTMcDTj8r+6kG3vSUPq/eGLHT7P0iK3uLaFfhdFYd3033P24g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=22.13.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 9", + "listr2": "10.2.1" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.5.4.tgz", + "integrity": "sha512-Kk4Kz3iyu1QiLsLZBS9Af1eSKUC8VR2T+/jyE2iAyuGw2VwK08pp5iTbZnXn6sWu0LogO/RFktMxOjiDA2sS3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.5.4.tgz", + "integrity": "sha512-BEe5Rp3trn26oxoXOVL5HVDoiYmjUDwr8NRPkBOdUdCSBEorKI+7JrZLRKAdxO+G6cGQLgseXk0gR7qIQa7aGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.5.4.tgz", + "integrity": "sha512-SGbFR7816uBcTHc2ZY4S6WyOkl9bICnzqTQd2Mv4V/j24cfds88xx2nC6cm/y8zGQL7Ds31YF/5NGxjgcdM5Hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.5.4.tgz", + "integrity": "sha512-cUXEengO8o60v1SWerJTH4/RH4U3+9jC0/4njp2Z9NdmvaGzhKsbRM2wpXuRYrN8tytsoJCg0SvWEWwHAwLbCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.5.4.tgz", + "integrity": "sha512-Gxq8jpgOWXwd0PUR+c9R2Ik1/uBnGd5GMIIzRRDqABCkvmjtC3KWcyhesV9jSPCz759isl0NlbsstZ2oyvk8lA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-arm64": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.5.4.tgz", + "integrity": "sha512-pKv1DJ1bPZAaHkdFsSz5IDfUG8x9vntgquXF9/Dm2xuupcIe/EkLzylpoBxppFVK5vzbV561Dq26jNY2fIMA7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.4.tgz", + "integrity": "sha512-JF1BmLCm9kGEVZgYmJq43zeQVdHVgAJnTi/NURWEsy6L1ZrrlSmdltS+D17QN4LODwf+1LMXAA9auIZVXtWwzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz", + "integrity": "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@napi-rs/nice": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", + "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.1.1", + "@napi-rs/nice-android-arm64": "1.1.1", + "@napi-rs/nice-darwin-arm64": "1.1.1", + "@napi-rs/nice-darwin-x64": "1.1.1", + "@napi-rs/nice-freebsd-x64": "1.1.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/nice-linux-arm64-gnu": "1.1.1", + "@napi-rs/nice-linux-arm64-musl": "1.1.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", + "@napi-rs/nice-linux-s390x-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-musl": "1.1.1", + "@napi-rs/nice-openharmony-arm64": "1.1.1", + "@napi-rs/nice-win32-arm64-msvc": "1.1.1", + "@napi-rs/nice-win32-ia32-msvc": "1.1.1", + "@napi-rs/nice-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", + "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", + "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", + "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-openharmony-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", + "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nestjs/cli": { + "version": "11.0.24", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.24.tgz", + "integrity": "sha512-aIHxQLSYtXShifA3zwWIeznEsZnNa3Iz2QRykFj+sl9IcbERBHr5nH87FRgywM+He3NxoF5WazHfR8FsmVeWxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.27", + "@angular-devkit/schematics": "19.2.27", + "@angular-devkit/schematics-cli": "19.2.27", + "@inquirer/prompts": "7.10.1", + "@nestjs/schematics": "^11.0.1", + "ansis": "4.2.0", + "chokidar": "4.0.3", + "cli-table3": "0.6.5", + "commander": "4.1.1", + "fork-ts-checker-webpack-plugin": "9.1.0", + "glob": "13.0.6", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.2.0", + "typescript": "5.9.3", + "webpack": "5.106.2", + "webpack-node-externals": "3.0.0" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 20.11" + }, + "peerDependencies": { + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@angular-devkit/core": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.27.tgz", + "integrity": "sha512-3amNzoCVSKd7ah6l6lBQL4onwwJvqvam7FMoQBILrxtW5LB5ezh8gMSPuA4zJjKjoRzf9uoWdlzqv/84I52xZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@angular-devkit/schematics": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.27.tgz", + "integrity": "sha512-/PZmyAlb2NGWPikRRuiWLdfHQd8Wrx6lX4HqvTcaDhlU43M3T0ud4PH2T3QDp7BzHYY92xtD8iPxX2asg67G1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.27", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@nestjs/cli/node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@nestjs/cli/node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@nestjs/cli/node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@nestjs/cli/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nestjs/cli/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@nestjs/cli/node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nestjs/cli/node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@nestjs/cli/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nestjs/cli/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nestjs/cli/node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/@nestjs/cli/node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@nestjs/cli/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nestjs/cli/node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nestjs/cli/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nestjs/cli/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@nestjs/cli/node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@nestjs/cli/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@nestjs/cli/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nestjs/cli/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/@nestjs/cli/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@nestjs/cli/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@nestjs/common": { + "version": "11.1.28", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.28.tgz", + "integrity": "sha512-bRImsxibie+AM7xjdwcrm/gr5YeacI65kSBNzTufa1Ib5iwziaY/lqMtRh9THq6pbV4e1HP9aI2ZxGUumnmaoQ==", + "license": "MIT", + "dependencies": { + "file-type": "21.3.4", + "iterare": "1.2.1", + "load-esm": "1.0.3", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": ">=0.4.1", + "class-validator": ">=0.13.2", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/config": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.4.tgz", + "integrity": "sha512-CJPjNitr0bAufSEnRe2N+JbnVmMmDoo6hvKCPzXgZoGwJSmp/dZPk9f/RMbuD/+Q1ZJPjwsRpq0vxna++Knwow==", + "license": "MIT", + "dependencies": { + "dotenv": "17.4.1", + "dotenv-expand": "12.0.3", + "lodash": "4.18.1" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/config/node_modules/dotenv": { + "version": "17.4.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz", + "integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@nestjs/config/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/@nestjs/core": { + "version": "11.1.28", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.28.tgz", + "integrity": "sha512-06m63xIRj8+l8uOeh/8LnYupGubkyu4f+bPKIadaSui6vK9KpXgoz7HveT1yOVLcEt0M0oCOEW5EuEXZkEmBBQ==", + "license": "MIT", + "dependencies": { + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "8.4.2", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/mapped-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", + "integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/platform-express": { + "version": "11.1.28", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.28.tgz", + "integrity": "sha512-hU+9Sz4m+onHrR5AmelI59QKmY/Re546bPnygnpqqeQdHDiJpBgjWbL4t6Jr73CBpS60cpyng7WzjgphNB9iwA==", + "license": "MIT", + "dependencies": { + "cors": "2.8.6", + "express": "5.2.1", + "multer": "2.2.0", + "path-to-regexp": "8.4.2", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0" + } + }, + "node_modules/@nestjs/schematics": { + "version": "11.0.9", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.9.tgz", + "integrity": "sha512-0NfPbPlEaGwIT8/TCThxLzrlz3yzDNkfRNpbL7FiplKq3w4qXpJg0JYwqgMEJnLQZm3L/L/5XjoyfJHUO3qX9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.17", + "@angular-devkit/schematics": "19.2.17", + "comment-json": "4.4.1", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "typescript": ">=4.8.2" + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.17.tgz", + "integrity": "sha512-Ah008x2RJkd0F+NLKqIpA34/vUGwjlprRCkvddjDopAWRzYn6xCkz1Tqwuhn0nR1Dy47wTLKYD999TYl5ONOAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.17.tgz", + "integrity": "sha512-ADfbaBsrG8mBF6Mfs+crKA/2ykB8AJI50Cv9tKmZfwcUcyAdmTr+vVvhsBCfvUAEokigSsgqgpYxfkJVxhJYeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.17", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@nestjs/schematics/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@nestjs/schematics/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@nestjs/schematics/node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nestjs/schematics/node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@nestjs/schematics/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nestjs/schematics/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nestjs/schematics/node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/@nestjs/schematics/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nestjs/schematics/node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nestjs/schematics/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@nestjs/schematics/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@nestjs/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@nestjs/schematics/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@nestjs/schematics/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nestjs/schematics/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/@nestjs/swagger": { + "version": "11.2.3", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.3.tgz", + "integrity": "sha512-a0xFfjeqk69uHIUpP8u0ryn4cKuHdra2Ug96L858i0N200Hxho+n3j+TlQXyOF4EstLSGjTfxI1Xb2E1lUxeNg==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.16.0", + "@nestjs/mapped-types": "2.1.0", + "js-yaml": "4.1.1", + "lodash": "4.17.21", + "path-to-regexp": "8.3.0", + "swagger-ui-dist": "5.30.2" + }, + "peerDependencies": { + "@fastify/static": "^8.0.0", + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/swagger/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@nestjs/testing": { + "version": "11.1.28", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.28.tgz", + "integrity": "sha512-B+VgRxeLaH7jkOMgAyUP3N3rpFlisQ7JRxixRbgHvG6a0VgKbbkNSofKExexCgKmQQak80undb3+2kE1lUBmRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "node_modules/@nestjs/throttler": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.4.0.tgz", + "integrity": "sha512-osL67i0PUuwU5nqSuJjtUJZMkxAnYB4VldgYUMGzvYRJDCqGRFMWbsbzm/CkUtPLRL30I8T74Xgt/OQxnYokiA==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0" + } + }, + "node_modules/@nestjs/typeorm": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.0.tgz", + "integrity": "sha512-SOeUQl70Lb2OfhGkvnh4KXWlsd+zA08RuuQgT7kKbzivngxzSo1Oc7Usu5VxCxACQC9wc2l9esOHILSJeK7rJA==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0", + "rxjs": "^7.2.0", + "typeorm": "^0.3.0" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@npmcli/agent": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.2.tgz", + "integrity": "sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/agent/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/agent/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/fs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz", + "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "ini": "^6.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz", + "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^5.0.0", + "npm-normalize-package-bin": "^5.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz", + "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz", + "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "glob": "^13.0.0", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^5.0.0", + "proc-log": "^6.0.0", + "semver": "^7.5.3", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", + "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", + "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz", + "integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "node-gyp": "^12.1.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@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/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@schematics/angular": { + "version": "22.0.6", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-22.0.6.tgz", + "integrity": "sha512-pZP52HlQaILmFKK4/iA2OyQ7mMP2V/K6z3gxbykWgXk/MNJ605PWaptHZOuvAAp+0qsFWJbj0MLmDdR3yMhdMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "22.0.6", + "@angular-devkit/schematics": "22.0.6", + "jsonc-parser": "3.3.1", + "typescript": "6.0.3" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@sigstore/bundle": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", + "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/core": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.2.1.tgz", + "integrity": "sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.1.tgz", + "integrity": "sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.1.tgz", + "integrity": "sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gar/promise-retry": "^1.0.2", + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.4", + "proc-log": "^6.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.2.tgz", + "integrity": "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.1.tgz", + "integrity": "sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.1", + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sqltools/formatter": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@testing-library/angular": { + "version": "19.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/angular/-/angular-19.4.1.tgz", + "integrity": "sha512-C9gIfKf3cpkLqNoUy0pbz8/YpmxyFQTZP6Qzo9HrolQPyVSMdc0+ev3vewCouZDyQQwfRkJNlpgjaHHyhL9oqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.3.1" + }, + "peerDependencies": { + "@angular/common": ">= 21.0.0", + "@angular/core": ">= 21.0.0", + "@angular/platform-browser": ">= 21.0.0", + "@angular/router": ">= 21.0.0", + "@testing-library/dom": "^10.0.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz", + "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^10.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@tufjs/models/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/@tufjs/models/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/@tufjs/models/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/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "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/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", + "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.2.tgz", + "integrity": "sha512-d3KvEXBSo/lOAMc2u6fkyDHBvetBHeqD7wm/AcXfLpSOQwlmG9D/aQ0SFswVjv05p7ullQS7Mjohj6/VdbZuTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "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/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "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/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": "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/serve-static/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/superagent": { + "version": "8.1.11", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.11.tgz", + "integrity": "sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "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.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz", + "integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/type-utils": "8.64.0", + "@typescript-eslint/utils": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.64.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz", + "integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz", + "integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.64.0", + "@typescript-eslint/types": "^8.64.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz", + "integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz", + "integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==", + "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.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz", + "integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/utils": "8.64.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz", + "integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==", + "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.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz", + "integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.64.0", + "@typescript-eslint/tsconfig-utils": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/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.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz", + "integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz", + "integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.3.0.tgz", + "integrity": "sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.8.tgz", + "integrity": "sha512-Rv0eabdP/xjAHQGr8cjBm+NnLHNoL268lMDK85w2aAGLFoVKLd8QGnVon5lLtkXQCoYaNL0wg04EGnyKkkKhPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.8", + "@vitest/utils": "4.0.8", + "chai": "^6.2.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.8.tgz", + "integrity": "sha512-9FRM3MZCedXH3+pIh+ME5Up2NBBHDq0wqwhOKkN4VnvCiKbVxddqH9mSGPZeawjd12pCOGnl+lo/ZGHt0/dQSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.8.tgz", + "integrity": "sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.8.tgz", + "integrity": "sha512-mdY8Sf1gsM8hKJUQfiPT3pn1n8RF4QBcJYFslgWh41JTfrK1cbqY8whpGCFzBl45LN028g0njLCYm0d7XxSaQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.8", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.8.tgz", + "integrity": "sha512-Nar9OTU03KGiubrIOFhcfHg8FYaRaNT+bh5VUlNz8stFhCZPNrJvmZkhsr1jtaYvuefYFwK2Hwrq026u4uPWCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.8.tgz", + "integrity": "sha512-nvGVqUunyCgZH7kmo+Ord4WgZ7lN0sOULYXUOYuHr55dvg9YvMz3izfB189Pgp28w0vWFbEEfNc/c3VTrqrXeA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.8.tgz", + "integrity": "sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.8", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "devOptional": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/algoliasearch": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.52.0.tgz", + "integrity": "sha512-0ZzY9mjqV7gop/AH8pIBiAS8giXP7WcSiUfoFYIzYAK9QC5c37E4SIVtJVBMwlURc0/uNt2o4RcNRvdHa4CJ5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.18.0", + "@algolia/client-abtesting": "5.52.0", + "@algolia/client-analytics": "5.52.0", + "@algolia/client-common": "5.52.0", + "@algolia/client-insights": "5.52.0", + "@algolia/client-personalization": "5.52.0", + "@algolia/client-query-suggestions": "5.52.0", + "@algolia/client-search": "5.52.0", + "@algolia/ingestion": "1.52.0", + "@algolia/monitoring": "1.52.0", + "@algolia/recommend": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/app-root-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "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/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "devOptional": true, + "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/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "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==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "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/baseline-browser-mapping": { + "version": "2.10.43", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/beasties": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.4.2.tgz", + "integrity": "sha512-NvcGjG/7AVUAfRbvrJmHunDQS9uHnE6Q/7AkaPr8oKE8HjOlpjRG5075z/th2Tmlezk3VlaaS8+X9I1RwHJMQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "css-select": "^6.0.0", + "css-what": "^7.0.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^10.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3", + "postcss-safe-parser": "^7.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "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/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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-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/cacache": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz", + "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "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/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/caniuse-lite": { + "version": "1.0.30001805", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz", + "integrity": "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", + "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.11.1", + "validator": "^13.9.0" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.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==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-json": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.4.1.tgz", + "integrity": "sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "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/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/concurrently/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/concurrently/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "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/convert-source-map": { + "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==", + "dev": true, + "license": "MIT" + }, + "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-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "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/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssstyle": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/data-urls": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", + "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^15.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "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/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "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/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "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/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "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/dotenv-expand": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", + "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.392", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.392.tgz", + "integrity": "sha512-1yQq3VQCZRwsnYc67Oc+1fge6Lwtn0hzi6zmEVkB61Zx21kTbwJAW4dFLadl5Rc1tKhG/kSpYXnfiAhu0f0a1g==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.2.tgz", + "integrity": "sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "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/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-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": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/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/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "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/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "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-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "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/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^4.0.1", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "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" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-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/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "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/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/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/glob/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/glob/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/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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/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==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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/hono": { + "version": "4.12.30", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.30.tgz", + "integrity": "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "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": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.0.0.tgz", + "integrity": "sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "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-walk": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", + "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/ignore-walk/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/ignore-walk/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/ignore-walk/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/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "dev": true, + "license": "MIT" + }, + "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/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/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/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "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-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "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-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "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/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jose": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.2.tgz", + "integrity": "sha512-MpcPtHLE5EmztuFIqB0vzHAWJPpmN1E6L4oo+kze56LIs3MyXIj9ZHMDxqOvkP38gBR7K1v3jqd4WU2+nrfONQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "27.2.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.2.0.tgz", + "integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.23", + "@asamuzakjp/dom-selector": "^6.7.4", + "cssstyle": "^5.3.3", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/jsdom/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-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-parse-even-better-errors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", + "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "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/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "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==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "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/libphonenumber-js": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.8.tgz", + "integrity": "sha512-80xal1m93rADejw2pMp2MSzFhHCPLEspjHxnH2UtqI+DgAmElsbmLMiqk9niwH9NWAfjsRtaJI+qBrOEmRx9nQ==", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.1.tgz", + "integrity": "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.2.0", + "eventemitter3": "^5.0.4", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^10.0.0" + }, + "engines": { + "node": ">=22.13.0" + } + }, + "node_modules/lmdb": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.4.tgz", + "integrity": "sha512-9FKQA6G1MMtqNxfxvSBNXD/axeG2QRjYbNh0/ykRL5xYcRbCm2vXq7B9bhc7nSuKdHzr8/BHIwfPuYYH1UsXXw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@harperfast/extended-iterable": "^1.0.3", + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.5.4", + "@lmdb/lmdb-darwin-x64": "3.5.4", + "@lmdb/lmdb-linux-arm": "3.5.4", + "@lmdb/lmdb-linux-arm64": "3.5.4", + "@lmdb/lmdb-linux-x64": "3.5.4", + "@lmdb/lmdb-win32-arm64": "3.5.4", + "@lmdb/lmdb-win32-x64": "3.5.4" + } + }, + "node_modules/load-esm": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz", + "integrity": "sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "engines": { + "node": ">=13.2.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "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.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "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/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/make-fetch-happen": { + "version": "15.0.6", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.6.tgz", + "integrity": "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/agent": "^4.0.0", + "@npmcli/redact": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "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/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "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/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "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/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", + "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "iconv-lite": "^0.7.2" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/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/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/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/minipass-sized": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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/msgpackr": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.12.1.tgz", + "integrity": "sha512-4EUH9tQHnMmEgzW/MdAP0KIfa1T9AF+htl0ffe2n5vb2EKn9y2co8ccpgWko6S52Jy1PQZKwRnx5/KkYjtd9MQ==", + "dev": true, + "license": "MIT", + "optional": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, + "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/multer/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/multer/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/multer/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/multer/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/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/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": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-bundled": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", + "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-install-checks": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz", + "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", + "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-package-arg": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", + "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-packlist": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz", + "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^8.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", + "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "npm-package-arg": "^13.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", + "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^4.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^15.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^13.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/oauth4webapi": { + "version": "3.8.6", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.6.tgz", + "integrity": "sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openid-client": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.8.1.tgz", + "integrity": "sha512-VoYT6enBo6Vj2j3Q5Ec0AezS+9YGzQo1f5Xc42lreMGlfP4ljiXPKVDvCADh+XHCV/bqPu/wWSiCVXbJKvrODw==", + "license": "MIT", + "dependencies": { + "jose": "^6.1.0", + "oauth4webapi": "^3.8.2" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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/ora": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.0.tgz", + "integrity": "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.2", + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ordered-binary": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz", + "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.5.tgz", + "integrity": "sha512-e8vJF4XdVkzqqSHguEMz41mQO1wKwxKm5ENrUJQUu9kLDCtn83cxbyHZcszr4QC5zEA7WffRRC4gsTecC7J9oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/pacote": { + "version": "21.5.1", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.5.1.tgz", + "integrity": "sha512-KvcJ9iy3crysCsgqc4+PknH/w6jkrp8JN36mpZBPwNaDRwTfMZD37YzRazNstiZUOhuF5pno9f78n9mEJBavwg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^6.0.0", + "sigstore": "^4.0.0", + "ssri": "^13.0.0", + "tar": "^7.4.3" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "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-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/json-parse-even-better-errors": { + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.1.tgz", + "integrity": "sha512-NaRku2aMpUN1Sh1Gyk1KWUh2A7EJx2c6qYzvwsPtqhoHoaURshdrceYK3LunVCm3WHhm6FS7Vcczbvdh3/UIVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0", + "parse5": "^8.0.0", + "parse5-sax-parser": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-8.0.0.tgz", + "integrity": "sha512-/dQ8UzHZwnrzs3EvDj6IkKrD/jIZyTlB+8XrHJvcjNgRdmWruNdN9i9RK/JtxakmlUdPwKubKPTCqvbTgzGhrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-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-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "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==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz", + "integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==", + "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-http": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-11.0.0.tgz", + "integrity": "sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==", + "license": "MIT", + "dependencies": { + "get-caller-file": "^2.0.5", + "pino": "^10.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.2.tgz", + "integrity": "sha512-3cN0tCakkT4f3zo9RXDIhy6GTvtYD6bK4CRBLN9j3E/ePqN1tugAXD5rGVfoChW6s0hiek+eyYlLNqc/BG7vBQ==", + "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", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/piscina": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.2.0.tgz", + "integrity": "sha512-DszUCKeVN/5G5QKo6jAVHL8fmKnkJvQ0ACiVgY7YGCq3TUB2oznAOayvZPIAdEThvhczkXR+qm3IHsNXpFCYfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.x" + }, + "optionalDependencies": { + "@napi-rs/nice": "^1.0.4" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.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/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/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/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.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "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": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "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-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz", + "integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/schema-utils/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/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/set-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==", + "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/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.1.tgz", + "integrity": "sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.1", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.1.1", + "@sigstore/tuf": "^4.0.2", + "@sigstore/verify": "^3.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/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/sql-highlight": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", + "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==", + "funding": [ + "https://github.com/scriptcoded/sql-highlight?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/scriptcoded" + } + ], + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ssri": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", + "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stdin-discarder": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", + "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", + "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^10.2.3" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "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/swagger-ui-dist": { + "version": "5.30.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.2.tgz", + "integrity": "sha512-HWCg1DTNE/Nmapt+0m2EPXFwNKNeKK4PwMjkwveN/zn1cV2Kxi9SURd+m0SpdcSgWEK/O64sf8bzXdtUhigtHA==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.20", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.20.tgz", + "integrity": "sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/terser": { + "version": "5.49.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.0.tgz", + "integrity": "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/thread-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", + "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.8", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.8.tgz", + "integrity": "sha512-htwgN/8KRB3z3vnC0BOETVh2m499g5GmyTK9Wq5JBLX3FNz6tSBveAd+fQhzy9hkjif8vy2jwDMR1sGhLtZl2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.8" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.8", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.8.tgz", + "integrity": "sha512-c1P7u0EhACHj7lPy4MJm8iTFEU8+nB0LCtddH0fhP7noaVoXAqafMtOOeX+ulpuPBqnrRgRhw494RICT3mbhnw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tuf-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", + "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "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-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/type-is/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/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==", + "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/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/typeorm": { + "version": "0.3.27", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.27.tgz", + "integrity": "sha512-pNV1bn+1n8qEe8tUNsNdD8ejuPcMAg47u2lUGnbsajiNUr3p2Js1XLKQjBMH0yMRMDfdX8T+fIRejFmIwy9x4A==", + "license": "MIT", + "dependencies": { + "@sqltools/formatter": "^1.2.5", + "ansis": "^3.17.0", + "app-root-path": "^3.1.0", + "buffer": "^6.0.3", + "dayjs": "^1.11.13", + "debug": "^4.4.0", + "dedent": "^1.6.0", + "dotenv": "^16.4.7", + "glob": "^10.4.5", + "sha.js": "^2.4.12", + "sql-highlight": "^6.0.0", + "tslib": "^2.8.1", + "uuid": "^11.1.0", + "yargs": "^17.7.2" + }, + "bin": { + "typeorm": "cli.js", + "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", + "typeorm-ts-node-esm": "cli-ts-node-esm.js" + }, + "engines": { + "node": ">=16.13.0" + }, + "funding": { + "url": "https://opencollective.com/typeorm" + }, + "peerDependencies": { + "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0", + "@sap/hana-client": "^2.14.22", + "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "ioredis": "^5.0.4", + "mongodb": "^5.8.0 || ^6.0.0", + "mssql": "^9.1.1 || ^10.0.1 || ^11.0.1", + "mysql2": "^2.2.5 || ^3.0.1", + "oracledb": "^6.3.0", + "pg": "^8.5.1", + "pg-native": "^3.0.0", + "pg-query-stream": "^4.0.0", + "redis": "^3.1.1 || ^4.0.0 || ^5.0.14", + "reflect-metadata": "^0.1.14 || ^0.2.0", + "sql.js": "^1.4.0", + "sqlite3": "^5.0.3", + "ts-node": "^10.7.0", + "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@google-cloud/spanner": { + "optional": true + }, + "@sap/hana-client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mssql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "redis": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "ts-node": { + "optional": true + }, + "typeorm-aurora-data-api-driver": { + "optional": true + } + } + }, + "node_modules/typeorm-ts-node-commonjs": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/typeorm-ts-node-commonjs/-/typeorm-ts-node-commonjs-0.3.20.tgz", + "integrity": "sha512-lXjve7w7OcF3s5+dHnCsrBjUTukpVeiS0bDe5KDXWcDx8TyRW0GTTg9kjWgHzFgHgBIBBu4WGXM0iuGpEgaV9g==", + "dev": true, + "license": "UNLICENSED", + "bin": { + "typeorm-ts-node-commonjs": "wrapper.sh" + } + }, + "node_modules/typeorm/node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/typeorm/node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typeorm/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/typeorm/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/typeorm/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/typeorm/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/typeorm/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.64.0.tgz", + "integrity": "sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.64.0", + "@typescript-eslint/parser": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/utils": "8.64.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", + "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/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/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/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/vitest": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.8.tgz", + "integrity": "sha512-urzu3NCEV0Qa0Y2PwvBtRgmNtxhj5t5ULw7cuKhIHh3OrkKTLlut0lnBOv9qe5OvbkMH2g38G7KPDCTpIytBVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.8", + "@vitest/mocker": "4.0.8", + "@vitest/pretty-format": "4.0.8", + "@vitest/runner": "4.0.8", + "@vitest/snapshot": "4.0.8", + "@vitest/spy": "4.0.8", + "@vitest/utils": "4.0.8", + "debug": "^4.4.3", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.8", + "@vitest/browser-preview": "4.0.8", + "@vitest/browser-webdriverio": "4.0.8", + "@vitest/ui": "4.0.8", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/webpack": { + "version": "5.106.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", + "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.1", + "mime-db": "^1.54.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", + "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "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/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", + "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "string-width": "^8.2.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "devOptional": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.2.tgz", + "integrity": "sha512-IynmDyxsEsb9RKzO3J9+4SxXnl2FTFSzNBaKKaMV6tsSk0rw9gYw9gs+JFCq/qk2LCZ78KDwyj+Z289TijSkUw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "packages/api-client": { + "name": "@boilerplate/api-client", + "version": "1.0.0", + "peerDependencies": { + "@angular/common": "22.0.6", + "@angular/core": "22.0.6", + "rxjs": "7.8.2" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..dfc629a --- /dev/null +++ b/package.json @@ -0,0 +1,63 @@ +{ + "name": "internal-business-app-boilerplate", + "version": "1.0.0", + "private": true, + "description": "Production-ready Angular/NestJS monorepo boilerplate for internal business applications.", + "license": "UNLICENSED", + "packageManager": "npm@11.12.1", + "engines": { + "node": "24.18.x", + "npm": "11.x" + }, + "workspaces": [ + "apps/frontend", + "apps/backend", + "packages/api-client" + ], + "scripts": { + "dev": "concurrently -n frontend,backend -c blue,green \"npm run dev:frontend\" \"npm run dev:backend\"", + "dev:frontend": "npm --workspace apps/frontend run start", + "dev:backend": "npm --workspace apps/backend run start:dev", + "lint": "eslint .", + "format": "prettier --write .", + "format:check": "prettier --check .", + "typecheck": "npm --workspace packages/api-client run typecheck && npm --workspace apps/backend run typecheck && npm --workspace apps/frontend run typecheck", + "test": "npm run test:backend && npm run test:frontend", + "test:frontend": "npm --workspace apps/frontend run test", + "test:backend": "npm --workspace apps/backend run test", + "build": "npm run api:generate && npm --workspace packages/api-client run build && npm run build:backend && npm run build:frontend && npm run build:bundle", + "build:frontend": "npm --workspace apps/frontend run build", + "build:backend": "npm --workspace apps/backend run build", + "build:bundle": "node scripts/copy-frontend.mjs", + "api:generate": "node scripts/generate-api-client.mjs", + "migration:status": "npm --workspace apps/backend run migration:status", + "migration:run": "npm --workspace apps/backend run migration:run", + "migration:generate": "npm --workspace apps/backend run migration:generate", + "docker:build": "docker build ." + }, + "devDependencies": { + "@angular/build": "22.0.6", + "@angular/cli": "22.0.6", + "@angular/compiler-cli": "22.0.6", + "@eslint/js": "9.39.1", + "@nestjs/cli": "11.0.24", + "@nestjs/schematics": "11.0.9", + "@nestjs/testing": "11.1.28", + "@testing-library/angular": "19.4.1", + "@types/cookie-parser": "1.4.10", + "@types/express": "5.0.5", + "@types/node": "24.10.1", + "@types/supertest": "6.0.3", + "concurrently": "9.2.1", + "eslint": "9.39.1", + "eslint-config-prettier": "10.1.8", + "globals": "16.5.0", + "jsdom": "27.2.0", + "prettier": "3.6.2", + "supertest": "7.1.4", + "ts-node": "^10.9.2", + "typescript": "6.0.3", + "typescript-eslint": "8.64.0", + "vitest": "4.0.8" + } +} diff --git a/packages/api-client/package.json b/packages/api-client/package.json new file mode 100644 index 0000000..5b2bc7e --- /dev/null +++ b/packages/api-client/package.json @@ -0,0 +1,23 @@ +{ + "name": "@boilerplate/api-client", + "version": "1.0.0", + "private": true, + "type": "module", + "main": "dist/public-api.js", + "types": "dist/public-api.d.ts", + "exports": { + ".": { + "types": "./src/public-api.ts", + "default": "./src/public-api.ts" + } + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "peerDependencies": { + "@angular/common": "22.0.6", + "@angular/core": "22.0.6", + "rxjs": "7.8.2" + } +} diff --git a/packages/api-client/src/api-client.service.ts b/packages/api-client/src/api-client.service.ts new file mode 100644 index 0000000..be9d6f3 --- /dev/null +++ b/packages/api-client/src/api-client.service.ts @@ -0,0 +1,343 @@ +// Generated from the backend OpenAPI contract. Do not edit manually. +import { Injectable, inject } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import type { Observable } from 'rxjs'; +import type { + AuditLogDto, + AdminSessionDto, + AdminUserDetailDto, + AdminUserListItemDto, + CreateNotificationDto, + ItemDto, + NotificationDto, + NotificationPageDto, + NotificationStatusFilter, + PageDto, + Permission, + RoleDto, + SaveItemDto, + SessionDto, + UserDto, +} from './models'; + +@Injectable({ providedIn: 'root' }) +export class ApiClientService { + private readonly http = inject(HttpClient); + private readonly base = '/api'; + + me(): Observable { + return this.http.get(`${this.base}/me`, { withCredentials: true }); + } + + updateSettings(body: { tablePageSize?: number; sidebarExpanded?: boolean }): Observable { + return this.http.patch(`${this.base}/me/settings`, body, { + withCredentials: true, + }); + } + + dashboard(): Observable<{ + userCount: number; + activeSessions: number; + roleCount: number; + itemCount: number; + }> { + return this.http.get<{ + userCount: number; + activeSessions: number; + roleCount: number; + itemCount: number; + }>(`${this.base}/dashboard`, { withCredentials: true }); + } + + items( + query: { + search?: string; + page?: number; + pageSize?: number; + sort?: string; + direction?: string; + } = {}, + ): Observable> { + return this.http.get>(`${this.base}/items`, { + params: this.params(query), + withCredentials: true, + }); + } + + item(id: string): Observable { + return this.http.get(`${this.base}/items/${id}`, { + withCredentials: true, + }); + } + + createItem(body: SaveItemDto): Observable { + return this.http.post(`${this.base}/items`, body, { + withCredentials: true, + }); + } + + updateItem(id: string, body: Required): Observable { + return this.http.put(`${this.base}/items/${id}`, body, { + withCredentials: true, + }); + } + + deleteItem(id: string, version: number): Observable { + return this.http.delete(`${this.base}/items/${id}`, { + params: { version }, + withCredentials: true, + }); + } + + users( + query: { search?: string; page?: number; pageSize?: number } = {}, + ): Observable> { + return this.http.get>(`${this.base}/users`, { + params: this.params(query), + withCredentials: true, + }); + } + + setUserActive(id: string, active: boolean): Observable { + return this.http.patch( + `${this.base}/users/${id}/active`, + { active }, + { withCredentials: true }, + ); + } + + setUserRoles(id: string, roleIds: string[]): Observable { + return this.http.patch( + `${this.base}/users/${id}/roles`, + { roleIds }, + { withCredentials: true }, + ); + } + + adminUsers( + query: { + search?: string; + active?: 'all' | 'active' | 'inactive'; + roleId?: string; + sort?: 'name' | 'email' | 'lastLoginAt' | 'createdAt'; + direction?: 'ASC' | 'DESC'; + page?: number; + pageSize?: number; + } = {}, + ): Observable> { + return this.http.get>(`${this.base}/admin/users`, { + params: this.params(query), + withCredentials: true, + }); + } + + adminUser(id: string): Observable { + return this.http.get(`${this.base}/admin/users/${id}`, { + withCredentials: true, + }); + } + + activateAdminUser(id: string): Observable { + return this.http.patch( + `${this.base}/admin/users/${id}/activate`, + {}, + { withCredentials: true }, + ); + } + + deactivateAdminUser(id: string): Observable { + return this.http.patch( + `${this.base}/admin/users/${id}/deactivate`, + {}, + { withCredentials: true }, + ); + } + + assignAdminUserRole(userId: string, roleId: string): Observable { + return this.http.post( + `${this.base}/admin/users/${userId}/roles/${roleId}`, + {}, + { withCredentials: true }, + ); + } + + removeAdminUserRole(userId: string, roleId: string): Observable { + return this.http.delete( + `${this.base}/admin/users/${userId}/roles/${roleId}`, + { withCredentials: true }, + ); + } + + adminUserSessions(id: string): Observable { + return this.http.get(`${this.base}/admin/users/${id}/sessions`, { + withCredentials: true, + }); + } + + revokeAdminUserSession(userId: string, sessionId: string): Observable { + return this.http.delete(`${this.base}/admin/users/${userId}/sessions/${sessionId}`, { + withCredentials: true, + }); + } + + revokeAdminUserSessions(userId: string): Observable<{ revoked: number }> { + return this.http.delete<{ revoked: number }>(`${this.base}/admin/users/${userId}/sessions`, { + withCredentials: true, + }); + } + + roles(): Observable { + return this.http.get(`${this.base}/roles`, { + withCredentials: true, + }); + } + + createRole(body: { name: string; permissions: string[] }): Observable { + return this.http.post(`${this.base}/roles`, body, { + withCredentials: true, + }); + } + + updateRole(id: string, body: { name: string; permissions: string[] }): Observable { + return this.http.put(`${this.base}/roles/${id}`, body, { + withCredentials: true, + }); + } + + deleteRole(id: string): Observable { + return this.http.delete(`${this.base}/roles/${id}`, { + withCredentials: true, + }); + } + + adminRoles(): Observable { + return this.http.get(`${this.base}/admin/roles`, { + withCredentials: true, + }); + } + + adminRole(id: string): Observable { + return this.http.get(`${this.base}/admin/roles/${id}`, { + withCredentials: true, + }); + } + + createAdminRole(body: { + name: string; + description?: string; + permissions: Permission[]; + }): Observable { + return this.http.post(`${this.base}/admin/roles`, body, { + withCredentials: true, + }); + } + + updateAdminRole( + id: string, + body: { name: string; description?: string; permissions: Permission[] }, + ): Observable { + return this.http.put(`${this.base}/admin/roles/${id}`, body, { + withCredentials: true, + }); + } + + deleteAdminRole(id: string): Observable { + return this.http.delete(`${this.base}/admin/roles/${id}`, { + withCredentials: true, + }); + } + + sessions(): Observable { + return this.http.get(`${this.base}/sessions/own`, { + withCredentials: true, + }); + } + + revokeSession(id: string): Observable { + return this.http.delete(`${this.base}/sessions/own/${id}`, { + withCredentials: true, + }); + } + + revokeOtherSessions(): Observable { + return this.http.delete(`${this.base}/sessions/own`, { + withCredentials: true, + }); + } + + revokeUserSessions(userId: string): Observable { + return this.http.delete(`${this.base}/sessions/users/${userId}`, { + withCredentials: true, + }); + } + + audit(page = 1, pageSize = 20): Observable> { + return this.http.get>(`${this.base}/audit-log`, { + params: { page, pageSize }, + withCredentials: true, + }); + } + + notifications( + query: { + page?: number; + pageSize?: number; + status?: NotificationStatusFilter; + } = {}, + ): Observable { + return this.http.get(`${this.base}/notifications`, { + params: this.params(query), + withCredentials: true, + }); + } + + unreadNotificationCount(): Observable<{ count: number }> { + return this.http.get<{ count: number }>(`${this.base}/notifications/unread-count`, { + withCredentials: true, + }); + } + + markNotificationRead(id: string): Observable { + return this.http.patch( + `${this.base}/notifications/${id}/read`, + {}, + { withCredentials: true }, + ); + } + + markNotificationUnread(id: string): Observable { + return this.http.patch( + `${this.base}/notifications/${id}/unread`, + {}, + { withCredentials: true }, + ); + } + + markAllNotificationsRead(): Observable<{ updated: number }> { + return this.http.patch<{ updated: number }>( + `${this.base}/notifications/read-all`, + {}, + { withCredentials: true }, + ); + } + + deleteNotification(id: string): Observable { + return this.http.delete(`${this.base}/notifications/${id}`, { + withCredentials: true, + }); + } + + createAdminNotification(body: CreateNotificationDto): Observable { + return this.http.post(`${this.base}/admin/notifications`, body, { + withCredentials: true, + }); + } + + private params(value: Record): HttpParams { + let params = new HttpParams(); + Object.entries(value).forEach(([key, entry]) => { + if (entry !== undefined && entry !== '') params = params.set(key, String(entry)); + }); + return params; + } +} diff --git a/packages/api-client/src/models.ts b/packages/api-client/src/models.ts new file mode 100644 index 0000000..ceea212 --- /dev/null +++ b/packages/api-client/src/models.ts @@ -0,0 +1,154 @@ +// Generated from the backend OpenAPI contract. Do not edit manually. +export type Permission = + | 'items.read' + | 'items.create' + | 'items.update' + | 'items.delete' + | 'users.read' + | 'users.manage' + | 'roles.read' + | 'roles.manage' + | 'audit.read' + | 'sessions.readOwn' + | 'sessions.revokeOwn' + | 'sessions.manage' + | 'notifications.readOwn' + | 'notifications.updateOwn' + | 'notifications.manage'; + +export interface ApiErrorBody { + status: number; + code: string; + message: string; + requestId: string; + validation?: { field: string; messages: string[] }[]; +} + +export interface PageDto { + items: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface UserDto { + id: string; + name: string; + email: string | null; + active: boolean; + lastLoginAt: string | null; + roles: RoleDto[]; + settings: { tablePageSize: number; sidebarExpanded: boolean }; +} + +export interface AdminRoleSummaryDto { + id: string; + name: string; + system: boolean; +} + +export interface AdminSessionDto { + id: string; + createdAt: string; + lastActivityAt: string; + userAgent: string | null; + approximateIp: string | null; + current: boolean; + expiresAt: string; + revokedAt: string | null; +} + +export interface AdminUserListItemDto { + id: string; + name: string; + email: string | null; + active: boolean; + roles: AdminRoleSummaryDto[]; + lastLoginAt: string | null; + createdAt: string; + activeSessionCount: number; +} + +export interface AdminUserDetailDto extends AdminUserListItemDto { + effectivePermissions: Permission[]; + sessions: AdminSessionDto[]; +} + +export interface RoleDto { + id: string; + name: string; + description: string; + system: boolean; + protected: boolean; + permissions: { id: Permission; description: string }[]; + userCount?: number; + users?: UserDto[]; +} + +export interface ItemDto { + id: string; + name: string; + description: string | null; + status: 'draft' | 'active' | 'archived'; + version: number; + createdAt: string; + updatedAt: string; + deletedAt: string | null; +} + +export interface SaveItemDto { + name: string; + description?: string; + status: ItemDto['status']; + version?: number; +} + +export interface SessionDto { + id: string; + createdAt: string; + lastActivityAt: string; + userAgent: string | null; + approximateIp: string | null; + current: boolean; + revokedAt: string | null; +} + +export interface AuditLogDto { + id: string; + createdAt: string; + actorUserId: string | null; + action: string; + targetType: string; + targetId: string; + metadata: Record | null; + requestId: string; +} + +export type NotificationType = 'system' | 'item.created' | 'item.updated' | 'user.role-changed'; + +export type NotificationStatusFilter = 'all' | 'read' | 'unread'; + +export interface NotificationDto { + id: string; + type: NotificationType; + title: string; + message: string; + link: string | null; + metadata: Record | null; + read: boolean; + readAt: string | null; + createdAt: string; +} + +export interface NotificationPageDto extends PageDto { + unreadCount: number; +} + +export interface CreateNotificationDto { + userId: string; + type: NotificationType; + title: string; + message: string; + link?: string; + metadata?: Record; +} diff --git a/packages/api-client/src/public-api.ts b/packages/api-client/src/public-api.ts new file mode 100644 index 0000000..8e96cb4 --- /dev/null +++ b/packages/api-client/src/public-api.ts @@ -0,0 +1,2 @@ +export * from './models'; +export * from './api-client.service'; diff --git a/packages/api-client/tsconfig.json b/packages/api-client/tsconfig.json new file mode 100644 index 0000000..ded111b --- /dev/null +++ b/packages/api-client/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "module": "ES2022", + "moduleResolution": "Bundler", + "lib": ["ES2023", "DOM"] + }, + "include": ["src/**/*.ts"] +} diff --git a/scripts/copy-frontend.mjs b/scripts/copy-frontend.mjs new file mode 100644 index 0000000..ff1cfd8 --- /dev/null +++ b/scripts/copy-frontend.mjs @@ -0,0 +1,10 @@ +import { cp, mkdir, rm } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +const root = resolve(import.meta.dirname, '..'); +const source = resolve(root, 'apps/frontend/dist/frontend/browser'); +const target = resolve(root, 'apps/backend/dist/public'); + +await rm(target, { recursive: true, force: true }); +await mkdir(target, { recursive: true }); +await cp(source, target, { recursive: true }); diff --git a/scripts/generate-api-client.mjs b/scripts/generate-api-client.mjs new file mode 100644 index 0000000..596d451 --- /dev/null +++ b/scripts/generate-api-client.mjs @@ -0,0 +1,546 @@ +import { mkdir, writeFile } from 'node:fs/promises'; +import { dirname, resolve } from 'node:path'; +import prettier from 'prettier'; + +const root = resolve(import.meta.dirname, '..'); +const files = new Map([ + [ + 'packages/api-client/src/public-api.ts', + `export * from './models';\nexport * from './api-client.service';\n`, + ], + [ + 'packages/api-client/src/models.ts', + `// Generated from the backend OpenAPI contract. Do not edit manually. +export type Permission = + | 'items.read' + | 'items.create' + | 'items.update' + | 'items.delete' + | 'users.read' + | 'users.manage' + | 'roles.read' + | 'roles.manage' + | 'audit.read' + | 'sessions.readOwn' + | 'sessions.revokeOwn' + | 'sessions.manage' + | 'notifications.readOwn' + | 'notifications.updateOwn' + | 'notifications.manage'; + +export interface ApiErrorBody { + status: number; + code: string; + message: string; + requestId: string; + validation?: { field: string; messages: string[] }[]; +} + +export interface PageDto { + items: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface UserDto { + id: string; + name: string; + email: string | null; + active: boolean; + lastLoginAt: string | null; + roles: RoleDto[]; + settings: { tablePageSize: number; sidebarExpanded: boolean }; +} + +export interface AdminRoleSummaryDto { + id: string; + name: string; + system: boolean; +} + +export interface AdminSessionDto { + id: string; + createdAt: string; + lastActivityAt: string; + userAgent: string | null; + approximateIp: string | null; + current: boolean; + expiresAt: string; + revokedAt: string | null; +} + +export interface AdminUserListItemDto { + id: string; + name: string; + email: string | null; + active: boolean; + roles: AdminRoleSummaryDto[]; + lastLoginAt: string | null; + createdAt: string; + activeSessionCount: number; +} + +export interface AdminUserDetailDto extends AdminUserListItemDto { + effectivePermissions: Permission[]; + sessions: AdminSessionDto[]; +} + +export interface RoleDto { + id: string; + name: string; + description: string; + system: boolean; + protected: boolean; + permissions: { id: Permission; description: string }[]; + userCount?: number; + users?: UserDto[]; +} + +export interface ItemDto { + id: string; + name: string; + description: string | null; + status: 'draft' | 'active' | 'archived'; + version: number; + createdAt: string; + updatedAt: string; + deletedAt: string | null; +} + +export interface SaveItemDto { + name: string; + description?: string; + status: ItemDto['status']; + version?: number; +} + +export interface SessionDto { + id: string; + createdAt: string; + lastActivityAt: string; + userAgent: string | null; + approximateIp: string | null; + current: boolean; + revokedAt: string | null; +} + +export interface AuditLogDto { + id: string; + createdAt: string; + actorUserId: string | null; + action: string; + targetType: string; + targetId: string; + metadata: Record | null; + requestId: string; +} + +export type NotificationType = + | 'system' + | 'item.created' + | 'item.updated' + | 'user.role-changed'; + +export type NotificationStatusFilter = 'all' | 'read' | 'unread'; + +export interface NotificationDto { + id: string; + type: NotificationType; + title: string; + message: string; + link: string | null; + metadata: Record | null; + read: boolean; + readAt: string | null; + createdAt: string; +} + +export interface NotificationPageDto extends PageDto { + unreadCount: number; +} + +export interface CreateNotificationDto { + userId: string; + type: NotificationType; + title: string; + message: string; + link?: string; + metadata?: Record; +} +`, + ], + [ + 'packages/api-client/src/api-client.service.ts', + `// Generated from the backend OpenAPI contract. Do not edit manually. +import { Injectable, inject } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import type { Observable } from 'rxjs'; +import type { + AuditLogDto, + AdminSessionDto, + AdminUserDetailDto, + AdminUserListItemDto, + CreateNotificationDto, + ItemDto, + NotificationDto, + NotificationPageDto, + NotificationStatusFilter, + PageDto, + Permission, + RoleDto, + SaveItemDto, + SessionDto, + UserDto, +} from './models'; + +@Injectable({ providedIn: 'root' }) +export class ApiClientService { + private readonly http = inject(HttpClient); + private readonly base = '/api'; + + me(): Observable { + return this.http.get(\`\${this.base}/me\`, { withCredentials: true }); + } + + updateSettings(body: { + tablePageSize?: number; + sidebarExpanded?: boolean; + }): Observable { + return this.http.patch(\`\${this.base}/me/settings\`, body, { + withCredentials: true, + }); + } + + dashboard(): Observable<{ + userCount: number; + activeSessions: number; + roleCount: number; + itemCount: number; + }> { + return this.http.get<{ + userCount: number; + activeSessions: number; + roleCount: number; + itemCount: number; + }>(\`\${this.base}/dashboard\`, { withCredentials: true }); + } + + items( + query: { + search?: string; + page?: number; + pageSize?: number; + sort?: string; + direction?: string; + } = {}, + ): Observable> { + return this.http.get>(\`\${this.base}/items\`, { + params: this.params(query), + withCredentials: true, + }); + } + + item(id: string): Observable { + return this.http.get(\`\${this.base}/items/\${id}\`, { + withCredentials: true, + }); + } + + createItem(body: SaveItemDto): Observable { + return this.http.post(\`\${this.base}/items\`, body, { + withCredentials: true, + }); + } + + updateItem(id: string, body: Required): Observable { + return this.http.put(\`\${this.base}/items/\${id}\`, body, { + withCredentials: true, + }); + } + + deleteItem(id: string, version: number): Observable { + return this.http.delete(\`\${this.base}/items/\${id}\`, { + params: { version }, + withCredentials: true, + }); + } + + users( + query: { search?: string; page?: number; pageSize?: number } = {}, + ): Observable> { + return this.http.get>(\`\${this.base}/users\`, { + params: this.params(query), + withCredentials: true, + }); + } + + setUserActive(id: string, active: boolean): Observable { + return this.http.patch( + \`\${this.base}/users/\${id}/active\`, + { active }, + { withCredentials: true }, + ); + } + + setUserRoles(id: string, roleIds: string[]): Observable { + return this.http.patch( + \`\${this.base}/users/\${id}/roles\`, + { roleIds }, + { withCredentials: true }, + ); + } + + adminUsers( + query: { + search?: string; + active?: 'all' | 'active' | 'inactive'; + roleId?: string; + sort?: 'name' | 'email' | 'lastLoginAt' | 'createdAt'; + direction?: 'ASC' | 'DESC'; + page?: number; + pageSize?: number; + } = {}, + ): Observable> { + return this.http.get>(\`\${this.base}/admin/users\`, { + params: this.params(query), + withCredentials: true, + }); + } + + adminUser(id: string): Observable { + return this.http.get(\`\${this.base}/admin/users/\${id}\`, { + withCredentials: true, + }); + } + + activateAdminUser(id: string): Observable { + return this.http.patch( + \`\${this.base}/admin/users/\${id}/activate\`, + {}, + { withCredentials: true }, + ); + } + + deactivateAdminUser(id: string): Observable { + return this.http.patch( + \`\${this.base}/admin/users/\${id}/deactivate\`, + {}, + { withCredentials: true }, + ); + } + + assignAdminUserRole(userId: string, roleId: string): Observable { + return this.http.post( + \`\${this.base}/admin/users/\${userId}/roles/\${roleId}\`, + {}, + { withCredentials: true }, + ); + } + + removeAdminUserRole(userId: string, roleId: string): Observable { + return this.http.delete( + \`\${this.base}/admin/users/\${userId}/roles/\${roleId}\`, + { withCredentials: true }, + ); + } + + adminUserSessions(id: string): Observable { + return this.http.get(\`\${this.base}/admin/users/\${id}/sessions\`, { + withCredentials: true, + }); + } + + revokeAdminUserSession(userId: string, sessionId: string): Observable { + return this.http.delete( + \`\${this.base}/admin/users/\${userId}/sessions/\${sessionId}\`, + { withCredentials: true }, + ); + } + + revokeAdminUserSessions(userId: string): Observable<{ revoked: number }> { + return this.http.delete<{ revoked: number }>( + \`\${this.base}/admin/users/\${userId}/sessions\`, + { withCredentials: true }, + ); + } + + roles(): Observable { + return this.http.get(\`\${this.base}/roles\`, { + withCredentials: true, + }); + } + + createRole(body: { name: string; permissions: string[] }): Observable { + return this.http.post(\`\${this.base}/roles\`, body, { + withCredentials: true, + }); + } + + updateRole( + id: string, + body: { name: string; permissions: string[] }, + ): Observable { + return this.http.put(\`\${this.base}/roles/\${id}\`, body, { + withCredentials: true, + }); + } + + deleteRole(id: string): Observable { + return this.http.delete(\`\${this.base}/roles/\${id}\`, { + withCredentials: true, + }); + } + + adminRoles(): Observable { + return this.http.get(\`\${this.base}/admin/roles\`, { + withCredentials: true, + }); + } + + adminRole(id: string): Observable { + return this.http.get(\`\${this.base}/admin/roles/\${id}\`, { + withCredentials: true, + }); + } + + createAdminRole(body: { + name: string; + description?: string; + permissions: Permission[]; + }): Observable { + return this.http.post(\`\${this.base}/admin/roles\`, body, { + withCredentials: true, + }); + } + + updateAdminRole( + id: string, + body: { name: string; description?: string; permissions: Permission[] }, + ): Observable { + return this.http.put(\`\${this.base}/admin/roles/\${id}\`, body, { + withCredentials: true, + }); + } + + deleteAdminRole(id: string): Observable { + return this.http.delete(\`\${this.base}/admin/roles/\${id}\`, { + withCredentials: true, + }); + } + + sessions(): Observable { + return this.http.get(\`\${this.base}/sessions/own\`, { + withCredentials: true, + }); + } + + revokeSession(id: string): Observable { + return this.http.delete(\`\${this.base}/sessions/own/\${id}\`, { + withCredentials: true, + }); + } + + revokeOtherSessions(): Observable { + return this.http.delete(\`\${this.base}/sessions/own\`, { + withCredentials: true, + }); + } + + revokeUserSessions(userId: string): Observable { + return this.http.delete(\`\${this.base}/sessions/users/\${userId}\`, { + withCredentials: true, + }); + } + + audit(page = 1, pageSize = 20): Observable> { + return this.http.get>(\`\${this.base}/audit-log\`, { + params: { page, pageSize }, + withCredentials: true, + }); + } + + notifications( + query: { + page?: number; + pageSize?: number; + status?: NotificationStatusFilter; + } = {}, + ): Observable { + return this.http.get(\`\${this.base}/notifications\`, { + params: this.params(query), + withCredentials: true, + }); + } + + unreadNotificationCount(): Observable<{ count: number }> { + return this.http.get<{ count: number }>( + \`\${this.base}/notifications/unread-count\`, + { withCredentials: true }, + ); + } + + markNotificationRead(id: string): Observable { + return this.http.patch( + \`\${this.base}/notifications/\${id}/read\`, + {}, + { withCredentials: true }, + ); + } + + markNotificationUnread(id: string): Observable { + return this.http.patch( + \`\${this.base}/notifications/\${id}/unread\`, + {}, + { withCredentials: true }, + ); + } + + markAllNotificationsRead(): Observable<{ updated: number }> { + return this.http.patch<{ updated: number }>( + \`\${this.base}/notifications/read-all\`, + {}, + { withCredentials: true }, + ); + } + + deleteNotification(id: string): Observable { + return this.http.delete(\`\${this.base}/notifications/\${id}\`, { + withCredentials: true, + }); + } + + createAdminNotification( + body: CreateNotificationDto, + ): Observable { + return this.http.post( + \`\${this.base}/admin/notifications\`, + body, + { withCredentials: true }, + ); + } + + private params(value: Record): HttpParams { + let params = new HttpParams(); + Object.entries(value).forEach(([key, entry]) => { + if (entry !== undefined && entry !== '') params = params.set(key, String(entry)); + }); + return params; + } +} +`, + ], +]); + +for (const [file, content] of files) { + const target = resolve(root, file); + await mkdir(dirname(target), { recursive: true }); + const formatted = await prettier.format(content, { + parser: file.endsWith('.ts') ? 'typescript' : 'json', + singleQuote: true, + semi: true, + printWidth: 100, + trailingComma: 'all', + }); + await writeFile(target, formatted, 'utf8'); +} diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..74669cf --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "ignoreDeprecations": "6.0", + "baseUrl": ".", + "paths": { + "@boilerplate/api-client": ["packages/api-client/src/public-api.ts"], + "@boilerplate/api-client/*": ["packages/api-client/src/*"] + } + } +}