From 590fb63ca711d2f73f170e364de0aa5ea7143e6b Mon Sep 17 00:00:00 2001 From: "S.Gromov" Date: Thu, 30 Jul 2026 09:19:59 +0300 Subject: [PATCH] =?UTF-8?q?chore:=20=D0=9D=D0=BE=D0=B2=D1=8B=D0=B9=20?= =?UTF-8?q?=D1=87=D0=B5=D1=80=D0=BD=D0=BE=D0=B2=D0=B8=D0=BA=20DRAFT,=20?= =?UTF-8?q?=D1=83=D0=B4=D0=B0=D0=BB=D0=B8=D1=82=D1=8C=20=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D1=8B=D0=B5=20docs-v?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DRAFT/README.md | 15 + DRAFT/domains/README.md | 84 + DRAFT/domains/auth-example.md | 173 + DRAFT/domains/business.md | 174 + DRAFT/domains/domain.md | 206 + DRAFT/domains/factory-ports-adapters.md | 200 + DRAFT/domains/framework-bindings.md | 109 + DRAFT/domains/open-questions.md | 113 + DRAFT/domains/presets.md | 141 + DRAFT/domains/testing.md | 402 + DRAFT/index.md | 33 + DRAFT/level-1/README.md | 53 + DRAFT/level-1/components.md | 65 + DRAFT/level-1/dependencies.md | 49 + DRAFT/level-1/groups.md | 25 + DRAFT/level-1/layers.md | 85 + DRAFT/level-1/lifecycle.md | 31 + DRAFT/level-1/modules.md | 43 + DRAFT/level-1/nested-modules.md | 30 + DRAFT/level-1/segments.md | 29 + DRAFT/level-1/terminology.md | 117 + DRAFT/level-1/validation.md | 23 + DRAFT/rules/README.md | 117 + DRAFT/rules/level-1.md | 102 + README.md | 13 +- docs/en/index.md | 24 - docs/index.md | 24 - docs/ru/index.md | 28 - docs/ru/specification/architecture-model.md | 72 - docs/ru/specification/architecture-modes.md | 81 - docs/ru/specification/foundations.md | 39 - docs/ru/specification/index.md | 93 - docs/ru/specification/layers/app.md | 70 - docs/ru/specification/layers/compositions.md | 85 - docs/ru/specification/layers/index.md | 44 - docs/ru/specification/layers/infra.md | 53 - docs/ru/specification/layers/shared.md | 42 - docs/ru/specification/layers/ui.md | 48 - .../specification/modes/advanced/domains.md | 122 - docs/ru/specification/modes/advanced/index.md | 60 - .../modes/pro/domains/business.md | 130 - .../modes/pro/domains/client-and-server.md | 105 - .../pro/domains/cross-domain-boundary.md | 125 - .../modes/pro/domains/framework.md | 81 - .../specification/modes/pro/domains/index.md | 158 - .../modes/pro/domains/ports-and-adapters.md | 100 - .../modes/pro/domains/testing.md | 63 - docs/ru/specification/modes/pro/index.md | 55 - docs/ru/specification/modules-and-groups.md | 72 - docs/ru/specification/monorepo.md | 54 - .../specification/public-api-and-imports.md | 55 - docs/ru/specification/rules.md | 13 - .../ru/specification/runtime-and-lifecycle.md | 83 - docs/ru/specification/segments.md | 59 - docs/ru/specification/state-and-data.md | 70 - docs/ru/specification/terminology.md | 61 - .../specification/testing-and-conformance.md | 58 - draft-rules.js | 219 + examples/demo-backend/.env.example | 10 + examples/demo-backend/.gitignore | 6 + examples/demo-backend/README.md | 117 + examples/demo-backend/docs/CASES.md | 133 + examples/demo-backend/docs/WEBSOCKET.md | 79 + examples/demo-backend/nest-cli.json | 8 + examples/demo-backend/openapi/complex.json | 7467 ++++++++++++++ examples/demo-backend/openapi/simple.json | 2677 +++++ examples/demo-backend/package-lock.json | 8654 +++++++++++++++++ examples/demo-backend/package.json | 82 + .../src/apps/complex/bootstrap.ts | 20 + .../src/apps/complex/chat.gateway.ts | 175 + .../src/apps/complex/complex.auth.ts | 261 + .../src/apps/complex/complex.module.ts | 71 + .../src/apps/complex/complex.store.ts | 1354 +++ .../controllers/catalog.controllers.ts | 217 + .../complex/controllers/chat.controller.ts | 125 + .../controllers/commerce.controllers.ts | 227 + .../controllers/identity.controllers.ts | 196 + .../controllers/operations.controllers.ts | 254 + .../complex/controllers/system.controllers.ts | 177 + .../src/apps/complex/dto/catalog.dto.ts | 329 + .../src/apps/complex/dto/chat.dto.ts | 87 + .../src/apps/complex/dto/commerce.dto.ts | 324 + .../src/apps/complex/dto/identity.dto.ts | 139 + .../src/apps/complex/dto/operations.dto.ts | 244 + .../demo-backend/src/apps/complex/main.ts | 12 + .../demo-backend/src/apps/simple/bootstrap.ts | 20 + examples/demo-backend/src/apps/simple/main.ts | 11 + .../src/apps/simple/simple.auth.ts | 197 + .../src/apps/simple/simple.controllers.ts | 399 + .../src/apps/simple/simple.dto.ts | 365 + .../src/apps/simple/simple.module.ts | 33 + .../src/apps/simple/simple.store.ts | 432 + .../src/common/api-exception.filter.ts | 79 + .../demo-backend/src/common/api.decorators.ts | 122 + examples/demo-backend/src/common/api.dto.ts | 135 + .../src/common/configure-application.ts | 57 + .../src/common/infrastructure.module.ts | 15 + .../src/common/observability.interceptor.ts | 26 + examples/demo-backend/src/common/openapi.ts | 71 + .../demo-backend/src/common/request.types.ts | 14 + .../src/common/scenario.interceptor.ts | 145 + .../src/scripts/generate-openapi.ts | 34 + .../src/scripts/validate-openapi.ts | 92 + .../demo-backend/test/complex.e2e-spec.ts | 288 + examples/demo-backend/test/simple.e2e-spec.ts | 159 + examples/demo-backend/tsconfig.build.json | 8 + examples/demo-backend/tsconfig.json | 23 + package-lock.json | 1 - package.json | 10 +- scripts/check-docs-search.mjs | 88 - scripts/check-docs.mjs | 118 - scripts/check-site.mjs | 149 + scripts/lib/specification.mjs | 129 - site/.vitepress/config.mts | 323 +- site/.vitepress/rules.data.mts | 14 - site/.vitepress/search.mts | 61 - site/.vitepress/theme/DocSetHeader.vue | 32 +- site/.vitepress/theme/Layout.vue | 19 +- site/.vitepress/theme/RuleCatalog.vue | 271 - site/.vitepress/theme/index.ts | 4 - site/.vitepress/theme/style.css | 104 +- site/README.md | 27 +- 122 files changed, 29145 insertions(+), 3253 deletions(-) create mode 100644 DRAFT/README.md create mode 100644 DRAFT/domains/README.md create mode 100644 DRAFT/domains/auth-example.md create mode 100644 DRAFT/domains/business.md create mode 100644 DRAFT/domains/domain.md create mode 100644 DRAFT/domains/factory-ports-adapters.md create mode 100644 DRAFT/domains/framework-bindings.md create mode 100644 DRAFT/domains/open-questions.md create mode 100644 DRAFT/domains/presets.md create mode 100644 DRAFT/domains/testing.md create mode 100644 DRAFT/index.md create mode 100644 DRAFT/level-1/README.md create mode 100644 DRAFT/level-1/components.md create mode 100644 DRAFT/level-1/dependencies.md create mode 100644 DRAFT/level-1/groups.md create mode 100644 DRAFT/level-1/layers.md create mode 100644 DRAFT/level-1/lifecycle.md create mode 100644 DRAFT/level-1/modules.md create mode 100644 DRAFT/level-1/nested-modules.md create mode 100644 DRAFT/level-1/segments.md create mode 100644 DRAFT/level-1/terminology.md create mode 100644 DRAFT/level-1/validation.md create mode 100644 DRAFT/rules/README.md create mode 100644 DRAFT/rules/level-1.md delete mode 100644 docs/en/index.md delete mode 100644 docs/index.md delete mode 100644 docs/ru/index.md delete mode 100644 docs/ru/specification/architecture-model.md delete mode 100644 docs/ru/specification/architecture-modes.md delete mode 100644 docs/ru/specification/foundations.md delete mode 100644 docs/ru/specification/index.md delete mode 100644 docs/ru/specification/layers/app.md delete mode 100644 docs/ru/specification/layers/compositions.md delete mode 100644 docs/ru/specification/layers/index.md delete mode 100644 docs/ru/specification/layers/infra.md delete mode 100644 docs/ru/specification/layers/shared.md delete mode 100644 docs/ru/specification/layers/ui.md delete mode 100644 docs/ru/specification/modes/advanced/domains.md delete mode 100644 docs/ru/specification/modes/advanced/index.md delete mode 100644 docs/ru/specification/modes/pro/domains/business.md delete mode 100644 docs/ru/specification/modes/pro/domains/client-and-server.md delete mode 100644 docs/ru/specification/modes/pro/domains/cross-domain-boundary.md delete mode 100644 docs/ru/specification/modes/pro/domains/framework.md delete mode 100644 docs/ru/specification/modes/pro/domains/index.md delete mode 100644 docs/ru/specification/modes/pro/domains/ports-and-adapters.md delete mode 100644 docs/ru/specification/modes/pro/domains/testing.md delete mode 100644 docs/ru/specification/modes/pro/index.md delete mode 100644 docs/ru/specification/modules-and-groups.md delete mode 100644 docs/ru/specification/monorepo.md delete mode 100644 docs/ru/specification/public-api-and-imports.md delete mode 100644 docs/ru/specification/rules.md delete mode 100644 docs/ru/specification/runtime-and-lifecycle.md delete mode 100644 docs/ru/specification/segments.md delete mode 100644 docs/ru/specification/state-and-data.md delete mode 100644 docs/ru/specification/terminology.md delete mode 100644 docs/ru/specification/testing-and-conformance.md create mode 100644 draft-rules.js create mode 100644 examples/demo-backend/.env.example create mode 100644 examples/demo-backend/.gitignore create mode 100644 examples/demo-backend/README.md create mode 100644 examples/demo-backend/docs/CASES.md create mode 100644 examples/demo-backend/docs/WEBSOCKET.md create mode 100644 examples/demo-backend/nest-cli.json create mode 100644 examples/demo-backend/openapi/complex.json create mode 100644 examples/demo-backend/openapi/simple.json create mode 100644 examples/demo-backend/package-lock.json create mode 100644 examples/demo-backend/package.json create mode 100644 examples/demo-backend/src/apps/complex/bootstrap.ts create mode 100644 examples/demo-backend/src/apps/complex/chat.gateway.ts create mode 100644 examples/demo-backend/src/apps/complex/complex.auth.ts create mode 100644 examples/demo-backend/src/apps/complex/complex.module.ts create mode 100644 examples/demo-backend/src/apps/complex/complex.store.ts create mode 100644 examples/demo-backend/src/apps/complex/controllers/catalog.controllers.ts create mode 100644 examples/demo-backend/src/apps/complex/controllers/chat.controller.ts create mode 100644 examples/demo-backend/src/apps/complex/controllers/commerce.controllers.ts create mode 100644 examples/demo-backend/src/apps/complex/controllers/identity.controllers.ts create mode 100644 examples/demo-backend/src/apps/complex/controllers/operations.controllers.ts create mode 100644 examples/demo-backend/src/apps/complex/controllers/system.controllers.ts create mode 100644 examples/demo-backend/src/apps/complex/dto/catalog.dto.ts create mode 100644 examples/demo-backend/src/apps/complex/dto/chat.dto.ts create mode 100644 examples/demo-backend/src/apps/complex/dto/commerce.dto.ts create mode 100644 examples/demo-backend/src/apps/complex/dto/identity.dto.ts create mode 100644 examples/demo-backend/src/apps/complex/dto/operations.dto.ts create mode 100644 examples/demo-backend/src/apps/complex/main.ts create mode 100644 examples/demo-backend/src/apps/simple/bootstrap.ts create mode 100644 examples/demo-backend/src/apps/simple/main.ts create mode 100644 examples/demo-backend/src/apps/simple/simple.auth.ts create mode 100644 examples/demo-backend/src/apps/simple/simple.controllers.ts create mode 100644 examples/demo-backend/src/apps/simple/simple.dto.ts create mode 100644 examples/demo-backend/src/apps/simple/simple.module.ts create mode 100644 examples/demo-backend/src/apps/simple/simple.store.ts create mode 100644 examples/demo-backend/src/common/api-exception.filter.ts create mode 100644 examples/demo-backend/src/common/api.decorators.ts create mode 100644 examples/demo-backend/src/common/api.dto.ts create mode 100644 examples/demo-backend/src/common/configure-application.ts create mode 100644 examples/demo-backend/src/common/infrastructure.module.ts create mode 100644 examples/demo-backend/src/common/observability.interceptor.ts create mode 100644 examples/demo-backend/src/common/openapi.ts create mode 100644 examples/demo-backend/src/common/request.types.ts create mode 100644 examples/demo-backend/src/common/scenario.interceptor.ts create mode 100644 examples/demo-backend/src/scripts/generate-openapi.ts create mode 100644 examples/demo-backend/src/scripts/validate-openapi.ts create mode 100644 examples/demo-backend/test/complex.e2e-spec.ts create mode 100644 examples/demo-backend/test/simple.e2e-spec.ts create mode 100644 examples/demo-backend/tsconfig.build.json create mode 100644 examples/demo-backend/tsconfig.json delete mode 100644 scripts/check-docs-search.mjs delete mode 100644 scripts/check-docs.mjs create mode 100644 scripts/check-site.mjs delete mode 100644 scripts/lib/specification.mjs delete mode 100644 site/.vitepress/rules.data.mts delete mode 100644 site/.vitepress/search.mts delete mode 100644 site/.vitepress/theme/RuleCatalog.vue diff --git a/DRAFT/README.md b/DRAFT/README.md new file mode 100644 index 0000000..7d4c29e --- /dev/null +++ b/DRAFT/README.md @@ -0,0 +1,15 @@ +# Черновики SLM + +> Материалы в `DRAFT` являются рабочими черновиками и не задают нормативную спецификацию SLM. + +## Материалы + +- [Первый уровень](./level-1/README.md) - базовые слои, модули и зависимости. +- [Домены](./domains/README.md) - исследование доменов и строгих границ выполнения. +- [Правила](./rules/README.md) - канонические наборы, формат и правила формулировки. + +## Соглашение + +Черновики могут содержать определения, правила, рекомендации, примеры и открытые вопросы. + +Нормативные определения задаются терминологией соответствующего уровня. Только блокирующие правила получают код SLM; тематические черновики ссылаются на канонические правила и не повторяют их формулировки. diff --git a/DRAFT/domains/README.md b/DRAFT/domains/README.md new file mode 100644 index 0000000..5428384 --- /dev/null +++ b/DRAFT/domains/README.md @@ -0,0 +1,84 @@ +# Domains: рабочие заметки + +> Статус: исследовательский черновик. Материалы в этой папке не являются спецификацией и пока не задают обязательных правил SLM. + +Эта папка фиксирует текущую гипотезу о новой сущности `Domain`, business-модуле внутри неё, framework-neutral factory, ports, adapters, presets и framework bindings. + +Идентификаторы вида `DOM-N001` и `FAC-N001` являются стабильными якорями заметок. Они нужны для обсуждения и последующего переноса решений в спецификацию, но не являются идентификаторами нормативных правил. + +## Основная формула + +```text +Business определяет ЧТО делать. +Ports описывают ЧТО business нужно. +Factory создаёт business API из ports. +Adapters реализуют ports в конкретной среде. +Preset выбирает adapters, scope и lifecycle. +Framework binding подключает готовый API к React, Vue, Next.js и другим фреймворкам. +``` + +Краткая схема: + +```text + ┌─ browser preset + ├─ SSR request preset +Business factory + ports ├─ server action preset + ├─ per-test assembly + └─ другой application preset + +готовый business API instance + ├─ framework bindings + ├─ compositions + └─ другие business factories через ports +``` + +## Зафиксированные гипотезы + +### DOM-N001: Domain является отдельной архитектурной сущностью + +Domain является границей владения одной предметной областью. Он содержит modules и logical groups с разной технической ролью, но общей доменной принадлежностью. + +### DOM-N002: Business внутри Domain является модулем + +`business` имеет собственную ответственность и public API, поэтому это module, а не segment. `types/`, `services/`, `errors/` и `lib/` внутри business остаются segments. + +### FAC-N001: Один business-контракт имеет одну factory + +Разные среды выполнения не требуют разных factories, если они предоставляют один и тот же API. Различия среды выражаются ports, adapters и presets. + +### PRE-N001: Одна factory допускает несколько presets + +Browser, SSR, server action, tests и другие контексты могут собирать одну factory с разными реализациями ports. + +### FAC-N002: Business и factory нейтральны к framework и environment + +Изоморфный business import graph не достигает React, Vue, Next.js, browser-only, server-only, SDK, storage implementations и других concrete runtimes. + +### PRE-N002: Среда является свойством preset + +Client/server/request различия определяются preset и выбранными adapters, а не `mode` внутри factory. Tests создают отдельную per-test assembly напрямую через factory и не требуют общего test preset. + +## Карта заметок + +- [Domain](./domain.md) - роль новой сущности, структура и публичные границы. +- [Business](./business.md) - ответственность business-модуля, types, pure functions и errors. +- [Factory, ports и adapters](./factory-ports-adapters.md) - контракт factory и требования изоморфности. +- [Presets и SSR](./presets.md) - варианты сборки, lifecycle и защита server-only кода. +- [Framework bindings](./framework-bindings.md) - React/Vue/Next-код внутри Domain. +- [Тестирование](./testing.md) - границы тестов, factory-level contract, harness, adapters, presets, framework и UI. +- [Auth как проверочный пример](./auth-example.md) - применение гипотез к реальному модулю. +- [Открытые вопросы](./open-questions.md) - решения, которые ещё нельзя превращать в правила. + +## Предварительная структура приложения + +```text +src/ +├── app/ +├── compositions/ +├── domains/ +├── infra/ +├── ui/ +└── shared/ +``` + +`domains/` пока рассматривается как новая верхнеуровневая область, заменяющая разнесение одной доменной ответственности между `business/{domain}` и `compositions/business/{domain}`. diff --git a/DRAFT/domains/auth-example.md b/DRAFT/domains/auth-example.md new file mode 100644 index 0000000..3f8f0a4 --- /dev/null +++ b/DRAFT/domains/auth-example.md @@ -0,0 +1,173 @@ +# Auth как проверочный пример + +> Рабочая заметка на основе реального модуля `/home/gromov/projects/biocad/newbiocadru/apps/web/src/business/auth`. Код проекта не изменялся. + +Цель примера: проверить гипотезы Domain на существующем SLM business-модуле, а не предложить немедленную миграцию. + +## Текущее устройство + +```text +business/auth/ +├── auth.factory.ts +├── errors/ +├── hooks/ +├── mappers/ +├── services/ +├── tests/ +├── types/ +└── index.ts +``` + +Runtime-сборка находится отдельно: + +```text +compositions/business/knv/auth/ +├── adapters/ +├── create-knv-auth-business.ts +└── index.ts +``` + +Новая сущность Domain может колоцировать обе ответственности без смешивания ролей: + +```text +domains/auth/ +├── business/ +├── presets/ +│ └── {preset-name}/ +│ └── adapters/ +└── {framework-binding}/ +``` + +## Factory и client boundary + +### AUTH-N001: Текущий AuthApi содержит client-oriented hook + +`auth.factory.ts` импортирует `createAuthHook`, а `hooks/use-auth.hook.ts` содержит `'use client'`. Кроме того, `AuthDeps.session` описывает `useToken`. + +Текущий transitive graph: + +```text +authFactory + → createAuthHook + → 'use client' +``` + +Это практический пример того, почему neutral factory должна проверяться по всему transitive import graph, а framework hooks должны находиться в отдельном framework module Domain. Точный путь этого module пока не выбран. + +Возможное направление: + +```text +business AuthApi + → framework-neutral state observation + +React binding + → useAuth над готовым AuthApi +``` + +Финальный state contract пока не выбран. + +## Pure phone logic + +### AUTH-N002: Нормализация телефона уже дублируется + +Business содержит private `normalizePhoneOtpPhone`, а auth-widget содержит отдельный `getPhoneDigits` и собственный `PHONE_DIGITS_LENGTH`. + +Это кандидат на public pure business function: + +```ts +import { + normalizeAuthPhone, + validateAuthPhone, +} from '@/domains/auth/business' +``` + +Business service и UI могут использовать одну семантику. Business service всё равно повторно валидирует вход независимо от UI-проверки. + +Существующий `business/user` показывает другой workaround: pure validators возвращаются через собранный `userFactory` API. Прямой pure export позволит не требовать assembly для детерминированной функции. + +## Error contract + +### AUTH-N003: Error contract фактически публичен, но описан не полностью + +Business создаёт `AuthBusinessError` с `code` и `retryAfterSeconds`, но public `index.ts` экспортирует только type `AuthErrorCode`. + +Consumer auth-widget поэтому: + +- повторяет строковые error codes в message map; +- создаёт локальный `AuthErrorData`; +- вручную проверяет `code` и `retryAfterSeconds` в `unknown`; +- самостоятельно нормализует форму caught error. + +Предварительное исправление границы: + +```ts +// Public business API. +export { AUTH_ERROR_CODES, isAuthError } +export type { AuthError, AuthErrorCode } + +// Business-private implementation. +class AuthBusinessError extends Error {} +const createAuthBusinessError = (...) => {} +``` + +Consumer получает безопасный observation contract, но не получает constructor и source mapping. + +## Presets + +### AUTH-N004: Текущий createKnvAuthBusiness является preset + +`createKnvAuthBusiness()` выбирает `knvAuthPhoneAdapter` и `appAuthSessionAdapter`, затем вызывает `authFactory`. + +В новой терминологии это application preset, внутри которого могут оставаться KNV-specific adapters: + +```text +domains/auth/presets/application/create-application-auth.ts +``` + +Он не является единственно допустимым assembly site. Tests, SSR request composition и другой product preset могут напрямую вызвать ту же `authFactory`. + +## SSR-вариант + +Одна factory позволяет получить request-scoped API без второй реализации business: + +```ts +import 'server-only' + +export const createAuthForRequest = (input: AuthRequestInput) => { + return authFactory({ + authPhone: createKnvServerAuthPhoneAdapter(input), + session: createRequestAuthSessionAdapter(input), + }) +} +``` + +Browser preset использует другую реализацию тех же ports. Factory, business types, pure functions и error contract остаются общими. + +## Предварительная целевая структура + +```text +domains/auth/ +├── business/ +│ ├── auth.factory.ts +│ ├── errors/ +│ ├── lib/ +│ ├── mappers/ +│ ├── services/ +│ ├── tests/ +│ ├── types/ +│ └── index.ts +├── presets/ +│ └── application/ +│ ├── adapters/ +│ ├── create-application-auth.ts +│ ├── create-application-auth.test.ts +│ └── index.ts +└── {framework-binding}/ + └── index.ts +``` + +Это только проверочная структура. Она не фиксирует обязательность всех папок и не должна использоваться как scaffold checklist. + +Server-only/request preset может быть добавлен отдельным module при реальной потребности. Он не образует обязательную `server`-ветку Domain. + +Tests не используют общий testing preset. Business tests выполняют per-test assembly напрямую через `authFactory`, а production presets тестируются рядом с собственной реализацией только на wiring, scope и lifecycle. diff --git a/DRAFT/domains/business.md b/DRAFT/domains/business.md new file mode 100644 index 0000000..d8f4912 --- /dev/null +++ b/DRAFT/domains/business.md @@ -0,0 +1,174 @@ +# Business module внутри Domain + +> Рабочая заметка. Не является нормативным разделом спецификации. + +## Роль + +### BUS-N001: Business является семантическим ядром Domain + +Business-модуль владеет: + +- публичными бизнес-сценариями; +- business-owned types и contracts; +- business API; +- factory и ports; +- детерминированными доменными правилами; +- доменным error contract; +- преобразованием внешних результатов в доменные результаты. + +Business не владеет concrete runtime, environment wiring и framework integration. + +## Public API business-модуля + +### BUS-N002: Business может экспортировать четыре категории сущностей + +| Категория | Примеры | +|---|---| +| Factory | `authFactory` | +| Types и contracts | `AuthApi`, `AuthDeps`, `AuthState`, `AuthErrorCode` | +| Pure domain functions | `normalizeAuthPhone`, `validateAuthPhone` | +| Error observation contract | `AUTH_ERROR_CODES`, `AuthError`, `isAuthError` | + +Это заменяет старую гипотезу, что business `index.ts` может экспортировать в runtime только factory. + +Предварительный public API: + +```ts +export { authFactory } from './auth.factory' + +export { + AUTH_ERROR_CODES, + isAuthError, +} from './errors/auth-error' + +export { + normalizeAuthPhone, + validateAuthPhone, +} from './lib/auth-phone' + +export type { + AuthApi, + AuthDeps, + AuthError, + AuthErrorCode, + AuthFactory, + AuthState, +} +``` + +## Types + +### BUS-N003: Business contracts остаются внутри business + +Отдельный `model` submodule пока не требуется. Типы размещаются по ownership: + +| Тип | Место | +|---|---| +| `AuthApi`, `AuthDeps`, `AuthState` | `domains/auth/business/types` | +| `AuthError`, `AuthErrorCode` | `domains/auth/business/types` или `errors` | +| SDK DTO | Adapter или infra runtime | +| React provider props | Выбранный React binding module Domain | +| View model конкретного screen | Consumer composition | + +`types/` является segment business-модуля, а не самостоятельным общим хранилищем Domain. + +## Pure domain functions + +### BUS-N004: Детерминированная доменная функция может экспортироваться напрямую + +Pure domain function: + +- получает все данные через аргументы; +- возвращает результат только на основе аргументов; +- не использует `Deps`; +- не выполняет I/O; +- не читает mutable runtime state; +- не зависит от clock, random, env или platform API; +- не импортирует React, Vue, Next.js или state manager; +- использует business language и реализует доменное правило. + +Примеры: + +```ts +normalizeAuthPhone(value) +validateAuthPhone(value) +calculateOrderTotal(order) +hasRequiredUserAgreements(user) +``` + +Consumer может использовать такую функцию для раннего UX feedback. Business scenario всё равно обязан повторно проверить вход на своей границе. + +### BUS-N005: Не каждая pure function становится public + +Функция остаётся private, если она нужна только одному service или является технической деталью реализации. Public export оправдан доменной семантикой и реальным внешним либо межмодульным consumer. + +Папки `domain/shared` и `domain/public` не создаются только ради видимости. Public contract определяется entrypoint business-модуля. + +## Domain errors + +### BUS-N006: Создание и наблюдение ошибки являются разными контрактами + +Business создаёт domain error. Consumer только распознаёт ошибку и читает поля, от которых зависит его поведение. + +Public observation contract: + +```ts +export const AUTH_ERROR_CODES = { + PHONE_OTP_PHONE_INVALID: 'AUTH_PHONE_OTP_PHONE_INVALID', + PHONE_OTP_VERIFY_CODE_INVALID: 'AUTH_PHONE_OTP_VERIFY_CODE_INVALID', + PHONE_OTP_RESEND_TOO_SOON: 'AUTH_PHONE_OTP_RESEND_TOO_SOON', +} as const + +export type AuthErrorCode = + (typeof AUTH_ERROR_CODES)[keyof typeof AUTH_ERROR_CODES] + +export type AuthError = Readonly<{ + code: AuthErrorCode + retryAfterSeconds: number | null +}> + +export const isAuthError = (value: unknown): value is AuthError => { + // Structural runtime validation. +} +``` + +Private creation contract: + +```ts +class AuthBusinessError extends Error implements AuthError { + // Constructor, cause и source diagnostics. +} + +const createAuthBusinessError = (...) => { + // Source error mapping. +} +``` + +### BUS-N007: Error constructor не является consumer API + +Consumer не должен создавать `AuthBusinessError`, выбирать source mapping или подделывать business failure. Поэтому наружу предполагается экспортировать: + +- stable error code values; +- error code type; +- read-only observable error shape; +- runtime guard или parser. + +Наружу не предполагается экспортировать: + +- error constructor; +- error factory; +- source error mapper; +- transport-specific error data; +- internal fallback selection. + +### BUS-N008: Одних типов недостаточно при throw-based API + +TypeScript не описывает checked exceptions. Для сигнатуры + +```ts +(data: VerifyPhoneOtpData) => Promise +``` + +значение в `catch` всё равно имеет тип `unknown`. Если consumer различает ошибки по `code`, business должен предоставить runtime discriminator либо перейти на typed `Result`. + +Выбор между throw + guard и typed `Result` пока не закрыт окончательно. Текущий минимальный путь совместимости: throw + public observation contract. diff --git a/DRAFT/domains/domain.md b/DRAFT/domains/domain.md new file mode 100644 index 0000000..759eb42 --- /dev/null +++ b/DRAFT/domains/domain.md @@ -0,0 +1,206 @@ +# Domain + +> Рабочая заметка. Не является нормативным разделом спецификации. + +## Определение + +### DOM-N003: Domain является границей владения предметной областью + +Domain группирует business-контракт, concrete integrations, готовые presets и framework-specific bindings одной предметной области. + +Примеры Domain: + +- `auth`; +- `user`; +- `catalog`; +- `orders`; +- `checkout`. + +Domain не является одним большим module. Он является границей, внутри которой могут находиться modules и logical groups с заданным направлением зависимостей. + +```text +Domain +├── business module +├── presets group +│ └── preset modules +├── framework binding module или group +└── optional reusable adapters group + └── adapter modules +``` + +## Предварительная структура + +```text +domains/auth/ +├── business/ +│ ├── auth.factory.ts +│ ├── errors/ +│ ├── lib/ +│ ├── services/ +│ ├── tests/ +│ ├── types/ +│ └── index.ts +├── presets/ +│ └── {preset-name}/ +│ ├── adapters/ +│ ├── create-auth.ts +│ ├── create-auth.test.ts +│ └── index.ts +└── {framework-binding}/ + ├── hooks/ + ├── providers/ + ├── tests/ + ├── ui/ + └── index.ts +``` + +`{preset-name}` и `{framework-binding}` являются placeholders, а не обязательными именами папок. Preset называется по своему scope или назначению. Framework binding может быть оформлен как `react`, `bindings/react`, `framework/react` или по другому локальному соглашению. + +Environment-specific preset, включая server-only вариант, может быть добавлен отдельным preset module. SLM не требует заранее делить `presets` или `adapters` на `browser`, `server` и другие технические категории. + +## Возможные ветки Domain + +### DOM-N007: Domain не имеет фиксированного набора верхних папок + +| Роль | Типичная форма | Статус | +|---|---|---| +| Business | Один business module | Основная гипотеза Domain | +| Presets | Logical group с preset modules | По наличию повторяемых assemblies | +| Framework bindings | Module или logical group | По наличию framework integration | +| Reusable adapters | Logical group с adapter modules | Только после promotion из владельца | +| Tests | Segment конкретного module | Не создаётся в корне Domain | + +`model`, `types`, `errors`, `lib`, `ui`, `client` и `server` не становятся верхними Domain-разделами автоматически. Они размещаются внутри module-владельца либо появляются как локальное соглашение с отдельным обоснованием. + +## Иерархия сущностей + +### DOM-N004: Роль и структурный вид являются независимыми характеристиками + +Архитектурная роль отвечает на вопрос «какую ответственность выполняет код»: + +- business; +- preset; +- framework binding; +- adapter. + +Структурный вид отвечает на вопрос «как оформлена граница кода»: + +- Domain; +- module; +- group; +- segment; +- file. + +```text +Domain +├── Module +│ ├── Segment +│ │ └── File +│ └── File +└── Group + ├── Module + └── Group + └── Module +``` + +Правила структурных видов: + +- Module владеет самостоятельной ответственностью и public API. +- Group является logical directory для навигации, не имеет `index.ts`, runtime и собственных файлов реализации. +- Segment существует внутри module, группирует его файлы по назначению и не имеет отдельного внешнего API. +- Имя папки само по себе не доказывает её структурный вид. + +Пример классификации: + +| Путь | Роль | Структурный вид | +|---|---|---| +| `domains/auth` | Предметная область Auth | Domain | +| `domains/auth/business` | Business | Module | +| `domains/auth/business/services` | Business scenarios | Segment | +| `domains/auth/business/tests` | Business tests | Segment | +| `domains/auth/presets` | Навигация presets | Group | +| `domains/auth/presets/{preset-name}` | Preset | Module | +| `domains/auth/presets/{preset-name}/adapters` | Private adapters preset | Segment | +| `domains/auth/{framework-binding}` | Framework binding | Module или Group по фактической границе | +| `domains/auth/adapters` | Навигация promoted adapters | Optional group | +| `domains/auth/adapters/{adapter-name}` | Reusable adapter | Module | + +## Публичные границы + +### DOM-N005: Domain предоставляет отдельные public submodules + +Предварительно Domain не имеет обязательного общего facade. Каждый public module предоставляет собственный entrypoint: + +```ts +import { authFactory, validateAuthPhone } from '@/domains/auth/business' +import { createApplicationAuth } from '@/domains/auth/presets/application' +import { AuthProvider, useAuth } from '@/domains/auth/react' +``` + +`application` и `react` здесь являются только примерами пользовательских имён. Отдельные entrypoints не смешивают business, concrete assembly и framework code в одном import graph. + +Возможные public entrypoints: + +```text +@/domains/auth/business +@/domains/auth/presets/{preset-name} +@/domains/auth/{framework-binding} +@/domains/auth/adapters/{adapter-name} # только для promoted adapter module +``` + +Private adapters внутри preset не получают собственного внешнего entrypoint. + +### DOM-N006: Omnibus barrel для всего Domain опасен + +Такой entrypoint может связать изоморфный, client-only и server-only graphs: + +```ts +// Не использовать как default-подход. +export * from './business' +export * from './presets/application' +export * from './react' +``` + +Tree shaking не считается security boundary. Server-only submodule не должен быть достижим из изоморфного или client entrypoint даже через re-export. + +## Предварительное направление зависимостей + +```text +business + ↑ +preset + private adapters + +готовый business API instance + ↑ +framework bindings / compositions +``` + +Более точная схема импортов: + +```text +business -/→ adapters | presets | framework | infra concrete runtime +preset-private adapters → business contracts + concrete runtime +promoted adapter module → business contracts + concrete runtime +presets → business factory + private or promoted adapters +framework → business contracts + ready API or preset +compositions → ready business API + framework bindings +``` + +Framework module может одновременно быть assembly site, если он явно владеет lifecycle API instance. Наличие папки `presets/` не даёт ей монополию на вызов factory. + +## Domain и compositions + +Domain владеет повторяемой доменной ответственностью. Composition по-прежнему владеет страницей, route tree, экраном и конкретным пользовательским outcome. + +Предварительная граница: + +| Ответственность | Владелец | +|---|---| +| Auth scenarios и contracts | `domains/auth/business` | +| Private auth adapters одной assembly | Segment внутри соответствующего preset module | +| Reusable auth adapter | Optional adapter module после promotion | +| Повторяемая сборка AuthApi | Конкретный preset module или другой assembly site | +| Auth React provider/access hook | Выбранный framework binding module | +| Текст ошибки, redirect, экран и route outcome | Consumer composition | + +Граница domain-specific UI пока остаётся открытым вопросом. diff --git a/DRAFT/domains/factory-ports-adapters.md b/DRAFT/domains/factory-ports-adapters.md new file mode 100644 index 0000000..6f0bec3 --- /dev/null +++ b/DRAFT/domains/factory-ports-adapters.md @@ -0,0 +1,200 @@ +# Factory, ports и adapters + +> Рабочая заметка. Не является нормативным разделом спецификации. + +## Терминология + +### FAC-N003: Собирается API instance, а не factory + +```text +Factory + Deps implementations → business API instance +``` + +- Factory является функцией создания. +- Ports являются business-owned контрактами capabilities. +- `Deps` группирует ports, нужные factory. +- Adapters реализуют ports в concrete runtime. +- Assembly site вызывает factory и получает API instance. +- Preset является готовой конфигурацией assembly. + +Формулировка «собранная фабрика» неточна. Factory конфигурируется зависимостями и создаёт собранный API. + +## Business factory + +### FAC-N004: Factory является framework-neutral и environment-neutral + +Factory не знает, где будет использована: + +- в browser; +- во время SSR; +- в server action; +- в background process; +- в unit test; +- в React, Vue или другом framework. + +```ts +export type AuthFactory = (deps: AuthDeps) => AuthApi +``` + +### FAC-N005: Factory имеет стабильную форму результата + +Все presets одной factory создают один и тот же business API contract. Среда не выбирается через аргумент `mode`, а форма API не зависит от наличия optional dependency. + +Не рекомендуется: + +```ts +authFactory({ + mode: 'server', + serverAdminClient: optionalClient, +}) +``` + +Не рекомендуется возвращать методы, которые существуют в общем API, но намеренно падают в одной из сред. + +### FAC-N006: Factory construction не выполняет side effects + +Вызов factory не должен: + +- выполнять network request; +- читать cookies, storage или env; +- запускать subscription или timer; +- обращаться к browser либо Node API; +- создавать скрытый application singleton; +- выбирать concrete adapter; +- выполнять framework lifecycle. + +Factory может синхронно создать детерминированные services и связать их с переданными ports. + +## Гигиена import graph + +### FAC-N007: Весь достижимый из business import graph должен быть изоморфным + +Недостаточно проверить только файл `{domain}.factory.ts`. Ни один production import, достижимый из business public entrypoint, не должен приводить к: + +- React, Vue, Next.js и другим frameworks; +- `'use client'`, `client-only` или `server-only` boundary; +- browser API; +- Node-only API; +- concrete SDK/client; +- concrete storage; +- state/query runtime; +- adapters и presets; +- environment configuration. + +Tree shaking не используется как доказательство изоляции. + +## Ports + +### PORT-N001: Port принадлежит business + +Port описывает capability на языке business, а не форму concrete implementation. + +```ts +export type AuthPhonePort = { + requestCode: (phone: string) => Promise + verifyCode: (data: VerifyPhoneOtpData) => Promise +} +``` + +Port не должен раскрывать SDK client, generated operation, `Request`, `Window`, React hook, Zustand `StoreApi` и другие environment/framework types. + +### PORT-N002: Ports абстрагируют implementation, но не доступность capability + +Одна factory возможна, пока каждый preset способен реализовать одинаковые ports. + +Server capability может остаться общим port, если browser adapter реализует её через безопасный HTTP/RPC boundary. Если capability принципиально невозможно реализовать в одной из поддерживаемых сред, её нельзя маскировать optional dependency общего API. + +### PORT-N003: Reactive port должен быть framework-neutral + +Client hook в `Deps` делает контракт client-oriented. Вместо `useToken` базовый port может описывать framework-neutral observation protocol: + +```ts +export type AuthSessionPort = { + getSnapshot: () => AuthState + subscribe: (listener: () => void) => () => void + setToken: (token: string | null) => void +} +``` + +React binding может построить `useAuth` поверх `getSnapshot` и `subscribe`. Vue binding использует тот же port через собственный lifecycle. + +Точная форма reactive ports требует отдельной проверки на реальном state manager. + +## Adapters + +### ADP-N001: Adapter реализует business port + +Adapter знает одновременно business contract и concrete runtime: + +```text +business port ← adapter → SDK / storage / browser / request +``` + +Adapter может: + +- преобразовать domain arguments в transport arguments; +- вызвать concrete source; +- привести concrete runtime к минимальной форме port; +- управлять техническими деталями конкретной integration. + +Adapter не должен: + +- определять business error code; +- выбирать domain fallback; +- менять business invariant; +- расширять public business API методами concrete client. + +### ADP-N002: Adapter размещается у минимального владельца + +SLM не задаёт обязательную структуру `adapters/browser`, `adapters/server` или другую техническую классификацию. + +Adapter может быть: + +- private файлом или segment конкретного preset module; +- самостоятельным Domain module после появления нескольких assembly consumers; +- частью пользовательской logical group, если она действительно упрощает навигацию. + +Default colocation для adapter, принадлежащего одной assembly: + +```text +domains/auth/presets/{preset-name}/ +├── adapters/ +├── create-auth.ts +└── index.ts +``` + +Возможный promotion переиспользуемого adapter: + +```text +domains/auth/adapters/ # optional logical group +└── {adapter-name}/ # adapter module + └── index.ts +``` + +Environment-specific code не должен быть достижим из entrypoint, объявленного framework-neutral или environment-neutral. Способ физической изоляции выбирает проект. Группировка по `browser/server` допустима как локальное соглашение, но не является требованием SLM. + +## Assembly sites + +### ASM-N001: Вызов factory определяет роль assembler + +Factory может быть вызвана в preset, provider, route/request composition, test setup или другом месте. Путь сам по себе не запрещает сборку. + +Assembly site обязан: + +- предоставить полный `Deps`; +- выбрать concrete adapters; +- определить предполагаемый scope API instance; +- вернуть необходимые lifecycle/dispose handles; +- не скрывать создание graph от фактического владельца. + +После возврата результата lifecycle принадлежит caller/graph owner, который удерживает API instance. Например, request владеет request-scoped instance, а Provider владеет instance до unmount. Preset описывает создание и передачу ownership, но не становится долгоживущим владельцем только из-за своего расположения. + +### ASM-N002: Consumer использует готовый API + +Screen, component или service, который только выполняет business-сценарий, получает готовый business API, например `AuthApi`. Если такой consumer вызывает factory, он становится assembler и должен удовлетворять всем требованиям assembly role. + +### ASM-N003: Cross-domain dependency получает собранный API + +Business одного Domain не создаёт factory другого Domain внутри себя. Он описывает необходимую capability через свой `Deps`, а graph owner передаёт уже собранный API. + +Tests вправе напрямую вызывать factory с mocks и fakes. Это один из основных сценариев существования factory. diff --git a/DRAFT/domains/framework-bindings.md b/DRAFT/domains/framework-bindings.md new file mode 100644 index 0000000..9555fa1 --- /dev/null +++ b/DRAFT/domains/framework-bindings.md @@ -0,0 +1,109 @@ +# Framework bindings внутри Domain + +> Рабочая заметка. Не является нормативным разделом спецификации. + +## Определение + +### FW-N001: Framework code определяется зависимостью от framework + +К framework code относится код, существующий из-за React, Vue, Next.js или другого framework/runtime contract: + +- components; +- providers и contexts; +- framework hooks; +- framework lifecycle; +- directives и framework entrypoints; +- framework-specific types; +- server/client component boundaries. + +Такой код может принадлежать Domain по смыслу, но не размещается внутри framework-neutral business. + +## Роль binding + +### FW-N002: Framework binding адаптирует готовый business API + +Framework binding может: + +- предоставить готовый business API через context/provider; +- построить React/Vue hook доступа; +- связать framework lifecycle с domain subscription; +- предоставить domain-specific framework component; +- получить API instance через props, context или preset. + +Framework binding не изменяет business rules и не реализует source adapter вместо Domain preset/adapters. + +## Возможная структура + +```text +domains/auth/{framework-binding}/ +├── providers/ +├── hooks/ +├── components/ +├── types/ +└── index.ts +``` + +`{framework-binding}` является placeholder. SLM пока не выбирает между `react`, `bindings/react`, `framework/react` и другим локальным соглашением. Чёткая граница определяется самостоятельным module и отдельным public entrypoint, а не обязательным именем родительской папки. + +Если одна папка предоставляет cohesive framework API, она является module. Если папка только классифицирует несколько самостоятельных binding modules, она является logical group и не имеет собственного `index.ts`. + +## Reactive state + +### FW-N003: Framework hook строится снаружи business + +Если business предоставляет framework-neutral `getSnapshot` и `subscribe`, React binding может использовать `useSyncExternalStore`: + +```ts +'use client' + +export const createUseAuth = (authApi: AuthApi) => { + return () => { + return useSyncExternalStore( + authApi.subscribeAuthState, + authApi.getAuthState, + authApi.getAuthState, + ) + } +} +``` + +Это только иллюстрация направления. Финальная форма state port должна учитывать реальный state/query runtime. + +Business при таком подходе не импортирует React и не возвращает React hook как единственный способ чтения состояния. + +## Framework module как assembly site + +### FW-N004: Provider может владеть API instance + +Provider вправе вызвать preset или factory, если provider является явным владельцем scope и lifecycle: + +```text +AuthProvider + → createBrowserAuth preset + → AuthApi instance + → context + → access hooks +``` + +Provider construction не должен запускать I/O или subscription до framework commit/effect. Cleanup выполняется владельцем lifecycle. + +Framework module не обязан собирать API. Он также может получить готовый instance от route/page/application graph owner. + +## Framework-neutral и environment-neutral + +Эти свойства различаются: + +| Свойство | Запрещённая зависимость | +|---|---| +| Framework-neutral | React, Vue, Next lifecycle и types | +| Environment-neutral | Browser-only, Node-only, server-only, env/runtime globals | + +Business factory должна удовлетворять обоим свойствам. Framework binding по определению framework-specific, а preset по определению может быть environment-specific. + +## Domain UI + +### FW-N005: Framework принадлежность не доказывает Domain ownership + +React component размещается внутри Domain только если его ответственность принадлежит Domain. Page, screen, route outcome, локальный текст ошибки и продуктовая композиция могут остаться в `compositions`. + +Граница между domain-specific components и consumer compositions пока требует отдельных примеров. diff --git a/DRAFT/domains/open-questions.md b/DRAFT/domains/open-questions.md new file mode 100644 index 0000000..640d3a9 --- /dev/null +++ b/DRAFT/domains/open-questions.md @@ -0,0 +1,113 @@ +# Открытые вопросы Domains + +> Эти вопросы намеренно не сформулированы как правила. + +## Ошибки + +### OPEN-N001: Throw или typed Result + +Нужно решить, остаются ли ожидаемые domain failures исключениями с public runtime guard или business API возвращает discriminated `Result`. + +Текущий совместимый вариант: throw + `isDomainError`. Typed Result потребует изменения формы всех scenario methods. + +### OPEN-N002: Универсальный или domain-specific error guard + +Нужно определить, достаточно ли общего `isDomainError`, либо каждый business-модуль экспортирует `isAuthError`, `isUserError` и собственную проверку code set. + +## Domain structure + +### OPEN-N003: Имена framework modules + +Варианты: + +```text +domains/auth/framework/react +domains/auth/bindings/react +domains/auth/react +``` + +`framework/react` явно классифицирует роль, `react` сокращает import path, а `bindings/react` подчёркивает adapter-like назначение границы. Выбор пока не сделан. + +### OPEN-N004: Нужен ли root Domain entrypoint + +Статус: предварительно закрыт в пользу нескольких entrypoints. + +Каждый public module Domain предоставляет собственную точку входа: business, конкретный preset, framework binding и promoted adapter. Обязательный root runtime barrel не создаётся, потому что он может смешать isomorphic, client-only и server-only graphs. + +### OPEN-N005: Public adapters + +Текущая гипотеза: adapter начинается как private segment минимального владельца, обычно preset module. При появлении самостоятельной ответственности или нескольких assembly consumers он может быть поднят в отдельный adapter module с собственным entrypoint. + +Открытым остаётся точный promotion criterion; фиксированная числовая граница пока не выбрана. + +## Factory и ports + +### OPEN-N006: Гранулярность одной factory + +Одна factory может возвращать большой API, хотя конкретному SSR scope нужны два метода. Нужно проверить, достаточно ли narrowed preset view, или крупные contracts требуют нескольких business modules/factories. + +Предварительный принцип: одна factory на один связный business API contract; разные environments сами по себе не создают новую factory. + +### OPEN-N007: Reactive state contract + +Нужно проверить на реальном Zustand/React/SSR кейсе форму framework-neutral state port: + +- `getSnapshot` + `subscribe`; +- commands/selectors; +- initial server snapshot; +- hydration; +- cleanup; +- concurrent rendering. + +## Framework boundary + +### OPEN-N008: Domain-specific UI + +Нужно решить, какие auth components принадлежат выбранному Auth framework binding module, а какие остаются composition widgets/screens. + +Framework dependency сама по себе не доказывает Domain ownership. + +## Cross-domain dependencies + +### OPEN-N009: Прямой импорт pure functions другого Domain + +Нужно определить, может ли business одного Domain напрямую импортировать pure function другого Domain или cross-domain связь всегда должна проходить через `Deps`. + +Возможный компромисс: + +- type-only contracts разрешены; +- runtime API передаётся через ports; +- pure function import разрешён только как явно зафиксированная ацикличная Domain dependency. + +## Уровни архитектуры + +### OPEN-N010: На каком уровне появляется Domain + +Нужно встроить Domain в монотонную шкалу архитектурных уровней. Более высокий уровень должен добавлять требования и не отменять правила предыдущего. + +Предварительный вариант: + +```text +Level 1: modules +Level 2: layers +Level 3: domains +Level 4: runtime-safe factories, ports, presets и verification +``` + +Точная классификация будет выполнена после извлечения атомарных правил из legacy-документации и этих заметок. + +## Проверяемость + +### OPEN-N011: Architecture lint + +Будущие проверки могут контролировать: + +- запрещённые imports из `business/**`; +- отсутствие server-only graph в isomorphic entrypoint; +- отсутствие client framework в factory graph; +- разрешённые категории exports business public API; +- запрет `export *` на environment boundaries; +- cycles между Domain modules; +- preset lifecycle declarations. + +Семантическую чистоту функции нельзя надёжно доказать только по имени export. Для этого потребуется сочетание folder conventions, import restrictions, AST checks и public API tests. diff --git a/DRAFT/domains/presets.md b/DRAFT/domains/presets.md new file mode 100644 index 0000000..bb24ba9 --- /dev/null +++ b/DRAFT/domains/presets.md @@ -0,0 +1,141 @@ +# Presets и SSR + +> Рабочая заметка. Не является нормативным разделом спецификации. + +## Определение + +### PRE-N003: Preset является готовым вариантом assembly + +Preset выбирает implementations ports и создаёт API одной business factory для конкретного execution context. + +```ts +export const createKnvAuthBusiness = (): AuthApi => { + return authFactory({ + authPhone: knvAuthPhoneAdapter, + session: appAuthSessionAdapter, + }) +} +``` + +`createKnvAuthBusiness` является preset builder, а не второй factory и не единственно допустимое место сборки. + +## Несколько presets одной factory + +```text +authFactory +├── createBrowserAuth +├── createAuthForRequest +├── createAuthForServerAction +└── другие production presets + +tests и custom graph owners могут вызывать authFactory напрямую +``` + +### PRE-N004: Presets могут отличаться adapters и lifecycle + +Browser preset может использовать browser storage и query runtime. Request preset может использовать cookies, headers и request-scoped client. Tests вместо общего preset создают локальную per-test assembly с memory ports, mocks или fakes. + +Business rules и форма создаваемого `AuthApi` при этом не меняются. + +### PRE-N005: Preset может предоставлять суженный API view + +Preset может не раскрывать consumer все методы созданного API: + +```ts +export type AuthSsrApi = Pick + +export const createAuthForRequest = ( + input: AuthRequestInput, +): AuthSsrApi => { + const authApi = authFactory(createRequestAuthDeps(input)) + + return { + resolveSession: authApi.resolveSession, + } +} +``` + +Это ограничивает contract конкретного scope, но не создаёт новую business factory. + +## SSR + +### PRE-N006: Request владеет instance, созданным request preset + +Если API зависит от cookies, headers, tenant, locale, request ID или abort signal, preset создаёт новый instance для каждого request и передаёт ownership вызывающему request scope. + +Application singleton для request data недопустим, потому что может смешать состояния независимых запросов. Если preset создаёт disposable resource, результат должен позволить request owner выполнить cleanup. + +Предварительная форма: + +```ts +import 'server-only' + +export const createAuthForRequest = ( + input: AuthRequestInput, +): AuthApi => { + return authFactory({ + authPhone: createServerAuthPhoneAdapter(input), + session: createRequestSessionAdapter(input), + }) +} +``` + +### PRE-N007: SSR использует тот же business contract + +Преимущества одной factory: + +- одинаковые business rules в browser и на server; +- одинаковые domain types и errors; +- request adapters не протекают в business; +- factory тестируется без Next.js; +- backend, cookies и headers заменяются независимо; +- server rendering не требует второй реализации business. + +## Server-only boundary + +### PRE-N008: Environment-specific preset может иметь отдельный public entrypoint + +Если preset должен быть недостижим из client graph, проект может выделить для него отдельный entrypoint и использовать framework/build marker. Имя и физическая группировка preset не задаются SLM. + +```ts +// Один из возможных server-only preset entrypoints. +import 'server-only' + +export { createAuthForRequest } from './create-auth-for-request' +``` + +Этот entrypoint не реэкспортируется через: + +- `domains/auth/business`; +- browser preset; +- React client binding; +- общий Domain barrel. + +Server adapters также могут иметь собственный `server-only` marker для защиты от ошибочного прямого импорта. + +### PRE-N009: Isomorphic factory не импортирует server-only marker + +`server-only` относится к preset/framework boundary, а не к business factory. Это позволяет вызывать factory в unit tests, другом server framework или browser preset. + +## Browser boundary + +### PRE-N010: Client-compatible preset не достигает server-only graph + +Client-compatible preset импортирует только isomorphic business и совместимые с ним adapters. Secrets, privileged SDK и Node-only modules не должны входить в его transitive import graph. + +Framework marker `'use client'` размещается в framework binding или client entrypoint, а не в business. + +## Preset не является обязательным посредником + +### PRE-N011: Custom assembly остаётся допустимой + +Graph owner может напрямую вызвать factory: + +```ts +const authApi = authFactory({ + authPhone: customAuthPhoneAdapter, + session: memorySessionAdapter, +}) +``` + +Preset нужен для повторяемой готовой конфигурации. Он не ограничивает DI-возможности factory. diff --git a/DRAFT/domains/testing.md b/DRAFT/domains/testing.md new file mode 100644 index 0000000..5ebde0e --- /dev/null +++ b/DRAFT/domains/testing.md @@ -0,0 +1,402 @@ +# Тестирование Domain + +> Рабочая заметка. Не является нормативным разделом спецификации. + +## Главный принцип + +### TST-N001: Тест размещается у владельца проверяемой ответственности + +Domain не получает одну общую папку `tests/` для всего кода. Business behavior, adapter wiring, preset lifecycle, framework bindings и UI имеют разных владельцев и тестируются рядом с ними. + +```text +business behavior → business tests +pure domain rule → colocated business test +adapter behavior → adapter test +preset assembly → preset test +framework lifecycle → framework binding test +UI interaction → UI owner test +cross-domain graph → graph owner test +``` + +## Матрица покрытия + +| Граница | Предварительная обязательность | Что проверяется | +|---|---|---| +| Business factory | Главная, обязательная | Scenarios, state, errors, ports, порядок effects | +| Public pure business functions | Обязательная | Validation, normalization, invariants и edge cases | +| Internal runtime-safe logic | По сложности | Mappers, guards, parsers, races и branching | +| Domain error implementation | Обязательная при runtime errors | Codes, guard, observable fields и source isolation | +| Adapters | Обязательная при наличии | Port contract, payload, raw result/error и cleanup | +| Production presets | Обязательная при наличии | Wiring, scope, ownership transfer и construction safety | +| Framework bindings | При наличии поведения | Provider, hooks, reactivity, lifecycle и hydration | +| Domain-owned UI | При наличии значимого поведения | States, interactions и accessibility contract | +| Cross-domain graph | При наличии graph | Assembly order, API handoff, scope и cleanup | +| E2E | По продуктовой потребности | Полный пользовательский поток | + +## Предварительная структура + +```text +domains/auth/ +├── business/ +│ ├── auth.factory.ts +│ ├── index.ts +│ ├── index.test.ts +│ ├── errors/ +│ │ ├── auth-error.ts +│ │ └── auth-error.test.ts +│ ├── lib/ +│ │ ├── auth-phone.ts +│ │ └── auth-phone.test.ts +│ ├── services/ +│ ├── types/ +│ └── tests/ +│ └── factory/ +│ ├── public-api.test.ts +│ ├── request-phone-otp.test.ts +│ ├── resend-phone-otp.test.ts +│ ├── verify-phone-otp.test.ts +│ └── testing/ +│ └── create-auth-test-harness.ts +├── presets/ +│ └── {preset-name}/ +│ ├── adapters/ +│ │ ├── auth-source.adapter.ts +│ │ └── auth-source.adapter.test.ts +│ ├── create-auth.ts +│ ├── create-auth.test.ts +│ └── index.ts +└── {framework-binding}/ + ├── auth.provider.tsx + ├── auth.provider.test.tsx + ├── use-auth.ts + └── use-auth.test.tsx +``` + +Это карта возможных тестов, а не обязательный scaffold. Файл создаётся только вместе с реальным поведением, которое требуется проверить. + +## Business tests + +### TST-N002: Factory-level tests являются главными тестами Domain behavior + +Business factory тестируется как black box через public API business-модуля: + +```ts +import { + authFactory, + AUTH_ERROR_CODES, + isAuthError, +} from '@/domains/auth/business' +``` + +Factory-level tests не зависят от React, Next.js, production SDK, real storage или production presets. Все runtime capabilities заменяются test ports, mocks, stubs или in-memory fakes. + +Обязательная матрица для public scenarios: + +- форма возвращаемого API; +- отсутствие side effects при вызове factory; +- happy path; +- input validation; +- нормализация результатов ports; +- nullable, empty и malformed results; +- rejected promise dependency; +- synchronous throw dependency; +- stable domain error code; +- отсутствие raw source error как consumer contract; +- порядок side effects; +- остановка следующих effects после failure; +- state transitions; +- repeated и concurrent calls, если они влияют на контракт; +- lifecycle operations и cleanup, если они входят в public business API. + +Если business behavior невозможно проверить без React, Vue, Next.js или concrete SDK, это сигнал о проникновении framework/runtime ответственности внутрь business. + +### TST-N003: Factory-level test использует per-test assembly + +Каждый test case создаёт factory с нужной именно ему конфигурацией ports: + +```ts +it('maps source failure to domain error', async () => { + const cause = new Error('Network failed') + const requestCode = vi.fn().mockRejectedValue(cause) + const { api } = createAuthTestHarness({ requestCode }) + + await expect(api.requestPhoneOtp(phone)).rejects.toMatchObject({ + code: AUTH_ERROR_CODES.PHONE_OTP_REQUEST_FAILED, + }) +}) +``` + +Другой test case создаёт независимую assembly: + +```ts +it('does not call source for invalid phone', async () => { + const requestCode = vi.fn() + const { api } = createAuthTestHarness({ requestCode }) + + await expect(api.requestPhoneOtp('123')).rejects.toMatchObject({ + code: AUTH_ERROR_CODES.PHONE_OTP_PHONE_INVALID, + }) + + expect(requestCode).not.toHaveBeenCalled() +}) +``` + +### TST-N004: Test harness не является preset + +Test harness является private test utility, которая уменьшает boilerplate и предоставляет observability: + +```ts +const { api, ports, state } = createAuthTestHarness(overrides) +``` + +Test harness: + +- private для конкретной test suite; +- не экспортируется production entrypoint; +- допускает произвольные scenario-specific overrides; +- создаёт новый API instance для каждого test case; +- не представляет устойчивую application environment; +- не имеет собственного production lifecycle; +- не размещается в `presets/`. + +Общий `test preset` по умолчанию не создаётся. Если Storybook, demo application или e2e environment получают устойчивую именованную конфигурацию, это отдельный application preset, а не универсальная конфигурация unit tests. + +Предварительное имя helper: + +```text +business/tests/factory/testing/create-auth-test-harness.ts +``` + +## Public API tests + +### TST-N005: Business public API проверяется отдельно + +Runtime public exports фиксируются тестом entrypoint: + +```ts +import * as authBusiness from '.' + +expect(Object.keys(authBusiness).sort()).toEqual([ + 'AUTH_ERROR_CODES', + 'authFactory', + 'isAuthError', + 'normalizeAuthPhone', + 'validateAuthPhone', +]) +``` + +Этот тест обнаруживает случайный runtime export, но не видит type-only exports. Полная проверка type surface должна выполняться будущим architecture lint или TypeScript API check. + +Форма API instance также фиксируется factory-level test: + +```ts +expect(Object.keys(authFactory(ports)).sort()).toEqual([ + 'requestPhoneOtp', + 'resendPhoneOtp', + 'signOut', + 'verifyPhoneOtp', +]) +``` + +## Pure domain functions + +### TST-N006: Pure functions тестируются рядом с реализацией + +```text +business/lib/auth-phone.ts +business/lib/auth-phone.test.ts +``` + +Проверяются: + +- canonical values; +- boundary values; +- malformed input; +- normalization; +- invariants; +- отсутствие mutation входа; +- детерминированность результата. + +```ts +describe('normalizeAuthPhone', () => { + it.each([ + ['8 (999) 111-22-33', '+79991112233'], + ['+7 999 111 22 33', '+79991112233'], + ['123', null], + ])('normalizes %s', (input, expected) => { + expect(normalizeAuthPhone(input)).toBe(expected) + }) +}) +``` + +Business scenario повторно применяет то же правило на своей границе. UI validation не заменяет business validation. + +## Internal tests + +### TST-N007: Colocated tests дополняют public contract tests + +Colocated tests оправданы для: + +- mappers и normalizers; +- runtime guards и parsers; +- private error implementation; +- сложного branching; +- race/concurrency algorithms; +- reusable internal pure functions. + +Отдельный test каждого service не требуется автоматически. Factory-level tests остаются главным доказательством, что внутренняя реализация подключена к public scenario правильно. + +Service test добавляется, если он существенно упрощает проверку сложного внутреннего алгоритма и не дублирует целиком factory-level matrix. + +## Domain errors + +### TST-N008: Consumer contract ошибки тестируется без public constructor + +Factory-level test проверяет observable contract: + +```ts +try { + await api.verifyPhoneOtp(data) +} catch (error) { + expect(isAuthError(error)).toBe(true) + + if (isAuthError(error)) { + expect(error.code).toBe( + AUTH_ERROR_CODES.PHONE_OTP_VERIFY_CODE_INVALID, + ) + } +} +``` + +Consumer-level test не использует private `AuthBusinessError` constructor и не зависит от `instanceof` internal class. + +Colocated test error implementation может отдельно проверить: + +- private constructor; +- `cause`; +- source code mapping; +- source metadata normalization; +- защиту от malformed error values. + +## Adapter tests + +### TST-N009: Adapter test проверяет port boundary, а не business behavior + +Adapter test размещается рядом с adapter и проверяет: + +- правильную concrete operation; +- transport payload; +- преобразование domain arguments в concrete arguments; +- raw/unknown result согласно port contract; +- проброс source error без создания domain error; +- subscription cleanup; +- отсутствие лишних SDK operations в минимальном client; +- environment boundary, если она проверяема build/lint средствами. + +Adapter test не повторяет domain error mapping, business fallback и scenario orchestration. + +Если несколько adapters реализуют один нетривиальный behavioral port contract, позднее можно выделить reusable contract test suite. Она остаётся test-only utility и не становится preset. + +## Preset tests + +### TST-N010: Production preset test проверяет assembly risk + +Preset test размещается рядом с production preset и проверяет: + +- выбор правильных adapters; +- передачу полного `Deps` в factory; +- exact narrowed API view, если preset его задаёт; +- отсутствие I/O при construction; +- отсутствие import-time subscriptions и storage reads; +- scope API instance; +- передачу lifecycle/dispose handles caller; +- изоляцию двух request-scoped instances; +- server/client import boundary. + +Preset test не повторяет happy path и error matrix business scenarios. Эти гарантии принадлежат factory-level tests. + +## Framework binding tests + +### TST-N011: Framework binding тестируется через fake business API + +Framework unit test по умолчанию получает fake API, а не собирает реальную factory: + +```tsx +const authApi = createAuthApiFake() + +render( + + + , +) +``` + +Проверяются: + +- Provider предоставляет переданный instance; +- access hook возвращает правильный API; +- использование без Provider даёт предсказуемую ошибку; +- изменение framework-neutral state вызывает framework update; +- subscriptions запускаются в правильной lifecycle phase; +- cleanup выполняется после unmount; +- Strict Mode не запускает construction side effects; +- server snapshot и hydration согласованы, если binding участвует в SSR. + +Отдельный smoke test с real factory и memory ports добавляется только при самостоятельном integration risk. Такой тест принадлежит framework module либо graph owner, который действительно собирает эту связку. + +## UI tests + +### TST-N012: Domain UI тестируется при наличии значимого поведения + +Компонент не требует test только потому, что он существует. Test оправдан, если Domain-owned UI: + +- содержит interaction; +- отображает несколько domain states; +- реагирует на domain error code; +- управляет focus или keyboard navigation; +- имеет значимый accessibility contract; +- использует framework lifecycle; +- содержит регрессионно опасную presentation logic. + +Проверяются observable behavior и accessibility semantics, а не внутренняя структура JSX/Vue template. + +Snapshot-only tests не являются обязательным доказательством. Визуальные различия при необходимости проверяются отдельным visual regression инструментом. + +Universal UI module тестируется в слое `ui`, а page/screen/composition UI тестируется у соответствующего composition owner. Наличие React/Vue само по себе не переносит ownership теста в Domain. + +## Graph и E2E tests + +### TST-N013: Cross-domain graph тестируется у graph owner + +Проверяются: + +- topological assembly order; +- передача собранных API в dependent factories; +- exact graph type; +- отсутствие повторной assembly без нужного scope; +- ownership instance; +- lifecycle start и cleanup; +- request/application/page isolation. + +Business modules не содержат tests полного application graph. + +### TST-N014: E2E дополняет, но не заменяет Domain tests + +E2E проверяет пользовательский поток через реальный application entry. Он не заменяет factory-level tests, потому что не способен дешёво и детерминированно перебрать malformed responses, synchronous throws, races и все domain error mappings. + +## Чего избегать + +### TST-N015: Test suite не повторяет одну ответственность на всех уровнях + +Не рекомендуется: + +- повторять одну scenario matrix в service, factory, preset и framework tests; +- тестировать business через production SDK; +- использовать общий mutable API instance между tests; +- экспортировать test harness из production public API; +- создавать `presets/testing` как default-механизм unit tests; +- проверять private implementation из factory-level tests; +- считать type-only файл требующим runtime unit test; +- использовать real network или process env в business tests. + +Минимальная правильная граница предпочтительнее большого количества дублирующих tests. diff --git a/DRAFT/index.md b/DRAFT/index.md new file mode 100644 index 0000000..55f5a05 --- /dev/null +++ b/DRAFT/index.md @@ -0,0 +1,33 @@ +--- +layout: home +title: SLM Level 1 + +hero: + name: SLM Level 1 + text: Базовая архитектура фронтенд-приложений + tagline: Слои, модули, зависимости, публичные границы и жизненный цикл без отдельной доменной архитектуры. + image: + src: /logo.svg + alt: SLM Design + actions: + - theme: brand + text: Читать Level 1 + link: /level-1/ + - theme: alt + text: Открыть правила + link: /rules/level-1 + +features: + - title: Пять слоёв + details: Линейный порядок app, compositions, infra, ui и shared задаёт роли кода и допустимое направление зависимостей. + - title: Модульные границы + details: Ответственность имеет одного владельца, а внешний код использует модуль только через его публичный API. + - title: 14 правил + details: Пять правил проверяются автоматически, девять требуют архитектурного ревью. +--- + +## Что опубликовано + +Сайт содержит только рабочий черновик SLM Level 1 и его канонический реестр правил. Доменные уровни, монорепозитории и дополнительные режимы архитектуры пока не входят в опубликованную документацию. + +Определения Level 1 нормативны внутри черновика. Точные формулировки блокирующих требований находятся только в [реестре правил](/rules/level-1). diff --git a/DRAFT/level-1/README.md b/DRAFT/level-1/README.md new file mode 100644 index 0000000..41365dd --- /dev/null +++ b/DRAFT/level-1/README.md @@ -0,0 +1,53 @@ +# SLM Level 1 + +> Статус: рабочий черновик. Документы в этой папке не являются спецификацией. + +Level 1 задаёт основу SLM для лёгких проектов, которым нужна понятная организация без отдельной доменной архитектуры. + +## Место в уровнях SLM + +| Уровень | Назначение | +|---|---| +| Level 1 | Слои, зависимости, структурные сущности и жизненный цикл ресурсов | +| Level 2 | Слой `domains` для модулей с доменной логикой | +| Level 3 | Строгие правила доменов для крупных и критичных проектов | + +Повышение уровня может требовать рефакторинга, но базовые понятия Level 1 сохраняются. + +## Область Level 1 + +Level 1 описывает слои, модули, группы, сегменты, компоненты, публичный API, граф зависимостей и владение жизненным циклом ресурсов. + +Level 1 не описывает домены, фабрики, порты, адаптеры, обязательный поток данных, монорепозитории, соглашения об именовании и файловый стайлгайд. + +Появление самостоятельной доменной логики является сигналом рассмотреть Level 2. + +## Виды утверждений + +- **Определение** нормативно задаёт смысл архитектурного термина, но не получает код правила. +- **Правило** задаёт блокирующий архитектурный инвариант и объявляется в каноническом реестре. +- **Рекомендация** помогает принять решение, но не делает архитектуру невалидной. +- **Пример** иллюстрирует модель и не задаёт обязательную структуру. + +Нормативные определения находятся в [терминологии](./terminology.md). Канонический набор правил: [правила SLM Level 1](../rules/level-1.md). Формат и требования к ним: [Правила SLM](../rules/). + +Остальные документы Level 1 объясняют и иллюстрируют модель, но не владеют точными формулировками определений и правил. + +## Основная идея + +Модуль является основной архитектурной единицей SLM. Слой задаёт роль и направление зависимостей. Группа помогает навигации, сегмент организует внутреннее содержимое, а компонент всегда принадлежит модулю. + +Level 1 требует отдельную папку и единый публичный API модуля. Имена этих элементов, внутренняя файловая форма модулей, сегментов и компонентов определяются стайлгайдом проекта. + +## Карта черновика + +- [Терминология](./terminology.md) +- [Слои](./layers.md) +- [Зависимости](./dependencies.md) +- [Модули](./modules.md) +- [Группы](./groups.md) +- [Сегменты](./segments.md) +- [Компоненты](./components.md) +- [Вложенные модули](./nested-modules.md) +- [Жизненный цикл](./lifecycle.md) +- [Проверка](./validation.md) diff --git a/DRAFT/level-1/components.md b/DRAFT/level-1/components.md new file mode 100644 index 0000000..94a4d56 --- /dev/null +++ b/DRAFT/level-1/components.md @@ -0,0 +1,65 @@ +# Компоненты Level 1 + +> Пояснение нормативной модели компонентов Level 1. + +Компонент является строительным элементом интерфейса, а не самостоятельной архитектурной единицей. + +## Связанные правила + +- [`SLM-L1-COMPONENT-R009`](../rules/level-1.md#slm-l1-component-r009) +- [`SLM-L1-LAYER-A002`](../rules/level-1.md#slm-l1-layer-a002) +- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004) +- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005) +- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011) +- [`SLM-L1-LIFECYCLE-R013`](../rules/level-1.md#slm-l1-lifecycle-r013) + +## Файловая форма + +Файловую форму компонента определяет стайлгайд. Компонент может быть одним файлом фреймворка или каталогом со вспомогательными файлами. + +```text +landing/ +└── ui/ + └── hero.tsx +``` + +```text +landing/ +└── ui/ + └── hero/ + ├── hero.tsx + ├── styles/ + │ └── hero.module.css + └── types/ + └── hero-props.type.ts +``` + +Наличие каталога, типов, стилей или локального `index.ts` не превращает компонент в модуль. + +## Реализация + +Компонент может отображать входные данные, вызывать переданные обработчики, условно строить интерфейс и хранить локальное состояние представления. + +Level 1 не вводит отдельного запрета на импорты, выполняемые кодом компонента. Каждый такой импорт считается зависимостью родительского модуля и должен соблюдать направление слоёв, публичные API и запрет циклов. + +Доступ к данным, состояние, контекст или код жизненного цикла внутри компонента сами по себе не создают новую архитектурную границу. Их источники, зависимости и область жизни определяет родительский модуль. + +Провайдер может технически реализовывать контекст и жизненный цикл фреймворка, но владельцем состояния и ресурсов остаётся родительский модуль. + +Файл в `app` может технически быть компонентом React или Vue. Архитектурно он является точкой входа фреймворка, а не компонентом SLM. + +## Компонент и модуль + +| Признак | Компонент | Модуль | +|---|---|---| +| Самостоятельная ответственность | Нет | Да | +| Собственный публичный API | Нет | Да | +| Собственная граница зависимостей | Нет | Да | +| Вспомогательные файлы | Может иметь | Может иметь | +| Сегменты и вложенные модули | Нет | Может иметь | + +Модуль может состоять всего из одного корневого компонента. Различие определяется владением, а не количеством файлов. + +## Когда нужен вложенный модуль + +Если часть интерфейса получает самостоятельную ответственность, публичный API, собственную границу зависимостей, область жизни или внутреннюю модульную декомпозицию, она является модулем. При локальном использовании такой модуль может размещаться как вложенный. diff --git a/DRAFT/level-1/dependencies.md b/DRAFT/level-1/dependencies.md new file mode 100644 index 0000000..4557e3a --- /dev/null +++ b/DRAFT/level-1/dependencies.md @@ -0,0 +1,49 @@ +# Зависимости Level 1 + +> Пояснение нормативной модели зависимостей Level 1. + +Слои задают допустимое направление связей, а модули образуют граф зависимостей. + +## Что считается зависимостью + +- Обычный импорт, импорт типа и реэкспорт одинаково создают архитектурную зависимость. +- Импорт между файлами одного модуля не пересекает модульную границу и не создаёт отдельный узел графа. +- Импорт любого внутреннего файла, сегмента или компонента считается зависимостью ближайшего модуля-владельца. +- Вложенный модуль является обычным самостоятельным узлом графа. +- Группы, сегменты и компоненты не являются самостоятельными узлами графа. + +Точка входа фреймворка не является модулем. Её импорты участвуют в проверке направления слоёв, но не образуют исходящий узел графа модулей. + +Ресурс `shared` также не является модулем. Его прямой импорт участвует в проверке направления слоёв, но не нарушает требование о публичном API модуля. + +## Направление + +- Модуль может импортировать модули своего или любого нижнего слоя. +- Модули одного слоя могут импортировать друг друга. +- Промежуточный слой не является обязательным посредником. + +Направление слоёв определено в [Слоях](./layers.md). + +## Связанные правила + +- [`SLM-L1-LAYER-A002`](../rules/level-1.md#slm-l1-layer-a002) +- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004) +- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005) +- [`SLM-L1-NESTED_MODULE-A010`](../rules/level-1.md#slm-l1-nested_module-a010) + +## Публичный API + +```ts +// Допустимо +import { Button } from '@/ui/button' + +// Недопустимо +import { Button } from '@/ui/button/button' +``` + +## Циклы + +```text +ui/modal → ui/button → ui/icon +ui/icon -/→ ui/modal +``` diff --git a/DRAFT/level-1/groups.md b/DRAFT/level-1/groups.md new file mode 100644 index 0000000..62059f8 --- /dev/null +++ b/DRAFT/level-1/groups.md @@ -0,0 +1,25 @@ +# Группы Level 1 + +> Пояснение нормативной модели групп Level 1. + +Группа помогает ориентироваться в большом количестве модулей. Она классифицирует структуру, но ничего не реализует и не образует узел графа зависимостей. + +## Связанное правило + +- [`SLM-L1-GROUP-R007`](../rules/level-1.md#slm-l1-group-r007) +- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011) + +## Пример + +```text +compositions/ +├── pages/ # Группа +│ ├── landing/ # Модуль +│ └── contacts/ # Модуль +└── layouts/ # Группа + └── main/ # Модуль +``` + +`pages` и `layouts` являются возможной группировкой проекта, а не обязательной структурой Level 1. + +Рекомендуется создавать группу только при реальной навигационной потребности. Если папка начинает владеть файлами реализации, состоянием, жизненным циклом или публичным API, она является модулем и должна получить модульную границу. diff --git a/DRAFT/level-1/layers.md b/DRAFT/level-1/layers.md new file mode 100644 index 0000000..234a520 --- /dev/null +++ b/DRAFT/level-1/layers.md @@ -0,0 +1,85 @@ +# Слои Level 1 + +> Пояснение нормативной модели слоёв Level 1. + +## Базовая структура + +```text +src/ +├── app/ +├── compositions/ +├── infra/ +├── ui/ +└── shared/ +``` + +`src/` здесь является примером SLM root. Фактическую границу определяет устройство приложения. + +Отсутствующая в проекте роль не требует пустой папки. Слой является доступной архитектурной ролью, а не обязательным элементом каркаса. + +## Роли слоёв + +### App + +`app` связывает приложение с фреймворком: запускает его, объявляет маршруты, преобразует входные данные и подключает публичные API нижних модулей или ресурсы `shared`. Файлы `app` являются точками входа фреймворка, а не модулями SLM. + +Точка входа может напрямую использовать `compositions`, `infra`, `ui` или `shared`, если зависимость разрешена общим порядком слоёв. Такое использование не переносит ответственность нижнего модуля в `app`. + +### Compositions + +`compositions` содержит продуктовый интерфейс: страницы, макеты, экраны, виджеты, точки входа и другие композиционные модули. Внутреннюю группировку слоя определяет проект. + +### Infra + +`infra` содержит технические сервисы приложения: аналитику, локализацию, тему, телеметрию и другие возможности среды выполнения без самостоятельной доменной модели. + +### UI + +`ui` содержит универсальные модули интерфейса, которые не зависят от конкретной страницы или продуктовой композиции. + +### Shared + +`shared` содержит независимый детерминированный фундамент без знания о продукте, изменяемого состояния и ввода-вывода. + +В `shared` допускаются обычные модули и специальные немодульные ресурсы: небольшие чистые утилиты, общие типы, стили, конфигурация и статические файлы. Ресурс не имеет самостоятельной ответственности, публичного API или жизненного цикла и может импортироваться напрямую по пути, установленному стайлгайдом. + +Если ресурсу нужны самостоятельная ответственность, собственные архитектурные зависимости, несколько файлов реализации, изменяемое состояние, ввод-вывод или область жизни, он оформляется как модуль. Каталог ресурсов не реэкспортирует модули и не используется для обхода их публичных API. + +## Порядок слоёв + +```text +app + ↓ +compositions + ↓ +infra + ↓ +ui + ↓ +shared +``` + +Код слоя может импортировать модули своего или любого нижнего слоя. Промежуточные слои можно пропускать. + +| Слой | Может импортировать нижние слои | +|---|---| +| `app` | `compositions`, `infra`, `ui`, `shared` | +| `compositions` | `infra`, `ui`, `shared` | +| `infra` | `ui`, `shared` | +| `ui` | `shared` | +| `shared` | Нет | + +Импорты внутри слоя, публичный API и циклы описаны отдельно в [Зависимостях](./dependencies.md). + +## Связанные правила + +- [`SLM-L1-LAYER-R001`](../rules/level-1.md#slm-l1-layer-r001) +- [`SLM-L1-LAYER-A002`](../rules/level-1.md#slm-l1-layer-a002) +- [`SLM-L1-LAYER-R003`](../rules/level-1.md#slm-l1-layer-r003) +- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011) + +## Граница Level 1 + +Разрешённый импорт не переносит владение. Например, `infra` может использовать `ui`, но продуктовый интерфейс по-прежнему принадлежит `compositions`. + +Самостоятельная доменная модель или сценарий являются сигналом рассмотреть Level 2, а не расширять ответственность `shared` или `infra`. diff --git a/DRAFT/level-1/lifecycle.md b/DRAFT/level-1/lifecycle.md new file mode 100644 index 0000000..343c788 --- /dev/null +++ b/DRAFT/level-1/lifecycle.md @@ -0,0 +1,31 @@ +# Жизненный цикл Level 1 + +> Пояснение нормативной модели владения ресурсами Level 1. + +Жизненный цикл является частью ответственности модуля. Файл, компонент, провайдер или точка входа фреймворка могут технически создавать и останавливать ресурс, но архитектурным владельцем остаётся модуль. + +## Связанные правила + +- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011) +- [`SLM-L1-LIFECYCLE-R013`](../rules/level-1.md#slm-l1-lifecycle-r013) + +## Граница ресурса + +Для ресурса определяются: + +- модуль-владелец; +- место создания; +- момент начала работы; +- область жизни; +- допустимое число экземпляров; +- способ остановки и очистки. + +Ресурс начинает работу не раньше начала своей области жизни и не остаётся активным после её завершения. Подписки, слушатели, таймеры, наблюдатели, запросы и соединения рассматриваются одинаково, если требуют явного завершения или отмены. + +## Реализация + +Очистку может выполнять сам модуль, компонент, провайдер или фреймворк. Способ реализации не меняет владельца и не переносит ответственность в технический файл. + +Одиночный экземпляр на всё приложение допустим только тогда, когда модуль действительно владеет областью жизни приложения или процесса. Размещение экземпляра на уровне файла само по себе этого не доказывает. + +Точка входа `app` может запускать или подключать ресурс через публичный API нижнего модуля, но не становится его владельцем. diff --git a/DRAFT/level-1/modules.md b/DRAFT/level-1/modules.md new file mode 100644 index 0000000..f5d2d4a --- /dev/null +++ b/DRAFT/level-1/modules.md @@ -0,0 +1,43 @@ +# Модули Level 1 + +> Пояснение нормативной модели модулей Level 1. + +Модуль является основной архитектурной единицей SLM. Он размещается в отдельной папке, но может состоять только из публичной точки входа и одного файла реализации. + +## Связанные правила + +- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004) +- [`SLM-L1-MODULE-A014`](../rules/level-1.md#slm-l1-module-a014) +- [`SLM-L1-MODULE-R006`](../rules/level-1.md#slm-l1-module-r006) +- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011) +- [`SLM-L1-MODULE-R012`](../rules/level-1.md#slm-l1-module-r012) + +## Владение + +Каждая самостоятельная ответственность имеет одного модуля-владельца. Модуль определяет её публичный API, зависимости, состояние, область жизни и внутреннее устройство независимо от того, в каком файле выполняется конкретный код. + +Точки входа `app` и нормативные ресурсы `shared` являются единственными немодульными исключениями. Остальной код внутри SLM root либо принадлежит существующему модулю, либо образует новый модуль. + +## Публичный API + +Модуль предоставляет один логический публичный API. Конкретное имя точки входа и механизм экспорта определяет стайлгайд проекта. + +Внешний код использует модуль только через публичный API. Сам API открывает только контракт, необходимый реальным внешним потребителям; внутренние механизмы, изменяемое состояние и детали жизненного цикла остаются закрытыми. + +## Внутреннее устройство + +Модуль может содержать корневые файлы, сегменты, компоненты и [вложенные модули](./nested-modules.md). Внутри своей границы он может использовать относительные импорты и не обязан обращаться к собственному публичному API; точную форму внутренних импортов определяет стайлгайд. + +SLM не требует полного каркаса или обязательного каталога сегментов. + +## Визуальный модуль + +Визуальный модуль обычно имеет корневой компонент, который экспортируется через публичный API. + +```text +button/ +├── button.tsx +└── index.ts +``` + +Корневой компонент остаётся компонентом, а владельцем ответственности является модуль `button`. diff --git a/DRAFT/level-1/nested-modules.md b/DRAFT/level-1/nested-modules.md new file mode 100644 index 0000000..ef0706a --- /dev/null +++ b/DRAFT/level-1/nested-modules.md @@ -0,0 +1,30 @@ +# Вложенные модули Level 1 + +> Пояснение нормативной модели вложенных модулей Level 1. + +Вложенный модуль является обычным модулем, размещённым внутри границы родительского модуля. Он имеет собственные ответственность, публичный API и узел графа зависимостей и подчиняется всем общим правилам модулей. + +## Связанные правила + +- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004) +- [`SLM-L1-MODULE-R006`](../rules/level-1.md#slm-l1-module-r006) +- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005) +- [`SLM-L1-NESTED_MODULE-A010`](../rules/level-1.md#slm-l1-nested_module-a010) + +## Пример + +```text +landing/ +├── landing.page.tsx +├── parts/ +│ └── hero/ +│ ├── hero.tsx +│ └── index.ts +└── index.ts +``` + +`parts/` здесь является примером сегмента, а не обязательным именем. + +Код родительского модуля использует вложенный модуль через его собственный публичный API. Код за пределами родительского модуля получает доступ только через публичный API родителя. + +Если вложенный модуль становится нужен за пределами родителя, рекомендуется перенести его в минимальную общую область без изменения внутренней формы. Доступ через API родителя при этом остаётся допустимым и сам по себе не требует переноса. diff --git a/DRAFT/level-1/segments.md b/DRAFT/level-1/segments.md new file mode 100644 index 0000000..0ab790a --- /dev/null +++ b/DRAFT/level-1/segments.md @@ -0,0 +1,29 @@ +# Сегменты Level 1 + +> Пояснение нормативной модели сегментов Level 1. + +Сегмент организует внутреннее содержимое модуля. Level 1 определяет роль сегмента, но не задаёт обязательный список имён. + +## Связанное правило + +- [`SLM-L1-SEGMENT-R008`](../rules/level-1.md#slm-l1-segment-r008) +- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004) + +## Файловая форма + +Названия, набор и содержимое сегментов определяет стайлгайд проекта. Сегмент может группировать файлы, компоненты или вложенные модули. + +Файлы и компоненты сегмента принадлежат родительскому модулю. Вложенный модуль внутри сегмента сохраняет собственные ответственность, публичный API и узел графа зависимостей. + +## Пример + +```text +landing/ # Модуль +└── ui/ # Сегмент модуля + └── hero/ # Каталог компонента + ├── hero.tsx + ├── styles/ # Вспомогательный каталог компонента + └── types/ # Вспомогательный каталог компонента +``` + +`styles/` и `types/` внутри каталога компонента не обязаны считаться сегментами SLM. Их форму определяет стайлгайд компонентов. diff --git a/DRAFT/level-1/terminology.md b/DRAFT/level-1/terminology.md new file mode 100644 index 0000000..d96a563 --- /dev/null +++ b/DRAFT/level-1/terminology.md @@ -0,0 +1,117 @@ +# Терминология Level 1 + +> Нормативные определения рабочего черновика. Этот раздел не объявляет правила. + +Определения Level 1 задают обязательный смысл архитектурных терминов и используются при толковании всех правил. Код получают только блокирующие требования, а не сами определения. + +## Базовые понятия + +### SLM root + +Граница структурной архитектуры одного приложения. Внутри неё определяются слои, модули и граф зависимостей Level 1. Монорепозиторий, пакеты и отношения между несколькими SLM root находятся за пределами Level 1. + +### Ответственность + +Связная часть приложения с одной причиной изменяться. Ответственность является самостоятельной, когда ей нужны собственные публичный API, зависимости, состояние или область жизни. + +### Владелец + +Модуль, который определяет публичный API ответственности, её зависимости, состояние, область жизни и внутреннее устройство. Место выполнения кода не переносит владение. + +### Публичный API + +Единая логическая точка внешнего доступа к модулю. Публичный API скрывает внутреннее устройство; конкретное имя файла и механизм экспорта определяет стайлгайд проекта. + +### Зависимость + +Статическая связь внутри одного SLM root, которую импорт или реэкспорт создаёт между архитектурными границами. Обычный импорт, импорт типа и реэкспорт одинаково создают архитектурную зависимость. + +Зависимость любого внутреннего файла, сегмента или компонента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу и становится отдельным узлом графа зависимостей. + +### Область жизни + +Период, в течение которого принадлежащие модулю состояние или долгоживущий ресурс должны оставаться активными. + +### Ресурс жизненного цикла + +Ресурс, работа которого продолжается после первоначального вызова и требует остановки, отмены, отписки или освобождения. Например, подписка, слушатель событий, таймер, наблюдатель, запрос или соединение. + +### Очистка + +Гарантированное прекращение работы ресурса не позже завершения его области жизни. Автоматическая очистка фреймворка считается очисткой владельца, если модуль устанавливает и контролирует соответствующую границу. + +## Структурные сущности + +### Слой + +Одна из пяти верхнеуровневых ролей внутри SLM root: + +| Слой | Роль | +|---|---| +| `app` | Связь приложения с фреймворком: запуск, маршруты и преобразование входных данных | +| `compositions` | Сборка продуктового интерфейса: страницы, макеты, экраны, виджеты и другие композиции | +| `infra` | Технические сервисы и возможности приложения | +| `ui` | Универсальные модули интерфейса без зависимости от конкретной продуктовой композиции | +| `shared` | Независимый детерминированный фундамент без знания о продукте, изменяемого состояния и ввода-вывода | + +Слои образуют линейный порядок `app → compositions → infra → ui → shared`. Нижним считается любой слой справа от исходного; промежуточный слой не является обязательным посредником. + +### Модуль + +Минимальная самостоятельная архитектурная единица Level 1. Модуль владеет одной связной ответственностью, размещается в отдельной папке и предоставляет публичный API. + +### Группа + +Навигационная папка для модулей и других групп. Группа не является владельцем ответственности, состояния, области жизни, публичного API или узла графа зависимостей. + +### Сегмент + +Внутренняя часть одного модуля, которая группирует его содержимое по назначению. Сегмент не является самостоятельным владельцем, публичным API или узлом графа зависимостей. + +### Компонент + +Сущность фреймворка, которая реализует часть интерфейса родительского модуля. Компонент не образует собственного владельца, публичного API или узла графа зависимостей. + +Импорты, состояние, доступ к данным и код жизненного цикла компонента принадлежат родительскому модулю. Их наличие само по себе не создаёт новый модуль; решающим признаком является самостоятельная ответственность. + +### Вложенный модуль + +Обычный модуль, размещённый внутри границы родительского модуля. Он имеет собственные ответственность, публичный API и узел графа зависимостей и подчиняется всем общим правилам модулей. + +Публичный API вложенного модуля доступен коду родительской границы. Для кода за пределами родительского модуля вложенный модуль остаётся внутренней реализацией родителя. + +### Точка входа фреймворка + +Специальная немодульная единица слоя `app`, которая непосредственно связывает приложение с фреймворком. Её импорты участвуют в проверке направления слоёв, но сама точка входа не является узлом графа модулей. + +### Ресурс shared + +Специальная немодульная единица слоя `shared`: небольшая детерминированная утилита, общий тип, стиль, конфигурация или статический ресурс без продуктового знания, изменяемого состояния, ввода-вывода, области жизни и собственного публичного API. + +Ресурс `shared` может импортироваться напрямую по пути, установленному стайлгайдом, и не является узлом графа модулей. Доступный по этому пути файл является всей единицей и не скрывает отдельное внутреннее устройство. + +Если ресурсу нужны самостоятельная ответственность, собственные архитектурные зависимости, несколько файлов реализации, изменяемое состояние, ввод-вывод или область жизни, он оформляется как модуль. + +## Структурная модель + +```text +SLM root +├── app +│ └── точка входа фреймворка +├── compositions | infra | ui +│ ├── группа +│ │ └── модуль +│ └── модуль +│ ├── корневые файлы +│ ├── сегмент +│ │ ├── файлы +│ │ ├── компоненты +│ │ └── вложенные модули +│ └── вложенный модуль +└── shared + ├── группа + ├── модуль + └── ресурс shared +``` + +Путь и имя папки сами по себе не определяют сущность. Её определяют ответственность, владелец и публичная граница. Физическое сопоставление путей с сущностями задаётся стайлгайдом или конфигурацией проверки проекта. diff --git a/DRAFT/level-1/validation.md b/DRAFT/level-1/validation.md new file mode 100644 index 0000000..85375b1 --- /dev/null +++ b/DRAFT/level-1/validation.md @@ -0,0 +1,23 @@ +# Проверка Level 1 + +> Граница автоматической проверки и архитектурного ревью Level 1. + +## Автоматическая проверка + +Проект, заявляющий соответствие Level 1, сопоставляет физические пути с SLM root, слоями, модулями, вложенными модулями, публичными точками входа, точками входа `app` и ресурсами `shared`. Такое сопоставление задаётся стайлгайдом или конфигурацией проверки и не изменяет нормативный смысл сущностей. + +Каждое правило класса `A` должно быть реализовано проверкой проекта и блокировать её при нарушении. Level 1 не навязывает конкретный инструмент. + +Скрипт `draft-rules.js` проверяет только целостность документов: формат и уникальность кодов, ссылки и наличие тематических упоминаний. Он не проверяет архитектуру приложения. + +Актуальный список правил скрипт получает из [канонического реестра](../rules/level-1.md). + +## Архитектурное ревью + +Правила класса `R` проверяются вручную. Статический анализ может обнаружить подозрительный код, но не способен окончательно определить: + +- ответственность и её владельца; +- соответствие кода роли слоя; +- необходимость экспортов публичного API; +- область жизни ресурса и достаточность очистки; +- наличие самостоятельной границы у компонента, группы или сегмента. diff --git a/DRAFT/rules/README.md b/DRAFT/rules/README.md new file mode 100644 index 0000000..d8d4a07 --- /dev/null +++ b/DRAFT/rules/README.md @@ -0,0 +1,117 @@ +# Правила SLM + +> Статус: системный черновик. Не является нормативной спецификацией. + +Эта директория является единственным местом объявления правил SLM. Остальные черновики объясняют архитектуру и ссылаются на канонические коды, но не повторяют формулировки правил. + +## Что считается правилом + +Правило задаёт один блокирующий архитектурный инвариант. + +Определения, рекомендации, разрешения, примеры и открытые вопросы не получают код правила. + +Нормативные определения объявляются в терминологии соответствующего уровня. Они обязательны для толкования правил, но нормативность определения сама по себе не превращает его в правило. + +## Код правила + +```text +SLM-L{level}-{group}-{class}{number} +``` + +| Часть | Значение | +|---|---| +| `SLM` | Принадлежность архитектуре SLM | +| `L{level}` | Уровень архитектуры | +| `group` | Раздел правил | +| `class` | Способ проверки: `A` или `R` | +| `number` | Трёхзначный номер внутри уровня | + +## Способы проверки + +### `A`: автоматическая проверка + +Всё правило можно однозначно проверить программно без понимания предметного смысла кода. Нарушение такого правила должно блокировать автоматическую проверку. + +### `R`: проверка на ревью + +Для окончательного решения требуется понимание ответственности, владения или смысла зависимости. Линтер может проверять отдельные признаки, но не заменяет решение на ревью. + +Одно правило не разделяется на автоматическую и ручную копии только из-за разных способов проверки. Если существенная часть инварианта требует смыслового решения, всё правило получает класс `R`. + +## Разделы правил + +| Код | Раздел | +|---|---| +| `LAYER` | Слои | +| `DEPENDENCY` | Зависимости | +| `MODULE` | Модули | +| `GROUP` | Группы | +| `SEGMENT` | Сегменты | +| `COMPONENT` | Компоненты | +| `NESTED_MODULE` | Вложенные модули | +| `LIFECYCLE` | Жизненный цикл | + +Код раздела записывается полным английским именем в `UPPER_SNAKE_CASE`. Новый код добавляется в таблицу до первого использования. + +## Формат записи + +```md +### SLM-L1-MODULE-A004 + +> **Публичный API модуля** +> +> Каждый модуль предоставляет единый публичный API; код за пределами модуля импортирует его содержимое только через этот API. +``` + +Код является заголовком третьего уровня и автоматически получает адрес для ссылки `#slm-l1-module-a004`. + +Название и описание входят в одну цитату. Название выделяется жирным и служит кратким именем правила. Описание полностью формулирует требование и занимает одну физическую строку. + +Ссылка из тематического черновика: + +```md +[`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004) +``` + +## Как формулировать правила + +1. Правило понятно без чтения тематической главы и опирается только на нормативные термины своего уровня. +2. Правило защищает один архитектурный инвариант. +3. Один инвариант получает один код независимо от числа участников и способов проверки. +4. Название является кратким и устойчивым именем правила. +5. Название обозначает предмет правила, а описание полностью формулирует требование. +6. Описание объясняет допустимую границу и то, что считается нарушением. +7. Описание раскрывает названный инвариант и не вводит второе независимое требование. +8. Описание использует нормативные определения и не пересказывает их без необходимости. +9. Название и описание используют человеческий язык и только необходимые архитектурные термины. +10. Обоснование, подробности, примеры и исключения размещаются в тематическом черновике, а не в описании. +11. Правило не создаётся отдельно с позиции владельца и потребителя, если обе формулировки защищают одну границу. +12. Перед добавлением правила реестр проверяется на дубли и противоречия. +13. Код присваивается после проверки правила на примерах и контрпримерах. + +## Нумерация + +1. Номер уникален внутри уровня независимо от раздела и способа проверки. +2. Номер не обозначает важность или порядок выполнения. +3. Удалённый номер не переиспользуется для другого правила. +4. При изменении способа проверки номер сохраняется, но меняется полный код. + +## Проверка качества + +Перед принятием правила нужно ответить «да»: + +- Понятно, о чём правило? +- Название кратко и однозначно называет правило? +- Понятно, что оно требует? +- Понятно, что является нарушением? +- Нельзя ли объединить его с существующим правилом? +- Не содержит ли оно рекомендацию или разрешение? +- Соответствует ли класс способу окончательной проверки? + +## Проверка документов + +Корневой скрипт `draft-rules.js` читает объявления только из этой директории, проверяет формат и уникальность кодов, валидирует ссылки из остальных черновиков и выводит правила разделами «Автоматические» и «Для ревью». Скрипт проверяет документы, а не архитектуру приложения. + +## Наборы правил + +- [Первый уровень](./level-1.md) diff --git a/DRAFT/rules/level-1.md b/DRAFT/rules/level-1.md new file mode 100644 index 0000000..397f17d --- /dev/null +++ b/DRAFT/rules/level-1.md @@ -0,0 +1,102 @@ +# Правила SLM первого уровня +Здесь собраны правила первого уровня. Это единственное место, где они формулируются; тематические черновики объясняют их и ссылаются на коды. + +## Размещение кода по слоям + +### SLM-L1-LAYER-R001 + +> **Назначение слоёв** +> +> Код внутри SLM root размещается в слое, нормативная роль которого соответствует ответственности этого кода. + +### SLM-L1-LAYER-A002 + +> **Направление зависимостей** +> +> Внутри одного SLM root код каждого слоя может зависеть только от кода этого же или любого нижнего слоя в порядке `app → compositions → infra → ui → shared`. + +### SLM-L1-LAYER-R003 + +> **Граница слоя `app`** +> +> В `app` размещаются только точки входа фреймворка для запуска, маршрутов, преобразования входных данных и подключения публичных API нижних модулей или ресурсов `shared`; ответственности нижних слоёв остаются за пределами `app`. + +## Границы модулей + +### SLM-L1-MODULE-A004 + +> **Публичный API модуля** +> +> Каждый модуль предоставляет единый публичный API; код за пределами модуля импортирует его содержимое только через этот API. + +### SLM-L1-MODULE-A014 + +> **Папка модуля** +> +> Каждый модуль размещается в отдельной папке; его публичный API и внутренняя реализация находятся внутри этой границы, а вложенные модули образуют собственные папки. + +### SLM-L1-MODULE-R006 + +> **Ответственность модуля** +> +> Одна модульная граница содержит код одной связной ответственности; части, которые изменяются по несвязанным причинам, размещаются в разных модулях. + +### SLM-L1-MODULE-R011 + +> **Владелец ответственности** +> +> Каждая самостоятельная ответственность и относящийся к ней код принадлежат ровно одному модулю; вне модульной границы допускаются только точки входа `app` и нормативные ресурсы `shared`. + +### SLM-L1-MODULE-R012 + +> **Состав публичного API** +> +> Публичный API модуля открывает только контракт, необходимый реальным внешним потребителям; детали реализации и изменяемые внутренние механизмы остаются закрытыми. + +## Зависимости между модулями + +### SLM-L1-DEPENDENCY-A005 + +> **Циклические зависимости** +> +> Граф зависимостей модулей внутри одного SLM root, включая вложенные модули, не содержит циклов. + +## Назначение групп + +### SLM-L1-GROUP-R007 + +> **Назначение группы** +> +> Группа содержит только модули и другие группы, не владеет файлами реализации, состоянием, жизненным циклом или публичным API и не импортируется внешним кодом. + +## Назначение сегментов + +### SLM-L1-SEGMENT-R008 + +> **Граница сегмента** +> +> Сегмент организует код только внутри одного модуля и не имеет собственной ответственности, публичного API или узла графа зависимостей. + +## Ответственность компонентов + +### SLM-L1-COMPONENT-R009 + +> **Ответственность компонента** +> +> Компонент реализует часть ответственности одного родительского модуля; все его зависимости, состояние и жизненный цикл принадлежат этому модулю и не образуют самостоятельную архитектурную границу. + +## Границы вложенных модулей + +### SLM-L1-NESTED_MODULE-A010 + +> **Доступ к вложенному модулю** +> +> Код за пределами родительского модуля не импортирует вложенный модуль напрямую и получает его экспорты только через публичный API родителя. + +## Жизненный цикл + +### SLM-L1-LIFECYCLE-R013 + +> **Жизненный цикл ресурсов** +> +> Для каждого ресурса жизненного цикла модуль-владелец определяет создание, область жизни, число экземпляров и очистку; ресурс активен только внутри своей области жизни. diff --git a/README.md b/README.md index 3683319..ce2181a 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,12 @@ ## Структура -- `docs/` — новый нормативный корпус и материалы сайта. -- `old-docs/` — действующая legacy-документация для текущего skill. -- `site/` — VitePress-конфигурация, тема и статические ресурсы. -- `src-skills/` — исходники agent skills. -- `skills/` — собранные skills для установки через `npx skills`. +- `DRAFT/` - рабочая документация Level 1 и источник содержимого сайта. +- `site/` - VitePress-конфигурация, тема и статические ресурсы. +- `docs/` и `docs-v3/` - архивные версии документации, не используемые сайтом. +- `old-docs/` - действующая legacy-документация для текущего skill. +- `src-skills/` - исходники agent skills. +- `skills/` - собранные skills для установки через `npx skills`. ## Сборка @@ -19,7 +20,7 @@ npm run build npm run check ``` -`npm run build` пересобирает текущий `skills/slm-design/` из `old-docs/` и `src-skills/slm-design/`. Не редактируй собранные файлы вручную. +`npm run build` пересобирает текущий `skills/slm-design/` из `old-docs/` и `src-skills/slm-design/`. `npm run check` дополнительно проверяет правила и собирает сайт из `DRAFT/`. Не редактируй собранные файлы вручную. ## Установка diff --git a/docs/en/index.md b/docs/en/index.md deleted file mode 100644 index 9b292be..0000000 --- a/docs/en/index.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -layout: home -title: SLM Design in English -titleTemplate: false -sidebar: false -hero: - name: English edition - text: Translation is planned - tagline: The Russian specification is currently the only normative source. The English edition will preserve its structure and rule IDs. - actions: - - theme: brand - text: Open Russian specification - link: /ru/specification/ - - theme: alt - text: Language selection - link: / -features: - - title: No partial translation - details: An incomplete English rule set is not published as normative documentation. - - title: Stable identifiers - details: Future translated requirements will use the same SLM rule IDs as the Russian source. - - title: Equal URL structure - details: English documentation is reserved under /en/ alongside the Russian /ru/ section. ---- diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 49efd43..0000000 --- a/docs/index.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -layout: home -title: SLM Design -titleTemplate: false -sidebar: false -hero: - name: SLM Design - text: Explicit architecture boundaries - tagline: A draft specification for ownership, dependencies, runtime, and lifecycle in product applications. - actions: - - theme: brand - text: Русская спецификация - link: /ru/ - - theme: alt - text: English - link: /en/ -features: - - title: Base SLM - details: A complete minimal architecture built around explicit ownership and five application layers. - - title: Independent overlays - details: Advanced and Pro add separate rule sets directly to base SLM without inheriting each other. - - title: Stable rules - details: Every normative requirement has a permanent rule ID suitable for reviews and automated checks. ---- diff --git a/docs/ru/index.md b/docs/ru/index.md deleted file mode 100644 index 35e4b08..0000000 --- a/docs/ru/index.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -layout: home -title: SLM Design -titleTemplate: false -sidebar: false -hero: - name: Документация SLM Design - text: Один портал для правил и практики - tagline: Нормативная спецификация, реестр правил и будущий архитектурный гайд в единой структуре. - actions: - - theme: brand - text: Открыть спецификацию - link: /ru/specification/ - - theme: alt - text: Найти правило - link: /ru/specification/rules -features: - - title: SLM Design Specification - details: Нормативный источник архитектурных правил, ownership boundaries и требований соответствия. - link: /ru/specification/ - linkText: Читать спецификацию - - title: Реестр правил - details: Все Base, Advanced и Pro rules с фильтрами, точными anchors и копируемыми permalink-ссылками. - link: /ru/specification/rules - linkText: Открыть реестр - - title: Architecture Guide - details: Будущий учебный материал для последовательного изучения и практического применения Specification. ---- diff --git a/docs/ru/specification/architecture-model.md b/docs/ru/specification/architecture-model.md deleted file mode 100644 index ca43821..0000000 --- a/docs/ru/specification/architecture-model.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: Архитектурная модель -status: draft -normative: true ---- - -# Архитектурная Модель - -## Структура приложения - -```text -src/ -├── app/ -├── compositions/ -├── infra/ -├── ui/ -└── shared/ -``` - -**SLM-BASE-ARCH-001 - ОБЯЗАН.** Base SLM-приложение должно разделять код по ответственности между слоями `app`, `compositions`, `infra`, `ui` и `shared`. - -Не каждый слой обязан содержать код в минимальном приложении. Пустые папки и speculative scaffolding не требуются. - -## Группы ответственности - -| Группа | Слои | Ответственность | -|---|---|---| -| Framework composition | `app`, `compositions` | Подключение к framework и сборка application flows | -| Product | Product owner; в base SLM - `compositions` | Product semantics, UI и flows владеющего module | -| Technical | `infra`, `ui` | Technical capabilities и универсальный UI | -| Foundation | `shared` | Детерминированный общий фундамент | - -## Верхнеуровневое направление - -```text -app -> compositions | shared -compositions -> compositions | infra | ui | shared -infra -> infra | shared -ui -> ui | shared -shared -/-> остальные SLM-слои -``` - -Framework APIs и external packages регулируются ответственностью импортирующего слоя и не показаны как SLM-слои. - -**SLM-BASE-ARCH-002 - ОБЯЗАН.** Верхнеуровневое направление зависимостей между base SLM-слоями должно соблюдаться для runtime imports и type imports, кроме явно описанных исключений. - -**SLM-BASE-ARCH-003 - ЗАПРЕЩЕНО.** Нижний слой не может импортировать `app` или `compositions`. - -**SLM-BASE-ARCH-004 - ЗАПРЕЩЕНО.** `infra`, `ui` и `shared` не могут владеть product wiring или выступать service locator для application modules. - -## Путь данных - -```text -app - -> product owner public API - -> infra public API - -> external source -``` - -**SLM-BASE-ARCH-005 - ОБЯЗАН.** Каждый переход product data должен сохранять ownership: framework связывает, product owner определяет semantics, а technical capability не присваивает себе product model. - -## Путь UI - -```text -app route - -> page/layout composition - -> product UI - -> universal UI - -> shared styles/resources -``` - -Product UI принадлежит product owner; в base SLM таким owner является composition. Универсальный product-agnostic UI принадлежит `ui`. diff --git a/docs/ru/specification/architecture-modes.md b/docs/ru/specification/architecture-modes.md deleted file mode 100644 index 4498e46..0000000 --- a/docs/ru/specification/architecture-modes.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: Архитектурные modes -status: draft -normative: true ---- - -# Архитектурные Modes - -SLM является самостоятельной базовой архитектурой. Architecture mode - опциональный независимый overlay, который добавляет или явно заменяет отдельные правила base SLM. - -```text -SLM Advanced = SLM + Advanced rules -SLM Pro = SLM + Pro rules -``` - -`SLM Advanced` и `SLM Pro` не наследуют друг друга. Совпадающее требование декларируется отдельно внутри каждого overlay и не создаёт общей mode-ветки. - -## Выбор архитектуры - -Приложение использует один из трёх вариантов: - -```text -SLM -SLM + Advanced -SLM + Pro -``` - -**SLM-BASE-MODE-001 - ОБЯЗАН.** Приложение должно зафиксировать использование base SLM и, при наличии, ровно одного overlay: `Advanced` или `Pro`. - -**SLM-BASE-MODE-002 - ЗАПРЕЩЕНО.** Одно приложение не может одновременно заявлять соответствие `SLM Advanced` и `SLM Pro`. - -**SLM-BASE-MODE-003 - ОБЯЗАН.** Выбранный overlay должен применяться ко всему приложению в пределах одной SLM application boundary. - -Выбор выполняет команда на стадии планирования. Сигналами могут быть количество product responsibilities, связанность modules, runtime state, client/server execution, lifecycle risks и количество команд разработки. Фиксированные числовые пороги не устанавливаются. - -| Вариант | Когда рассматривать | -|---|---| -| `SLM` | Product responsibilities удобно удерживать внутри compositions без дополнительного слоя | -| `SLM Advanced` | Нужны самостоятельные domains, но команда хочет свободно выбирать их внутреннюю структуру и связи | -| `SLM Pro` | Нужны изолированные domains, явные runtime contracts, adapters, lifecycle и усиленные checks | - -## Применимость правил - -Base-правило имеет идентификатор вида: - -```text -SLM-BASE-AREA-NNN -``` - -Mode-specific правила имеют идентификаторы: - -```text -SLM-ADV-AREA-NNN -SLM-PRO-AREA-NNN -``` - -**SLM-BASE-MODE-004 - ОБЯЗАН.** Base-правила SLM применяются при любом выбранном варианте архитектуры. Если overlay явно заменяет base rule только в определённом scope, исходное base-правило продолжает действовать за пределами этого scope. - -**SLM-BASE-MODE-005 - ОБЯЗАН.** Для `SLM Advanced` применяются только base-правила и правила из `modes/advanced`. - -**SLM-BASE-MODE-006 - ОБЯЗАН.** Для `SLM Pro` применяются только base-правила и правила из `modes/pro`. - -**SLM-BASE-MODE-007 - ЗАПРЕЩЕНО.** Правило другого overlay не может использоваться как обязательное требование, разрешение или исключение. - -**SLM-BASE-MODE-008 - ОБЯЗАН.** Mode-specific правило, заменяющее base-поведение, должно явно назвать заменяемый base rule ID или нормативный раздел и точный scope замены. - -## Независимые overlays - -### SLM Advanced - -[SLM Advanced](./modes/advanced/index.md) описывает полный Advanced-delta относительно base SLM. - -### SLM Pro - -[SLM Pro](./modes/pro/index.md) описывает полный Pro-delta относительно base SLM. - -## Изменение overlay - -**SLM-BASE-MODE-009 - МОЖЕТ.** Команда может подключить, заменить или удалить overlay при изменении требований к архитектуре. - -**SLM-BASE-MODE-010 - ОБЯЗАН.** После изменения конфигурации приложение может заявлять соответствие только после выполнения применимых base-правил с учётом scoped replacements и, при наличии, полного rule set выбранного overlay. diff --git a/docs/ru/specification/foundations.md b/docs/ru/specification/foundations.md deleted file mode 100644 index 1e59051..0000000 --- a/docs/ru/specification/foundations.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: Основные инварианты -status: draft -normative: true ---- - -# Основные Инварианты - -SLM Design организует frontend-приложение по владельцам ответственности. Архитектурная единица определяется не типом файла, а тем, кто владеет моделью, поведением, данными, runtime и lifecycle. - -## Ответственность до размещения - -**SLM-BASE-FND-001 - ОБЯЗАН.** Перед размещением кода необходимо определить его владельца, public boundary, runtime dependencies и lifecycle scope. - -**SLM-BASE-FND-002 - СЛЕДУЕТ.** Код следует размещать в минимальном scope, который полностью владеет его ответственностью. - -**SLM-BASE-FND-003 - ЗАПРЕЩЕНО.** Нельзя переносить код в общий слой или общий package только на основании предполагаемого будущего переиспользования. - -## Путь продуктовых данных - -Product data проходят через public boundary текущего владельца согласно [SLM-BASE-DATA-001](./state-and-data.md#product-gateway). Внешний сервис может оставаться физическим источником данных, но transport contract не становится product model автоматически. - -## Явные зависимости - -**SLM-BASE-FND-007 - ОБЯЗАН.** Runtime capabilities должны поступать владельцу поведения через разрешённые imports, явные arguments или contracts, а не через скрытый service locator или global mutable state. - -**SLM-BASE-FND-008 - ЗАПРЕЩЕНО.** Type cast, barrel, alias, dynamic import или helper в `shared` не могут использоваться для обхода применимой архитектурной границы. - -## Public API - -Межмодульное взаимодействие и deep imports регулируются [SLM-BASE-API-001 - SLM-BASE-API-005](./public-api-and-imports.md#общие-правила). - -## Scope и lifecycle - -Создание, scope, activation и cleanup применимых runtimes и resources определены в [Runtime и lifecycle](./runtime-and-lifecycle.md). - -## Overlays - -Base SLM не вводит дополнительные архитектурные слои и специализированные runtime contracts. Каждый overlay самостоятельно определяет свои добавления и замены base-правил. diff --git a/docs/ru/specification/index.md b/docs/ru/specification/index.md deleted file mode 100644 index b73d420..0000000 --- a/docs/ru/specification/index.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -title: SLM Design Specification -version: 0.1.0-draft -status: draft -normative: true ---- - -# SLM Design Specification - -Эта директория содержит единый нормативный корпус SLM Design 2.0. Base SLM является законченной минимальной архитектурой; дополнительные ограничения подключаются независимыми overlays `SLM Advanced` или `SLM Pro`. - -Пока статус равен `draft`, документы описывают проектируемую архитектуру и не заменяют действующую документацию в `old-docs/`. - -## Нормативный язык - -| Термин | Значение | -|---|---| -| `ОБЯЗАН` | Требование необходимо выполнить для соответствия спецификации | -| `ЗАПРЕЩЕНО` | Действие является нарушением спецификации | -| `СЛЕДУЕТ` | Рекомендуемое решение; отступление требует явного обоснования | -| `МОЖЕТ` | Допустимый, но необязательный вариант | - -Правила имеют стабильные идентификаторы. Base использует формат `SLM-BASE-AREA-NNN`, Advanced - `SLM-ADV-AREA-NNN`, Pro - `SLM-PRO-AREA-NNN`. Точное нормативное требование принадлежит только той главе, где объявлен его rule ID. - -Все объявления доступны в [реестре правил](./rules.md). Для прямого перехода можно открыть поиск `Ctrl/⌘ K` и ввести полный rule ID. - -## Architecture modes - -Base SLM не требует overlay. Если команда выбирает дополнительную архитектурную политику, она подключает ровно один независимый mode согласно [Архитектурным modes](./architecture-modes.md): - -```text -SLM Advanced = SLM + Advanced rules -SLM Pro = SLM + Pro rules -``` - -## Приоритет - -**SLM-BASE-DOC-001 - ОБЯЗАН.** При конфликте между главами спецификации и любым ненормативным материалом приоритет имеет спецификация. - -**SLM-BASE-DOC-002 - ЗАПРЕЩЕНО.** Ненормативный документ не может вводить новое обязательное правило, исключение или архитектурную границу. - -**SLM-BASE-DOC-003 - ОБЯЗАН.** Изменение принятого архитектурного правила должно вноситься в главу, которая владеет соответствующим rule ID. - -## Base SLM - -### Основы - -- [Основные инварианты](./foundations.md) -- [Терминология](./terminology.md) -- [Архитектурная модель](./architecture-model.md) - -### Слои - -- [Обзор слоёв](./layers/index.md) -- [App](./layers/app.md) -- [Compositions](./layers/compositions.md) -- [Infra](./layers/infra.md) -- [UI](./layers/ui.md) -- [Shared](./layers/shared.md) - -### Общие правила - -- [Модули и группы](./modules-and-groups.md) -- [Сегменты](./segments.md) -- [Public API и импорты](./public-api-and-imports.md) -- [State и data](./state-and-data.md) -- [Runtime и lifecycle](./runtime-and-lifecycle.md) -- [Тестирование и соответствие](./testing-and-conformance.md) -- [Монорепозитории](./monorepo.md) - -## Overlays - -### SLM Advanced - -- [Отличия Advanced от base SLM](./modes/advanced/index.md) -- [Domains в SLM Advanced](./modes/advanced/domains.md) - -### SLM Pro - -- [Отличия Pro от base SLM](./modes/pro/index.md) -- [Domains в SLM Pro](./modes/pro/domains/index.md) -- [Business](./modes/pro/domains/business.md) -- [Framework surface](./modes/pro/domains/framework.md) -- [Ports и adapters](./modes/pro/domains/ports-and-adapters.md) -- [Client и server assembly](./modes/pro/domains/client-and-server.md) -- [Cross-domain boundary](./modes/pro/domains/cross-domain-boundary.md) -- [Тестирование Pro domains](./modes/pro/domains/testing.md) - -## Область текущего draft - -Base SLM фиксирует ownership, пять основных слоёв, public boundaries, state и lifecycle. Текущие версии Advanced и Pro в первую очередь определяют собственные независимые модели слоя `domains`; будущие mode-specific правила могут относиться к любому разделу архитектуры. - -Точная форма React Providers, окончательная политика package extraction и единая модель query cache не фиксируются сверх явно объявленных инвариантов base или выбранного overlay. diff --git a/docs/ru/specification/layers/app.md b/docs/ru/specification/layers/app.md deleted file mode 100644 index 2e730a8..0000000 --- a/docs/ru/specification/layers/app.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: Слой App -status: draft -normative: true ---- - -# Слой App - -`app` является boundary между framework routing/runtime и SLM-модулями приложения. - -## Ответственность - -`app` может содержать: - -- route files; -- framework layout/error/loading/not-found entries; -- framework metadata и route parameters; -- bootstrap imports; -- подключение global styles/assets; -- framework-required middleware и handlers. - -## Правила - -**SLM-BASE-APP-001 - ОБЯЗАН.** Route entry должен оставаться тонким adapter, нормализующим framework input и делегирующим готовому composition module. - -```text -framework route - → composition entry -``` - -**SLM-BASE-APP-002 - ЗАПРЕЩЕНО.** `app` не может владеть product page, screen, widget, product scenario, store или application wiring. - -**SLM-BASE-APP-003 - ЗАПРЕЩЕНО.** Route entry не должен напрямую собирать product integrations, вызывать SDK или формировать product model. - -**SLM-BASE-APP-004 - ОБЯЗАН.** Framework-specific input должен быть считан в `app` и передан вниз в минимальной нормализованной форме. - -Механическая нормализация включает извлечение route params, headers и framework wrappers. Product validation, создание value objects и выбор product outcome остаются у владельца product semantics. - -Запрет другим SLM-слоям импортировать `app` определяется base-правилом `SLM-BASE-ARCH-003`. - -**SLM-BASE-APP-006 - СЛЕДУЕТ.** Framework behavior, которому нужны product dependencies или product UI, следует реализовать готовым composition entry и только подключить из `app`. - -**SLM-BASE-APP-007 - МОЖЕТ.** `app` может напрямую импортировать framework APIs и static/global resources из `shared`, если framework требует подключить их в root entry. - -## Допустимая структура - -Структуру `app` определяет framework. SLM не требует превращать framework directories в SLM modules и не требует `index.ts` для route folders. - -```text -app/ -├── layout.tsx -├── error.tsx -├── not-found.tsx -├── api/ -└── products/ - └── [product]/ - └── page.tsx -``` - -## Примеры нарушений - -Следующие сущности являются примерами нарушений `SLM-BASE-APP-002` и `SLM-BASE-APP-003`: - -- `ProductPage`; -- product Provider; -- application service creator; -- page-local store; -- product mapper; -- reusable product component; -- concrete product integration. diff --git a/docs/ru/specification/layers/compositions.md b/docs/ru/specification/layers/compositions.md deleted file mode 100644 index b3b40f2..0000000 --- a/docs/ru/specification/layers/compositions.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: Слой Compositions -status: draft -normative: true ---- - -# Слой Compositions - -`compositions` собирает application flows из module public APIs, technical capabilities и UI modules и может владеть product logic в пределах своей ответственности. - -## Ответственность - -Composition может быть: - -- page; -- route composition entry; -- layout; -- screen; -- widget; -- provider composition; -- multi-module hook; -- non-visual application wiring owner. - -Структура слоя свободна и отражает продуктовую навигацию приложения. - -```text -compositions/ -├── pages/ -├── layouts/ -├── screens/ -├── widgets/ -└── providers/ -``` - -Эти папки являются groups, а не отдельными слоями. - -## Product ownership - -**SLM-BASE-CMP-001 - ОБЯЗАН.** Product flow и его локальная product logic должны принадлежать минимальной composition, охватывающей всех consumers этой ответственности. - -Composition может использовать public API `infra` для external operations, сохраняя product mapping, outcomes и fallback semantics у себя. - -## Public boundaries - -**SLM-BASE-CMP-005 - ЗАПРЕЩЕНО.** Composition не может импортировать private services, integrations, stores, Context или другие internal paths используемого module. - -## Product UI - -**SLM-BASE-CMP-006 - ОБЯЗАН.** UI, объединяющий несколько самостоятельных modules, route/page scope либо application flow, принадлежит `compositions`. - -Примеры: - -- application header; -- order flow, объединяющий несколько product responsibilities; -- page screen; -- route guard с navigation outcome; -- widget, использующий public APIs двух самостоятельных modules. - -**SLM-BASE-CMP-007 - МОЖЕТ.** Composition может использовать product UI, опубликованный другими modules, и universal UI, передавая props, callbacks и slots. - -## State - -**SLM-BASE-CMP-008 - ОБЯЗАН.** Page-local presentation state принадлежит минимальной composition, охватывающей всех его consumers. - -Примеры page-local state: - -- открытие sidebar; -- активная вкладка; -- route-local wizard step; -- presentation filters; -- состояние раскрытия section. - -**SLM-BASE-CMP-009 - ЗАПРЕЩЕНО.** Page store не может становиться параллельным владельцем product model или canonical product cache другого owner. - -## Imports - -**SLM-BASE-CMP-010 - МОЖЕТ.** Composition module может импортировать public API других composition modules, infra, ui и shared. - -Runtime-циклы между composition modules запрещены base-правилом `SLM-BASE-API-016`. - -**SLM-BASE-CMP-015 - ОБЯЗАН.** Client и server composition entries должны иметь раздельные public entrypoints и environment markers, если composition участвует в обоих runtime graphs. - -## Scope - -Composition может владеть application, route, page, request или test scope. Выбор scope должен следовать правилам [runtime и lifecycle](../runtime-and-lifecycle.md). diff --git a/docs/ru/specification/layers/index.md b/docs/ru/specification/layers/index.md deleted file mode 100644 index c683c14..0000000 --- a/docs/ru/specification/layers/index.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: Слои -status: draft -normative: true ---- - -# Слои - -Слой определяет вид ответственности, допустимые зависимости и типы modules внутри верхнеуровневой папки `src`. - -## Матрица ответственности - -| Слой | Владеет | Не владеет | -|---|---|---| -| [`app`](./app.md) | Framework routes, bootstrap, глобальные framework boundaries | Product UI, product logic, page state, application wiring | -| [`compositions`](./compositions.md) | Pages, layouts, screens, widgets, product flows, application wiring и scope | Universal UI primitives, technical transports | -| [`infra`](./infra.md) | Technical services, transports, platform integrations | Product semantics и application wiring | -| [`ui`](./ui.md) | Product-agnostic UI modules | Product scenarios и data sources | -| [`shared`](./shared.md) | Детерминированные общие resources | Runtime state, I/O и product knowledge | - -## Общие правила - -**SLM-BASE-LAY-001 - ОБЯЗАН.** Module должен располагаться в слое, который владеет его основной ответственностью. - -**SLM-BASE-LAY-002 - ЗАПРЕЩЕНО.** Нельзя выбирать слой по техническому типу файла без определения владельца поведения и данных. - -**SLM-BASE-LAY-003 - ОБЯЗАН.** Межслойный import должен одновременно соответствовать общей dependency direction и public API импортируемого module. - -**SLM-BASE-LAY-004 - ЗАПРЕЩЕНО.** Нельзя создавать proxy module в разрешённом слое только для обхода запрещённого направления import. - -**SLM-BASE-LAY-005 - СЛЕДУЕТ.** При смешанной ответственности module следует разделить по реальным владельцам. Application flow и UI нескольких самостоятельных modules следует собирать в `compositions`. - -## Выбор слоя - -| Вопрос | Слой | -|---|---| -| Код существует только из-за framework route/bootstrap? | `app` | -| Код собирает page, route или несколько самостоятельных modules? | `compositions` | -| Код выражает product flow или product responsibility без owner, введённого overlay? | `compositions` | -| Код предоставляет technical capability приложения? | `infra` | -| Компонент не содержит product semantics и scenario? | `ui` | -| Код детерминирован, не знает продукт и не имеет runtime state? | `shared` | - -Overlay может добавлять собственный слой и изменять ownership только в явно объявленном delta. diff --git a/docs/ru/specification/layers/infra.md b/docs/ru/specification/layers/infra.md deleted file mode 100644 index b86cf58..0000000 --- a/docs/ru/specification/layers/infra.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: Слой Infra -status: draft -normative: true ---- - -# Слой Infra - -`infra` содержит technical capabilities приложения, не определяющие product model и scenarios. - -## Примеры modules - -```text -infra/ -├── http/ -├── backend-api/ -├── realtime/ -├── analytics/ -├── logger/ -├── app-config/ -├── storage/ -├── i18n/ -└── theme/ -``` - -## Правила - -**SLM-BASE-INF-001 - ОБЯЗАН.** Infra module должен описывать technical capability, а не product semantics или scenario. - -**SLM-BASE-INF-002 - МОЖЕТ.** Infra module может импортировать public API другого infra module и `shared`. - -Запрет infra импортировать `compositions` или `app` определяется base-правилом `SLM-BASE-ARCH-003`. - -**SLM-BASE-INF-004 - ЗАПРЕЩЕНО.** Infra не может владеть product wiring, собирать application graph или предоставлять generic product service locator. - -**SLM-BASE-INF-005 - ЗАПРЕЩЕНО.** Infra не создаёт product errors, product fallback и product model из transport DTO. - -**SLM-BASE-INF-006 - МОЖЕТ.** Infra может экспортировать technical client, transport, event source, storage primitive или platform wrapper через собственный public API. - -**SLM-BASE-INF-007 - ОБЯЗАН.** Generated SDK и transport details должны оставаться внутри technical или private integration boundary владельца и не становиться частью public product contract. - -## Product integration - -Infra знает technical mechanism: - -```text -HTTP client -WebSocket transport -local storage primitive -analytics SDK -``` - -Product owner определяет semantics использования capability; infra предоставляет механизм через public API. Один infra module может использоваться несколькими product owners без знания их semantics. diff --git a/docs/ru/specification/layers/shared.md b/docs/ru/specification/layers/shared.md deleted file mode 100644 index f55e5d9..0000000 --- a/docs/ru/specification/layers/shared.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: Слой Shared -status: draft -normative: true ---- - -# Слой Shared - -`shared` является детерминированным фундаментом приложения и не знает о SLM-модулях верхних слоёв. - -## Допустимое содержимое - -- pure utilities; -- value predicates; -- product-agnostic types; -- styling foundation и tokens; -- static resources; -- compile-time constants без product ownership; -- deterministic formatting primitives. - -## Правила - -**SLM-BASE-SHR-001 - ОБЯЗАН.** Результат shared utility должен определяться явными аргументами и не зависеть от скрытого runtime environment. - -**SLM-BASE-SHR-002 - ЗАПРЕЩЕНО.** `shared` не может импортировать `app`, `compositions`, `infra` или `ui`. - -**SLM-BASE-SHR-003 - ЗАПРЕЩЕНО.** `shared` не может владеть product types, product rules, runtime state, I/O, storage access или event subscriptions. - -**SLM-BASE-SHR-004 - ЗАПРЕЩЕНО.** Нельзя переносить product helper, DTO, integration contract или product config в `shared` для обхода import boundary. - -**SLM-BASE-SHR-005 - СЛЕДУЕТ.** Код следует поднимать в `shared` только при подтверждённой product-agnostic semantics, а не из-за повторения нескольких строк. - -## Отличие от других слоёв - -| Код | Владелец | -|---|---| -| Email validator с product rules | Владеющий product module | -| Generic string trim utility | `shared` | -| Browser storage wrapper | `infra` | -| Product storage integration | Product owner; storage primitive - `infra` | -| UI spacing tokens | `shared` | -| Button consuming spacing tokens | `ui` | diff --git a/docs/ru/specification/layers/ui.md b/docs/ru/specification/layers/ui.md deleted file mode 100644 index a358907..0000000 --- a/docs/ru/specification/layers/ui.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: Слой UI -status: draft -normative: true ---- - -# Слой UI - -`ui` содержит reusable presentation modules без product scenario и product ownership. - -## Примеры - -```text -ui/ -├── button/ -├── input/ -├── icon/ -├── modal/ -├── carousel/ -├── tabs/ -└── tooltip/ -``` - -## Правила - -**SLM-BASE-UI-001 - ОБЯЗАН.** UI module должен быть применим без product-specific knowledge. - -**SLM-BASE-UI-002 - ЗАПРЕЩЕНО.** UI module не может импортировать `compositions`, `app` или product-specific infra. - -**SLM-BASE-UI-003 - МОЖЕТ.** UI module может импортировать public API других UI modules и `shared`. - -**SLM-BASE-UI-004 - ЗАПРЕЩЕНО.** UI module не выбирает product data source, не вызывает product scenario и не владеет multi-module behavior. - -**SLM-BASE-UI-005 - МОЖЕТ.** UI module может владеть локальным interaction state, необходимым только для собственной presentation mechanics. - -**SLM-BASE-UI-006 - ОБЯЗАН.** Компонент с product semantics должен принадлежать владеющему product module, а не `ui`. - -## Классификация - -| Сущность | Владелец | -|---|---| -| `Button`, `Input`, `Modal` | `ui` | -| `LoginForm` одной auth responsibility | Владеющий product module | -| Application header | `compositions` | -| Generic date picker | `ui` | -| Medication schedule | Владеющий product module согласно ownership | - -Универсальность определяется отсутствием product knowledge, а не количеством текущих consumers. diff --git a/docs/ru/specification/modes/advanced/domains.md b/docs/ru/specification/modes/advanced/domains.md deleted file mode 100644 index 8c826c3..0000000 --- a/docs/ru/specification/modes/advanced/domains.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -title: Domains в SLM Advanced -status: draft -normative: true -overlay: advanced -base: slm ---- - -# Domains в SLM Advanced - -> Overlay: `SLM Advanced`. Base: [SLM](../../index.md). - -Domain является законченным вертикальным product module с одной предметной ответственностью и явным public boundary. Кроме base-правил modules и segments, Advanced не предписывает обязательную внутреннюю архитектуру domain. - -## Domain и group - -**SLM-ADV-DOM-001 - ОБЯЗАН.** Конечный domain должен располагаться непосредственно в `domains` или внутри одной или нескольких навигационных groups. - -```text -domains/{domain} -domains/{group}/{domain} -domains/{group}/{nested-group}/{domain} -``` - -**SLM-ADV-DOM-002 - ОБЯЗАН.** Узел domain tree с собственным public API, state, integration или runtime должен классифицироваться как конечный domain, а не domain group. - -```text -domains/ -├── navigation/ # domain -└── knv/ # group - ├── auth/ # domain - ├── user/ # domain - └── orders/ # domain -``` - -**SLM-ADV-DOM-003 - ОБЯЗАН.** Первой архитектурной единицей в group tree является конечная папка, владеющая самостоятельной product responsibility. - -## Ownership - -**SLM-ADV-DOM-004 - ОБЯЗАН.** Domain должен владеть одной сформулированной product responsibility и предоставлять её внешним consumers через собственный public API. - -Domain может владеть: - -- product model и value objects; -- scenarios и operations; -- domain state и transitions; -- normalization и product errors; -- product source integration; -- framework hooks и UI одного domain; -- runtime-specific setup. - -**SLM-ADV-DOM-005 - ЗАПРЕЩЕНО.** Domain не может владеть framework route entry, page/layout composition, UI нескольких самостоятельных product responsibilities, universal technical capability или product-agnostic UI primitive. - -## Структура - -```text -domains/knv/auth/ -├── hooks/ -├── providers/ -├── services/ -├── stores/ -├── mappers/ -├── types/ -├── ui/ -├── parts/ -└── index.ts -``` - -Это пример, а не обязательный scaffold. Небольшой domain может состоять из одного файла и public entrypoint. - -Domain может хранить файлы в корне и использовать любые необходимые segments согласно base-правилам [SLM-BASE-SEG-001 - SLM-BASE-SEG-003](../../segments.md#правила). - -**SLM-ADV-DOM-006 - ЗАПРЕЩЕНО.** Нельзя создавать пустые segments или копировать полную структуру другого domain без текущей ответственности. - -**SLM-ADV-DOM-007 - МОЖЕТ.** Domain может владеть hooks, Providers, Context, services, stores, mappers, types, product UI и другими implementation units своей ответственности. - -## Public API - -Public boundary Advanced domain следует base-правилам `SLM-BASE-API-001` и `SLM-BASE-API-002`. - -**SLM-ADV-DOM-009 - МОЖЕТ.** Public API domain может экспортировать выбранные командой hooks, Providers, Context, components, service APIs, store access APIs и types как стабильный contract. - -**SLM-ADV-DOM-010 - ЗАПРЕЩЕНО.** Если product responsibility получила domain owner, app, composition или infra не могут создавать параллельную модель этой ответственности либо обходить её public boundary. - -## Dependencies - -```text -composition -> domain -domain -> domain | infra | ui | shared -``` - -**SLM-ADV-DOM-011 - МОЖЕТ.** Domain может runtime-импортировать public API другого Advanced domain. - -**SLM-ADV-DOM-012 - МОЖЕТ.** Domain может напрямую использовать public API `infra`, `ui` и `shared` без обязательной промежуточной abstraction. - -Runtime cycles запрещены base-правилом `SLM-BASE-API-016`. - -**SLM-ADV-DOM-013 - ЗАПРЕЩЕНО.** Type-only dependency cycle между domains запрещён, даже если runtime graph остаётся ацикличным. - -## Data flow - -```text -composition - -> domain public API - -> domain hook/service - -> infra - -> external source -``` - -**SLM-ADV-DOM-014 - ОБЯЗАН.** Product consumers за пределами domain должны получать его данные и поведение через public API domain, а не повторять тот же integration flow напрямую через `infra`. - -## Product UI - -**SLM-ADV-DOM-015 - МОЖЕТ.** Product UI одной domain responsibility может принадлежать этому domain. - -UI нескольких самостоятельных responsibilities остаётся в `compositions` согласно base-правилу `SLM-BASE-CMP-006`. - -## Monorepo boundary - -**SLM-ADV-DOM-016 - ОБЯЗАН.** Advanced domain должен оставаться внутри `apps/{app}/src/domains` до принятия отдельной package-модели. - -**SLM-ADV-DOM-017 - ЗАПРЕЩЕНО.** Workspace package не может называться Advanced Domain для целей Specification, если он не соответствует application path и ownership этой главы. diff --git a/docs/ru/specification/modes/advanced/index.md b/docs/ru/specification/modes/advanced/index.md deleted file mode 100644 index 8a7964d..0000000 --- a/docs/ru/specification/modes/advanced/index.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: SLM Advanced -status: draft -normative: true -overlay: advanced -base: slm ---- - -# SLM Advanced - -`SLM Advanced` является независимым overlay непосредственно над [base SLM](../../index.md). - -```text -SLM Advanced = SLM + Advanced rules -``` - -## Отличия от SLM - -| Область | Base SLM | SLM Advanced | -|---|---|---| -| Product ownership | Product logic принадлежит compositions | Устойчивая product responsibility может быть извлечена в domain | -| Слои | `app`, `compositions`, `infra`, `ui`, `shared` | Добавляется `domains` | -| Структура domain | Отсутствует | Свободная, внутренние роли выбирает команда | -| Domain dependencies | Отсутствуют | Ацикличные imports через public API разрешены | -| External integration | Composition использует infra | Domain может использовать infra напрямую | - -## Расширение архитектурной модели - -**SLM-ADV-ARCH-001 - ОБЯЗАН.** SLM Advanced должен расширять набор base-слоёв слоем `domains` для самостоятельных product responsibilities. - -```text -src/ -├── app/ -├── compositions/ -├── domains/ -├── infra/ -├── ui/ -└── shared/ -``` - -**SLM-ADV-ARCH-002 - ОБЯЗАН.** Дополнительные dependency edges Advanced должны соответствовать следующему направлению: - -```text -compositions -> domains -domains -> domains | infra | ui | shared -``` - -Base dependency direction для остальных слоёв сохраняется. - -## Изменение product ownership - -**SLM-ADV-CMP-001 - ОБЯЗАН.** Если product responsibility получила domain owner, Advanced заменяет для этой ответственности base-правило `SLM-BASE-CMP-001`: domain владеет собственной product logic, а composition владеет application flow и связывает public APIs. - -Product logic без domain owner продолжает следовать base SLM и принадлежит минимальной composition. - -**SLM-ADV-CMP-010 - МОЖЕТ.** Composition module может импортировать public API Advanced domains в дополнение к imports, разрешённым base-правилом `SLM-BASE-CMP-010`. - -## Advanced Domain Specification - -Полная Advanced-модель слоя описана в [Domains](./domains.md). Других mode-specific отличий текущий draft Advanced не вводит. diff --git a/docs/ru/specification/modes/pro/domains/business.md b/docs/ru/specification/modes/pro/domains/business.md deleted file mode 100644 index 3bbbc46..0000000 --- a/docs/ru/specification/modes/pro/domains/business.md +++ /dev/null @@ -1,130 +0,0 @@ ---- -title: Business в SLM Pro -status: draft -normative: true -overlay: pro -base: slm ---- - -# Business - -> Overlay: `SLM Pro`. - -`business` является framework-neutral зоной domain и единственным владельцем его продуктовой semantics. - -## Структура - -```text -domains/{group...}/{domain}/business/ -├── {domain}.factory.ts -├── index.ts -├── types/ -├── ports/ -├── services/ -├── errors/ -├── mappers/ -├── selectors/ -├── validators/ -└── lib/ -``` - -Конкретный набор внутренних segments определяется размером domain. Обязательны роль factory и public boundary, но не каждая папка из примера. - -## Factory boundary - -**SLM-PRO-BUS-001 - ОБЯЗАН.** Business должен создавать public runtime API через factory `{domain}Factory`. - -**SLM-PRO-BUS-002 - ОБЯЗАН.** Factory должна принимать все runtime capabilities через business-owned dependency contracts. - -**SLM-PRO-BUS-003 - ОБЯЗАН.** Factory должна возвращать framework-neutral DomainRuntime. Stateless logic API считается DomainRuntime и соблюдает тот же public boundary. - -**SLM-PRO-BUS-004 - ЗАПРЕЩЕНО.** Factory не может возвращать React hooks, components, Providers, layouts, route guards или framework boundaries. - -**SLM-PRO-BUS-005 - ЗАПРЕЩЕНО.** Factory constructor не может выполнять I/O, открывать socket, регистрировать subscription, запускать timer или читать hidden environment. - -## Public API - -**SLM-PRO-BUS-006 - ОБЯЗАН.** `business/index.ts` должен экспортировать единственное runtime value: factory. - -**SLM-PRO-BUS-007 - МОЖЕТ.** `business/index.ts` может экспортировать business-owned types через `export type`. - -```ts -export { authFactory } from './auth.factory' - -export type { - AuthDeps, - AuthFactory, - AuthRuntime, - AuthState, -} from './types' -``` - -**SLM-PRO-BUS-008 - ЗАПРЕЩЕНО.** Error classes, error guards, error code constants, selectors, validators, formatters, services, mappers и port implementations не экспортируются как отдельные runtime values. - -Если внешнему consumer нужна такая capability, она должна быть осмысленной частью factory runtime API, а не обходным direct export. - -## Runtime API - -DomainRuntime может предоставлять: - -- commands; -- imperative queries; -- snapshots; -- subscriptions; -- selectors через стабильные methods; -- validation operations; -- typed outcomes; -- explicit lifecycle operations. - -**SLM-PRO-BUS-009 - ОБЯЗАН.** Runtime API должен говорить на языке domain и не повторять endpoint names, SDK tree или storage schema. - -**SLM-PRO-BUS-010 - ЗАПРЕЩЕНО.** Public contract не может раскрывать generated DTO, SDK client, query-library result, concrete store API, raw Context или adapter. - -## Dependencies и ports - -**SLM-PRO-BUS-011 - ОБЯЗАН.** Business-owned dependency описывает минимальную внешнюю возможность на языке domain. - -```ts -export type AuthPhonePort = { - requestCode: (phone: string) => Promise - verifyCode: (input: VerifyPhoneCodeInput) => Promise -} -``` - -**SLM-PRO-BUS-012 - ОБЯЗАН.** Ненадёжный внешний результат должен приниматься как `unknown`, если business обязан проверить его runtime-форму. - -**SLM-PRO-BUS-013 - ЗАПРЕЩЕНО.** Business dependency не может быть generated DTO, полный SDK client, `StoreApi`, QueryClient или framework hook. - -**SLM-PRO-BUS-014 - ОБЯЗАН.** Subscription port должен предоставлять cleanup contract. - -## Imports - -Business может runtime-импортировать: - -- собственные файлы; -- детерминированный `shared`; -- pure libraries без I/O, hidden state и public type leakage. - -Business может type-only импортировать стабильный public contract другого domain, если dependency невозможно корректно описать локальным port. Локальный consumer-owned port является предпочтительным вариантом. - -**SLM-PRO-BUS-015 - ЗАПРЕЩЕНО.** Business не импортирует React, query runtime, state manager, SDK, generated operation, HTTP client, storage implementation, browser API, infra, composition или assembly. - -Cross-domain imports дополнительно регулируются правилами `SLM-PRO-XDOM-*` в [Cross-domain boundary](./cross-domain-boundary.md). - -## Normalization и errors - -**SLM-PRO-BUS-017 - ОБЯЗАН.** External result должен быть нормализован в business-owned model до выхода из DomainRuntime. - -**SLM-PRO-BUS-018 - ОБЯЗАН.** Malformed successful response должен считаться нарушением runtime contract, а не валидным отсутствием данных. - -**SLM-PRO-BUS-019 - ОБЯЗАН.** Expected domain outcome и technical failure должны быть различимы в public contract. - -**SLM-PRO-BUS-020 - ЗАПРЕЩЕНО.** Source error, HTTP status, SDK error class, raw response и transport message не могут быть consumer contract. - -Business может выражать ожидаемые outcomes через typed result или domain error. Эта draft-версия не предписывает единственную форму обработки ожидаемых ошибок, но требует business-owned semantics и стабильных discriminants. - -## State - -**SLM-PRO-BUS-021 - ОБЯЗАН.** Business владеет domain state model, допустимыми transitions и semantics commands/selectors. - -Framework-neutral state runtime может быть создан самой factory или предоставлен через business-owned port. Concrete store implementation остаётся запрещённой dependency по [SLM-PRO-BUS-015](#imports) и не раскрывается в public API. diff --git a/docs/ru/specification/modes/pro/domains/client-and-server.md b/docs/ru/specification/modes/pro/domains/client-and-server.md deleted file mode 100644 index f3c9b01..0000000 --- a/docs/ru/specification/modes/pro/domains/client-and-server.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -title: Client и server assembly в SLM Pro -status: draft -normative: true -overlay: pro -base: slm ---- - -# Client и Server Assembly - -> Overlay: `SLM Pro`. - -`client` и `server` создают готовые runtime-specific instances одного domain поверх его business factory и adapters. - -## Client assembly - -```text -domains/{group...}/{domain}/client/ -├── create-{domain}-client-runtime.ts -└── index.ts -``` - -```ts -export const createAuthClientRuntime = (): AuthRuntime => { - return authFactory({ - phoneAuth: browserPhoneAuthAdapter, - session: browserSessionAdapter, - }) -} -``` - -Client technical inputs ограничены client environment/config и platform capabilities, необходимыми для создания adapters собственного domain. Runtime values другого domain technical input не являются. - -**SLM-PRO-ASM-001 - ОБЯЗАН.** Runtime imports client assembly должны ограничиваться собственной business factory, собственными client adapters, собственной React surface и необходимыми client technical inputs. Type-only foreign contracts допускаются по [SLM-PRO-XDOM-005](./cross-domain-boundary.md#type-only-contracts). - -**SLM-PRO-ASM-002 - ОБЯЗАН.** Client assembly должна возвращать готовый runtime собственного domain. - -Запрет на foreign runtime values определяется правилом [SLM-PRO-XDOM-001](./cross-domain-boundary.md#runtime-imports). - -**SLM-PRO-ASM-004 - МОЖЕТ.** Client или server assembly может принимать готовую внешнюю capability через собственный input contract. - -```ts -createUserClientRuntime({ auth: auth.session }) -``` - -Такой input не даёт user domain права создавать AuthRuntime или импортировать его client entrypoint. - -## Server assembly - -```text -domains/{group...}/{domain}/server/ -├── create-{domain}-server-runtime.ts -└── index.ts -``` - -Server technical inputs ограничены request/framework data, server environment/config и platform capabilities, необходимыми для создания server adapters собственного domain. Runtime values другого domain technical input не являются. - -**SLM-PRO-ASM-005 - ОБЯЗАН.** Server assembly должна создавать новый runtime в scope, соответствующем request или другой явно выбранной server lifetime. - -**SLM-PRO-ASM-006 - ЗАПРЕЩЕНО.** Server assembly не может повторно использовать adapter или runtime instance, захвативший request credentials, cookies, headers или user-specific state другого scope. - -**SLM-PRO-ASM-007 - ОБЯЗАН.** Framework/request input используется только для создания server adapters и не протекает как raw framework object в business API. - -**SLM-PRO-ASM-008 - ОБЯЗАН.** Server entrypoint должен иметь явный server-only marker, если framework предоставляет такой механизм. - -**SLM-PRO-ASM-016 - ОБЯЗАН.** Runtime imports server assembly должны ограничиваться собственной business factory, собственными server adapters и необходимыми server technical inputs; runtime import React/client surface запрещён. Type-only foreign contracts допускаются по [SLM-PRO-XDOM-005](./cross-domain-boundary.md#type-only-contracts). - -## Constructor и activation - -Assembly определяет способ создания, но не владеет полным cross-domain graph. - -Отсутствие product request, socket connection и background resource при вызове runtime creator определяется base-правилом `SLM-BASE-LIFE-002`. - -```text -module import - → определяет creator - -creator call - → создаёт runtime instance - -explicit start - → запускает resources -``` - -**SLM-PRO-ASM-010 - ОБЯЗАН.** Resources запускает composition scope owner в выбранном scope согласно [lifecycle rules](../../../runtime-and-lifecycle.md). - -## Public entrypoints - -**SLM-PRO-CMP-004 - ЗАПРЕЩЕНО.** Composition не может повторять adapter wiring, если domain public assembly уже создаёт готовый runtime. - -**SLM-PRO-CMP-013 - ОБЯЗАН.** Composition должна использовать public client/server creator domain, если domain предоставляет runtime-specific assembly. - -**SLM-PRO-CMP-014 - МОЖЕТ.** Composition может вызвать public business factory напрямую только для universal domain, у которого нет external ports, concrete adapters и runtime-specific input. - -**SLM-PRO-ASM-011 - ОБЯЗАН.** Client и server assembly должны иметь разные public entrypoints. - -**SLM-PRO-ASM-012 - ЗАПРЕЩЕНО.** Общий domain barrel не может runtime-реэкспортировать одновременно client и server surfaces. - -## Server/client bridge - -Client и server runtimes являются разными instances над общей business semantics. - -Запрет на передачу DomainRuntime, functions, Context, store или query client через serializable server/client boundary определяется правилом [SLM-BASE-DATA-012](../../../state-and-data.md#serializable-boundaries). - -**SLM-PRO-ASM-014 - МОЖЕТ.** Server может передать client assembly только serializable business-owned bootstrap data без secrets и mutable runtime objects. diff --git a/docs/ru/specification/modes/pro/domains/cross-domain-boundary.md b/docs/ru/specification/modes/pro/domains/cross-domain-boundary.md deleted file mode 100644 index 04132d2..0000000 --- a/docs/ru/specification/modes/pro/domains/cross-domain-boundary.md +++ /dev/null @@ -1,125 +0,0 @@ ---- -title: Cross-domain boundary -status: draft -normative: true -overlay: pro -base: slm ---- - -# Cross-domain Boundary - -> Overlay: `SLM Pro`. - -Domains не образуют скрытый runtime graph внутри слоя `domains`. Граф связывается только graph owner в `compositions`. - -## Composition graph - -**SLM-PRO-CMP-001 - ОБЯЗАН.** Runtime graph нескольких domains должен собираться в composition, которая владеет его scope. - -```ts -const auth = createAuthRuntime() -const user = createUserRuntime({ auth: auth.session }) -const orders = createOrdersRuntime({ user: user.agreements }) -``` - -**SLM-PRO-CMP-002 - ОБЯЗАН.** Composition должна создавать domain runtimes в явном ацикличном порядке. - -**SLM-PRO-CMP-003 - ОБЯЗАН.** Cross-domain dependency должна передаваться как готовая минимальная capability, а не разрешаться service locator или domain import. - -**SLM-PRO-CMP-012 - ОБЯЗАН.** App-specific graph type должен отражать только реально предоставленные runtimes; `Partial` с последующим приведением к полному graph запрещён. - -## Runtime imports - -**SLM-PRO-XDOM-001 - ЗАПРЕЩЕНО.** Ни одна zone domain A не может импортировать, реэкспортировать, dynamic-import или разрешать через service locator runtime value domain B. - -Запрет включает foreign business API, hooks, Provider, Context, components, adapters, runtime creators и event emitters. - -Foreign runtime capability может поступить только argument-ом от composition согласно разделу [Runtime capability injection](#runtime-capability-injection). - -## Type-only contracts - -**SLM-PRO-API-008 - СЛЕДУЕТ.** Cross-domain capability следует описывать consumer-owned structural port вместо зависимости от полного foreign API type. - -**SLM-PRO-XDOM-014 - ЗАПРЕЩЕНО.** Public contract зависимого domain не может реэкспортировать полный foreign DomainRuntime type как собственную cross-domain dependency. - -**SLM-PRO-XDOM-005 - МОЖЕТ.** Business и client/server input contracts domain могут type-only импортировать минимальный стабильный business contract другого domain. - -Предпочтение consumer-owned port определяется правилом `SLM-PRO-API-008`. - -```ts -export type UserAuthPort = { - getSessionSnapshot: () => SessionSnapshot - subscribeToSession: (listener: () => void) => () => void -} -``` - -Type-only import не разрешает runtime import и не переносит ownership. - -**SLM-PRO-XDOM-012 - ЗАПРЕЩЕНО.** Type dependency cycle между domains запрещён, даже если не создаёт runtime cycle. - -## Runtime capability injection - -**SLM-PRO-XDOM-007 - МОЖЕТ.** Domain runtime creator может принять готовую structurally compatible capability, созданную другим domain и переданную composition. - -```ts -const auth = createAuthClientRuntime() -const user = createUserClientRuntime({ auth: auth.session }) -``` - -User domain знает только свой input contract. Он не знает creator, Provider, adapters и scope AuthRuntime. - -**SLM-PRO-XDOM-008 - ОБЯЗАН.** Передаваемая capability должна быть минимальной и не раскрывать raw store, Context, SDK client или mutable internals foreign domain. - -**SLM-PRO-XDOM-013 - МОЖЕТ.** Structurally compatible foreign capability может реализовать consumer-owned port напрямую. Wrapper adapter создаётся только при необходимости преобразовать contracts или lifecycle. - -## React composition - -Если React-сущность использует runtime API двух domains, она принадлежит `compositions`. - -```tsx -const ProtectedOrderForm = () => { - const auth = useAuth() - const order = useOrder() - - return auth.isAuthenticated - ? - : -} -``` - -**SLM-PRO-XDOM-009 - ОБЯЗАН.** Props, callbacks и slots, передаваемые из composition в domain UI, должны оставаться domain-local или presentation-neutral. Foreign domain semantics остаётся во владеющей composition. - -```tsx - - - -``` - -Такое связывание выполняется в composition, а не внутри auth или orders. - -## Events - -Прямая подписка на event emitter другого domain через runtime import запрещена правилом `SLM-PRO-XDOM-001`. - -Composition может передать event capability через consumer-owned port: - -```ts -const orders = createOrdersClientRuntime({ - userEvents: { - subscribeToIdentity: user.identity.subscribe, - }, -}) -``` - -## Cycles - -**SLM-PRO-XDOM-011 - ЗАПРЕЩЕНО.** Runtime dependency cycle между domains является нарушением границы и не может скрываться event bus, lazy resolution или two-way service locator. - -**SLM-PRO-LIFE-008 - ОБЯЗАН.** Cross-domain graph запускается в dependency order и освобождается в обратном порядке. - -Ненормативное пояснение: при обнаружении цикла следует пересмотреть один из вариантов: - -- пересмотреть границы domains; -- перенести orchestration в composition; -- выделить отдельную product responsibility; -- инвертировать зависимость через consumer-owned port. diff --git a/docs/ru/specification/modes/pro/domains/framework.md b/docs/ru/specification/modes/pro/domains/framework.md deleted file mode 100644 index 6f84a5d..0000000 --- a/docs/ru/specification/modes/pro/domains/framework.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: Framework surface в SLM Pro -status: draft -normative: true -overlay: pro -base: slm ---- - -# Framework Surface - -> Overlay: `SLM Pro`. - -Framework surface адаптирует готовый DomainRuntime к execution model конкретного framework. В текущей структуре React surface располагается в `react/`. - -## Структура React surface - -```text -domains/{group...}/{domain}/react/ -├── context/ -├── providers/ -├── hooks/ -├── ui/ -└── index.ts -``` - -Ни один segment не обязателен без реальной потребности. - -## Runtime access - -**SLM-PRO-FRM-001 - ОБЯЗАН.** Framework surface должна работать с конкретным DomainRuntime через domain-owned runtime access boundary. - -**SLM-PRO-FRM-002 - ЗАПРЕЩЕНО.** Framework hook или component не может самостоятельно вызывать business factory, создавать adapters или разрешать runtime из global service locator. - -**SLM-PRO-FRM-003 - ОБЯЗАН.** Runtime access boundary должна получать готовый DomainRuntime извне и не создавать параллельное domain state. - -Для React типичным механизмом является private Context, связывающий статически экспортированные hooks/components с переданным runtime instance. Это пояснение не предписывает точную форму или количество Providers в текущем draft. - -**Domain runtime Provider** - часть framework surface, получающая готовый DomainRuntime и предоставляющая его framework consumers одного domain. Provider не создаёт cross-domain graph автоматически. - -## Imports - -**SLM-PRO-FRM-004 - ОБЯЗАН.** React surface должна импортировать business runtime contracts только через `import type`. - -**SLM-PRO-FRM-005 - ЗАПРЕЩЕНО.** React surface не может runtime-импортировать business factory, private business services, selectors, validators, errors или constants. - -**SLM-PRO-FRM-006 - ЗАПРЕЩЕНО.** React surface не может импортировать domain adapters, SDK, product infra client или assembly. - -**SLM-PRO-FRM-007 - МОЖЕТ.** React surface может импортировать public API `ui`, `shared` и framework libraries, разрешённые её runtime profile. - -Cross-domain runtime imports framework surface запрещены правилом [SLM-PRO-XDOM-001](./cross-domain-boundary.md#runtime-imports). - -## Hooks - -**SLM-PRO-FRM-009 - ОБЯЗАН.** Domain hook должен получать product data и behavior только через текущий DomainRuntime. - -**SLM-PRO-FRM-010 - МОЖЕТ.** Hook может использовать framework query/cache runtime как private implementation поверх imperative DomainRuntime query. - -**SLM-PRO-FRM-011 - ЗАПРЕЩЕНО.** Query hook не может использовать adapter или SDK call как fetcher в обход DomainRuntime. - -**SLM-PRO-FRM-012 - ЗАПРЕЩЕНО.** Query-library types, cache keys и raw mutate API не могут становиться public business contract. - -## Domain UI - -Domain React UI может: - -- вызывать hooks своего domain; -- использовать universal UI; -- отображать domain-owned states и outcomes; -- принимать callbacks, props и slots от composition. - -**SLM-PRO-FRM-013 - ЗАПРЕЩЕНО.** Domain UI не может импортировать runtime другого domain или оркестрировать route/page flow. - -Владение React UI, использующим несколько domains, определено base-правилом [SLM-BASE-CMP-006](../../../layers/compositions.md#product-ui). - -## Client boundary - -**SLM-PRO-FRM-015 - ОБЯЗАН.** Entry point React hooks, Context и interactive UI должен быть явно отмечен как client runtime согласно правилам используемого framework. - -**SLM-PRO-FRM-016 - ЗАПРЕЩЕНО.** Server-compatible React export не может попадать в client entrypoint только из-за нахождения рядом с client hooks или Provider. - -React не является синонимом client runtime; environment profile определяется фактическими dependencies export. diff --git a/docs/ru/specification/modes/pro/domains/index.md b/docs/ru/specification/modes/pro/domains/index.md deleted file mode 100644 index e3a97c4..0000000 --- a/docs/ru/specification/modes/pro/domains/index.md +++ /dev/null @@ -1,158 +0,0 @@ ---- -title: Domains в SLM Pro -status: draft -normative: true -overlay: pro -base: slm ---- - -# Domains в SLM Pro - -> Overlay: `SLM Pro`. Base: [SLM](../../../index.md). - -Domain является изолированным вертикальным product module с одной предметной ответственностью, явным public boundary и строгими внутренними dependency zones. - -## Domain и group - -**SLM-PRO-DOM-001 - ОБЯЗАН.** Конечный Pro domain должен располагаться непосредственно в `domains` или внутри одной или нескольких навигационных groups. - -```text -domains/{domain} -domains/{group}/{domain} -domains/{group}/{nested-group}/{domain} -``` - -**SLM-PRO-DOM-002 - ОБЯЗАН.** Узел domain tree с собственным public API, state, integration, assembly или runtime должен классифицироваться как конечный domain, а не domain group. - -```text -domains/ -├── navigation/ # domain -└── knv/ # group - ├── auth/ # domain - ├── user/ # domain - └── orders/ # domain -``` - -**SLM-PRO-DOM-003 - ОБЯЗАН.** Первой архитектурной единицей в group tree является конечная папка, владеющая самостоятельной product responsibility. - -## Ownership - -**SLM-PRO-DOM-004 - ОБЯЗАН.** Pro domain должен владеть одной сформулированной product responsibility и предоставлять её внешним consumers через собственные public entrypoints. - -Pro domain может владеть: - -- product model и value objects; -- scenarios и operations; -- domain state и transitions; -- normalization и product errors; -- business-owned ports; -- concrete integrations собственных ports; -- framework hooks и UI одного domain; -- client/server runtime assembly. - -**SLM-PRO-DOM-005 - ЗАПРЕЩЕНО.** Domain не может владеть framework route entry, page/layout composition, UI нескольких самостоятельных product responsibilities, universal technical capability или product-agnostic UI primitive. - -Public entrypoints Pro domain следуют base-правилам `SLM-BASE-API-001` и `SLM-BASE-API-002`; Pro-главы вводят дополнительные ограничения exports. - -**SLM-PRO-DOM-007 - ЗАПРЕЩЕНО.** Если product responsibility получила Pro domain owner, app, composition или infra не могут создавать параллельную модель этой ответственности либо обходить её public boundary. - -**SLM-PRO-DOM-008 - ОБЯЗАН.** Для domain-owned responsibility это правило заменяет base-правило `SLM-BASE-CMP-001`: business владеет product logic, а composition владеет application flow и runtime graph. - -Product responsibility считается устойчивой, если имеет самостоятельную product model или transitions, используется несколькими application flows либо владеет external integration/lifecycle contract. - -**SLM-PRO-DOM-017 - ОБЯЗАН.** Каждая устойчивая product responsibility должна иметь Pro domain owner; route/page-local presentation flow остаётся ответственностью composition. - -## Внутренние zones - -```text -domains/{group...}/{domain}/ -├── business/ -├── react/ -├── adapters/ -├── client/ -└── server/ -``` - -| Zone | Статус | Ответственность | -|---|---|---| -| [`business`](./business.md) | Обязательная | Product model, factory, ports, scenarios, errors | -| [`react`](./framework.md) | Опциональная | React runtime access, hooks, Providers, domain UI | -| [`adapters`](./ports-and-adapters.md) | Опциональная | Concrete реализации business-owned ports | -| [`client`](./client-and-server.md) | Опциональная | Browser/client assembly одного domain | -| [`server`](./client-and-server.md) | Опциональная | Server/request assembly одного domain | - -**SLM-PRO-DOM-009 - ОБЯЗАН.** Каждый Pro domain должен содержать `business` как единственного владельца product model и business semantics. - -**SLM-PRO-DOM-010 - СЛЕДУЕТ.** Опциональную zone следует добавлять только при наличии реального runtime consumer и самостоятельной ответственности. - -**SLM-PRO-DOM-011 - ЗАПРЕЩЕНО.** Нельзя создавать пустые симметричные `react`, `adapters`, `client` или `server` на будущее. - -**SLM-PRO-DOM-012 - ОБЯЗАН.** Domain zones должны соблюдать внутреннюю dependency direction, даже если физически находятся под одним владельцем. - -**SLM-PRO-MOD-001 - ОБЯЗАН.** `business`, `react`, `adapters`, `client` и `server` являются внутренними zones одного domain, а не самостоятельными верхнеуровневыми modules. - -**SLM-PRO-SEG-001 - ЗАПРЕЩЕНО.** Domain zones нельзя трактовать как взаимозаменяемые generic segments. - -Внутри каждой zone могут использоваться обычные base SLM segments по фактической необходимости. - -## Внутреннее направление - -```text -business -> shared | pure libraries -react -> ui | shared | framework libraries -adapters -> infra | SDK | platform runtime -client -> own business factory | own client adapters | own framework surface | client technical inputs -server -> own business factory | own server adapters | server technical inputs -``` - -Матрица описывает runtime imports. React surface может type-only импортировать собственные business contracts, adapters - собственные business ports/types, а client/server inputs - разрешённые cross-domain contracts. - -## Путь данных - -```text -composition - -> domain client/server assembly при наличии runtime-specific setup - или напрямую business factory для universal domain - -> DomainRuntime - -> business scenario - -> business-owned port - -> domain adapter - -> infra / SDK / storage / external source -``` - -**SLM-PRO-DOM-013 - ОБЯЗАН.** DomainRuntime, созданный business factory, должен быть единственным product gateway своего Pro domain для runtime consumers. - -Stateless logic API также является DomainRuntime, если он создан factory и соблюдает тот же public boundary. - -## Product UI - -**SLM-PRO-DOM-014 - МОЖЕТ.** Product UI одной Pro domain responsibility может принадлежать framework surface этого domain. - -UI нескольких самостоятельных responsibilities остаётся в `compositions` согласно base-правилу `SLM-BASE-CMP-006`. - -## Cross-domain graph - -```text -composition - -> создаёт несколько domain runtimes - -> передаёт готовые capabilities -``` - -Pro domain не создаёт runtime другого domain и не импортирует его runtime surface. Точные правила определены в [Cross-domain boundary](./cross-domain-boundary.md). - -**Graph owner** - composition, являющаяся scope owner нескольких DomainRuntime, связанных направленными dependencies в одном ацикличном graph, и определяющая порядок их создания, activation и cleanup. - -## Monorepo boundary - -**SLM-PRO-DOM-015 - ОБЯЗАН.** Pro domain должен оставаться внутри `apps/{app}/src/domains` до принятия отдельной package-модели. - -**SLM-PRO-DOM-016 - ЗАПРЕЩЕНО.** Workspace package не может называться Pro Domain для целей Specification, если он не соответствует application path и ownership этой главы. - -## Главы Pro Domain Specification - -- [Business](./business.md) -- [Framework surface](./framework.md) -- [Ports и adapters](./ports-and-adapters.md) -- [Client и server assembly](./client-and-server.md) -- [Cross-domain boundary](./cross-domain-boundary.md) -- [Тестирование Pro domains](./testing.md) diff --git a/docs/ru/specification/modes/pro/domains/ports-and-adapters.md b/docs/ru/specification/modes/pro/domains/ports-and-adapters.md deleted file mode 100644 index d2f6692..0000000 --- a/docs/ru/specification/modes/pro/domains/ports-and-adapters.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: Ports и adapters в SLM Pro -status: draft -normative: true -overlay: pro -base: slm ---- - -# Ports и Adapters - -> Overlay: `SLM Pro`. - -Port определяет потребность business. Adapter связывает эту потребность с concrete runtime. - -## Ownership - -```text -domain/ -├── business/ -│ └── ports/ -└── adapters/ -``` - -**SLM-PRO-ADP-001 - ОБЯЗАН.** Port должен принадлежать `business` того domain, который потребляет capability. - -**SLM-PRO-ADP-002 - ОБЯЗАН.** Concrete adapter должен принадлежать тому же domain, но находиться вне `business`. - -**SLM-PRO-ADP-003 - ЗАПРЕЩЕНО.** Infra или external SDK не могут объявлять business port от имени domain. - -**SLM-PRO-ADP-014 - ОБЯЗАН.** External technical capability из infra, SDK, storage или platform runtime должна реализовывать business port через adapter собственного domain, а этот adapter должен подключаться assembly того же domain. Готовая capability другого DomainRuntime может удовлетворять consumer-owned port напрямую только по правилу [SLM-PRO-XDOM-013](./cross-domain-boundary.md#runtime-capability-injection). - -## Adapter contract - -**SLM-PRO-ADP-004 - ОБЯЗАН.** Responsibilities adapter должны ограничиваться применимыми integration operations: - -- импортировать type-only business port и domain input types; -- импортировать public infra API, SDK или platform runtime; -- переводить domain arguments в transport arguments; -- возвращать raw/unknown source result для business normalization; -- подписываться на concrete event source через явный lifecycle contract. - -**SLM-PRO-ADP-005 - ЗАПРЕЩЕНО.** Adapter не может выполнять следующие domain/framework responsibilities: - -- создавать domain error; -- выбирать domain fallback; -- реализовывать business rule; -- объявлять domain model; -- экспортировать concrete client consumer-коду; -- вызывать framework hook; -- runtime-импортировать или самостоятельно разрешать другой domain runtime. - -Adapter может работать с минимальной foreign capability, явно переданной composition, только для преобразования contract или lifecycle согласно `SLM-PRO-XDOM-013`. - -**SLM-PRO-ADP-006 - ОБЯЗАН.** Adapter должен реализовывать ровно тот port contract, который необходим business. - -**SLM-PRO-ADP-007 - ЗАПРЕЩЕНО.** Нельзя передавать полный client, если port требует ограниченный набор capabilities. - -**SLM-PRO-ADP-008 - ЗАПРЕЩЕНО.** Adapter integration logic не должна писаться inline в composition или runtime assembly. - -## Client и server adapters - -Adapters могут быть разделены по runtime: - -```text -adapters/ -├── client/ -│ ├── browser-session.adapter.ts -│ └── websocket-orders.adapter.ts -└── server/ - ├── request-session.adapter.ts - └── server-orders-api.adapter.ts -``` - -**SLM-PRO-ADP-009 - ОБЯЗАН.** Client adapter не должен попадать в server graph, а server adapter - в client graph. - -**SLM-PRO-ADP-010 - ОБЯЗАН.** Runtime-specific adapter должен иметь явный environment marker, если framework предоставляет такой механизм. - -## Event sources - -Socket, subscription и event listener реализуют event port: - -```ts -export type OrdersEventsPort = { - subscribe: (listener: (event: unknown) => void) => () => void -} -``` - -**SLM-PRO-ADP-011 - ОБЯЗАН.** Event adapter должен возвращать cleanup и не открывать connection при module import. - -**SLM-PRO-ADP-012 - ОБЯЗАН.** Wire event проходит business normalization до изменения domain state или передачи consumer-коду. - -**SLM-PRO-LIFE-013 - МОЖЕТ.** Один physical transport может обслуживать adapters нескольких domains, если transport остаётся domain-agnostic, а adapters получают суженные channels. - -## Public boundary - -**SLM-PRO-API-014 - ЗАПРЕЩЕНО.** Public business, framework, client или server entrypoint не может реэкспортировать concrete adapter внешним consumers. - -**SLM-PRO-ADP-013 - ЗАПРЕЩЕНО.** `adapters` не имеет внешнего public API для app, compositions или других domains. - -Adapters доступны только assembly собственного domain и собственным contract tests. diff --git a/docs/ru/specification/modes/pro/domains/testing.md b/docs/ru/specification/modes/pro/domains/testing.md deleted file mode 100644 index 9b34347..0000000 --- a/docs/ru/specification/modes/pro/domains/testing.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: Тестирование Pro domains -status: draft -normative: true -overlay: pro -base: slm ---- - -# Тестирование Pro Domains - -> Overlay: `SLM Pro`. - -Общие правила [тестирования и соответствия](../../../testing-and-conformance.md) дополняются проверками строгих business, adapter, assembly и framework boundaries. - -## Business factory tests - -**SLM-PRO-TEST-001 - ОБЯЗАН.** Каждый public method runtime API, возвращаемого factory, должен иметь factory-level tests. - -Factory-level tests должны проверять применимые случаи: - -- happy path; -- malformed external result; -- rejected dependency; -- синхронное исключение dependency; -- domain outcome/error semantics; -- side-effect order; -- state transition; -- отсутствие constructor-time I/O; -- public API shape. - -**SLM-PRO-TEST-002 - ОБЯЗАН.** Factory-level test должен создавать runtime через public `business` entrypoint, а не deep-import factory internals. - -## Adapter tests - -**SLM-PRO-TEST-003 - ОБЯЗАН.** Adapter с mapping, transport payload, error channel или lifecycle должен иметь contract tests на применимые responsibilities. - -**SLM-PRO-TEST-004 - ЗАПРЕЩЕНО.** Adapter test не должен дублировать business scenario tests или утверждать domain fallback/error semantics. - -## Assembly tests - -**SLM-PRO-TEST-005 - ОБЯЗАН.** Client/server assembly tests должны проверять корректную передачу ports, runtime profile isolation и отсутствие I/O при creation. - -**SLM-PRO-TEST-006 - ОБЯЗАН.** Server assembly с request data должен иметь isolation test для параллельных scopes. - -## Framework tests - -**SLM-PRO-TEST-007 - ОБЯЗАН.** Framework surface tests должны проверять runtime access boundary, предсказуемую ошибку при отсутствии runtime boundary, mapping public outcomes и lifecycle integration. - -## Composition tests - -**SLM-PRO-TEST-009 - ОБЯЗАН.** Tests cross-domain composition должны проверять topology, точный graph contract, переданные capabilities и lifecycle cleanup. - -**SLM-PRO-TEST-010 - ОБЯЗАН.** Scope с неполным набором domains не должен типизироваться как полный application graph. - -## Architecture checks - -**SLM-PRO-TEST-019 - ОБЯЗАН.** Pro repository checks должны проверять применимые строгие domain boundaries: - -- client/server markers; -- forbidden runtime imports между domains; -- private adapters; -- business entrypoint shape; -- zone dependency direction. diff --git a/docs/ru/specification/modes/pro/index.md b/docs/ru/specification/modes/pro/index.md deleted file mode 100644 index be7fb46..0000000 --- a/docs/ru/specification/modes/pro/index.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: SLM Pro -status: draft -normative: true -overlay: pro -base: slm ---- - -# SLM Pro - -`SLM Pro` является независимым overlay непосредственно над [base SLM](../../index.md). - -```text -SLM Pro = SLM + Pro rules -``` - -## Отличия от SLM - -| Область | Base SLM | SLM Pro | -|---|---|---| -| Product ownership | Product logic принадлежит compositions | Устойчивая product responsibility принадлежит изолированному domain | -| Слои | `app`, `compositions`, `infra`, `ui`, `shared` | Добавляется `domains` | -| Структура domain | Отсутствует | `business`, framework surface, adapters, client/server assembly | -| Domain dependencies | Отсутствуют | Cross-domain runtime imports запрещены, capabilities передаются composition | -| External integration | Composition использует infra | Private domain adapter реализует business-owned port | -| Testing | Risk-based base tests | Обязательные tests для используемых factory, adapter, assembly и graph boundaries | - -## Расширение архитектурной модели - -**SLM-PRO-ARCH-001 - ОБЯЗАН.** SLM Pro должен расширять набор base-слоёв слоем `domains` для изолированных product responsibilities. - -```text -src/ -├── app/ -├── compositions/ -├── domains/ -├── infra/ -├── ui/ -└── shared/ -``` - -**SLM-PRO-ARCH-002 - ОБЯЗАН.** Дополнительные dependency edges Pro должны соответствовать следующему направлению: - -```text -compositions -> domains -domains -> согласно внутренним Pro zones -``` - -Base dependency direction для остальных слоёв сохраняется. - -**SLM-PRO-CMP-010 - МОЖЕТ.** Composition module может импортировать public entrypoints Pro domains в дополнение к imports, разрешённым base-правилом `SLM-BASE-CMP-010`. - -## Pro Domain Specification - -Полная Pro-модель слоя описана в [Domains](./domains/index.md). Других mode-specific отличий текущий draft Pro не вводит. diff --git a/docs/ru/specification/modules-and-groups.md b/docs/ru/specification/modules-and-groups.md deleted file mode 100644 index 753f283..0000000 --- a/docs/ru/specification/modules-and-groups.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: Модули и группы -status: draft -normative: true ---- - -# Модули и Группы - -## Module - -Module является минимальным самостоятельным владельцем ответственности и предоставляет public boundary внешнему коду. - -**SLM-BASE-MOD-001 - ОБЯЗАН.** Module должен иметь одну сформулированную ответственность и одного архитектурного owner. - -**SLM-BASE-MOD-002 - ОБЯЗАН.** Внешний consumer взаимодействует с module только через его public API. - -**SLM-BASE-MOD-003 - СЛЕДУЕТ.** Module следует ограничивать только теми внутренними parts и segments, которые необходимы текущей ответственности. - -Типичные modules: - -- page, layout, screen или widget в `compositions`; -- technical service в `infra`; -- reusable UI module в `ui`. - -`app` содержит framework entries и не обязан организовываться как SLM modules. `shared` может содержать небольшие public units, но не runtime modules. - -## Group - -Group классифицирует modules и другие groups, но не владеет поведением. - -**SLM-BASE-MOD-004 - ЗАПРЕЩЕНО.** Group не может иметь `index.ts`, public API, state, runtime, dependencies или assembly. - -**SLM-BASE-MOD-005 - ЗАПРЕЩЕНО.** Внешний код не может импортировать group path. - -**SLM-BASE-MOD-006 - МОЖЕТ.** Group может содержать другие groups и конечные modules. - -```text -compositions/ -└── pages/ # group - ├── home/ # composition module - └── profile/ # composition module -``` - -## Component - -Component является presentation unit внутри module и не считается самостоятельным архитектурным owner. - -**SLM-BASE-MOD-008 - ЗАПРЕЩЕНО.** Component не может самостоятельно выбирать application-level product source, выполнять module wiring или оркестрировать несколько самостоятельных modules. - -**SLM-BASE-MOD-009 - МОЖЕТ.** Component может владеть локальной presentation mechanics и рендерить другие components, разрешённые слоем владельца. - -**SLM-BASE-MOD-010 - ОБЯЗАН.** Presentation unit с самостоятельной ответственностью, внешними архитектурными dependencies или внутренней modular structure должна оформляться как module или nested module. Сам факт локального hook/state не делает component модулем. - -## Nested module - -Самостоятельная часть родительского module может быть оформлена nested module, если имеет собственную ответственность и public boundary только внутри родителя. - -```text -compositions/pages/home/ -└── parts/ - └── hero-section/ - ├── hero-section.tsx - └── index.ts -``` - -**SLM-BASE-MOD-011 - ЗАПРЕЩЕНО.** Nested module не может использоваться для сокрытия ответственности, которой фактически владеет другой module или layer. - -## Scope evolution - -**SLM-BASE-MOD-012 - СЛЕДУЕТ.** Код следует поднимать из локального owner в более широкий module только после появления реального совместного consumer или общей ответственности. - -**SLM-BASE-MOD-013 - ЗАПРЕЩЕНО.** Физическое повторение само по себе не доказывает общий ownership. diff --git a/docs/ru/specification/monorepo.md b/docs/ru/specification/monorepo.md deleted file mode 100644 index 843373a..0000000 --- a/docs/ru/specification/monorepo.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: Монорепозитории -status: draft -normative: true ---- - -# Монорепозитории - -SLM применяется внутри границы каждого frontend-приложения. Workspace packages имеют собственные public boundaries и ownership. - -## Application boundary - -```text -apps/ -└── web/ - └── src/ - ├── app/ - ├── compositions/ - ├── infra/ - ├── ui/ - └── shared/ -``` - -**SLM-BASE-MONO-001 - ОБЯЗАН.** Каждое приложение должно самостоятельно определять свои application compositions, product ownership и runtime wiring. - -**SLM-BASE-MONO-002 - ЗАПРЕЩЕНО.** Workspace package не может импортировать код из `apps/*`. - -**SLM-BASE-MONO-003 - ЗАПРЕЩЕНО.** Одно приложение не может deep-import исходники другого приложения вместо общего package contract. - -## Package boundary - -**SLM-BASE-MONO-004 - ОБЯЗАН.** Package должен иметь самостоятельного owner, public exports и подтверждённую reuse/ownership semantics. - -**SLM-BASE-MONO-005 - ЗАПРЕЩЕНО.** Нельзя создавать package только для обхода layer direction, public API или иной объявленной dependency boundary. - -**SLM-BASE-MONO-006 - ОБЯЗАН.** Consumers импортируют package через объявленный package export, а не через filesystem path к internal source. - -## Типичные packages - -Допустимыми кандидатами являются: - -- product-agnostic UI kit; -- technical infra client; -- deterministic shared foundation; -- schema/codegen/tooling package; -- configuration package без application-specific wiring. - -Base SLM не присваивает package дополнительный архитектурный статус автоматически. - -## Dependency direction - -**SLM-BASE-MONO-009 - ОБЯЗАН.** Package dependency graph должен оставаться ацикличным и соответствовать заявленной ответственности packages. - -**SLM-BASE-MONO-010 - ЗАПРЕЩЕНО.** Shared package не может импортировать application composition или app-specific infra package. diff --git a/docs/ru/specification/public-api-and-imports.md b/docs/ru/specification/public-api-and-imports.md deleted file mode 100644 index 9791fb6..0000000 --- a/docs/ru/specification/public-api-and-imports.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: Public API и импорты -status: draft -normative: true ---- - -# Public API и Импорты - -Public API ограничивает знание consumers о внутренней структуре module. Точная форма entrypoint определяется владельцем и не требует обязательного `index.ts`. - -## Общие правила - -**SLM-BASE-API-001 - ОБЯЗАН.** Межмодульный import должен использовать объявленный public entrypoint импортируемого module. - -**SLM-BASE-API-002 - ЗАПРЕЩЕНО.** Deep imports во внутренние segments, files и иные private paths другого module запрещены. - -**SLM-BASE-API-003 - ОБЯЗАН.** Каждый runtime export должен иметь реального consumer за пределами владеющего entrypoint и стабильную ответственность. - -**SLM-BASE-API-004 - ЗАПРЕЩЕНО.** Public API не может случайно раскрывать implementation unit, который владелец считает private или lifecycle которого не является частью public contract. - -**SLM-BASE-API-005 - ОБЯЗАН.** Alias или package subpath должен физически разрешаться TypeScript, tests и production build. - -**SLM-BASE-API-009 - МОЖЕТ.** Public entrypoint может быть root `index.ts`, отдельным named entry, package export или другим явно объявленным path. - -**SLM-BASE-API-010 - ОБЯЗАН.** Public и private paths module должны быть различимы consumers и repository tooling. - -## Layer matrix - -| Importer | Runtime imports | -|---|---| -| `app` | Public composition entries, shared static/global resources | -| `compositions` | Compositions, infra, ui, shared | -| `infra` | Infra, shared | -| `ui` | UI, shared | -| `shared` | External pure libraries only | - -## Type-only imports - -**SLM-BASE-API-006 - МОЖЕТ.** `import type` может использоваться для разрешённого contract dependency без создания runtime edge. - -**SLM-BASE-API-007 - ЗАПРЕЩЕНО.** Type-only import не разрешает перенос ownership, импорт private concrete runtime type или обход layer boundary. - -## Groups - -Отсутствие public entrypoint у group определяется base-правилом `SLM-BASE-MOD-004`. - -**SLM-BASE-API-015 - ОБЯЗАН.** Composition public API экспортирует только entry components, access APIs, types и contracts, необходимые внешним composition consumers. - -## Cycles - -**SLM-BASE-API-016 - ЗАПРЕЩЕНО.** Runtime import cycle между modules запрещён независимо от того, способен ли bundler его выполнить. - -**SLM-BASE-API-017 - ЗАПРЕЩЕНО.** Barrel не должен создавать скрытый cycle между ready composition и access API её children. - -Дополнительные entrypoints и import restrictions принадлежат overlay, который их вводит. diff --git a/docs/ru/specification/rules.md b/docs/ru/specification/rules.md deleted file mode 100644 index 592250a..0000000 --- a/docs/ru/specification/rules.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Реестр правил -status: draft -normative: false -search: false -aside: false ---- - -# Реестр Правил - -Реестр формируется автоматически из нормативных объявлений Specification. Для быстрого перехода к известному ID также можно открыть поиск `Ctrl/⌘ K`, ввести полный идентификатор и нажать `Enter`. - - diff --git a/docs/ru/specification/runtime-and-lifecycle.md b/docs/ru/specification/runtime-and-lifecycle.md deleted file mode 100644 index b497702..0000000 --- a/docs/ru/specification/runtime-and-lifecycle.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: Runtime и lifecycle -status: draft -normative: true ---- - -# Runtime и Lifecycle - -Lifecycle является архитектурной частью любого mutable runtime, subscription и external resource. Эти правила не требуют создавать отдельный runtime или factory, если у module нет соответствующего состояния или resources. - -## Definition, creation и activation - -Для module с создаваемым runtime применима модель: - -```text -definition - -> module объявляет creator - -creation - -> creator создаёт instance без external effects - -activation - -> scope owner запускает resources и получает cleanup -``` - -**SLM-BASE-LIFE-001 - ЗАПРЕЩЕНО.** Module import не должен выполнять product I/O, открывать connection или регистрировать global listener. - -**SLM-BASE-LIFE-002 - ОБЯЗАН.** Если module предоставляет factory или runtime creator, creation должна быть side-effect free относительно external resources. - -**SLM-BASE-LIFE-003 - ОБЯЗАН.** Subscription, socket, timer и listener запускаются явной operation владельца scope. - -**SLM-BASE-LIFE-004 - ОБЯЗАН.** Каждый запущенный resource должен иметь cleanup или dispose contract. - -## Scope - -| Scope | Примеры владельца | -|---|---| -| Application | Root composition/provider | -| Route branch | Route layout composition | -| Page | Page composition/provider | -| Component flow | Nested composition module | -| Request | Server composition/request builder | -| Test | Test setup/wrapper | - -**SLM-BASE-LIFE-005 - ОБЯЗАН.** Scope owner должен определить количество instances и duration каждого mutable runtime или resource. - -**SLM-BASE-LIFE-006 - ЗАПРЕЩЕНО.** Module-level singleton не может использоваться как случайная замена application scope. - -**SLM-BASE-LIFE-007 - МОЖЕТ.** Application singleton допустим только при явном application ownership и отсутствии request-, identity- и user-specific data. - -## Activation и cleanup - -**SLM-BASE-LIFE-009 - ОБЯЗАН.** Повторный mount/unmount, включая development Strict Mode, не должен оставлять duplicate subscription или abandoned resource. - -**SLM-BASE-LIFE-010 - СЛЕДУЕТ.** `start` и cleanup следует проектировать idempotent либо явно защищать от повторного вызова. - -**SLM-BASE-LIFE-018 - ОБЯЗАН.** Если activation составного resource set завершилась ошибкой, scope owner должен освободить уже успешно запущенную часть в обратном порядке. - -**SLM-BASE-LIFE-019 - ОБЯЗАН.** Ошибка cleanup должна быть наблюдаемой и не должна препятствовать попытке освободить остальные resources scope. - -## Events и sockets - -Product event обрабатывается владельцем product semantics; socket остаётся technical transport. - -**SLM-BASE-LIFE-011 - ЗАПРЕЩЕНО.** Framework component не может открывать product socket напрямую при render или module import. - -**SLM-BASE-LIFE-012 - ОБЯЗАН.** Invalid event и connection failure должны преобразовываться в product state/outcome либо technical telemetry согласно их semantics; callback error нельзя терять через unobserved throw. - -## Revalidation events - -Event может содержать product update или только сообщать об устаревании данных. - -**SLM-BASE-LIFE-014 - ОБЯЗАН.** Invalidation intent должен выражаться product language и не требовать import конкретной query library в public product contract. - -## Server runtime - -**SLM-BASE-LIFE-015 - ОБЯЗАН.** User-specific server runtime создаётся в request scope. - -**SLM-BASE-LIFE-016 - ЗАПРЕЩЕНО.** Process singleton не может захватывать request headers, cookies, credentials, AbortSignal или user-specific cache. - -**SLM-BASE-LIFE-017 - ОБЯЗАН.** Request cancellation должна передаваться external operations, если runtime и используемая integration поддерживают cancellation. - -Overlay может вводить дополнительные lifecycle boundaries только внутри собственного delta. diff --git a/docs/ru/specification/segments.md b/docs/ru/specification/segments.md deleted file mode 100644 index f4f23b2..0000000 --- a/docs/ru/specification/segments.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: Сегменты -status: draft -normative: true ---- - -# Сегменты - -Segment группирует внутренние файлы module по устойчивой роли. Segment не является самостоятельным layer или module. - -## Базовые segments - -| Segment | Роль | -|---|---| -| `ui/` | Presentation components текущего module | -| `parts/` | Nested modules текущего module | -| `hooks/` | Framework hooks текущей ответственности | -| `providers/` | Provider implementations текущего module | -| `stores/` | Concrete state runtime текущего owner | -| `services/` | Scenario operations и service objects | -| `mappers/` | Transformation на границе ответственности | -| `types/` | Types текущего module | -| `styles/` | Styles текущего module | -| `lib/` | Небольшие internal utilities | -| `config/` | Constants и configuration текущего module | -| `tests/` | Tests публичной границы или составного runtime | - -## Правила - -**SLM-BASE-SEG-001 - МОЖЕТ.** Module может использовать любые необходимые segments и не обязан создавать остальные. - -**SLM-BASE-SEG-002 - ЗАПРЕЩЕНО.** Нельзя создавать полный симметричный набор segments как scaffold без реального содержимого. - -**SLM-BASE-SEG-003 - ОБЯЗАН.** Если файл помещён в segment, роль segment должна соответствовать фактической роли файла, а не только его расширению или имени. Файлы могут оставаться в корне небольшого module. - -**SLM-BASE-SEG-004 - ЗАПРЕЩЕНО.** Segment не имеет внешнего public API независимо от module owner. - -Запрет deep import в segment другого module определяется base-правилом `SLM-BASE-API-002`. - -## UI и Parts - -`ui/` содержит presentation components без самостоятельного architectural ownership. - -`parts/` содержит nested modules с собственной внутренней структурой и локальным public boundary. - -**SLM-BASE-SEG-006 - ОБЯЗАН.** Сущность с самостоятельной ответственностью, внешними архитектурными dependencies или nested modules должна размещаться в `parts`, а не маскироваться как плоский component. Локальные presentation hooks/state сами по себе не требуют `parts`. - -## Hooks - -**SLM-BASE-SEG-007 - ОБЯЗАН.** Hook принадлежит тому module, чью ответственность и runtime он выражает. - -Примеры: - -- product hook - владеющий product module; -- page-local hook - владеющая page composition; -- reusable technical hook - соответствующий infra module; -- product-agnostic UI hook - владеющий UI module. - -Segments являются только внутренними организационными ролями и не вводят дополнительных архитектурных zones. diff --git a/docs/ru/specification/state-and-data.md b/docs/ru/specification/state-and-data.md deleted file mode 100644 index 49ee235..0000000 --- a/docs/ru/specification/state-and-data.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: State и data -status: draft -normative: true ---- - -# State и Data - -SLM рассматривает данные и состояние через одного владельца semantics, даже если runtime использует несколько caches и projections. - -## Ownership matrix - -| Вид | Владелец | -|---|---| -| Product model и transitions | Product owner | -| Product source integration | Product owner; technical mechanism остаётся в infra | -| Framework projection product data | Public surface владельца product data | -| Page-local presentation state | Composition | -| Component-local interaction | Владеющий component/module | -| Technical connection/cache state | Infra или runtime-specific owner | -| Request context | Server/framework scope | -| Universal UI state | Владеющий UI module | - -## Product gateway - -**SLM-BASE-DATA-001 - ОБЯЗАН.** Consumer должен получать product data через public boundary владеющего module. - -**SLM-BASE-DATA-002 - ЗАПРЕЩЕНО.** Composition, UI или app не могут маппить transport DTO в параллельную product model, если модель уже имеет другого owner. - -**SLM-BASE-DATA-003 - ОБЯЗАН.** Product owner владеет normalization, validation и semantics отсутствия данных. - -## Product state - -**SLM-BASE-DATA-004 - ОБЯЗАН.** Product state model и допустимые transitions должны определяться product owner независимо от concrete state manager. - -**SLM-BASE-DATA-005 - ЗАПРЕЩЕНО.** Concrete mutable store implementation не может становиться public product contract без явно объявленного владельцем стабильного store access API. - -**SLM-BASE-DATA-006 - ОБЯЗАН.** Mutable product instance должен быть привязан к явному lifecycle scope. - -## Query cache - -Framework или technical query cache может хранить projection результата product query. - -**SLM-BASE-DATA-007 - ОБЯЗАН.** Query/cache consumer за пределами product owner должен использовать public boundary владельца и не может обходить его прямым вызовом private integration или SDK. - -**SLM-BASE-DATA-008 - ЗАПРЕЩЕНО.** Query cache не может объявлять собственную product model, error taxonomy или fallback policy. - -**SLM-BASE-DATA-009 - ОБЯЗАН.** User/session-scoped cache keys и invalidation должны изолировать данные разных identities и scopes без использования secret как публичного key contract. - -Эта draft-версия не предписывает единственное физическое место QueryClient/SWR cache. Конкретная модель оценивается по правилам public boundary владельца, lifecycle и identity isolation. - -**SLM-BASE-DATA-015 - ОБЯЗАН.** Cache instance должен иметь явного creator и scope owner в composition или runtime setup. - -**SLM-BASE-DATA-016 - ОБЯЗАН.** Shared framework cache должен передаваться consumers через framework-supported runtime boundary, а не через import app-specific mutable singleton. - -**SLM-BASE-DATA-017 - ОБЯЗАН.** Scope owner должен очищать или изолировать private cache при смене identity и завершении соответствующего scope. - -## Presentation state - -**SLM-BASE-DATA-010 - МОЖЕТ.** Composition или component может использовать concrete state manager для локального presentation state. - -**SLM-BASE-DATA-011 - ЗАПРЕЩЕНО.** Presentation store не должен копировать canonical product state как второй source of truth. - -## Serializable boundaries - -**SLM-BASE-DATA-012 - ОБЯЗАН.** Через server/client boundary передаются только serializable product-owned data без functions, stores, clients, Context и resources. - -**SLM-BASE-DATA-013 - ЗАПРЕЩЕНО.** Secrets, access tokens и request credentials не должны включаться в client bootstrap snapshot. - -**SLM-BASE-DATA-014 - ОБЯЗАН.** Server и client initial snapshots должны быть согласованы, если framework выполняет hydration одного UI state. diff --git a/docs/ru/specification/terminology.md b/docs/ru/specification/terminology.md deleted file mode 100644 index 933e1e9..0000000 --- a/docs/ru/specification/terminology.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: Терминология -status: draft -normative: true ---- - -# Терминология - -## Base SLM - -**Base SLM** - самостоятельная минимальная архитектура, применяемая без дополнительного overlay. - -## Overlay - -**Overlay** - независимое опциональное нормативное расширение, применяемое непосредственно поверх base SLM. Overlay не наследует правила другого overlay. - -## Слой - -**Layer** - верхнеуровневая зона `src`, определяющая вид ответственности и допустимые направления зависимостей. - -Base SLM использует слои `app`, `compositions`, `infra`, `ui` и `shared`. - -## Модуль - -**Module** - минимальный самостоятельный владелец ответственности с public boundary. Модуль может содержать код разных технических типов, если весь этот код принадлежит одной ответственности. - -## Product owner - -**Product owner** - module, владеющий product semantics, model, behavior, data boundary и public API одной ответственности. - -## Группа - -**Group** - навигационная папка, классифицирующая модули или другие группы. Группа не является модулем, не имеет public API и не владеет runtime. - -## Composition - -**Composition** - product module, связывающий public APIs и technical capabilities в page, route, layout, screen, widget или другой application flow. - -## Scope owner - -**Scope owner** - composition, request setup, provider setup или test setup, которое выбирает runtime instances и resources, их lifetime, activation и cleanup. - -## Segment - -**Segment** - внутренняя папка модуля, группирующая файлы по роли, например `hooks`, `services`, `types`, `styles` или `lib`. - -## Компонент - -**Component** - presentation unit внутри владеющего module. Компонент не является самостоятельным архитектурным owner и не выбирает application dependencies самостоятельно. - -## Продуктовые данные - -**Product data** - данные, состояние и outcomes, имеющие смысл в предметной области продукта. Transport DTO, raw SDK response и browser storage schema не являются product model автоматически. - -## Runtime dependency - -**Runtime dependency** - dependency, необходимая выполняемому коду: API другого объекта, external source, store, query runtime, event source, clock, environment или platform capability. - -`import type` не создаёт runtime dependency, но может создавать статическую связанность contracts. - -Термины, вводимые `SLM Advanced` или `SLM Pro`, определяются и имеют нормативную силу только внутри соответствующего overlay. diff --git a/docs/ru/specification/testing-and-conformance.md b/docs/ru/specification/testing-and-conformance.md deleted file mode 100644 index d29c5b2..0000000 --- a/docs/ru/specification/testing-and-conformance.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -title: Тестирование и соответствие -status: draft -normative: true ---- - -# Тестирование и Соответствие - -Тесты проверяют public boundaries и runtime risks каждого owner. Base SLM не требует создавать неиспользуемые архитектурные конструкции ради тестовой формы. - -## Risk-based tests - -**SLM-BASE-TEST-018 - ОБЯЗАН.** Tests изменённого module должны покрывать применимые риски его public behavior, data boundaries и lifecycle. - -Типичные риски: - -- public behavior; -- malformed external data; -- rejected dependencies; -- state transitions; -- lifecycle activation и cleanup; -- request и identity isolation; -- client/server boundary; -- отсутствие import-time I/O. - -**SLM-BASE-TEST-008 - ОБЯЗАН.** Client/server import boundary должна проверяться инструментом, понимающим реальный framework module graph, если application имеет раздельные environment entries. DOM unit test не заменяет production build probe. - -Mode-specific test suites принадлежат overlay, который вводит соответствующие конструкции. - -## Architecture conformance - -Типичные mechanically enforceable checks: - -- направление imports; -- deep imports; -- public entrypoints; -- runtime cycles; -- заявленный overlay и его rule set; -- unique rule IDs документации; -- generated artifacts, если они используются. - -**SLM-BASE-TEST-011 - ЗАПРЕЩЕНО.** Документированное правило не считается mechanically enforced, если repository tooling его фактически не проверяет. - -## Единица соответствия - -**SLM-BASE-TEST-014 - ОБЯЗАН.** Application соответствует base SLM, если выполняет все base-правила. Соответствие заявленному overlay оценивается как base-правила с учётом точного scope каждой замены плюс полный rule set выбранного overlay. - -**SLM-BASE-TEST-015 - ОБЯЗАН.** Изменение соответствует заявленной архитектуре, если новые и изменённые modules не создают новых нарушений применимых base-правил или правил выбранного overlay и проходят существующие checks. - -**SLM-BASE-TEST-016 - ОБЯЗАН.** Отступление от правила `СЛЕДУЕТ` должно быть зафиксировано в архитектурном review или принятом decision с указанием причины и scope. - -**SLM-BASE-TEST-017 - ОБЯЗАН.** Manual conformance и mechanical enforcement должны различаться явно; отсутствие автоматической проверки не отменяет применимое нормативное правило. - -## Completion gate - -**SLM-BASE-TEST-012 - ОБЯЗАН.** Изменение считается завершённым только после выполнения ближайших tests, typecheck, lint, build и architecture checks, существующих в repository. - -**SLM-BASE-TEST-013 - ОБЯЗАН.** Невыполненная проверка и остаточный риск должны быть явно указаны в результате работы. diff --git a/draft-rules.js b/draft-rules.js new file mode 100644 index 0000000..84877b2 --- /dev/null +++ b/draft-rules.js @@ -0,0 +1,219 @@ +import { readdir, readFile } from 'node:fs/promises' +import { dirname, join, relative, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const rootDirectory = dirname(fileURLToPath(import.meta.url)) +const draftDirectory = join(rootDirectory, 'DRAFT') +const rulesDirectory = join(draftDirectory, 'rules') +const ruleCodeSource = 'SLM-L\\d+-[A-Z][A-Z_]{1,31}-[AR]\\d{3}' +const ruleHeadingPattern = new RegExp(`^###\\s+(?SLM-L(?\\d+)-(?[A-Z][A-Z_]{1,31})-(?[AR])(?\\d{3}))$`) +const ruleCodePattern = new RegExp(`\\b${ruleCodeSource}\\b`, 'g') +const ruleReferencePattern = new RegExp( + '\\[`(?' + ruleCodeSource + ')`\\]\\((?[^)\\s]+)\\)', + 'g', +) +const ruleTitlePattern = /^>\s+\*\*(?\S(?:.*\S)?)\*\*\s*$/ +const ruleDescriptionPattern = /^>\s+(?<description>\S(?:.*\S)?)\s*$/ +const quoteSeparatorPattern = /^>\s*$/ + +const getMarkdownFiles = async (directory) => { + const entries = await readdir(directory, { withFileTypes: true }) + const files = [] + + for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) { + const path = join(directory, entry.name) + + if (entry.isDirectory()) { + files.push(...(await getMarkdownFiles(path))) + } else if (entry.isFile() && entry.name.endsWith('.md')) { + files.push(path) + } + } + + return files +} + +const visitMarkdownLines = (lines, visitor) => { + let fence = null + + for (const [index, line] of lines.entries()) { + const fenceMatch = line.match(/^\s*(`{3,}|~{3,})/) + + if (fenceMatch) { + const marker = fenceMatch[1] + + if (fence === null) { + fence = marker + } else if (marker[0] === fence[0] && marker.length >= fence.length) { + fence = null + } + + continue + } + + if (fence !== null) { + continue + } + + visitor(line, index) + } +} + +const parseRules = async (file) => { + const content = await readFile(file, 'utf8') + const lines = content.split(/\r?\n/) + const rules = [] + + visitMarkdownLines(lines, (line, index) => { + const match = line.match(ruleHeadingPattern) + + if (match?.groups) { + const source = `${relative(rootDirectory, file)}:${index + 1}` + const titleMatch = lines[index + 2]?.match(ruleTitlePattern) + const descriptionMatch = lines[index + 4]?.match(ruleDescriptionPattern) + + if (lines[index + 1]?.trim() !== '') { + throw new Error(`SLM rule ${match.groups.code} must be followed by a blank line at ${source}`) + } + + if (!titleMatch?.groups) { + throw new Error(`SLM rule ${match.groups.code} must have a one-line bold title in blockquote at ${source}`) + } + + if (!quoteSeparatorPattern.test(lines[index + 3] ?? '')) { + throw new Error(`SLM rule ${match.groups.code} must separate title and description with a quoted blank line at ${source}`) + } + + if (!descriptionMatch?.groups || /^>/.test(lines[index + 5] ?? '')) { + throw new Error(`SLM rule ${match.groups.code} must have a one-line blockquote description at ${source}`) + } + + rules.push({ + code: match.groups.code, + classification: match.groups.classification, + description: descriptionMatch.groups.description.trim(), + file, + level: Number(match.groups.level), + number: Number(match.groups.number), + title: titleMatch.groups.title.trim(), + source, + }) + } else if (/^#{1,6}\s+SLM-L/.test(line)) { + throw new Error(`Invalid SLM rule heading at ${relative(rootDirectory, file)}:${index + 1}`) + } + }) + + return rules +} + +const parseReferences = async (file) => { + const content = await readFile(file, 'utf8') + const lines = content.split(/\r?\n/) + const references = [] + + visitMarkdownLines(lines, (line, index) => { + const source = `${relative(rootDirectory, file)}:${index + 1}` + + if (/^#{1,6}\s+SLM-L/.test(line)) { + throw new Error(`SLM rule declaration is only allowed in DRAFT/rules: ${source}`) + } + + const codeMatches = [...line.matchAll(ruleCodePattern)] + const linkMatches = [...line.matchAll(ruleReferencePattern)] + + for (const codeMatch of codeMatches) { + const linkMatch = linkMatches.find((candidate) => ( + candidate.groups?.code === codeMatch[0] + && codeMatch.index >= candidate.index + && codeMatch.index < candidate.index + candidate[0].length + )) + + if (!linkMatch?.groups) { + throw new Error(`SLM rule reference must be a Markdown link at ${source}: ${codeMatch[0]}`) + } + } + + for (const linkMatch of linkMatches) { + const { code, target } = linkMatch.groups + const hashIndex = target.lastIndexOf('#') + + if (hashIndex < 1 || target.slice(hashIndex + 1) !== code.toLowerCase()) { + throw new Error(`Invalid anchor for SLM rule ${code} at ${source}`) + } + + references.push({ + code, + file, + source, + targetFile: resolve(dirname(file), target.slice(0, hashIndex)), + }) + } + }) + + return references +} + +const printSection = (title, rules) => { + console.log(`${title} (${rules.length})`) + + for (const [index, rule] of rules.entries()) { + console.log(`${rule.code}: ${rule.title}`) + console.log(` ${rule.description}`) + + if (index < rules.length - 1) { + console.log() + } + } +} + +const ruleFiles = await getMarkdownFiles(rulesDirectory) +const draftFiles = (await getMarkdownFiles(draftDirectory)) + .filter((file) => !ruleFiles.includes(file)) +const rules = (await Promise.all(ruleFiles.map(parseRules))).flat() +const rulesByCode = new Map() + +for (const rule of rules) { + const duplicate = rulesByCode.get(rule.code) + + if (duplicate) { + throw new Error(`Duplicate SLM rule ${rule.code}: ${duplicate.source}, ${rule.source}`) + } + + rulesByCode.set(rule.code, rule) +} + +const references = (await Promise.all(draftFiles.map(parseReferences))).flat() +const referencedCodes = new Set() + +for (const reference of references) { + const rule = rulesByCode.get(reference.code) + + if (!rule) { + throw new Error(`Unknown SLM rule ${reference.code} at ${reference.source}`) + } + + if (reference.targetFile !== rule.file) { + throw new Error(`SLM rule ${reference.code} points to a non-canonical file at ${reference.source}`) + } + + referencedCodes.add(reference.code) +} + +for (const rule of rules) { + if (!referencedCodes.has(rule.code)) { + throw new Error(`SLM rule ${rule.code} is not referenced by any draft`) + } +} + +rules.sort((left, right) => ( + left.level - right.level + || left.number - right.number + || left.code.localeCompare(right.code) +)) + +const automaticRules = rules.filter((rule) => rule.classification === 'A') +const reviewRules = rules.filter((rule) => rule.classification === 'R') + +printSection('АВТОМАТИЧЕСКИЕ', automaticRules) +console.log() +printSection('ДЛЯ РЕВЬЮ', reviewRules) diff --git a/examples/demo-backend/.env.example b/examples/demo-backend/.env.example new file mode 100644 index 0000000..402a5f4 --- /dev/null +++ b/examples/demo-backend/.env.example @@ -0,0 +1,10 @@ +SIMPLE_PORT=3001 +COMPLEX_PORT=3002 +JWT_ACCESS_SECRET=demo-access-secret-change-me +JWT_REFRESH_SECRET=demo-refresh-secret-change-me +JWT_ACCESS_TTL=60s +JWT_REFRESH_TTL=7d +COOKIE_SESSION_TTL_MS=1800000 +COOKIE_SECURE=false +MOCK_SLOW_DELAY_MS=1500 +MOCK_TIMEOUT_DELAY_MS=30000 diff --git a/examples/demo-backend/.gitignore b/examples/demo-backend/.gitignore new file mode 100644 index 0000000..196887f --- /dev/null +++ b/examples/demo-backend/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +coverage/ +.env +npm-debug.log* +*.tsbuildinfo diff --git a/examples/demo-backend/README.md b/examples/demo-backend/README.md new file mode 100644 index 0000000..b3ab070 --- /dev/null +++ b/examples/demo-backend/README.md @@ -0,0 +1,117 @@ +# Demo backend + +An isolated NestJS package containing two independently launched HTTP applications and two OpenAPI contracts. Data is deterministic and stored in memory; no database or external service is required. + +## Applications + +| Application | Port | Authentication | Swagger UI | OpenAPI JSON | +| ----------- | -----: | ------------------------------ | ---------------------------- | ------------------------------------ | +| Simple | `3001` | JWT access/refresh | `http://localhost:3001/docs` | `http://localhost:3001/openapi.json` | +| Complex | `3002` | HttpOnly cookie session + CSRF | `http://localhost:3002/docs` | `http://localhost:3002/openapi.json` | + +Committed specifications are generated at `openapi/simple.json` and `openapi/complex.json`. + +## Requirements + +- Node.js 20 or newer +- npm 10 or newer + +## Start + +```bash +npm install +npm run start:dev +``` + +Run only one application: + +```bash +npm run dev:simple +npm run dev:complex +``` + +Production-style build and start: + +```bash +npm run build +npm run start:simple +npm run start:complex +``` + +## Demo users + +All passwords are `demo1234`. + +### Simple API + +| Email | Role | +| --------------------- | -------- | +| `admin@demo.local` | admin | +| `customer@demo.local` | customer | + +### Complex API + +| Email | Role | Organizations | +| ---------------------- | ------- | ------------------------ | +| `admin@complex.demo` | admin | `org-acme`, `org-globex` | +| `manager@complex.demo` | manager | `org-acme` | +| `support@complex.demo` | support | `org-acme` | +| `viewer@complex.demo` | viewer | `org-acme` | + +## Simple authentication example + +```bash +curl -s http://localhost:3001/api/v1/auth/login \ + -H 'Content-Type: application/json' \ + -d '{"email":"admin@demo.local","password":"demo1234"}' +``` + +Use `data.tokens.accessToken` as a Bearer token. + +## Complex authentication example + +```bash +curl -i -c /tmp/demo-cookies.txt http://localhost:3002/api/v1/auth/login \ + -H 'Content-Type: application/json' \ + -d '{"email":"admin@complex.demo","password":"demo1234"}' +``` + +The response body contains `data.csrfToken`. Protected reads require cookies and tenant context: + +```bash +curl -b /tmp/demo-cookies.txt http://localhost:3002/api/v1/products \ + -H 'X-Organization-Id: org-acme' +``` + +Mutations also require `X-CSRF-Token` with the value returned by login. + +## OpenAPI + +Generate and validate both specifications: + +```bash +npm run openapi:generate +npm run openapi:validate +``` + +The validator checks OpenAPI validity, unique operation IDs, expected security schemes and route isolation between the two applications. + +## Verification + +```bash +npm run typecheck +npm run build +npm run test:e2e +npm run openapi:generate +npm run openapi:validate +``` + +## Built-in frontend cases + +The intentionally supported cases are documented in [docs/CASES.md](./docs/CASES.md). They include controlled latency and errors, JWT refresh races, cookie expiration, CSRF, RBAC, tenant switching, offset and cursor pagination, ETag, optimistic locking, idempotency, background jobs, multipart files, polymorphic DTOs, audit events and realtime reconnection/deduplication. + +The Socket.IO event contract is documented separately in [docs/WEBSOCKET.md](./docs/WEBSOCKET.md). + +## Important limitation + +This is a frontend architecture fixture, not a production identity or commerce service. Passwords, sessions, files and mutations live only in process memory. Restart or `POST /api/v1/testing/reset` restores deterministic data. diff --git a/examples/demo-backend/docs/CASES.md b/examples/demo-backend/docs/CASES.md new file mode 100644 index 0000000..aed43ff --- /dev/null +++ b/examples/demo-backend/docs/CASES.md @@ -0,0 +1,133 @@ +# Frontend architecture cases + +This document is the contract for behaviors intentionally built into the demo backend. The APIs are deterministic: reset state before a demo with `POST /api/v1/testing/reset`. + +## Project-size mapping + +| Frontend type | Recommended API surface | +| -------------------- | ------------------------------------------------------------------------------------ | +| Landing or small SPA | Public Simple products and categories | +| Medium application | Full Simple API with JWT, user profile and orders | +| Large application | Complex API with cookie session, tenant context, RBAC, jobs, files and realtime chat | + +## Controlled network scenarios + +Send `X-Demo-Scenario` with any HTTP request. The response repeats the selected value in `X-Demo-Scenario`. + +| Value | Behavior | Frontend concern | +| --------------- | --------------------------------------------- | --------------------------------------- | +| `normal` | Normal deterministic response | Happy path | +| `slow` | Delays for `MOCK_SLOW_DELAY_MS` | Loading states, cancellation, skeletons | +| `timeout` | Delays for `MOCK_TIMEOUT_DELAY_MS` | Client timeout and abort handling | +| `server-error` | Returns `500` | Error boundaries and retry UX | +| `rate-limited` | Returns `429` and `Retry-After: 3` | Backoff and retry policy | +| `empty` | Changes a list response to a valid empty page | Empty states | +| `expired-auth` | Protected endpoint returns `401` | Refresh or re-login flow | +| `forbidden` | Protected endpoint returns `403` | Permission UI | +| `conflict` | Mutation returns `409` | Conflict UX and rollback | +| `large-dataset` | Expands a list response to 250 items | Rendering and virtualization | + +Scenario effects are request-local. They do not introduce random failures or make automated tests flaky. + +## Authentication cases + +### Simple JWT + +- Access and refresh tokens are returned by `POST /api/v1/auth/login`. +- Access tokens are sent as `Authorization: Bearer <token>`. +- Refresh tokens rotate. Reusing an already rotated refresh token returns `401 REFRESH_TOKEN_REUSED`. +- Logout revokes the supplied refresh token and is idempotent. +- Access and refresh lifetimes are configurable through environment variables. +- Protected requests support forced `401` and `403` scenarios. + +### Complex cookie session + +- Login sets HttpOnly `demo_session` and readable `demo_csrf` cookies. +- Browser requests must use `credentials: 'include'`. +- Mutations additionally send the `demo_csrf` value in `X-CSRF-Token`. +- Session refresh rotates both the session ID and CSRF token. +- `POST /api/v1/testing/session/expire` expires the current session without changing frontend state. +- Roles can change while the session remains active. +- Socket.IO authenticates using the same `demo_session` cookie. + +## Data fetching cases + +| Case | Endpoint example | +| -------------------- | ------------------------------------------------------ | +| Offset pagination | Simple products, customers, audit events | +| Cursor pagination | Complex products, orders, notifications, chat messages | +| Search and filtering | Products and customers | +| Sorting | Simple products | +| Nested resources | Organization members and conversation messages | +| Nullable values | Avatar, category parent, publish date, file/job result | +| Decimal strings | Complex prices, discounts and totals | +| Polymorphic union | Order, inventory and system notification payloads | +| Tree data | Complex categories with parent/child IDs | +| Large seed | `POST /api/v1/testing/seed/large` | + +## Cache case + +Product details return a weak `ETag`. Repeat the request with `If-None-Match`; unchanged data returns `304 Not Modified`. Product updates increment `version` and produce a new ETag. + +## Mutation and concurrency cases + +- Simple and Complex product updates require the last-read `version`. +- Inventory adjustments require `version` and reject negative stock. +- Stale writes return `409` with a stable error code. +- Complex order creation requires `Idempotency-Key`. +- Repeating the same order request and key returns the original order instead of creating a duplicate. +- Invalid nested forms return field-oriented validation details. +- Order cancellation validates allowed state transitions. +- Mutations are added to the Complex audit log. + +## RBAC and multitenancy cases + +- Complex domain routes require `X-Organization-Id`. +- Missing tenant context returns `400 ORGANIZATION_REQUIRED`. +- A tenant unavailable to the current user returns `403 ORGANIZATION_FORBIDDEN`. +- Admin and manager can mutate catalog/inventory and start exports. +- Support can create and cancel orders but cannot change products. +- Viewer is read-only. +- Test users and memberships are documented in the main README. + +## Background work + +`POST /api/v1/exports/orders` returns `202` and a job. Poll `GET /api/v1/jobs/:id` to observe: + +```text +pending -> processing -> completed +``` + +Progress and `resultUrl` change over time. Downloading the result before completion returns `409 JOB_NOT_COMPLETED`. + +## Files + +- Multipart upload accepts a `file` field up to 5 MiB. +- Metadata and bytes remain in memory until reset or restart. +- Download returns binary content and `Content-Disposition`. +- The API supports missing-file validation, loading progress and cancellation testing. + +## Realtime chat + +- REST supplies conversation and cursor-paginated message history. +- Socket.IO supplies joins, typing state and messages. +- `clientMessageId` deduplicates retried sends. +- Invalid or expired cookie sessions are disconnected. +- REST-created messages are also broadcast to the Socket.IO room. +- The complete event contract is in [WEBSOCKET.md](./WEBSOCKET.md). + +## Observability and errors + +Every HTTP response exposes `X-Request-Id`, `X-Response-Time` and `X-Demo-Scenario`. A frontend can supply its own `X-Request-Id`. Error bodies include that request ID, a stable machine-readable `code`, human-readable `message`, field `details`, timestamp and path. + +## Reset behavior + +| Endpoint | Effect | +| ------------------------------------- | ------------------------------------- | +| `POST /api/v1/testing/reset` | Restores the small deterministic seed | +| `POST /api/v1/testing/seed/small` | Loads normal fixtures | +| `POST /api/v1/testing/seed/large` | Loads 250 products and customers | +| `POST /api/v1/testing/users/:id/role` | Changes access without a new login | +| `POST /api/v1/testing/session/expire` | Expires the current Complex session | + +All mutations are process-local and disappear after restart. diff --git a/examples/demo-backend/docs/WEBSOCKET.md b/examples/demo-backend/docs/WEBSOCKET.md new file mode 100644 index 0000000..b61dfd1 --- /dev/null +++ b/examples/demo-backend/docs/WEBSOCKET.md @@ -0,0 +1,79 @@ +# Socket.IO chat contract + +## Connection + +Connect to the Complex API namespace: + +```text +http://localhost:3002/chat +``` + +The browser must already have a valid `demo_session` cookie and use credentials. Both `websocket` and `polling` transports are enabled. + +```ts +import { io } from "socket.io-client"; + +const socket = io("http://localhost:3002/chat", { + withCredentials: true, +}); +``` + +An invalid session receives `chat:error` and is disconnected. + +## Client events + +### `chat:join` + +```json +{ + "organizationId": "org-acme", + "conversationId": "conversation-support" +} +``` + +Response event: `chat:joined`. + +### `chat:leave` + +Uses the same payload. Response event: `chat:left`. + +### `message:send` + +```json +{ + "organizationId": "org-acme", + "conversationId": "conversation-support", + "text": "Can you check this order?", + "clientMessageId": "frontend-generated-uuid" +} +``` + +Response event: `message:ack`. Room broadcast: `message:created`. + +Sending the same `clientMessageId` again returns the existing message. Frontends should also deduplicate incoming `message:created` by server message `id`. + +### `typing:start` and `typing:stop` + +Use the `chat:join` payload. Other room members receive `typing:started` or `typing:stopped`: + +```json +{ + "conversationId": "conversation-support", + "userId": "complex-user-support" +} +``` + +## Error event + +`chat:error` always contains a stable shape: + +```json +{ + "code": "CHAT_OPERATION_FAILED", + "message": "Conversation not found." +} +``` + +## Reconnect expectations + +After reconnect, the frontend should join active conversations again and reload messages after its last known cursor. A session expired by `POST /api/v1/testing/session/expire` rejects the next Socket.IO connection. diff --git a/examples/demo-backend/nest-cli.json b/examples/demo-backend/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/examples/demo-backend/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/examples/demo-backend/openapi/complex.json b/examples/demo-backend/openapi/complex.json new file mode 100644 index 0000000..468a407 --- /dev/null +++ b/examples/demo-backend/openapi/complex.json @@ -0,0 +1,7467 @@ +{ + "openapi": "3.0.0", + "paths": { + "/api/v1/health": { + "get": { + "operationId": "ComplexHealth_health", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponseDto" + } + } + } + } + }, + "summary": "Check Complex API availability", + "tags": [ + "Health" + ] + } + }, + "/api/v1/auth/login": { + "post": { + "operationId": "ComplexAuth_login", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComplexLoginDto" + } + } + } + }, + "responses": { + "200": { + "headers": { + "Set-Cookie": { + "description": "Sets demo_session (HttpOnly) and demo_csrf cookies.", + "schema": { + "type": "string" + } + } + }, + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CookieSessionResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "summary": "Login and establish HttpOnly cookie session", + "tags": [ + "Auth" + ] + } + }, + "/api/v1/auth/refresh": { + "post": { + "operationId": "ComplexAuth_refresh", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CookieSessionResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "csrf": [] + }, + { + "cookieSession": [] + } + ], + "summary": "Rotate the current cookie session and CSRF token", + "tags": [ + "Auth" + ] + } + }, + "/api/v1/auth/logout": { + "post": { + "operationId": "ComplexAuth_logout", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + } + ], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "csrf": [] + }, + { + "cookieSession": [] + } + ], + "summary": "Revoke cookie session and clear authentication cookies", + "tags": [ + "Auth" + ] + } + }, + "/api/v1/users/me": { + "get": { + "operationId": "ComplexUsers_me", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComplexUserResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "Get the authenticated user and available organizations", + "tags": [ + "Users" + ] + } + }, + "/api/v1/users": { + "get": { + "operationId": "ComplexUsers_list", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComplexUsersResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "List users in the current tenant", + "tags": [ + "Users" + ] + } + }, + "/api/v1/organizations": { + "get": { + "operationId": "Organizations_list", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationsResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "List organizations available to the current user", + "tags": [ + "Organizations" + ] + } + }, + "/api/v1/organizations/{id}": { + "get": { + "operationId": "Organizations_get", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "Get one available organization", + "tags": [ + "Organizations" + ] + } + }, + "/api/v1/organizations/{id}/members": { + "get": { + "operationId": "Organizations_members", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MembersResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "List organization members and their roles", + "tags": [ + "Organizations" + ] + } + }, + "/api/v1/products": { + "get": { + "operationId": "ComplexProducts_list", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "schema": { + "minimum": 1, + "maximum": 100, + "default": 20, + "allOf": [ + { + "$ref": "#/components/schemas/Object" + } + ] + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "schema": { + "example": "complex-product-mouse", + "type": "string" + } + }, + { + "name": "search", + "required": false, + "in": "query", + "schema": { + "example": "keyboard", + "type": "string" + } + }, + { + "name": "status", + "required": false, + "in": "query", + "schema": { + "type": "string", + "enum": [ + "draft", + "active", + "archived" + ] + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComplexProductsResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "List tenant products using cursor pagination", + "tags": [ + "Products" + ] + }, + "post": { + "operationId": "ComplexProducts_create", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateComplexProductDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComplexProductResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + }, + { + "csrf": [] + } + ], + "summary": "Create a tenant product with variants", + "tags": [ + "Products" + ] + } + }, + "/api/v1/products/{id}": { + "get": { + "operationId": "ComplexProducts_get", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComplexProductResponseDto" + } + } + } + }, + "304": { + "description": "" + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "Get a rich product with variants and ETag support", + "tags": [ + "Products" + ] + }, + "patch": { + "operationId": "ComplexProducts_update", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateComplexProductDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComplexProductResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "409": { + "description": "Business or optimistic-lock conflict.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + }, + { + "csrf": [] + } + ], + "summary": "Update a product using optimistic locking", + "tags": [ + "Products" + ] + } + }, + "/api/v1/categories": { + "get": { + "operationId": "ComplexCatalog_categories", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComplexCategoriesResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "List a recursive category tree", + "tags": [ + "Catalog" + ] + } + }, + "/api/v1/brands": { + "get": { + "operationId": "ComplexCatalog_brands", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BrandsResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "List product brands", + "tags": [ + "Catalog" + ] + } + }, + "/api/v1/warehouses": { + "get": { + "operationId": "ComplexInventory_warehouses", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WarehousesResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "List tenant warehouses", + "tags": [ + "Inventory" + ] + } + }, + "/api/v1/inventory": { + "get": { + "operationId": "ComplexInventory_inventory", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + }, + { + "name": "productId", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InventoryResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "List inventory with optional product filter", + "tags": [ + "Inventory" + ] + } + }, + "/api/v1/inventory/{id}/adjust": { + "post": { + "operationId": "ComplexInventory_adjust", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdjustInventoryDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InventoryItemResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "409": { + "description": "Business or optimistic-lock conflict.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + }, + { + "csrf": [] + } + ], + "summary": "Adjust stock using optimistic locking", + "tags": [ + "Inventory" + ] + } + }, + "/api/v1/customers": { + "get": { + "operationId": "Customers_list", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + }, + { + "name": "page", + "required": false, + "in": "query", + "schema": { + "minimum": 1, + "default": 1, + "allOf": [ + { + "$ref": "#/components/schemas/Object" + } + ] + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "schema": { + "minimum": 1, + "maximum": 100, + "default": 20, + "allOf": [ + { + "$ref": "#/components/schemas/Object" + } + ] + } + }, + { + "name": "search", + "required": false, + "in": "query", + "schema": { + "example": "ada", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomersResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "List customers using offset pagination and search", + "tags": [ + "Customers" + ] + } + }, + "/api/v1/customers/{id}": { + "get": { + "operationId": "Customers_get", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomerResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "Get one customer with nested address", + "tags": [ + "Customers" + ] + } + }, + "/api/v1/orders": { + "get": { + "operationId": "ComplexOrders_list", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "schema": { + "minimum": 1, + "maximum": 100, + "default": 20, + "allOf": [ + { + "$ref": "#/components/schemas/Object" + } + ] + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "schema": { + "example": "complex-order-001", + "type": "string" + } + }, + { + "name": "status", + "required": false, + "in": "query", + "schema": { + "type": "string", + "enum": [ + "draft", + "awaiting-payment", + "paid", + "fulfillment", + "shipped", + "cancelled" + ] + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComplexOrdersResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "List tenant orders using cursor pagination", + "tags": [ + "Orders" + ] + }, + "post": { + "operationId": "ComplexOrders_create", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + }, + { + "name": "idempotency-key", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "description": "Repeating a request with the same key returns the original order.", + "required": true, + "schema": { + "type": "string", + "example": "checkout-6c5f92ad" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateComplexOrderDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComplexOrderResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "409": { + "description": "Business or optimistic-lock conflict.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + }, + { + "csrf": [] + } + ], + "summary": "Create an order idempotently", + "tags": [ + "Orders" + ] + } + }, + "/api/v1/orders/{id}": { + "get": { + "operationId": "ComplexOrders_get", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComplexOrderResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "Get one order and its state", + "tags": [ + "Orders" + ] + } + }, + "/api/v1/orders/{id}/cancel": { + "post": { + "operationId": "ComplexOrders_cancel", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComplexOrderResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "409": { + "description": "Business or optimistic-lock conflict.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + }, + { + "csrf": [] + } + ], + "summary": "Apply a validated order state transition", + "tags": [ + "Orders" + ] + } + }, + "/api/v1/payments": { + "get": { + "operationId": "Payments_payments", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "List payments for tenant orders", + "tags": [ + "Payments and promotions" + ] + } + }, + "/api/v1/promotions": { + "get": { + "operationId": "Payments_promotions", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PromotionsResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "List active and expired promotion contracts", + "tags": [ + "Payments and promotions" + ] + } + }, + "/api/v1/reviews": { + "get": { + "operationId": "Reviews_list", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + }, + { + "name": "productId", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReviewsResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "List reviews with moderation state", + "tags": [ + "Reviews" + ] + }, + "post": { + "operationId": "Reviews_create", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateReviewDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReviewResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + }, + { + "csrf": [] + } + ], + "summary": "Create a review in pending moderation state", + "tags": [ + "Reviews" + ] + } + }, + "/api/v1/notifications": { + "get": { + "operationId": "Notifications_list", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "schema": { + "minimum": 1, + "maximum": 100, + "default": 20, + "allOf": [ + { + "$ref": "#/components/schemas/Object" + } + ] + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "schema": { + "example": "notification-020", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationsResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "List polymorphic notifications using cursor pagination", + "tags": [ + "Notifications" + ] + } + }, + "/api/v1/notifications/{id}/read": { + "post": { + "operationId": "Notifications_markRead", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + }, + { + "csrf": [] + } + ], + "summary": "Mark a notification as read", + "tags": [ + "Notifications" + ] + } + }, + "/api/v1/files": { + "get": { + "operationId": "Files_list", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FilesResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "List uploaded file metadata", + "tags": [ + "Files" + ] + }, + "post": { + "operationId": "Files_upload", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "file" + ], + "properties": { + "file": { + "type": "string", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + }, + { + "csrf": [] + } + ], + "summary": "Upload a file up to 5 MiB and retain it in memory", + "tags": [ + "Files" + ] + } + }, + "/api/v1/files/{id}/download": { + "get": { + "operationId": "Files_download", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Binary file content.", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "Download an in-memory file", + "tags": [ + "Files" + ] + } + }, + "/api/v1/audit-events": { + "get": { + "operationId": "Audit_list", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + }, + { + "name": "page", + "required": false, + "in": "query", + "schema": { + "minimum": 1, + "default": 1, + "allOf": [ + { + "$ref": "#/components/schemas/Object" + } + ] + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "schema": { + "minimum": 1, + "maximum": 100, + "default": 20, + "allOf": [ + { + "$ref": "#/components/schemas/Object" + } + ] + } + }, + { + "name": "action", + "required": false, + "in": "query", + "schema": { + "example": "product.updated", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuditEventsResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "List immutable audit events using offset pagination", + "tags": [ + "Audit" + ] + } + }, + "/api/v1/exports/orders": { + "post": { + "operationId": "Jobs_startExport", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + } + ], + "responses": { + "202": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JobResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + }, + { + "csrf": [] + } + ], + "summary": "Start an asynchronous orders export", + "tags": [ + "Background jobs" + ] + } + }, + "/api/v1/jobs/{id}": { + "get": { + "operationId": "Jobs_getJob", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JobResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "Poll background job progress", + "tags": [ + "Background jobs" + ] + } + }, + "/api/v1/jobs/{id}/result": { + "get": { + "operationId": "Jobs_result", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Generated orders CSV.", + "content": { + "text/csv": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "text/csv": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "text/csv": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "text/csv": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "text/csv": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "409": { + "description": "Business or optimistic-lock conflict.", + "content": { + "text/csv": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "text/csv": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "text/csv": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "Download a completed export; returns 409 while processing", + "tags": [ + "Background jobs" + ] + } + }, + "/api/v1/conversations": { + "get": { + "operationId": "Chat_list", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationsResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "List conversations available to the current user", + "tags": [ + "Chat" + ] + } + }, + "/api/v1/conversations/{id}": { + "get": { + "operationId": "Chat_get", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "Get one conversation", + "tags": [ + "Chat" + ] + } + }, + "/api/v1/conversations/{id}/messages": { + "get": { + "operationId": "Chat_messages", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "schema": { + "minimum": 1, + "maximum": 100, + "default": 20, + "allOf": [ + { + "$ref": "#/components/schemas/Object" + } + ] + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "schema": { + "example": "notification-020", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessagesResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + } + ], + "summary": "Load message history using cursor pagination", + "tags": [ + "Chat" + ] + }, + "post": { + "operationId": "Chat_send", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "X-Organization-Id", + "in": "header", + "description": "Current tenant. The authenticated user must be a member.", + "required": true, + "schema": { + "type": "string", + "example": "org-acme" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SendMessageDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "cookieSession": [] + }, + { + "csrf": [] + } + ], + "summary": "Send an idempotent message through REST and broadcast it over Socket.IO", + "tags": [ + "Chat" + ] + } + }, + "/api/v1/testing/scenarios": { + "get": { + "operationId": "ComplexTesting_scenarios", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScenariosResponseDto" + } + } + } + } + }, + "summary": "List deterministic X-Demo-Scenario values", + "tags": [ + "Testing" + ] + } + }, + "/api/v1/testing/reset": { + "post": { + "operationId": "ComplexTesting_reset", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestingActionResponseDto" + } + } + } + } + }, + "summary": "Reset all Complex API data and active sessions", + "tags": [ + "Testing" + ] + } + }, + "/api/v1/testing/seed/{preset}": { + "post": { + "operationId": "ComplexTesting_seed", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "preset", + "required": true, + "in": "path", + "schema": { + "enum": [ + "small", + "large" + ], + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestingActionResponseDto" + } + } + } + } + }, + "summary": "Select a small or large deterministic dataset", + "tags": [ + "Testing" + ] + } + }, + "/api/v1/testing/session/expire": { + "post": { + "operationId": "ComplexTesting_expireSession", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestingActionResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "csrf": [] + }, + { + "cookieSession": [] + } + ], + "summary": "Expire the current cookie session after this response", + "tags": [ + "Testing" + ] + } + }, + "/api/v1/testing/users/{userId}/role": { + "post": { + "operationId": "ComplexTesting_changeRole", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeComplexRoleDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComplexUserResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "summary": "Change a role while sessions remain active", + "tags": [ + "Testing" + ] + } + } + }, + "info": { + "title": "Demo Complex API", + "description": "Cookie-session, multitenant and realtime API for large frontend applications.", + "version": "1.0.0", + "contact": {} + }, + "tags": [], + "servers": [ + { + "url": "http://localhost:3002", + "description": "Local development" + } + ], + "components": { + "securitySchemes": { + "cookieSession": { + "type": "apiKey", + "in": "cookie", + "name": "demo_session" + }, + "csrf": { + "type": "apiKey", + "in": "header", + "name": "X-CSRF-Token", + "description": "Required for authenticated mutations." + } + }, + "schemas": { + "HealthDataDto": { + "type": "object", + "properties": { + "application": { + "type": "string", + "enum": [ + "simple", + "complex" + ], + "example": "simple" + }, + "status": { + "type": "string", + "enum": [ + "ok" + ], + "example": "ok" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "string", + "example": "1.0.0" + } + }, + "required": [ + "application", + "status", + "timestamp", + "version" + ] + }, + "HealthResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/HealthDataDto" + } + }, + "required": [ + "data" + ] + }, + "ComplexLoginDto": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "example": "admin@complex.demo" + }, + "password": { + "type": "string", + "format": "password", + "example": "demo1234", + "minLength": 8 + } + }, + "required": [ + "email", + "password" + ] + }, + "ComplexUserDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "complex-user-admin" + }, + "email": { + "type": "string", + "format": "email", + "example": "admin@complex.demo" + }, + "name": { + "type": "string", + "example": "Complex Admin" + }, + "role": { + "type": "string", + "enum": [ + "admin", + "manager", + "support", + "viewer" + ] + }, + "avatarUrl": { + "type": "object", + "nullable": true, + "example": "https://i.pravatar.cc/160?img=20" + }, + "organizationIds": { + "example": [ + "org-acme", + "org-globex" + ], + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "email", + "name", + "role", + "avatarUrl", + "organizationIds" + ] + }, + "CookieSessionDataDto": { + "type": "object", + "properties": { + "user": { + "$ref": "#/components/schemas/ComplexUserDto" + }, + "csrfToken": { + "type": "string", + "example": "5f2642ca-2ba4-4ee3-bf2e-d5d37a97686c", + "description": "Send this value in X-CSRF-Token for authenticated mutations." + }, + "expiresAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "user", + "csrfToken", + "expiresAt" + ] + }, + "CookieSessionResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/CookieSessionDataDto" + } + }, + "required": [ + "data" + ] + }, + "ErrorDetailDto": { + "type": "object", + "properties": { + "field": { + "type": "string", + "example": "email" + }, + "message": { + "type": "string", + "example": "must be an email" + }, + "code": { + "type": "string", + "example": "isEmail" + } + }, + "required": [ + "message" + ] + }, + "ErrorResponseDto": { + "type": "object", + "properties": { + "statusCode": { + "type": "number", + "example": 404 + }, + "code": { + "type": "string", + "example": "PRODUCT_NOT_FOUND" + }, + "message": { + "type": "string", + "example": "Product not found" + }, + "details": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ErrorDetailDto" + } + }, + "timestamp": { + "type": "string", + "format": "date-time", + "example": "2026-07-30T12:00:00.000Z" + }, + "path": { + "type": "string", + "example": "/api/v1/products/product-404" + }, + "requestId": { + "type": "string", + "example": "req-5c9f7a3d" + } + }, + "required": [ + "statusCode", + "code", + "message", + "details", + "timestamp", + "path", + "requestId" + ] + }, + "ComplexUserResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ComplexUserDto" + } + }, + "required": [ + "data" + ] + }, + "ComplexUsersResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ComplexUserDto" + } + } + }, + "required": [ + "data" + ] + }, + "OrganizationDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "org-acme" + }, + "name": { + "type": "string", + "example": "Acme Commerce" + }, + "plan": { + "type": "string", + "enum": [ + "starter", + "business", + "enterprise" + ] + }, + "timezone": { + "type": "string", + "example": "Europe/Berlin" + }, + "currency": { + "type": "string", + "enum": [ + "USD", + "EUR" + ], + "example": "USD" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "name", + "plan", + "timezone", + "currency", + "createdAt" + ] + }, + "OrganizationsResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrganizationDto" + } + } + }, + "required": [ + "data" + ] + }, + "OrganizationResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/OrganizationDto" + } + }, + "required": [ + "data" + ] + }, + "MemberDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "member-001" + }, + "organizationId": { + "type": "string", + "example": "org-acme" + }, + "userId": { + "type": "string", + "example": "complex-user-manager" + }, + "userName": { + "type": "string", + "example": "Complex Manager" + }, + "email": { + "type": "string", + "format": "email", + "example": "manager@complex.demo" + }, + "role": { + "type": "string", + "enum": [ + "admin", + "manager", + "support", + "viewer" + ] + }, + "status": { + "type": "string", + "enum": [ + "active", + "invited", + "suspended" + ], + "example": "active" + } + }, + "required": [ + "id", + "organizationId", + "userId", + "userName", + "email", + "role", + "status" + ] + }, + "MembersResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MemberDto" + } + } + }, + "required": [ + "data" + ] + }, + "Object": { + "type": "object", + "properties": {} + }, + "MoneyDto": { + "type": "object", + "properties": { + "amount": { + "type": "string", + "example": "129.90", + "pattern": "^\\d+\\.\\d{2}$", + "description": "Decimal string; never parse money as a floating-point number." + }, + "currency": { + "type": "string", + "enum": [ + "USD", + "EUR" + ], + "example": "USD" + } + }, + "required": [ + "amount", + "currency" + ] + }, + "ProductVariantDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "variant-keyboard-black" + }, + "sku": { + "type": "string", + "example": "KEYBOARD-BLACK-US" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "example": { + "color": "black", + "layout": "US" + } + }, + "price": { + "$ref": "#/components/schemas/MoneyDto" + } + }, + "required": [ + "id", + "sku", + "attributes", + "price" + ] + }, + "ComplexProductDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "complex-product-keyboard" + }, + "organizationId": { + "type": "string", + "example": "org-acme" + }, + "name": { + "type": "string", + "example": "Pro Mechanical Keyboard" + }, + "slug": { + "type": "string", + "example": "pro-mechanical-keyboard" + }, + "description": { + "type": "string", + "example": "Configurable keyboard sold in multiple variants." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "active", + "archived" + ] + }, + "categoryId": { + "type": "string", + "example": "complex-category-electronics" + }, + "brandId": { + "type": "string", + "example": "brand-northstar" + }, + "price": { + "$ref": "#/components/schemas/MoneyDto" + }, + "variants": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProductVariantDto" + } + }, + "tags": { + "example": [ + "featured", + "office" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "publishedAt": { + "type": "object", + "format": "date-time", + "nullable": true + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "number", + "example": 3, + "description": "Optimistic-lock version." + } + }, + "required": [ + "id", + "organizationId", + "name", + "slug", + "description", + "status", + "categoryId", + "brandId", + "price", + "variants", + "tags", + "publishedAt", + "createdAt", + "version" + ] + }, + "CursorMetaDto": { + "type": "object", + "properties": { + "limit": { + "type": "number", + "example": 20, + "minimum": 1 + }, + "nextCursor": { + "type": "object", + "nullable": true, + "example": "product-020" + }, + "hasMore": { + "type": "boolean", + "example": true + } + }, + "required": [ + "limit", + "hasMore" + ] + }, + "ComplexProductsResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ComplexProductDto" + } + }, + "meta": { + "$ref": "#/components/schemas/CursorMetaDto" + } + }, + "required": [ + "data", + "meta" + ] + }, + "ComplexProductResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ComplexProductDto" + } + }, + "required": [ + "data" + ] + }, + "CreateVariantDto": { + "type": "object", + "properties": { + "sku": { + "type": "string", + "example": "KEYBOARD-WHITE-US" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "example": { + "color": "white", + "layout": "US" + } + }, + "priceAmount": { + "type": "string", + "example": "139.90" + } + }, + "required": [ + "sku", + "attributes", + "priceAmount" + ] + }, + "CreateComplexProductDto": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Pro Mechanical Keyboard" + }, + "description": { + "type": "string", + "example": "Configurable keyboard sold in multiple variants." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "active", + "archived" + ], + "default": "draft" + }, + "categoryId": { + "type": "string", + "example": "complex-category-electronics" + }, + "brandId": { + "type": "string", + "example": "brand-northstar" + }, + "priceAmount": { + "type": "string", + "example": "129.90" + }, + "variants": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CreateVariantDto" + } + }, + "tags": { + "example": [ + "featured" + ], + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "name", + "description", + "status", + "categoryId", + "brandId", + "priceAmount", + "variants" + ] + }, + "UpdateComplexProductDto": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Pro Mechanical Keyboard" + }, + "description": { + "type": "string", + "example": "Configurable keyboard sold in multiple variants." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "active", + "archived" + ], + "default": "draft" + }, + "categoryId": { + "type": "string", + "example": "complex-category-electronics" + }, + "brandId": { + "type": "string", + "example": "brand-northstar" + }, + "priceAmount": { + "type": "string", + "example": "129.90" + }, + "variants": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CreateVariantDto" + } + }, + "tags": { + "example": [ + "featured" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "version": { + "type": "number", + "minimum": 1, + "example": 3, + "description": "Version last read by the frontend." + } + }, + "required": [ + "version" + ] + }, + "ComplexCategoryDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "complex-category-electronics" + }, + "organizationId": { + "type": "string", + "example": "org-acme" + }, + "name": { + "type": "string", + "example": "Electronics" + }, + "parentId": { + "type": "object", + "nullable": true, + "example": null + }, + "childIds": { + "example": [ + "complex-category-keyboards" + ], + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "organizationId", + "name", + "parentId", + "childIds" + ] + }, + "ComplexCategoriesResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ComplexCategoryDto" + } + } + }, + "required": [ + "data" + ] + }, + "BrandDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "brand-northstar" + }, + "organizationId": { + "type": "string", + "example": "org-acme" + }, + "name": { + "type": "string", + "example": "Northstar" + }, + "logoUrl": { + "type": "object", + "format": "uri", + "nullable": true + } + }, + "required": [ + "id", + "organizationId", + "name", + "logoUrl" + ] + }, + "BrandsResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BrandDto" + } + } + }, + "required": [ + "data" + ] + }, + "WarehouseDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "warehouse-berlin" + }, + "organizationId": { + "type": "string", + "example": "org-acme" + }, + "name": { + "type": "string", + "example": "Berlin Warehouse" + }, + "countryCode": { + "type": "string", + "example": "DE" + }, + "status": { + "type": "string", + "enum": [ + "active", + "maintenance" + ], + "example": "active" + } + }, + "required": [ + "id", + "organizationId", + "name", + "countryCode", + "status" + ] + }, + "WarehousesResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WarehouseDto" + } + } + }, + "required": [ + "data" + ] + }, + "InventoryItemDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "inventory-keyboard-berlin" + }, + "productId": { + "type": "string", + "example": "complex-product-keyboard" + }, + "variantId": { + "type": "string", + "example": "variant-keyboard-black" + }, + "warehouseId": { + "type": "string", + "example": "warehouse-berlin" + }, + "available": { + "type": "number", + "example": 42 + }, + "reserved": { + "type": "number", + "example": 5 + }, + "reorderPoint": { + "type": "number", + "example": 10 + }, + "version": { + "type": "number", + "example": 2 + } + }, + "required": [ + "id", + "productId", + "variantId", + "warehouseId", + "available", + "reserved", + "reorderPoint", + "version" + ] + }, + "InventoryResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InventoryItemDto" + } + } + }, + "required": [ + "data" + ] + }, + "AdjustInventoryDto": { + "type": "object", + "properties": { + "delta": { + "type": "number", + "example": -2, + "description": "Signed stock adjustment." + }, + "reason": { + "type": "string", + "example": "Damaged during delivery" + }, + "version": { + "type": "number", + "example": 2, + "description": "Version last read by the frontend." + } + }, + "required": [ + "delta", + "reason", + "version" + ] + }, + "InventoryItemResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/InventoryItemDto" + } + }, + "required": [ + "data" + ] + }, + "AddressDto": { + "type": "object", + "properties": { + "line1": { + "type": "string", + "example": "Friedrichstrasse 100" + }, + "line2": { + "type": "object", + "nullable": true, + "example": null + }, + "city": { + "type": "string", + "example": "Berlin" + }, + "postalCode": { + "type": "string", + "example": "10117" + }, + "countryCode": { + "type": "string", + "example": "DE" + } + }, + "required": [ + "line1", + "line2", + "city", + "postalCode", + "countryCode" + ] + }, + "CustomerDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "customer-ada" + }, + "organizationId": { + "type": "string", + "example": "org-acme" + }, + "name": { + "type": "string", + "example": "Ada Lovelace" + }, + "email": { + "type": "string", + "format": "email", + "example": "ada@example.test" + }, + "defaultAddress": { + "$ref": "#/components/schemas/AddressDto" + }, + "tags": { + "example": [ + "vip", + "newsletter" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "organizationId", + "name", + "email", + "defaultAddress", + "tags", + "createdAt" + ] + }, + "PageMetaDto": { + "type": "object", + "properties": { + "page": { + "type": "number", + "example": 1, + "minimum": 1 + }, + "limit": { + "type": "number", + "example": 20, + "minimum": 1 + }, + "total": { + "type": "number", + "example": 48, + "minimum": 0 + }, + "totalPages": { + "type": "number", + "example": 3, + "minimum": 0 + } + }, + "required": [ + "page", + "limit", + "total", + "totalPages" + ] + }, + "CustomersResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomerDto" + } + }, + "meta": { + "$ref": "#/components/schemas/PageMetaDto" + } + }, + "required": [ + "data", + "meta" + ] + }, + "CustomerResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/CustomerDto" + } + }, + "required": [ + "data" + ] + }, + "ComplexOrderItemDto": { + "type": "object", + "properties": { + "productId": { + "type": "string", + "example": "complex-product-keyboard" + }, + "variantId": { + "type": "string", + "example": "variant-keyboard-black" + }, + "name": { + "type": "string", + "example": "Pro Mechanical Keyboard" + }, + "quantity": { + "type": "number", + "example": 1 + }, + "unitPrice": { + "$ref": "#/components/schemas/MoneyDto" + } + }, + "required": [ + "productId", + "variantId", + "name", + "quantity", + "unitPrice" + ] + }, + "ComplexOrderDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "complex-order-001" + }, + "organizationId": { + "type": "string", + "example": "org-acme" + }, + "customerId": { + "type": "string", + "example": "customer-ada" + }, + "status": { + "type": "string", + "enum": [ + "draft", + "awaiting-payment", + "paid", + "fulfillment", + "shipped", + "cancelled" + ] + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ComplexOrderItemDto" + } + }, + "subtotal": { + "$ref": "#/components/schemas/MoneyDto" + }, + "discount": { + "$ref": "#/components/schemas/MoneyDto" + }, + "total": { + "$ref": "#/components/schemas/MoneyDto" + }, + "shippingAddress": { + "$ref": "#/components/schemas/AddressDto" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "number", + "example": 1 + } + }, + "required": [ + "id", + "organizationId", + "customerId", + "status", + "items", + "subtotal", + "discount", + "total", + "shippingAddress", + "createdAt", + "version" + ] + }, + "ComplexOrdersResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ComplexOrderDto" + } + }, + "meta": { + "$ref": "#/components/schemas/CursorMetaDto" + } + }, + "required": [ + "data", + "meta" + ] + }, + "ComplexOrderResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ComplexOrderDto" + } + }, + "required": [ + "data" + ] + }, + "CreateComplexOrderItemDto": { + "type": "object", + "properties": { + "productId": { + "type": "string", + "example": "complex-product-keyboard" + }, + "variantId": { + "type": "string", + "example": "variant-keyboard-black" + }, + "quantity": { + "type": "number", + "example": 1, + "minimum": 1, + "maximum": 50 + } + }, + "required": [ + "productId", + "variantId", + "quantity" + ] + }, + "CreateComplexOrderDto": { + "type": "object", + "properties": { + "customerId": { + "type": "string", + "example": "customer-ada" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CreateComplexOrderItemDto" + } + }, + "promotionCode": { + "type": "string", + "example": "WELCOME10" + } + }, + "required": [ + "customerId", + "items" + ] + }, + "PaymentDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "payment-001" + }, + "orderId": { + "type": "string", + "example": "complex-order-001" + }, + "status": { + "type": "string", + "enum": [ + "pending", + "succeeded", + "failed", + "refunded" + ], + "example": "succeeded" + }, + "method": { + "type": "string", + "enum": [ + "card", + "bank-transfer" + ], + "example": "card" + }, + "amount": { + "$ref": "#/components/schemas/MoneyDto" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "orderId", + "status", + "method", + "amount", + "createdAt" + ] + }, + "PaymentsResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentDto" + } + } + }, + "required": [ + "data" + ] + }, + "PromotionDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "promotion-welcome" + }, + "code": { + "type": "string", + "example": "WELCOME10" + }, + "type": { + "type": "string", + "enum": [ + "percentage", + "fixed" + ], + "example": "percentage" + }, + "value": { + "type": "string", + "example": "10.00" + }, + "validUntil": { + "type": "string", + "format": "date-time" + }, + "active": { + "type": "boolean", + "example": true + } + }, + "required": [ + "id", + "code", + "type", + "value", + "validUntil", + "active" + ] + }, + "PromotionsResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PromotionDto" + } + } + }, + "required": [ + "data" + ] + }, + "ReviewDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "review-001" + }, + "productId": { + "type": "string", + "example": "complex-product-keyboard" + }, + "customerId": { + "type": "string", + "example": "customer-ada" + }, + "rating": { + "type": "number", + "example": 5, + "minimum": 1, + "maximum": 5 + }, + "comment": { + "type": "string", + "example": "Excellent keyboard for daily development." + }, + "status": { + "type": "string", + "enum": [ + "pending", + "published", + "rejected" + ], + "example": "published" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "productId", + "customerId", + "rating", + "comment", + "status", + "createdAt" + ] + }, + "ReviewsResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReviewDto" + } + } + }, + "required": [ + "data" + ] + }, + "CreateReviewDto": { + "type": "object", + "properties": { + "productId": { + "type": "string", + "example": "complex-product-keyboard" + }, + "customerId": { + "type": "string", + "example": "customer-ada" + }, + "rating": { + "type": "number", + "minimum": 1, + "maximum": 5, + "example": 5 + }, + "comment": { + "type": "string", + "example": "Excellent keyboard for daily development." + } + }, + "required": [ + "productId", + "customerId", + "rating", + "comment" + ] + }, + "ReviewResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ReviewDto" + } + }, + "required": [ + "data" + ] + }, + "OrderNotificationPayloadDto": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "order" + ], + "example": "order" + }, + "orderId": { + "type": "string", + "example": "complex-order-001" + }, + "status": { + "type": "string", + "enum": [ + "paid", + "shipped", + "cancelled" + ], + "example": "shipped" + } + }, + "required": [ + "type", + "orderId", + "status" + ] + }, + "InventoryNotificationPayloadDto": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "inventory" + ], + "example": "inventory" + }, + "productId": { + "type": "string", + "example": "complex-product-keyboard" + }, + "remaining": { + "type": "number", + "example": 4 + } + }, + "required": [ + "type", + "productId", + "remaining" + ] + }, + "SystemNotificationPayloadDto": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "system" + ], + "example": "system" + }, + "text": { + "type": "string", + "example": "Scheduled maintenance begins at 02:00 UTC." + } + }, + "required": [ + "type", + "text" + ] + }, + "NotificationDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "notification-001" + }, + "kind": { + "type": "string", + "enum": [ + "order", + "inventory", + "system" + ] + }, + "title": { + "type": "string", + "example": "Order shipped" + }, + "payload": { + "oneOf": [ + { + "$ref": "#/components/schemas/OrderNotificationPayloadDto" + }, + { + "$ref": "#/components/schemas/InventoryNotificationPayloadDto" + }, + { + "$ref": "#/components/schemas/SystemNotificationPayloadDto" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "order": "#/components/schemas/OrderNotificationPayloadDto", + "inventory": "#/components/schemas/InventoryNotificationPayloadDto", + "system": "#/components/schemas/SystemNotificationPayloadDto" + } + } + }, + "read": { + "type": "boolean", + "example": false + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "kind", + "title", + "payload", + "read", + "createdAt" + ] + }, + "NotificationsResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationDto" + } + }, + "meta": { + "$ref": "#/components/schemas/CursorMetaDto" + } + }, + "required": [ + "data", + "meta" + ] + }, + "NotificationResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/NotificationDto" + } + }, + "required": [ + "data" + ] + }, + "FileMetadataDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "file-001" + }, + "name": { + "type": "string", + "example": "products.csv" + }, + "mimeType": { + "type": "string", + "example": "text/csv" + }, + "size": { + "type": "number", + "example": 18432 + }, + "downloadUrl": { + "type": "string", + "format": "uri", + "example": "/api/v1/files/file-001/download" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "name", + "mimeType", + "size", + "downloadUrl", + "createdAt" + ] + }, + "FilesResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileMetadataDto" + } + } + }, + "required": [ + "data" + ] + }, + "FileResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/FileMetadataDto" + } + }, + "required": [ + "data" + ] + }, + "AuditEventDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "audit-001" + }, + "action": { + "type": "string", + "example": "product.updated" + }, + "actorId": { + "type": "string", + "example": "complex-user-admin" + }, + "resourceType": { + "type": "string", + "example": "product" + }, + "resourceId": { + "type": "string", + "example": "complex-product-keyboard" + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "example": { + "version": 4 + } + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "action", + "actorId", + "resourceType", + "resourceId", + "metadata", + "createdAt" + ] + }, + "AuditEventsResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AuditEventDto" + } + }, + "meta": { + "$ref": "#/components/schemas/PageMetaDto" + } + }, + "required": [ + "data", + "meta" + ] + }, + "JobDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "job-001" + }, + "type": { + "type": "string", + "enum": [ + "orders-export" + ], + "example": "orders-export" + }, + "status": { + "type": "string", + "enum": [ + "pending", + "processing", + "completed", + "failed" + ] + }, + "progress": { + "type": "number", + "example": 75, + "minimum": 0, + "maximum": 100 + }, + "resultUrl": { + "type": "object", + "format": "uri", + "nullable": true, + "example": "/api/v1/jobs/job-001/result" + }, + "error": { + "type": "object", + "nullable": true, + "example": null + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "type", + "status", + "progress", + "resultUrl", + "error", + "createdAt", + "updatedAt" + ] + }, + "JobResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/JobDto" + } + }, + "required": [ + "data" + ] + }, + "ConversationDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "conversation-support" + }, + "organizationId": { + "type": "string", + "example": "org-acme" + }, + "title": { + "type": "string", + "example": "Order support" + }, + "participantIds": { + "example": [ + "complex-user-admin", + "complex-user-support" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "lastMessagePreview": { + "type": "object", + "nullable": true, + "example": "Can you check order complex-order-001?" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "organizationId", + "title", + "participantIds", + "lastMessagePreview", + "updatedAt" + ] + }, + "ConversationsResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConversationDto" + } + } + }, + "required": [ + "data" + ] + }, + "ConversationResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ConversationDto" + } + }, + "required": [ + "data" + ] + }, + "ChatMessageDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "message-001" + }, + "conversationId": { + "type": "string", + "example": "conversation-support" + }, + "senderId": { + "type": "string", + "example": "complex-user-support" + }, + "text": { + "type": "string", + "example": "The order has already been packed." + }, + "clientMessageId": { + "type": "string", + "example": "client-message-4c08", + "description": "Frontend-generated key used to deduplicate retries." + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "conversationId", + "senderId", + "text", + "clientMessageId", + "createdAt" + ] + }, + "MessagesResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChatMessageDto" + } + }, + "meta": { + "$ref": "#/components/schemas/CursorMetaDto" + } + }, + "required": [ + "data", + "meta" + ] + }, + "SendMessageDto": { + "type": "object", + "properties": { + "text": { + "type": "string", + "example": "The order has already been packed." + }, + "clientMessageId": { + "type": "string", + "example": "client-message-4c08" + } + }, + "required": [ + "text", + "clientMessageId" + ] + }, + "MessageResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ChatMessageDto" + } + }, + "required": [ + "data" + ] + }, + "ScenarioDto": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "slow" + }, + "description": { + "type": "string", + "example": "Delays the response to exercise loading and cancellation states." + } + }, + "required": [ + "name", + "description" + ] + }, + "ScenariosResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScenarioDto" + } + } + }, + "required": [ + "data" + ] + }, + "TestingActionDataDto": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "State reset to the default deterministic seed." + } + }, + "required": [ + "success", + "message" + ] + }, + "TestingActionResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/TestingActionDataDto" + } + }, + "required": [ + "data" + ] + }, + "ChangeComplexRoleDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "admin", + "manager", + "support", + "viewer" + ], + "example": "viewer" + } + }, + "required": [ + "role" + ] + } + } + } +} diff --git a/examples/demo-backend/openapi/simple.json b/examples/demo-backend/openapi/simple.json new file mode 100644 index 0000000..f00b563 --- /dev/null +++ b/examples/demo-backend/openapi/simple.json @@ -0,0 +1,2677 @@ +{ + "openapi": "3.0.0", + "paths": { + "/api/v1/health": { + "get": { + "operationId": "SimpleHealth_health", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponseDto" + } + } + } + } + }, + "summary": "Check Simple API availability", + "tags": [ + "Health" + ] + } + }, + "/api/v1/auth/login": { + "post": { + "operationId": "SimpleAuth_login", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JwtAuthResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "summary": "Login and receive JWT access/refresh tokens", + "tags": [ + "Auth" + ] + } + }, + "/api/v1/auth/refresh": { + "post": { + "operationId": "SimpleAuth_refresh", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefreshTokenDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JwtAuthResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "summary": "Rotate a refresh token and issue a new token pair", + "tags": [ + "Auth" + ] + } + }, + "/api/v1/auth/logout": { + "post": { + "operationId": "SimpleAuth_logout", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefreshTokenDto" + } + } + } + }, + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "summary": "Revoke a refresh token; the operation is idempotent", + "tags": [ + "Auth" + ] + } + }, + "/api/v1/users/me": { + "get": { + "operationId": "SimpleUsers_me", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SimpleUserResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get the authenticated user", + "tags": [ + "Users" + ] + } + }, + "/api/v1/products": { + "get": { + "operationId": "SimpleProducts_list", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "page", + "required": false, + "in": "query", + "schema": { + "minimum": 1, + "default": 1, + "allOf": [ + { + "$ref": "#/components/schemas/Object" + } + ] + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "schema": { + "minimum": 1, + "maximum": 100, + "default": 20, + "allOf": [ + { + "$ref": "#/components/schemas/Object" + } + ] + } + }, + { + "name": "search", + "required": false, + "in": "query", + "schema": { + "example": "keyboard", + "type": "string" + } + }, + { + "name": "categoryId", + "required": false, + "in": "query", + "schema": { + "example": "category-electronics", + "type": "string" + } + }, + { + "name": "sort", + "required": false, + "in": "query", + "schema": { + "default": "newest", + "type": "string", + "enum": [ + "newest", + "price-asc", + "price-desc", + "name" + ] + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductsResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "summary": "List products using offset pagination, filters and sorting", + "tags": [ + "Products" + ] + }, + "post": { + "operationId": "SimpleProducts_create", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateProductDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Create a product as an administrator", + "tags": [ + "Products" + ] + } + }, + "/api/v1/products/{id}": { + "get": { + "operationId": "SimpleProducts_get", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductResponseDto" + } + } + } + }, + "304": { + "description": "The supplied If-None-Match value is current." + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "summary": "Get one product with ETag support", + "tags": [ + "Products" + ] + }, + "patch": { + "operationId": "SimpleProducts_update", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateProductDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "409": { + "description": "Business or optimistic-lock conflict.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Update a product using optimistic locking", + "tags": [ + "Products" + ] + }, + "delete": { + "operationId": "SimpleProducts_remove", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MutationResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Delete a product as an administrator", + "tags": [ + "Products" + ] + } + }, + "/api/v1/categories": { + "get": { + "operationId": "SimpleCategories_list", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CategoriesResponseDto" + } + } + } + } + }, + "summary": "List product categories", + "tags": [ + "Categories" + ] + } + }, + "/api/v1/categories/{id}": { + "get": { + "operationId": "SimpleCategories_get", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CategoryResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "summary": "Get one category", + "tags": [ + "Categories" + ] + } + }, + "/api/v1/orders": { + "get": { + "operationId": "SimpleOrders_list", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "page", + "required": false, + "in": "query", + "schema": { + "minimum": 1, + "default": 1, + "allOf": [ + { + "$ref": "#/components/schemas/Object" + } + ] + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "schema": { + "minimum": 1, + "maximum": 100, + "default": 20, + "allOf": [ + { + "$ref": "#/components/schemas/Object" + } + ] + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrdersResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "List orders visible to the current user", + "tags": [ + "Orders" + ] + }, + "post": { + "operationId": "SimpleOrders_create", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrderDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrderResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "409": { + "description": "Business or optimistic-lock conflict.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Create an order and validate product stock", + "tags": [ + "Orders" + ] + } + }, + "/api/v1/orders/{id}": { + "get": { + "operationId": "SimpleOrders_get", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrderResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get one visible order", + "tags": [ + "Orders" + ] + } + }, + "/api/v1/orders/{id}/cancel": { + "post": { + "operationId": "SimpleOrders_cancel", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrderResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Authentication is missing or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "The current user lacks permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "409": { + "description": "Business or optimistic-lock conflict.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Cancel an order if its state permits the transition", + "tags": [ + "Orders" + ] + } + }, + "/api/v1/testing/scenarios": { + "get": { + "operationId": "SimpleTesting_scenarios", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScenariosResponseDto" + } + } + } + } + }, + "summary": "List deterministic X-Demo-Scenario values", + "tags": [ + "Testing" + ] + } + }, + "/api/v1/testing/reset": { + "post": { + "operationId": "SimpleTesting_reset", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestingActionResponseDto" + } + } + } + } + }, + "summary": "Reset all Simple API state and token revocations", + "tags": [ + "Testing" + ] + } + }, + "/api/v1/testing/seed/{preset}": { + "post": { + "operationId": "SimpleTesting_seed", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "preset", + "required": true, + "in": "path", + "schema": { + "enum": [ + "small", + "large" + ], + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestingActionResponseDto" + } + } + } + } + }, + "summary": "Select a small or large deterministic dataset", + "tags": [ + "Testing" + ] + } + }, + "/api/v1/testing/users/{userId}/role": { + "post": { + "operationId": "SimpleTesting_changeRole", + "parameters": [ + { + "name": "X-Demo-Scenario", + "in": "header", + "description": "Forces a deterministic frontend-testing scenario for this request.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset" + ] + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeSimpleRoleDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SimpleUserResponseDto" + } + } + } + }, + "400": { + "description": "Invalid request or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "The requested resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "429": { + "description": "Demo rate limit scenario.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Unexpected or simulated server error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "summary": "Change a user role to exercise dynamic access control", + "tags": [ + "Testing" + ] + } + } + }, + "info": { + "title": "Demo Simple API", + "description": "JWT-based API for landing pages and medium frontend applications.", + "version": "1.0.0", + "contact": {} + }, + "tags": [], + "servers": [ + { + "url": "http://localhost:3001", + "description": "Local development" + } + ], + "components": { + "securitySchemes": { + "jwt": { + "scheme": "bearer", + "bearerFormat": "JWT", + "type": "http" + } + }, + "schemas": { + "HealthDataDto": { + "type": "object", + "properties": { + "application": { + "type": "string", + "enum": [ + "simple", + "complex" + ], + "example": "simple" + }, + "status": { + "type": "string", + "enum": [ + "ok" + ], + "example": "ok" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "string", + "example": "1.0.0" + } + }, + "required": [ + "application", + "status", + "timestamp", + "version" + ] + }, + "HealthResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/HealthDataDto" + } + }, + "required": [ + "data" + ] + }, + "LoginDto": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "example": "admin@demo.local" + }, + "password": { + "type": "string", + "format": "password", + "example": "demo1234", + "minLength": 8 + } + }, + "required": [ + "email", + "password" + ] + }, + "JwtTokensDto": { + "type": "object", + "properties": { + "accessToken": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "expiresIn": { + "type": "number", + "example": 60, + "description": "Access-token lifetime in seconds." + }, + "tokenType": { + "type": "string", + "enum": [ + "Bearer" + ], + "example": "Bearer" + } + }, + "required": [ + "accessToken", + "refreshToken", + "expiresIn", + "tokenType" + ] + }, + "SimpleUserDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "user-admin" + }, + "email": { + "type": "string", + "format": "email", + "example": "admin@demo.local" + }, + "name": { + "type": "string", + "example": "Demo Admin" + }, + "role": { + "type": "string", + "enum": [ + "admin", + "customer" + ] + }, + "avatarUrl": { + "type": "object", + "nullable": true, + "example": "https://i.pravatar.cc/160?img=12" + } + }, + "required": [ + "id", + "email", + "name", + "role", + "avatarUrl" + ] + }, + "JwtAuthDataDto": { + "type": "object", + "properties": { + "tokens": { + "$ref": "#/components/schemas/JwtTokensDto" + }, + "user": { + "$ref": "#/components/schemas/SimpleUserDto" + } + }, + "required": [ + "tokens", + "user" + ] + }, + "JwtAuthResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/JwtAuthDataDto" + } + }, + "required": [ + "data" + ] + }, + "ErrorDetailDto": { + "type": "object", + "properties": { + "field": { + "type": "string", + "example": "email" + }, + "message": { + "type": "string", + "example": "must be an email" + }, + "code": { + "type": "string", + "example": "isEmail" + } + }, + "required": [ + "message" + ] + }, + "ErrorResponseDto": { + "type": "object", + "properties": { + "statusCode": { + "type": "number", + "example": 404 + }, + "code": { + "type": "string", + "example": "PRODUCT_NOT_FOUND" + }, + "message": { + "type": "string", + "example": "Product not found" + }, + "details": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ErrorDetailDto" + } + }, + "timestamp": { + "type": "string", + "format": "date-time", + "example": "2026-07-30T12:00:00.000Z" + }, + "path": { + "type": "string", + "example": "/api/v1/products/product-404" + }, + "requestId": { + "type": "string", + "example": "req-5c9f7a3d" + } + }, + "required": [ + "statusCode", + "code", + "message", + "details", + "timestamp", + "path", + "requestId" + ] + }, + "RefreshTokenDto": { + "type": "object", + "properties": { + "refreshToken": { + "type": "string", + "description": "Refresh token returned by login or the previous refresh call." + } + }, + "required": [ + "refreshToken" + ] + }, + "SimpleUserResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SimpleUserDto" + } + }, + "required": [ + "data" + ] + }, + "Object": { + "type": "object", + "properties": {} + }, + "SimpleProductDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "product-keyboard" + }, + "name": { + "type": "string", + "example": "Mechanical Keyboard" + }, + "slug": { + "type": "string", + "example": "mechanical-keyboard" + }, + "description": { + "type": "string", + "example": "Hot-swappable compact keyboard." + }, + "priceCents": { + "type": "number", + "example": 12990, + "description": "Price in the smallest currency unit." + }, + "currency": { + "type": "string", + "enum": [ + "USD", + "EUR" + ], + "example": "USD" + }, + "categoryId": { + "type": "string", + "example": "category-electronics" + }, + "stock": { + "type": "number", + "example": 24, + "minimum": 0 + }, + "rating": { + "type": "number", + "example": 4.8, + "minimum": 0, + "maximum": 5 + }, + "imageUrl": { + "type": "string", + "format": "uri", + "example": "https://picsum.photos/seed/keyboard/640/480" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "number", + "example": 1, + "description": "Optimistic-lock version." + } + }, + "required": [ + "id", + "name", + "slug", + "description", + "priceCents", + "currency", + "categoryId", + "stock", + "rating", + "imageUrl", + "createdAt", + "version" + ] + }, + "PageMetaDto": { + "type": "object", + "properties": { + "page": { + "type": "number", + "example": 1, + "minimum": 1 + }, + "limit": { + "type": "number", + "example": 20, + "minimum": 1 + }, + "total": { + "type": "number", + "example": 48, + "minimum": 0 + }, + "totalPages": { + "type": "number", + "example": 3, + "minimum": 0 + } + }, + "required": [ + "page", + "limit", + "total", + "totalPages" + ] + }, + "ProductsResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SimpleProductDto" + } + }, + "meta": { + "$ref": "#/components/schemas/PageMetaDto" + } + }, + "required": [ + "data", + "meta" + ] + }, + "ProductResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SimpleProductDto" + } + }, + "required": [ + "data" + ] + }, + "CreateProductDto": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "USB-C Dock" + }, + "description": { + "type": "string", + "example": "Dock with HDMI, Ethernet and power delivery." + }, + "priceCents": { + "type": "number", + "example": 8990, + "minimum": 0 + }, + "currency": { + "type": "string", + "enum": [ + "USD", + "EUR" + ], + "example": "USD" + }, + "categoryId": { + "type": "string", + "example": "category-electronics" + }, + "stock": { + "type": "number", + "example": 15, + "minimum": 0 + }, + "imageUrl": { + "type": "string", + "format": "uri", + "example": "https://picsum.photos/seed/dock/640/480" + } + }, + "required": [ + "name", + "description", + "priceCents", + "currency", + "categoryId", + "stock", + "imageUrl" + ] + }, + "UpdateProductDto": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "USB-C Dock" + }, + "description": { + "type": "string", + "example": "Dock with HDMI, Ethernet and power delivery." + }, + "priceCents": { + "type": "number", + "example": 8990, + "minimum": 0 + }, + "currency": { + "type": "string", + "enum": [ + "USD", + "EUR" + ], + "example": "USD" + }, + "categoryId": { + "type": "string", + "example": "category-electronics" + }, + "stock": { + "type": "number", + "example": 15, + "minimum": 0 + }, + "imageUrl": { + "type": "string", + "format": "uri", + "example": "https://picsum.photos/seed/dock/640/480" + }, + "version": { + "type": "number", + "example": 1, + "minimum": 1, + "description": "Version last read by the frontend." + } + }, + "required": [ + "version" + ] + }, + "MutationResultDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "product-001" + }, + "success": { + "type": "boolean", + "example": true + } + }, + "required": [ + "id", + "success" + ] + }, + "MutationResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/MutationResultDto" + } + }, + "required": [ + "data" + ] + }, + "SimpleCategoryDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "category-electronics" + }, + "name": { + "type": "string", + "example": "Electronics" + }, + "slug": { + "type": "string", + "example": "electronics" + }, + "productCount": { + "type": "number", + "example": 4 + } + }, + "required": [ + "id", + "name", + "slug", + "productCount" + ] + }, + "CategoriesResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SimpleCategoryDto" + } + } + }, + "required": [ + "data" + ] + }, + "CategoryResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SimpleCategoryDto" + } + }, + "required": [ + "data" + ] + }, + "SimpleOrderItemDto": { + "type": "object", + "properties": { + "productId": { + "type": "string", + "example": "product-keyboard" + }, + "productName": { + "type": "string", + "example": "Mechanical Keyboard" + }, + "quantity": { + "type": "number", + "example": 1 + }, + "unitPriceCents": { + "type": "number", + "example": 12990 + } + }, + "required": [ + "productId", + "productName", + "quantity", + "unitPriceCents" + ] + }, + "SimpleOrderDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "order-001" + }, + "userId": { + "type": "string", + "example": "user-customer" + }, + "status": { + "type": "string", + "enum": [ + "pending", + "paid", + "shipped", + "cancelled" + ] + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SimpleOrderItemDto" + } + }, + "totalCents": { + "type": "number", + "example": 17980 + }, + "currency": { + "type": "string", + "enum": [ + "USD", + "EUR" + ], + "example": "USD" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "userId", + "status", + "items", + "totalCents", + "currency", + "createdAt" + ] + }, + "OrdersResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SimpleOrderDto" + } + }, + "meta": { + "$ref": "#/components/schemas/PageMetaDto" + } + }, + "required": [ + "data", + "meta" + ] + }, + "OrderResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SimpleOrderDto" + } + }, + "required": [ + "data" + ] + }, + "CreateOrderItemDto": { + "type": "object", + "properties": { + "productId": { + "type": "string", + "example": "product-keyboard" + }, + "quantity": { + "type": "number", + "example": 1, + "minimum": 1, + "maximum": 20 + } + }, + "required": [ + "productId", + "quantity" + ] + }, + "CreateOrderDto": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CreateOrderItemDto" + } + } + }, + "required": [ + "items" + ] + }, + "ScenarioDto": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "slow" + }, + "description": { + "type": "string", + "example": "Delays the response to exercise loading and cancellation states." + } + }, + "required": [ + "name", + "description" + ] + }, + "ScenariosResponseDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScenarioDto" + } + } + }, + "required": [ + "data" + ] + }, + "TestingActionDataDto": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "State reset to the default deterministic seed." + } + }, + "required": [ + "success", + "message" + ] + }, + "TestingActionResponseDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/TestingActionDataDto" + } + }, + "required": [ + "data" + ] + }, + "ChangeSimpleRoleDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "admin", + "customer" + ], + "example": "customer" + } + }, + "required": [ + "role" + ] + } + } + } +} diff --git a/examples/demo-backend/package-lock.json b/examples/demo-backend/package-lock.json new file mode 100644 index 0000000..0fd0b90 --- /dev/null +++ b/examples/demo-backend/package-lock.json @@ -0,0 +1,8654 @@ +{ + "name": "demo-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "demo-backend", + "version": "1.0.0", + "license": "UNLICENSED", + "dependencies": { + "@nestjs/common": "^11.1.6", + "@nestjs/core": "^11.1.6", + "@nestjs/jwt": "^11.0.1", + "@nestjs/platform-express": "^11.1.6", + "@nestjs/platform-socket.io": "^11.1.6", + "@nestjs/swagger": "^11.2.0", + "@nestjs/websockets": "^11.1.6", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", + "cookie": "^1.0.2", + "cookie-parser": "^1.4.7", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.2", + "socket.io": "^4.8.1" + }, + "devDependencies": { + "@apidevtools/swagger-parser": "^12.0.0", + "@nestjs/cli": "^11.0.10", + "@nestjs/testing": "^11.1.6", + "@types/cookie-parser": "^1.4.9", + "@types/express": "^5.0.3", + "@types/jest": "^29.5.14", + "@types/multer": "^2.0.0", + "@types/node": "^24.0.15", + "@types/supertest": "^6.0.3", + "concurrently": "^9.2.0", + "jest": "^29.7.0", + "prettier": "^3.6.2", + "socket.io-client": "^4.8.1", + "supertest": "^7.1.4", + "ts-jest": "^29.4.0", + "typescript": "^5.9.2" + } + }, + "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/core/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/core/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": { + "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": { + "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/@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/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/@apidevtools/json-schema-ref-parser": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz", + "integrity": "sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-12.1.0.tgz", + "integrity": "sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "14.0.1", + "@apidevtools/openapi-schemas": "^2.1.0", + "@apidevtools/swagger-methods": "^3.0.2", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "call-me-maybe": "^1.0.2" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "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.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/@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-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-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "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/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-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/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "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/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/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/@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/@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/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "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", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=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==", + "dev": 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==", + "dev": 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/@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/@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/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/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/jwt": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.2.tgz", + "integrity": "sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.10", + "jsonwebtoken": "9.0.3" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, + "node_modules/@nestjs/mapped-types": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.1.tgz", + "integrity": "sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==", + "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 || ^0.15.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/platform-socket.io": { + "version": "11.1.28", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.28.tgz", + "integrity": "sha512-vY+GmU2jBcymvgm5rEnftUx4qNxK8cDJmXjl1/1NcpITTNJo0vg07xYR43MwXHcMqe7b0jwqt5+UCTzxqQFIqA==", + "license": "MIT", + "dependencies": { + "socket.io": "4.8.3", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/schematics": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.1.0.tgz", + "integrity": "sha512-lVxGZ46tcdItFMoXr6vyKWlnOsm1SZm/GUqAEDvy2RL4Q4O+3bkziAhrO7Y8JLssFUUvNFEGqAizI52WAxhjDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.24", + "@angular-devkit/schematics": "19.2.24", + "comment-json": "5.0.0", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "prettier": "^3.0.0", + "typescript": ">=4.8.2" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": { + "version": "19.2.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.24.tgz", + "integrity": "sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==", + "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/schematics/node_modules/@angular-devkit/schematics": { + "version": "19.2.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.24.tgz", + "integrity": "sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.24", + "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.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/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/swagger": { + "version": "11.4.6", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.4.6.tgz", + "integrity": "sha512-Le136h2WC7HGsd70+WyK1qrm+Zq7kFxBLkYC1JgAVqNRCt8kNh7bMF7Qkn65D5j2t/aks0+VbWmUVlYIwPrs3A==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.16.0", + "@nestjs/mapped-types": "2.1.1", + "js-yaml": "5.2.1", + "lodash": "4.18.1", + "path-to-regexp": "8.4.2", + "swagger-ui-dist": "5.32.8" + }, + "peerDependencies": { + "@fastify/static": "^8.0.0 || ^9.0.0 || ^10.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/js-yaml": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.2.tgz", + "integrity": "sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==", + "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.mjs" + } + }, + "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/websockets": { + "version": "11.1.28", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.28.tgz", + "integrity": "sha512-jeyclAURCJTN8S8lctDhfLdiJeDKjZmYWWLav653Fb9hl9c+zx5jPhavI8Xk5++R8u+lX9qzaRxtsjEoxTtjyw==", + "license": "MIT", + "dependencies": { + "iterare": "1.2.1", + "object-hash": "3.0.0", + "tslib": "2.8.1" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/platform-socket.io": "^11.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/platform-socket.io": { + "optional": true + } + } + }, + "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/@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/@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/@sinclair/typebox": { + "version": "0.27.12", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz", + "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "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/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "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/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/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "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/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "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/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.2.0.tgz", + "integrity": "sha512-3U1troeqGV8Ntp7Q3klwf4zr23VEoqYVocYXaswm9+8z3O9UHDYAqLxjJ/h550iRADTjKdOdhhasXw6gD6kYtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.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": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "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/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "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/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.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": 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/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-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "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/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": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/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/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-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/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/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/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-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.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==", + "dev": true, + "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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.7.tgz", + "integrity": "sha512-APw5YuIQAg6L9w4sHDI6j26DGFJI6RpYOhnkMPdC9lWbkKvsyPHzDsve1yd73lk21yz7Y09Kci8B2Pp9FonzWA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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/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/brace-expansion": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.17.tgz", + "integrity": "sha512-w+aeW/mkgM4PyRMOJCgi3fOrTm5Q8QY1OSfn2TO2iuDj3ezIHqejmuxbjfPrqUkgqRew1iqkyAn0tr0ZwHD9+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "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.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "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/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "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/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "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/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/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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": "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/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/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "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.4", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.4.tgz", + "integrity": "sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.22" + } + }, + "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/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/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-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "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/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "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/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/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/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": "5.0.0", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-5.0.0.tgz", + "integrity": "sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^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.4", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz", + "integrity": "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.9.0", + "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/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": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/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/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-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/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/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/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/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "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-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.398", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.398.tgz", + "integrity": "sha512-AsvhAxopJGh6museTDMIjn6JpDYOfgu4RLlygomt87MUwBUqTfd/1EiPtx10/LZE8xpTvkP2E9Gafq7lkLtodQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/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==", + "dev": true, + "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/engine.io": { + "version": "6.6.9", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.9.tgz", + "integrity": "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz", + "integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/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/engine.io/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/engine.io/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/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.4.tgz", + "integrity": "sha512-GVoi+ICHocoOIU7qVVM48wOJziRsqrsyqlI0Ce0LdowRn6v3bcH2zUa9kp85ncx0nwIb9/HOCOLS3fdThDG/XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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": "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/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/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/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/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/esrecurse/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/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/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/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/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/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/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.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/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/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-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-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "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/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "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": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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/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-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/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "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/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==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-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.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/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/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "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/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "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/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "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/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/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-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-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-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/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/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/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/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "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", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "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", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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.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/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-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/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/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/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.13.9", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.9.tgz", + "integrity": "sha512-VNS5vWMM7r0P66BYv+TQJATxExEgLxN+34hfHDVhDkUsGAE4cRg0shCNSLTXNKm7nIUscC7AfB51TjxEeF7msQ==", + "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/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": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/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/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/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/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "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/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==", + "dev": true, + "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==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz", + "integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/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": "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/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-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-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "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/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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-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": "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/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "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/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": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/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-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-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/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/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "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": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "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/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/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.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/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": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "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==", + "dev": true, + "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": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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/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/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/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/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/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "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/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/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz", + "integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.21.0" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz", + "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/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/socket.io/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/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/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/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "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/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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/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-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "dev": true, + "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/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.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/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.32.8", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.8.tgz", + "integrity": "sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA==", + "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/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/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/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/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/terser/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/terser/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/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "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", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/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/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-jest": { + "version": "29.4.12", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.12.tgz", + "integrity": "sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.8.5", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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/tsconfig-paths/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/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/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/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/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "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/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.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-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/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/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.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/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "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/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/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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "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/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/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/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/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "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/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "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": "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/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==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-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" + } + } + } +} diff --git a/examples/demo-backend/package.json b/examples/demo-backend/package.json new file mode 100644 index 0000000..3d26949 --- /dev/null +++ b/examples/demo-backend/package.json @@ -0,0 +1,82 @@ +{ + "name": "demo-backend", + "version": "1.0.0", + "private": true, + "description": "Two isolated NestJS demo APIs for testing frontend application architecture", + "license": "UNLICENSED", + "scripts": { + "build": "tsc -p tsconfig.build.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "start:dev": "concurrently -k -n simple,complex -c blue,magenta \"npm:dev:simple\" \"npm:dev:complex\"", + "dev:simple": "nest start --watch --entryFile apps/simple/main", + "dev:complex": "nest start --watch --entryFile apps/complex/main", + "start:simple": "node dist/apps/simple/main.js", + "start:complex": "node dist/apps/complex/main.js", + "openapi:generate": "npm run build && node dist/scripts/generate-openapi.js", + "openapi:validate": "node dist/scripts/validate-openapi.js", + "test": "jest", + "test:e2e": "jest --runInBand", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"*.{json,md}\" \"docs/**/*.md\"" + }, + "dependencies": { + "@nestjs/common": "^11.1.6", + "@nestjs/core": "^11.1.6", + "@nestjs/jwt": "^11.0.1", + "@nestjs/platform-express": "^11.1.6", + "@nestjs/platform-socket.io": "^11.1.6", + "@nestjs/swagger": "^11.2.0", + "@nestjs/websockets": "^11.1.6", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", + "cookie": "^1.0.2", + "cookie-parser": "^1.4.7", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.2", + "socket.io": "^4.8.1" + }, + "devDependencies": { + "@apidevtools/swagger-parser": "^12.0.0", + "@nestjs/cli": "^11.0.10", + "@nestjs/testing": "^11.1.6", + "@types/cookie-parser": "^1.4.9", + "@types/express": "^5.0.3", + "@types/jest": "^29.5.14", + "@types/multer": "^2.0.0", + "@types/node": "^24.0.15", + "@types/supertest": "^6.0.3", + "concurrently": "^9.2.0", + "jest": "^29.7.0", + "prettier": "^3.6.2", + "socket.io-client": "^4.8.1", + "supertest": "^7.1.4", + "ts-jest": "^29.4.0", + "typescript": "^5.9.2" + }, + "overrides": { + "@nestjs/swagger": { + "js-yaml": "5.2.2" + } + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": ".", + "testRegex": ".*\\.e2e-spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": [ + "ts-jest", + { + "tsconfig": "tsconfig.json" + } + ] + }, + "collectCoverageFrom": [ + "src/**/*.(t|j)s" + ], + "coverageDirectory": "coverage", + "testEnvironment": "node" + } +} diff --git a/examples/demo-backend/src/apps/complex/bootstrap.ts b/examples/demo-backend/src/apps/complex/bootstrap.ts new file mode 100644 index 0000000..6c333f7 --- /dev/null +++ b/examples/demo-backend/src/apps/complex/bootstrap.ts @@ -0,0 +1,20 @@ +import { NestFactory } from "@nestjs/core"; +import type { NestExpressApplication } from "@nestjs/platform-express"; +import type { OpenAPIObject } from "@nestjs/swagger"; +import { configureApplication } from "../../common/configure-application"; +import { createOpenApiDocument, mountOpenApi } from "../../common/openapi"; +import { ComplexAppModule } from "./complex.module"; + +export async function createComplexApplication( + logger: false | undefined = undefined, +): Promise<{ app: NestExpressApplication; document: OpenAPIObject }> { + const app = await NestFactory.create<NestExpressApplication>( + ComplexAppModule, + { logger }, + ); + configureApplication(app); + const port = Number(process.env.COMPLEX_PORT ?? 3002); + const document = createOpenApiDocument(app, { kind: "complex", port }); + mountOpenApi(app, document, "Demo Complex API"); + return { app, document }; +} diff --git a/examples/demo-backend/src/apps/complex/chat.gateway.ts b/examples/demo-backend/src/apps/complex/chat.gateway.ts new file mode 100644 index 0000000..7fb0a5d --- /dev/null +++ b/examples/demo-backend/src/apps/complex/chat.gateway.ts @@ -0,0 +1,175 @@ +import { parse as parseCookie } from "cookie"; +import { Logger } from "@nestjs/common"; +import { + ConnectedSocket, + MessageBody, + OnGatewayConnection, + SubscribeMessage, + WebSocketGateway, + WebSocketServer, +} from "@nestjs/websockets"; +import type { Server, Socket } from "socket.io"; +import { ComplexSessionService, SESSION_COOKIE } from "./complex.auth"; +import { ComplexStore } from "./complex.store"; +import type { ChatMessageDto, SendMessageDto } from "./dto/chat.dto"; + +interface ChatSocketData { + userId: string; + organizationIds: string[]; +} + +interface ChatContextPayload { + organizationId: string; + conversationId: string; +} + +interface SocketMessagePayload extends ChatContextPayload, SendMessageDto {} + +@WebSocketGateway({ + namespace: "/chat", + cors: { origin: true, credentials: true }, + transports: ["websocket", "polling"], +}) +export class ChatGateway implements OnGatewayConnection { + @WebSocketServer() + server!: Server; + + private readonly logger = new Logger(ChatGateway.name); + + constructor( + private readonly sessions: ComplexSessionService, + private readonly store: ComplexStore, + ) {} + + handleConnection(client: Socket): void { + const cookies = parseCookie(client.handshake.headers.cookie ?? ""); + const session = this.sessions.get(cookies[SESSION_COOKIE]); + const user = session ? this.store.findUserById(session.userId) : undefined; + if (!session || !user) { + client.emit("chat:error", { + code: "SESSION_EXPIRED", + message: "Valid demo_session cookie is required.", + }); + client.disconnect(true); + return; + } + client.data = { + userId: user.id, + organizationIds: user.organizationIds, + } satisfies ChatSocketData; + this.logger.debug(`Socket ${client.id} connected as ${user.id}`); + } + + @SubscribeMessage("chat:join") + join( + @ConnectedSocket() client: Socket, + @MessageBody() payload: ChatContextPayload, + ) { + try { + const data = client.data as ChatSocketData; + this.assertOrganization(data, payload.organizationId); + this.store.getConversation( + payload.organizationId, + data.userId, + payload.conversationId, + ); + void client.join(this.room(payload.conversationId)); + return { + event: "chat:joined", + data: { conversationId: payload.conversationId }, + }; + } catch (error) { + return { event: "chat:error", data: this.socketError(error) }; + } + } + + @SubscribeMessage("chat:leave") + leave( + @ConnectedSocket() client: Socket, + @MessageBody() payload: ChatContextPayload, + ) { + void client.leave(this.room(payload.conversationId)); + return { + event: "chat:left", + data: { conversationId: payload.conversationId }, + }; + } + + @SubscribeMessage("message:send") + send( + @ConnectedSocket() client: Socket, + @MessageBody() payload: SocketMessagePayload, + ) { + try { + const data = client.data as ChatSocketData; + this.assertOrganization(data, payload.organizationId); + const message = this.store.sendMessage( + payload.organizationId, + data.userId, + payload.conversationId, + payload, + ); + this.broadcastMessage(message); + return { event: "message:ack", data: message }; + } catch (error) { + return { event: "chat:error", data: this.socketError(error) }; + } + } + + @SubscribeMessage("typing:start") + typingStart( + @ConnectedSocket() client: Socket, + @MessageBody() payload: ChatContextPayload, + ): void { + this.broadcastTyping(client, payload, true); + } + + @SubscribeMessage("typing:stop") + typingStop( + @ConnectedSocket() client: Socket, + @MessageBody() payload: ChatContextPayload, + ): void { + this.broadcastTyping(client, payload, false); + } + + broadcastMessage(message: ChatMessageDto): void { + this.server + .to(this.room(message.conversationId)) + .emit("message:created", message); + } + + private broadcastTyping( + client: Socket, + payload: ChatContextPayload, + active: boolean, + ): void { + const data = client.data as ChatSocketData; + if (!data.organizationIds.includes(payload.organizationId)) return; + client + .to(this.room(payload.conversationId)) + .emit(active ? "typing:started" : "typing:stopped", { + conversationId: payload.conversationId, + userId: data.userId, + }); + } + + private assertOrganization( + data: ChatSocketData, + organizationId: string, + ): void { + if (!data.organizationIds.includes(organizationId)) + throw new Error("Organization is unavailable to this user."); + } + + private room(conversationId: string): string { + return `conversation:${conversationId}`; + } + + private socketError(error: unknown): { code: string; message: string } { + return { + code: "CHAT_OPERATION_FAILED", + message: + error instanceof Error ? error.message : "Chat operation failed.", + }; + } +} diff --git a/examples/demo-backend/src/apps/complex/complex.auth.ts b/examples/demo-backend/src/apps/complex/complex.auth.ts new file mode 100644 index 0000000..95f3bde --- /dev/null +++ b/examples/demo-backend/src/apps/complex/complex.auth.ts @@ -0,0 +1,261 @@ +import { randomUUID } from "node:crypto"; +import { + BadRequestException, + CanActivate, + ExecutionContext, + ForbiddenException, + Injectable, + SetMetadata, + UnauthorizedException, +} from "@nestjs/common"; +import { Reflector } from "@nestjs/core"; +import type { CookieOptions, Request } from "express"; +import type { DemoRequest } from "../../common/request.types"; +import { assertNoForcedAuthFailure } from "../../common/scenario.interceptor"; +import { + ComplexRole, + type ComplexLoginDto, + type ComplexUserDto, + type CookieSessionResponseDto, +} from "./dto/identity.dto"; +import { ComplexStore } from "./complex.store"; + +export const SESSION_COOKIE = "demo_session"; +export const CSRF_COOKIE = "demo_csrf"; +const ROLES_METADATA = "complex-roles"; + +export interface ComplexDemoRequest extends DemoRequest { + sessionToken?: string; +} + +interface SessionRecord { + token: string; + userId: string; + csrfToken: string; + expiresAt: number; +} + +export interface CreatedSession { + token: string; + csrfToken: string; + expiresAt: string; + user: ComplexUserDto; +} + +export const ComplexRoles = (...roles: ComplexRole[]) => + SetMetadata(ROLES_METADATA, roles); + +export function sessionCookieOptions(): CookieOptions { + return { + httpOnly: true, + sameSite: "lax", + secure: process.env.COOKIE_SECURE === "true", + maxAge: Number(process.env.COOKIE_SESSION_TTL_MS ?? 1_800_000), + path: "/", + }; +} + +export function csrfCookieOptions(): CookieOptions { + return { + httpOnly: false, + sameSite: "lax", + secure: process.env.COOKIE_SECURE === "true", + maxAge: Number(process.env.COOKIE_SESSION_TTL_MS ?? 1_800_000), + path: "/", + }; +} + +@Injectable() +export class ComplexSessionService { + private readonly sessions = new Map<string, SessionRecord>(); + + constructor(private readonly store: ComplexStore) {} + + login(dto: ComplexLoginDto): CreatedSession { + const user = this.store.findUserByEmail(dto.email); + if (!user || user.password !== dto.password) { + throw new UnauthorizedException({ + code: "INVALID_CREDENTIALS", + message: "Email or password is incorrect.", + }); + } + return this.create(user.id); + } + + get(token: string | undefined): SessionRecord | undefined { + if (!token) return undefined; + const session = this.sessions.get(token); + if (!session) return undefined; + if (session.expiresAt <= Date.now()) { + this.sessions.delete(token); + return undefined; + } + return session; + } + + rotate(token: string): CreatedSession { + const existing = this.get(token); + if (!existing) + throw new UnauthorizedException({ + code: "SESSION_EXPIRED", + message: "Cookie session is missing or expired.", + }); + this.sessions.delete(token); + return this.create(existing.userId); + } + + revoke(token: string | undefined): void { + if (token) this.sessions.delete(token); + } + + expire(token: string | undefined): void { + const session = token ? this.sessions.get(token) : undefined; + if (session) session.expiresAt = 0; + } + + reset(): void { + this.sessions.clear(); + } + + response(session: CreatedSession): CookieSessionResponseDto { + return { + data: { + user: session.user, + csrfToken: session.csrfToken, + expiresAt: session.expiresAt, + }, + }; + } + + private create(userId: string): CreatedSession { + const user = this.store.findUserById(userId); + if (!user) + throw new UnauthorizedException({ + code: "USER_NOT_FOUND", + message: "Session user no longer exists.", + }); + const ttl = Number(process.env.COOKIE_SESSION_TTL_MS ?? 1_800_000); + const record: SessionRecord = { + token: randomUUID(), + userId, + csrfToken: randomUUID(), + expiresAt: Date.now() + ttl, + }; + this.sessions.set(record.token, record); + return { + token: record.token, + csrfToken: record.csrfToken, + expiresAt: new Date(record.expiresAt).toISOString(), + user: this.store.publicUser(user), + }; + } +} + +@Injectable() +export class ComplexCookieGuard implements CanActivate { + constructor( + private readonly sessions: ComplexSessionService, + private readonly store: ComplexStore, + ) {} + + canActivate(context: ExecutionContext): boolean { + const request = context + .switchToHttp() + .getRequest<Request>() as ComplexDemoRequest; + assertNoForcedAuthFailure(request); + const cookies = request.cookies as Record<string, string> | undefined; + const token = cookies?.[SESSION_COOKIE]; + const session = this.sessions.get(token); + if (!session) + throw new UnauthorizedException({ + code: "SESSION_EXPIRED", + message: "Cookie session is missing or expired.", + }); + const user = this.store.findUserById(session.userId); + if (!user) + throw new UnauthorizedException({ + code: "USER_NOT_FOUND", + message: "Session user no longer exists.", + }); + request.user = this.store.publicUser(user); + request.sessionToken = token; + return true; + } +} + +@Injectable() +export class ComplexCsrfGuard implements CanActivate { + constructor(private readonly sessions: ComplexSessionService) {} + + canActivate(context: ExecutionContext): boolean { + const request = context + .switchToHttp() + .getRequest<Request>() as ComplexDemoRequest; + const session = this.sessions.get(request.sessionToken); + const headerToken = request.headers["x-csrf-token"]; + const cookieToken = ( + request.cookies as Record<string, string> | undefined + )?.[CSRF_COOKIE]; + if ( + !session || + typeof headerToken !== "string" || + headerToken !== session.csrfToken || + cookieToken !== session.csrfToken + ) { + throw new ForbiddenException({ + code: "CSRF_INVALID", + message: + "A matching X-CSRF-Token header and demo_csrf cookie are required.", + }); + } + return true; + } +} + +@Injectable() +export class ComplexOrganizationGuard implements CanActivate { + constructor(private readonly store: ComplexStore) {} + + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest<ComplexDemoRequest>(); + const organizationId = request.headers["x-organization-id"]; + if (typeof organizationId !== "string" || organizationId.length === 0) { + throw new BadRequestException({ + code: "ORGANIZATION_REQUIRED", + message: "X-Organization-Id header is required.", + }); + } + if ( + !request.user || + !this.store.isMember(request.user.id, organizationId) + ) { + throw new ForbiddenException({ + code: "ORGANIZATION_FORBIDDEN", + message: "The user is not an active member of this organization.", + }); + } + request.organizationId = organizationId; + return true; + } +} + +@Injectable() +export class ComplexRolesGuard implements CanActivate { + constructor(private readonly reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const required = this.reflector.getAllAndOverride<ComplexRole[]>( + ROLES_METADATA, + [context.getHandler(), context.getClass()], + ); + if (!required?.length) return true; + const request = context.switchToHttp().getRequest<ComplexDemoRequest>(); + if (!request.user || !required.includes(request.user.role as ComplexRole)) { + throw new ForbiddenException({ + code: "ROLE_FORBIDDEN", + message: `One of these roles is required: ${required.join(", ")}.`, + }); + } + return true; + } +} diff --git a/examples/demo-backend/src/apps/complex/complex.module.ts b/examples/demo-backend/src/apps/complex/complex.module.ts new file mode 100644 index 0000000..e6373ad --- /dev/null +++ b/examples/demo-backend/src/apps/complex/complex.module.ts @@ -0,0 +1,71 @@ +import { Module } from "@nestjs/common"; +import { InfrastructureModule } from "../../common/infrastructure.module"; +import { + ComplexCookieGuard, + ComplexCsrfGuard, + ComplexOrganizationGuard, + ComplexRolesGuard, + ComplexSessionService, +} from "./complex.auth"; +import { ComplexStore } from "./complex.store"; +import { ChatGateway } from "./chat.gateway"; +import { ChatController } from "./controllers/chat.controller"; +import { + ComplexCatalogController, + ComplexInventoryController, + ComplexProductsController, +} from "./controllers/catalog.controllers"; +import { + ComplexOrdersController, + CustomersController, + PaymentsController, + ReviewsController, +} from "./controllers/commerce.controllers"; +import { + ComplexAuthController, + ComplexUsersController, + OrganizationsController, +} from "./controllers/identity.controllers"; +import { + AuditController, + FilesController, + JobsController, + NotificationsController, +} from "./controllers/operations.controllers"; +import { + ComplexHealthController, + ComplexTestingController, +} from "./controllers/system.controllers"; + +@Module({ + imports: [InfrastructureModule], + controllers: [ + ComplexHealthController, + ComplexAuthController, + ComplexUsersController, + OrganizationsController, + ComplexProductsController, + ComplexCatalogController, + ComplexInventoryController, + CustomersController, + ComplexOrdersController, + PaymentsController, + ReviewsController, + NotificationsController, + FilesController, + AuditController, + JobsController, + ChatController, + ComplexTestingController, + ], + providers: [ + ComplexStore, + ComplexSessionService, + ComplexCookieGuard, + ComplexCsrfGuard, + ComplexOrganizationGuard, + ComplexRolesGuard, + ChatGateway, + ], +}) +export class ComplexAppModule {} diff --git a/examples/demo-backend/src/apps/complex/complex.store.ts b/examples/demo-backend/src/apps/complex/complex.store.ts new file mode 100644 index 0000000..c18c8e9 --- /dev/null +++ b/examples/demo-backend/src/apps/complex/complex.store.ts @@ -0,0 +1,1354 @@ +import { + ConflictException, + Injectable, + NotFoundException, + UnprocessableEntityException, +} from "@nestjs/common"; +import { + type AdjustInventoryDto, + type BrandDto, + type ComplexCategoryDto, + type ComplexProductDto, + ComplexProductStatus, + type CreateComplexProductDto, + type CursorProductQueryDto, + type InventoryItemDto, + type UpdateComplexProductDto, + type WarehouseDto, +} from "./dto/catalog.dto"; +import { + type ComplexOrderDto, + ComplexOrderStatus, + type CreateComplexOrderDto, + type CreateReviewDto, + type CustomerDto, + type CustomerQueryDto, + type OrderCursorQueryDto, + type PaymentDto, + type PromotionDto, + type ReviewDto, +} from "./dto/commerce.dto"; +import { + type ChatMessageDto, + type ConversationDto, + type SendMessageDto, +} from "./dto/chat.dto"; +import { + ComplexRole, + type ComplexUserDto, + type MemberDto, + type OrganizationDto, + OrganizationPlan, +} from "./dto/identity.dto"; +import { + type AuditEventDto, + type AuditQueryDto, + type FileMetadataDto, + type JobDto, + JobStatus, + type NotificationDto, + NotificationKind, +} from "./dto/operations.dto"; + +interface ComplexUserRecord extends ComplexUserDto { + password: string; +} + +interface TenantNotification extends NotificationDto { + organizationId: string; +} + +interface TenantFile extends FileMetadataDto { + organizationId: string; +} + +interface TenantAuditEvent extends AuditEventDto { + organizationId: string; +} + +interface TenantJob extends JobDto { + organizationId: string; + startedAtMs: number; +} + +interface TenantReview extends ReviewDto { + organizationId: string; +} + +@Injectable() +export class ComplexStore { + private users: ComplexUserRecord[] = []; + private organizations: OrganizationDto[] = []; + private members: MemberDto[] = []; + private products: ComplexProductDto[] = []; + private categories: ComplexCategoryDto[] = []; + private brands: BrandDto[] = []; + private warehouses: WarehouseDto[] = []; + private inventory: InventoryItemDto[] = []; + private customers: CustomerDto[] = []; + private orders: ComplexOrderDto[] = []; + private payments: PaymentDto[] = []; + private promotions: PromotionDto[] = []; + private reviews: TenantReview[] = []; + private notifications: TenantNotification[] = []; + private files: TenantFile[] = []; + private fileContents = new Map<string, Buffer>(); + private auditEvents: TenantAuditEvent[] = []; + private jobs: TenantJob[] = []; + private conversations: ConversationDto[] = []; + private messages: ChatMessageDto[] = []; + private orderIdempotency = new Map<string, string>(); + private sequences = { + product: 20, + order: 20, + review: 20, + file: 20, + audit: 20, + job: 20, + message: 20, + }; + + constructor() { + this.reset("small"); + } + + reset(preset: "small" | "large" = "small"): void { + this.users = [ + { + id: "complex-user-admin", + email: "admin@complex.demo", + password: "demo1234", + name: "Complex Admin", + role: ComplexRole.Admin, + avatarUrl: "https://i.pravatar.cc/160?img=20", + organizationIds: ["org-acme", "org-globex"], + }, + { + id: "complex-user-manager", + email: "manager@complex.demo", + password: "demo1234", + name: "Complex Manager", + role: ComplexRole.Manager, + avatarUrl: "https://i.pravatar.cc/160?img=32", + organizationIds: ["org-acme"], + }, + { + id: "complex-user-support", + email: "support@complex.demo", + password: "demo1234", + name: "Complex Support", + role: ComplexRole.Support, + avatarUrl: null, + organizationIds: ["org-acme"], + }, + { + id: "complex-user-viewer", + email: "viewer@complex.demo", + password: "demo1234", + name: "Complex Viewer", + role: ComplexRole.Viewer, + avatarUrl: null, + organizationIds: ["org-acme"], + }, + ]; + this.organizations = [ + { + id: "org-acme", + name: "Acme Commerce", + plan: OrganizationPlan.Enterprise, + timezone: "Europe/Berlin", + currency: "USD", + createdAt: "2025-03-01T10:00:00.000Z", + }, + { + id: "org-globex", + name: "Globex Retail", + plan: OrganizationPlan.Business, + timezone: "America/New_York", + currency: "EUR", + createdAt: "2025-08-15T14:00:00.000Z", + }, + ]; + this.members = this.users.flatMap((user, userIndex) => + user.organizationIds.map((organizationId, organizationIndex) => ({ + id: `member-${userIndex + 1}-${organizationIndex + 1}`, + organizationId, + userId: user.id, + userName: user.name, + email: user.email, + role: user.role, + status: "active" as const, + })), + ); + this.categories = [ + { + id: "complex-category-electronics", + organizationId: "org-acme", + name: "Electronics", + parentId: null, + childIds: ["complex-category-keyboards"], + }, + { + id: "complex-category-keyboards", + organizationId: "org-acme", + name: "Keyboards", + parentId: "complex-category-electronics", + childIds: [], + }, + { + id: "complex-category-office", + organizationId: "org-acme", + name: "Office", + parentId: null, + childIds: [], + }, + { + id: "globex-category-home", + organizationId: "org-globex", + name: "Home", + parentId: null, + childIds: [], + }, + ]; + this.brands = [ + { + id: "brand-northstar", + organizationId: "org-acme", + name: "Northstar", + logoUrl: "https://picsum.photos/seed/northstar/160/80", + }, + { + id: "brand-contoso", + organizationId: "org-acme", + name: "Contoso", + logoUrl: null, + }, + { + id: "brand-globex", + organizationId: "org-globex", + name: "Globex", + logoUrl: null, + }, + ]; + const baseProducts = [ + this.product( + "complex-product-keyboard", + "org-acme", + "Pro Mechanical Keyboard", + "129.90", + "complex-category-keyboards", + "brand-northstar", + ComplexProductStatus.Active, + ), + this.product( + "complex-product-mouse", + "org-acme", + "Precision Mouse", + "79.90", + "complex-category-electronics", + "brand-northstar", + ComplexProductStatus.Active, + ), + this.product( + "complex-product-desk", + "org-acme", + "Modular Standing Desk", + "599.00", + "complex-category-office", + "brand-contoso", + ComplexProductStatus.Draft, + ), + this.product( + "globex-product-lamp", + "org-globex", + "Ambient Home Lamp", + "89.00", + "globex-category-home", + "brand-globex", + ComplexProductStatus.Active, + ), + ]; + this.products = + preset === "large" + ? Array.from({ length: 250 }, (_, index) => { + const source = baseProducts[index % 3]; + const number = index + 1; + return { + ...source, + id: `complex-product-${String(number).padStart(3, "0")}`, + name: `${source.name} ${number}`, + slug: `${source.slug}-${number}`, + variants: source.variants.map((variant) => ({ + ...variant, + id: `${variant.id}-${number}`, + sku: `${variant.sku}-${number}`, + })), + }; + }) + : baseProducts; + this.warehouses = [ + { + id: "warehouse-berlin", + organizationId: "org-acme", + name: "Berlin Warehouse", + countryCode: "DE", + status: "active", + }, + { + id: "warehouse-paris", + organizationId: "org-acme", + name: "Paris Overflow", + countryCode: "FR", + status: "maintenance", + }, + { + id: "warehouse-new-york", + organizationId: "org-globex", + name: "New York Warehouse", + countryCode: "US", + status: "active", + }, + ]; + this.inventory = [ + { + id: "inventory-keyboard-berlin", + productId: "complex-product-keyboard", + variantId: "variant-complex-product-keyboard-default", + warehouseId: "warehouse-berlin", + available: 42, + reserved: 5, + reorderPoint: 10, + version: 2, + }, + { + id: "inventory-mouse-berlin", + productId: "complex-product-mouse", + variantId: "variant-complex-product-mouse-default", + warehouseId: "warehouse-berlin", + available: 8, + reserved: 2, + reorderPoint: 12, + version: 1, + }, + { + id: "inventory-lamp-new-york", + productId: "globex-product-lamp", + variantId: "variant-globex-product-lamp-default", + warehouseId: "warehouse-new-york", + available: 30, + reserved: 0, + reorderPoint: 5, + version: 1, + }, + ]; + const baseCustomers: CustomerDto[] = [ + { + id: "customer-ada", + organizationId: "org-acme", + name: "Ada Lovelace", + email: "ada@example.test", + defaultAddress: { + line1: "Friedrichstrasse 100", + line2: null, + city: "Berlin", + postalCode: "10117", + countryCode: "DE", + }, + tags: ["vip", "newsletter"], + createdAt: "2026-01-10T09:00:00.000Z", + }, + { + id: "customer-grace", + organizationId: "org-acme", + name: "Grace Hopper", + email: "grace@example.test", + defaultAddress: { + line1: "1 Compiler Lane", + line2: "Suite 5", + city: "Paris", + postalCode: "75001", + countryCode: "FR", + }, + tags: ["b2b"], + createdAt: "2026-02-12T11:30:00.000Z", + }, + { + id: "customer-katherine", + organizationId: "org-acme", + name: "Katherine Johnson", + email: "katherine@example.test", + defaultAddress: { + line1: "42 Orbit Road", + line2: null, + city: "London", + postalCode: "SW1A 1AA", + countryCode: "GB", + }, + tags: [], + createdAt: "2026-03-15T08:00:00.000Z", + }, + ]; + this.customers = + preset === "large" + ? Array.from({ length: 250 }, (_, index) => { + const source = baseCustomers[index % baseCustomers.length]; + return { + ...source, + id: `customer-${String(index + 1).padStart(3, "0")}`, + name: `${source.name} ${index + 1}`, + email: `customer${index + 1}@example.test`, + }; + }) + : baseCustomers; + this.orders = [ + { + id: "complex-order-001", + organizationId: "org-acme", + customerId: "customer-ada", + status: ComplexOrderStatus.Paid, + items: [ + { + productId: "complex-product-keyboard", + variantId: "variant-complex-product-keyboard-default", + name: "Pro Mechanical Keyboard", + quantity: 1, + unitPrice: { amount: "129.90", currency: "USD" }, + }, + ], + subtotal: { amount: "129.90", currency: "USD" }, + discount: { amount: "0.00", currency: "USD" }, + total: { amount: "129.90", currency: "USD" }, + shippingAddress: baseCustomers[0].defaultAddress, + createdAt: "2026-07-20T10:00:00.000Z", + version: 1, + }, + { + id: "complex-order-002", + organizationId: "org-acme", + customerId: "customer-grace", + status: ComplexOrderStatus.Shipped, + items: [ + { + productId: "complex-product-mouse", + variantId: "variant-complex-product-mouse-default", + name: "Precision Mouse", + quantity: 2, + unitPrice: { amount: "79.90", currency: "USD" }, + }, + ], + subtotal: { amount: "159.80", currency: "USD" }, + discount: { amount: "15.98", currency: "USD" }, + total: { amount: "143.82", currency: "USD" }, + shippingAddress: baseCustomers[1].defaultAddress, + createdAt: "2026-07-22T12:30:00.000Z", + version: 2, + }, + ]; + this.payments = [ + { + id: "payment-001", + orderId: "complex-order-001", + status: "succeeded", + method: "card", + amount: { amount: "129.90", currency: "USD" }, + createdAt: "2026-07-20T10:03:00.000Z", + }, + { + id: "payment-002", + orderId: "complex-order-002", + status: "succeeded", + method: "bank-transfer", + amount: { amount: "143.82", currency: "USD" }, + createdAt: "2026-07-22T12:45:00.000Z", + }, + ]; + this.promotions = [ + { + id: "promotion-welcome", + code: "WELCOME10", + type: "percentage", + value: "10.00", + validUntil: "2027-01-01T00:00:00.000Z", + active: true, + }, + { + id: "promotion-old", + code: "OLD20", + type: "percentage", + value: "20.00", + validUntil: "2025-01-01T00:00:00.000Z", + active: false, + }, + ]; + this.reviews = [ + { + id: "review-001", + organizationId: "org-acme", + productId: "complex-product-keyboard", + customerId: "customer-ada", + rating: 5, + comment: "Excellent keyboard for daily development.", + status: "published", + createdAt: "2026-07-21T10:00:00.000Z", + }, + ]; + this.notifications = [ + { + id: "notification-001", + organizationId: "org-acme", + kind: NotificationKind.Order, + title: "Order shipped", + payload: { + type: "order", + orderId: "complex-order-002", + status: "shipped", + }, + read: false, + createdAt: "2026-07-23T09:00:00.000Z", + }, + { + id: "notification-002", + organizationId: "org-acme", + kind: NotificationKind.Inventory, + title: "Low stock", + payload: { + type: "inventory", + productId: "complex-product-mouse", + remaining: 8, + }, + read: false, + createdAt: "2026-07-23T08:00:00.000Z", + }, + { + id: "notification-003", + organizationId: "org-acme", + kind: NotificationKind.System, + title: "Maintenance", + payload: { + type: "system", + text: "Scheduled maintenance begins at 02:00 UTC.", + }, + read: true, + createdAt: "2026-07-22T16:00:00.000Z", + }, + ]; + this.files = [ + { + id: "file-001", + organizationId: "org-acme", + name: "products.csv", + mimeType: "text/csv", + size: 45, + downloadUrl: "/api/v1/files/file-001/download", + createdAt: "2026-07-24T10:00:00.000Z", + }, + ]; + this.fileContents = new Map([ + ["file-001", Buffer.from("id,name\ncomplex-product-keyboard,Keyboard\n")], + ]); + this.auditEvents = [ + { + id: "audit-001", + organizationId: "org-acme", + action: "order.created", + actorId: "complex-user-admin", + resourceType: "order", + resourceId: "complex-order-001", + metadata: { source: "seed" }, + createdAt: "2026-07-20T10:00:00.000Z", + }, + { + id: "audit-002", + organizationId: "org-acme", + action: "product.updated", + actorId: "complex-user-manager", + resourceType: "product", + resourceId: "complex-product-keyboard", + metadata: { version: 3 }, + createdAt: "2026-07-19T10:00:00.000Z", + }, + ]; + this.jobs = []; + this.conversations = [ + { + id: "conversation-support", + organizationId: "org-acme", + title: "Order support", + participantIds: ["complex-user-admin", "complex-user-support"], + lastMessagePreview: "Can you check order complex-order-001?", + updatedAt: "2026-07-25T10:05:00.000Z", + }, + ]; + this.messages = [ + { + id: "message-001", + conversationId: "conversation-support", + senderId: "complex-user-admin", + text: "Can you check order complex-order-001?", + clientMessageId: "seed-message-001", + createdAt: "2026-07-25T10:05:00.000Z", + }, + ]; + this.orderIdempotency.clear(); + this.sequences = { + product: this.products.length + 20, + order: 20, + review: 20, + file: 20, + audit: 20, + job: 20, + message: 20, + }; + } + + findUserByEmail(email: string): ComplexUserRecord | undefined { + return this.users.find( + (user) => user.email.toLowerCase() === email.toLowerCase(), + ); + } + + findUserById(id: string): ComplexUserRecord | undefined { + return this.users.find((user) => user.id === id); + } + + publicUser(user: ComplexUserRecord): ComplexUserDto { + const { password: _password, ...publicUser } = user; + return publicUser; + } + + listUsers(organizationId: string): ComplexUserDto[] { + return this.users + .filter((user) => user.organizationIds.includes(organizationId)) + .map((user) => this.publicUser(user)); + } + + changeUserRole(userId: string, role: ComplexRole): ComplexUserDto { + const user = this.findUserById(userId); + if (!user) + throw new NotFoundException({ + code: "USER_NOT_FOUND", + message: "User not found.", + }); + user.role = role; + for (const member of this.members.filter((item) => item.userId === userId)) + member.role = role; + return this.publicUser(user); + } + + isMember(userId: string, organizationId: string): boolean { + return this.members.some( + (member) => + member.userId === userId && + member.organizationId === organizationId && + member.status === "active", + ); + } + + listOrganizations(userId: string): OrganizationDto[] { + const user = this.findUserById(userId); + return this.organizations.filter((organization) => + user?.organizationIds.includes(organization.id), + ); + } + + getOrganization(id: string, userId: string): OrganizationDto { + const organization = this.organizations.find( + (item) => item.id === id && this.isMember(userId, id), + ); + if (!organization) + throw new NotFoundException({ + code: "ORGANIZATION_NOT_FOUND", + message: "Organization not found or unavailable.", + }); + return organization; + } + + listMembers(organizationId: string): MemberDto[] { + return this.members.filter( + (member) => member.organizationId === organizationId, + ); + } + + listProducts(organizationId: string, query: CursorProductQueryDto) { + let products = this.products.filter( + (product) => product.organizationId === organizationId, + ); + if (query.search) + products = products.filter((product) => + `${product.name} ${product.description}` + .toLowerCase() + .includes(query.search!.toLowerCase()), + ); + if (query.status) + products = products.filter((product) => product.status === query.status); + return this.cursorPage(products, query.cursor, query.limit); + } + + getProduct(organizationId: string, id: string): ComplexProductDto { + const product = this.products.find( + (item) => item.id === id && item.organizationId === organizationId, + ); + if (!product) + throw new NotFoundException({ + code: "PRODUCT_NOT_FOUND", + message: "Product not found.", + }); + return product; + } + + createProduct( + organizationId: string, + actorId: string, + dto: CreateComplexProductDto, + ): ComplexProductDto { + if ( + !this.categories.some( + (category) => + category.id === dto.categoryId && + category.organizationId === organizationId, + ) + ) { + throw new UnprocessableEntityException({ + code: "CATEGORY_NOT_FOUND", + message: "Selected category does not exist in this organization.", + details: [{ field: "categoryId", message: "Unknown tenant category." }], + }); + } + const id = `complex-product-${this.sequences.product++}`; + const organization = this.organizations.find( + (item) => item.id === organizationId, + )!; + const product: ComplexProductDto = { + id, + organizationId, + name: dto.name, + slug: `${dto.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${id}`, + description: dto.description, + status: dto.status, + categoryId: dto.categoryId, + brandId: dto.brandId, + price: { amount: dto.priceAmount, currency: organization.currency }, + variants: dto.variants.map((variant, index) => ({ + id: `${id}-variant-${index + 1}`, + sku: variant.sku, + attributes: variant.attributes, + price: { amount: variant.priceAmount, currency: organization.currency }, + })), + tags: dto.tags, + publishedAt: + dto.status === ComplexProductStatus.Active + ? new Date().toISOString() + : null, + createdAt: new Date().toISOString(), + version: 1, + }; + this.products.unshift(product); + this.addAudit(organizationId, actorId, "product.created", "product", id, { + version: 1, + }); + return product; + } + + updateProduct( + organizationId: string, + actorId: string, + id: string, + dto: UpdateComplexProductDto, + ): ComplexProductDto { + const product = this.getProduct(organizationId, id); + if (product.version !== dto.version) { + throw new ConflictException({ + code: "PRODUCT_VERSION_CONFLICT", + message: "Product was changed by another user.", + details: [ + { + field: "version", + message: `Current version is ${product.version}.`, + }, + ], + }); + } + const { version: _version, priceAmount, variants, ...changes } = dto; + Object.assign(product, changes); + if (priceAmount) product.price.amount = priceAmount; + if (variants) + product.variants = variants.map((variant, index) => ({ + id: `${id}-variant-${index + 1}`, + sku: variant.sku, + attributes: variant.attributes, + price: { + amount: variant.priceAmount, + currency: product.price.currency, + }, + })); + if (dto.status === ComplexProductStatus.Active && !product.publishedAt) + product.publishedAt = new Date().toISOString(); + product.version += 1; + this.addAudit(organizationId, actorId, "product.updated", "product", id, { + version: product.version, + }); + return product; + } + + listCategories(organizationId: string): ComplexCategoryDto[] { + return this.categories.filter( + (category) => category.organizationId === organizationId, + ); + } + + listBrands(organizationId: string): BrandDto[] { + return this.brands.filter( + (brand) => brand.organizationId === organizationId, + ); + } + + listWarehouses(organizationId: string): WarehouseDto[] { + return this.warehouses.filter( + (warehouse) => warehouse.organizationId === organizationId, + ); + } + + listInventory( + organizationId: string, + productId?: string, + ): InventoryItemDto[] { + const warehouseIds = new Set( + this.listWarehouses(organizationId).map((warehouse) => warehouse.id), + ); + return this.inventory.filter( + (item) => + warehouseIds.has(item.warehouseId) && + (!productId || item.productId === productId), + ); + } + + adjustInventory( + organizationId: string, + actorId: string, + id: string, + dto: AdjustInventoryDto, + ): InventoryItemDto { + const item = this.listInventory(organizationId).find( + (inventory) => inventory.id === id, + ); + if (!item) + throw new NotFoundException({ + code: "INVENTORY_NOT_FOUND", + message: "Inventory item not found.", + }); + if (item.version !== dto.version) + throw new ConflictException({ + code: "INVENTORY_VERSION_CONFLICT", + message: "Inventory was changed by another user.", + }); + if (item.available + dto.delta < 0) + throw new ConflictException({ + code: "NEGATIVE_INVENTORY", + message: "Adjustment would make available stock negative.", + }); + item.available += dto.delta; + item.version += 1; + this.addAudit( + organizationId, + actorId, + "inventory.adjusted", + "inventory", + id, + { delta: dto.delta, reason: dto.reason, version: item.version }, + ); + return item; + } + + listCustomers(organizationId: string, query: CustomerQueryDto) { + let customers = this.customers.filter( + (customer) => customer.organizationId === organizationId, + ); + if (query.search) + customers = customers.filter((customer) => + `${customer.name} ${customer.email}` + .toLowerCase() + .includes(query.search!.toLowerCase()), + ); + const total = customers.length; + const start = (query.page - 1) * query.limit; + return { + data: customers.slice(start, start + query.limit), + meta: { + page: query.page, + limit: query.limit, + total, + totalPages: Math.ceil(total / query.limit), + }, + }; + } + + getCustomer(organizationId: string, id: string): CustomerDto { + const customer = this.customers.find( + (item) => item.id === id && item.organizationId === organizationId, + ); + if (!customer) + throw new NotFoundException({ + code: "CUSTOMER_NOT_FOUND", + message: "Customer not found.", + }); + return customer; + } + + listOrders(organizationId: string, query: OrderCursorQueryDto) { + let orders = this.orders.filter( + (order) => order.organizationId === organizationId, + ); + if (query.status) + orders = orders.filter((order) => order.status === query.status); + return this.cursorPage(orders, query.cursor, query.limit); + } + + getOrder(organizationId: string, id: string): ComplexOrderDto { + const order = this.orders.find( + (item) => item.id === id && item.organizationId === organizationId, + ); + if (!order) + throw new NotFoundException({ + code: "ORDER_NOT_FOUND", + message: "Order not found.", + }); + return order; + } + + createOrder( + organizationId: string, + actorId: string, + idempotencyKey: string, + dto: CreateComplexOrderDto, + ): ComplexOrderDto { + const previousOrderId = this.orderIdempotency.get( + `${organizationId}:${idempotencyKey}`, + ); + if (previousOrderId) return this.getOrder(organizationId, previousOrderId); + const customer = this.getCustomer(organizationId, dto.customerId); + if (dto.items.length === 0) + throw new UnprocessableEntityException({ + code: "EMPTY_ORDER", + message: "Order must contain at least one item.", + details: [{ field: "items", message: "Add at least one item." }], + }); + const organization = this.organizations.find( + (item) => item.id === organizationId, + )!; + const items = dto.items.map((input) => { + const product = this.getProduct(organizationId, input.productId); + const variant = product.variants.find( + (item) => item.id === input.variantId, + ); + if (!variant) + throw new UnprocessableEntityException({ + code: "VARIANT_NOT_FOUND", + message: `Variant ${input.variantId} does not exist.`, + details: [{ field: "items", message: "Unknown product variant." }], + }); + return { + productId: product.id, + variantId: variant.id, + name: product.name, + quantity: input.quantity, + unitPrice: variant.price, + }; + }); + const subtotalCents = items.reduce( + (sum, item) => + sum + this.moneyToCents(item.unitPrice.amount) * item.quantity, + 0, + ); + const promotion = dto.promotionCode + ? this.promotions.find( + (item) => item.code === dto.promotionCode && item.active, + ) + : undefined; + const discountCents = + promotion?.type === "percentage" + ? Math.round((subtotalCents * Number(promotion.value)) / 100) + : promotion + ? this.moneyToCents(promotion.value) + : 0; + const id = `complex-order-${String(this.sequences.order++).padStart(3, "0")}`; + const order: ComplexOrderDto = { + id, + organizationId, + customerId: customer.id, + status: ComplexOrderStatus.AwaitingPayment, + items, + subtotal: { + amount: this.centsToMoney(subtotalCents), + currency: organization.currency, + }, + discount: { + amount: this.centsToMoney(discountCents), + currency: organization.currency, + }, + total: { + amount: this.centsToMoney(Math.max(0, subtotalCents - discountCents)), + currency: organization.currency, + }, + shippingAddress: customer.defaultAddress, + createdAt: new Date().toISOString(), + version: 1, + }; + this.orders.unshift(order); + this.orderIdempotency.set(`${organizationId}:${idempotencyKey}`, id); + this.addAudit(organizationId, actorId, "order.created", "order", id, { + idempotencyKey, + }); + return order; + } + + cancelOrder( + organizationId: string, + actorId: string, + id: string, + ): ComplexOrderDto { + const order = this.getOrder(organizationId, id); + if ( + [ComplexOrderStatus.Shipped, ComplexOrderStatus.Cancelled].includes( + order.status, + ) + ) { + throw new ConflictException({ + code: "ORDER_CANNOT_BE_CANCELLED", + message: `Order in ${order.status} status cannot be cancelled.`, + }); + } + order.status = ComplexOrderStatus.Cancelled; + order.version += 1; + this.addAudit(organizationId, actorId, "order.cancelled", "order", id, { + version: order.version, + }); + return order; + } + + listPayments(organizationId: string): PaymentDto[] { + const orderIds = new Set( + this.orders + .filter((order) => order.organizationId === organizationId) + .map((order) => order.id), + ); + return this.payments.filter((payment) => orderIds.has(payment.orderId)); + } + + listPromotions(): PromotionDto[] { + return this.promotions; + } + + listReviews(organizationId: string, productId?: string): ReviewDto[] { + return this.reviews.filter( + (review) => + review.organizationId === organizationId && + (!productId || review.productId === productId), + ); + } + + createReview(organizationId: string, dto: CreateReviewDto): ReviewDto { + this.getProduct(organizationId, dto.productId); + this.getCustomer(organizationId, dto.customerId); + const review: TenantReview = { + id: `review-${this.sequences.review++}`, + organizationId, + ...dto, + status: "pending", + createdAt: new Date().toISOString(), + }; + this.reviews.unshift(review); + return review; + } + + listNotifications(organizationId: string, cursor?: string, limit = 20) { + return this.cursorPage( + this.notifications.filter( + (notification) => notification.organizationId === organizationId, + ), + cursor, + limit, + ); + } + + markNotificationRead(organizationId: string, id: string): NotificationDto { + const notification = this.notifications.find( + (item) => item.id === id && item.organizationId === organizationId, + ); + if (!notification) + throw new NotFoundException({ + code: "NOTIFICATION_NOT_FOUND", + message: "Notification not found.", + }); + notification.read = true; + return notification; + } + + listFiles(organizationId: string): FileMetadataDto[] { + return this.files.filter((file) => file.organizationId === organizationId); + } + + addFile(organizationId: string, file: Express.Multer.File): FileMetadataDto { + const id = `file-${this.sequences.file++}`; + const metadata: TenantFile = { + id, + organizationId, + name: file.originalname, + mimeType: file.mimetype, + size: file.size, + downloadUrl: `/api/v1/files/${id}/download`, + createdAt: new Date().toISOString(), + }; + this.files.unshift(metadata); + this.fileContents.set(id, file.buffer); + return metadata; + } + + getFile( + organizationId: string, + id: string, + ): { metadata: FileMetadataDto; content: Buffer } { + const metadata = this.files.find( + (file) => file.id === id && file.organizationId === organizationId, + ); + const content = this.fileContents.get(id); + if (!metadata || !content) + throw new NotFoundException({ + code: "FILE_NOT_FOUND", + message: "File not found.", + }); + return { metadata, content }; + } + + listAudit(organizationId: string, query: AuditQueryDto) { + let events = this.auditEvents.filter( + (event) => event.organizationId === organizationId, + ); + if (query.action) + events = events.filter((event) => event.action === query.action); + const total = events.length; + const start = (query.page - 1) * query.limit; + return { + data: events.slice(start, start + query.limit), + meta: { + page: query.page, + limit: query.limit, + total, + totalPages: Math.ceil(total / query.limit), + }, + }; + } + + startOrdersExport(organizationId: string): JobDto { + const now = new Date(); + const job: TenantJob = { + id: `job-${this.sequences.job++}`, + organizationId, + type: "orders-export", + status: JobStatus.Pending, + progress: 0, + resultUrl: null, + error: null, + createdAt: now.toISOString(), + updatedAt: now.toISOString(), + startedAtMs: Date.now(), + }; + this.jobs.unshift(job); + return job; + } + + getJob(organizationId: string, id: string): JobDto { + const job = this.jobs.find( + (item) => item.id === id && item.organizationId === organizationId, + ); + if (!job) + throw new NotFoundException({ + code: "JOB_NOT_FOUND", + message: "Background job not found.", + }); + const elapsed = Date.now() - job.startedAtMs; + if (elapsed >= 800) { + job.status = JobStatus.Completed; + job.progress = 100; + job.resultUrl = `/api/v1/jobs/${job.id}/result`; + } else if (elapsed >= 200) { + job.status = JobStatus.Processing; + job.progress = Math.min(90, Math.max(20, Math.floor(elapsed / 8))); + } + job.updatedAt = new Date().toISOString(); + return job; + } + + jobResult(organizationId: string, id: string): Buffer { + const job = this.getJob(organizationId, id); + if (job.status !== JobStatus.Completed) + throw new ConflictException({ + code: "JOB_NOT_COMPLETED", + message: "The export is not ready yet.", + }); + const rows = this.orders + .filter((order) => order.organizationId === organizationId) + .map((order) => `${order.id},${order.status},${order.total.amount}`); + return Buffer.from(`id,status,total\n${rows.join("\n")}\n`); + } + + listConversations(organizationId: string, userId: string): ConversationDto[] { + return this.conversations.filter( + (conversation) => + conversation.organizationId === organizationId && + conversation.participantIds.includes(userId), + ); + } + + getConversation( + organizationId: string, + userId: string, + id: string, + ): ConversationDto { + const conversation = this.listConversations(organizationId, userId).find( + (item) => item.id === id, + ); + if (!conversation) + throw new NotFoundException({ + code: "CONVERSATION_NOT_FOUND", + message: "Conversation not found.", + }); + return conversation; + } + + listMessages( + organizationId: string, + userId: string, + conversationId: string, + cursor?: string, + limit = 30, + ) { + this.getConversation(organizationId, userId, conversationId); + return this.cursorPage( + this.messages.filter( + (message) => message.conversationId === conversationId, + ), + cursor, + limit, + ); + } + + sendMessage( + organizationId: string, + userId: string, + conversationId: string, + dto: SendMessageDto, + ): ChatMessageDto { + const conversation = this.getConversation( + organizationId, + userId, + conversationId, + ); + const duplicate = this.messages.find( + (message) => + message.conversationId === conversationId && + message.clientMessageId === dto.clientMessageId, + ); + if (duplicate) return duplicate; + const message: ChatMessageDto = { + id: `message-${this.sequences.message++}`, + conversationId, + senderId: userId, + text: dto.text, + clientMessageId: dto.clientMessageId, + createdAt: new Date().toISOString(), + }; + this.messages.push(message); + conversation.lastMessagePreview = dto.text; + conversation.updatedAt = message.createdAt; + return message; + } + + private product( + id: string, + organizationId: string, + name: string, + amount: string, + categoryId: string, + brandId: string, + status: ComplexProductStatus, + ): ComplexProductDto { + const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-"); + const currency = organizationId === "org-acme" ? "USD" : "EUR"; + return { + id, + organizationId, + name, + slug, + description: `${name} demonstrates variants, tenant scope and optimistic updates.`, + status, + categoryId, + brandId, + price: { amount, currency }, + variants: [ + { + id: `variant-${id}-default`, + sku: `${id.toUpperCase()}-DEFAULT`, + attributes: { color: "black", size: "standard" }, + price: { amount, currency }, + }, + ], + tags: status === ComplexProductStatus.Active ? ["featured"] : ["draft"], + publishedAt: + status === ComplexProductStatus.Active + ? "2026-07-10T09:00:00.000Z" + : null, + createdAt: "2026-07-01T09:00:00.000Z", + version: status === ComplexProductStatus.Active ? 3 : 1, + }; + } + + private cursorPage<T extends { id: string }>( + items: T[], + cursor: string | undefined, + limit: number, + ) { + const cursorIndex = cursor + ? items.findIndex((item) => item.id === cursor) + : -1; + const start = cursorIndex >= 0 ? cursorIndex + 1 : 0; + const data = items.slice(start, start + limit); + const hasMore = start + data.length < items.length; + return { + data, + meta: { + limit, + nextCursor: hasMore ? (data.at(-1)?.id ?? null) : null, + hasMore, + }, + }; + } + + private addAudit( + organizationId: string, + actorId: string, + action: string, + resourceType: string, + resourceId: string, + metadata: Record<string, unknown>, + ): void { + this.auditEvents.unshift({ + id: `audit-${this.sequences.audit++}`, + organizationId, + action, + actorId, + resourceType, + resourceId, + metadata, + createdAt: new Date().toISOString(), + }); + } + + private moneyToCents(amount: string): number { + const [whole, fraction = "0"] = amount.split("."); + return Number(whole) * 100 + Number(fraction.padEnd(2, "0").slice(0, 2)); + } + + private centsToMoney(cents: number): string { + return `${Math.floor(cents / 100)}.${String(cents % 100).padStart(2, "0")}`; + } +} diff --git a/examples/demo-backend/src/apps/complex/controllers/catalog.controllers.ts b/examples/demo-backend/src/apps/complex/controllers/catalog.controllers.ts new file mode 100644 index 0000000..ae68602 --- /dev/null +++ b/examples/demo-backend/src/apps/complex/controllers/catalog.controllers.ts @@ -0,0 +1,217 @@ +import { + Body, + Controller, + Get, + Headers, + Param, + Patch, + Post, + Query, + Req, + Res, + UseGuards, +} from "@nestjs/common"; +import { + ApiCookieAuth, + ApiCreatedResponse, + ApiNotModifiedResponse, + ApiOkResponse, + ApiOperation, + ApiSecurity, + ApiTags, +} from "@nestjs/swagger"; +import type { Response } from "express"; +import { + ApiDemoScenarioHeader, + ApiOrganizationHeader, + ApiStandardErrors, +} from "../../../common/api.decorators"; +import { + ComplexCookieGuard, + ComplexCsrfGuard, + type ComplexDemoRequest, + ComplexOrganizationGuard, + ComplexRoles, + ComplexRolesGuard, +} from "../complex.auth"; +import { ComplexStore } from "../complex.store"; +import { + AdjustInventoryDto, + BrandsResponseDto, + ComplexCategoriesResponseDto, + ComplexProductResponseDto, + ComplexProductsResponseDto, + CreateComplexProductDto, + CursorProductQueryDto, + InventoryItemResponseDto, + InventoryResponseDto, + UpdateComplexProductDto, + WarehousesResponseDto, +} from "../dto/catalog.dto"; +import { ComplexRole } from "../dto/identity.dto"; + +@ApiTags("Products") +@ApiCookieAuth("cookieSession") +@ApiOrganizationHeader() +@ApiDemoScenarioHeader() +@UseGuards(ComplexCookieGuard, ComplexOrganizationGuard) +@Controller("products") +export class ComplexProductsController { + constructor(private readonly store: ComplexStore) {} + + @Get() + @ApiOperation({ summary: "List tenant products using cursor pagination" }) + @ApiOkResponse({ type: ComplexProductsResponseDto }) + @ApiStandardErrors({ auth: true }) + list( + @Req() request: ComplexDemoRequest, + @Query() query: CursorProductQueryDto, + ): ComplexProductsResponseDto { + return this.store.listProducts(request.organizationId!, query); + } + + @Get(":id") + @ApiOperation({ + summary: "Get a rich product with variants and ETag support", + }) + @ApiOkResponse({ type: ComplexProductResponseDto }) + @ApiNotModifiedResponse() + @ApiStandardErrors({ auth: true, notFound: true }) + get( + @Req() request: ComplexDemoRequest, + @Param("id") id: string, + @Headers("if-none-match") ifNoneMatch: string | undefined, + @Res({ passthrough: true }) response: Response, + ): ComplexProductResponseDto | undefined { + const product = this.store.getProduct(request.organizationId!, id); + const etag = `W/\"${product.id}-v${product.version}\"`; + response.setHeader("ETag", etag); + response.setHeader("Cache-Control", "private, max-age=0, must-revalidate"); + if (etag === ifNoneMatch) { + response.status(304); + return undefined; + } + return { data: product }; + } + + @Post() + @UseGuards(ComplexRolesGuard, ComplexCsrfGuard) + @ComplexRoles(ComplexRole.Admin, ComplexRole.Manager) + @ApiSecurity("csrf") + @ApiOperation({ summary: "Create a tenant product with variants" }) + @ApiCreatedResponse({ type: ComplexProductResponseDto }) + @ApiStandardErrors({ auth: true }) + create( + @Req() request: ComplexDemoRequest, + @Body() dto: CreateComplexProductDto, + ): ComplexProductResponseDto { + return { + data: this.store.createProduct( + request.organizationId!, + request.user!.id, + dto, + ), + }; + } + + @Patch(":id") + @UseGuards(ComplexRolesGuard, ComplexCsrfGuard) + @ComplexRoles(ComplexRole.Admin, ComplexRole.Manager) + @ApiSecurity("csrf") + @ApiOperation({ summary: "Update a product using optimistic locking" }) + @ApiOkResponse({ type: ComplexProductResponseDto }) + @ApiStandardErrors({ auth: true, notFound: true, conflict: true }) + update( + @Req() request: ComplexDemoRequest, + @Param("id") id: string, + @Body() dto: UpdateComplexProductDto, + ): ComplexProductResponseDto { + return { + data: this.store.updateProduct( + request.organizationId!, + request.user!.id, + id, + dto, + ), + }; + } +} + +@ApiTags("Catalog") +@ApiCookieAuth("cookieSession") +@ApiOrganizationHeader() +@ApiDemoScenarioHeader() +@UseGuards(ComplexCookieGuard, ComplexOrganizationGuard) +@Controller() +export class ComplexCatalogController { + constructor(private readonly store: ComplexStore) {} + + @Get("categories") + @ApiOperation({ summary: "List a recursive category tree" }) + @ApiOkResponse({ type: ComplexCategoriesResponseDto }) + @ApiStandardErrors({ auth: true }) + categories(@Req() request: ComplexDemoRequest): ComplexCategoriesResponseDto { + return { data: this.store.listCategories(request.organizationId!) }; + } + + @Get("brands") + @ApiOperation({ summary: "List product brands" }) + @ApiOkResponse({ type: BrandsResponseDto }) + @ApiStandardErrors({ auth: true }) + brands(@Req() request: ComplexDemoRequest): BrandsResponseDto { + return { data: this.store.listBrands(request.organizationId!) }; + } +} + +@ApiTags("Inventory") +@ApiCookieAuth("cookieSession") +@ApiOrganizationHeader() +@ApiDemoScenarioHeader() +@UseGuards(ComplexCookieGuard, ComplexOrganizationGuard) +@Controller() +export class ComplexInventoryController { + constructor(private readonly store: ComplexStore) {} + + @Get("warehouses") + @ApiOperation({ summary: "List tenant warehouses" }) + @ApiOkResponse({ type: WarehousesResponseDto }) + @ApiStandardErrors({ auth: true }) + warehouses(@Req() request: ComplexDemoRequest): WarehousesResponseDto { + return { data: this.store.listWarehouses(request.organizationId!) }; + } + + @Get("inventory") + @ApiOperation({ summary: "List inventory with optional product filter" }) + @ApiOkResponse({ type: InventoryResponseDto }) + @ApiStandardErrors({ auth: true }) + inventory( + @Req() request: ComplexDemoRequest, + @Query("productId") productId?: string, + ): InventoryResponseDto { + return { + data: this.store.listInventory(request.organizationId!, productId), + }; + } + + @Post("inventory/:id/adjust") + @UseGuards(ComplexRolesGuard, ComplexCsrfGuard) + @ComplexRoles(ComplexRole.Admin, ComplexRole.Manager) + @ApiSecurity("csrf") + @ApiOperation({ summary: "Adjust stock using optimistic locking" }) + @ApiOkResponse({ type: InventoryItemResponseDto }) + @ApiStandardErrors({ auth: true, notFound: true, conflict: true }) + adjust( + @Req() request: ComplexDemoRequest, + @Param("id") id: string, + @Body() dto: AdjustInventoryDto, + ): InventoryItemResponseDto { + return { + data: this.store.adjustInventory( + request.organizationId!, + request.user!.id, + id, + dto, + ), + }; + } +} diff --git a/examples/demo-backend/src/apps/complex/controllers/chat.controller.ts b/examples/demo-backend/src/apps/complex/controllers/chat.controller.ts new file mode 100644 index 0000000..a41f567 --- /dev/null +++ b/examples/demo-backend/src/apps/complex/controllers/chat.controller.ts @@ -0,0 +1,125 @@ +import { + Body, + Controller, + Get, + HttpCode, + Param, + Post, + Query, + Req, + UseGuards, +} from "@nestjs/common"; +import { + ApiCookieAuth, + ApiOkResponse, + ApiOperation, + ApiSecurity, + ApiTags, +} from "@nestjs/swagger"; +import { + ApiDemoScenarioHeader, + ApiOrganizationHeader, + ApiStandardErrors, +} from "../../../common/api.decorators"; +import { ChatGateway } from "../chat.gateway"; +import { + ComplexCookieGuard, + ComplexCsrfGuard, + type ComplexDemoRequest, + ComplexOrganizationGuard, +} from "../complex.auth"; +import { ComplexStore } from "../complex.store"; +import { + ConversationResponseDto, + ConversationsResponseDto, + MessageResponseDto, + MessagesResponseDto, + SendMessageDto, +} from "../dto/chat.dto"; +import { CursorQueryDto } from "../dto/operations.dto"; + +@ApiTags("Chat") +@ApiCookieAuth("cookieSession") +@ApiOrganizationHeader() +@ApiDemoScenarioHeader() +@UseGuards(ComplexCookieGuard, ComplexOrganizationGuard) +@Controller("conversations") +export class ChatController { + constructor( + private readonly store: ComplexStore, + private readonly gateway: ChatGateway, + ) {} + + @Get() + @ApiOperation({ summary: "List conversations available to the current user" }) + @ApiOkResponse({ type: ConversationsResponseDto }) + @ApiStandardErrors({ auth: true }) + list(@Req() request: ComplexDemoRequest): ConversationsResponseDto { + return { + data: this.store.listConversations( + request.organizationId!, + request.user!.id, + ), + }; + } + + @Get(":id") + @ApiOperation({ summary: "Get one conversation" }) + @ApiOkResponse({ type: ConversationResponseDto }) + @ApiStandardErrors({ auth: true, notFound: true }) + get( + @Req() request: ComplexDemoRequest, + @Param("id") id: string, + ): ConversationResponseDto { + return { + data: this.store.getConversation( + request.organizationId!, + request.user!.id, + id, + ), + }; + } + + @Get(":id/messages") + @ApiOperation({ summary: "Load message history using cursor pagination" }) + @ApiOkResponse({ type: MessagesResponseDto }) + @ApiStandardErrors({ auth: true, notFound: true }) + messages( + @Req() request: ComplexDemoRequest, + @Param("id") id: string, + @Query() query: CursorQueryDto, + ): MessagesResponseDto { + return this.store.listMessages( + request.organizationId!, + request.user!.id, + id, + query.cursor, + query.limit, + ); + } + + @Post(":id/messages") + @HttpCode(200) + @UseGuards(ComplexCsrfGuard) + @ApiSecurity("csrf") + @ApiOperation({ + summary: + "Send an idempotent message through REST and broadcast it over Socket.IO", + }) + @ApiOkResponse({ type: MessageResponseDto }) + @ApiStandardErrors({ auth: true, notFound: true }) + send( + @Req() request: ComplexDemoRequest, + @Param("id") id: string, + @Body() dto: SendMessageDto, + ): MessageResponseDto { + const message = this.store.sendMessage( + request.organizationId!, + request.user!.id, + id, + dto, + ); + this.gateway.broadcastMessage(message); + return { data: message }; + } +} diff --git a/examples/demo-backend/src/apps/complex/controllers/commerce.controllers.ts b/examples/demo-backend/src/apps/complex/controllers/commerce.controllers.ts new file mode 100644 index 0000000..4327e24 --- /dev/null +++ b/examples/demo-backend/src/apps/complex/controllers/commerce.controllers.ts @@ -0,0 +1,227 @@ +import { + BadRequestException, + Body, + Controller, + Get, + Headers, + HttpCode, + Param, + Post, + Query, + Req, + UseGuards, +} from "@nestjs/common"; +import { + ApiCookieAuth, + ApiCreatedResponse, + ApiOkResponse, + ApiOperation, + ApiSecurity, + ApiTags, +} from "@nestjs/swagger"; +import { + ApiDemoScenarioHeader, + ApiIdempotencyHeader, + ApiOrganizationHeader, + ApiStandardErrors, +} from "../../../common/api.decorators"; +import { + ComplexCookieGuard, + ComplexCsrfGuard, + type ComplexDemoRequest, + ComplexOrganizationGuard, + ComplexRoles, + ComplexRolesGuard, +} from "../complex.auth"; +import { ComplexStore } from "../complex.store"; +import { + ComplexOrderResponseDto, + ComplexOrdersResponseDto, + CreateComplexOrderDto, + CreateReviewDto, + CustomerQueryDto, + CustomerResponseDto, + CustomersResponseDto, + OrderCursorQueryDto, + PaymentsResponseDto, + PromotionsResponseDto, + ReviewResponseDto, + ReviewsResponseDto, +} from "../dto/commerce.dto"; +import { ComplexRole } from "../dto/identity.dto"; + +@ApiTags("Customers") +@ApiCookieAuth("cookieSession") +@ApiOrganizationHeader() +@ApiDemoScenarioHeader() +@UseGuards(ComplexCookieGuard, ComplexOrganizationGuard) +@Controller("customers") +export class CustomersController { + constructor(private readonly store: ComplexStore) {} + + @Get() + @ApiOperation({ + summary: "List customers using offset pagination and search", + }) + @ApiOkResponse({ type: CustomersResponseDto }) + @ApiStandardErrors({ auth: true }) + list( + @Req() request: ComplexDemoRequest, + @Query() query: CustomerQueryDto, + ): CustomersResponseDto { + return this.store.listCustomers(request.organizationId!, query); + } + + @Get(":id") + @ApiOperation({ summary: "Get one customer with nested address" }) + @ApiOkResponse({ type: CustomerResponseDto }) + @ApiStandardErrors({ auth: true, notFound: true }) + get( + @Req() request: ComplexDemoRequest, + @Param("id") id: string, + ): CustomerResponseDto { + return { data: this.store.getCustomer(request.organizationId!, id) }; + } +} + +@ApiTags("Orders") +@ApiCookieAuth("cookieSession") +@ApiOrganizationHeader() +@ApiDemoScenarioHeader() +@UseGuards(ComplexCookieGuard, ComplexOrganizationGuard) +@Controller("orders") +export class ComplexOrdersController { + constructor(private readonly store: ComplexStore) {} + + @Get() + @ApiOperation({ summary: "List tenant orders using cursor pagination" }) + @ApiOkResponse({ type: ComplexOrdersResponseDto }) + @ApiStandardErrors({ auth: true }) + list( + @Req() request: ComplexDemoRequest, + @Query() query: OrderCursorQueryDto, + ): ComplexOrdersResponseDto { + return this.store.listOrders(request.organizationId!, query); + } + + @Get(":id") + @ApiOperation({ summary: "Get one order and its state" }) + @ApiOkResponse({ type: ComplexOrderResponseDto }) + @ApiStandardErrors({ auth: true, notFound: true }) + get( + @Req() request: ComplexDemoRequest, + @Param("id") id: string, + ): ComplexOrderResponseDto { + return { data: this.store.getOrder(request.organizationId!, id) }; + } + + @Post() + @UseGuards(ComplexRolesGuard, ComplexCsrfGuard) + @ComplexRoles(ComplexRole.Admin, ComplexRole.Manager, ComplexRole.Support) + @ApiSecurity("csrf") + @ApiIdempotencyHeader() + @ApiOperation({ summary: "Create an order idempotently" }) + @ApiCreatedResponse({ type: ComplexOrderResponseDto }) + @ApiStandardErrors({ auth: true, conflict: true }) + create( + @Req() request: ComplexDemoRequest, + @Headers("idempotency-key") idempotencyKey: string, + @Body() dto: CreateComplexOrderDto, + ): ComplexOrderResponseDto { + if (!idempotencyKey) { + throw new BadRequestException({ + code: "IDEMPOTENCY_KEY_REQUIRED", + message: "Idempotency-Key header is required.", + }); + } + return { + data: this.store.createOrder( + request.organizationId!, + request.user!.id, + idempotencyKey, + dto, + ), + }; + } + + @Post(":id/cancel") + @HttpCode(200) + @UseGuards(ComplexRolesGuard, ComplexCsrfGuard) + @ComplexRoles(ComplexRole.Admin, ComplexRole.Manager, ComplexRole.Support) + @ApiSecurity("csrf") + @ApiOperation({ summary: "Apply a validated order state transition" }) + @ApiOkResponse({ type: ComplexOrderResponseDto }) + @ApiStandardErrors({ auth: true, notFound: true, conflict: true }) + cancel( + @Req() request: ComplexDemoRequest, + @Param("id") id: string, + ): ComplexOrderResponseDto { + return { + data: this.store.cancelOrder( + request.organizationId!, + request.user!.id, + id, + ), + }; + } +} + +@ApiTags("Payments and promotions") +@ApiCookieAuth("cookieSession") +@ApiOrganizationHeader() +@ApiDemoScenarioHeader() +@UseGuards(ComplexCookieGuard, ComplexOrganizationGuard) +@Controller() +export class PaymentsController { + constructor(private readonly store: ComplexStore) {} + + @Get("payments") + @ApiOperation({ summary: "List payments for tenant orders" }) + @ApiOkResponse({ type: PaymentsResponseDto }) + @ApiStandardErrors({ auth: true }) + payments(@Req() request: ComplexDemoRequest): PaymentsResponseDto { + return { data: this.store.listPayments(request.organizationId!) }; + } + + @Get("promotions") + @ApiOperation({ summary: "List active and expired promotion contracts" }) + @ApiOkResponse({ type: PromotionsResponseDto }) + @ApiStandardErrors({ auth: true }) + promotions(): PromotionsResponseDto { + return { data: this.store.listPromotions() }; + } +} + +@ApiTags("Reviews") +@ApiCookieAuth("cookieSession") +@ApiOrganizationHeader() +@ApiDemoScenarioHeader() +@UseGuards(ComplexCookieGuard, ComplexOrganizationGuard) +@Controller("reviews") +export class ReviewsController { + constructor(private readonly store: ComplexStore) {} + + @Get() + @ApiOperation({ summary: "List reviews with moderation state" }) + @ApiOkResponse({ type: ReviewsResponseDto }) + @ApiStandardErrors({ auth: true }) + list( + @Req() request: ComplexDemoRequest, + @Query("productId") productId?: string, + ): ReviewsResponseDto { + return { data: this.store.listReviews(request.organizationId!, productId) }; + } + + @Post() + @UseGuards(ComplexCsrfGuard) + @ApiSecurity("csrf") + @ApiOperation({ summary: "Create a review in pending moderation state" }) + @ApiCreatedResponse({ type: ReviewResponseDto }) + @ApiStandardErrors({ auth: true, notFound: true }) + create( + @Req() request: ComplexDemoRequest, + @Body() dto: CreateReviewDto, + ): ReviewResponseDto { + return { data: this.store.createReview(request.organizationId!, dto) }; + } +} diff --git a/examples/demo-backend/src/apps/complex/controllers/identity.controllers.ts b/examples/demo-backend/src/apps/complex/controllers/identity.controllers.ts new file mode 100644 index 0000000..c311e34 --- /dev/null +++ b/examples/demo-backend/src/apps/complex/controllers/identity.controllers.ts @@ -0,0 +1,196 @@ +import { + Body, + Controller, + Get, + HttpCode, + Param, + Post, + Req, + Res, + UseGuards, +} from "@nestjs/common"; +import { + ApiCookieAuth, + ApiNoContentResponse, + ApiOkResponse, + ApiOperation, + ApiSecurity, + ApiTags, +} from "@nestjs/swagger"; +import type { Response } from "express"; +import { + ApiDemoScenarioHeader, + ApiOrganizationHeader, + ApiStandardErrors, +} from "../../../common/api.decorators"; +import type { ComplexDemoRequest } from "../complex.auth"; +import { + ComplexCookieGuard, + ComplexCsrfGuard, + ComplexOrganizationGuard, + ComplexRoles, + ComplexRolesGuard, + ComplexSessionService, + CSRF_COOKIE, + csrfCookieOptions, + SESSION_COOKIE, + sessionCookieOptions, +} from "../complex.auth"; +import { ComplexStore } from "../complex.store"; +import { + ComplexLoginDto, + ComplexRole, + ComplexUsersResponseDto, + ComplexUserResponseDto, + CookieSessionResponseDto, + MembersResponseDto, + OrganizationResponseDto, + OrganizationsResponseDto, +} from "../dto/identity.dto"; + +@ApiTags("Auth") +@ApiDemoScenarioHeader() +@Controller("auth") +export class ComplexAuthController { + constructor(private readonly sessions: ComplexSessionService) {} + + @Post("login") + @HttpCode(200) + @ApiOperation({ summary: "Login and establish HttpOnly cookie session" }) + @ApiOkResponse({ + type: CookieSessionResponseDto, + headers: { + "Set-Cookie": { + description: "Sets demo_session (HttpOnly) and demo_csrf cookies.", + schema: { type: "string" }, + }, + }, + }) + @ApiStandardErrors() + login( + @Body() dto: ComplexLoginDto, + @Res({ passthrough: true }) response: Response, + ): CookieSessionResponseDto { + const session = this.sessions.login(dto); + this.setCookies(response, session.token, session.csrfToken); + return this.sessions.response(session); + } + + @Post("refresh") + @HttpCode(200) + @UseGuards(ComplexCookieGuard, ComplexCsrfGuard) + @ApiCookieAuth("cookieSession") + @ApiSecurity("csrf") + @ApiOperation({ summary: "Rotate the current cookie session and CSRF token" }) + @ApiOkResponse({ type: CookieSessionResponseDto }) + @ApiStandardErrors({ auth: true }) + refresh( + @Req() request: ComplexDemoRequest, + @Res({ passthrough: true }) response: Response, + ): CookieSessionResponseDto { + const session = this.sessions.rotate(request.sessionToken!); + this.setCookies(response, session.token, session.csrfToken); + return this.sessions.response(session); + } + + @Post("logout") + @HttpCode(204) + @UseGuards(ComplexCookieGuard, ComplexCsrfGuard) + @ApiCookieAuth("cookieSession") + @ApiSecurity("csrf") + @ApiOperation({ + summary: "Revoke cookie session and clear authentication cookies", + }) + @ApiNoContentResponse() + @ApiStandardErrors({ auth: true }) + logout( + @Req() request: ComplexDemoRequest, + @Res({ passthrough: true }) response: Response, + ): void { + this.sessions.revoke(request.sessionToken); + response.clearCookie(SESSION_COOKIE, { path: "/" }); + response.clearCookie(CSRF_COOKIE, { path: "/" }); + } + + private setCookies( + response: Response, + token: string, + csrfToken: string, + ): void { + response.cookie(SESSION_COOKIE, token, sessionCookieOptions()); + response.cookie(CSRF_COOKIE, csrfToken, csrfCookieOptions()); + } +} + +@ApiTags("Users") +@ApiCookieAuth("cookieSession") +@ApiDemoScenarioHeader() +@UseGuards(ComplexCookieGuard) +@Controller("users") +export class ComplexUsersController { + constructor(private readonly store: ComplexStore) {} + + @Get("me") + @ApiOperation({ + summary: "Get the authenticated user and available organizations", + }) + @ApiOkResponse({ type: ComplexUserResponseDto }) + @ApiStandardErrors({ auth: true }) + me(@Req() request: ComplexDemoRequest): ComplexUserResponseDto { + const user = this.store.findUserById(request.user!.id)!; + return { data: this.store.publicUser(user) }; + } + + @Get() + @ApiOrganizationHeader() + @UseGuards(ComplexOrganizationGuard, ComplexRolesGuard) + @ComplexRoles(ComplexRole.Admin, ComplexRole.Manager) + @ApiOperation({ summary: "List users in the current tenant" }) + @ApiOkResponse({ type: ComplexUsersResponseDto }) + @ApiStandardErrors({ auth: true }) + list(@Req() request: ComplexDemoRequest): ComplexUsersResponseDto { + return { data: this.store.listUsers(request.organizationId!) }; + } +} + +@ApiTags("Organizations") +@ApiCookieAuth("cookieSession") +@ApiDemoScenarioHeader() +@UseGuards(ComplexCookieGuard) +@Controller("organizations") +export class OrganizationsController { + constructor(private readonly store: ComplexStore) {} + + @Get() + @ApiOperation({ summary: "List organizations available to the current user" }) + @ApiOkResponse({ type: OrganizationsResponseDto }) + @ApiStandardErrors({ auth: true }) + list(@Req() request: ComplexDemoRequest): OrganizationsResponseDto { + return { data: this.store.listOrganizations(request.user!.id) }; + } + + @Get(":id") + @ApiOperation({ summary: "Get one available organization" }) + @ApiOkResponse({ type: OrganizationResponseDto }) + @ApiStandardErrors({ auth: true, notFound: true }) + get( + @Req() request: ComplexDemoRequest, + @Param("id") id: string, + ): OrganizationResponseDto { + return { data: this.store.getOrganization(id, request.user!.id) }; + } + + @Get(":id/members") + @UseGuards(ComplexRolesGuard) + @ComplexRoles(ComplexRole.Admin, ComplexRole.Manager) + @ApiOperation({ summary: "List organization members and their roles" }) + @ApiOkResponse({ type: MembersResponseDto }) + @ApiStandardErrors({ auth: true, notFound: true }) + members( + @Req() request: ComplexDemoRequest, + @Param("id") id: string, + ): MembersResponseDto { + this.store.getOrganization(id, request.user!.id); + return { data: this.store.listMembers(id) }; + } +} diff --git a/examples/demo-backend/src/apps/complex/controllers/operations.controllers.ts b/examples/demo-backend/src/apps/complex/controllers/operations.controllers.ts new file mode 100644 index 0000000..36f6910 --- /dev/null +++ b/examples/demo-backend/src/apps/complex/controllers/operations.controllers.ts @@ -0,0 +1,254 @@ +import { + BadRequestException, + Controller, + Get, + HttpCode, + Param, + Post, + Query, + Req, + Res, + StreamableFile, + UploadedFile, + UseGuards, + UseInterceptors, +} from "@nestjs/common"; +import { FileInterceptor } from "@nestjs/platform-express"; +import { + ApiAcceptedResponse, + ApiBody, + ApiConsumes, + ApiCookieAuth, + ApiCreatedResponse, + ApiExtraModels, + ApiOkResponse, + ApiOperation, + ApiProduces, + ApiSecurity, + ApiTags, +} from "@nestjs/swagger"; +import type { Response } from "express"; +import { + ApiBinaryResponse, + ApiDemoScenarioHeader, + ApiOrganizationHeader, + ApiStandardErrors, +} from "../../../common/api.decorators"; +import { + ComplexCookieGuard, + ComplexCsrfGuard, + type ComplexDemoRequest, + ComplexOrganizationGuard, + ComplexRoles, + ComplexRolesGuard, +} from "../complex.auth"; +import { ComplexStore } from "../complex.store"; +import { ComplexRole } from "../dto/identity.dto"; +import { + AuditEventsResponseDto, + AuditQueryDto, + CursorQueryDto, + FileResponseDto, + FilesResponseDto, + InventoryNotificationPayloadDto, + JobResponseDto, + NotificationResponseDto, + NotificationsResponseDto, + OrderNotificationPayloadDto, + SystemNotificationPayloadDto, +} from "../dto/operations.dto"; + +@ApiTags("Notifications") +@ApiExtraModels( + OrderNotificationPayloadDto, + InventoryNotificationPayloadDto, + SystemNotificationPayloadDto, +) +@ApiCookieAuth("cookieSession") +@ApiOrganizationHeader() +@ApiDemoScenarioHeader() +@UseGuards(ComplexCookieGuard, ComplexOrganizationGuard) +@Controller("notifications") +export class NotificationsController { + constructor(private readonly store: ComplexStore) {} + + @Get() + @ApiOperation({ + summary: "List polymorphic notifications using cursor pagination", + }) + @ApiOkResponse({ type: NotificationsResponseDto }) + @ApiStandardErrors({ auth: true }) + list( + @Req() request: ComplexDemoRequest, + @Query() query: CursorQueryDto, + ): NotificationsResponseDto { + return this.store.listNotifications( + request.organizationId!, + query.cursor, + query.limit, + ); + } + + @Post(":id/read") + @HttpCode(200) + @UseGuards(ComplexCsrfGuard) + @ApiSecurity("csrf") + @ApiOperation({ summary: "Mark a notification as read" }) + @ApiOkResponse({ type: NotificationResponseDto }) + @ApiStandardErrors({ auth: true, notFound: true }) + markRead( + @Req() request: ComplexDemoRequest, + @Param("id") id: string, + ): NotificationResponseDto { + return { + data: this.store.markNotificationRead(request.organizationId!, id), + }; + } +} + +@ApiTags("Files") +@ApiCookieAuth("cookieSession") +@ApiOrganizationHeader() +@ApiDemoScenarioHeader() +@UseGuards(ComplexCookieGuard, ComplexOrganizationGuard) +@Controller("files") +export class FilesController { + constructor(private readonly store: ComplexStore) {} + + @Get() + @ApiOperation({ summary: "List uploaded file metadata" }) + @ApiOkResponse({ type: FilesResponseDto }) + @ApiStandardErrors({ auth: true }) + list(@Req() request: ComplexDemoRequest): FilesResponseDto { + return { data: this.store.listFiles(request.organizationId!) }; + } + + @Post() + @UseInterceptors( + FileInterceptor("file", { limits: { fileSize: 5 * 1024 * 1024 } }), + ) + @UseGuards(ComplexRolesGuard, ComplexCsrfGuard) + @ComplexRoles(ComplexRole.Admin, ComplexRole.Manager) + @ApiSecurity("csrf") + @ApiConsumes("multipart/form-data") + @ApiBody({ + schema: { + type: "object", + required: ["file"], + properties: { file: { type: "string", format: "binary" } }, + }, + }) + @ApiOperation({ + summary: "Upload a file up to 5 MiB and retain it in memory", + }) + @ApiCreatedResponse({ type: FileResponseDto }) + @ApiStandardErrors({ auth: true }) + upload( + @Req() request: ComplexDemoRequest, + @UploadedFile() file?: Express.Multer.File, + ): FileResponseDto { + if (!file) + throw new BadRequestException({ + code: "FILE_REQUIRED", + message: 'Multipart field "file" is required.', + }); + return { data: this.store.addFile(request.organizationId!, file) }; + } + + @Get(":id/download") + @ApiProduces("application/octet-stream") + @ApiOperation({ summary: "Download an in-memory file" }) + @ApiBinaryResponse("Binary file content.") + @ApiStandardErrors({ auth: true, notFound: true }) + download( + @Req() request: ComplexDemoRequest, + @Param("id") id: string, + @Res({ passthrough: true }) response: Response, + ): StreamableFile { + const file = this.store.getFile(request.organizationId!, id); + response.setHeader("Content-Type", file.metadata.mimeType); + response.setHeader( + "Content-Disposition", + `attachment; filename="${file.metadata.name.replace(/"/g, "")}"`, + ); + return new StreamableFile(file.content); + } +} + +@ApiTags("Audit") +@ApiCookieAuth("cookieSession") +@ApiOrganizationHeader() +@ApiDemoScenarioHeader() +@UseGuards(ComplexCookieGuard, ComplexOrganizationGuard, ComplexRolesGuard) +@ComplexRoles(ComplexRole.Admin, ComplexRole.Manager) +@Controller("audit-events") +export class AuditController { + constructor(private readonly store: ComplexStore) {} + + @Get() + @ApiOperation({ + summary: "List immutable audit events using offset pagination", + }) + @ApiOkResponse({ type: AuditEventsResponseDto }) + @ApiStandardErrors({ auth: true }) + list( + @Req() request: ComplexDemoRequest, + @Query() query: AuditQueryDto, + ): AuditEventsResponseDto { + return this.store.listAudit(request.organizationId!, query); + } +} + +@ApiTags("Background jobs") +@ApiCookieAuth("cookieSession") +@ApiOrganizationHeader() +@ApiDemoScenarioHeader() +@UseGuards(ComplexCookieGuard, ComplexOrganizationGuard) +@Controller() +export class JobsController { + constructor(private readonly store: ComplexStore) {} + + @Post("exports/orders") + @HttpCode(202) + @UseGuards(ComplexRolesGuard, ComplexCsrfGuard) + @ComplexRoles(ComplexRole.Admin, ComplexRole.Manager) + @ApiSecurity("csrf") + @ApiOperation({ summary: "Start an asynchronous orders export" }) + @ApiAcceptedResponse({ type: JobResponseDto }) + @ApiStandardErrors({ auth: true }) + startExport(@Req() request: ComplexDemoRequest): JobResponseDto { + return { data: this.store.startOrdersExport(request.organizationId!) }; + } + + @Get("jobs/:id") + @ApiOperation({ summary: "Poll background job progress" }) + @ApiOkResponse({ type: JobResponseDto }) + @ApiStandardErrors({ auth: true, notFound: true }) + getJob( + @Req() request: ComplexDemoRequest, + @Param("id") id: string, + ): JobResponseDto { + return { data: this.store.getJob(request.organizationId!, id) }; + } + + @Get("jobs/:id/result") + @ApiProduces("text/csv") + @ApiOperation({ + summary: "Download a completed export; returns 409 while processing", + }) + @ApiBinaryResponse("Generated orders CSV.", "text/csv") + @ApiStandardErrors({ auth: true, notFound: true, conflict: true }) + result( + @Req() request: ComplexDemoRequest, + @Param("id") id: string, + @Res({ passthrough: true }) response: Response, + ): StreamableFile { + const content = this.store.jobResult(request.organizationId!, id); + response.setHeader("Content-Type", "text/csv"); + response.setHeader( + "Content-Disposition", + `attachment; filename="orders-${id}.csv"`, + ); + return new StreamableFile(content); + } +} diff --git a/examples/demo-backend/src/apps/complex/controllers/system.controllers.ts b/examples/demo-backend/src/apps/complex/controllers/system.controllers.ts new file mode 100644 index 0000000..23dbe0f --- /dev/null +++ b/examples/demo-backend/src/apps/complex/controllers/system.controllers.ts @@ -0,0 +1,177 @@ +import { + BadRequestException, + Body, + Controller, + Get, + HttpCode, + Param, + Post, + Req, + UseGuards, +} from "@nestjs/common"; +import { + ApiCookieAuth, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiSecurity, + ApiTags, +} from "@nestjs/swagger"; +import { + ApiDemoScenarioHeader, + ApiStandardErrors, +} from "../../../common/api.decorators"; +import { + HealthResponseDto, + ScenariosResponseDto, + TestingActionResponseDto, +} from "../../../common/api.dto"; +import { + ComplexCookieGuard, + ComplexCsrfGuard, + type ComplexDemoRequest, + ComplexSessionService, +} from "../complex.auth"; +import { ComplexStore } from "../complex.store"; +import { + ChangeComplexRoleDto, + ComplexUserResponseDto, +} from "../dto/identity.dto"; + +@ApiTags("Health") +@ApiDemoScenarioHeader() +@Controller("health") +export class ComplexHealthController { + @Get() + @ApiOperation({ summary: "Check Complex API availability" }) + @ApiOkResponse({ type: HealthResponseDto }) + health(): HealthResponseDto { + return { + data: { + application: "complex", + status: "ok", + timestamp: new Date().toISOString(), + version: "1.0.0", + }, + }; + } +} + +@ApiTags("Testing") +@ApiDemoScenarioHeader() +@Controller("testing") +export class ComplexTestingController { + constructor( + private readonly store: ComplexStore, + private readonly sessions: ComplexSessionService, + ) {} + + @Get("scenarios") + @ApiOperation({ summary: "List deterministic X-Demo-Scenario values" }) + @ApiOkResponse({ type: ScenariosResponseDto }) + scenarios(): ScenariosResponseDto { + return { + data: [ + { name: "normal", description: "Default behavior." }, + { + name: "slow", + description: + "Delays the response to exercise loading and cancellation states.", + }, + { + name: "timeout", + description: + "Delays long enough for a frontend timeout or cancellation.", + }, + { + name: "server-error", + description: "Returns a deterministic 500 error.", + }, + { name: "rate-limited", description: "Returns 429 with Retry-After." }, + { + name: "empty", + description: "Turns list responses into a valid empty state.", + }, + { + name: "expired-auth", + description: "Makes protected routes return 401.", + }, + { + name: "forbidden", + description: "Makes protected routes return 403.", + }, + { name: "conflict", description: "Makes mutation routes return 409." }, + { + name: "large-dataset", + description: "Expands list responses to 250 deterministic items.", + }, + ], + }; + } + + @Post("reset") + @HttpCode(200) + @ApiOperation({ summary: "Reset all Complex API data and active sessions" }) + @ApiOkResponse({ type: TestingActionResponseDto }) + reset(): TestingActionResponseDto { + this.store.reset("small"); + this.sessions.reset(); + return { + data: { + success: true, + message: + "Complex API state and sessions reset to the default deterministic seed.", + }, + }; + } + + @Post("seed/:preset") + @HttpCode(200) + @ApiParam({ name: "preset", enum: ["small", "large"] }) + @ApiOperation({ summary: "Select a small or large deterministic dataset" }) + @ApiOkResponse({ type: TestingActionResponseDto }) + seed(@Param("preset") preset: string): TestingActionResponseDto { + if (preset !== "small" && preset !== "large") { + throw new BadRequestException({ + code: "UNKNOWN_SEED_PRESET", + message: "Preset must be small or large.", + }); + } + this.store.reset(preset); + return { + data: { + success: true, + message: `Complex API loaded the ${preset} dataset.`, + }, + }; + } + + @Post("session/expire") + @HttpCode(200) + @UseGuards(ComplexCookieGuard, ComplexCsrfGuard) + @ApiCookieAuth("cookieSession") + @ApiSecurity("csrf") + @ApiOperation({ + summary: "Expire the current cookie session after this response", + }) + @ApiOkResponse({ type: TestingActionResponseDto }) + @ApiStandardErrors({ auth: true }) + expireSession(@Req() request: ComplexDemoRequest): TestingActionResponseDto { + this.sessions.expire(request.sessionToken); + return { + data: { success: true, message: "Current cookie session expired." }, + }; + } + + @Post("users/:userId/role") + @HttpCode(200) + @ApiOperation({ summary: "Change a role while sessions remain active" }) + @ApiOkResponse({ type: ComplexUserResponseDto }) + @ApiStandardErrors({ notFound: true }) + changeRole( + @Param("userId") userId: string, + @Body() dto: ChangeComplexRoleDto, + ): ComplexUserResponseDto { + return { data: this.store.changeUserRole(userId, dto.role) }; + } +} diff --git a/examples/demo-backend/src/apps/complex/dto/catalog.dto.ts b/examples/demo-backend/src/apps/complex/dto/catalog.dto.ts new file mode 100644 index 0000000..dda39f5 --- /dev/null +++ b/examples/demo-backend/src/apps/complex/dto/catalog.dto.ts @@ -0,0 +1,329 @@ +import { Type } from "class-transformer"; +import { + IsArray, + IsEnum, + IsInt, + IsObject, + IsOptional, + IsString, + Max, + MaxLength, + Min, + MinLength, + ValidateNested, +} from "class-validator"; +import { ApiProperty, ApiPropertyOptional, PartialType } from "@nestjs/swagger"; +import { CursorMetaDto } from "../../../common/api.dto"; + +export class MoneyDto { + @ApiProperty({ + example: "129.90", + pattern: "^\\d+\\.\\d{2}$", + description: + "Decimal string; never parse money as a floating-point number.", + }) + amount!: string; + + @ApiProperty({ enum: ["USD", "EUR"], example: "USD" }) + currency!: "USD" | "EUR"; +} + +export enum ComplexProductStatus { + Draft = "draft", + Active = "active", + Archived = "archived", +} + +export class ProductVariantDto { + @ApiProperty({ example: "variant-keyboard-black" }) + id!: string; + + @ApiProperty({ example: "KEYBOARD-BLACK-US" }) + sku!: string; + + @ApiProperty({ + type: "object", + additionalProperties: { type: "string" }, + example: { color: "black", layout: "US" }, + }) + attributes!: Record<string, string>; + + @ApiProperty({ type: MoneyDto }) + price!: MoneyDto; +} + +export class ComplexProductDto { + @ApiProperty({ example: "complex-product-keyboard" }) + id!: string; + + @ApiProperty({ example: "org-acme" }) + organizationId!: string; + + @ApiProperty({ example: "Pro Mechanical Keyboard" }) + name!: string; + + @ApiProperty({ example: "pro-mechanical-keyboard" }) + slug!: string; + + @ApiProperty({ example: "Configurable keyboard sold in multiple variants." }) + description!: string; + + @ApiProperty({ enum: ComplexProductStatus }) + status!: ComplexProductStatus; + + @ApiProperty({ example: "complex-category-electronics" }) + categoryId!: string; + + @ApiProperty({ example: "brand-northstar" }) + brandId!: string; + + @ApiProperty({ type: MoneyDto }) + price!: MoneyDto; + + @ApiProperty({ type: [ProductVariantDto] }) + variants!: ProductVariantDto[]; + + @ApiProperty({ type: [String], example: ["featured", "office"] }) + tags!: string[]; + + @ApiProperty({ format: "date-time", nullable: true }) + publishedAt!: string | null; + + @ApiProperty({ format: "date-time" }) + createdAt!: string; + + @ApiProperty({ example: 3, description: "Optimistic-lock version." }) + version!: number; +} + +export class CursorProductQueryDto { + @ApiPropertyOptional({ minimum: 1, maximum: 100, default: 20 }) + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + @IsOptional() + limit = 20; + + @ApiPropertyOptional({ example: "complex-product-mouse" }) + @IsString() + @IsOptional() + cursor?: string; + + @ApiPropertyOptional({ example: "keyboard" }) + @IsString() + @IsOptional() + search?: string; + + @ApiPropertyOptional({ enum: ComplexProductStatus }) + @IsEnum(ComplexProductStatus) + @IsOptional() + status?: ComplexProductStatus; +} + +export class ComplexProductsResponseDto { + @ApiProperty({ type: [ComplexProductDto] }) + data!: ComplexProductDto[]; + + @ApiProperty({ type: CursorMetaDto }) + meta!: CursorMetaDto; +} + +export class ComplexProductResponseDto { + @ApiProperty({ type: ComplexProductDto }) + data!: ComplexProductDto; +} + +export class CreateVariantDto { + @ApiProperty({ example: "KEYBOARD-WHITE-US" }) + @IsString() + sku!: string; + + @ApiProperty({ + type: "object", + additionalProperties: { type: "string" }, + example: { color: "white", layout: "US" }, + }) + @IsObject() + attributes!: Record<string, string>; + + @ApiProperty({ example: "139.90" }) + @IsString() + priceAmount!: string; +} + +export class CreateComplexProductDto { + @ApiProperty({ example: "Pro Mechanical Keyboard" }) + @IsString() + @MinLength(2) + @MaxLength(120) + name!: string; + + @ApiProperty({ example: "Configurable keyboard sold in multiple variants." }) + @IsString() + @MinLength(10) + @MaxLength(2000) + description!: string; + + @ApiProperty({ + enum: ComplexProductStatus, + default: ComplexProductStatus.Draft, + }) + @IsEnum(ComplexProductStatus) + status!: ComplexProductStatus; + + @ApiProperty({ example: "complex-category-electronics" }) + @IsString() + categoryId!: string; + + @ApiProperty({ example: "brand-northstar" }) + @IsString() + brandId!: string; + + @ApiProperty({ example: "129.90" }) + @IsString() + priceAmount!: string; + + @ApiProperty({ type: [CreateVariantDto] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreateVariantDto) + variants!: CreateVariantDto[]; + + @ApiPropertyOptional({ type: [String], example: ["featured"] }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + tags: string[] = []; +} + +export class UpdateComplexProductDto extends PartialType( + CreateComplexProductDto, +) { + @ApiProperty({ + minimum: 1, + example: 3, + description: "Version last read by the frontend.", + }) + @IsInt() + @Min(1) + version!: number; +} + +export class ComplexCategoryDto { + @ApiProperty({ example: "complex-category-electronics" }) + id!: string; + + @ApiProperty({ example: "org-acme" }) + organizationId!: string; + + @ApiProperty({ example: "Electronics" }) + name!: string; + + @ApiProperty({ nullable: true, example: null }) + parentId!: string | null; + + @ApiProperty({ type: [String], example: ["complex-category-keyboards"] }) + childIds!: string[]; +} + +export class ComplexCategoriesResponseDto { + @ApiProperty({ type: [ComplexCategoryDto] }) + data!: ComplexCategoryDto[]; +} + +export class BrandDto { + @ApiProperty({ example: "brand-northstar" }) + id!: string; + + @ApiProperty({ example: "org-acme" }) + organizationId!: string; + + @ApiProperty({ example: "Northstar" }) + name!: string; + + @ApiProperty({ format: "uri", nullable: true }) + logoUrl!: string | null; +} + +export class BrandsResponseDto { + @ApiProperty({ type: [BrandDto] }) + data!: BrandDto[]; +} + +export class WarehouseDto { + @ApiProperty({ example: "warehouse-berlin" }) + id!: string; + + @ApiProperty({ example: "org-acme" }) + organizationId!: string; + + @ApiProperty({ example: "Berlin Warehouse" }) + name!: string; + + @ApiProperty({ example: "DE" }) + countryCode!: string; + + @ApiProperty({ enum: ["active", "maintenance"], example: "active" }) + status!: "active" | "maintenance"; +} + +export class WarehousesResponseDto { + @ApiProperty({ type: [WarehouseDto] }) + data!: WarehouseDto[]; +} + +export class InventoryItemDto { + @ApiProperty({ example: "inventory-keyboard-berlin" }) + id!: string; + + @ApiProperty({ example: "complex-product-keyboard" }) + productId!: string; + + @ApiProperty({ example: "variant-keyboard-black" }) + variantId!: string; + + @ApiProperty({ example: "warehouse-berlin" }) + warehouseId!: string; + + @ApiProperty({ example: 42 }) + available!: number; + + @ApiProperty({ example: 5 }) + reserved!: number; + + @ApiProperty({ example: 10 }) + reorderPoint!: number; + + @ApiProperty({ example: 2 }) + version!: number; +} + +export class InventoryResponseDto { + @ApiProperty({ type: [InventoryItemDto] }) + data!: InventoryItemDto[]; +} + +export class InventoryItemResponseDto { + @ApiProperty({ type: InventoryItemDto }) + data!: InventoryItemDto; +} + +export class AdjustInventoryDto { + @ApiProperty({ example: -2, description: "Signed stock adjustment." }) + @IsInt() + delta!: number; + + @ApiProperty({ example: "Damaged during delivery" }) + @IsString() + @MinLength(3) + reason!: string; + + @ApiProperty({ + example: 2, + description: "Version last read by the frontend.", + }) + @IsInt() + @Min(1) + version!: number; +} diff --git a/examples/demo-backend/src/apps/complex/dto/chat.dto.ts b/examples/demo-backend/src/apps/complex/dto/chat.dto.ts new file mode 100644 index 0000000..ca5434e --- /dev/null +++ b/examples/demo-backend/src/apps/complex/dto/chat.dto.ts @@ -0,0 +1,87 @@ +import { IsString, MinLength } from "class-validator"; +import { ApiProperty } from "@nestjs/swagger"; +import { CursorMetaDto } from "../../../common/api.dto"; + +export class ConversationDto { + @ApiProperty({ example: "conversation-support" }) + id!: string; + + @ApiProperty({ example: "org-acme" }) + organizationId!: string; + + @ApiProperty({ example: "Order support" }) + title!: string; + + @ApiProperty({ + type: [String], + example: ["complex-user-admin", "complex-user-support"], + }) + participantIds!: string[]; + + @ApiProperty({ + nullable: true, + example: "Can you check order complex-order-001?", + }) + lastMessagePreview!: string | null; + + @ApiProperty({ format: "date-time" }) + updatedAt!: string; +} + +export class ConversationsResponseDto { + @ApiProperty({ type: [ConversationDto] }) + data!: ConversationDto[]; +} + +export class ConversationResponseDto { + @ApiProperty({ type: ConversationDto }) + data!: ConversationDto; +} + +export class ChatMessageDto { + @ApiProperty({ example: "message-001" }) + id!: string; + + @ApiProperty({ example: "conversation-support" }) + conversationId!: string; + + @ApiProperty({ example: "complex-user-support" }) + senderId!: string; + + @ApiProperty({ example: "The order has already been packed." }) + text!: string; + + @ApiProperty({ + example: "client-message-4c08", + description: "Frontend-generated key used to deduplicate retries.", + }) + clientMessageId!: string; + + @ApiProperty({ format: "date-time" }) + createdAt!: string; +} + +export class MessagesResponseDto { + @ApiProperty({ type: [ChatMessageDto] }) + data!: ChatMessageDto[]; + + @ApiProperty({ type: CursorMetaDto }) + meta!: CursorMetaDto; +} + +export class SendMessageDto { + @ApiProperty({ example: "The order has already been packed." }) + @IsString() + @MinLength(1) + text!: string; + + @ApiProperty({ example: "client-message-4c08" }) + @IsString() + @MinLength(3) + clientMessageId!: string; +} + +export class MessageResponseDto { + @ApiProperty({ type: ChatMessageDto }) + data!: ChatMessageDto; +} diff --git a/examples/demo-backend/src/apps/complex/dto/commerce.dto.ts b/examples/demo-backend/src/apps/complex/dto/commerce.dto.ts new file mode 100644 index 0000000..c3c4e8c --- /dev/null +++ b/examples/demo-backend/src/apps/complex/dto/commerce.dto.ts @@ -0,0 +1,324 @@ +import { Type } from "class-transformer"; +import { + IsArray, + IsInt, + IsOptional, + IsString, + Max, + Min, + ValidateNested, +} from "class-validator"; +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { CursorMetaDto, PageMetaDto } from "../../../common/api.dto"; +import { MoneyDto } from "./catalog.dto"; + +export class AddressDto { + @ApiProperty({ example: "Friedrichstrasse 100" }) + line1!: string; + + @ApiProperty({ nullable: true, example: null }) + line2!: string | null; + + @ApiProperty({ example: "Berlin" }) + city!: string; + + @ApiProperty({ example: "10117" }) + postalCode!: string; + + @ApiProperty({ example: "DE" }) + countryCode!: string; +} + +export class CustomerDto { + @ApiProperty({ example: "customer-ada" }) + id!: string; + + @ApiProperty({ example: "org-acme" }) + organizationId!: string; + + @ApiProperty({ example: "Ada Lovelace" }) + name!: string; + + @ApiProperty({ format: "email", example: "ada@example.test" }) + email!: string; + + @ApiProperty({ type: AddressDto }) + defaultAddress!: AddressDto; + + @ApiProperty({ type: [String], example: ["vip", "newsletter"] }) + tags!: string[]; + + @ApiProperty({ format: "date-time" }) + createdAt!: string; +} + +export class CustomerQueryDto { + @ApiPropertyOptional({ default: 1, minimum: 1 }) + @Type(() => Number) + @IsInt() + @Min(1) + @IsOptional() + page = 1; + + @ApiPropertyOptional({ default: 20, minimum: 1, maximum: 100 }) + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + @IsOptional() + limit = 20; + + @ApiPropertyOptional({ example: "ada" }) + @IsString() + @IsOptional() + search?: string; +} + +export class CustomersResponseDto { + @ApiProperty({ type: [CustomerDto] }) + data!: CustomerDto[]; + + @ApiProperty({ type: PageMetaDto }) + meta!: PageMetaDto; +} + +export class CustomerResponseDto { + @ApiProperty({ type: CustomerDto }) + data!: CustomerDto; +} + +export enum ComplexOrderStatus { + Draft = "draft", + AwaitingPayment = "awaiting-payment", + Paid = "paid", + Fulfillment = "fulfillment", + Shipped = "shipped", + Cancelled = "cancelled", +} + +export class ComplexOrderItemDto { + @ApiProperty({ example: "complex-product-keyboard" }) + productId!: string; + + @ApiProperty({ example: "variant-keyboard-black" }) + variantId!: string; + + @ApiProperty({ example: "Pro Mechanical Keyboard" }) + name!: string; + + @ApiProperty({ example: 1 }) + quantity!: number; + + @ApiProperty({ type: MoneyDto }) + unitPrice!: MoneyDto; +} + +export class ComplexOrderDto { + @ApiProperty({ example: "complex-order-001" }) + id!: string; + + @ApiProperty({ example: "org-acme" }) + organizationId!: string; + + @ApiProperty({ example: "customer-ada" }) + customerId!: string; + + @ApiProperty({ enum: ComplexOrderStatus }) + status!: ComplexOrderStatus; + + @ApiProperty({ type: [ComplexOrderItemDto] }) + items!: ComplexOrderItemDto[]; + + @ApiProperty({ type: MoneyDto }) + subtotal!: MoneyDto; + + @ApiProperty({ type: MoneyDto }) + discount!: MoneyDto; + + @ApiProperty({ type: MoneyDto }) + total!: MoneyDto; + + @ApiProperty({ type: AddressDto }) + shippingAddress!: AddressDto; + + @ApiProperty({ format: "date-time" }) + createdAt!: string; + + @ApiProperty({ example: 1 }) + version!: number; +} + +export class OrderCursorQueryDto { + @ApiPropertyOptional({ minimum: 1, maximum: 100, default: 20 }) + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + @IsOptional() + limit = 20; + + @ApiPropertyOptional({ example: "complex-order-001" }) + @IsString() + @IsOptional() + cursor?: string; + + @ApiPropertyOptional({ enum: ComplexOrderStatus }) + @IsString() + @IsOptional() + status?: ComplexOrderStatus; +} + +export class ComplexOrdersResponseDto { + @ApiProperty({ type: [ComplexOrderDto] }) + data!: ComplexOrderDto[]; + + @ApiProperty({ type: CursorMetaDto }) + meta!: CursorMetaDto; +} + +export class ComplexOrderResponseDto { + @ApiProperty({ type: ComplexOrderDto }) + data!: ComplexOrderDto; +} + +export class CreateComplexOrderItemDto { + @ApiProperty({ example: "complex-product-keyboard" }) + @IsString() + productId!: string; + + @ApiProperty({ example: "variant-keyboard-black" }) + @IsString() + variantId!: string; + + @ApiProperty({ example: 1, minimum: 1, maximum: 50 }) + @IsInt() + @Min(1) + @Max(50) + quantity!: number; +} + +export class CreateComplexOrderDto { + @ApiProperty({ example: "customer-ada" }) + @IsString() + customerId!: string; + + @ApiProperty({ type: [CreateComplexOrderItemDto] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreateComplexOrderItemDto) + items!: CreateComplexOrderItemDto[]; + + @ApiPropertyOptional({ example: "WELCOME10" }) + @IsString() + @IsOptional() + promotionCode?: string; +} + +export class PaymentDto { + @ApiProperty({ example: "payment-001" }) + id!: string; + + @ApiProperty({ example: "complex-order-001" }) + orderId!: string; + + @ApiProperty({ + enum: ["pending", "succeeded", "failed", "refunded"], + example: "succeeded", + }) + status!: "pending" | "succeeded" | "failed" | "refunded"; + + @ApiProperty({ enum: ["card", "bank-transfer"], example: "card" }) + method!: "card" | "bank-transfer"; + + @ApiProperty({ type: MoneyDto }) + amount!: MoneyDto; + + @ApiProperty({ format: "date-time" }) + createdAt!: string; +} + +export class PaymentsResponseDto { + @ApiProperty({ type: [PaymentDto] }) + data!: PaymentDto[]; +} + +export class PromotionDto { + @ApiProperty({ example: "promotion-welcome" }) + id!: string; + + @ApiProperty({ example: "WELCOME10" }) + code!: string; + + @ApiProperty({ enum: ["percentage", "fixed"], example: "percentage" }) + type!: "percentage" | "fixed"; + + @ApiProperty({ example: "10.00" }) + value!: string; + + @ApiProperty({ format: "date-time" }) + validUntil!: string; + + @ApiProperty({ example: true }) + active!: boolean; +} + +export class PromotionsResponseDto { + @ApiProperty({ type: [PromotionDto] }) + data!: PromotionDto[]; +} + +export class ReviewDto { + @ApiProperty({ example: "review-001" }) + id!: string; + + @ApiProperty({ example: "complex-product-keyboard" }) + productId!: string; + + @ApiProperty({ example: "customer-ada" }) + customerId!: string; + + @ApiProperty({ example: 5, minimum: 1, maximum: 5 }) + rating!: number; + + @ApiProperty({ example: "Excellent keyboard for daily development." }) + comment!: string; + + @ApiProperty({ + enum: ["pending", "published", "rejected"], + example: "published", + }) + status!: "pending" | "published" | "rejected"; + + @ApiProperty({ format: "date-time" }) + createdAt!: string; +} + +export class ReviewsResponseDto { + @ApiProperty({ type: [ReviewDto] }) + data!: ReviewDto[]; +} + +export class CreateReviewDto { + @ApiProperty({ example: "complex-product-keyboard" }) + @IsString() + productId!: string; + + @ApiProperty({ example: "customer-ada" }) + @IsString() + customerId!: string; + + @ApiProperty({ minimum: 1, maximum: 5, example: 5 }) + @IsInt() + @Min(1) + @Max(5) + rating!: number; + + @ApiProperty({ example: "Excellent keyboard for daily development." }) + @IsString() + comment!: string; +} + +export class ReviewResponseDto { + @ApiProperty({ type: ReviewDto }) + data!: ReviewDto; +} diff --git a/examples/demo-backend/src/apps/complex/dto/identity.dto.ts b/examples/demo-backend/src/apps/complex/dto/identity.dto.ts new file mode 100644 index 0000000..98ce5c5 --- /dev/null +++ b/examples/demo-backend/src/apps/complex/dto/identity.dto.ts @@ -0,0 +1,139 @@ +import { IsEmail, IsEnum, IsString, MinLength } from "class-validator"; +import { ApiProperty } from "@nestjs/swagger"; + +export enum ComplexRole { + Admin = "admin", + Manager = "manager", + Support = "support", + Viewer = "viewer", +} + +export class ComplexLoginDto { + @ApiProperty({ format: "email", example: "admin@complex.demo" }) + @IsEmail() + email!: string; + + @ApiProperty({ format: "password", example: "demo1234", minLength: 8 }) + @IsString() + @MinLength(8) + password!: string; +} + +export class ComplexUserDto { + @ApiProperty({ example: "complex-user-admin" }) + id!: string; + + @ApiProperty({ format: "email", example: "admin@complex.demo" }) + email!: string; + + @ApiProperty({ example: "Complex Admin" }) + name!: string; + + @ApiProperty({ enum: ComplexRole }) + role!: ComplexRole; + + @ApiProperty({ nullable: true, example: "https://i.pravatar.cc/160?img=20" }) + avatarUrl!: string | null; + + @ApiProperty({ type: [String], example: ["org-acme", "org-globex"] }) + organizationIds!: string[]; +} + +export class ComplexUserResponseDto { + @ApiProperty({ type: ComplexUserDto }) + data!: ComplexUserDto; +} + +export class ComplexUsersResponseDto { + @ApiProperty({ type: [ComplexUserDto] }) + data!: ComplexUserDto[]; +} + +export class CookieSessionDataDto { + @ApiProperty({ type: ComplexUserDto }) + user!: ComplexUserDto; + + @ApiProperty({ + example: "5f2642ca-2ba4-4ee3-bf2e-d5d37a97686c", + description: "Send this value in X-CSRF-Token for authenticated mutations.", + }) + csrfToken!: string; + + @ApiProperty({ format: "date-time" }) + expiresAt!: string; +} + +export class CookieSessionResponseDto { + @ApiProperty({ type: CookieSessionDataDto }) + data!: CookieSessionDataDto; +} + +export enum OrganizationPlan { + Starter = "starter", + Business = "business", + Enterprise = "enterprise", +} + +export class OrganizationDto { + @ApiProperty({ example: "org-acme" }) + id!: string; + + @ApiProperty({ example: "Acme Commerce" }) + name!: string; + + @ApiProperty({ enum: OrganizationPlan }) + plan!: OrganizationPlan; + + @ApiProperty({ example: "Europe/Berlin" }) + timezone!: string; + + @ApiProperty({ enum: ["USD", "EUR"], example: "USD" }) + currency!: "USD" | "EUR"; + + @ApiProperty({ format: "date-time" }) + createdAt!: string; +} + +export class OrganizationsResponseDto { + @ApiProperty({ type: [OrganizationDto] }) + data!: OrganizationDto[]; +} + +export class OrganizationResponseDto { + @ApiProperty({ type: OrganizationDto }) + data!: OrganizationDto; +} + +export class MemberDto { + @ApiProperty({ example: "member-001" }) + id!: string; + + @ApiProperty({ example: "org-acme" }) + organizationId!: string; + + @ApiProperty({ example: "complex-user-manager" }) + userId!: string; + + @ApiProperty({ example: "Complex Manager" }) + userName!: string; + + @ApiProperty({ format: "email", example: "manager@complex.demo" }) + email!: string; + + @ApiProperty({ enum: ComplexRole }) + role!: ComplexRole; + + @ApiProperty({ enum: ["active", "invited", "suspended"], example: "active" }) + status!: "active" | "invited" | "suspended"; +} + +export class MembersResponseDto { + @ApiProperty({ type: [MemberDto] }) + data!: MemberDto[]; +} + +export class ChangeComplexRoleDto { + @ApiProperty({ enum: ComplexRole, example: ComplexRole.Viewer }) + @IsEnum(ComplexRole) + role!: ComplexRole; +} diff --git a/examples/demo-backend/src/apps/complex/dto/operations.dto.ts b/examples/demo-backend/src/apps/complex/dto/operations.dto.ts new file mode 100644 index 0000000..af2ec5b --- /dev/null +++ b/examples/demo-backend/src/apps/complex/dto/operations.dto.ts @@ -0,0 +1,244 @@ +import { Type } from "class-transformer"; +import { IsInt, IsOptional, IsString, Max, Min } from "class-validator"; +import { + ApiExtraModels, + ApiProperty, + ApiPropertyOptional, + getSchemaPath, +} from "@nestjs/swagger"; +import { CursorMetaDto, PageMetaDto } from "../../../common/api.dto"; + +export class OrderNotificationPayloadDto { + @ApiProperty({ enum: ["order"], example: "order" }) + type!: "order"; + + @ApiProperty({ example: "complex-order-001" }) + orderId!: string; + + @ApiProperty({ enum: ["paid", "shipped", "cancelled"], example: "shipped" }) + status!: string; +} + +export class InventoryNotificationPayloadDto { + @ApiProperty({ enum: ["inventory"], example: "inventory" }) + type!: "inventory"; + + @ApiProperty({ example: "complex-product-keyboard" }) + productId!: string; + + @ApiProperty({ example: 4 }) + remaining!: number; +} + +export class SystemNotificationPayloadDto { + @ApiProperty({ enum: ["system"], example: "system" }) + type!: "system"; + + @ApiProperty({ example: "Scheduled maintenance begins at 02:00 UTC." }) + text!: string; +} + +export enum NotificationKind { + Order = "order", + Inventory = "inventory", + System = "system", +} + +@ApiExtraModels( + OrderNotificationPayloadDto, + InventoryNotificationPayloadDto, + SystemNotificationPayloadDto, +) +export class NotificationDto { + @ApiProperty({ example: "notification-001" }) + id!: string; + + @ApiProperty({ enum: NotificationKind }) + kind!: NotificationKind; + + @ApiProperty({ example: "Order shipped" }) + title!: string; + + @ApiProperty({ + oneOf: [ + { $ref: getSchemaPath(OrderNotificationPayloadDto) }, + { $ref: getSchemaPath(InventoryNotificationPayloadDto) }, + { $ref: getSchemaPath(SystemNotificationPayloadDto) }, + ], + discriminator: { + propertyName: "type", + mapping: { + order: getSchemaPath(OrderNotificationPayloadDto), + inventory: getSchemaPath(InventoryNotificationPayloadDto), + system: getSchemaPath(SystemNotificationPayloadDto), + }, + }, + }) + payload!: + | OrderNotificationPayloadDto + | InventoryNotificationPayloadDto + | SystemNotificationPayloadDto; + + @ApiProperty({ example: false }) + read!: boolean; + + @ApiProperty({ format: "date-time" }) + createdAt!: string; +} + +export class NotificationsResponseDto { + @ApiProperty({ type: [NotificationDto] }) + data!: NotificationDto[]; + + @ApiProperty({ type: CursorMetaDto }) + meta!: CursorMetaDto; +} + +export class NotificationResponseDto { + @ApiProperty({ type: NotificationDto }) + data!: NotificationDto; +} + +export class CursorQueryDto { + @ApiPropertyOptional({ minimum: 1, maximum: 100, default: 20 }) + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + @IsOptional() + limit = 20; + + @ApiPropertyOptional({ example: "notification-020" }) + @IsString() + @IsOptional() + cursor?: string; +} + +export class FileMetadataDto { + @ApiProperty({ example: "file-001" }) + id!: string; + + @ApiProperty({ example: "products.csv" }) + name!: string; + + @ApiProperty({ example: "text/csv" }) + mimeType!: string; + + @ApiProperty({ example: 18432 }) + size!: number; + + @ApiProperty({ format: "uri", example: "/api/v1/files/file-001/download" }) + downloadUrl!: string; + + @ApiProperty({ format: "date-time" }) + createdAt!: string; +} + +export class FileResponseDto { + @ApiProperty({ type: FileMetadataDto }) + data!: FileMetadataDto; +} + +export class FilesResponseDto { + @ApiProperty({ type: [FileMetadataDto] }) + data!: FileMetadataDto[]; +} + +export class AuditEventDto { + @ApiProperty({ example: "audit-001" }) + id!: string; + + @ApiProperty({ example: "product.updated" }) + action!: string; + + @ApiProperty({ example: "complex-user-admin" }) + actorId!: string; + + @ApiProperty({ example: "product" }) + resourceType!: string; + + @ApiProperty({ example: "complex-product-keyboard" }) + resourceId!: string; + + @ApiProperty({ + type: "object", + additionalProperties: true, + example: { version: 4 }, + }) + metadata!: Record<string, unknown>; + + @ApiProperty({ format: "date-time" }) + createdAt!: string; +} + +export class AuditQueryDto { + @ApiPropertyOptional({ default: 1, minimum: 1 }) + @Type(() => Number) + @IsInt() + @Min(1) + @IsOptional() + page = 1; + + @ApiPropertyOptional({ default: 20, minimum: 1, maximum: 100 }) + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + @IsOptional() + limit = 20; + + @ApiPropertyOptional({ example: "product.updated" }) + @IsString() + @IsOptional() + action?: string; +} + +export class AuditEventsResponseDto { + @ApiProperty({ type: [AuditEventDto] }) + data!: AuditEventDto[]; + + @ApiProperty({ type: PageMetaDto }) + meta!: PageMetaDto; +} + +export enum JobStatus { + Pending = "pending", + Processing = "processing", + Completed = "completed", + Failed = "failed", +} + +export class JobDto { + @ApiProperty({ example: "job-001" }) + id!: string; + + @ApiProperty({ enum: ["orders-export"], example: "orders-export" }) + type!: "orders-export"; + + @ApiProperty({ enum: JobStatus }) + status!: JobStatus; + + @ApiProperty({ example: 75, minimum: 0, maximum: 100 }) + progress!: number; + + @ApiProperty({ + format: "uri", + nullable: true, + example: "/api/v1/jobs/job-001/result", + }) + resultUrl!: string | null; + + @ApiProperty({ nullable: true, example: null }) + error!: string | null; + + @ApiProperty({ format: "date-time" }) + createdAt!: string; + + @ApiProperty({ format: "date-time" }) + updatedAt!: string; +} + +export class JobResponseDto { + @ApiProperty({ type: JobDto }) + data!: JobDto; +} diff --git a/examples/demo-backend/src/apps/complex/main.ts b/examples/demo-backend/src/apps/complex/main.ts new file mode 100644 index 0000000..dde086a --- /dev/null +++ b/examples/demo-backend/src/apps/complex/main.ts @@ -0,0 +1,12 @@ +import { createComplexApplication } from "./bootstrap"; + +async function bootstrap(): Promise<void> { + const { app } = await createComplexApplication(); + const port = Number(process.env.COMPLEX_PORT ?? 3002); + await app.listen(port); + console.log(`Complex API: http://localhost:${port}/api/v1`); + console.log(`Complex Swagger: http://localhost:${port}/docs`); + console.log(`Complex Socket.IO namespace: ws://localhost:${port}/chat`); +} + +void bootstrap(); diff --git a/examples/demo-backend/src/apps/simple/bootstrap.ts b/examples/demo-backend/src/apps/simple/bootstrap.ts new file mode 100644 index 0000000..a1bfefe --- /dev/null +++ b/examples/demo-backend/src/apps/simple/bootstrap.ts @@ -0,0 +1,20 @@ +import { NestFactory } from "@nestjs/core"; +import type { NestExpressApplication } from "@nestjs/platform-express"; +import type { OpenAPIObject } from "@nestjs/swagger"; +import { configureApplication } from "../../common/configure-application"; +import { createOpenApiDocument, mountOpenApi } from "../../common/openapi"; +import { SimpleAppModule } from "./simple.module"; + +export async function createSimpleApplication( + logger: false | undefined = undefined, +): Promise<{ app: NestExpressApplication; document: OpenAPIObject }> { + const app = await NestFactory.create<NestExpressApplication>( + SimpleAppModule, + { logger }, + ); + configureApplication(app); + const port = Number(process.env.SIMPLE_PORT ?? 3001); + const document = createOpenApiDocument(app, { kind: "simple", port }); + mountOpenApi(app, document, "Demo Simple API"); + return { app, document }; +} diff --git a/examples/demo-backend/src/apps/simple/main.ts b/examples/demo-backend/src/apps/simple/main.ts new file mode 100644 index 0000000..5923748 --- /dev/null +++ b/examples/demo-backend/src/apps/simple/main.ts @@ -0,0 +1,11 @@ +import { createSimpleApplication } from "./bootstrap"; + +async function bootstrap(): Promise<void> { + const { app } = await createSimpleApplication(); + const port = Number(process.env.SIMPLE_PORT ?? 3001); + await app.listen(port); + console.log(`Simple API: http://localhost:${port}/api/v1`); + console.log(`Simple Swagger: http://localhost:${port}/docs`); +} + +void bootstrap(); diff --git a/examples/demo-backend/src/apps/simple/simple.auth.ts b/examples/demo-backend/src/apps/simple/simple.auth.ts new file mode 100644 index 0000000..1bde69b --- /dev/null +++ b/examples/demo-backend/src/apps/simple/simple.auth.ts @@ -0,0 +1,197 @@ +import { randomUUID } from "node:crypto"; +import { + CanActivate, + ExecutionContext, + ForbiddenException, + Injectable, + UnauthorizedException, +} from "@nestjs/common"; +import { JwtService } from "@nestjs/jwt"; +import type { Request } from "express"; +import type { DemoRequest } from "../../common/request.types"; +import { assertNoForcedAuthFailure } from "../../common/scenario.interceptor"; +import { + type JwtAuthResponseDto, + type JwtTokensDto, + type LoginDto, + type RefreshTokenDto, + SimpleRole, +} from "./simple.dto"; +import { SimpleStore } from "./simple.store"; + +interface DemoJwtPayload { + sub: string; + type: "access" | "refresh"; + jti: string; +} + +@Injectable() +export class SimpleAuthService { + private readonly revokedRefreshTokens = new Set<string>(); + + constructor( + private readonly jwtService: JwtService, + private readonly store: SimpleStore, + ) {} + + async login(dto: LoginDto): Promise<JwtAuthResponseDto> { + const user = this.store.findUserByEmail(dto.email); + if (!user || user.password !== dto.password) { + throw new UnauthorizedException({ + code: "INVALID_CREDENTIALS", + message: "Email or password is incorrect.", + }); + } + return { + data: { + tokens: await this.issueTokens(user.id), + user: this.store.publicUser(user), + }, + }; + } + + async refresh(dto: RefreshTokenDto): Promise<JwtAuthResponseDto> { + let payload: DemoJwtPayload; + try { + payload = await this.jwtService.verifyAsync<DemoJwtPayload>( + dto.refreshToken, + { secret: this.refreshSecret }, + ); + } catch { + throw new UnauthorizedException({ + code: "INVALID_REFRESH_TOKEN", + message: "Refresh token is invalid or expired.", + }); + } + if ( + payload.type !== "refresh" || + this.revokedRefreshTokens.has(payload.jti) + ) { + throw new UnauthorizedException({ + code: "REFRESH_TOKEN_REUSED", + message: "Refresh token was revoked or already used.", + }); + } + const user = this.store.findUserById(payload.sub); + if (!user) { + throw new UnauthorizedException({ + code: "USER_NOT_FOUND", + message: "Token user no longer exists.", + }); + } + this.revokedRefreshTokens.add(payload.jti); + return { + data: { + tokens: await this.issueTokens(user.id), + user: this.store.publicUser(user), + }, + }; + } + + async logout(dto: RefreshTokenDto): Promise<void> { + try { + const payload = await this.jwtService.verifyAsync<DemoJwtPayload>( + dto.refreshToken, + { secret: this.refreshSecret }, + ); + this.revokedRefreshTokens.add(payload.jti); + } catch { + // Logout remains idempotent even when the token has already expired. + } + } + + reset(): void { + this.revokedRefreshTokens.clear(); + } + + private async issueTokens(userId: string): Promise<JwtTokensDto> { + const accessJti = randomUUID(); + const refreshJti = randomUUID(); + const accessTtl = process.env.JWT_ACCESS_TTL ?? "60s"; + const refreshTtl = process.env.JWT_REFRESH_TTL ?? "7d"; + const [accessToken, refreshToken] = await Promise.all([ + this.jwtService.signAsync( + { sub: userId, type: "access", jti: accessJti }, + { secret: this.accessSecret, expiresIn: accessTtl as never }, + ), + this.jwtService.signAsync( + { sub: userId, type: "refresh", jti: refreshJti }, + { secret: this.refreshSecret, expiresIn: refreshTtl as never }, + ), + ]); + return { + accessToken, + refreshToken, + expiresIn: this.accessTtlSeconds(accessTtl), + tokenType: "Bearer", + }; + } + + private accessTtlSeconds(value: string): number { + const match = /^(\d+)([smhd])$/.exec(value); + if (!match) return 60; + const multipliers = { s: 1, m: 60, h: 3600, d: 86400 }; + return Number(match[1]) * multipliers[match[2] as keyof typeof multipliers]; + } + + private get accessSecret(): string { + return process.env.JWT_ACCESS_SECRET ?? "demo-access-secret-change-me"; + } + + private get refreshSecret(): string { + return process.env.JWT_REFRESH_SECRET ?? "demo-refresh-secret-change-me"; + } +} + +@Injectable() +export class SimpleJwtGuard implements CanActivate { + constructor( + private readonly jwtService: JwtService, + private readonly store: SimpleStore, + ) {} + + async canActivate(context: ExecutionContext): Promise<boolean> { + const request = context.switchToHttp().getRequest<Request>() as DemoRequest; + assertNoForcedAuthFailure(request); + const authorization = request.headers.authorization; + if (!authorization?.startsWith("Bearer ")) { + throw new UnauthorizedException({ + code: "JWT_MISSING", + message: "Bearer access token is required.", + }); + } + try { + const payload = await this.jwtService.verifyAsync<DemoJwtPayload>( + authorization.slice(7), + { + secret: + process.env.JWT_ACCESS_SECRET ?? "demo-access-secret-change-me", + }, + ); + if (payload.type !== "access") throw new Error("Wrong token type"); + const user = this.store.findUserById(payload.sub); + if (!user) throw new Error("Unknown user"); + request.user = this.store.publicUser(user); + return true; + } catch { + throw new UnauthorizedException({ + code: "JWT_INVALID", + message: "Access token is invalid or expired.", + }); + } + } +} + +@Injectable() +export class SimpleAdminGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest<DemoRequest>(); + if (request.user?.role !== SimpleRole.Admin) { + throw new ForbiddenException({ + code: "ADMIN_REQUIRED", + message: "Administrator role is required.", + }); + } + return true; + } +} diff --git a/examples/demo-backend/src/apps/simple/simple.controllers.ts b/examples/demo-backend/src/apps/simple/simple.controllers.ts new file mode 100644 index 0000000..c9eca0b --- /dev/null +++ b/examples/demo-backend/src/apps/simple/simple.controllers.ts @@ -0,0 +1,399 @@ +import { + BadRequestException, + Body, + Controller, + Delete, + Get, + Headers, + HttpCode, + Param, + Patch, + Post, + Query, + Req, + Res, + UseGuards, +} from "@nestjs/common"; +import { + ApiBearerAuth, + ApiCreatedResponse, + ApiNoContentResponse, + ApiNotModifiedResponse, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiTags, +} from "@nestjs/swagger"; +import type { Response } from "express"; +import { + ApiDemoScenarioHeader, + ApiStandardErrors, +} from "../../common/api.decorators"; +import { + HealthResponseDto, + MutationResponseDto, + ScenariosResponseDto, + TestingActionResponseDto, +} from "../../common/api.dto"; +import type { DemoRequest } from "../../common/request.types"; +import { + SimpleAdminGuard, + SimpleAuthService, + SimpleJwtGuard, +} from "./simple.auth"; +import { + CategoriesResponseDto, + CategoryResponseDto, + ChangeSimpleRoleDto, + CreateOrderDto, + CreateProductDto, + JwtAuthResponseDto, + LoginDto, + OrderResponseDto, + OrdersResponseDto, + PageQueryDto, + ProductQueryDto, + ProductResponseDto, + ProductsResponseDto, + RefreshTokenDto, + SimpleRole, + SimpleUserResponseDto, + UpdateProductDto, +} from "./simple.dto"; +import { SimpleStore } from "./simple.store"; + +@ApiTags("Health") +@ApiDemoScenarioHeader() +@Controller("health") +export class SimpleHealthController { + @Get() + @ApiOperation({ summary: "Check Simple API availability" }) + @ApiOkResponse({ type: HealthResponseDto }) + health(): HealthResponseDto { + return { + data: { + application: "simple", + status: "ok", + timestamp: new Date().toISOString(), + version: "1.0.0", + }, + }; + } +} + +@ApiTags("Auth") +@ApiDemoScenarioHeader() +@Controller("auth") +export class SimpleAuthController { + constructor(private readonly auth: SimpleAuthService) {} + + @Post("login") + @HttpCode(200) + @ApiOperation({ summary: "Login and receive JWT access/refresh tokens" }) + @ApiOkResponse({ type: JwtAuthResponseDto }) + @ApiStandardErrors() + login(@Body() dto: LoginDto): Promise<JwtAuthResponseDto> { + return this.auth.login(dto); + } + + @Post("refresh") + @HttpCode(200) + @ApiOperation({ + summary: "Rotate a refresh token and issue a new token pair", + }) + @ApiOkResponse({ type: JwtAuthResponseDto }) + @ApiStandardErrors({ auth: true }) + refresh(@Body() dto: RefreshTokenDto): Promise<JwtAuthResponseDto> { + return this.auth.refresh(dto); + } + + @Post("logout") + @HttpCode(204) + @ApiOperation({ + summary: "Revoke a refresh token; the operation is idempotent", + }) + @ApiNoContentResponse() + @ApiStandardErrors() + async logout(@Body() dto: RefreshTokenDto): Promise<void> { + await this.auth.logout(dto); + } +} + +@ApiTags("Users") +@ApiBearerAuth("jwt") +@ApiDemoScenarioHeader() +@UseGuards(SimpleJwtGuard) +@Controller("users") +export class SimpleUsersController { + constructor(private readonly store: SimpleStore) {} + + @Get("me") + @ApiOperation({ summary: "Get the authenticated user" }) + @ApiOkResponse({ type: SimpleUserResponseDto }) + @ApiStandardErrors({ auth: true }) + me(@Req() request: DemoRequest): SimpleUserResponseDto { + const user = this.store.findUserById(request.user!.id)!; + return { data: this.store.publicUser(user) }; + } +} + +@ApiTags("Products") +@ApiDemoScenarioHeader() +@Controller("products") +export class SimpleProductsController { + constructor(private readonly store: SimpleStore) {} + + @Get() + @ApiOperation({ + summary: "List products using offset pagination, filters and sorting", + }) + @ApiOkResponse({ type: ProductsResponseDto }) + @ApiStandardErrors() + list(@Query() query: ProductQueryDto): ProductsResponseDto { + return this.store.listProducts(query); + } + + @Get(":id") + @ApiOperation({ summary: "Get one product with ETag support" }) + @ApiOkResponse({ type: ProductResponseDto }) + @ApiNotModifiedResponse({ + description: "The supplied If-None-Match value is current.", + }) + @ApiStandardErrors({ notFound: true }) + get( + @Param("id") id: string, + @Headers("if-none-match") ifNoneMatch: string | undefined, + @Res({ passthrough: true }) response: Response, + ): ProductResponseDto | undefined { + const product = this.store.getProduct(id); + const etag = `W/\"${product.id}-v${product.version}\"`; + response.setHeader("ETag", etag); + response.setHeader("Cache-Control", "private, max-age=0, must-revalidate"); + if (ifNoneMatch === etag) { + response.status(304); + return undefined; + } + return { data: product }; + } + + @Post() + @UseGuards(SimpleJwtGuard, SimpleAdminGuard) + @ApiBearerAuth("jwt") + @ApiOperation({ summary: "Create a product as an administrator" }) + @ApiCreatedResponse({ type: ProductResponseDto }) + @ApiStandardErrors({ auth: true }) + create(@Body() dto: CreateProductDto): ProductResponseDto { + return { data: this.store.createProduct(dto) }; + } + + @Patch(":id") + @UseGuards(SimpleJwtGuard, SimpleAdminGuard) + @ApiBearerAuth("jwt") + @ApiOperation({ summary: "Update a product using optimistic locking" }) + @ApiOkResponse({ type: ProductResponseDto }) + @ApiStandardErrors({ auth: true, notFound: true, conflict: true }) + update( + @Param("id") id: string, + @Body() dto: UpdateProductDto, + ): ProductResponseDto { + return { data: this.store.updateProduct(id, dto) }; + } + + @Delete(":id") + @UseGuards(SimpleJwtGuard, SimpleAdminGuard) + @ApiBearerAuth("jwt") + @ApiOperation({ summary: "Delete a product as an administrator" }) + @ApiOkResponse({ type: MutationResponseDto }) + @ApiStandardErrors({ auth: true, notFound: true }) + remove(@Param("id") id: string): MutationResponseDto { + this.store.deleteProduct(id); + return { data: { id, success: true } }; + } +} + +@ApiTags("Categories") +@ApiDemoScenarioHeader() +@Controller("categories") +export class SimpleCategoriesController { + constructor(private readonly store: SimpleStore) {} + + @Get() + @ApiOperation({ summary: "List product categories" }) + @ApiOkResponse({ type: CategoriesResponseDto }) + list(): CategoriesResponseDto { + return { data: this.store.listCategories() }; + } + + @Get(":id") + @ApiOperation({ summary: "Get one category" }) + @ApiOkResponse({ type: CategoryResponseDto }) + @ApiStandardErrors({ notFound: true }) + get(@Param("id") id: string): CategoryResponseDto { + return { data: this.store.getCategory(id) }; + } +} + +@ApiTags("Orders") +@ApiBearerAuth("jwt") +@ApiDemoScenarioHeader() +@UseGuards(SimpleJwtGuard) +@Controller("orders") +export class SimpleOrdersController { + constructor(private readonly store: SimpleStore) {} + + @Get() + @ApiOperation({ summary: "List orders visible to the current user" }) + @ApiOkResponse({ type: OrdersResponseDto }) + @ApiStandardErrors({ auth: true }) + list( + @Req() request: DemoRequest, + @Query() query: PageQueryDto, + ): OrdersResponseDto { + return this.store.listOrders( + request.user!.id, + request.user!.role, + query.page, + query.limit, + ); + } + + @Get(":id") + @ApiOperation({ summary: "Get one visible order" }) + @ApiOkResponse({ type: OrderResponseDto }) + @ApiStandardErrors({ auth: true, notFound: true }) + get(@Param("id") id: string, @Req() request: DemoRequest): OrderResponseDto { + return { + data: this.store.getOrder(id, request.user!.id, request.user!.role), + }; + } + + @Post() + @ApiOperation({ summary: "Create an order and validate product stock" }) + @ApiCreatedResponse({ type: OrderResponseDto }) + @ApiStandardErrors({ auth: true, conflict: true }) + create( + @Req() request: DemoRequest, + @Body() dto: CreateOrderDto, + ): OrderResponseDto { + return { data: this.store.createOrder(request.user!.id, dto) }; + } + + @Post(":id/cancel") + @HttpCode(200) + @ApiOperation({ + summary: "Cancel an order if its state permits the transition", + }) + @ApiOkResponse({ type: OrderResponseDto }) + @ApiStandardErrors({ auth: true, notFound: true, conflict: true }) + cancel( + @Param("id") id: string, + @Req() request: DemoRequest, + ): OrderResponseDto { + return { + data: this.store.cancelOrder(id, request.user!.id, request.user!.role), + }; + } +} + +@ApiTags("Testing") +@ApiDemoScenarioHeader() +@Controller("testing") +export class SimpleTestingController { + constructor( + private readonly store: SimpleStore, + private readonly auth: SimpleAuthService, + ) {} + + @Get("scenarios") + @ApiOperation({ summary: "List deterministic X-Demo-Scenario values" }) + @ApiOkResponse({ type: ScenariosResponseDto }) + scenarios(): ScenariosResponseDto { + return { + data: [ + { name: "normal", description: "Default behavior." }, + { + name: "slow", + description: + "Delays the response to exercise loading and cancellation states.", + }, + { + name: "timeout", + description: + "Delays long enough for a frontend timeout or cancellation.", + }, + { + name: "server-error", + description: "Returns a deterministic 500 error.", + }, + { name: "rate-limited", description: "Returns 429 with Retry-After." }, + { + name: "empty", + description: "Turns list responses into a valid empty state.", + }, + { + name: "expired-auth", + description: "Makes protected routes return 401.", + }, + { + name: "forbidden", + description: "Makes protected routes return 403.", + }, + { name: "conflict", description: "Makes mutation routes return 409." }, + { + name: "large-dataset", + description: "Expands list responses to 250 deterministic items.", + }, + ], + }; + } + + @Post("reset") + @HttpCode(200) + @ApiOperation({ summary: "Reset all Simple API state and token revocations" }) + @ApiOkResponse({ type: TestingActionResponseDto }) + reset(): TestingActionResponseDto { + this.store.reset("small"); + this.auth.reset(); + return { + data: { + success: true, + message: "Simple API state reset to the default deterministic seed.", + }, + }; + } + + @Post("seed/:preset") + @HttpCode(200) + @ApiParam({ name: "preset", enum: ["small", "large"] }) + @ApiOperation({ summary: "Select a small or large deterministic dataset" }) + @ApiOkResponse({ type: TestingActionResponseDto }) + seed(@Param("preset") preset: string): TestingActionResponseDto { + if (preset !== "small" && preset !== "large") { + throw new BadRequestException({ + code: "UNKNOWN_SEED_PRESET", + message: "Preset must be small or large.", + }); + } + this.store.reset(preset); + return { + data: { + success: true, + message: `Simple API loaded the ${preset} dataset.`, + }, + }; + } + + @Post("users/:userId/role") + @HttpCode(200) + @ApiOperation({ + summary: "Change a user role to exercise dynamic access control", + }) + @ApiOkResponse({ type: SimpleUserResponseDto }) + @ApiStandardErrors({ notFound: true }) + changeRole( + @Param("userId") userId: string, + @Body() dto: ChangeSimpleRoleDto, + ): SimpleUserResponseDto { + return { data: this.store.changeRole(userId, dto.role) }; + } +} diff --git a/examples/demo-backend/src/apps/simple/simple.dto.ts b/examples/demo-backend/src/apps/simple/simple.dto.ts new file mode 100644 index 0000000..3254b74 --- /dev/null +++ b/examples/demo-backend/src/apps/simple/simple.dto.ts @@ -0,0 +1,365 @@ +import { Type } from "class-transformer"; +import { + IsArray, + IsEmail, + IsEnum, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + Max, + MaxLength, + Min, + MinLength, + ValidateNested, +} from "class-validator"; +import { ApiProperty, ApiPropertyOptional, PartialType } from "@nestjs/swagger"; +import { PageMetaDto } from "../../common/api.dto"; + +export enum SimpleRole { + Admin = "admin", + Customer = "customer", +} + +export class SimpleUserDto { + @ApiProperty({ example: "user-admin" }) + id!: string; + + @ApiProperty({ format: "email", example: "admin@demo.local" }) + email!: string; + + @ApiProperty({ example: "Demo Admin" }) + name!: string; + + @ApiProperty({ enum: SimpleRole }) + role!: SimpleRole; + + @ApiProperty({ nullable: true, example: "https://i.pravatar.cc/160?img=12" }) + avatarUrl!: string | null; +} + +export class SimpleUserResponseDto { + @ApiProperty({ type: SimpleUserDto }) + data!: SimpleUserDto; +} + +export class LoginDto { + @ApiProperty({ format: "email", example: "admin@demo.local" }) + @IsEmail() + email!: string; + + @ApiProperty({ format: "password", example: "demo1234", minLength: 8 }) + @IsString() + @MinLength(8) + password!: string; +} + +export class RefreshTokenDto { + @ApiProperty({ + description: + "Refresh token returned by login or the previous refresh call.", + }) + @IsString() + @IsNotEmpty() + refreshToken!: string; +} + +export class JwtTokensDto { + @ApiProperty() + accessToken!: string; + + @ApiProperty() + refreshToken!: string; + + @ApiProperty({ + example: 60, + description: "Access-token lifetime in seconds.", + }) + expiresIn!: number; + + @ApiProperty({ enum: ["Bearer"], example: "Bearer" }) + tokenType!: "Bearer"; +} + +export class JwtAuthDataDto { + @ApiProperty({ type: JwtTokensDto }) + tokens!: JwtTokensDto; + + @ApiProperty({ type: SimpleUserDto }) + user!: SimpleUserDto; +} + +export class JwtAuthResponseDto { + @ApiProperty({ type: JwtAuthDataDto }) + data!: JwtAuthDataDto; +} + +export enum ProductSort { + Newest = "newest", + PriceAsc = "price-asc", + PriceDesc = "price-desc", + Name = "name", +} + +export class ProductQueryDto { + @ApiPropertyOptional({ default: 1, minimum: 1 }) + @Type(() => Number) + @IsInt() + @Min(1) + @IsOptional() + page = 1; + + @ApiPropertyOptional({ default: 20, minimum: 1, maximum: 100 }) + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + @IsOptional() + limit = 20; + + @ApiPropertyOptional({ example: "keyboard" }) + @IsString() + @IsOptional() + search?: string; + + @ApiPropertyOptional({ example: "category-electronics" }) + @IsString() + @IsOptional() + categoryId?: string; + + @ApiPropertyOptional({ enum: ProductSort, default: ProductSort.Newest }) + @IsEnum(ProductSort) + @IsOptional() + sort = ProductSort.Newest; +} + +export class PageQueryDto { + @ApiPropertyOptional({ default: 1, minimum: 1 }) + @Type(() => Number) + @IsInt() + @Min(1) + @IsOptional() + page = 1; + + @ApiPropertyOptional({ default: 20, minimum: 1, maximum: 100 }) + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + @IsOptional() + limit = 20; +} + +export class SimpleProductDto { + @ApiProperty({ example: "product-keyboard" }) + id!: string; + + @ApiProperty({ example: "Mechanical Keyboard" }) + name!: string; + + @ApiProperty({ example: "mechanical-keyboard" }) + slug!: string; + + @ApiProperty({ example: "Hot-swappable compact keyboard." }) + description!: string; + + @ApiProperty({ + example: 12990, + description: "Price in the smallest currency unit.", + }) + priceCents!: number; + + @ApiProperty({ enum: ["USD", "EUR"], example: "USD" }) + currency!: "USD" | "EUR"; + + @ApiProperty({ example: "category-electronics" }) + categoryId!: string; + + @ApiProperty({ example: 24, minimum: 0 }) + stock!: number; + + @ApiProperty({ example: 4.8, minimum: 0, maximum: 5 }) + rating!: number; + + @ApiProperty({ + format: "uri", + example: "https://picsum.photos/seed/keyboard/640/480", + }) + imageUrl!: string; + + @ApiProperty({ format: "date-time" }) + createdAt!: string; + + @ApiProperty({ example: 1, description: "Optimistic-lock version." }) + version!: number; +} + +export class ProductsResponseDto { + @ApiProperty({ type: [SimpleProductDto] }) + data!: SimpleProductDto[]; + + @ApiProperty({ type: PageMetaDto }) + meta!: PageMetaDto; +} + +export class ProductResponseDto { + @ApiProperty({ type: SimpleProductDto }) + data!: SimpleProductDto; +} + +export class CreateProductDto { + @ApiProperty({ example: "USB-C Dock" }) + @IsString() + @MinLength(2) + @MaxLength(120) + name!: string; + + @ApiProperty({ example: "Dock with HDMI, Ethernet and power delivery." }) + @IsString() + @MinLength(10) + @MaxLength(1000) + description!: string; + + @ApiProperty({ example: 8990, minimum: 0 }) + @IsInt() + @Min(0) + priceCents!: number; + + @ApiProperty({ enum: ["USD", "EUR"], example: "USD" }) + @IsEnum(["USD", "EUR"]) + currency!: "USD" | "EUR"; + + @ApiProperty({ example: "category-electronics" }) + @IsString() + categoryId!: string; + + @ApiProperty({ example: 15, minimum: 0 }) + @IsInt() + @Min(0) + stock!: number; + + @ApiProperty({ + format: "uri", + example: "https://picsum.photos/seed/dock/640/480", + }) + @IsString() + imageUrl!: string; +} + +export class UpdateProductDto extends PartialType(CreateProductDto) { + @ApiProperty({ + example: 1, + minimum: 1, + description: "Version last read by the frontend.", + }) + @IsInt() + @Min(1) + version!: number; +} + +export class SimpleCategoryDto { + @ApiProperty({ example: "category-electronics" }) + id!: string; + + @ApiProperty({ example: "Electronics" }) + name!: string; + + @ApiProperty({ example: "electronics" }) + slug!: string; + + @ApiProperty({ example: 4 }) + productCount!: number; +} + +export class CategoriesResponseDto { + @ApiProperty({ type: [SimpleCategoryDto] }) + data!: SimpleCategoryDto[]; +} + +export class CategoryResponseDto { + @ApiProperty({ type: SimpleCategoryDto }) + data!: SimpleCategoryDto; +} + +export enum OrderStatus { + Pending = "pending", + Paid = "paid", + Shipped = "shipped", + Cancelled = "cancelled", +} + +export class CreateOrderItemDto { + @ApiProperty({ example: "product-keyboard" }) + @IsString() + productId!: string; + + @ApiProperty({ example: 1, minimum: 1, maximum: 20 }) + @IsInt() + @Min(1) + @Max(20) + quantity!: number; +} + +export class CreateOrderDto { + @ApiProperty({ type: [CreateOrderItemDto] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreateOrderItemDto) + items!: CreateOrderItemDto[]; +} + +export class SimpleOrderItemDto { + @ApiProperty({ example: "product-keyboard" }) + productId!: string; + + @ApiProperty({ example: "Mechanical Keyboard" }) + productName!: string; + + @ApiProperty({ example: 1 }) + quantity!: number; + + @ApiProperty({ example: 12990 }) + unitPriceCents!: number; +} + +export class SimpleOrderDto { + @ApiProperty({ example: "order-001" }) + id!: string; + + @ApiProperty({ example: "user-customer" }) + userId!: string; + + @ApiProperty({ enum: OrderStatus }) + status!: OrderStatus; + + @ApiProperty({ type: [SimpleOrderItemDto] }) + items!: SimpleOrderItemDto[]; + + @ApiProperty({ example: 17980 }) + totalCents!: number; + + @ApiProperty({ enum: ["USD", "EUR"], example: "USD" }) + currency!: "USD" | "EUR"; + + @ApiProperty({ format: "date-time" }) + createdAt!: string; +} + +export class OrdersResponseDto { + @ApiProperty({ type: [SimpleOrderDto] }) + data!: SimpleOrderDto[]; + + @ApiProperty({ type: PageMetaDto }) + meta!: PageMetaDto; +} + +export class OrderResponseDto { + @ApiProperty({ type: SimpleOrderDto }) + data!: SimpleOrderDto; +} + +export class ChangeSimpleRoleDto { + @ApiProperty({ enum: SimpleRole, example: SimpleRole.Customer }) + @IsEnum(SimpleRole) + role!: SimpleRole; +} diff --git a/examples/demo-backend/src/apps/simple/simple.module.ts b/examples/demo-backend/src/apps/simple/simple.module.ts new file mode 100644 index 0000000..0eae9a8 --- /dev/null +++ b/examples/demo-backend/src/apps/simple/simple.module.ts @@ -0,0 +1,33 @@ +import { Module } from "@nestjs/common"; +import { JwtModule } from "@nestjs/jwt"; +import { InfrastructureModule } from "../../common/infrastructure.module"; +import { + SimpleAdminGuard, + SimpleAuthService, + SimpleJwtGuard, +} from "./simple.auth"; +import { + SimpleAuthController, + SimpleCategoriesController, + SimpleHealthController, + SimpleOrdersController, + SimpleProductsController, + SimpleTestingController, + SimpleUsersController, +} from "./simple.controllers"; +import { SimpleStore } from "./simple.store"; + +@Module({ + imports: [InfrastructureModule, JwtModule.register({})], + controllers: [ + SimpleHealthController, + SimpleAuthController, + SimpleUsersController, + SimpleProductsController, + SimpleCategoriesController, + SimpleOrdersController, + SimpleTestingController, + ], + providers: [SimpleStore, SimpleAuthService, SimpleJwtGuard, SimpleAdminGuard], +}) +export class SimpleAppModule {} diff --git a/examples/demo-backend/src/apps/simple/simple.store.ts b/examples/demo-backend/src/apps/simple/simple.store.ts new file mode 100644 index 0000000..312f8cf --- /dev/null +++ b/examples/demo-backend/src/apps/simple/simple.store.ts @@ -0,0 +1,432 @@ +import { + ConflictException, + Injectable, + NotFoundException, + UnprocessableEntityException, +} from "@nestjs/common"; +import { + type CreateOrderDto, + type CreateProductDto, + OrderStatus, + ProductSort, + type ProductQueryDto, + type SimpleCategoryDto, + type SimpleOrderDto, + type SimpleProductDto, + SimpleRole, + type SimpleUserDto, + type UpdateProductDto, +} from "./simple.dto"; + +interface SimpleUserRecord extends SimpleUserDto { + password: string; +} + +@Injectable() +export class SimpleStore { + private users: SimpleUserRecord[] = []; + private categories: SimpleCategoryDto[] = []; + private products: SimpleProductDto[] = []; + private orders: SimpleOrderDto[] = []; + private productSequence = 10; + private orderSequence = 10; + + constructor() { + this.reset("small"); + } + + reset(preset: "small" | "large" = "small"): void { + this.users = [ + { + id: "user-admin", + email: "admin@demo.local", + password: "demo1234", + name: "Demo Admin", + role: SimpleRole.Admin, + avatarUrl: "https://i.pravatar.cc/160?img=12", + }, + { + id: "user-customer", + email: "customer@demo.local", + password: "demo1234", + name: "Demo Customer", + role: SimpleRole.Customer, + avatarUrl: null, + }, + ]; + this.categories = [ + { + id: "category-electronics", + name: "Electronics", + slug: "electronics", + productCount: 0, + }, + { + id: "category-home", + name: "Home office", + slug: "home-office", + productCount: 0, + }, + { id: "category-books", name: "Books", slug: "books", productCount: 0 }, + ]; + const baseProducts: SimpleProductDto[] = [ + this.product( + "product-keyboard", + "Mechanical Keyboard", + 12990, + "category-electronics", + 24, + 4.8, + ), + this.product( + "product-mouse", + "Ergonomic Mouse", + 6990, + "category-electronics", + 42, + 4.6, + ), + this.product( + "product-desk", + "Standing Desk", + 45900, + "category-home", + 8, + 4.9, + ), + this.product( + "product-lamp", + "Focus Desk Lamp", + 5490, + "category-home", + 31, + 4.5, + ), + this.product( + "product-book", + "Frontend Systems Handbook", + 3990, + "category-books", + 100, + 4.7, + ), + this.product( + "product-headphones", + "Studio Headphones", + 18990, + "category-electronics", + 12, + 4.4, + ), + ]; + this.products = + preset === "large" + ? Array.from({ length: 250 }, (_, index) => { + const source = baseProducts[index % baseProducts.length]; + const number = index + 1; + return { + ...source, + id: `product-${String(number).padStart(3, "0")}`, + name: `${source.name} ${number}`, + slug: `${source.slug}-${number}`, + }; + }) + : baseProducts; + this.orders = [ + { + id: "order-001", + userId: "user-customer", + status: OrderStatus.Paid, + items: [ + { + productId: "product-keyboard", + productName: "Mechanical Keyboard", + quantity: 1, + unitPriceCents: 12990, + }, + ], + totalCents: 12990, + currency: "USD", + createdAt: "2026-07-20T10:30:00.000Z", + }, + { + id: "order-002", + userId: "user-customer", + status: OrderStatus.Shipped, + items: [ + { + productId: "product-book", + productName: "Frontend Systems Handbook", + quantity: 1, + unitPriceCents: 3990, + }, + ], + totalCents: 3990, + currency: "USD", + createdAt: "2026-07-22T14:00:00.000Z", + }, + ]; + this.productSequence = this.products.length + 10; + this.orderSequence = 10; + this.updateCategoryCounts(); + } + + findUserByEmail(email: string): SimpleUserRecord | undefined { + return this.users.find( + (user) => user.email.toLowerCase() === email.toLowerCase(), + ); + } + + findUserById(id: string): SimpleUserRecord | undefined { + return this.users.find((user) => user.id === id); + } + + publicUser(user: SimpleUserRecord): SimpleUserDto { + const { password: _password, ...publicUser } = user; + return publicUser; + } + + changeRole(userId: string, role: SimpleRole): SimpleUserDto { + const user = this.findUserById(userId); + if (!user) { + throw new NotFoundException({ + code: "USER_NOT_FOUND", + message: "User not found.", + }); + } + user.role = role; + return this.publicUser(user); + } + + listProducts(query: ProductQueryDto): { + data: SimpleProductDto[]; + meta: { page: number; limit: number; total: number; totalPages: number }; + } { + let products = [...this.products]; + if (query.search) { + const search = query.search.toLowerCase(); + products = products.filter((product) => + `${product.name} ${product.description}`.toLowerCase().includes(search), + ); + } + if (query.categoryId) { + products = products.filter( + (product) => product.categoryId === query.categoryId, + ); + } + products.sort((left, right) => { + if (query.sort === ProductSort.PriceAsc) + return left.priceCents - right.priceCents; + if (query.sort === ProductSort.PriceDesc) + return right.priceCents - left.priceCents; + if (query.sort === ProductSort.Name) + return left.name.localeCompare(right.name); + return right.createdAt.localeCompare(left.createdAt); + }); + const total = products.length; + const start = (query.page - 1) * query.limit; + return { + data: products.slice(start, start + query.limit), + meta: { + page: query.page, + limit: query.limit, + total, + totalPages: Math.ceil(total / query.limit), + }, + }; + } + + getProduct(id: string): SimpleProductDto { + const product = this.products.find((item) => item.id === id); + if (!product) { + throw new NotFoundException({ + code: "PRODUCT_NOT_FOUND", + message: "Product not found.", + }); + } + return product; + } + + createProduct(dto: CreateProductDto): SimpleProductDto { + if (!this.categories.some((category) => category.id === dto.categoryId)) { + throw new UnprocessableEntityException({ + code: "CATEGORY_NOT_FOUND", + message: "Selected category does not exist.", + details: [{ field: "categoryId", message: "Unknown category." }], + }); + } + const id = `product-${this.productSequence++}`; + const product: SimpleProductDto = { + id, + name: dto.name, + slug: `${dto.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${id}`, + description: dto.description, + priceCents: dto.priceCents, + currency: dto.currency, + categoryId: dto.categoryId, + stock: dto.stock, + rating: 0, + imageUrl: dto.imageUrl, + createdAt: new Date().toISOString(), + version: 1, + }; + this.products.unshift(product); + this.updateCategoryCounts(); + return product; + } + + updateProduct(id: string, dto: UpdateProductDto): SimpleProductDto { + const product = this.getProduct(id); + if (product.version !== dto.version) { + throw new ConflictException({ + code: "PRODUCT_VERSION_CONFLICT", + message: "Product was changed by another user.", + details: [ + { + field: "version", + message: `Expected ${product.version}, received ${dto.version}.`, + }, + ], + }); + } + const { version: _version, ...changes } = dto; + Object.assign(product, changes, { version: product.version + 1 }); + this.updateCategoryCounts(); + return product; + } + + deleteProduct(id: string): void { + this.getProduct(id); + this.products = this.products.filter((product) => product.id !== id); + this.updateCategoryCounts(); + } + + listCategories(): SimpleCategoryDto[] { + return this.categories; + } + + getCategory(id: string): SimpleCategoryDto { + const category = this.categories.find((item) => item.id === id); + if (!category) { + throw new NotFoundException({ + code: "CATEGORY_NOT_FOUND", + message: "Category not found.", + }); + } + return category; + } + + listOrders(userId: string, role: string, page = 1, limit = 20) { + const orders = + role === SimpleRole.Admin + ? this.orders + : this.orders.filter((order) => order.userId === userId); + const total = orders.length; + const start = (page - 1) * limit; + return { + data: orders.slice(start, start + limit), + meta: { page, limit, total, totalPages: Math.ceil(total / limit) }, + }; + } + + getOrder(id: string, userId: string, role: string): SimpleOrderDto { + const order = this.orders.find( + (item) => + item.id === id && (role === SimpleRole.Admin || item.userId === userId), + ); + if (!order) { + throw new NotFoundException({ + code: "ORDER_NOT_FOUND", + message: "Order not found.", + }); + } + return order; + } + + createOrder(userId: string, dto: CreateOrderDto): SimpleOrderDto { + if (dto.items.length === 0) { + throw new UnprocessableEntityException({ + code: "EMPTY_ORDER", + message: "Order must contain at least one item.", + details: [{ field: "items", message: "Add at least one item." }], + }); + } + const items = dto.items.map(({ productId, quantity }) => { + const product = this.getProduct(productId); + if (product.stock < quantity) { + throw new ConflictException({ + code: "INSUFFICIENT_STOCK", + message: `Not enough stock for ${product.name}.`, + }); + } + return { + productId, + productName: product.name, + quantity, + unitPriceCents: product.priceCents, + }; + }); + const order: SimpleOrderDto = { + id: `order-${String(this.orderSequence++).padStart(3, "0")}`, + userId, + status: OrderStatus.Pending, + items, + totalCents: items.reduce( + (sum, item) => sum + item.unitPriceCents * item.quantity, + 0, + ), + currency: "USD", + createdAt: new Date().toISOString(), + }; + this.orders.unshift(order); + return order; + } + + cancelOrder(id: string, userId: string, role: string): SimpleOrderDto { + const order = this.getOrder(id, userId, role); + if ( + order.status === OrderStatus.Shipped || + order.status === OrderStatus.Cancelled + ) { + throw new ConflictException({ + code: "ORDER_CANNOT_BE_CANCELLED", + message: `Order in ${order.status} status cannot be cancelled.`, + }); + } + order.status = OrderStatus.Cancelled; + return order; + } + + private product( + id: string, + name: string, + priceCents: number, + categoryId: string, + stock: number, + rating: number, + ): SimpleProductDto { + const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-"); + return { + id, + name, + slug, + description: `${name} is a deterministic demo product used by frontend examples.`, + priceCents, + currency: "USD", + categoryId, + stock, + rating, + imageUrl: `https://picsum.photos/seed/${slug}/640/480`, + createdAt: `2026-07-${String(10 + this.products.length).padStart(2, "0")}T09:00:00.000Z`, + version: 1, + }; + } + + private updateCategoryCounts(): void { + for (const category of this.categories) { + category.productCount = this.products.filter( + (product) => product.categoryId === category.id, + ).length; + } + } +} diff --git a/examples/demo-backend/src/common/api-exception.filter.ts b/examples/demo-backend/src/common/api-exception.filter.ts new file mode 100644 index 0000000..ed72dbe --- /dev/null +++ b/examples/demo-backend/src/common/api-exception.filter.ts @@ -0,0 +1,79 @@ +import { + ArgumentsHost, + Catch, + ExceptionFilter, + HttpException, + HttpStatus, + Injectable, +} from "@nestjs/common"; +import type { Request, Response } from "express"; +import type { DemoRequest } from "./request.types"; + +interface HttpErrorBody { + code?: string; + message?: string | string[]; + details?: unknown[]; + error?: string; +} + +const STATUS_CODES: Record<number, string> = { + 400: "BAD_REQUEST", + 401: "UNAUTHORIZED", + 403: "FORBIDDEN", + 404: "NOT_FOUND", + 409: "CONFLICT", + 413: "PAYLOAD_TOO_LARGE", + 422: "UNPROCESSABLE_ENTITY", + 429: "RATE_LIMITED", + 500: "INTERNAL_SERVER_ERROR", +}; + +@Injectable() +@Catch() +export class ApiExceptionFilter implements ExceptionFilter { + catch(exception: unknown, host: ArgumentsHost): void { + const context = host.switchToHttp(); + const request = context.getRequest<Request>() as DemoRequest; + const response = context.getResponse<Response>(); + const externalCode = + typeof exception === "object" && exception !== null && "code" in exception + ? String((exception as { code: unknown }).code) + : undefined; + const isOversizedFile = externalCode === "LIMIT_FILE_SIZE"; + const status = + exception instanceof HttpException + ? exception.getStatus() + : isOversizedFile + ? HttpStatus.PAYLOAD_TOO_LARGE + : HttpStatus.INTERNAL_SERVER_ERROR; + const rawBody = + exception instanceof HttpException ? exception.getResponse() : undefined; + const body: HttpErrorBody = + typeof rawBody === "object" && rawBody !== null + ? (rawBody as HttpErrorBody) + : {}; + const validationMessages = Array.isArray(body.message) ? body.message : []; + const publicMessage = isOversizedFile + ? "Uploaded file exceeds the 5 MiB limit" + : status === 500 && !(exception instanceof HttpException) + ? "Internal server error" + : Array.isArray(body.message) + ? "Request validation failed" + : (body.message ?? + (typeof rawBody === "string" ? rawBody : body.error) ?? + "Request failed"); + + response.status(status).json({ + statusCode: status, + code: isOversizedFile || status === HttpStatus.PAYLOAD_TOO_LARGE + ? "FILE_TOO_LARGE" + : (body.code ?? STATUS_CODES[status] ?? `HTTP_${status}`), + message: publicMessage, + details: + body.details ?? validationMessages.map((message) => ({ message })), + timestamp: new Date().toISOString(), + path: request.originalUrl, + requestId: request.requestId ?? "unknown", + }); + } +} diff --git a/examples/demo-backend/src/common/api.decorators.ts b/examples/demo-backend/src/common/api.decorators.ts new file mode 100644 index 0000000..3df608a --- /dev/null +++ b/examples/demo-backend/src/common/api.decorators.ts @@ -0,0 +1,122 @@ +import { applyDecorators } from "@nestjs/common"; +import { + ApiBadRequestResponse, + ApiConflictResponse, + ApiForbiddenResponse, + ApiHeader, + ApiInternalServerErrorResponse, + ApiNotFoundResponse, + ApiResponse, + ApiTooManyRequestsResponse, + ApiUnauthorizedResponse, +} from "@nestjs/swagger"; +import { ErrorResponseDto } from "./api.dto"; + +export function ApiStandardErrors( + options: { auth?: boolean; notFound?: boolean; conflict?: boolean } = {}, +) { + const decorators: Array< + ClassDecorator | MethodDecorator | PropertyDecorator + > = [ + ApiBadRequestResponse({ + description: "Invalid request or validation error.", + type: ErrorResponseDto, + }), + ApiTooManyRequestsResponse({ + description: "Demo rate limit scenario.", + type: ErrorResponseDto, + }), + ApiInternalServerErrorResponse({ + description: "Unexpected or simulated server error.", + type: ErrorResponseDto, + }), + ]; + + if (options.auth) { + decorators.push( + ApiUnauthorizedResponse({ + description: "Authentication is missing or expired.", + type: ErrorResponseDto, + }), + ApiForbiddenResponse({ + description: "The current user lacks permission.", + type: ErrorResponseDto, + }), + ); + } + + if (options.notFound) { + decorators.push( + ApiNotFoundResponse({ + description: "The requested resource does not exist.", + type: ErrorResponseDto, + }), + ); + } + + if (options.conflict) { + decorators.push( + ApiConflictResponse({ + description: "Business or optimistic-lock conflict.", + type: ErrorResponseDto, + }), + ); + } + + return applyDecorators(...decorators); +} + +export function ApiDemoScenarioHeader() { + return ApiHeader({ + name: "X-Demo-Scenario", + required: false, + enum: [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset", + ], + description: + "Forces a deterministic frontend-testing scenario for this request.", + }); +} + +export function ApiOrganizationHeader() { + return ApiHeader({ + name: "X-Organization-Id", + required: true, + example: "org-acme", + description: "Current tenant. The authenticated user must be a member.", + }); +} + +export function ApiIdempotencyHeader() { + return ApiHeader({ + name: "Idempotency-Key", + required: true, + example: "checkout-6c5f92ad", + description: + "Repeating a request with the same key returns the original order.", + }); +} + +export function ApiBinaryResponse( + description: string, + mediaType = "application/octet-stream", +) { + return ApiResponse({ + status: 200, + description, + content: { + [mediaType]: { + schema: { type: "string", format: "binary" }, + }, + }, + }); +} diff --git a/examples/demo-backend/src/common/api.dto.ts b/examples/demo-backend/src/common/api.dto.ts new file mode 100644 index 0000000..30d343e --- /dev/null +++ b/examples/demo-backend/src/common/api.dto.ts @@ -0,0 +1,135 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; + +export class ErrorDetailDto { + @ApiPropertyOptional({ example: "email" }) + field?: string; + + @ApiProperty({ example: "must be an email" }) + message!: string; + + @ApiPropertyOptional({ example: "isEmail" }) + code?: string; +} + +export class ErrorResponseDto { + @ApiProperty({ example: 404 }) + statusCode!: number; + + @ApiProperty({ example: "PRODUCT_NOT_FOUND" }) + code!: string; + + @ApiProperty({ example: "Product not found" }) + message!: string; + + @ApiProperty({ type: [ErrorDetailDto] }) + details!: ErrorDetailDto[]; + + @ApiProperty({ format: "date-time", example: "2026-07-30T12:00:00.000Z" }) + timestamp!: string; + + @ApiProperty({ example: "/api/v1/products/product-404" }) + path!: string; + + @ApiProperty({ example: "req-5c9f7a3d" }) + requestId!: string; +} + +export class PageMetaDto { + @ApiProperty({ example: 1, minimum: 1 }) + page!: number; + + @ApiProperty({ example: 20, minimum: 1 }) + limit!: number; + + @ApiProperty({ example: 48, minimum: 0 }) + total!: number; + + @ApiProperty({ example: 3, minimum: 0 }) + totalPages!: number; +} + +export class CursorMetaDto { + @ApiProperty({ example: 20, minimum: 1 }) + limit!: number; + + @ApiPropertyOptional({ nullable: true, example: "product-020" }) + nextCursor!: string | null; + + @ApiProperty({ example: true }) + hasMore!: boolean; +} + +export class MutationResultDto { + @ApiProperty({ example: "product-001" }) + id!: string; + + @ApiProperty({ example: true }) + success!: boolean; +} + +export class MutationResponseDto { + @ApiProperty({ type: MutationResultDto }) + data!: MutationResultDto; +} + +export class HealthDataDto { + @ApiProperty({ enum: ["simple", "complex"], example: "simple" }) + application!: "simple" | "complex"; + + @ApiProperty({ enum: ["ok"], example: "ok" }) + status!: "ok"; + + @ApiProperty({ format: "date-time" }) + timestamp!: string; + + @ApiProperty({ example: "1.0.0" }) + version!: string; +} + +export class HealthResponseDto { + @ApiProperty({ type: HealthDataDto }) + data!: HealthDataDto; +} + +export const DEMO_SCENARIOS = [ + "normal", + "slow", + "timeout", + "server-error", + "rate-limited", + "empty", + "expired-auth", + "forbidden", + "conflict", + "large-dataset", +] as const; + +export type DemoScenario = (typeof DEMO_SCENARIOS)[number]; + +export class ScenarioDto { + @ApiProperty({ example: "slow" }) + name!: string; + + @ApiProperty({ + example: "Delays the response to exercise loading and cancellation states.", + }) + description!: string; +} + +export class ScenariosResponseDto { + @ApiProperty({ type: [ScenarioDto] }) + data!: ScenarioDto[]; +} + +export class TestingActionDataDto { + @ApiProperty({ example: true }) + success!: boolean; + + @ApiProperty({ example: "State reset to the default deterministic seed." }) + message!: string; +} + +export class TestingActionResponseDto { + @ApiProperty({ type: TestingActionDataDto }) + data!: TestingActionDataDto; +} diff --git a/examples/demo-backend/src/common/configure-application.ts b/examples/demo-backend/src/common/configure-application.ts new file mode 100644 index 0000000..1d4bedb --- /dev/null +++ b/examples/demo-backend/src/common/configure-application.ts @@ -0,0 +1,57 @@ +import { randomUUID } from "node:crypto"; +import { ValidationPipe } from "@nestjs/common"; +import type { NestExpressApplication } from "@nestjs/platform-express"; +import cookieParser from "cookie-parser"; +import type { NextFunction, Request, Response } from "express"; +import { ApiExceptionFilter } from "./api-exception.filter"; +import { ObservabilityInterceptor } from "./observability.interceptor"; +import type { DemoRequest } from "./request.types"; +import { ScenarioInterceptor } from "./scenario.interceptor"; + +export function configureApplication(app: NestExpressApplication): void { + app.setGlobalPrefix("api/v1"); + app.use(cookieParser()); + app.use((request: Request, response: Response, next: NextFunction) => { + const demoRequest = request as DemoRequest; + demoRequest.requestId = String( + request.headers["x-request-id"] ?? `req-${randomUUID()}`, + ); + response.setHeader("X-Request-Id", demoRequest.requestId); + next(); + }); + app.enableCors({ + origin: true, + credentials: true, + methods: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], + allowedHeaders: [ + "Content-Type", + "Authorization", + "X-CSRF-Token", + "X-Demo-Scenario", + "X-Organization-Id", + "X-Request-Id", + "Idempotency-Key", + "If-None-Match", + ], + exposedHeaders: [ + "ETag", + "Retry-After", + "X-Request-Id", + "X-Response-Time", + "X-Demo-Scenario", + ], + }); + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + whitelist: true, + forbidNonWhitelisted: true, + }), + ); + app.useGlobalFilters(app.get(ApiExceptionFilter)); + app.useGlobalInterceptors( + app.get(ObservabilityInterceptor), + app.get(ScenarioInterceptor), + ); + app.enableShutdownHooks(); +} diff --git a/examples/demo-backend/src/common/infrastructure.module.ts b/examples/demo-backend/src/common/infrastructure.module.ts new file mode 100644 index 0000000..3eefe84 --- /dev/null +++ b/examples/demo-backend/src/common/infrastructure.module.ts @@ -0,0 +1,15 @@ +import { Global, Module } from "@nestjs/common"; +import { ApiExceptionFilter } from "./api-exception.filter"; +import { ObservabilityInterceptor } from "./observability.interceptor"; +import { ScenarioInterceptor } from "./scenario.interceptor"; + +@Global() +@Module({ + providers: [ + ApiExceptionFilter, + ObservabilityInterceptor, + ScenarioInterceptor, + ], + exports: [ApiExceptionFilter, ObservabilityInterceptor, ScenarioInterceptor], +}) +export class InfrastructureModule {} diff --git a/examples/demo-backend/src/common/observability.interceptor.ts b/examples/demo-backend/src/common/observability.interceptor.ts new file mode 100644 index 0000000..7d818c6 --- /dev/null +++ b/examples/demo-backend/src/common/observability.interceptor.ts @@ -0,0 +1,26 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from "@nestjs/common"; +import type { Response } from "express"; +import { Observable, tap } from "rxjs"; + +@Injectable() +export class ObservabilityInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> { + const startedAt = performance.now(); + const response = context.switchToHttp().getResponse<Response>(); + const setTiming = () => { + if (!response.headersSent) { + response.setHeader( + "X-Response-Time", + `${Math.round(performance.now() - startedAt)}ms`, + ); + } + }; + + return next.handle().pipe(tap({ next: setTiming, error: setTiming })); + } +} diff --git a/examples/demo-backend/src/common/openapi.ts b/examples/demo-backend/src/common/openapi.ts new file mode 100644 index 0000000..f9f379a --- /dev/null +++ b/examples/demo-backend/src/common/openapi.ts @@ -0,0 +1,71 @@ +import type { INestApplication } from "@nestjs/common"; +import { + DocumentBuilder, + SwaggerModule, + type OpenAPIObject, +} from "@nestjs/swagger"; + +interface OpenApiOptions { + kind: "simple" | "complex"; + port: number; +} + +export function createOpenApiDocument( + app: INestApplication, + options: OpenApiOptions, +): OpenAPIObject { + const isSimple = options.kind === "simple"; + const builder = new DocumentBuilder() + .setTitle(isSimple ? "Demo Simple API" : "Demo Complex API") + .setDescription( + isSimple + ? "JWT-based API for landing pages and medium frontend applications." + : "Cookie-session, multitenant and realtime API for large frontend applications.", + ) + .setVersion("1.0.0") + .addServer(`http://localhost:${options.port}`, "Local development"); + + if (isSimple) { + builder.addBearerAuth( + { type: "http", scheme: "bearer", bearerFormat: "JWT" }, + "jwt", + ); + } else { + builder + .addCookieAuth( + "demo_session", + { type: "apiKey", in: "cookie" }, + "cookieSession", + ) + .addApiKey( + { + type: "apiKey", + in: "header", + name: "X-CSRF-Token", + description: "Required for authenticated mutations.", + }, + "csrf", + ); + } + + return SwaggerModule.createDocument(app, builder.build(), { + operationIdFactory: (controllerKey, methodKey) => + `${controllerKey.replace(/Controller$/, "")}_${methodKey}`, + }); +} + +export function mountOpenApi( + app: INestApplication, + document: OpenAPIObject, + title: string, +): void { + SwaggerModule.setup("docs", app, document, { + jsonDocumentUrl: "/openapi.json", + customSiteTitle: title, + swaggerOptions: { + persistAuthorization: true, + displayRequestDuration: true, + filter: true, + }, + }); +} diff --git a/examples/demo-backend/src/common/request.types.ts b/examples/demo-backend/src/common/request.types.ts new file mode 100644 index 0000000..7447c34 --- /dev/null +++ b/examples/demo-backend/src/common/request.types.ts @@ -0,0 +1,14 @@ +import type { Request } from "express"; + +export interface AuthenticatedUser { + id: string; + email: string; + name: string; + role: string; +} + +export interface DemoRequest extends Request { + requestId: string; + user?: AuthenticatedUser; + organizationId?: string; +} diff --git a/examples/demo-backend/src/common/scenario.interceptor.ts b/examples/demo-backend/src/common/scenario.interceptor.ts new file mode 100644 index 0000000..4ff9604 --- /dev/null +++ b/examples/demo-backend/src/common/scenario.interceptor.ts @@ -0,0 +1,145 @@ +import { + CallHandler, + ConflictException, + ExecutionContext, + ForbiddenException, + HttpException, + Injectable, + InternalServerErrorException, + NestInterceptor, + UnauthorizedException, +} from "@nestjs/common"; +import type { Request, Response } from "express"; +import { Observable } from "rxjs"; +import { delay, map } from "rxjs/operators"; +import { DEMO_SCENARIOS, type DemoScenario } from "./api.dto"; + +function scenarioFromRequest(request: Request): DemoScenario { + const value = String(request.headers["x-demo-scenario"] ?? "normal"); + return DEMO_SCENARIOS.includes(value as DemoScenario) + ? (value as DemoScenario) + : "normal"; +} + +export function assertNoForcedAuthFailure(request: Request): void { + const scenario = scenarioFromRequest(request); + if (scenario === "expired-auth") { + throw new UnauthorizedException({ + code: "DEMO_AUTH_EXPIRED", + message: "Authentication was expired by the demo scenario.", + }); + } + if (scenario === "forbidden") { + throw new ForbiddenException({ + code: "DEMO_FORBIDDEN", + message: "Access was denied by the demo scenario.", + }); + } +} + +@Injectable() +export class ScenarioInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> { + const http = context.switchToHttp(); + const request = http.getRequest<Request>(); + const response = http.getResponse<Response>(); + const scenario = scenarioFromRequest(request); + response.setHeader("X-Demo-Scenario", scenario); + + if (scenario === "server-error") { + throw new InternalServerErrorException({ + code: "DEMO_SERVER_ERROR", + message: "Server error forced by X-Demo-Scenario.", + }); + } + + if (scenario === "rate-limited") { + response.setHeader("Retry-After", "3"); + throw new HttpException( + { + code: "DEMO_RATE_LIMITED", + message: "Rate limit forced by X-Demo-Scenario.", + }, + 429, + ); + } + + if ( + scenario === "conflict" && + !["GET", "HEAD", "OPTIONS"].includes(request.method) + ) { + throw new ConflictException({ + code: "DEMO_CONFLICT", + message: "Conflict forced by X-Demo-Scenario.", + }); + } + + const configuredDelay = + scenario === "slow" + ? Number(process.env.MOCK_SLOW_DELAY_MS ?? 1500) + : scenario === "timeout" + ? Number(process.env.MOCK_TIMEOUT_DELAY_MS ?? 30000) + : 0; + + return next.handle().pipe( + configuredDelay > 0 ? delay(configuredDelay) : (source) => source, + map((payload) => this.transformPayload(payload, scenario)), + ); + } + + private transformPayload(payload: unknown, scenario: DemoScenario): unknown { + if (!payload || typeof payload !== "object" || !("data" in payload)) { + return payload; + } + + const response = payload as { + data: unknown; + meta?: Record<string, unknown>; + }; + if (!Array.isArray(response.data)) { + return payload; + } + + if (scenario === "empty") { + return { + ...response, + data: [], + meta: response.meta + ? { + ...response.meta, + total: 0, + totalPages: 0, + nextCursor: null, + hasMore: false, + } + : response.meta, + }; + } + + if (scenario === "large-dataset" && response.data.length > 0) { + const source = response.data as Array<Record<string, unknown>>; + const data = Array.from({ length: 250 }, (_, index) => { + const original = source[index % source.length]; + return { + ...original, + id: `${String(original.id ?? "item")}-scenario-${String(index + 1).padStart(3, "0")}`, + }; + }); + return { + ...response, + data, + meta: response.meta + ? { + ...response.meta, + total: data.length, + totalPages: 1, + nextCursor: null, + hasMore: false, + } + : response.meta, + }; + } + + return payload; + } +} diff --git a/examples/demo-backend/src/scripts/generate-openapi.ts b/examples/demo-backend/src/scripts/generate-openapi.ts new file mode 100644 index 0000000..4f2d182 --- /dev/null +++ b/examples/demo-backend/src/scripts/generate-openapi.ts @@ -0,0 +1,34 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { createComplexApplication } from "../apps/complex/bootstrap"; +import { createSimpleApplication } from "../apps/simple/bootstrap"; + +async function generate(): Promise<void> { + const outputDirectory = resolve(process.cwd(), "openapi"); + await mkdir(outputDirectory, { recursive: true }); + + const simple = await createSimpleApplication(false); + await simple.app.init(); + await writeFile( + resolve(outputDirectory, "simple.json"), + `${JSON.stringify(simple.document, null, 2)}\n`, + "utf8", + ); + await simple.app.close(); + + const complex = await createComplexApplication(false); + await complex.app.init(); + await writeFile( + resolve(outputDirectory, "complex.json"), + `${JSON.stringify(complex.document, null, 2)}\n`, + "utf8", + ); + await complex.app.close(); + + console.log("Generated openapi/simple.json and openapi/complex.json"); +} + +void generate().catch((error: unknown) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/examples/demo-backend/src/scripts/validate-openapi.ts b/examples/demo-backend/src/scripts/validate-openapi.ts new file mode 100644 index 0000000..ef5dc37 --- /dev/null +++ b/examples/demo-backend/src/scripts/validate-openapi.ts @@ -0,0 +1,92 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import SwaggerParser from "@apidevtools/swagger-parser"; +import type { OpenAPIObject } from "@nestjs/swagger"; + +interface ContractExpectation { + file: string; + requiredSecuritySchemes: string[]; + requiredPath: string; + forbiddenPath: string; +} + +async function validateContract( + expectation: ContractExpectation, +): Promise<void> { + const filePath = resolve(process.cwd(), "openapi", expectation.file); + await SwaggerParser.validate(filePath); + const document = JSON.parse( + await readFile(filePath, "utf8"), + ) as OpenAPIObject; + const operationIds = new Set<string>(); + let operationCount = 0; + + for (const pathItem of Object.values(document.paths)) { + if (!pathItem) continue; + for (const method of [ + "get", + "post", + "put", + "patch", + "delete", + "options", + "head", + ] as const) { + const operation = pathItem[method] as + { operationId?: string } | undefined; + if (!operation) continue; + operationCount += 1; + if (!operation.operationId) + throw new Error( + `${expectation.file}: ${method.toUpperCase()} operation has no operationId.`, + ); + if (operationIds.has(operation.operationId)) + throw new Error( + `${expectation.file}: duplicate operationId ${operation.operationId}.`, + ); + operationIds.add(operation.operationId); + } + } + + if (!document.paths[expectation.requiredPath]) + throw new Error( + `${expectation.file}: required path ${expectation.requiredPath} is missing.`, + ); + if (document.paths[expectation.forbiddenPath]) + throw new Error( + `${expectation.file}: path ${expectation.forbiddenPath} leaked from the other application.`, + ); + for (const scheme of expectation.requiredSecuritySchemes) { + if (!document.components?.securitySchemes?.[scheme]) + throw new Error( + `${expectation.file}: security scheme ${scheme} is missing.`, + ); + } + if (operationCount < 10) + throw new Error( + `${expectation.file}: suspiciously small contract (${operationCount} operations).`, + ); + console.log( + `Validated ${expectation.file}: ${operationCount} operations, ${operationIds.size} unique operation IDs.`, + ); +} + +async function validate(): Promise<void> { + await validateContract({ + file: "simple.json", + requiredSecuritySchemes: ["jwt"], + requiredPath: "/api/v1/products", + forbiddenPath: "/api/v1/organizations", + }); + await validateContract({ + file: "complex.json", + requiredSecuritySchemes: ["cookieSession", "csrf"], + requiredPath: "/api/v1/organizations", + forbiddenPath: "/api/v1/auth/refresh-token", + }); +} + +void validate().catch((error: unknown) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/examples/demo-backend/test/complex.e2e-spec.ts b/examples/demo-backend/test/complex.e2e-spec.ts new file mode 100644 index 0000000..f6c08f7 --- /dev/null +++ b/examples/demo-backend/test/complex.e2e-spec.ts @@ -0,0 +1,288 @@ +import type { AddressInfo } from "node:net"; +import type { INestApplication } from "@nestjs/common"; +import { io, type Socket } from "socket.io-client"; +import request from "supertest"; +import { createComplexApplication } from "../src/apps/complex/bootstrap"; + +interface AuthenticatedAgent { + agent: ReturnType<typeof request.agent>; + csrfToken: string; + cookieHeader: string; +} + +describe("Complex API", () => { + let app: INestApplication; + let baseUrl: string; + + beforeAll(async () => { + const created = await createComplexApplication(false); + app = created.app; + await app.listen(0, "127.0.0.1"); + const address = app.getHttpServer().address() as AddressInfo; + baseUrl = `http://127.0.0.1:${address.port}`; + }); + + beforeEach(async () => { + await request(app.getHttpServer()) + .post("/api/v1/testing/reset") + .expect(200); + }); + + afterAll(async () => { + await app.close(); + }); + + async function login( + email = "admin@complex.demo", + ): Promise<AuthenticatedAgent> { + const agent = request.agent(app.getHttpServer()); + const response = await agent + .post("/api/v1/auth/login") + .send({ email, password: "demo1234" }) + .expect(200); + const setCookies = response.headers["set-cookie"] as unknown as string[]; + return { + agent, + csrfToken: response.body.data.csrfToken as string, + cookieHeader: setCookies + .map((cookie) => cookie.split(";", 1)[0]) + .join("; "), + }; + } + + it("serves an isolated cookie-auth OpenAPI document", async () => { + const contract = await request(app.getHttpServer()) + .get("/openapi.json") + .expect(200); + expect(contract.body.info.title).toBe("Demo Complex API"); + expect( + contract.body.components.securitySchemes.cookieSession, + ).toBeDefined(); + expect(contract.body.components.securitySchemes.csrf).toBeDefined(); + expect(contract.body.paths["/api/v1/organizations"]).toBeDefined(); + }); + + it("establishes a cookie session and enforces tenant context", async () => { + await request(app.getHttpServer()).get("/api/v1/users/me").expect(401); + const authenticated = await login(); + + const me = await authenticated.agent.get("/api/v1/users/me").expect(200); + expect(me.body.data.email).toBe("admin@complex.demo"); + + const missingTenant = await authenticated.agent + .get("/api/v1/products") + .expect(400); + expect(missingTenant.body.code).toBe("ORGANIZATION_REQUIRED"); + + const products = await authenticated.agent + .get("/api/v1/products") + .set("X-Organization-Id", "org-acme") + .expect(200); + expect(products.body.data[0].organizationId).toBe("org-acme"); + + await authenticated.agent + .get("/api/v1/products") + .set("X-Organization-Id", "org-unknown") + .expect(403); + }); + + it("enforces CSRF and optimistic product versions", async () => { + const authenticated = await login(); + const url = "/api/v1/products/complex-product-keyboard"; + + await authenticated.agent + .patch(url) + .set("X-Organization-Id", "org-acme") + .send({ version: 3, name: "No CSRF" }) + .expect(403); + + const updated = await authenticated.agent + .patch(url) + .set("X-Organization-Id", "org-acme") + .set("X-CSRF-Token", authenticated.csrfToken) + .send({ version: 3, name: "Keyboard Enterprise" }) + .expect(200); + expect(updated.body.data.version).toBe(4); + + const stale = await authenticated.agent + .patch(url) + .set("X-Organization-Id", "org-acme") + .set("X-CSRF-Token", authenticated.csrfToken) + .send({ version: 3, name: "Stale Keyboard" }) + .expect(409); + expect(stale.body.code).toBe("PRODUCT_VERSION_CONFLICT"); + }); + + it("creates orders idempotently", async () => { + const authenticated = await login(); + const body = { + customerId: "customer-ada", + items: [ + { + productId: "complex-product-keyboard", + variantId: "variant-complex-product-keyboard-default", + quantity: 1, + }, + ], + promotionCode: "WELCOME10", + }; + const create = () => + authenticated.agent + .post("/api/v1/orders") + .set("X-Organization-Id", "org-acme") + .set("X-CSRF-Token", authenticated.csrfToken) + .set("Idempotency-Key", "checkout-test-001") + .send(body) + .expect(201); + + const first = await create(); + const second = await create(); + expect(second.body.data.id).toBe(first.body.data.id); + expect(first.body.data.total.amount).toBe("116.91"); + }); + + it("supports role changes without recreating the session", async () => { + const authenticated = await login("viewer@complex.demo"); + await authenticated.agent + .post("/api/v1/products") + .set("X-Organization-Id", "org-acme") + .set("X-CSRF-Token", authenticated.csrfToken) + .send({}) + .expect(403); + + await request(app.getHttpServer()) + .post("/api/v1/testing/users/complex-user-viewer/role") + .send({ role: "manager" }) + .expect(200); + + const allowedToReachValidation = await authenticated.agent + .post("/api/v1/products") + .set("X-Organization-Id", "org-acme") + .set("X-CSRF-Token", authenticated.csrfToken) + .send({}) + .expect(400); + expect(allowedToReachValidation.body.code).toBe("BAD_REQUEST"); + }); + + it("expires an active session on demand", async () => { + const authenticated = await login(); + await authenticated.agent + .post("/api/v1/testing/session/expire") + .set("X-CSRF-Token", authenticated.csrfToken) + .expect(200); + await authenticated.agent.get("/api/v1/users/me").expect(401); + }); + + it("runs a 202 background export and exposes the completed result", async () => { + const authenticated = await login(); + const started = await authenticated.agent + .post("/api/v1/exports/orders") + .set("X-Organization-Id", "org-acme") + .set("X-CSRF-Token", authenticated.csrfToken) + .expect(202); + const jobId = started.body.data.id as string; + + await authenticated.agent + .get(`/api/v1/jobs/${jobId}/result`) + .set("X-Organization-Id", "org-acme") + .expect(409); + + await new Promise((resolve) => setTimeout(resolve, 850)); + const completed = await authenticated.agent + .get(`/api/v1/jobs/${jobId}`) + .set("X-Organization-Id", "org-acme") + .expect(200); + expect(completed.body.data).toMatchObject({ + status: "completed", + progress: 100, + }); + + const result = await authenticated.agent + .get(`/api/v1/jobs/${jobId}/result`) + .set("X-Organization-Id", "org-acme") + .expect(200); + expect(result.headers["content-type"]).toMatch(/text\/csv/); + }); + + it("uploads and downloads a multipart file", async () => { + const authenticated = await login(); + const uploaded = await authenticated.agent + .post("/api/v1/files") + .set("X-Organization-Id", "org-acme") + .set("X-CSRF-Token", authenticated.csrfToken) + .attach("file", Buffer.from("sku,stock\nABC,10\n"), "inventory.csv") + .expect(201); + expect(uploaded.body.data.name).toBe("inventory.csv"); + + const downloaded = await authenticated.agent + .get(`/api/v1/files/${uploaded.body.data.id as string}/download`) + .set("X-Organization-Id", "org-acme") + .expect(200); + expect(downloaded.headers["content-disposition"]).toContain( + "inventory.csv", + ); + + const tooLarge = await authenticated.agent + .post("/api/v1/files") + .set("X-Organization-Id", "org-acme") + .set("X-CSRF-Token", authenticated.csrfToken) + .attach("file", Buffer.alloc(5 * 1024 * 1024 + 1), "too-large.bin") + .expect(413); + expect(tooLarge.body.code).toBe("FILE_TOO_LARGE"); + }); + + it("authenticates Socket.IO with the session cookie and deduplicates messages", async () => { + const authenticated = await login(); + const socket = io(`${baseUrl}/chat`, { + transports: ["websocket"], + reconnection: false, + forceNew: true, + extraHeaders: { Cookie: authenticated.cookieHeader }, + }); + + await waitForSocketEvent(socket, "connect"); + const joined = waitForSocketEvent<{ conversationId: string }>( + socket, + "chat:joined", + ); + socket.emit("chat:join", { + organizationId: "org-acme", + conversationId: "conversation-support", + }); + expect((await joined).conversationId).toBe("conversation-support"); + + const messagePayload = { + organizationId: "org-acme", + conversationId: "conversation-support", + text: "Socket E2E message", + clientMessageId: "socket-e2e-001", + }; + const firstAck = waitForSocketEvent<{ id: string }>(socket, "message:ack"); + socket.emit("message:send", messagePayload); + const first = await firstAck; + + const secondAck = waitForSocketEvent<{ id: string }>(socket, "message:ack"); + socket.emit("message:send", messagePayload); + const second = await secondAck; + expect(second.id).toBe(first.id); + socket.disconnect(); + }, 10_000); +}); + +function waitForSocketEvent<T = void>( + socket: Socket, + event: string, + timeoutMs = 3000, +): Promise<T> { + return new Promise<T>((resolve, reject) => { + const timer = setTimeout(() => { + socket.off(event, handler); + reject(new Error(`Timed out waiting for Socket.IO event ${event}.`)); + }, timeoutMs); + const handler = (payload: T) => { + clearTimeout(timer); + resolve(payload); + }; + socket.once(event, handler); + }); +} diff --git a/examples/demo-backend/test/simple.e2e-spec.ts b/examples/demo-backend/test/simple.e2e-spec.ts new file mode 100644 index 0000000..9c76887 --- /dev/null +++ b/examples/demo-backend/test/simple.e2e-spec.ts @@ -0,0 +1,159 @@ +import type { INestApplication } from "@nestjs/common"; +import request from "supertest"; +import { createSimpleApplication } from "../src/apps/simple/bootstrap"; + +describe("Simple API", () => { + let app: INestApplication; + + beforeAll(async () => { + const created = await createSimpleApplication(false); + app = created.app; + await app.init(); + }); + + beforeEach(async () => { + await request(app.getHttpServer()) + .post("/api/v1/testing/reset") + .expect(200); + }); + + afterAll(async () => { + await app.close(); + }); + + async function login(email = "admin@demo.local") { + return request(app.getHttpServer()) + .post("/api/v1/auth/login") + .send({ email, password: "demo1234" }) + .expect(200); + } + + it("serves health, Swagger JSON and observable headers", async () => { + const health = await request(app.getHttpServer()) + .get("/api/v1/health") + .set("X-Request-Id", "frontend-request-001") + .expect(200); + + expect(health.body.data).toMatchObject({ + application: "simple", + status: "ok", + }); + expect(health.headers["x-request-id"]).toBe("frontend-request-001"); + expect(health.headers["x-response-time"]).toMatch(/^\d+ms$/); + + const contract = await request(app.getHttpServer()) + .get("/openapi.json") + .expect(200); + expect(contract.body.info.title).toBe("Demo Simple API"); + expect(contract.body.components.securitySchemes.jwt).toBeDefined(); + }); + + it("supports empty, large, error and rate-limit scenarios deterministically", async () => { + const empty = await request(app.getHttpServer()) + .get("/api/v1/products") + .set("X-Demo-Scenario", "empty") + .expect(200); + expect(empty.body.data).toEqual([]); + expect(empty.body.meta.total).toBe(0); + + const large = await request(app.getHttpServer()) + .get("/api/v1/categories") + .set("X-Demo-Scenario", "large-dataset") + .expect(200); + expect(large.body.data).toHaveLength(250); + + const limited = await request(app.getHttpServer()) + .get("/api/v1/products") + .set("X-Demo-Scenario", "rate-limited") + .expect(429); + expect(limited.headers["retry-after"]).toBe("3"); + expect(limited.body.code).toBe("DEMO_RATE_LIMITED"); + + const failed = await request(app.getHttpServer()) + .get("/api/v1/products") + .set("X-Demo-Scenario", "server-error") + .expect(500); + expect(failed.body.code).toBe("DEMO_SERVER_ERROR"); + }); + + it("authenticates, rotates refresh tokens and rejects token reuse", async () => { + const authenticated = await login(); + const { accessToken, refreshToken } = authenticated.body.data.tokens; + + const me = await request(app.getHttpServer()) + .get("/api/v1/users/me") + .set("Authorization", `Bearer ${accessToken}`) + .expect(200); + expect(me.body.data.email).toBe("admin@demo.local"); + + await request(app.getHttpServer()) + .get("/api/v1/users/me") + .set("Authorization", `Bearer ${accessToken}`) + .set("X-Demo-Scenario", "expired-auth") + .expect(401); + + const rotated = await request(app.getHttpServer()) + .post("/api/v1/auth/refresh") + .send({ refreshToken }) + .expect(200); + expect(rotated.body.data.tokens.refreshToken).not.toBe(refreshToken); + + const reused = await request(app.getHttpServer()) + .post("/api/v1/auth/refresh") + .send({ refreshToken }) + .expect(401); + expect(reused.body.code).toBe("REFRESH_TOKEN_REUSED"); + }); + + it("returns 304 for a current product ETag", async () => { + const first = await request(app.getHttpServer()) + .get("/api/v1/products/product-keyboard") + .expect(200); + expect(first.headers.etag).toBeDefined(); + + await request(app.getHttpServer()) + .get("/api/v1/products/product-keyboard") + .set("If-None-Match", first.headers.etag) + .expect(304); + }); + + it("detects stale product updates and returns structured conflicts", async () => { + const authenticated = await login(); + const accessToken = authenticated.body.data.tokens.accessToken as string; + const authorization = `Bearer ${accessToken}`; + + const updated = await request(app.getHttpServer()) + .patch("/api/v1/products/product-keyboard") + .set("Authorization", authorization) + .send({ version: 1, name: "Mechanical Keyboard Updated" }) + .expect(200); + expect(updated.body.data.version).toBe(2); + + const conflict = await request(app.getHttpServer()) + .patch("/api/v1/products/product-keyboard") + .set("Authorization", authorization) + .send({ version: 1, name: "Stale Update" }) + .expect(409); + expect(conflict.body.code).toBe("PRODUCT_VERSION_CONFLICT"); + expect(conflict.body.requestId).toMatch(/^req-/); + }); + + it("enforces roles and validates nested orders", async () => { + const customer = await login("customer@demo.local"); + const authorization = `Bearer ${customer.body.data.tokens.accessToken as string}`; + + await request(app.getHttpServer()) + .post("/api/v1/products") + .set("Authorization", authorization) + .send({}) + .expect(403); + + const order = await request(app.getHttpServer()) + .post("/api/v1/orders") + .set("Authorization", authorization) + .send({ items: [{ productId: "product-keyboard", quantity: 1 }] }) + .expect(201); + expect(order.body.data.userId).toBe("user-customer"); + expect(order.body.data.totalCents).toBe(12990); + }); +}); diff --git a/examples/demo-backend/tsconfig.build.json b/examples/demo-backend/tsconfig.build.json new file mode 100644 index 0000000..2f7479c --- /dev/null +++ b/examples/demo-backend/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "test", "**/*.spec.ts"] +} diff --git a/examples/demo-backend/tsconfig.json b/examples/demo-backend/tsconfig.json new file mode 100644 index 0000000..20166ef --- /dev/null +++ b/examples/demo-backend/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "target": "ES2022", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strict": true, + "strictPropertyInitialization": false, + "noImplicitOverride": true, + "useUnknownInCatchVariables": true + }, + "include": ["src/**/*.ts", "test/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/package-lock.json b/package-lock.json index 534c88e..17a8815 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,6 @@ "": { "name": "slm-design", "devDependencies": { - "minisearch": "7.2.0", "vitepress": "1.6.4" }, "engines": { diff --git a/package.json b/package.json index d22434a..7d0d280 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,12 @@ "scripts": { "build": "npm run build:skill", "build:skill": "node scripts/build-skill.mjs", - "check": "npm run build && npm run check:skill && npm run check:docs-all", + "check": "npm run build && npm run check:skill && npm run check:site", "check:skill": "node scripts/check-skill.mjs", - "check:docs": "node scripts/check-docs.mjs", - "check:docs-search": "node scripts/check-docs-search.mjs", - "check:docs-all": "npm run check:docs && npm run docs:build && npm run check:docs-search", + "check:draft-rules": "node draft-rules.js", + "check:docs": "npm run check:draft-rules", + "check:docs-all": "npm run check:site", + "check:site": "npm run check:draft-rules && npm run docs:build && node scripts/check-site.mjs", "docs:dev": "vitepress dev site", "docs:build": "vitepress build site", "docs:preview": "vitepress preview site" @@ -18,7 +19,6 @@ "node": ">=20" }, "devDependencies": { - "minisearch": "7.2.0", "vitepress": "1.6.4" } } diff --git a/scripts/check-docs-search.mjs b/scripts/check-docs-search.mjs deleted file mode 100644 index d462844..0000000 --- a/scripts/check-docs-search.mjs +++ /dev/null @@ -1,88 +0,0 @@ -import { readFile, readdir } from 'node:fs/promises' -import path from 'node:path' -import { fileURLToPath, pathToFileURL } from 'node:url' -import MiniSearch from 'minisearch' -import { collectRules, RULE_SEARCH_OPTIONS } from './lib/specification.mjs' - -const repoRoot = fileURLToPath(new URL('../', import.meta.url)) -const specificationRoot = path.join(repoRoot, 'docs', 'ru', 'specification') -const distRoot = path.join(repoRoot, 'site', '.vitepress', 'dist') -const chunksDirectory = path.join(distRoot, 'assets', 'chunks') -const rules = await collectRules(specificationRoot) -const chunkNames = (await readdir(chunksDirectory)) - .filter((file) => file.startsWith('@localSearchIndexru.') && file.endsWith('.js')) - -if (chunkNames.length !== 1) { - throw new Error(`Expected one Russian search index chunk, found ${chunkNames.length}`) -} - -const searchData = ( - await import(`${pathToFileURL(path.join(chunksDirectory, chunkNames[0])).href}?t=${Date.now()}`) -).default -const searchIndex = MiniSearch.loadJSON(searchData, { - fields: ['title', 'titles', 'text'], - storeFields: ['title', 'titles'], -}) -const catalogHtml = await readFile( - path.join(distRoot, 'ru', 'specification', 'rules.html'), - 'utf8', -) -const htmlCache = new Map() - -function pageHtmlPath(pageHref) { - const route = pageHref.replace(/^\/ru\/specification\/?/, '') - return route.endsWith('/') || route === '' - ? path.join(distRoot, 'ru', 'specification', route, 'index.html') - : path.join(distRoot, 'ru', 'specification', `${route}.html`) -} - -for (const rule of rules) { - const expectedId = `/slm-design${rule.href}` - const firstResult = searchIndex.search(rule.id, RULE_SEARCH_OPTIONS)[0] - - if (firstResult?.id !== expectedId) { - throw new Error( - `Search for ${rule.id} returned ${firstResult?.id || 'nothing'} instead of ${expectedId}`, - ) - } - - if (!firstResult.title.startsWith(rule.id)) { - throw new Error(`Search title for ${rule.id} does not start with the exact rule ID`) - } - - if (firstResult.titles?.[0] !== 'Спецификация') { - throw new Error(`Search breadcrumb for ${rule.id} does not identify Specification`) - } - - const htmlPath = pageHtmlPath(rule.pageHref) - let html = htmlCache.get(htmlPath) - if (!html) { - html = await readFile(htmlPath, 'utf8') - htmlCache.set(htmlPath, html) - } - - if (!html.includes(`id="${rule.anchor}"`)) { - throw new Error(`Missing HTML anchor for ${rule.id} in ${rule.relativePath}`) - } - - if (!html.includes(`class="slm-rule__permalink" href="#${rule.anchor}"`)) { - throw new Error(`Missing permalink for ${rule.id} in ${rule.relativePath}`) - } - - if (!catalogHtml.includes(`>${rule.id}</a>`)) { - throw new Error(`Rule catalog does not contain ${rule.id}`) - } -} - -const representativePage = await readFile( - path.join(distRoot, 'ru', 'specification', 'foundations.html'), - 'utf8', -) - -if (!representativePage.includes('class="doc-set-header"')) { - throw new Error('Specification sidebar does not contain the document-set header') -} - -console.log( - `Documentation search check passed: ${rules.length} exact rule queries, anchors, permalinks, and catalog entries.`, -) diff --git a/scripts/check-docs.mjs b/scripts/check-docs.mjs deleted file mode 100644 index b5ecc69..0000000 --- a/scripts/check-docs.mjs +++ /dev/null @@ -1,118 +0,0 @@ -import { readFile } from 'node:fs/promises' -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import { - collectMarkdownFiles, - collectRules, - RULE_ID_PATTERN_SOURCE, - RULE_LEVEL_PATTERN_SOURCE, -} from './lib/specification.mjs' - -const specificationRoot = fileURLToPath( - new URL('../docs/ru/specification/', import.meta.url), -) - -const declarationPattern = new RegExp( - `^\\*\\*(${RULE_ID_PATTERN_SOURCE}) - (${RULE_LEVEL_PATTERN_SOURCE})\\.\\*\\*`, - 'gm', -) -const declarationLinePattern = new RegExp( - `^\\*\\*(${RULE_ID_PATTERN_SOURCE}) - (${RULE_LEVEL_PATTERN_SOURCE})\\.\\*\\*`, -) -const referencePattern = new RegExp(`\\b${RULE_ID_PATTERN_SOURCE}\\b`, 'g') -const legacyBaseReferencePattern = /\bSLM-(?!BASE-|ADV-|PRO-)[A-Z][A-Z0-9]*-\d{3}\b/g - -function lineNumberAt(content, index) { - return content.slice(0, index).split('\n').length -} - -const files = await collectMarkdownFiles(specificationRoot) -const registry = await collectRules(specificationRoot) -const declarations = new Map() -const references = [] -const errors = [] -const ruleCounts = { BASE: 0, ADV: 0, PRO: 0 } - -for (const file of files) { - const content = await readFile(file, 'utf8') - const relativePath = path.relative(specificationRoot, file).split(path.sep).join('/') - const lines = content.split('\n') - - for (const [index, line] of lines.entries()) { - if (line.startsWith('**SLM-') && !declarationLinePattern.test(line)) { - errors.push(`${relativePath}:${index + 1}: malformed rule declaration`) - } - } - - for (const match of content.matchAll(declarationPattern)) { - const id = match[1] - const location = `${relativePath}:${lineNumberAt(content, match.index)}` - const existingLocation = declarations.get(id) - - if (existingLocation) { - errors.push(`${location}: duplicate ${id}; first declared at ${existingLocation}`) - } else { - declarations.set(id, location) - ruleCounts[id.split('-')[1]] += 1 - } - - if (id.startsWith('SLM-BASE-') && relativePath.startsWith('modes/')) { - errors.push(`${location}: base rule ${id} cannot be declared in an overlay`) - } else if (id.startsWith('SLM-ADV-') && !relativePath.startsWith('modes/advanced/')) { - errors.push(`${location}: ${id} must be declared under modes/advanced`) - } else if (id.startsWith('SLM-PRO-') && !relativePath.startsWith('modes/pro/')) { - errors.push(`${location}: ${id} must be declared under modes/pro`) - } - } - - for (const match of content.matchAll(referencePattern)) { - references.push({ - id: match[0], - location: `${relativePath}:${lineNumberAt(content, match.index)}`, - }) - } - - for (const match of content.matchAll(legacyBaseReferencePattern)) { - errors.push( - `${relativePath}:${lineNumberAt(content, match.index)}: legacy base rule ID ${match[0]}`, - ) - } - - if (relativePath.startsWith('modes/advanced/') && /\bSLM-PRO-[A-Z-]+-\d{3}\b/.test(content)) { - errors.push(`${relativePath}: Advanced overlay references a Pro rule`) - } - - if (relativePath.startsWith('modes/pro/') && /\bSLM-ADV-[A-Z-]+-\d{3}\b/.test(content)) { - errors.push(`${relativePath}: Pro overlay references an Advanced rule`) - } -} - -for (const reference of references) { - if (!declarations.has(reference.id)) { - errors.push(`${reference.location}: unknown rule reference ${reference.id}`) - } -} - -if (registry.length !== declarations.size) { - errors.push( - `rule registry contains ${registry.length} records, but validator found ${declarations.size} declarations`, - ) -} - -for (const rule of registry) { - if (!declarations.has(rule.id)) { - errors.push(`${rule.relativePath}:${rule.line}: registry contains undeclared rule ${rule.id}`) - } -} - -if (errors.length > 0) { - console.error(`Documentation check failed with ${errors.length} error(s):`) - for (const error of errors) console.error(`- ${error}`) - process.exitCode = 1 -} else { - console.log( - `Documentation check passed: ${files.length} files, ${declarations.size} rules ` - + `(${ruleCounts.BASE} base, ${ruleCounts.ADV} advanced, ${ruleCounts.PRO} pro), ` - + `${references.length} rule occurrences.`, - ) -} diff --git a/scripts/check-site.mjs b/scripts/check-site.mjs new file mode 100644 index 0000000..59a074a --- /dev/null +++ b/scripts/check-site.mjs @@ -0,0 +1,149 @@ +import { readFile, readdir } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const repositoryRoot = fileURLToPath(new URL('../', import.meta.url)) +const distRoot = path.join(repositoryRoot, 'site', '.vitepress', 'dist') +const rulesSource = path.join(repositoryRoot, 'DRAFT', 'rules', 'level-1.md') +const siteOrigin = 'https://site.test' +const siteBase = '/slm-design/' + +const expectedPages = [ + '404.html', + 'index.html', + 'level-1/index.html', + 'level-1/terminology.html', + 'level-1/layers.html', + 'level-1/dependencies.html', + 'level-1/modules.html', + 'level-1/groups.html', + 'level-1/segments.html', + 'level-1/components.html', + 'level-1/nested-modules.html', + 'level-1/lifecycle.html', + 'level-1/validation.html', + 'rules/index.html', + 'rules/level-1.html', +].sort() + +async function collectHtmlFiles(directory, prefix = '') { + const entries = await readdir(directory, { withFileTypes: true }) + const files = [] + + for (const entry of entries) { + const relativePath = path.posix.join(prefix, entry.name) + const absolutePath = path.join(directory, entry.name) + + if (entry.isDirectory()) { + files.push(...await collectHtmlFiles(absolutePath, relativePath)) + } else if (entry.isFile() && entry.name.endsWith('.html')) { + files.push(relativePath) + } + } + + return files +} + +function pageUrl(relativePath) { + if (relativePath === 'index.html') return `${siteOrigin}${siteBase}` + if (relativePath.endsWith('/index.html')) { + return `${siteOrigin}${siteBase}${relativePath.slice(0, -'index.html'.length)}` + } + return `${siteOrigin}${siteBase}${relativePath.slice(0, -'.html'.length)}` +} + +function htmlPathForUrl(url) { + const route = decodeURIComponent(url.pathname.slice(siteBase.length)) + if (!route) return 'index.html' + if (route.endsWith('/')) return `${route}index.html` + return `${route}.html` +} + +const actualPages = (await collectHtmlFiles(distRoot)).sort() +if (JSON.stringify(actualPages) !== JSON.stringify(expectedPages)) { + throw new Error( + `Published page set differs from allowlist.\nExpected: ${expectedPages.join(', ')}\nActual: ${actualPages.join(', ')}`, + ) +} + +const htmlByPage = new Map() +for (const relativePath of actualPages) { + htmlByPage.set(relativePath, await readFile(path.join(distRoot, relativePath), 'utf8')) +} + +for (const [relativePath, html] of htmlByPage) { + const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map((match) => match[1]) + const duplicateIds = ids.filter((id, index) => ids.indexOf(id) !== index) + if (duplicateIds.length > 0) { + throw new Error(`${relativePath} contains duplicate ids: ${[...new Set(duplicateIds)].join(', ')}`) + } + + for (const match of html.matchAll(/<a\b[^>]*\bhref="([^"]+)"/g)) { + const target = new URL(match[1], pageUrl(relativePath)) + if (target.origin !== siteOrigin) continue + if (!target.pathname.startsWith(siteBase)) { + throw new Error(`${relativePath} links outside the configured base: ${match[1]}`) + } + + const targetPath = htmlPathForUrl(target) + const targetHtml = htmlByPage.get(targetPath) + if (!targetHtml) { + throw new Error(`${relativePath} contains broken link ${match[1]}`) + } + + if (target.hash) { + const anchor = decodeURIComponent(target.hash.slice(1)) + if (!targetHtml.includes(`id="${anchor}"`)) { + throw new Error(`${relativePath} links to missing anchor ${match[1]}`) + } + } + } +} + +const rulesMarkdown = await readFile(rulesSource, 'utf8') +const ruleIds = [...rulesMarkdown.matchAll(/^### (SLM-L1-[A-Z_]+-[AR]\d{3})$/gm)] + .map((match) => match[1]) +const rulesHtml = htmlByPage.get('rules/level-1.html') +const searchChunksDirectory = path.join(distRoot, 'assets', 'chunks') +const searchChunks = (await readdir(searchChunksDirectory)) + .filter((file) => file.startsWith('@localSearchIndex') && file.endsWith('.js')) + +if (searchChunks.length !== 1) { + throw new Error(`Expected one local search index, found ${searchChunks.length}`) +} + +const searchModuleUrl = `${pathToFileURL(path.join(searchChunksDirectory, searchChunks[0])).href}?t=${Date.now()}` +const searchData = JSON.parse((await import(searchModuleUrl)).default) +const searchUrls = new Set(Object.values(searchData.documentIds)) + +for (const ruleId of ruleIds) { + const anchor = ruleId.toLowerCase() + const expectedUrl = `${siteBase}rules/level-1#${anchor}` + + if (!rulesHtml.includes(`id="${anchor}"`)) { + throw new Error(`Published registry does not contain anchor ${anchor}`) + } + + if (!searchUrls.has(expectedUrl)) { + throw new Error(`Local search index does not contain canonical record ${expectedUrl}`) + } +} + +const levelOneHtml = htmlByPage.get('level-1/index.html') +if (!levelOneHtml.includes('class="doc-set-header"')) { + throw new Error('Level 1 pages do not render the document header') +} + +const notFoundHtml = htmlByPage.get('404.html') +if (!notFoundHtml.includes('Страница не найдена')) { + throw new Error('404 page is not localized') +} + +const sitemap = await readFile(path.join(distRoot, 'sitemap.xml'), 'utf8') +for (const forbiddenRoute of ['/ru/', '/domains/', '/specification/']) { + if (sitemap.includes(forbiddenRoute)) { + throw new Error(`Sitemap contains archival or excluded route ${forbiddenRoute}`) + } +} + +console.log(`Site check passed: ${actualPages.length - 1} pages and ${ruleIds.length} searchable rules.`) diff --git a/scripts/lib/specification.mjs b/scripts/lib/specification.mjs deleted file mode 100644 index 74ff51a..0000000 --- a/scripts/lib/specification.mjs +++ /dev/null @@ -1,129 +0,0 @@ -import { readdir, readFile } from 'node:fs/promises' -import path from 'node:path' - -export const RULE_ID_PATTERN_SOURCE = 'SLM-(?:BASE|ADV|PRO)-[A-Z][A-Z0-9]*-\\d{3}' -export const RULE_LEVEL_PATTERN_SOURCE = 'ОБЯЗАН|ЗАПРЕЩЕНО|СЛЕДУЕТ|МОЖЕТ' -export const RULE_SEARCH_OPTIONS = { - fuzzy: false, - prefix: true, - combineWith: 'AND', - boost: { title: 50, text: 2, titles: 1 }, -} - -const ruleIdPattern = /^SLM-(BASE|ADV|PRO)-([A-Z][A-Z0-9]*)-(\d{3})$/ -const declarationPattern = new RegExp( - `^\\*\\*(${RULE_ID_PATTERN_SOURCE}) - (${RULE_LEVEL_PATTERN_SOURCE})\\.\\*\\*\\s+(.+?)\\s*$`, -) -const rulesetOrder = { BASE: 0, ADV: 1, PRO: 2 } - -export async function collectMarkdownFiles(directory) { - const entries = await readdir(directory, { withFileTypes: true }) - const files = [] - - for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) { - const entryPath = path.join(directory, entry.name) - - if (entry.isDirectory()) { - files.push(...await collectMarkdownFiles(entryPath)) - } else if (entry.isFile() && entry.name.endsWith('.md')) { - files.push(entryPath) - } - } - - return files -} - -export function parseRuleId(id) { - const match = id.match(ruleIdPattern) - if (!match) return null - - return { - ruleset: match[1], - area: match[2], - number: Number(match[3]), - } -} - -export function parseRuleDeclaration(line) { - const match = line.match(declarationPattern) - if (!match) return null - - const parsedId = parseRuleId(match[1]) - if (!parsedId) return null - - return { - id: match[1], - ...parsedId, - level: match[2], - markdown: match[3], - text: stripInlineMarkdown(match[3]), - } -} - -export function stripInlineMarkdown(value) { - return value - .replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1') - .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1') - .replace(/`([^`]+)`/g, '$1') - .replace(/<[^>]+>/g, '') - .replace(/[*_~]/g, '') - .replace(/\\([\\`*{}\[\]()#+.!_-])/g, '$1') - .replace(/\s+/g, ' ') - .trim() -} - -export function specificationPathToHref(relativePath, basePath = '/ru/specification/') { - let route = relativePath.split(path.sep).join('/') - - if (route === 'index.md') route = '' - else route = route.replace(/(?:^|\/)index\.md$/, '/').replace(/\.md$/, '') - - return `${basePath}${route}` -} - -export async function collectRules(specificationRoot, basePath = '/ru/specification/') { - const files = await collectMarkdownFiles(specificationRoot) - const rules = [] - - for (const file of files) { - const content = await readFile(file, 'utf8') - const relativePath = path.relative(specificationRoot, file).split(path.sep).join('/') - const headings = [] - - for (const [lineIndex, line] of content.split('\n').entries()) { - const heading = line.match(/^(#{1,6})\s+(.+?)\s*$/) - if (heading) { - const level = heading[1].length - headings.length = level - headings[level - 1] = stripInlineMarkdown(heading[2]) - continue - } - - const rule = parseRuleDeclaration(line) - if (!rule) continue - - const pageTitle = headings[0] || relativePath - const sectionTitles = headings.slice(1).filter(Boolean) - const pageHref = specificationPathToHref(relativePath, basePath) - - rules.push({ - ...rule, - anchor: rule.id.toLowerCase(), - href: `${pageHref}#${rule.id.toLowerCase()}`, - line: lineIndex + 1, - pageHref, - pageTitle, - relativePath, - sectionTitle: sectionTitles.at(-1) || pageTitle, - sectionTitles, - }) - } - } - - return rules.sort((left, right) => ( - rulesetOrder[left.ruleset] - rulesetOrder[right.ruleset] - || left.area.localeCompare(right.area) - || left.number - right.number - || left.id.localeCompare(right.id) - )) -} diff --git a/site/.vitepress/config.mts b/site/.vitepress/config.mts index 9acd2de..94061a3 100644 --- a/site/.vitepress/config.mts +++ b/site/.vitepress/config.mts @@ -1,165 +1,91 @@ -import { defineConfig } from 'vitepress' -import type MarkdownIt from 'markdown-it' import { fileURLToPath } from 'node:url' -import { splitSearchSections } from './search.mts' -import { RULE_SEARCH_OPTIONS } from '../../scripts/lib/specification.mjs' +import { defineConfig } from 'vitepress' const repositoryUrl = 'https://github.com/gromlab-ru/slm-design' const viteConfigPath = fileURLToPath(new URL('../vite.config.mts', import.meta.url)) -const specificationSidebar = [ +function slugifyHeading(value: string) { + const slug = value + .replace(/<[^>]+>/g, '') + .replace(/`/g, '') + .trim() + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^\p{L}\p{N}_-]/gu, '') + .replace(/-+/g, '-') + + return ['app', 'compositions', 'infra', 'ui', 'shared'].includes(slug) + ? `layer-${slug}` + : slug +} + +const levelOneSidebar = [ { - text: 'Начало', + text: 'SLM Level 1', items: [ - { text: 'Обзор спецификации', link: '/ru/specification/' }, - { text: 'Architecture modes', link: '/ru/specification/architecture-modes' }, - { text: 'Реестр правил', link: '/ru/specification/rules' }, + { text: 'Обзор', link: '/level-1/' }, + { text: 'Терминология', link: '/level-1/terminology' }, + { text: 'Слои', link: '/level-1/layers' }, + { text: 'Зависимости', link: '/level-1/dependencies' }, + { text: 'Модули', link: '/level-1/modules' }, + { text: 'Группы', link: '/level-1/groups' }, + { text: 'Сегменты', link: '/level-1/segments' }, + { text: 'Компоненты', link: '/level-1/components' }, + { text: 'Вложенные модули', link: '/level-1/nested-modules' }, + { text: 'Жизненный цикл', link: '/level-1/lifecycle' }, + { text: 'Проверка', link: '/level-1/validation' }, ], }, { - text: 'Base SLM', - collapsed: false, + text: 'Правила', items: [ - { - text: 'Основы', - items: [ - { text: 'Основные инварианты', link: '/ru/specification/foundations' }, - { text: 'Терминология', link: '/ru/specification/terminology' }, - { text: 'Архитектурная модель', link: '/ru/specification/architecture-model' }, - ], - }, - { - text: 'Слои', - collapsed: true, - items: [ - { text: 'Обзор', link: '/ru/specification/layers/' }, - { text: 'App', link: '/ru/specification/layers/app' }, - { text: 'Compositions', link: '/ru/specification/layers/compositions' }, - { text: 'Infra', link: '/ru/specification/layers/infra' }, - { text: 'UI', link: '/ru/specification/layers/ui' }, - { text: 'Shared', link: '/ru/specification/layers/shared' }, - ], - }, - { - text: 'Общие правила', - collapsed: true, - items: [ - { text: 'Модули и группы', link: '/ru/specification/modules-and-groups' }, - { text: 'Сегменты', link: '/ru/specification/segments' }, - { text: 'Public API и импорты', link: '/ru/specification/public-api-and-imports' }, - { text: 'State и data', link: '/ru/specification/state-and-data' }, - { text: 'Runtime и lifecycle', link: '/ru/specification/runtime-and-lifecycle' }, - { text: 'Тестирование', link: '/ru/specification/testing-and-conformance' }, - { text: 'Монорепозитории', link: '/ru/specification/monorepo' }, - ], - }, - ], - }, - { - text: 'Overlays', - collapsed: false, - items: [ - { - text: 'SLM Advanced', - collapsed: true, - items: [ - { text: 'Advanced overlay', link: '/ru/specification/modes/advanced/' }, - { text: 'Domains', link: '/ru/specification/modes/advanced/domains' }, - ], - }, - { - text: 'SLM Pro', - collapsed: true, - items: [ - { text: 'Pro overlay', link: '/ru/specification/modes/pro/' }, - { text: 'Domains', link: '/ru/specification/modes/pro/domains/' }, - { text: 'Business', link: '/ru/specification/modes/pro/domains/business' }, - { text: 'Framework surface', link: '/ru/specification/modes/pro/domains/framework' }, - { text: 'Ports и adapters', link: '/ru/specification/modes/pro/domains/ports-and-adapters' }, - { text: 'Client и server', link: '/ru/specification/modes/pro/domains/client-and-server' }, - { text: 'Cross-domain boundary', link: '/ru/specification/modes/pro/domains/cross-domain-boundary' }, - { text: 'Тестирование domains', link: '/ru/specification/modes/pro/domains/testing' }, - ], - }, + { text: 'Как устроены правила', link: '/rules/' }, + { text: 'Реестр Level 1', link: '/rules/level-1' }, ], }, ] -function addRuleAnchors(md: MarkdownIt) { - const rulePattern = /^\*\*(SLM-(?:BASE|ADV|PRO)-[A-Z][A-Z-]*-\d{3}) - (ОБЯЗАН|ЗАПРЕЩЕНО|СЛЕДУЕТ|МОЖЕТ)\.\*\*/ - const kindByKeyword: Record<string, string> = { - ОБЯЗАН: 'required', - ЗАПРЕЩЕНО: 'prohibited', - СЛЕДУЕТ: 'recommended', - МОЖЕТ: 'optional', - } - - md.core.ruler.after('inline', 'slm-rule-anchors', (state) => { - for (let index = 0; index < state.tokens.length - 1; index += 1) { - const paragraph = state.tokens[index] - const content = state.tokens[index + 1] - - if (paragraph.type !== 'paragraph_open' || content.type !== 'inline') continue - - const match = content.content.match(rulePattern) - if (!match) continue - - paragraph.attrSet('id', match[1].toLowerCase()) - paragraph.attrJoin('class', 'slm-rule') - paragraph.attrJoin('class', `slm-rule--${kindByKeyword[match[2]]}`) - - const strongCloseIndex = content.children?.findIndex((token) => token.type === 'strong_close') ?? -1 - if (strongCloseIndex < 0 || !content.children) continue - - const permalinkOpen = new state.Token('link_open', 'a', 1) - permalinkOpen.attrSet('class', 'slm-rule__permalink') - permalinkOpen.attrSet('href', `#${match[1].toLowerCase()}`) - permalinkOpen.attrSet('aria-label', `Ссылка на правило ${match[1]}`) - permalinkOpen.attrSet('title', `Ссылка на ${match[1]}`) - - const permalinkText = new state.Token('text', '', 0) - permalinkText.content = '#' - - const permalinkClose = new state.Token('link_close', 'a', -1) - content.children.splice( - strongCloseIndex + 1, - 0, - permalinkOpen, - permalinkText, - permalinkClose, - ) - } - }) -} +const rulesSidebar = [ + { + text: 'Правила SLM', + items: [ + { text: 'Формат и классификация', link: '/rules/' }, + { text: 'Правила Level 1', link: '/rules/level-1' }, + ], + }, + { + text: 'Документация', + items: [ + { text: 'Обзор Level 1', link: '/level-1/' }, + { text: 'Проверка', link: '/level-1/validation' }, + ], + }, +] export default defineConfig({ - srcDir: '../docs', + srcDir: '../DRAFT', + srcExclude: ['README.md', 'domains/**'], + rewrites: { + 'level-1/README.md': 'level-1/index.md', + 'rules/README.md': 'rules/index.md', + }, title: 'SLM Design', - description: 'Specification for explicit architecture boundaries in product applications', - lang: 'en-US', + description: 'Базовая архитектура фронтенд-приложений SLM Level 1', + lang: 'ru-RU', base: '/slm-design/', cleanUrls: true, lastUpdated: true, sitemap: { hostname: 'https://gromlab-ru.github.io/slm-design/', - transformItems: (items) => items.map((item) => { - if (!item.links?.some((link) => link.url === 'ru/')) return item - - return { - ...item, - links: [ - { lang: 'x-default', url: '' }, - ...item.links.filter((link) => link.url !== ''), - ], - } - }), }, head: [ ['link', { rel: 'icon', type: 'image/svg+xml', href: '/slm-design/logo.svg' }], ['meta', { name: 'theme-color', content: '#d97706' }], ], markdown: { - config: addRuleAnchors, + anchor: { + slugify: slugifyHeading, + }, }, vite: { configFile: viteConfigPath, @@ -167,103 +93,68 @@ export default defineConfig({ themeConfig: { logo: '/logo.svg', siteTitle: 'SLM Design', - i18nRouting: false, nav: [ - { text: 'Русская спецификация', link: '/ru/' }, - { text: 'English', link: '/en/' }, + { text: 'Level 1', link: '/level-1/' }, + { text: 'Правила', link: '/rules/level-1' }, ], + sidebar: { + '/level-1/': levelOneSidebar, + '/rules/': levelOneSidebar, + }, socialLinks: [{ icon: 'github', link: repositoryUrl }], search: { provider: 'local', options: { detailedView: false, disableQueryPersistence: true, - miniSearch: { - searchOptions: RULE_SEARCH_OPTIONS, - _splitIntoSections: splitSearchSections, - }, - locales: { - ru: { - translations: { - button: { - buttonText: 'Поиск или rule ID', - buttonAriaLabel: 'Поиск по документации или rule ID', - }, - modal: { - displayDetails: 'Показать подробности', - resetButtonTitle: 'Сбросить поиск', - backButtonTitle: 'Закрыть поиск', - noResultsText: 'Ничего не найдено по запросу', - footer: { - selectText: 'выбрать', - navigateText: 'перейти', - closeText: 'закрыть', - }, - }, + translations: { + button: { + buttonText: 'Поиск по документации', + buttonAriaLabel: 'Поиск по документации или коду правила', + }, + modal: { + displayDetails: 'Показать подробности', + resetButtonTitle: 'Сбросить поиск', + backButtonTitle: 'Закрыть поиск', + noResultsText: 'Ничего не найдено', + footer: { + selectText: 'выбрать', + navigateText: 'перейти', + closeText: 'закрыть', }, }, }, }, }, - }, - locales: { - ru: { - label: 'Русский', - lang: 'ru-RU', - link: '/ru/', - title: 'SLM Design', - description: 'Спецификация архитектурных границ продуктовых приложений', - themeConfig: { - nav: [ - { text: 'Документация', link: '/ru/' }, - { text: 'Спецификация', link: '/ru/specification/' }, - ], - sidebar: { - '/ru/specification/': specificationSidebar, - }, - outline: { level: [2, 3], label: 'На этой странице' }, - editLink: { - pattern: `${repositoryUrl}/edit/master/docs/:path`, - text: 'Предложить изменение', - }, - lastUpdated: { - text: 'Обновлено', - formatOptions: { dateStyle: 'medium' }, - }, - docFooter: { - prev: 'Предыдущая страница', - next: 'Следующая страница', - }, - darkModeSwitchLabel: 'Оформление', - lightModeSwitchTitle: 'Светлая тема', - darkModeSwitchTitle: 'Тёмная тема', - sidebarMenuLabel: 'Содержание', - returnToTopLabel: 'Наверх', - langMenuLabel: 'Изменить язык', - skipToContentLabel: 'Перейти к содержанию', - footer: { - message: 'SLM Design 2.0 Draft', - copyright: 'Нормативная русская версия находится в статусе draft.', - }, - }, + outline: { level: [2, 3], label: 'На этой странице' }, + notFound: { + code: '404', + title: 'Страница не найдена', + quote: 'Запрошенная страница отсутствует в документации Level 1.', + linkLabel: 'Перейти на главную', + linkText: 'Вернуться к документации', }, - en: { - label: 'English', - lang: 'en-US', - link: '/en/', - title: 'SLM Design', - description: 'English translation placeholder for the SLM Design specification', - themeConfig: { - nav: [ - { text: 'English status', link: '/en/' }, - { text: 'Russian specification', link: '/ru/specification/' }, - ], - outline: { level: [2, 3], label: 'On this page' }, - footer: { - message: 'SLM Design 2.0 Draft', - copyright: 'The English edition is not normative yet.', - }, - }, + editLink: { + pattern: `${repositoryUrl}/edit/master/DRAFT/:path`, + text: 'Предложить изменение', + }, + lastUpdated: { + text: 'Обновлено', + formatOptions: { dateStyle: 'medium' }, + }, + docFooter: { + prev: 'Предыдущая страница', + next: 'Следующая страница', + }, + darkModeSwitchLabel: 'Оформление', + lightModeSwitchTitle: 'Светлая тема', + darkModeSwitchTitle: 'Тёмная тема', + sidebarMenuLabel: 'Содержание', + returnToTopLabel: 'Наверх', + skipToContentLabel: 'Перейти к содержанию', + footer: { + message: 'SLM Level 1', + copyright: 'Рабочий черновик архитектуры.', }, }, }) diff --git a/site/.vitepress/rules.data.mts b/site/.vitepress/rules.data.mts deleted file mode 100644 index c0b7877..0000000 --- a/site/.vitepress/rules.data.mts +++ /dev/null @@ -1,14 +0,0 @@ -import { fileURLToPath } from 'node:url' -import { defineLoader } from 'vitepress' -import { collectRules } from '../../scripts/lib/specification.mjs' - -const specificationRoot = fileURLToPath( - new URL('../../docs/ru/specification/', import.meta.url), -) - -export default defineLoader({ - watch: '../../docs/ru/specification/**/*.md', - async load() { - return collectRules(specificationRoot) - }, -}) diff --git a/site/.vitepress/search.mts b/site/.vitepress/search.mts deleted file mode 100644 index e11d099..0000000 --- a/site/.vitepress/search.mts +++ /dev/null @@ -1,61 +0,0 @@ -const headingPattern = /<h(\d).*?>(.*?<a.*? href="#.*?".*?>.*?<\/a>)<\/h\1>/gi -const headingContentPattern = /(.*?)<a.*? href="#(.*?)".*?>.*?<\/a>/i -const ruleBlockPattern = /<p id="(slm-(?:base|adv|pro)-[a-z][a-z0-9]*-\d{3})" class="[^"]*\bslm-rule\b[^"]*"[^>]*>([\s\S]*?)<\/p>/gi -const rulePermalinkPattern = /<a\b[^>]*class="[^"]*\bslm-rule__permalink\b[^"]*"[^>]*>[\s\S]*?<\/a>/i - -function clearHtml(value: string) { - return value.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim() -} - -function makeRulesSearchable(html: string) { - return html.replace(ruleBlockPattern, (_block, anchor: string, innerHtml: string) => { - const strong = innerHtml.match(/<strong>([\s\S]*?)<\/strong>/i) - const label = clearHtml(strong?.[1] || anchor.toUpperCase()) - const bodyHtml = innerHtml - .replace(/<strong>[\s\S]*?<\/strong>/i, '') - .replace(rulePermalinkPattern, '') - .trim() - return `<h6>${label}<a href="#${anchor}"></a></h6><p>${bodyHtml}</p>` - }) -} - -function* splitByHeadings(html: string) { - const parts = html.split(headingPattern) - parts.shift() - let parentTitles: string[] = [] - - for (let index = 0; index < parts.length; index += 3) { - const level = Number.parseInt(parts[index], 10) - 1 - const heading = headingContentPattern.exec(parts[index + 1]) - const title = clearHtml(heading?.[1] || '') - const anchor = heading?.[2] || '' - const text = clearHtml(parts[index + 2] || '') - - if (!title || !text) continue - - let titles = parentTitles.slice(0, level) - titles[level] = title - titles = titles.filter(Boolean) - - yield { anchor, titles, text } - - if (level === 0) parentTitles = [title] - else parentTitles[level] = title - } -} - -export function* splitSearchSections(file: string, html: string) { - const normalizedFile = file.replaceAll('\\', '/') - const documentTitle = normalizedFile.includes('/ru/specification/') - ? 'Спецификация' - : normalizedFile.includes('/ru/guide/') - ? 'Архитектурный гайд' - : null - - for (const section of splitByHeadings(makeRulesSearchable(html))) { - yield { - ...section, - titles: documentTitle ? [documentTitle, ...section.titles] : section.titles, - } - } -} diff --git a/site/.vitepress/theme/DocSetHeader.vue b/site/.vitepress/theme/DocSetHeader.vue index 659edb4..b62d48c 100644 --- a/site/.vitepress/theme/DocSetHeader.vue +++ b/site/.vitepress/theme/DocSetHeader.vue @@ -5,21 +5,21 @@ import { useData, withBase } from 'vitepress' const { page } = useData() const documentSet = computed(() => { - if (page.value.relativePath.startsWith('ru/specification/')) { + if (page.value.relativePath.startsWith('level-1/')) { return { - eyebrow: 'Нормативный документ', - href: '/ru/specification/', - meta: 'DRAFT · v0.1.0', - title: 'SLM Design Specification', + eyebrow: 'Архитектурная документация', + href: '/level-1/', + meta: 'LEVEL 1 · DRAFT', + title: 'SLM Level 1', } } - if (page.value.relativePath.startsWith('ru/guide/')) { + if (page.value.relativePath.startsWith('rules/')) { return { - eyebrow: 'Учебный материал', - href: '/ru/guide/', - meta: 'GUIDE', - title: 'SLM Architecture Guide', + eyebrow: 'Канонический реестр', + href: '/rules/level-1', + meta: '14 RULES · DRAFT', + title: 'Правила SLM', } } @@ -27,15 +27,3 @@ const documentSet = computed(() => { }) </script> -<template> - <div v-if="documentSet" class="doc-set-header"> - <a class="doc-set-header__back" :href="withBase('/ru/')">Документация</a> - <a class="doc-set-header__title" :href="withBase(documentSet.href)"> - {{ documentSet.title }} - </a> - <div class="doc-set-header__meta"> - <span>{{ documentSet.eyebrow }}</span> - <span>{{ documentSet.meta }}</span> - </div> - </div> -</template> diff --git a/site/.vitepress/theme/Layout.vue b/site/.vitepress/theme/Layout.vue index 8aa0af5..f4b46a2 100644 --- a/site/.vitepress/theme/Layout.vue +++ b/site/.vitepress/theme/Layout.vue @@ -1,29 +1,12 @@ <script setup lang="ts"> -import { computed } from 'vue' -import { useData } from 'vitepress' import DefaultTheme from 'vitepress/theme' import DocSetHeader from './DocSetHeader.vue' const { Layout } = DefaultTheme -const { lang } = useData() - -const bannerText = computed(() => - lang.value.startsWith('ru') - ? 'SLM 2.0 DRAFT / Не заменяет действующую документацию' - : 'SLM 2.0 DRAFT / Not the current stable documentation', -) </script> <template> <Layout> - <template #layout-top> - <div class="draft-banner"> - <span class="draft-banner__mark" aria-hidden="true" /> - {{ bannerText }} - </div> - </template> - <template #sidebar-nav-before> - <DocSetHeader /> - </template> + </Layout> </template> diff --git a/site/.vitepress/theme/RuleCatalog.vue b/site/.vitepress/theme/RuleCatalog.vue deleted file mode 100644 index 8f0b2a1..0000000 --- a/site/.vitepress/theme/RuleCatalog.vue +++ /dev/null @@ -1,271 +0,0 @@ -<script setup lang="ts"> -import { computed, ref } from 'vue' -import { withBase } from 'vitepress' -import { data as rules } from '../rules.data.mts' - -type Rule = (typeof rules)[number] - -const query = ref('') -const ruleset = ref('ALL') -const area = ref('ALL') -const level = ref('ALL') -const copiedId = ref('') - -const areas = [...new Set(rules.map((rule) => rule.area))].sort() -const levels = ['ОБЯЗАН', 'ЗАПРЕЩЕНО', 'СЛЕДУЕТ', 'МОЖЕТ'] - -function relevance(rule: Rule, normalizedQuery: string) { - const id = rule.id.toLowerCase() - if (id === normalizedQuery) return 0 - if (id.startsWith(normalizedQuery)) return 1 - if (id.includes(normalizedQuery)) return 2 - return 3 -} - -const filteredRules = computed(() => { - const normalizedQuery = query.value.trim().toLowerCase() - - return rules - .filter((rule) => ruleset.value === 'ALL' || rule.ruleset === ruleset.value) - .filter((rule) => area.value === 'ALL' || rule.area === area.value) - .filter((rule) => level.value === 'ALL' || rule.level === level.value) - .filter((rule) => { - if (!normalizedQuery) return true - return `${rule.id} ${rule.text} ${rule.pageTitle} ${rule.sectionTitle}` - .toLowerCase() - .includes(normalizedQuery) - }) - .sort((left, right) => relevance(left, normalizedQuery) - relevance(right, normalizedQuery)) -}) - -async function copyRuleLink(rule: Rule) { - const url = new URL(withBase(rule.href), window.location.origin).href - await navigator.clipboard.writeText(url) - copiedId.value = rule.id - window.setTimeout(() => { - if (copiedId.value === rule.id) copiedId.value = '' - }, 1600) -} -</script> - -<template> - <div class="rule-catalog"> - <div class="rule-catalog__controls"> - <label class="rule-catalog__search"> - <span>Правило или текст</span> - <input v-model="query" type="search" placeholder="SLM-BASE-FND-003" /> - </label> - - <label> - <span>Rule set</span> - <select v-model="ruleset"> - <option value="ALL">Все</option> - <option value="BASE">Base</option> - <option value="ADV">Advanced</option> - <option value="PRO">Pro</option> - </select> - </label> - - <label> - <span>Area</span> - <select v-model="area"> - <option value="ALL">Все</option> - <option v-for="item in areas" :key="item" :value="item">{{ item }}</option> - </select> - </label> - - <label> - <span>Уровень</span> - <select v-model="level"> - <option value="ALL">Все</option> - <option v-for="item in levels" :key="item" :value="item">{{ item }}</option> - </select> - </label> - </div> - - <div class="rule-catalog__summary" aria-live="polite"> - Найдено: <strong>{{ filteredRules.length }}</strong> из {{ rules.length }} - </div> - - <div class="rule-catalog__list"> - <article v-for="rule in filteredRules" :key="rule.id" class="rule-catalog__item"> - <div class="rule-catalog__item-head"> - <a :href="withBase(rule.href)" class="rule-catalog__id">{{ rule.id }}</a> - <div class="rule-catalog__badges"> - <span :class="`rule-catalog__badge rule-catalog__badge--${rule.ruleset.toLowerCase()}`"> - {{ rule.ruleset }} - </span> - <span class="rule-catalog__badge">{{ rule.area }}</span> - <span class="rule-catalog__badge">{{ rule.level }}</span> - </div> - </div> - - <p>{{ rule.text }}</p> - - <div class="rule-catalog__source"> - <a :href="withBase(rule.href)">{{ rule.pageTitle }} · {{ rule.sectionTitle }}</a> - <button type="button" @click="copyRuleLink(rule)"> - {{ copiedId === rule.id ? 'Скопировано' : 'Копировать ссылку' }} - </button> - </div> - </article> - </div> - </div> -</template> - -<style scoped> -.rule-catalog { - margin-top: 28px; -} - -.rule-catalog__controls { - display: grid; - grid-template-columns: minmax(220px, 1fr) repeat(3, minmax(120px, 0.35fr)); - gap: 12px; - padding: 16px; - border: 1px solid var(--vp-c-divider); - border-radius: 10px; - background: var(--vp-c-bg-soft); -} - -.rule-catalog__controls label { - display: grid; - gap: 6px; - color: var(--vp-c-text-2); - font-size: 12px; - font-weight: 650; -} - -.rule-catalog__controls input, -.rule-catalog__controls select { - width: 100%; - min-height: 40px; - padding: 8px 10px; - border: 1px solid var(--vp-c-divider); - border-radius: 6px; - outline: none; - background: var(--vp-c-bg); - color: var(--vp-c-text-1); - font: inherit; - font-size: 14px; -} - -.rule-catalog__controls input:focus, -.rule-catalog__controls select:focus { - border-color: var(--vp-c-brand-1); - box-shadow: 0 0 0 3px var(--vp-c-brand-soft); -} - -.rule-catalog__summary { - margin: 14px 2px; - color: var(--vp-c-text-2); - font-size: 13px; -} - -.rule-catalog__list { - display: grid; - gap: 10px; -} - -.rule-catalog__item { - padding: 16px 18px; - border: 1px solid var(--vp-c-divider); - border-radius: 8px; - background: var(--vp-c-bg-soft); -} - -.rule-catalog__item p { - margin: 12px 0; - color: var(--vp-c-text-1); - line-height: 1.6; -} - -.rule-catalog__item-head, -.rule-catalog__source, -.rule-catalog__badges { - display: flex; - align-items: center; -} - -.rule-catalog__item-head, -.rule-catalog__source { - justify-content: space-between; - gap: 12px; -} - -.rule-catalog__badges { - flex-wrap: wrap; - justify-content: flex-end; - gap: 6px; -} - -.rule-catalog__id { - color: var(--vp-c-brand-1); - font-family: var(--vp-font-family-mono); - font-size: 14px; - font-weight: 700; -} - -.rule-catalog__badge { - padding: 3px 7px; - border-radius: 4px; - background: var(--vp-c-default-soft); - color: var(--vp-c-text-2); - font-family: var(--vp-font-family-mono); - font-size: 10px; - font-weight: 650; -} - -.rule-catalog__badge--base { - background: var(--vp-c-brand-soft); - color: var(--vp-c-brand-1); -} - -.rule-catalog__source { - color: var(--vp-c-text-3); - font-size: 12px; -} - -.rule-catalog__source a { - color: inherit; -} - -.rule-catalog__source button { - flex: 0 0 auto; - border: 0; - background: transparent; - color: var(--vp-c-brand-1); - cursor: pointer; - font: inherit; -} - -@media (max-width: 820px) { - .rule-catalog__controls { - grid-template-columns: 1fr 1fr; - } - - .rule-catalog__search { - grid-column: 1 / -1; - } -} - -@media (max-width: 560px) { - .rule-catalog__controls { - grid-template-columns: 1fr; - } - - .rule-catalog__search { - grid-column: auto; - } - - .rule-catalog__item-head, - .rule-catalog__source { - align-items: flex-start; - flex-direction: column; - } - - .rule-catalog__badges { - justify-content: flex-start; - } -} -</style> diff --git a/site/.vitepress/theme/index.ts b/site/.vitepress/theme/index.ts index 499176f..0f50ed1 100644 --- a/site/.vitepress/theme/index.ts +++ b/site/.vitepress/theme/index.ts @@ -1,12 +1,8 @@ import DefaultTheme from 'vitepress/theme' import Layout from './Layout.vue' -import RuleCatalog from './RuleCatalog.vue' import './style.css' export default { extends: DefaultTheme, Layout, - enhanceApp({ app }) { - app.component('RuleCatalog', RuleCatalog) - }, } diff --git a/site/.vitepress/theme/style.css b/site/.vitepress/theme/style.css index c25a6ce..0cc7d0a 100644 --- a/site/.vitepress/theme/style.css +++ b/site/.vitepress/theme/style.css @@ -155,74 +155,60 @@ html { font-size: clamp(2rem, 4vw, 2.65rem); } -.vp-doc table { - display: table; - width: 100%; +.vp-doc h3[id^='slm-l1-'] { + margin-top: 34px; + scroll-margin-top: calc(var(--vp-nav-height) + var(--vp-layout-top-height) + 24px); + color: var(--vp-c-brand-1); + font-family: var(--vp-font-family-mono); + font-size: 0.9rem; + font-weight: 750; + letter-spacing: -0.02em; } -.vp-doc .slm-rule { +.vp-doc h3[id^='slm-l1-'] + blockquote { position: relative; - margin: 18px 0; - padding: 15px 18px 15px 20px; + margin: 10px 0 24px; + padding: 42px 20px 18px; scroll-margin-top: calc(var(--vp-nav-height) + var(--vp-layout-top-height) + 24px); border: 1px solid var(--vp-c-divider); border-left: 3px solid var(--vp-c-brand-2); border-radius: 0 8px 8px 0; background: var(--vp-c-bg-soft); + color: var(--vp-c-text-1); line-height: 1.7; } -.vp-doc .slm-rule strong:first-child { +.vp-doc h3[id^='slm-l1-'] + blockquote::before { + position: absolute; + top: 14px; + left: 20px; + color: var(--vp-c-text-3); + font-family: var(--vp-font-family-mono); + font-size: 9px; + font-weight: 750; + letter-spacing: 0.075em; + text-transform: uppercase; +} + +.vp-doc h3[id^='slm-l1-'][id*='-a'] + blockquote::before { + content: 'Автоматическая проверка'; +} + +.vp-doc h3[id^='slm-l1-'][id*='-r'] + blockquote::before { + content: 'Проверка на ревью'; +} + +.vp-doc h3[id^='slm-l1-'] + blockquote > p { + margin: 0; +} + +.vp-doc h3[id^='slm-l1-'] + blockquote > p + p { + margin-top: 8px; +} + +.vp-doc h3[id^='slm-l1-'] + blockquote strong:first-child { color: var(--vp-c-text-1); - font-family: var(--vp-font-family-mono); - font-size: 0.89em; - letter-spacing: -0.015em; -} - -.vp-doc .slm-rule__permalink { - display: inline-flex; - align-items: center; - justify-content: center; - width: 22px; - height: 22px; - margin: 0 2px 0 7px; - border-radius: 4px; - color: var(--vp-c-brand-1); - font-family: var(--vp-font-family-mono); - font-size: 13px; font-weight: 700; - opacity: 0; - transition: background-color 0.15s, opacity 0.15s; - vertical-align: -1px; -} - -.vp-doc .slm-rule:hover .slm-rule__permalink, -.vp-doc .slm-rule__permalink:focus-visible { - opacity: 1; -} - -.vp-doc .slm-rule__permalink:hover, -.vp-doc .slm-rule__permalink:focus-visible { - background: var(--vp-c-brand-soft); -} - -.vp-doc .slm-rule--prohibited { - border-left-color: #dc2626; - background: rgba(220, 38, 38, 0.06); -} - -.vp-doc .slm-rule--recommended { - border-left-color: #2563eb; - background: rgba(37, 99, 235, 0.055); -} - -.vp-doc .slm-rule--optional { - border-left-color: #64748b; -} - -.vp-doc .slm-rule:target { - outline: 3px solid var(--vp-c-brand-soft); - outline-offset: 3px; } @media (max-width: 640px) { @@ -237,12 +223,8 @@ html { font-size: 16px; } - .vp-doc .slm-rule { + .vp-doc h3[id^='slm-l1-'] + blockquote { margin-inline: -8px; - padding: 13px 14px; - } - - .vp-doc .slm-rule__permalink { - opacity: 1; + padding: 42px 14px 14px; } } diff --git a/site/README.md b/site/README.md index 34c65be..8899f7a 100644 --- a/site/README.md +++ b/site/README.md @@ -1,18 +1,15 @@ -# SLM Design 2.0 Draft +# Сайт SLM Design -`site/` содержит VitePress-конфигурацию, тему и статические ресурсы сайта SLM Design. +`site/` содержит конфигурацию, тему и статические ресурсы VitePress. -Новый publishable corpus находится в `docs/`. Действующая legacy-документация и reference текущего skill находятся в `old-docs/` до отдельного решения о принятии новой спецификации. +Источником опубликованной документации служит [`DRAFT`](../DRAFT/index.md). Сайт включает только Level 1 и его правила; каталог `DRAFT/domains` исключён из маршрутов и поиска. -## Точка входа +## Маршруты -[SLM Design Specification](../docs/ru/specification/index.md) - -Specification определяет base SLM и два независимых [architecture modes](../docs/ru/specification/architecture-modes.md): `SLM Advanced` и `SLM Pro`. Каждый mode является отдельным overlay непосредственно над base SLM. - -## Границы текущего этапа - -На этом этапе в `docs/ru/specification/` размещается только нормативная русская спецификация. Английский раздел зарезервирован под будущий перевод. Учебные материалы, руководства, примеры, справочники и agent skill будут проектироваться после стабилизации правил. +- `/` - главная страница; +- `/level-1/` - документация Level 1; +- `/rules/` - устройство правил; +- `/rules/level-1` - канонический реестр Level 1. ## Локальный запуск @@ -20,4 +17,10 @@ Specification определяет base SLM и два независимых [ar npm run docs:dev ``` -Production build создаётся командой `npm run docs:build`. +## Проверка + +```bash +npm run check:site +``` + +Команда проверяет реестр правил, собирает VitePress и проверяет опубликованные маршруты и якоря правил.