diff --git a/.env.example b/.env.example index 12e9d13b5f1..4ec27e211fd 100644 --- a/.env.example +++ b/.env.example @@ -39,3 +39,10 @@ MINIO_ROOT_PASSWORD=minioadmin CHAIN_URL=http://127.0.0.1:8888 API_URL=http://127.0.0.1:2998/v1/graphql MONGODB_URL=mongodb://127.0.0.1:27017 + +# apps-catalog proxy (Story 9.5.b). Если оба пустые — мы в degraded mode, +# резолвер appsCatalogRemotePackages вернёт пустой список без ошибки. +# URL = DNS-имя контейнера ca-admin в общей сети mono-shared (см. +# docker network inspect mono-shared). +APPS_CATALOG_URL=http://apps-catalog-ca-admin:3002 +APPS_CATALOG_API_KEY=dev-fixture-admin-api-key-DO-NOT-USE-IN-PROD-12345 diff --git a/components/controller/package.json b/components/controller/package.json index 21fabb6f804..8374beae419 100644 --- a/components/controller/package.json +++ b/components/controller/package.json @@ -72,6 +72,7 @@ ], "dependencies": { "@a2seven/yoo-checkout": "^1.1.4", + "@apollo/subgraph": "^2", "@aws-sdk/client-s3": "^3.1035.0", "@coopenomics/factory": "workspace:*", "@coopenomics/inter": "workspace:*", diff --git a/components/controller/src/config/config.ts b/components/controller/src/config/config.ts index a3c445b2eb5..2acc1329bef 100644 --- a/components/controller/src/config/config.ts +++ b/components/controller/src/config/config.ts @@ -193,6 +193,18 @@ const envVarsSchema = z.object({ .string() .optional() .describe('База публичного URL контроллера для read-URL; пусто — берётся BACKEND_URL'), + + // apps-catalog proxy (Story 9.5.b). Витрина remote-пакетов — это HTTP-прокси + // через controller → ca-admin /v1/public/packages. Браузер напрямую в каталог + // не ходит, admin-secret в SPA не утекает (см. architecture-choice C). + APPS_CATALOG_URL: z + .string() + .optional() + .describe('Base URL ca-admin (apps-catalog). Пусто = резолвер вернёт пустой список (degraded mode).'), + APPS_CATALOG_API_KEY: z + .string() + .optional() + .describe('Admin API ключ для apps-catalog. Пусто = degraded mode.'), }); const envInput = isSchemaGeneration ? { ...SCHEMA_GEN_ENV_DEFAULTS, ...process.env } : process.env; @@ -324,4 +336,8 @@ export default { signing_secret: envVars.data.FILE_STORAGE_SIGNING_SECRET || envVars.data.SERVER_SECRET, public_base_url: envVars.data.FILE_STORAGE_PUBLIC_BASE_URL || envVars.data.BACKEND_URL, }, + apps_catalog: { + url: envVars.data.APPS_CATALOG_URL, + api_key: envVars.data.APPS_CATALOG_API_KEY, + }, }; diff --git a/components/controller/src/extensions/apps-catalog-proxy/application/controllers/install-script.controller.ts b/components/controller/src/extensions/apps-catalog-proxy/application/controllers/install-script.controller.ts new file mode 100644 index 00000000000..368871dfef8 --- /dev/null +++ b/components/controller/src/extensions/apps-catalog-proxy/application/controllers/install-script.controller.ts @@ -0,0 +1,59 @@ +import { + Controller, + Get, + Header, + HttpException, + HttpStatus, + Param, + Res, + UseGuards, +} from '@nestjs/common'; +import type { Response } from 'express'; +import { HttpJwtAuthGuard } from '~/application/auth/guards/http-jwt-auth.guard'; +import { AppsCatalogHttpService } from '../../infrastructure/apps-catalog-http.service'; + +const NAME_REGEX = /^[a-z0-9][a-z0-9-]{0,63}$/; + +/** + * Story 9.4.b — REST-проксирование install.js пакета из apps-catalog к + * desktop'у. Защищён HTTP JWT — пайщик должен быть залогинен в свой + * кооператив, иначе нельзя загружать remote-расширения. Admin-API ключ + * apps-catalog'а инкапсулирован в backend'е и не утекает в браузер + * (см. architecture-choice C в Epic 9.5.b). + * + * Endpoint: + * `GET /v1/apps-catalog/install/:scope/:name` + * → 200 text/javascript — CJS install.js + * → 404 если пакета нет (или ca-admin недоступен в degraded mode). + * → 401 без JWT. + */ +@Controller('v1/apps-catalog/install') +@UseGuards(HttpJwtAuthGuard) +export class AppsCatalogInstallScriptController { + constructor(private readonly catalog: AppsCatalogHttpService) {} + + @Get(':scope/:name') + @Header('Content-Type', 'application/javascript; charset=utf-8') + @Header('Cache-Control', 'no-store') + async execute( + @Param('scope') scope: string, + @Param('name') name: string, + @Res({ passthrough: true }) res: Response, + ): Promise { + if (!NAME_REGEX.test(scope) || !NAME_REGEX.test(name)) { + throw new HttpException( + 'Invalid package coordinates', + HttpStatus.NOT_FOUND, + ); + } + const code = await this.catalog.fetchInstallScript(scope, name); + if (code === null) { + throw new HttpException( + 'install.js not available', + HttpStatus.NOT_FOUND, + ); + } + res.status(HttpStatus.OK); + return code; + } +} diff --git a/components/controller/src/extensions/apps-catalog-proxy/application/dto/approve-moderation-input.dto.ts b/components/controller/src/extensions/apps-catalog-proxy/application/dto/approve-moderation-input.dto.ts new file mode 100644 index 00000000000..61d16033370 --- /dev/null +++ b/components/controller/src/extensions/apps-catalog-proxy/application/dto/approve-moderation-input.dto.ts @@ -0,0 +1,29 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { ReleaseScopeInputDTO } from './release-scope-input.dto'; + +/** + * Входные данные `Mutation.approveModeration` (story 9.9). + * + * Chairman восхода одобряет заявку на модерацию из стола восхода. + * Прокидывается на ca-admin `POST /v1/admin/moderation/:id/approve`, + * где ca-admin атомарно переводит заявку в APPROVED и активирует + * релиз. + */ +@InputType() +export class ApproveModerationInputDTO { + @Field({ description: 'UUID заявки на модерацию' }) + moderationId!: string; + + @Field(() => ReleaseScopeInputDTO, { + description: 'Область видимости релиза при активации', + }) + scope!: ReleaseScopeInputDTO; + + @Field({ + nullable: true, + description: + 'Явное согласие модератора на одобрение пакета с критической ' + + 'уязвимостью (requires_override=true). Без него — 403.', + }) + override?: boolean; +} diff --git a/components/controller/src/extensions/apps-catalog-proxy/application/dto/approve-moderation-result.dto.ts b/components/controller/src/extensions/apps-catalog-proxy/application/dto/approve-moderation-result.dto.ts new file mode 100644 index 00000000000..08d5eced446 --- /dev/null +++ b/components/controller/src/extensions/apps-catalog-proxy/application/dto/approve-moderation-result.dto.ts @@ -0,0 +1,51 @@ +import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; + +/** + * Статусы результата `Mutation.approveModeration`. + * + * - `applied` — release ACTIVE, on-chain `apps::setrelease` прошёл; + * - `pendingChain` — moderation одобрена, но on-chain провалился + * (HTTP 423); recovery worker повторит позже; + * - `conflict` — заявка не в SUBMITTED (HTTP 409); + * - `requiresOverride` — нужен явный override:true (HTTP 403); + * - `failed` — прочие ошибки. + */ +export enum ApproveModerationStatus { + APPLIED = 'applied', + PENDING_CHAIN = 'pendingChain', + CONFLICT = 'conflict', + REQUIRES_OVERRIDE = 'requiresOverride', + FAILED = 'failed', +} + +registerEnumType(ApproveModerationStatus, { + name: 'ApproveModerationStatus', + description: 'Статус мутации approveModeration', +}); + +@ObjectType() +export class ApproveModerationResultDTO { + @Field(() => ApproveModerationStatus, { description: 'Discriminator' }) + status!: ApproveModerationStatus; + + @Field({ description: 'UUID запроса (для логов / идемпотентности)' }) + requestId!: string; + + @Field({ + nullable: true, + description: 'Идентификатор пакета (только при status=applied)', + }) + packageId?: string; + + @Field({ + nullable: true, + description: 'Версия активированного релиза (только при status=applied)', + }) + version?: string; + + @Field({ + nullable: true, + description: 'Человекочитаемое сообщение об ошибке', + }) + error?: string; +} diff --git a/components/controller/src/extensions/apps-catalog-proxy/application/dto/apps-catalog-remote-package.dto.ts b/components/controller/src/extensions/apps-catalog-proxy/application/dto/apps-catalog-remote-package.dto.ts new file mode 100644 index 00000000000..b66978e9e38 --- /dev/null +++ b/components/controller/src/extensions/apps-catalog-proxy/application/dto/apps-catalog-remote-package.dto.ts @@ -0,0 +1,43 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +/** + * GraphQL-проекция одного remote-пакета в публичном каталоге apps-catalog + * (Story 9.5.b). Источник — `GET /v1/public/packages` на ca-admin, который + * controller проксирует через `Query.appsCatalogRemotePackages`. На фронте + * подписи витрины строятся из этих полей; фильтр self/чужое и self-sub + * bypass делает desktop по `publisher === coopname`. + */ +@ObjectType() +export class AppsCatalogRemotePackageDTO { + @Field({ description: 'Идентификатор пакета (например, @voskhod/demoapp)' }) + packageId!: string; + + @Field({ description: 'Имя владельца пакета (кооператив-разработчик)' }) + publisher!: string; + + @Field(() => [String], { + description: 'Совместимые subnet (chain_id блокчейна ЦК)', + }) + compatibleSubnets!: string[]; + + @Field(() => String, { + nullable: true, + description: 'Последняя активная версия (semver). null = релизов ещё нет.', + }) + lastActiveVersion?: string | null; + + @Field({ + description: 'Заголовок пакета для UI (в MVP — packageId, в будущем из manifest)', + }) + title!: string; + + @Field({ + description: 'Краткое описание (в MVP — заглушка, в будущем из manifest)', + }) + description!: string; + + @Field({ + description: 'Стоимость подписки, RUB/месяц (в MVP — фиксированно из dev-pricing seed)', + }) + rubPerMonth!: number; +} diff --git a/components/controller/src/extensions/apps-catalog-proxy/application/dto/moderation-request.dto.ts b/components/controller/src/extensions/apps-catalog-proxy/application/dto/moderation-request.dto.ts new file mode 100644 index 00000000000..3164ec650b0 --- /dev/null +++ b/components/controller/src/extensions/apps-catalog-proxy/application/dto/moderation-request.dto.ts @@ -0,0 +1,75 @@ +import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; +import GraphQLJSON from 'graphql-type-json'; + +/** + * Статусы заявки на модерацию (зеркало `ModerationStatus` из ca-admin). + */ +export enum ModerationStatusEnum { + SUBMITTED = 'SUBMITTED', + WITHDRAWN = 'WITHDRAWN', + APPROVED = 'APPROVED', + APPROVED_PENDING_CHAIN = 'APPROVED_PENDING_CHAIN', + REJECTED = 'REJECTED', +} + +registerEnumType(ModerationStatusEnum, { + name: 'ModerationStatusEnum', + description: 'Статус заявки на модерацию пакета', +}); + +export enum ReleaseTypeEnum { + FULL = 'full', + CANARY = 'canary', +} + +registerEnumType(ReleaseTypeEnum, { + name: 'ReleaseTypeEnum', + description: 'Тип релиза: full / canary', +}); + +/** + * Запись заявки на модерацию для стола восхода (story 9.9). + * + * Возвращается из `appsCatalogPendingModerations`. Поля `scope` — JSON + * (union all/subnets/cooperatives/empty), фронт сам разбирает по `.type`. + */ +@ObjectType() +export class ModerationRequestDTO { + @Field({ description: 'UUID заявки' }) + id!: string; + + @Field({ description: 'Идентификатор пакета (@scope/name)' }) + packageId!: string; + + @Field({ description: 'Версия пакета (SemVer)' }) + version!: string; + + @Field(() => GraphQLJSON, { + description: 'ReleaseScope: { type, subnets?, coopnames? }', + }) + scope!: unknown; + + @Field({ description: 'Краткое описание / release notes от разработчика' }) + brief!: string; + + @Field(() => ReleaseTypeEnum, { description: 'Тип релиза' }) + releaseType!: ReleaseTypeEnum; + + @Field(() => ModerationStatusEnum, { description: 'Текущий статус' }) + status!: ModerationStatusEnum; + + @Field({ description: 'Кто подал заявку (Antelope-имя)' }) + submittedBy!: string; + + @Field({ description: 'Когда подана (ISO 8601)' }) + submittedAt!: string; + + @Field({ description: 'Когда последний раз обновлена (ISO 8601)' }) + updatedAt!: string; + + @Field({ + description: + 'Требуется ли явный override:true при approve (критическая уязвимость в scan_report)', + }) + requiresOverride!: boolean; +} diff --git a/components/controller/src/extensions/apps-catalog-proxy/application/dto/publish-package-input.dto.ts b/components/controller/src/extensions/apps-catalog-proxy/application/dto/publish-package-input.dto.ts new file mode 100644 index 00000000000..e99fc2bb196 --- /dev/null +++ b/components/controller/src/extensions/apps-catalog-proxy/application/dto/publish-package-input.dto.ts @@ -0,0 +1,37 @@ +import { Field, InputType } from '@nestjs/graphql'; + +/** + * Входные данные `Mutation.publishPackage` (story 9.3.b-pub). + * + * Эта мутация — первичная регистрация пакета в каталоге восхода. Она + * прокидывается на ca-admin `POST /v1/admin/package` (action + * `apps::regpkg` на blockchain), а ca-admin сам подписывает on-chain от + * имени chairman'а кооператива-оператора (voskhod). Поэтому здесь нет + * никаких ключей пайщика — только координаты пакета. + * + * Multipart upload install.js под Federation v2 более НЕ нужен: + * расширение публикуется как npm-package + docker-image в Nexus, а + * developer пушит их отдельной процедурой (`npm publish` + `docker + * push`) через CA-auth scoped JWT. Story 9.3.b-pub оставляет только + * on-chain регистрацию; релизы с manifest'ом — story 9.3.b-rel. + */ +@InputType() +export class PublishPackageInputDTO { + @Field({ + description: + 'Идентификатор пакета в формате @scope/name, например @voskhod/demoapp', + }) + packageId!: string; + + @Field({ + description: + 'Antelope-имя владельца пакета (1..12 символов из [a-z1-5.])', + }) + ownerUsername!: string; + + @Field(() => [String], { + description: + 'chain_id совместимых подсетей (каждый — 64 hex-символа), хотя бы одна', + }) + compatibleSubnets!: string[]; +} diff --git a/components/controller/src/extensions/apps-catalog-proxy/application/dto/publish-package-result.dto.ts b/components/controller/src/extensions/apps-catalog-proxy/application/dto/publish-package-result.dto.ts new file mode 100644 index 00000000000..136ffca5927 --- /dev/null +++ b/components/controller/src/extensions/apps-catalog-proxy/application/dto/publish-package-result.dto.ts @@ -0,0 +1,27 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +/** + * Результат `Mutation.publishPackage` (story 9.3.b-pub). + * + * Discriminator `status` — `applied` если ca-admin вернул 200 (on-chain + * `apps::regpkg` улетел и подтверждён), `conflict` если пакет уже + * зарегистрирован (HTTP 409), `failed` для остальных ошибок (включая + * degraded-mode когда APPS_CATALOG_API_KEY не задан). + */ +@ObjectType() +export class PublishPackageResultDTO { + @Field({ + description: + 'Статус: applied (зарегистрирован on-chain), conflict (уже есть), failed (ошибка)', + }) + status!: 'applied' | 'conflict' | 'failed'; + + @Field({ + description: + 'Идентификатор запроса (UUIDv4), который ca-admin использовал для идемпотентности', + }) + requestId!: string; + + @Field({ nullable: true, description: 'Человекочитаемое сообщение об ошибке' }) + error?: string; +} diff --git a/components/controller/src/extensions/apps-catalog-proxy/application/dto/publish-release-input.dto.ts b/components/controller/src/extensions/apps-catalog-proxy/application/dto/publish-release-input.dto.ts new file mode 100644 index 00000000000..542b7c3e3b2 --- /dev/null +++ b/components/controller/src/extensions/apps-catalog-proxy/application/dto/publish-release-input.dto.ts @@ -0,0 +1,44 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { GraphQLJSON } from 'graphql-type-json'; + +/** + * Входные данные `Mutation.publishRelease` (story 9.3.b-rel). + * + * Эта мутация регистрирует новый релиз ранее зарегистрированного + * пакета (см. `publishPackage`/9.3.b-pub). Прокидывается на ca-admin + * `POST /v1/admin/releases` (action `apps::setrelease` on-chain), который + * сам подписывает от имени chairman'а кооператива-оператора. + * + * `manifest` под архитектуру E10 содержит ссылки на артефакты в Nexus: + * `coopenomics.backend.image` — docker-image (для subgraph'а + * расширения), `coopenomics.frontend.tarball` — npm tarball (для + * desktop-bundle). Сам manifest валидируется Zod-схемой + * `PackageManifestSchema` на стороне ca-admin (HTTP 422 → resolver + * мапит в `failed`). + */ +@InputType() +export class PublishReleaseInputDTO { + @Field({ + description: + 'Идентификатор пакета в формате @scope/name (должен быть уже зарегистрирован через publishPackage)', + }) + packageId!: string; + + @Field({ description: 'Версия релиза в формате semver, например 1.0.0' }) + version!: string; + + @Field(() => GraphQLJSON, { + description: + 'Package manifest (валидируется Zod-схемой на ca-admin). ' + + 'Содержит coopenomics.backend.image (docker) + coopenomics.frontend.tarball (npm), ' + + 'requires/provides, GraphQL-схему и pricing-параметры.', + }) + manifest!: Record; + + @Field({ + nullable: true, + description: + 'sha256 npm tarball\'а (HEX). Если не передан — ca-admin использует sentinel zero-hash.', + }) + tarballSha256?: string; +} diff --git a/components/controller/src/extensions/apps-catalog-proxy/application/dto/publish-release-result.dto.ts b/components/controller/src/extensions/apps-catalog-proxy/application/dto/publish-release-result.dto.ts new file mode 100644 index 00000000000..c4d07d23c19 --- /dev/null +++ b/components/controller/src/extensions/apps-catalog-proxy/application/dto/publish-release-result.dto.ts @@ -0,0 +1,40 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +/** + * Результат `Mutation.publishRelease` (story 9.3.b-rel). + * + * Discriminator `status`: + * - `applied` — ca-admin вернул 200, on-chain `apps::setrelease` улетел; + * - `invalidManifest` — HTTP 422 INVALID_MANIFEST (Zod-валидация + * manifest'а провалилась на стороне ca-admin); UI должен показать + * `error` как валидационное сообщение; + * - `failed` — прочие ошибки (network, 401, degraded-mode без + * APPS_CATALOG_API_KEY). + */ +@ObjectType() +export class PublishReleaseResultDTO { + @Field({ + description: + 'Статус: applied | invalidManifest | failed', + }) + status!: 'applied' | 'invalidManifest' | 'failed'; + + @Field({ + description: + 'Идентификатор запроса (UUIDv4), который ca-admin использовал для идемпотентности', + }) + requestId!: string; + + @Field({ + nullable: true, + description: + 'Идентификатор blockchain-транзакции (если ca-admin её вернул)', + }) + transactionId?: string; + + @Field({ + nullable: true, + description: 'Человекочитаемое сообщение об ошибке', + }) + error?: string; +} diff --git a/components/controller/src/extensions/apps-catalog-proxy/application/dto/reject-moderation-input.dto.ts b/components/controller/src/extensions/apps-catalog-proxy/application/dto/reject-moderation-input.dto.ts new file mode 100644 index 00000000000..b47b4beb979 --- /dev/null +++ b/components/controller/src/extensions/apps-catalog-proxy/application/dto/reject-moderation-input.dto.ts @@ -0,0 +1,18 @@ +import { Field, InputType } from '@nestjs/graphql'; + +/** + * Входные данные `Mutation.rejectModeration` (story 9.9). + * + * Chairman восхода отклоняет заявку на модерацию с причиной. Причина + * пишется в `status_reason` и доставляется разработчику через outbox. + */ +@InputType() +export class RejectModerationInputDTO { + @Field({ description: 'UUID заявки на модерацию' }) + moderationId!: string; + + @Field({ + description: 'Причина отказа (3..2000 символов), увидит разработчик', + }) + reason!: string; +} diff --git a/components/controller/src/extensions/apps-catalog-proxy/application/dto/reject-moderation-result.dto.ts b/components/controller/src/extensions/apps-catalog-proxy/application/dto/reject-moderation-result.dto.ts new file mode 100644 index 00000000000..ed421ad58b2 --- /dev/null +++ b/components/controller/src/extensions/apps-catalog-proxy/application/dto/reject-moderation-result.dto.ts @@ -0,0 +1,34 @@ +import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; + +/** + * Статусы результата `Mutation.rejectModeration`. + * + * - `applied` — REJECTED, статус-причина сохранена; + * - `conflict` — заявка не в SUBMITTED (HTTP 409); + * - `failed` — прочие ошибки. + */ +export enum RejectModerationStatus { + APPLIED = 'applied', + CONFLICT = 'conflict', + FAILED = 'failed', +} + +registerEnumType(RejectModerationStatus, { + name: 'RejectModerationStatus', + description: 'Статус мутации rejectModeration', +}); + +@ObjectType() +export class RejectModerationResultDTO { + @Field(() => RejectModerationStatus, { description: 'Discriminator' }) + status!: RejectModerationStatus; + + @Field({ description: 'UUID запроса' }) + requestId!: string; + + @Field({ + nullable: true, + description: 'Человекочитаемое сообщение об ошибке', + }) + error?: string; +} diff --git a/components/controller/src/extensions/apps-catalog-proxy/application/dto/release-scope-input.dto.ts b/components/controller/src/extensions/apps-catalog-proxy/application/dto/release-scope-input.dto.ts new file mode 100644 index 00000000000..ea162c2ec2b --- /dev/null +++ b/components/controller/src/extensions/apps-catalog-proxy/application/dto/release-scope-input.dto.ts @@ -0,0 +1,51 @@ +import { Field, InputType, registerEnumType } from '@nestjs/graphql'; + +/** + * Тип области видимости релиза. + * + * - `all` — для всех подсетей (любых кооперативов); + * - `subnets` — только перечисленные chain_id подсетей; + * - `cooperatives` — только перечисленные кооперативы; + * - `empty` — выключить релиз (полная отзывка видимости). + * + * См. epics.md Story 3.7 — одобрение заявки на модерацию. + */ +export enum ReleaseScopeType { + ALL = 'all', + SUBNETS = 'subnets', + COOPERATIVES = 'cooperatives', + EMPTY = 'empty', +} + +registerEnumType(ReleaseScopeType, { + name: 'ReleaseScopeType', + description: 'Тип области видимости релиза при одобрении заявки', +}); + +/** + * Область видимости релиза, передаётся в `approveModeration`. + * + * При `type=subnets` ожидается заполненный `subnets`. При + * `type=cooperatives` — заполненный `coopnames`. Для `all` / `empty` + * массивы игнорируются. + * + * Резолвер сам валидирует соответствие type → массив и нормализует + * перед отправкой на ca-admin. + */ +@InputType() +export class ReleaseScopeInputDTO { + @Field(() => ReleaseScopeType, { description: 'Тип scope' }) + type!: ReleaseScopeType; + + @Field(() => [String], { + nullable: true, + description: 'chain_id подсетей (для type=subnets)', + }) + subnets?: string[]; + + @Field(() => [String], { + nullable: true, + description: 'Antelope-имена кооперативов (для type=cooperatives)', + }) + coopnames?: string[]; +} diff --git a/components/controller/src/extensions/apps-catalog-proxy/application/resolvers/apps-catalog-proxy.resolver.ts b/components/controller/src/extensions/apps-catalog-proxy/application/resolvers/apps-catalog-proxy.resolver.ts new file mode 100644 index 00000000000..df2e766e53d --- /dev/null +++ b/components/controller/src/extensions/apps-catalog-proxy/application/resolvers/apps-catalog-proxy.resolver.ts @@ -0,0 +1,374 @@ +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { BadRequestException, Logger, UseGuards } from '@nestjs/common'; +import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard'; +import { RolesGuard } from '~/application/auth/guards/roles.guard'; +import { AuthRoles } from '~/application/auth/decorators/auth.decorator'; +import { + AppsCatalogHttpService, + type ModerationStatus, + type ReleaseScopeInput, +} from '../../infrastructure/apps-catalog-http.service'; +import { AppsCatalogRemotePackageDTO } from '../dto/apps-catalog-remote-package.dto'; +import { ApproveModerationInputDTO } from '../dto/approve-moderation-input.dto'; +import { + ApproveModerationResultDTO, + ApproveModerationStatus, +} from '../dto/approve-moderation-result.dto'; +import { + ModerationRequestDTO, + ModerationStatusEnum, + ReleaseTypeEnum, +} from '../dto/moderation-request.dto'; +import { PublishPackageInputDTO } from '../dto/publish-package-input.dto'; +import { PublishPackageResultDTO } from '../dto/publish-package-result.dto'; +import { PublishReleaseInputDTO } from '../dto/publish-release-input.dto'; +import { PublishReleaseResultDTO } from '../dto/publish-release-result.dto'; +import { + ReleaseScopeInputDTO, + ReleaseScopeType, +} from '../dto/release-scope-input.dto'; +import { RejectModerationInputDTO } from '../dto/reject-moderation-input.dto'; +import { + RejectModerationResultDTO, + RejectModerationStatus, +} from '../dto/reject-moderation-result.dto'; + +const toReleaseScope = (dto: ReleaseScopeInputDTO): ReleaseScopeInput => { + switch (dto.type) { + case ReleaseScopeType.ALL: + return { type: 'all' }; + case ReleaseScopeType.EMPTY: + return { type: 'empty' }; + case ReleaseScopeType.SUBNETS: { + const subnets = dto.subnets ?? []; + if (subnets.length === 0) { + throw new BadRequestException( + 'scope.type=subnets требует непустой subnets', + ); + } + return { type: 'subnets', subnets }; + } + case ReleaseScopeType.COOPERATIVES: { + const coopnames = dto.coopnames ?? []; + if (coopnames.length === 0) { + throw new BadRequestException( + 'scope.type=cooperatives требует непустой coopnames', + ); + } + return { type: 'cooperatives', coopnames }; + } + } +}; + +/** + * Story 9.5.b — публичный каталог remote-пакетов на desktop'е магазина. + * + * Прокси-резолвер: controller ходит к ca-admin /v1/public/packages, маппит + * snake_case wire-формат в camelCase GraphQL-проекцию, добавляет UI-поля + * (title/description/rubPerMonth) для витрины. В V1 description и + * rubPerMonth — заглушки; в V2 будут читаться из package manifest и + * apps-catalog pricing соответственно. + * + * Guard: GqlJwtAuthGuard — каталог видят только авторизованные пайщики + * (по умолчанию роль `user`). Self-filter и self-sub bypass делает + * desktop по сравнению `publisher` и текущего coopname. + */ +@Resolver() +export class AppsCatalogProxyResolver { + private readonly logger = new Logger(AppsCatalogProxyResolver.name); + + constructor(private readonly client: AppsCatalogHttpService) {} + + @Query(() => [AppsCatalogRemotePackageDTO], { + name: 'appsCatalogRemotePackages', + description: + 'Список remote-пакетов из публичного каталога apps-catalog. ' + + 'Защищён JWT (видят только авторизованные пайщики). ' + + 'Источник — ca-admin /v1/public/packages; controller проксирует.', + }) + @UseGuards(GqlJwtAuthGuard) + async appsCatalogRemotePackages( + @Args('page', { type: () => Number, defaultValue: 1 }) page: number, + @Args('pageSize', { type: () => Number, defaultValue: 50 }) pageSize: number, + ): Promise { + const packages = await this.client.listPublicPackages(page, pageSize); + return packages.map((p) => ({ + packageId: p.packageId, + publisher: p.publisher, + compatibleSubnets: p.compatibleSubnets, + lastActiveVersion: p.lastActiveVersion, + title: this.buildTitle(p.packageId), + description: this.buildDescription(p.publisher), + rubPerMonth: 1000, + })); + } + + private buildTitle(packageId: string): string { + const tail = packageId.split('/').pop() ?? packageId; + return tail.charAt(0).toUpperCase() + tail.slice(1); + } + + private buildDescription(publisher: string): string { + return `Удалённое расширение от ${publisher} — устанавливается без перезагрузки сервера.`; + } + + /** + * Story 9.3.b-pub — стол разработчика публикует пакет в каталоге восхода. + * + * Защита: только chairman кооператива-оператора (`voskhod` на dev). + * Сама подпись on-chain `apps::regpkg` делает ca-admin от имени chairman'а + * восхода — controller только проксирует HTTP-запрос и возвращает + * discriminated outcome (`applied | conflict | failed`). + * + * Multipart upload install.js здесь НЕ делается. Под архитектуру E10 + * расширения публикуются как npm-package + docker-image в Nexus + * отдельной процедурой (`npm publish` / `docker push`) через scoped + * JWT от CA-auth. Эта мутация — только on-chain маркер «такой + * пакет существует». + */ + @Mutation(() => PublishPackageResultDTO, { + name: 'publishPackage', + description: + 'Регистрирует пакет on-chain (action apps::regpkg) через ca-admin. ' + + 'Подписывает chairman кооператива-оператора каталога. Доступно ' + + 'только chairman\'у (стол разработчика).', + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman']) + async publishPackage( + @Args('data', { type: () => PublishPackageInputDTO }) + data: PublishPackageInputDTO, + ): Promise { + const outcome = await this.client.registerPackage({ + packageId: data.packageId, + ownerUsername: data.ownerUsername, + compatibleSubnets: data.compatibleSubnets, + }); + if (outcome.status === 'applied') { + this.logger.log( + `publishPackage applied: ${data.packageId} (request ${outcome.requestId})`, + ); + return { status: 'applied', requestId: outcome.requestId }; + } + if (outcome.status === 'conflict') { + return { + status: 'conflict', + requestId: outcome.requestId, + error: outcome.error, + }; + } + return { + status: 'failed', + requestId: outcome.requestId, + error: outcome.error, + }; + } + + /** + * Story 9.3.b-rel — стол разработчика выкладывает новый релиз пакета. + * + * Прокидывает manifest + версию на ca-admin `POST /v1/admin/releases`. + * ca-admin валидирует manifest Zod-схемой и подписывает on-chain + * `apps::setrelease` от имени chairman'а кооператива-оператора. Под + * архитектуру E10 manifest должен содержать ссылки на артефакты в + * Nexus (`coopenomics.backend.image` + `coopenomics.frontend.tarball`). + */ + @Mutation(() => PublishReleaseResultDTO, { + name: 'publishRelease', + description: + 'Создаёт новый релиз пакета (action apps::setrelease) через ca-admin. ' + + 'Подписывает chairman кооператива-оператора каталога. Доступно ' + + 'только chairman\'у (стол разработчика).', + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman']) + async publishRelease( + @Args('data', { type: () => PublishReleaseInputDTO }) + data: PublishReleaseInputDTO, + ): Promise { + const outcome = await this.client.createRelease({ + packageId: data.packageId, + version: data.version, + manifest: data.manifest, + tarballSha256: data.tarballSha256, + }); + if (outcome.status === 'applied') { + this.logger.log( + `publishRelease applied: ${data.packageId}@${data.version} (request ${outcome.requestId}, tx ${outcome.transactionId ?? '-'})`, + ); + return { + status: 'applied', + requestId: outcome.requestId, + transactionId: outcome.transactionId, + }; + } + if (outcome.status === 'invalidManifest') { + return { + status: 'invalidManifest', + requestId: outcome.requestId, + error: outcome.error, + }; + } + return { + status: 'failed', + requestId: outcome.requestId, + error: outcome.error, + }; + } + + /** + * Story 9.9 — стол восхода: список заявок на модерацию. + * + * Защита: chairman кооператива-оператора (voskhod). Возвращает заявки + * в указанном статусе (по умолчанию SUBMITTED), отсортированные + * `updatedAt` ASC (старшие — первыми). + * + * На degraded-mode (без APPS_CATALOG_API_KEY) — пустой массив, чтобы + * UI стола восхода нормально показал «pending пусто». + */ + @Query(() => [ModerationRequestDTO], { + name: 'appsCatalogPendingModerations', + description: + 'Заявки на модерацию пакетов в каталоге восхода. По умолчанию ' + + 'SUBMITTED (ждут approve/reject). Только chairman.', + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman']) + async appsCatalogPendingModerations( + @Args('status', { + type: () => ModerationStatusEnum, + nullable: true, + defaultValue: ModerationStatusEnum.SUBMITTED, + }) + status: ModerationStatusEnum, + @Args('limit', { type: () => Number, nullable: true }) + limit?: number, + ): Promise { + const items = await this.client.listSubmittedModerations( + status as unknown as ModerationStatus, + limit, + ); + return items.map((r) => ({ + id: r.id, + packageId: r.packageId, + version: r.version, + scope: r.scope, + brief: r.brief, + releaseType: r.releaseType as ReleaseTypeEnum, + status: r.status as unknown as ModerationStatusEnum, + submittedBy: r.submittedBy, + submittedAt: r.submittedAt, + updatedAt: r.updatedAt, + requiresOverride: r.requiresOverride, + })); + } + + /** + * Story 9.9-approve — chairman восхода одобряет заявку на модерацию. + * + * Защита: только chairman кооператива-оператора (voskhod). ca-admin + * атомарно переводит moderation в APPROVED + активирует release + + * выкладывает outbox-event `release.activated` (он же триггер для + * on-chain `apps::setrelease` и orchestrator'а install pipeline). + * + * Discriminated outcome — клиент должен switch на `status`. + */ + @Mutation(() => ApproveModerationResultDTO, { + name: 'approveModeration', + description: + 'Одобряет заявку на модерацию в каталоге восхода. Только chairman.', + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman']) + async approveModeration( + @Args('data', { type: () => ApproveModerationInputDTO }) + data: ApproveModerationInputDTO, + ): Promise { + const outcome = await this.client.approveModeration({ + moderationId: data.moderationId, + scope: toReleaseScope(data.scope), + override: data.override, + }); + if (outcome.status === 'applied') { + this.logger.log( + `approveModeration applied: ${outcome.packageId}@${outcome.version} (request ${outcome.requestId})`, + ); + return { + status: ApproveModerationStatus.APPLIED, + requestId: outcome.requestId, + packageId: outcome.packageId, + version: outcome.version, + }; + } + if (outcome.status === 'pendingChain') { + return { + status: ApproveModerationStatus.PENDING_CHAIN, + requestId: outcome.requestId, + error: outcome.error, + }; + } + if (outcome.status === 'conflict') { + return { + status: ApproveModerationStatus.CONFLICT, + requestId: outcome.requestId, + error: outcome.error, + }; + } + if (outcome.status === 'requiresOverride') { + return { + status: ApproveModerationStatus.REQUIRES_OVERRIDE, + requestId: outcome.requestId, + error: outcome.error, + }; + } + return { + status: ApproveModerationStatus.FAILED, + requestId: outcome.requestId, + error: outcome.error, + }; + } + + /** + * Story 9.9-reject — chairman восхода отклоняет заявку. + * + * Защита: только chairman кооператива-оператора (voskhod). Причина + * пишется в `status_reason` и доставляется разработчику через outbox. + */ + @Mutation(() => RejectModerationResultDTO, { + name: 'rejectModeration', + description: + 'Отклоняет заявку на модерацию с причиной. Только chairman.', + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman']) + async rejectModeration( + @Args('data', { type: () => RejectModerationInputDTO }) + data: RejectModerationInputDTO, + ): Promise { + const outcome = await this.client.rejectModeration({ + moderationId: data.moderationId, + reason: data.reason, + }); + if (outcome.status === 'applied') { + this.logger.log( + `rejectModeration applied: ${data.moderationId} (request ${outcome.requestId})`, + ); + return { + status: RejectModerationStatus.APPLIED, + requestId: outcome.requestId, + }; + } + if (outcome.status === 'conflict') { + return { + status: RejectModerationStatus.CONFLICT, + requestId: outcome.requestId, + error: outcome.error, + }; + } + return { + status: RejectModerationStatus.FAILED, + requestId: outcome.requestId, + error: outcome.error, + }; + } +} diff --git a/components/controller/src/extensions/apps-catalog-proxy/apps-catalog-proxy.module.ts b/components/controller/src/extensions/apps-catalog-proxy/apps-catalog-proxy.module.ts new file mode 100644 index 00000000000..28cabd8ff34 --- /dev/null +++ b/components/controller/src/extensions/apps-catalog-proxy/apps-catalog-proxy.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { AppsCatalogHttpService } from './infrastructure/apps-catalog-http.service'; +import { AppsCatalogInstallScriptController } from './application/controllers/install-script.controller'; +import { AppsCatalogProxyResolver } from './application/resolvers/apps-catalog-proxy.resolver'; + +/** + * Story 9.5.b + 9.4.b — модуль-прокси для публичного каталога apps-catalog. + * + * GraphQL `Query.appsCatalogRemotePackages` (9.5.b) + REST + * `GET /v1/apps-catalog/install/:scope/:name` (9.4.b) + HTTP-клиент к + * ca-admin. Никакого state, никаких миграций, без TypeORM. Включается в + * `ExtensionsModule` в общий imports/exports список. + */ +@Module({ + controllers: [AppsCatalogInstallScriptController], + providers: [AppsCatalogHttpService, AppsCatalogProxyResolver], + exports: [AppsCatalogHttpService], +}) +export class AppsCatalogProxyModule {} diff --git a/components/controller/src/extensions/apps-catalog-proxy/infrastructure/apps-catalog-http.service.ts b/components/controller/src/extensions/apps-catalog-proxy/infrastructure/apps-catalog-http.service.ts new file mode 100644 index 00000000000..5983acf9260 --- /dev/null +++ b/components/controller/src/extensions/apps-catalog-proxy/infrastructure/apps-catalog-http.service.ts @@ -0,0 +1,510 @@ +import { Injectable, Logger } from '@nestjs/common'; +import axios, { type AxiosInstance, AxiosError } from 'axios'; +import { v4 as uuidv4 } from 'uuid'; +import config from '~/config/config'; + +interface PublicPackageWireFormat { + package_id: string; + owner_username: string; + compatible_subnets: string[]; + last_active_version: string | null; +} + +export interface AppsCatalogPackage { + packageId: string; + publisher: string; + compatibleSubnets: string[]; + lastActiveVersion: string | null; +} + +export interface RegisterPackageInput { + packageId: string; + ownerUsername: string; + compatibleSubnets: string[]; + /** Optional. Если не передан — генерируется uuidv4. */ + requestId?: string; +} + +export type RegisterPackageOutcome = + | { status: 'applied'; requestId: string } + | { status: 'conflict'; requestId: string; error: string } + | { status: 'failed'; requestId: string; error: string }; + +export interface CreateReleaseInput { + packageId: string; + version: string; + manifest: Record; + tarballSha256?: string; + requestId?: string; +} + +export type CreateReleaseOutcome = + | { status: 'applied'; requestId: string; transactionId?: string } + | { status: 'invalidManifest'; requestId: string; error: string } + | { status: 'failed'; requestId: string; error: string }; + +export type ModerationStatus = + | 'SUBMITTED' + | 'WITHDRAWN' + | 'APPROVED' + | 'APPROVED_PENDING_CHAIN' + | 'REJECTED'; + +export interface ModerationRequestRow { + id: string; + packageId: string; + version: string; + scope: unknown; + brief: string; + releaseType: 'full' | 'canary'; + status: ModerationStatus; + submittedBy: string; + submittedAt: string; + updatedAt: string; + requiresOverride: boolean; +} + +interface ModerationRequestWireFormat { + id: string; + package_id: string; + version: string; + scope: unknown; + brief: string; + release_type: 'full' | 'canary'; + status: ModerationStatus; + submitted_by: string; + submitted_at: string; + updated_at: string; + requires_override: boolean; +} + +export type ReleaseScopeInput = + | { type: 'all' } + | { type: 'empty' } + | { type: 'subnets'; subnets: string[] } + | { type: 'cooperatives'; coopnames: string[] }; + +export interface ApproveModerationInput { + moderationId: string; + scope: ReleaseScopeInput; + override?: boolean; + requestId?: string; +} + +export type ApproveModerationOutcome = + | { + status: 'applied'; + requestId: string; + packageId: string; + version: string; + } + | { + status: 'pendingChain'; + requestId: string; + error: string; + } + | { + status: 'conflict'; + requestId: string; + error: string; + } + | { + status: 'requiresOverride'; + requestId: string; + error: string; + } + | { + status: 'failed'; + requestId: string; + error: string; + }; + +export interface RejectModerationInput { + moderationId: string; + reason: string; + requestId?: string; +} + +export type RejectModerationOutcome = + | { status: 'applied'; requestId: string } + | { status: 'conflict'; requestId: string; error: string } + | { status: 'failed'; requestId: string; error: string }; + +/** + * HTTP-клиент к ca-admin (apps-catalog) для Story 9.5.b. Защищён admin-API + * ключом из env (APPS_CATALOG_API_KEY). Используется только сервером — + * ключ в браузер не утекает. + * + * Degraded mode: если APPS_CATALOG_URL / APPS_CATALOG_API_KEY не заданы — + * возвращает пустой список вместо ошибки, чтобы dev-окружение без + * apps-catalog (или CI без сети) не валило desktop boot. + */ +@Injectable() +export class AppsCatalogHttpService { + private readonly logger = new Logger(AppsCatalogHttpService.name); + private readonly client: AxiosInstance | null; + + constructor() { + if (config.apps_catalog.url && config.apps_catalog.api_key) { + this.client = axios.create({ + baseURL: config.apps_catalog.url, + timeout: 5000, + headers: { + Authorization: `Bearer ${config.apps_catalog.api_key}`, + }, + }); + } else { + this.client = null; + this.logger.warn( + 'APPS_CATALOG_URL/APPS_CATALOG_API_KEY не заданы — apps-catalog-proxy в degraded mode (пустой каталог)', + ); + } + } + + async listPublicPackages(page = 1, pageSize = 50): Promise { + if (!this.client) return []; + try { + const res = await this.client.get( + '/v1/public/packages', + { params: { page, page_size: pageSize } }, + ); + return res.data.map((p) => ({ + packageId: p.package_id, + publisher: p.owner_username, + compatibleSubnets: p.compatible_subnets, + lastActiveVersion: p.last_active_version, + })); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + this.logger.error(`apps-catalog list failed: ${msg}`); + return []; + } + } + + /** + * Story 9.4.b: получить install.js пакета remote-расширения. + * + * Возвращает plain CJS-текст из ca-admin endpoint'а + * `GET /v1/public/packages/:scope/:name/install.js`. На degraded mode + * (без env-config'а apps-catalog'а) либо если ca-admin отвечает 404 + * — возвращает `null`, чтобы desktop'у было ясно «нет такого пакета / + * каталог недоступен» вместо ошибки 500 в браузере. + * + * @param scope владелец пакета (`voskhod` из `@voskhod/demo-app`). + * @param name short-name пакета (`demo-app`). + */ + /** + * Story 9.3.b-pub: первичная регистрация пакета в каталоге. + * + * Прокидывает входной DTO на ca-admin `POST /v1/admin/package`. ca-admin + * сам подписывает on-chain action `apps::regpkg` от имени chairman'а + * кооператива-оператора каталога (voskhod на dev) — поэтому здесь не + * требуется session-key пайщика. + * + * Discriminated outcome (vs. throw): + * - `applied` — HTTP 200, on-chain transaction подтверждён; + * - `conflict` — HTTP 409 (пакет уже зарегистрирован или дубликат + * `request_id`); resolver мапит на `status: 'conflict'`; + * - `failed` — любое прочее (network, 400, 401, degraded-mode без + * APPS_CATALOG_API_KEY); resolver мапит на `status: 'failed'`. + * + * Degraded mode (нет client'а) — возвращает `failed` с явным сообщением: + * mutation в degraded-стенде не делает silent no-op, чтобы UI не + * вводился в заблуждение «пакет опубликован». + */ + async registerPackage( + input: RegisterPackageInput, + ): Promise { + const requestId = input.requestId ?? uuidv4(); + if (!this.client) { + const error = 'APPS_CATALOG_URL/APPS_CATALOG_API_KEY не заданы'; + this.logger.warn( + `registerPackage refused (degraded mode): ${input.packageId}`, + ); + return { status: 'failed', requestId, error }; + } + try { + await this.client.post('/v1/admin/package', { + request_id: requestId, + package_id: input.packageId, + owner_username: input.ownerUsername, + compatible_subnets: input.compatibleSubnets, + }); + return { status: 'applied', requestId }; + } catch (err) { + const status = (err as AxiosError).response?.status; + const responseData = (err as AxiosError).response?.data; + const detail = + typeof responseData === 'object' && responseData + ? JSON.stringify(responseData) + : err instanceof Error + ? err.message + : String(err); + if (status === 409) { + this.logger.warn( + `registerPackage conflict для ${input.packageId}: ${detail}`, + ); + return { status: 'conflict', requestId, error: detail }; + } + this.logger.error( + `registerPackage failed для ${input.packageId}: ${detail}`, + ); + return { status: 'failed', requestId, error: detail }; + } + } + + /** + * Story 9.3.b-rel: создание нового релиза пакета (action `apps::setrelease`). + * + * Прокидывает manifest + версию на ca-admin `POST /v1/admin/releases`. + * ca-admin сам валидирует manifest Zod-схемой и подписывает on-chain. + * + * Discriminated outcome: + * - `applied` — HTTP 200, on-chain прошёл; в payload может быть + * `transaction_id` (если адаптер не stub'овый); + * - `invalidManifest` — HTTP 422 INVALID_MANIFEST (manifest не прошёл + * Zod-валидацию на стороне ca-admin); `error` содержит детали + * валидации в стабильном формате; + * - `failed` — прочие ошибки (network, 401, 503, degraded-mode). + */ + async createRelease(input: CreateReleaseInput): Promise { + const requestId = input.requestId ?? uuidv4(); + if (!this.client) { + const error = 'APPS_CATALOG_URL/APPS_CATALOG_API_KEY не заданы'; + this.logger.warn( + `createRelease refused (degraded mode): ${input.packageId}@${input.version}`, + ); + return { status: 'failed', requestId, error }; + } + try { + const res = await this.client.post<{ + ok: boolean; + transaction_id?: string; + }>('/v1/admin/releases', { + request_id: requestId, + package_id: input.packageId, + version: input.version, + manifest: input.manifest, + ...(input.tarballSha256 + ? { tarball_sha256: input.tarballSha256 } + : {}), + }); + return { + status: 'applied', + requestId, + transactionId: res.data?.transaction_id, + }; + } catch (err) { + const status = (err as AxiosError).response?.status; + const responseData = (err as AxiosError).response?.data; + const detail = + typeof responseData === 'object' && responseData + ? JSON.stringify(responseData) + : err instanceof Error + ? err.message + : String(err); + if (status === 422) { + this.logger.warn( + `createRelease invalidManifest для ${input.packageId}@${input.version}: ${detail}`, + ); + return { status: 'invalidManifest', requestId, error: detail }; + } + this.logger.error( + `createRelease failed для ${input.packageId}@${input.version}: ${detail}`, + ); + return { status: 'failed', requestId, error: detail }; + } + } + + /** + * Story 9.9: список заявок на модерацию по статусу. + * + * Дёргает ca-admin `GET /v1/admin/moderation?status=...&limit=...`. + * Используется столом восхода (chairman voskhod) для просмотра pending + * заявок и принятия решения (approve/reject). + * + * Degraded mode (нет ca-admin client'а) → пустой массив, чтобы UI + * нормально показал «pending пусто» вместо ошибки. + */ + async listSubmittedModerations( + status: ModerationStatus = 'SUBMITTED', + limit?: number, + ): Promise { + if (!this.client) return []; + try { + const params: Record = { status }; + if (limit !== undefined) params.limit = limit; + const res = await this.client.get<{ + items: ModerationRequestWireFormat[]; + }>('/v1/admin/moderation', { params }); + return res.data.items.map((r) => ({ + id: r.id, + packageId: r.package_id, + version: r.version, + scope: r.scope, + brief: r.brief, + releaseType: r.release_type, + status: r.status, + submittedBy: r.submitted_by, + submittedAt: r.submitted_at, + updatedAt: r.updated_at, + requiresOverride: r.requires_override, + })); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + this.logger.error(`listSubmittedModerations failed: ${msg}`); + return []; + } + } + + /** + * Story 9.9: chairman восхода одобряет заявку на модерацию. + * + * Дёргает ca-admin `POST /v1/admin/moderation/:id/approve`. ca-admin + * атомарно переводит moderation в APPROVED, активирует release и + * выкладывает outbox-event `release.activated` (он же триггер для + * on-chain `apps::setrelease` и orchestrator'а install pipeline). + * + * Discriminated outcome: + * - `applied` — HTTP 200, release ACTIVE, package_id/version в payload; + * - `pendingChain` — HTTP 423 APPROVED_PENDING_CHAIN: moderation + * одобрена, но on-chain провалился; recovery worker повторит; + * - `conflict` — HTTP 409: заявка не в SUBMITTED (уже approved / + * withdrawn); + * - `requiresOverride` — HTTP 403: scan_report критичен, нужен + * явный `override: true`; + * - `failed` — прочие ошибки. + */ + async approveModeration( + input: ApproveModerationInput, + ): Promise { + const requestId = input.requestId ?? uuidv4(); + if (!this.client) { + const error = 'APPS_CATALOG_URL/APPS_CATALOG_API_KEY не заданы'; + this.logger.warn( + `approveModeration refused (degraded mode): ${input.moderationId}`, + ); + return { status: 'failed', requestId, error }; + } + try { + const res = await this.client.post<{ + ok: boolean; + package_id: string; + version: string; + request_id: string; + }>(`/v1/admin/moderation/${encodeURIComponent(input.moderationId)}/approve`, { + request_id: requestId, + scope: input.scope, + ...(input.override !== undefined ? { override: input.override } : {}), + }); + return { + status: 'applied', + requestId: res.data.request_id ?? requestId, + packageId: res.data.package_id, + version: res.data.version, + }; + } catch (err) { + const httpStatus = (err as AxiosError).response?.status; + const responseData = (err as AxiosError).response?.data; + const detail = + typeof responseData === 'object' && responseData + ? JSON.stringify(responseData) + : err instanceof Error + ? err.message + : String(err); + if (httpStatus === 423) { + this.logger.warn( + `approveModeration pendingChain ${input.moderationId}: ${detail}`, + ); + return { status: 'pendingChain', requestId, error: detail }; + } + if (httpStatus === 409) { + this.logger.warn( + `approveModeration conflict ${input.moderationId}: ${detail}`, + ); + return { status: 'conflict', requestId, error: detail }; + } + if (httpStatus === 403) { + this.logger.warn( + `approveModeration requiresOverride ${input.moderationId}: ${detail}`, + ); + return { status: 'requiresOverride', requestId, error: detail }; + } + this.logger.error( + `approveModeration failed ${input.moderationId}: ${detail}`, + ); + return { status: 'failed', requestId, error: detail }; + } + } + + /** + * Story 9.9: chairman восхода отклоняет заявку на модерацию. + * + * Дёргает ca-admin `POST /v1/admin/moderation/:id/reject` с `reason`. + * ca-admin переводит moderation в REJECTED (compare-and-set из + * SUBMITTED) и пишет аудит-запись. + * + * Discriminated outcome: + * - `applied` — HTTP 200, заявка REJECTED; + * - `conflict` — HTTP 409: заявка уже не в SUBMITTED; + * - `failed` — прочие ошибки. + */ + async rejectModeration( + input: RejectModerationInput, + ): Promise { + const requestId = input.requestId ?? uuidv4(); + if (!this.client) { + const error = 'APPS_CATALOG_URL/APPS_CATALOG_API_KEY не заданы'; + this.logger.warn( + `rejectModeration refused (degraded mode): ${input.moderationId}`, + ); + return { status: 'failed', requestId, error }; + } + try { + await this.client.post( + `/v1/admin/moderation/${encodeURIComponent(input.moderationId)}/reject`, + { + request_id: requestId, + reason: input.reason, + }, + ); + return { status: 'applied', requestId }; + } catch (err) { + const httpStatus = (err as AxiosError).response?.status; + const responseData = (err as AxiosError).response?.data; + const detail = + typeof responseData === 'object' && responseData + ? JSON.stringify(responseData) + : err instanceof Error + ? err.message + : String(err); + if (httpStatus === 409) { + this.logger.warn( + `rejectModeration conflict ${input.moderationId}: ${detail}`, + ); + return { status: 'conflict', requestId, error: detail }; + } + this.logger.error( + `rejectModeration failed ${input.moderationId}: ${detail}`, + ); + return { status: 'failed', requestId, error: detail }; + } + } + + async fetchInstallScript(scope: string, name: string): Promise { + if (!this.client) return null; + try { + const res = await this.client.get( + `/v1/public/packages/${encodeURIComponent(scope)}/${encodeURIComponent(name)}/install.js`, + { responseType: 'text', transformResponse: (data) => data as string }, + ); + return typeof res.data === 'string' ? res.data : String(res.data); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + this.logger.error( + `apps-catalog install.js fetch failed for ${scope}/${name}: ${msg}`, + ); + return null; + } + } +} diff --git a/components/controller/src/extensions/extensions.module.ts b/components/controller/src/extensions/extensions.module.ts index 92af08ac052..7ef68726dc4 100644 --- a/components/controller/src/extensions/extensions.module.ts +++ b/components/controller/src/extensions/extensions.module.ts @@ -11,6 +11,7 @@ import { ParticipantPluginModule } from './participant/participant-extension.mod import { ChatCoopPluginModule } from './chatcoop/chatcoop-extension.module'; import { OneCoopPluginModule } from './1ccoop/oneccoop-extension.module'; import { ReportsExtensionModule } from './reports/reports-extension.module'; +import { AppsCatalogProxyModule } from './apps-catalog-proxy/apps-catalog-proxy.module'; import { ExtensionDomainModule } from '~/domain/extension/extension-domain.module'; import { GatewayDomainModule } from '~/domain/gateway/gateway-domain.module'; @@ -36,6 +37,7 @@ export class ExtensionsModule { InterCommunicationBridgeModule, OneCoopPluginModule, ReportsExtensionModule, + AppsCatalogProxyModule, ], providers: [], // Экспортируем все модули расширений, чтобы их провайдеры были доступны @@ -53,6 +55,7 @@ export class ExtensionsModule { InterCommunicationBridgeModule, OneCoopPluginModule, ReportsExtensionModule, + AppsCatalogProxyModule, ], }; } diff --git a/components/controller/src/infrastructure/graphql/graphql.module.ts b/components/controller/src/infrastructure/graphql/graphql.module.ts index 1fc6ec5700f..a45669e4d14 100644 --- a/components/controller/src/infrastructure/graphql/graphql.module.ts +++ b/components/controller/src/infrastructure/graphql/graphql.module.ts @@ -1,8 +1,17 @@ // infrastructure/graphql/graphql.module.ts +// +// Epic 10 / Story 10.1 — coopback переключён с ApolloDriver на +// ApolloFederationDriver. Локальная core-схема становится первым subgraph'ом +// федерации; gateway-режим зайдёт отдельным процессом (10.1b / 10.3 orchestrator), +// когда появится первый внешний subgraph (chatcoop и т.д.). +// +// На MVP клиенты не должны заметить переключения: subgraph endpoint на том же +// `/v1/graphql`, federation v2 добавляет лишь служебные `_service { sdl }` и +// `_entities` query. Старые desktop-сессии продолжают работать. import { Global, Module } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; import config from '~/config/config'; -import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; +import { ApolloFederationDriver, ApolloFederationDriverConfig } from '@nestjs/apollo'; import { docDirectiveTransformer } from './directives/doc.directive'; import { GraphQLError, GraphQLFormattedError } from 'graphql'; import { fieldAuthDirectiveTransformer } from './directives/fieldAuth.directive'; @@ -11,10 +20,13 @@ import logger from '~/config/logger'; @Global() @Module({ imports: [ - GraphQLModule.forRoot({ - driver: ApolloDriver, + GraphQLModule.forRoot({ + driver: ApolloFederationDriver, introspection: true, - autoSchemaFile: 'schema.gql', + autoSchemaFile: { + path: 'schema.gql', + federation: 2, + }, sortSchema: true, debug: config.env !== 'production', // context: ({ req }) => req, diff --git a/components/desktop/extensions/developer/install.ts b/components/desktop/extensions/developer/install.ts new file mode 100644 index 00000000000..f0e83fd7185 --- /dev/null +++ b/components/desktop/extensions/developer/install.ts @@ -0,0 +1,80 @@ +// Стол разработчика — Epic 9, story 9.3. +// +// Виден только chairman'у кооператива-оператора каталога (`voskhod` на dev, +// определяется по флагу `is_operator_coop` в Vuex/Pinia store, который +// устанавливается через DesktopWorkspace.grants от controller'а). +// +// V1 содержание: 4 страницы-заглушки, описывающие, что разработчик мог +// бы сделать (мои пакеты, опубликовать релиз, подписчики, pricing). Все +// реальные действия идут REST'ом в ca-admin через chairman-token. На этом +// этапе UI-минимум, реальные mutation'ы — story 9.3.b. + +import { markRaw } from 'vue'; +import type { IWorkspaceConfig } from 'src/shared/lib/types/workspace'; +import { DeveloperMyPackagesPage } from './pages/DeveloperMyPackagesPage'; +import { DeveloperPublishReleasePage } from './pages/DeveloperPublishReleasePage'; +import { DeveloperSubscribersPage } from './pages/DeveloperSubscribersPage'; +import { DeveloperPricingPage } from './pages/DeveloperPricingPage'; + +export default async function (): Promise { + return [{ + workspace: 'developer', + extension_name: 'developer', + title: 'Стол разработчика', + icon: 'fa-solid fa-code', + defaultRoute: 'developer-my-packages', + routes: [ + { + meta: { + title: 'Стол разработчика', + icon: 'fa-solid fa-code', + roles: ['chairman'], + }, + path: '/:coopname/developer', + name: 'developer', + children: [ + { + path: 'packages', + name: 'developer-my-packages', + component: markRaw(DeveloperMyPackagesPage), + meta: { + title: 'Мои пакеты', + icon: 'fa-solid fa-box', + roles: ['chairman'], + }, + }, + { + path: 'publish', + name: 'developer-publish-release', + component: markRaw(DeveloperPublishReleasePage), + meta: { + title: 'Опубликовать релиз', + icon: 'fa-solid fa-upload', + roles: ['chairman'], + }, + }, + { + path: 'subscribers', + name: 'developer-subscribers', + component: markRaw(DeveloperSubscribersPage), + meta: { + title: 'Подписчики', + icon: 'fa-solid fa-users', + roles: ['chairman'], + }, + }, + { + path: 'pricing', + name: 'developer-pricing', + component: markRaw(DeveloperPricingPage), + meta: { + title: 'Pricing', + icon: 'fa-solid fa-rouble-sign', + roles: ['chairman'], + }, + }, + ], + }, + ], + }]; +} diff --git a/components/desktop/extensions/developer/pages/DeveloperMyPackagesPage.ts b/components/desktop/extensions/developer/pages/DeveloperMyPackagesPage.ts new file mode 100644 index 00000000000..5670c96658c --- /dev/null +++ b/components/desktop/extensions/developer/pages/DeveloperMyPackagesPage.ts @@ -0,0 +1 @@ +export { default as DeveloperMyPackagesPage } from './DeveloperMyPackagesPage.vue'; diff --git a/components/desktop/extensions/developer/pages/DeveloperMyPackagesPage.vue b/components/desktop/extensions/developer/pages/DeveloperMyPackagesPage.vue new file mode 100644 index 00000000000..3bf602304c5 --- /dev/null +++ b/components/desktop/extensions/developer/pages/DeveloperMyPackagesPage.vue @@ -0,0 +1,23 @@ + + + diff --git a/components/desktop/extensions/developer/pages/DeveloperPricingPage.ts b/components/desktop/extensions/developer/pages/DeveloperPricingPage.ts new file mode 100644 index 00000000000..05cb44036ff --- /dev/null +++ b/components/desktop/extensions/developer/pages/DeveloperPricingPage.ts @@ -0,0 +1 @@ +export { default as DeveloperPricingPage } from './DeveloperPricingPage.vue'; diff --git a/components/desktop/extensions/developer/pages/DeveloperPricingPage.vue b/components/desktop/extensions/developer/pages/DeveloperPricingPage.vue new file mode 100644 index 00000000000..1aabfc67fcd --- /dev/null +++ b/components/desktop/extensions/developer/pages/DeveloperPricingPage.vue @@ -0,0 +1,29 @@ + + + diff --git a/components/desktop/extensions/developer/pages/DeveloperPublishReleasePage.ts b/components/desktop/extensions/developer/pages/DeveloperPublishReleasePage.ts new file mode 100644 index 00000000000..51863e212b9 --- /dev/null +++ b/components/desktop/extensions/developer/pages/DeveloperPublishReleasePage.ts @@ -0,0 +1 @@ +export { default as DeveloperPublishReleasePage } from './DeveloperPublishReleasePage.vue'; diff --git a/components/desktop/extensions/developer/pages/DeveloperPublishReleasePage.vue b/components/desktop/extensions/developer/pages/DeveloperPublishReleasePage.vue new file mode 100644 index 00000000000..443f29d1088 --- /dev/null +++ b/components/desktop/extensions/developer/pages/DeveloperPublishReleasePage.vue @@ -0,0 +1,26 @@ + + + diff --git a/components/desktop/extensions/developer/pages/DeveloperSubscribersPage.ts b/components/desktop/extensions/developer/pages/DeveloperSubscribersPage.ts new file mode 100644 index 00000000000..d506bc00ea3 --- /dev/null +++ b/components/desktop/extensions/developer/pages/DeveloperSubscribersPage.ts @@ -0,0 +1 @@ +export { default as DeveloperSubscribersPage } from './DeveloperSubscribersPage.vue'; diff --git a/components/desktop/extensions/developer/pages/DeveloperSubscribersPage.vue b/components/desktop/extensions/developer/pages/DeveloperSubscribersPage.vue new file mode 100644 index 00000000000..2827f79d1d4 --- /dev/null +++ b/components/desktop/extensions/developer/pages/DeveloperSubscribersPage.vue @@ -0,0 +1,16 @@ + + + diff --git a/components/desktop/extensions/developer/pages/index.ts b/components/desktop/extensions/developer/pages/index.ts new file mode 100644 index 00000000000..6faea31aadd --- /dev/null +++ b/components/desktop/extensions/developer/pages/index.ts @@ -0,0 +1,4 @@ +export { DeveloperMyPackagesPage } from './DeveloperMyPackagesPage'; +export { DeveloperPublishReleasePage } from './DeveloperPublishReleasePage'; +export { DeveloperSubscribersPage } from './DeveloperSubscribersPage'; +export { DeveloperPricingPage } from './DeveloperPricingPage'; diff --git a/components/desktop/src/entities/Extension/api/index.ts b/components/desktop/src/entities/Extension/api/index.ts index 5c19303458a..3d57ecdd49c 100644 --- a/components/desktop/src/entities/Extension/api/index.ts +++ b/components/desktop/src/entities/Extension/api/index.ts @@ -27,7 +27,24 @@ async function loadExtensionLogs(data?: Queries.Extensions.GetExtensionLogs.IInp return output; } +async function loadAppsCatalogRemotePackages( + page = 1, + pageSize = 50, +): Promise { + const { [Queries.Extensions.AppsCatalogRemotePackages.name]: output } = await client.Query( + Queries.Extensions.AppsCatalogRemotePackages.query, + { + variables: { + page, + pageSize, + }, + }, + ); + return output; +} + export const api ={ loadExtensions, - loadExtensionLogs + loadExtensionLogs, + loadAppsCatalogRemotePackages } diff --git a/components/desktop/src/pages/ExtensionStore/ExtensionsShowcase/ExtensionsShowcase.vue b/components/desktop/src/pages/ExtensionStore/ExtensionsShowcase/ExtensionsShowcase.vue index 2c2302af950..cdcc480904e 100644 --- a/components/desktop/src/pages/ExtensionStore/ExtensionsShowcase/ExtensionsShowcase.vue +++ b/components/desktop/src/pages/ExtensionStore/ExtensionsShowcase/ExtensionsShowcase.vue @@ -1,38 +1,325 @@ diff --git a/components/desktop/src/processes/init-installed-extensions/extensions-registry.ts b/components/desktop/src/processes/init-installed-extensions/extensions-registry.ts index f410687e7f8..731b50e453a 100644 --- a/components/desktop/src/processes/init-installed-extensions/extensions-registry.ts +++ b/components/desktop/src/processes/init-installed-extensions/extensions-registry.ts @@ -8,6 +8,10 @@ import participantInstall from '../../../extensions/participant/install'; import powerupInstall from '../../../extensions/powerup/install'; import sovietInstall from '../../../extensions/soviet/install'; import reportsInstall from '../../../extensions/reports/install'; +// Epic 9 story 9.3: «Стол разработчика» — виден только chairman'у +// кооператива-оператора каталога. Видимость регулируется meta.roles +// в install.ts и (V2) grants от controller'а через DesktopWorkspace. +import developerInstall from '../../../extensions/developer/install'; /** * Единый регистр всех доступных расширений @@ -21,6 +25,7 @@ export const extensionsRegistry: Record Promise r.name === 'base'); + if (baseRoute) { + config.routes.forEach((r: any) => { + const existing = router.getRoutes().find((x) => x.name === r.name); + if (!existing) { + router.addRoute('base', r); + } + }); + } + } + } + } catch (err) { + console.error('[init-installed-extensions] remote pass failed:', err); + } } // Функция для динамической загрузки маршрутов конкретного расширения diff --git a/components/desktop/src/processes/init-installed-extensions/remote-loader.ts b/components/desktop/src/processes/init-installed-extensions/remote-loader.ts new file mode 100644 index 00000000000..fe0c57734b5 --- /dev/null +++ b/components/desktop/src/processes/init-installed-extensions/remote-loader.ts @@ -0,0 +1,198 @@ +// Epic 9 stories 9.4 (skeleton) + 9.4.b (real fetch+eval). +// +// На boot'е desktop'а, после регистрации bundle-extensions, идём в coopback +// за списком пакетов с активной подпиской у текущего кооператива и пробуем +// dynamic-eval'ом распаковать install.js из каждого пакета, чтобы получить +// `IWorkspaceConfig[]` и добавить роуты в router. +// +// MVP (9.4.b) +// - Список remote-пакетов читаем из публичного каталога apps-catalog +// (Queries.Extensions.AppsCatalogRemotePackages) — это все пакеты, +// для пилота demo-app self-sub bypass превращает «всех» в «всё что +// должно быть установлено у кооператива-разработчика». +// - На каждый — REST `GET /v1/apps-catalog/install/:scope/:name` за CJS +// install.js (controller-proxy через ca-admin). HTTP JWT desktop'а. +// - sha256 пока не проверяем (release-таблица ещё не реализована; +// V2 9.4.c добавит manifest+sha256+release-row). +// - eval через `new Function('module','exports','require', code)` — +// контракт `module.exports = { install: () => Promise }`. +// - Try/catch per item: падение одного пакета не валит остальные. +// +// Изоляция: V1 НЕ выполняет код пакетов в sandbox — это будет в story 11.x. +// На MVP исполнение происходит в том же window/contexte что и desktop SPA. + +import axios from 'axios'; +import type { Router } from 'vue-router'; +import { Queries } from '@coopenomics/sdk'; +import { client } from 'src/shared/api/client'; +import { env } from 'src/shared/config'; +import { useGlobalStore } from 'src/shared/store'; +import type { IWorkspaceConfig } from 'src/shared/lib/types/workspace'; + +export interface RemoteExtensionDescriptor { + packageId: string; + scope: string; + name: string; + publisher: string; + version: string | null; + title: string; + description: string; +} + +interface RemoteInstallModule { + install?: () => Promise | IWorkspaceConfig[]; + default?: () => Promise | IWorkspaceConfig[]; +} + +/** + * Распилить `@scope/name` на компоненты. `null` если формат не подходит — + * безымянные / без scope пакеты на MVP не поддерживаем. + */ +function splitPackageId( + packageId: string, +): { scope: string; name: string } | null { + const match = /^@([a-z0-9][a-z0-9-]{0,63})\/([a-z0-9][a-z0-9-]{0,63})$/.exec( + packageId, + ); + if (!match) return null; + return { scope: match[1], name: match[2] }; +} + +/** + * Запросить у coopback список remote-пакетов через apps-catalog-proxy + * (Story 9.5.b). На MVP отдаются ВСЕ опубликованные пакеты — фильтр + * «подписаны / не подписаны» появится в 9.6.c вместе с реальной + * subscription read-port. + */ +export async function fetchInstalledRemotePackages( + coopname: string, +): Promise { + // coopname зарезервирован для 9.6.c (фильтр по подпискам tenant'а); + // сейчас прокси отдаёт публичный каталог без фильтра. + void coopname; + try { + const out = await client.Query( + Queries.Extensions.AppsCatalogRemotePackages.query, + { variables: { page: 1, pageSize: 50 } }, + ); + const list = + out[Queries.Extensions.AppsCatalogRemotePackages.name] ?? []; + const result: RemoteExtensionDescriptor[] = []; + for (const pkg of list) { + const coords = splitPackageId(pkg.packageId); + if (!coords) { + console.warn( + `[remote-loader] пропуск пакета с неподдерживаемым packageId: ${pkg.packageId}`, + ); + continue; + } + result.push({ + packageId: pkg.packageId, + scope: coords.scope, + name: coords.name, + publisher: pkg.publisher, + version: pkg.lastActiveVersion ?? null, + title: pkg.title, + description: pkg.description, + }); + } + return result; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.error('[remote-loader] не удалось получить каталог:', msg); + return []; + } +} + +/** + * Скачать install.js пакета и eval'нуть его в plain CJS-обёртке. + * Возвращает массив workspace'ов, который install() пакета вернул. + * + * На MVP sha256 не валидируется; manifest / release-row появятся в 9.4.c. + */ +export async function loadRemoteExtension( + desc: RemoteExtensionDescriptor, +): Promise { + const { tokens } = useGlobalStore(); + const url = `${env.BACKEND_URL}/v1/apps-catalog/install/${encodeURIComponent( + desc.scope, + )}/${encodeURIComponent(desc.name)}`; + const response = await axios.get(url, { + responseType: 'text', + transformResponse: (data: unknown) => + typeof data === 'string' ? data : String(data), + headers: tokens?.access?.token + ? { Authorization: `Bearer ${tokens.access.token}` } + : {}, + }); + const code: string = response.data; + if (typeof code !== 'string' || code.length === 0) { + throw new Error('empty install.js response'); + } + + const module: { exports: RemoteInstallModule } = { exports: {} }; + const requireNotSupported = (id: string) => { + throw new Error( + `require("${id}") не поддерживается в remote-loader; пакет ${desc.packageId} должен быть CJS без внешних зависимостей`, + ); + }; + + const factory = new Function( + 'module', + 'exports', + 'require', + `"use strict";\n${code}`, + ) as ( + module: { exports: RemoteInstallModule }, + exports: RemoteInstallModule, + require: (id: string) => unknown, + ) => void; + factory(module, module.exports, requireNotSupported); + + const installFn = module.exports.install ?? module.exports.default; + if (typeof installFn !== 'function') { + throw new Error( + `пакет ${desc.packageId} не экспортирует install() — ${typeof installFn}`, + ); + } + const cfgs = await installFn(); + if (!Array.isArray(cfgs)) { + throw new Error( + `install() пакета ${desc.packageId} вернул не массив: ${typeof cfgs}`, + ); + } + return cfgs; +} + +/** + * Один pass remote-loader'а: получить список → загрузить каждый → отдать + * слитый массив workspace'ов вызывателю, который сам впишет их в store/router. + * + * Падение одного пакета не должно валить остальные (try/catch per item). + */ +export async function installRemoteExtensions( + coopname: string, + router: Router, +): Promise { + // router зарезервирован для будущей story 11.x (sandbox-аутоинъекция + // роутов из install.js без возврата в caller); сейчас caller сам + // вписывает routes в `router.addRoute('base', ...)`. + void router; + const descriptors = await fetchInstalledRemotePackages(coopname); + const all: IWorkspaceConfig[] = []; + for (const desc of descriptors) { + try { + const cfgs = await loadRemoteExtension(desc); + all.push(...cfgs); + console.log( + `[remote-loader] установлен ${desc.packageId}${desc.version ? '@' + desc.version : ''} (${cfgs.length} workspace[ов])`, + ); + } catch (err) { + console.error( + `[remote-loader] не удалось установить ${desc.packageId}:`, + err, + ); + } + } + return all; +} diff --git a/components/extension-hello-subgraph/package.json b/components/extension-hello-subgraph/package.json new file mode 100644 index 00000000000..e50cb302847 --- /dev/null +++ b/components/extension-hello-subgraph/package.json @@ -0,0 +1,32 @@ +{ + "name": "@coopenomics/extension-hello-subgraph", + "version": "0.1.0", + "description": "PoC subgraph для smoke-теста @coopenomics/extension-sdk (Epic 10 Story 10.2b). НЕ для прода.", + "private": true, + "main": "dist/main.js", + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node dist/main.js", + "dev": "ts-node-dev --respawn src/main.ts" + }, + "dependencies": { + "@apollo/subgraph": "^2.9.0", + "@coopenomics/extension-sdk": "workspace:*", + "@nestjs/apollo": "^12.2.1", + "@nestjs/common": "^10.4.0", + "@nestjs/core": "^10.4.0", + "@nestjs/graphql": "^12.2.1", + "@nestjs/passport": "^10.0.3", + "@nestjs/platform-express": "^10.4.0", + "graphql": "^16.9.0", + "passport-jwt": "^4.0.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/passport-jwt": "^4.0.1", + "ts-node-dev": "^2.0.0", + "typescript": "^5.4.0" + } +} diff --git a/components/extension-hello-subgraph/src/app.module.ts b/components/extension-hello-subgraph/src/app.module.ts new file mode 100644 index 00000000000..d19ee86e8d2 --- /dev/null +++ b/components/extension-hello-subgraph/src/app.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { + ExtensionAuthModule, + ExtensionFederationModule, + HealthController, + loadExtensionConfig, +} from '@coopenomics/extension-sdk'; +import { HelloResolver } from './hello.resolver'; + +const cfg = loadExtensionConfig(); + +@Module({ + imports: [ + ExtensionFederationModule.forRoot(), + ExtensionAuthModule.forRoot({ secret: cfg.jwtSecret }), + ], + controllers: [HealthController], + providers: [HelloResolver], +}) +export class AppModule {} diff --git a/components/extension-hello-subgraph/src/hello.resolver.ts b/components/extension-hello-subgraph/src/hello.resolver.ts new file mode 100644 index 00000000000..ad289a5a7e5 --- /dev/null +++ b/components/extension-hello-subgraph/src/hello.resolver.ts @@ -0,0 +1,28 @@ +import { Args, Field, ObjectType, Query, Resolver } from '@nestjs/graphql'; + +@ObjectType('HelloResponse') +class HelloResponse { + @Field(() => String) + greeting!: string; + + @Field(() => String) + fromExtension!: string; +} + +/** + * Минимальный subgraph resolver — доказывает что Federation v2 driver + * поднимается на @coopenomics/extension-sdk. Cross-extension entity + * references (SharedCooperator @key + @external + ResolveField) живут + * в реальном chatcoop subgraph'е (story 10.8) — там есть локальная + * сущность ChatThread которая может ссылаться на пайщика. + */ +@Resolver() +export class HelloResolver { + @Query(() => HelloResponse, { name: 'helloFromExtension' }) + hello(@Args('name') name: string): HelloResponse { + return { + greeting: `Здравствуй, ${name}!`, + fromExtension: 'hello-subgraph@0.1.0', + }; + } +} diff --git a/components/extension-hello-subgraph/src/main.ts b/components/extension-hello-subgraph/src/main.ts new file mode 100644 index 00000000000..686e16dbb79 --- /dev/null +++ b/components/extension-hello-subgraph/src/main.ts @@ -0,0 +1,18 @@ +import 'reflect-metadata'; +import { NestFactory } from '@nestjs/core'; +import { loadExtensionConfig } from '@coopenomics/extension-sdk'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const cfg = loadExtensionConfig(); + const app = await NestFactory.create(AppModule); + await app.listen(cfg.subgraphPort); + // eslint-disable-next-line no-console + console.log(`[hello-subgraph] up at :${cfg.subgraphPort} (coopname=${cfg.coopname})`); +} + +bootstrap().catch((err) => { + // eslint-disable-next-line no-console + console.error('[hello-subgraph] failed to bootstrap:', err); + process.exit(1); +}); diff --git a/components/extension-hello-subgraph/tsconfig.json b/components/extension-hello-subgraph/tsconfig.json new file mode 100644 index 00000000000..1b20d563127 --- /dev/null +++ b/components/extension-hello-subgraph/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "declaration": false, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*"] +} diff --git a/components/extension-sdk/README.md b/components/extension-sdk/README.md new file mode 100644 index 00000000000..fd57c25e9fd --- /dev/null +++ b/components/extension-sdk/README.md @@ -0,0 +1,89 @@ +# @coopenomics/extension-sdk + +SDK для разработки backend-расширений Apollo Federation v2 subgraph'ов экосистемы цифрового кооператива. + +См. подробнее: `apps-catalog/docs/epics/E10-federation-runtime.md`. + +## Зачем + +Расширение в платформе цифрового кооператива — это пара артефактов: +- **frontend** (`install.js` bundle, попадает в desktop через remote-loader) +- **backend** (Nest-app, поднимается оркестратором как docker-контейнер и подключается к Apollo Gateway как subgraph) + +Этот пакет задаёт **контракт backend-части**: какие модули обязательны (federation driver + JWT guard + healthcheck), как ссылаться на core-entity (Cooperator/Cooperative/Account), как читать env'ы. + +## Минимальный пример + +```ts +// src/main.ts +import 'reflect-metadata'; +import { NestFactory } from '@nestjs/core'; +import { loadExtensionConfig } from '@coopenomics/extension-sdk'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const cfg = loadExtensionConfig(); + const app = await NestFactory.create(AppModule); + await app.listen(cfg.subgraphPort); +} +bootstrap(); +``` + +```ts +// src/app.module.ts +import { Module } from '@nestjs/common'; +import { + ExtensionFederationModule, + ExtensionAuthModule, + HealthController, + loadExtensionConfig, +} from '@coopenomics/extension-sdk'; +import { ChatResolver } from './chat/chat.resolver'; + +const cfg = loadExtensionConfig(); + +@Module({ + imports: [ + ExtensionFederationModule.forRoot(), + ExtensionAuthModule.forRoot({ secret: cfg.jwtSecret }), + ], + controllers: [HealthController], + providers: [ChatResolver], +}) +export class AppModule {} +``` + +```ts +// src/chat/chat.resolver.ts +import { ResolveField, Resolver, Parent, Query, UseGuards } from '@nestjs/graphql'; +import { SharedCooperator, ExtensionJwtAuthGuard } from '@coopenomics/extension-sdk'; +import { ChatService } from './chat.service'; + +@Resolver(() => SharedCooperator) +export class ChatResolver { + constructor(private readonly chat: ChatService) {} + + @ResolveField(() => [String]) + @UseGuards(ExtensionJwtAuthGuard) + async chatThreads(@Parent() cooperator: SharedCooperator): Promise { + return this.chat.listThreads(cooperator.username); + } +} +``` + +После build'а: +- `docker build` через `templates/Dockerfile` +- `docker push registry.coopenomics.world//:` +- `npm publish` install.js bundle +- on-chain `apps::regpkg` через voskhod operator +- orchestrator подхватит после approve + +## ENV-переменные + +| Переменная | Описание | Обязательная | +|---|---|---| +| `SUBGRAPH_PORT` | Порт subgraph'а внутри контейнера | ✓ | +| `JWT_SECRET` | Тот же что у core'а (выдаёт core, проверяет subgraph) | ✓ | +| `COOPNAME` | Account name кооператива-tenant'а | ✓ | +| `DATABASE_URL` | Postgres connection string (если расширение пишет в БД) | | +| `CORE_GRAPHQL_URL` | Endpoint core subgraph'а (для cross-extension queries) | | diff --git a/components/extension-sdk/package.json b/components/extension-sdk/package.json new file mode 100644 index 00000000000..8220aba97ae --- /dev/null +++ b/components/extension-sdk/package.json @@ -0,0 +1,43 @@ +{ + "name": "@coopenomics/extension-sdk", + "version": "0.1.0", + "description": "SDK для разработки backend-расширений Apollo Federation v2 subgraph'ов экосистемы кооперативов", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "templates", + "README.md" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "clean": "rm -rf dist" + }, + "peerDependencies": { + "@apollo/subgraph": "^2.0.0", + "@nestjs/apollo": "^12.0.0", + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/graphql": "^12.0.0", + "@nestjs/passport": "^10.0.0", + "graphql": "^16.0.0", + "passport-jwt": "^4.0.0", + "reflect-metadata": "^0.2.0", + "rxjs": "^7.0.0" + }, + "devDependencies": { + "@apollo/subgraph": "^2.9.0", + "@nestjs/apollo": "^12.2.1", + "@nestjs/common": "^10.4.0", + "@nestjs/core": "^10.4.0", + "@nestjs/graphql": "^12.2.1", + "@nestjs/passport": "^10.0.3", + "@types/passport-jwt": "^4", + "graphql": "^16.9.0", + "passport-jwt": "^4.0.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "typescript": "^5.4.0", + "unbuild": "^2.0.0" + } +} diff --git a/components/extension-sdk/src/auth/extension-auth.module.ts b/components/extension-sdk/src/auth/extension-auth.module.ts new file mode 100644 index 00000000000..0c8919ba219 --- /dev/null +++ b/components/extension-sdk/src/auth/extension-auth.module.ts @@ -0,0 +1,34 @@ +import { DynamicModule, Module } from '@nestjs/common'; +import { PassportModule } from '@nestjs/passport'; +import { ExtensionJwtStrategy } from './extension-jwt.strategy'; +import { ExtensionJwtAuthGuard } from './extension-jwt.guard'; + +export interface ExtensionAuthOptions { + secret: string; +} + +/** + * Auth-модуль расширения. Подключается в корневой AppModule: + * + * imports: [ExtensionAuthModule.forRoot({ secret: env.JWT_SECRET })] + * + * Регистрирует passport-jwt стратегию + guard. Импортируйте `ExtensionJwtAuthGuard` + * в resolver'ах через `@UseGuards`. + */ +@Module({}) +export class ExtensionAuthModule { + static forRoot(options: ExtensionAuthOptions): DynamicModule { + return { + module: ExtensionAuthModule, + imports: [PassportModule.register({ defaultStrategy: 'jwt' })], + providers: [ + { + provide: ExtensionJwtStrategy, + useFactory: () => new ExtensionJwtStrategy({ secret: options.secret }), + }, + ExtensionJwtAuthGuard, + ], + exports: [PassportModule, ExtensionJwtStrategy, ExtensionJwtAuthGuard], + }; + } +} diff --git a/components/extension-sdk/src/auth/extension-jwt.guard.ts b/components/extension-sdk/src/auth/extension-jwt.guard.ts new file mode 100644 index 00000000000..565346f795f --- /dev/null +++ b/components/extension-sdk/src/auth/extension-jwt.guard.ts @@ -0,0 +1,19 @@ +import { Injectable, ExecutionContext } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { GqlExecutionContext } from '@nestjs/graphql'; + +/** + * Guard для GraphQL resolver'ов расширения. Достаёт Bearer-токен из + * Authorization-header'а, проверяет подпись через ExtensionJwtStrategy. + * + * Используется через `@UseGuards(ExtensionJwtAuthGuard)` на resolver'е / + * на отдельном поле. Для HTTP-controller'а (например `/_health`) — НЕ нужен; + * healthcheck оставляем публичным. + */ +@Injectable() +export class ExtensionJwtAuthGuard extends AuthGuard('jwt') { + getRequest(context: ExecutionContext) { + const gqlCtx = GqlExecutionContext.create(context); + return gqlCtx.getContext().req; + } +} diff --git a/components/extension-sdk/src/auth/extension-jwt.strategy.ts b/components/extension-sdk/src/auth/extension-jwt.strategy.ts new file mode 100644 index 00000000000..fc4489147f8 --- /dev/null +++ b/components/extension-sdk/src/auth/extension-jwt.strategy.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy, ExtractJwt } from 'passport-jwt'; + +export interface ExtensionJwtPayload { + sub: string; + username: string; + coopname?: string; + type?: string; + scope?: string[]; + iat?: number; + exp?: number; +} + +export interface ExtensionJwtStrategyOptions { + secret: string; +} + +/** + * JWT-стратегия для расширения. Validate'ит токен, который Apollo Gateway + * forward'ит в `Authorization: Bearer …` заголовке. Не выдаёт токены — это + * делает core-coopback; расширение только проверяет подпись и срок. + * + * **Контракт payload'а** — фиксирован core'ом: `sub`, `username`, `coopname`, + * `type`. Поле `scope: string[]` опционально и используется для apps-catalog + * подписок (`@scope("@voskhod/chatcoop")`). + * + * Поле `secret` приходит из env (`JWT_SECRET`) — обязательно совпадает с + * core'овым. Никакой выдачи токенов на расширении. + */ +@Injectable() +export class ExtensionJwtStrategy extends PassportStrategy(Strategy, 'jwt') { + constructor(options: ExtensionJwtStrategyOptions) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: options.secret, + }); + } + + async validate(payload: ExtensionJwtPayload): Promise { + return payload; + } +} diff --git a/components/extension-sdk/src/config/extension-config.ts b/components/extension-sdk/src/config/extension-config.ts new file mode 100644 index 00000000000..e07fd0dc808 --- /dev/null +++ b/components/extension-sdk/src/config/extension-config.ts @@ -0,0 +1,49 @@ +/** + * Стандартный набор env-переменных расширения. + * + * SUBGRAPH_PORT — порт, на котором subgraph слушает (например 3001) + * JWT_SECRET — общий секрет с core'ом для валидации JWT + * COOPNAME — account name кооператива-tenant'а + * DATABASE_URL — connection string Postgres tenant'а (опц.) + * CORE_GRAPHQL_URL — endpoint core subgraph'а для inter-extension fetch (опц.) + * + * Расширение читает их через `loadExtensionConfig()` на bootstrap; падает + * сразу же если обязательное поле не задано — fail-fast, чтобы orchestrator + * увидел unhealthy и не зарегистрировал subgraph. + */ +export interface ExtensionConfig { + subgraphPort: number; + jwtSecret: string; + coopname: string; + databaseUrl?: string; + coreGraphqlUrl?: string; +} + +export class ExtensionConfigError extends Error { + constructor(message: string) { + super(`[extension-sdk] ${message}`); + this.name = 'ExtensionConfigError'; + } +} + +export function loadExtensionConfig(env: NodeJS.ProcessEnv = process.env): ExtensionConfig { + const port = Number(env.SUBGRAPH_PORT); + if (!Number.isFinite(port) || port <= 0) { + throw new ExtensionConfigError('SUBGRAPH_PORT не задан или некорректен'); + } + const jwtSecret = env.JWT_SECRET; + if (!jwtSecret) { + throw new ExtensionConfigError('JWT_SECRET не задан'); + } + const coopname = env.COOPNAME; + if (!coopname) { + throw new ExtensionConfigError('COOPNAME не задан'); + } + return { + subgraphPort: port, + jwtSecret, + coopname, + databaseUrl: env.DATABASE_URL, + coreGraphqlUrl: env.CORE_GRAPHQL_URL, + }; +} diff --git a/components/extension-sdk/src/entities/shared-account.entity.ts b/components/extension-sdk/src/entities/shared-account.entity.ts new file mode 100644 index 00000000000..80f5185d81d --- /dev/null +++ b/components/extension-sdk/src/entities/shared-account.entity.ts @@ -0,0 +1,16 @@ +import { Directive, Field, ObjectType } from '@nestjs/graphql'; + +/** + * Federation v2 ссылка на core-entity `Account` (общий аккаунт — пайщик + * или кооператив; различие — флагом is_cooperative на стороне core). + * Расширения обычно работают через `Cooperator` или `Cooperative`, но + * иногда нужен общий `Account` (например marketplace может ссылаться на + * поставщика, который может быть как пайщиком, так и кооперативом). + */ +@ObjectType('Account') +@Directive('@key(fields: "username")') +export class SharedAccount { + @Field(() => String, { description: 'Account name (EOSIO username)' }) + @Directive('@external') + username!: string; +} diff --git a/components/extension-sdk/src/entities/shared-cooperative.entity.ts b/components/extension-sdk/src/entities/shared-cooperative.entity.ts new file mode 100644 index 00000000000..c3f7aa2e0a5 --- /dev/null +++ b/components/extension-sdk/src/entities/shared-cooperative.entity.ts @@ -0,0 +1,13 @@ +import { Directive, Field, ObjectType } from '@nestjs/graphql'; + +/** + * Federation v2 ссылка на core-entity `Cooperative` (кооператив / DC). + * `username` — EOSIO account name кооператива (например `voskhod`, `partner1`). + */ +@ObjectType('Cooperative') +@Directive('@key(fields: "username")') +export class SharedCooperative { + @Field(() => String, { description: 'Account name кооператива' }) + @Directive('@external') + username!: string; +} diff --git a/components/extension-sdk/src/entities/shared-cooperator.entity.ts b/components/extension-sdk/src/entities/shared-cooperator.entity.ts new file mode 100644 index 00000000000..3d1525e4882 --- /dev/null +++ b/components/extension-sdk/src/entities/shared-cooperator.entity.ts @@ -0,0 +1,26 @@ +import { Directive, Field, ObjectType } from '@nestjs/graphql'; + +/** + * Federation v2 ссылка на core-entity `Cooperator` (пайщик). + * + * Расширения, которым нужно расширить пайщика своими полями, **не должны** + * описывать `Cooperator` целиком; они импортируют этот класс и добавляют + * к нему свои поля через extend pattern на стороне core. Здесь — только + * stub с `@key(fields: "username")` и `@external` на ключевом поле. + * + * @ObjectType('Cooperator') + * @Directive('@extends') + * export class CooperatorWithChat extends SharedCooperator { + * @Field(() => [ChatThread]) chatThreads!: ChatThread[]; + * } + * + * Контракт идентичности: `username` — EOSIO account name пайщика (≤13 символов, + * a-z, 1-5, .). Этот же ключ используется во всех core-таблицах. + */ +@ObjectType('Cooperator') +@Directive('@key(fields: "username")') +export class SharedCooperator { + @Field(() => String, { description: 'Account name пайщика (EOSIO username)' }) + @Directive('@external') + username!: string; +} diff --git a/components/extension-sdk/src/federation/federation.module.ts b/components/extension-sdk/src/federation/federation.module.ts new file mode 100644 index 00000000000..9fcc797fffc --- /dev/null +++ b/components/extension-sdk/src/federation/federation.module.ts @@ -0,0 +1,59 @@ +import { DynamicModule, Module } from '@nestjs/common'; +import { GraphQLModule } from '@nestjs/graphql'; +import { ApolloFederationDriver, ApolloFederationDriverConfig } from '@nestjs/apollo'; + +/** + * Опции для базового федеративного subgraph'а расширения. + * + * @property path — путь endpoint'а GraphQL (по умолчанию `/v1/graphql`). + * @property schemaFile — куда писать сгенерированную SDL (`schema.gql` по умолчанию). + * @property introspection — оставлять true даже в проде: gateway пользуется + * introspection'ом для пересборки supergraph'а. + * @property playground — Apollo Sandbox в development. + */ +export interface ExtensionFederationOptions { + path?: string; + schemaFile?: string; + introspection?: boolean; + playground?: boolean; +} + +/** + * Стандартный модуль федеративного subgraph'а расширения. + * + * Подключается в `imports` корневого `AppModule` расширения: + * + * imports: [ExtensionFederationModule.forRoot({ path: '/v1/graphql' })] + * + * Драйвер `ApolloFederationDriver` + `autoSchemaFile.federation: 2` дают + * subgraph совместимый с Apollo Gateway. На стороне расширения никаких + * настроек федерации больше делать не надо — `@key/@requires/@external` + * объявляются прямо в @ObjectType классах через @Directive. + */ +@Module({}) +export class ExtensionFederationModule { + static forRoot(options: ExtensionFederationOptions = {}): DynamicModule { + const path = options.path ?? '/v1/graphql'; + const schemaFile = options.schemaFile ?? 'schema.gql'; + const introspection = options.introspection ?? true; + const playground = options.playground ?? false; + + return { + module: ExtensionFederationModule, + imports: [ + GraphQLModule.forRoot({ + driver: ApolloFederationDriver, + path, + introspection, + playground, + autoSchemaFile: { + path: schemaFile, + federation: 2, + }, + sortSchema: true, + }), + ], + exports: [GraphQLModule], + }; + } +} diff --git a/components/extension-sdk/src/health/health.controller.ts b/components/extension-sdk/src/health/health.controller.ts new file mode 100644 index 00000000000..cf34987dc70 --- /dev/null +++ b/components/extension-sdk/src/health/health.controller.ts @@ -0,0 +1,28 @@ +import { Controller, Get } from '@nestjs/common'; + +/** + * Стандартный healthcheck endpoint расширения. + * + * Orchestrator опрашивает этот endpoint после `docker compose up -d`; + * только после `200 OK` пишет subgraph URL в registry и триггерит + * gateway recompose. + * + * Использование: + * + * @Module({ controllers: [HealthController], ... }) + * + * Endpoint: `GET /_health` → `{ status: 'ok', version, uptime }`. + */ +@Controller('_health') +export class HealthController { + private readonly startedAt = Date.now(); + + @Get() + health(): { status: 'ok'; version: string; uptimeSeconds: number } { + return { + status: 'ok', + version: process.env.npm_package_version ?? 'unknown', + uptimeSeconds: Math.floor((Date.now() - this.startedAt) / 1000), + }; + } +} diff --git a/components/extension-sdk/src/index.ts b/components/extension-sdk/src/index.ts new file mode 100644 index 00000000000..198461fcfda --- /dev/null +++ b/components/extension-sdk/src/index.ts @@ -0,0 +1,44 @@ +/** + * @coopenomics/extension-sdk + * + * SDK для разработки backend-расширений Apollo Federation v2 subgraph'ов + * экосистемы цифрового кооператива. + * + * Контракт: + * - расширение = Nest-приложение, использующее `ExtensionFederationModule` + * и `ExtensionAuthModule` в корневом AppModule. + * - schema собирается через autoSchemaFile { federation: 2 }. + * - cross-extension references на core entity (Cooperator, Cooperative, + * Account) — через `SharedCooperator/SharedCooperative/SharedAccount` + * stub'ы (`@key + @external`). + * - JWT валидируется тем же секретом что и core; gateway forward'ит + * Authorization-header в subgraph. + * - healthcheck `GET /_health` обязателен — orchestrator опрашивает. + * + * См. подробнее: docs/epics/E10-federation-runtime.md в репо apps-catalog + * и Architecture v3 в blago. + */ + +export { + ExtensionFederationModule, + type ExtensionFederationOptions, +} from './federation/federation.module'; + +export { + ExtensionAuthModule, + type ExtensionAuthOptions, +} from './auth/extension-auth.module'; +export { ExtensionJwtStrategy, type ExtensionJwtPayload } from './auth/extension-jwt.strategy'; +export { ExtensionJwtAuthGuard } from './auth/extension-jwt.guard'; + +export { SharedCooperator } from './entities/shared-cooperator.entity'; +export { SharedCooperative } from './entities/shared-cooperative.entity'; +export { SharedAccount } from './entities/shared-account.entity'; + +export { HealthController } from './health/health.controller'; + +export { + loadExtensionConfig, + ExtensionConfigError, + type ExtensionConfig, +} from './config/extension-config'; diff --git a/components/extension-sdk/templates/Dockerfile b/components/extension-sdk/templates/Dockerfile new file mode 100644 index 00000000000..be3b12304b9 --- /dev/null +++ b/components/extension-sdk/templates/Dockerfile @@ -0,0 +1,40 @@ +# Multi-stage Dockerfile template для backend-расширения. +# +# Расширение должно скопировать этот файл к себе в репозиторий и кастомизировать +# при необходимости (например, добавить native deps). Базовый pattern: +# 1. install deps (cached layer) +# 2. build TypeScript → dist +# 3. runtime образ — только prod-deps + dist + healthcheck wget + +# ───── builder ────── +FROM node:20-alpine AS builder + +WORKDIR /app + +RUN corepack enable && corepack prepare pnpm@9.15.9 --activate + +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +COPY . . +RUN pnpm run build + +# ───── runtime ────── +FROM node:20-alpine AS runtime + +WORKDIR /app + +RUN corepack enable && corepack prepare pnpm@9.15.9 --activate + +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile --prod && pnpm store prune + +COPY --from=builder /app/dist ./dist + +EXPOSE 3001 + +# Orchestrator опрашивает GET /_health после up -d +HEALTHCHECK --interval=10s --timeout=3s --start-period=20s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost:3001/_health || exit 1 + +CMD ["node", "dist/main.js"] diff --git a/components/extension-sdk/templates/docker-compose.snippet.yaml b/components/extension-sdk/templates/docker-compose.snippet.yaml new file mode 100644 index 00000000000..093cf51c654 --- /dev/null +++ b/components/extension-sdk/templates/docker-compose.snippet.yaml @@ -0,0 +1,30 @@ +# Snippet, который orchestrator добавляет в общий docker-compose.yaml DC. +# +# Имя service'а = `ext-` (например `ext-chatcoop`). +# Image берётся из manifest пакета (`coopenomics.backend.image`). +# Порт subgraph'а внутри docker-network проброшен НЕ на хост — gateway +# обращается к нему через service name + internal port. + +services: + ext-${EXT_NAME}: + image: ${EXT_IMAGE} + container_name: ext-${EXT_NAME} + restart: unless-stopped + environment: + SUBGRAPH_PORT: ${EXT_PORT} + JWT_SECRET: ${JWT_SECRET} + COOPNAME: ${COOPNAME} + DATABASE_URL: ${EXT_DATABASE_URL} + CORE_GRAPHQL_URL: http://coopback:3000/v1/graphql + networks: + - coopnet + healthcheck: + test: ['CMD', 'wget', '--quiet', '--tries=1', '--spider', 'http://localhost:${EXT_PORT}/_health'] + interval: 10s + timeout: 3s + retries: 3 + start_period: 20s + +networks: + coopnet: + external: true diff --git a/components/extension-sdk/tsconfig.json b/components/extension-sdk/tsconfig.json new file mode 100644 index 00000000000..6110c6e64dc --- /dev/null +++ b/components/extension-sdk/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "Node", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "declaration": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*"] +} diff --git a/components/orchestrator/jest.config.js b/components/orchestrator/jest.config.js new file mode 100644 index 00000000000..40d4a49eeeb --- /dev/null +++ b/components/orchestrator/jest.config.js @@ -0,0 +1,15 @@ +/** + * @fileoverview Jest конфиг для orchestrator. + * + * Юнит-тесты лежат рядом с код-файлами (`*.spec.ts`). Все внешние + * зависимости (docker / http / postgres) подменяются ин-мемори + * фейками — testEnvironment 'node', без shell. + */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + rootDir: 'src', + testRegex: '.*\\.spec\\.ts$', + collectCoverageFrom: ['**/*.ts', '!**/*.spec.ts', '!main.ts'], + moduleFileExtensions: ['ts', 'js', 'json'], +}; diff --git a/components/orchestrator/package.json b/components/orchestrator/package.json new file mode 100644 index 00000000000..7e805e5c934 --- /dev/null +++ b/components/orchestrator/package.json @@ -0,0 +1,35 @@ +{ + "name": "@coopenomics/orchestrator", + "version": "0.1.0", + "description": "Per-tenant сервис: Apollo Federation Gateway + orchestrator расширений (docker compose + on-chain auto-update). Epic 10.", + "private": true, + "main": "dist/main.js", + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node dist/main.js", + "dev": "ts-node-dev --respawn src/main.ts", + "test": "jest --config jest.config.js" + }, + "dependencies": { + "@apollo/gateway": "^2.9.0", + "@nestjs/apollo": "^12.2.1", + "@nestjs/common": "^10.4.0", + "@nestjs/core": "^10.4.0", + "@nestjs/graphql": "^12.2.1", + "@nestjs/platform-express": "^10.4.0", + "@nestjs/typeorm": "^10.0.2", + "graphql": "^16.9.0", + "pg": "^8.13.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "typeorm": "^0.3.20" + }, + "devDependencies": { + "@types/jest": "^29.5.0", + "@types/node": "^20.0.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.0", + "ts-node-dev": "^2.0.0", + "typescript": "^5.4.0" + } +} diff --git a/components/orchestrator/src/app.module.ts b/components/orchestrator/src/app.module.ts new file mode 100644 index 00000000000..8b53e25af8f --- /dev/null +++ b/components/orchestrator/src/app.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { GatewayModule } from './gateway/gateway.module'; +import { OrchestratorModule } from './orchestrator/orchestrator.module'; +import { loadAppConfig } from './config/app-config'; +import { SubgraphRegistryEntity } from './gateway/subgraph-registry.entity'; + +const cfg = loadAppConfig(); + +@Module({ + imports: [ + TypeOrmModule.forRoot({ + type: 'postgres', + url: cfg.postgresUrl, + entities: [SubgraphRegistryEntity], + // На bootstrap безопасно создаём таблицу — registry — единственный + // объект, который реально нужен gateway. Прод схему мигрирует + // отдельная миграция при PR на интеграцию с deploy-pipeline. + synchronize: true, + }), + GatewayModule, + OrchestratorModule, + ], +}) +export class AppModule {} diff --git a/components/orchestrator/src/config/app-config.ts b/components/orchestrator/src/config/app-config.ts new file mode 100644 index 00000000000..1efb3daf51c --- /dev/null +++ b/components/orchestrator/src/config/app-config.ts @@ -0,0 +1,53 @@ +/** + * ENV-конфигурация orchestrator (per-tenant). + * + * Все обязательные поля fail-fast валидируются на bootstrap. Если orchestrator + * стартует и не находит coopback subgraph URL — он не сможет ничего + * скомпоновать; полагаться на default'ы для tenant'а опасно. + */ +export interface AppConfig { + port: number; + postgresUrl: string; + coreSubgraphUrl: string; + coopname: string; + jwtSecret: string; + compositionPollIntervalMs: number; +} + +export class AppConfigError extends Error { + constructor(msg: string) { + super(`[orchestrator] ${msg}`); + this.name = 'AppConfigError'; + } +} + +export function loadAppConfig(env: NodeJS.ProcessEnv = process.env): AppConfig { + const port = Number(env.GATEWAY_PORT ?? 4000); + if (!Number.isFinite(port) || port <= 0) { + throw new AppConfigError('GATEWAY_PORT некорректен'); + } + const postgresUrl = env.DATABASE_URL; + if (!postgresUrl) { + throw new AppConfigError('DATABASE_URL не задан'); + } + const coreSubgraphUrl = env.CORE_SUBGRAPH_URL; + if (!coreSubgraphUrl) { + throw new AppConfigError('CORE_SUBGRAPH_URL не задан (например http://coopback:3000/v1/graphql)'); + } + const coopname = env.COOPNAME; + if (!coopname) { + throw new AppConfigError('COOPNAME не задан'); + } + const jwtSecret = env.JWT_SECRET; + if (!jwtSecret) { + throw new AppConfigError('JWT_SECRET не задан'); + } + return { + port, + postgresUrl, + coreSubgraphUrl, + coopname, + jwtSecret, + compositionPollIntervalMs: Number(env.COMPOSITION_POLL_INTERVAL_MS ?? 10000), + }; +} diff --git a/components/orchestrator/src/gateway/gateway.module.ts b/components/orchestrator/src/gateway/gateway.module.ts new file mode 100644 index 00000000000..9d0fde99c61 --- /dev/null +++ b/components/orchestrator/src/gateway/gateway.module.ts @@ -0,0 +1,75 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { GraphQLModule } from '@nestjs/graphql'; +import { ApolloGatewayDriver, ApolloGatewayDriverConfig } from '@nestjs/apollo'; +import { IntrospectAndCompose, RemoteGraphQLDataSource } from '@apollo/gateway'; +import { SubgraphRegistryEntity } from './subgraph-registry.entity'; +import { SubgraphRegistryService } from './subgraph-registry.service'; + +/** + * Apollo Federation Gateway tenant'а. + * + * IntrospectAndCompose читает active subgraph'ы из Postgres registry с + * periodic polling. Gateway не перезагружается при появлении нового + * subgraph'а — он re-introspect'ит и swap'ает supergraph in-memory. + * + * JWT forwarding: gateway получает Authorization-header от desktop, + * прокидывает в каждый subgraph через willSendRequest. Subgraph'ы + * валидируют JWT тем же секретом через @coopenomics/extension-sdk. + */ +@Module({ + imports: [ + TypeOrmModule.forFeature([SubgraphRegistryEntity]), + GraphQLModule.forRootAsync({ + driver: ApolloGatewayDriver, + imports: [TypeOrmModule.forFeature([SubgraphRegistryEntity])], + inject: [SubgraphRegistryService], + useFactory: async (registry: SubgraphRegistryService) => { + // MVP-режим: subgraphs читаются из Postgres registry один раз на + // bootstrap. IntrospectAndCompose делает polling existing endpoints + // и автоматически re-compose'ит если у subgraph'а изменилась схема. + // + // Но: добавление НОВОГО subgraph'а (POST /v1/internal/extensions/install) + // потребует рестарта контейнера orchestrator. На MVP это + // ОК — gateway маленький, warmup ~5 сек, делается orchestrator-pipeline + // при первом install'е приложения (Story 10.4). + // + // Полностью dynamic supergraph composition (без рестарта) — отдельная + // story 10.3b: custom SupergraphManager, который опрашивает registry + // на каждый poll и собирает supergraphSdl.fetch(). + const subgraphs = await registry.listForCompose(); + if (subgraphs.length === 0) { + throw new Error('[gateway] subgraph registry пуст — orchestrator должен seed core до boot gateway'); + } + return { + server: { + path: '/v1/graphql', + introspection: true, + context: ({ req }: { req: unknown }) => ({ req }), + }, + gateway: { + supergraphSdl: new IntrospectAndCompose({ + subgraphs, + pollIntervalInMs: 10000, + }), + buildService({ url }) { + return new RemoteGraphQLDataSource({ + url, + willSendRequest({ request, context }) { + const incoming = (context as { req?: { headers?: Record } })?.req?.headers ?? {}; + const auth = incoming.authorization ?? incoming.Authorization; + if (auth && request.http) { + request.http.headers.set('authorization', auth); + } + }, + }); + }, + }, + }; + }, + }), + ], + providers: [SubgraphRegistryService], + exports: [SubgraphRegistryService, TypeOrmModule], +}) +export class GatewayModule {} diff --git a/components/orchestrator/src/gateway/subgraph-registry.entity.ts b/components/orchestrator/src/gateway/subgraph-registry.entity.ts new file mode 100644 index 00000000000..93bd3d7a920 --- /dev/null +++ b/components/orchestrator/src/gateway/subgraph-registry.entity.ts @@ -0,0 +1,37 @@ +import { Entity, PrimaryColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +/** + * Регистр subgraph'ов tenant'а. Apollo Gateway периодически читает active=true + * запись и пересобирает supergraph. + * + * Стартовая запись для core-subgraph'а (coopback) добавляется на bootstrap + * orchestrator'а; остальные приезжают через POST /v1/internal/extensions/install. + * + * package_id = '@scope/name' (для core — sentinel 'core@coopback'). + * version = семантическая версия image'а (для core — 'monolith'). + * url = http://service-name:port/v1/graphql — резолвится docker DNS. + * active = маршрутизируется ли в supergraph (false = выпиленный extension). + */ +@Entity('subgraph_registry') +export class SubgraphRegistryEntity { + @PrimaryColumn({ type: 'varchar', length: 128, comment: 'package_id (@scope/name) или core sentinel' }) + packageId!: string; + + @Column({ type: 'varchar', length: 32 }) + version!: string; + + @Column({ type: 'varchar', length: 512 }) + url!: string; + + @Column({ type: 'boolean', default: true }) + active!: boolean; + + @Column({ type: 'varchar', length: 16, default: 'unknown', comment: 'last healthcheck result' }) + healthStatus!: string; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} diff --git a/components/orchestrator/src/gateway/subgraph-registry.service.ts b/components/orchestrator/src/gateway/subgraph-registry.service.ts new file mode 100644 index 00000000000..9d926917c50 --- /dev/null +++ b/components/orchestrator/src/gateway/subgraph-registry.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { SubgraphRegistryEntity } from './subgraph-registry.entity'; + +export interface SubgraphDescriptor { + name: string; + url: string; +} + +/** + * Чтение/запись регистра subgraph'ов. Используется и Apollo Gateway'ем + * (для compose), и orchestrator REST-эндпоинтами (install/uninstall/refresh). + */ +@Injectable() +export class SubgraphRegistryService { + constructor( + @InjectRepository(SubgraphRegistryEntity) + private readonly repo: Repository, + ) {} + + /** + * Список active subgraph'ов в формате который ждёт Apollo Gateway's + * IntrospectAndCompose. На bootstrap'е orchestrator'а должна быть как + * минимум одна запись (core), иначе gateway не скомпонует ничего. + */ + async listForCompose(): Promise { + const rows = await this.repo.find({ where: { active: true } }); + return rows.map((r) => ({ name: r.packageId, url: r.url })); + } + + async upsert(packageId: string, version: string, url: string): Promise { + await this.repo.upsert( + { + packageId, + version, + url, + active: true, + healthStatus: 'unknown', + }, + ['packageId'], + ); + } + + async deactivate(packageId: string): Promise { + await this.repo.update({ packageId }, { active: false }); + } + + async setHealthStatus(packageId: string, healthStatus: string): Promise { + await this.repo.update({ packageId }, { healthStatus }); + } +} diff --git a/components/orchestrator/src/main.ts b/components/orchestrator/src/main.ts new file mode 100644 index 00000000000..9f6e555e16c --- /dev/null +++ b/components/orchestrator/src/main.ts @@ -0,0 +1,54 @@ +import 'reflect-metadata'; +import { NestFactory } from '@nestjs/core'; +import { DataSource } from 'typeorm'; +import { AppModule } from './app.module'; +import { loadAppConfig } from './config/app-config'; +import { SubgraphRegistryEntity } from './gateway/subgraph-registry.entity'; + +/** + * Перед стартом Apollo Gateway seed'им запись core-subgraph'а в registry, + * иначе IntrospectAndCompose упадёт с "пустой registry". + * + * Bootstrap делается в отдельной DataSource (минуя Nest DI), чтобы быть + * уверенным что seed произошёл ДО запуска GraphQLModule.forRootAsync. + */ +async function seedCoreSubgraph(cfg: ReturnType) { + const ds = new DataSource({ + type: 'postgres', + url: cfg.postgresUrl, + entities: [SubgraphRegistryEntity], + synchronize: true, + }); + await ds.initialize(); + const repo = ds.getRepository(SubgraphRegistryEntity); + await repo.upsert( + { + packageId: 'core@coopback', + version: 'monolith', + url: cfg.coreSubgraphUrl, + active: true, + healthStatus: 'unknown', + }, + ['packageId'], + ); + await ds.destroy(); +} + +async function bootstrap() { + const cfg = loadAppConfig(); + await seedCoreSubgraph(cfg); + + const app = await NestFactory.create(AppModule); + await app.listen(cfg.port); + // eslint-disable-next-line no-console + console.log( + `[orchestrator] up at :${cfg.port} ` + + `(coopname=${cfg.coopname}, core=${cfg.coreSubgraphUrl})`, + ); +} + +bootstrap().catch((err) => { + // eslint-disable-next-line no-console + console.error('[orchestrator] failed to bootstrap:', err); + process.exit(1); +}); diff --git a/components/orchestrator/src/orchestrator/composition.controller.ts b/components/orchestrator/src/orchestrator/composition.controller.ts new file mode 100644 index 00000000000..a9763465e92 --- /dev/null +++ b/components/orchestrator/src/orchestrator/composition.controller.ts @@ -0,0 +1,43 @@ +import { Controller, Get, Post, HttpCode } from '@nestjs/common'; +import { SubgraphRegistryService } from '../gateway/subgraph-registry.service'; + +/** + * Endpoint'ы для статуса и force-recompose supergraph'а. + * + * IntrospectAndCompose polling каждые 10 сек подхватит изменения registry + * и без force-refresh'а; force-endpoint полезен для интеграционных тестов + * и для быстрого reaction'а после install (когда не хочется ждать polling). + * + * **Важно:** реальный recompose делает сам Apollo Gateway по polling'у; этот + * endpoint только возвращает текущее состояние registry. Принудительная + * пересборка сейчас выполняется через изменение registry + ожидание + * polling'а (≤10 сек). Если позже понадобится hard-trigger — потребуется + * GraphQL Plugin для inline-вызова supergraphSdl.fetch(). + */ +@Controller('v1/internal/composition') +export class CompositionController { + constructor(private readonly registry: SubgraphRegistryService) {} + + @Get('status') + async status() { + const list = await this.registry.listForCompose(); + return { + subgraphCount: list.length, + subgraphs: list, + }; + } + + @Post('refresh') + @HttpCode(202) + async refresh() { + // Триггер сейчас implicit — IntrospectAndCompose polling. + // Возвращаем 202 Accepted чтобы orchestrator install pipeline мог + // вызывать этот endpoint и продолжать без ожидания. + const list = await this.registry.listForCompose(); + return { + accepted: true, + currentSubgraphCount: list.length, + pollIntervalMs: 10000, + }; + } +} diff --git a/components/orchestrator/src/orchestrator/docker-runner.impl.ts b/components/orchestrator/src/orchestrator/docker-runner.impl.ts new file mode 100644 index 00000000000..ee13dcf1dce --- /dev/null +++ b/components/orchestrator/src/orchestrator/docker-runner.impl.ts @@ -0,0 +1,75 @@ +/** + * @fileoverview Shell-out импл `DockerRunnerPort`. Зовёт `docker` CLI + * через `child_process.execFile` (без shell-инъекции через `exec`). + * + * Зачем CLI, а не dockerode/socket: orchestrator деплоится в + * tenant-окружении, где доступ к docker.sock даётся compose-mount'ом; + * CLI работает с тем же сокетом и не тянет лишних зависимостей. + * + * Все вызовы получают аргументы массивом — нет конкатенации с + * пользовательским вводом, командной инъекции нет. + */ +import { Injectable, Logger } from '@nestjs/common'; +import { execFile } from 'child_process'; +import { promisify } from 'util'; +import { DockerRunnerPort } from './ports'; + +const execFileAsync = promisify(execFile); +const DOCKER_TIMEOUT_MS = 120_000; + +@Injectable() +export class ShellDockerRunner implements DockerRunnerPort { + private readonly logger = new Logger(ShellDockerRunner.name); + + async pullImage(opts: { imageRef: string; bearerToken: string }): Promise { + /* + * docker pull использует уже логиненный креденшал-helper; токен + * прокидывается через переменную окружения DOCKER_AUTH_TOKEN, + * которую читает per-registry credential helper (см. deploy/). + * В MWP здесь только pull — login лежит на инфраструктурном + * скрипте при bootstrap'е orchestrator'а. + */ + this.logger.log(`docker pull ${opts.imageRef}`); + await this.run('docker', ['pull', opts.imageRef], { + DOCKER_AUTH_TOKEN: opts.bearerToken, + }); + } + + async composeUp(opts: { composeFile: string; serviceName: string }): Promise { + this.logger.log(`docker compose -f ${opts.composeFile} up -d ${opts.serviceName}`); + await this.run('docker', [ + 'compose', + '-f', + opts.composeFile, + 'up', + '-d', + opts.serviceName, + ]); + } + + async composeDown(opts: { composeFile: string; serviceName: string }): Promise { + this.logger.log(`docker compose -f ${opts.composeFile} rm -fsv ${opts.serviceName}`); + await this.run('docker', [ + 'compose', + '-f', + opts.composeFile, + 'rm', + '-fsv', + opts.serviceName, + ]); + } + + private async run(cmd: string, args: string[], extraEnv: NodeJS.ProcessEnv = {}): Promise { + try { + const { stdout, stderr } = await execFileAsync(cmd, args, { + timeout: DOCKER_TIMEOUT_MS, + env: { ...process.env, ...extraEnv }, + }); + if (stdout) this.logger.debug(stdout.trim()); + if (stderr) this.logger.debug(stderr.trim()); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + throw new Error(`docker ${args[0] ?? ''} failed: ${msg}`); + } + } +} diff --git a/components/orchestrator/src/orchestrator/health-probe.impl.ts b/components/orchestrator/src/orchestrator/health-probe.impl.ts new file mode 100644 index 00000000000..50316eff588 --- /dev/null +++ b/components/orchestrator/src/orchestrator/health-probe.impl.ts @@ -0,0 +1,60 @@ +/** + * @fileoverview HTTP-импл `HealthProbePort`. Поллит target URL до 200 OK + * или таймаута. + * + * Без зависимостей: используем встроенный `fetch` (Node 20+). + * + * Между попытками — backoff 500ms. На каждой попытке timeout запроса + * 2 сек, чтоб не залипать на slow-DNS / hanging connect'е. + */ +import { Injectable } from '@nestjs/common'; +import { HealthOutcome, HealthProbePort } from './ports'; + +const POLL_INTERVAL_MS = 500; +const REQUEST_TIMEOUT_MS = 2_000; + +@Injectable() +export class HttpHealthProbe implements HealthProbePort { + async waitUntilHealthy(opts: { url: string; timeoutMs: number }): Promise { + const start = Date.now(); + let lastError: string | undefined; + let lastReason: 'timeout' | 'badStatus' | 'transportError' = 'timeout'; + + while (Date.now() - start < opts.timeoutMs) { + const probe = await this.singleProbe(opts.url); + if (probe.ok) { + return { ok: true, elapsedMs: Date.now() - start }; + } + lastReason = probe.reason; + lastError = probe.error; + await sleep(POLL_INTERVAL_MS); + } + return { ok: false, reason: lastReason, lastError }; + } + + private async singleProbe(url: string): Promise< + { ok: true } | { ok: false; reason: 'badStatus' | 'transportError'; error: string } + > { + try { + const ctrl = new AbortController(); + const timer = setTimeout(() => ctrl.abort(), REQUEST_TIMEOUT_MS); + try { + const resp = await fetch(url, { method: 'GET', signal: ctrl.signal }); + if (resp.status >= 200 && resp.status < 300) { + return { ok: true }; + } + return { ok: false, reason: 'badStatus', error: `HTTP ${resp.status}` }; + } finally { + clearTimeout(timer); + } + } catch (e) { + return { + ok: false, + reason: 'transportError', + error: e instanceof Error ? e.message : String(e), + }; + } + } +} + +const sleep = (ms: number): Promise => new Promise((res) => setTimeout(res, ms)); diff --git a/components/orchestrator/src/orchestrator/install-orchestrator.service.spec.ts b/components/orchestrator/src/orchestrator/install-orchestrator.service.spec.ts new file mode 100644 index 00000000000..49a9284a368 --- /dev/null +++ b/components/orchestrator/src/orchestrator/install-orchestrator.service.spec.ts @@ -0,0 +1,215 @@ +/** + * @fileoverview Юнит-тесты install-pipeline'а. Все внешние зависимости + * (docker/health/OCI) подменены ин-мемори моками; SubgraphRegistryService + * подменяется fake'ом с in-memory map'ой. + * + * Что покрываем (Story 10.4): + * 1. happy path с imageRef: OCI token → docker pull → compose up → healthy → registry. + * 2. happy path без imageRef (core-subgraph-style): только healthcheck + registry. + * 3. healthcheck timeout → failed + rollback composeDown. + * 4. OCI token error без jwt → failed без shell-вызовов. + * 5. docker pull error → failed на этапе docker-pull. + * 6. registry write error → failed + rollback composeDown. + */ +import { + InstallOrchestratorService, + InstallExtensionInput, +} from './install-orchestrator.service'; +import { + DockerRunnerPort, + HealthOutcome, + HealthProbePort, + OciTokenClientPort, +} from './ports'; +import { SubgraphRegistryService } from '../gateway/subgraph-registry.service'; + +class FakeDockerRunner implements DockerRunnerPort { + pulls: Array<{ imageRef: string; bearerToken: string }> = []; + ups: Array<{ composeFile: string; serviceName: string }> = []; + downs: Array<{ composeFile: string; serviceName: string }> = []; + pullError?: Error; + upError?: Error; + async pullImage(opts: { imageRef: string; bearerToken: string }): Promise { + this.pulls.push(opts); + if (this.pullError) throw this.pullError; + } + async composeUp(opts: { composeFile: string; serviceName: string }): Promise { + this.ups.push(opts); + if (this.upError) throw this.upError; + } + async composeDown(opts: { composeFile: string; serviceName: string }): Promise { + this.downs.push(opts); + } +} + +class FakeHealthProbe implements HealthProbePort { + outcome: HealthOutcome = { ok: true, elapsedMs: 100 }; + calls: Array<{ url: string; timeoutMs: number }> = []; + async waitUntilHealthy(opts: { url: string; timeoutMs: number }): Promise { + this.calls.push(opts); + return this.outcome; + } +} + +class FakeOciTokenClient implements OciTokenClientPort { + calls: Array<{ packageId: string; jwt: string }> = []; + token = 'fake-bearer-token'; + error?: Error; + async issueToken(opts: { packageId: string; jwt: string }): Promise { + this.calls.push(opts); + if (this.error) throw this.error; + return this.token; + } +} + +class FakeRegistryService { + upserts: Array<{ packageId: string; version: string; url: string }> = []; + healthSets: Array<{ packageId: string; healthStatus: string }> = []; + upsertError?: Error; + async upsert(packageId: string, version: string, url: string): Promise { + this.upserts.push({ packageId, version, url }); + if (this.upsertError) throw this.upsertError; + } + async setHealthStatus(packageId: string, healthStatus: string): Promise { + this.healthSets.push({ packageId, healthStatus }); + } +} + +const buildHarness = (): { + service: InstallOrchestratorService; + docker: FakeDockerRunner; + health: FakeHealthProbe; + oci: FakeOciTokenClient; + registry: FakeRegistryService; +} => { + const docker = new FakeDockerRunner(); + const health = new FakeHealthProbe(); + const oci = new FakeOciTokenClient(); + const registry = new FakeRegistryService(); + const service = new InstallOrchestratorService( + docker, + health, + oci, + registry as unknown as SubgraphRegistryService, + ); + return { service, docker, health, oci, registry }; +}; + +const baseInput: InstallExtensionInput = { + packageId: '@coopenomics/chatcoop', + version: '1.0.0', + url: 'http://chatcoop:3000/graphql', +}; + +describe('InstallOrchestratorService', () => { + it('happy path c imageRef: OCI → pull → up → healthy → registry', async () => { + const h = buildHarness(); + const result = await h.service.install({ + ...baseInput, + imageRef: 'registry.local/coopenomics/chatcoop:1.0.0', + composeFile: '/etc/orchestrator/extensions.yaml', + composeService: 'chatcoop', + cooperativeJwt: 'jwt-of-voskhod', + }); + + expect(result.status).toBe('applied'); + if (result.status !== 'applied') return; // narrowing + expect(result.packageId).toBe(baseInput.packageId); + expect(result.healthAfterMs).toBe(100); + + expect(h.oci.calls).toEqual([{ packageId: baseInput.packageId, jwt: 'jwt-of-voskhod' }]); + expect(h.docker.pulls).toEqual([ + { imageRef: 'registry.local/coopenomics/chatcoop:1.0.0', bearerToken: 'fake-bearer-token' }, + ]); + expect(h.docker.ups).toEqual([ + { composeFile: '/etc/orchestrator/extensions.yaml', serviceName: 'chatcoop' }, + ]); + expect(h.health.calls).toEqual([ + { url: baseInput.url, timeoutMs: 60_000 }, + ]); + expect(h.registry.upserts).toEqual([ + { packageId: baseInput.packageId, version: baseInput.version, url: baseInput.url }, + ]); + expect(h.registry.healthSets).toEqual([ + { packageId: baseInput.packageId, healthStatus: 'ok' }, + ]); + expect(h.docker.downs).toEqual([]); + }); + + it('happy path без imageRef — только healthcheck и registry (core-style)', async () => { + const h = buildHarness(); + const result = await h.service.install(baseInput); + expect(result.status).toBe('applied'); + expect(h.oci.calls).toEqual([]); + expect(h.docker.pulls).toEqual([]); + expect(h.docker.ups).toEqual([]); + expect(h.health.calls.length).toBe(1); + expect(h.registry.upserts.length).toBe(1); + }); + + it('healthcheck timeout → failed + rollback composeDown', async () => { + const h = buildHarness(); + h.health.outcome = { ok: false, reason: 'timeout', lastError: 'no response' }; + const result = await h.service.install({ + ...baseInput, + composeFile: '/etc/orchestrator/extensions.yaml', + composeService: 'chatcoop', + }); + expect(result.status).toBe('failed'); + if (result.status !== 'failed') return; + expect(result.reason).toBe('healthcheck'); + expect(result.error).toContain('timeout'); + expect(h.docker.downs).toEqual([ + { composeFile: '/etc/orchestrator/extensions.yaml', serviceName: 'chatcoop' }, + ]); + expect(h.registry.upserts).toEqual([]); + }); + + it('imageRef без cooperativeJwt → failed: oci-token, без shell-вызовов', async () => { + const h = buildHarness(); + const result = await h.service.install({ ...baseInput, imageRef: 'reg/img:1.0.0' }); + expect(result.status).toBe('failed'); + if (result.status !== 'failed') return; + expect(result.reason).toBe('oci-token'); + expect(h.oci.calls).toEqual([]); + expect(h.docker.pulls).toEqual([]); + }); + + it('docker pull error → failed: docker-pull', async () => { + const h = buildHarness(); + h.docker.pullError = new Error('docker pull failed: unauthorized'); + const result = await h.service.install({ + ...baseInput, + imageRef: 'reg/img:1.0.0', + cooperativeJwt: 'jwt', + }); + expect(result.status).toBe('failed'); + if (result.status !== 'failed') return; + expect(result.reason).toBe('docker-pull'); + expect(result.error).toContain('docker pull failed'); + expect(h.health.calls).toEqual([]); + expect(h.registry.upserts).toEqual([]); + }); + + it('registry upsert error → failed: registry-write + rollback composeDown', async () => { + const h = buildHarness(); + h.registry.upsertError = new Error('postgres unavailable'); + const result = await h.service.install({ + ...baseInput, + composeFile: '/etc/orchestrator/extensions.yaml', + composeService: 'chatcoop', + }); + expect(result.status).toBe('failed'); + if (result.status !== 'failed') return; + expect(result.reason).toBe('registry-write'); + expect(h.docker.downs).toEqual([ + { composeFile: '/etc/orchestrator/extensions.yaml', serviceName: 'chatcoop' }, + ]); + }); + + it('healthcheckTimeoutMs из input пробрасывается в HealthProbe', async () => { + const h = buildHarness(); + await h.service.install({ ...baseInput, healthcheckTimeoutMs: 5_000 }); + expect(h.health.calls).toEqual([{ url: baseInput.url, timeoutMs: 5_000 }]); + }); +}); diff --git a/components/orchestrator/src/orchestrator/install-orchestrator.service.ts b/components/orchestrator/src/orchestrator/install-orchestrator.service.ts new file mode 100644 index 00000000000..07202fba141 --- /dev/null +++ b/components/orchestrator/src/orchestrator/install-orchestrator.service.ts @@ -0,0 +1,175 @@ +/** + * @fileoverview Сценарий установки subgraph-расширения (Story 10.4). + * + * Шаги pipeline'а: + * 1. (опционально) issue OCI token у CA-auth и docker pull образа; + * 2. (опционально) docker compose up сервиса в проекте orchestrator'а; + * 3. healthcheck по URL — ждём пока subgraph внутри контейнера + * прогреется и начнёт отвечать 200; + * 4. upsert записи в `subgraph_registry` (active=true, healthStatus='ok'); + * 5. Apollo Gateway за ≤10 сек подтянет новую запись polling'ом и + * добавит subgraph в supergraph — клиенты увидят его без рестарта. + * + * Discriminated outcome `applied | failed` — клиент должен switch'ить + * на `status`. На любом failure делаем rollback (compose down + deactivate + * в registry), чтобы не оставить «висящий» сервис, в который никто не + * маршрутизируется. + * + * Сервис намеренно не знает про docker socket / OCI / HTTP напрямую — + * это всё за портами {@link DockerRunnerPort}, {@link HealthProbePort}, + * {@link OciTokenClientPort}. Тесты гоняют сценарий через моки портов + * без реального docker daemon'а. + */ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { SubgraphRegistryService } from '../gateway/subgraph-registry.service'; +import { + DOCKER_RUNNER, + DockerRunnerPort, + HEALTH_PROBE, + HealthProbePort, + OCI_TOKEN_CLIENT, + OciTokenClientPort, +} from './ports'; + +export interface InstallExtensionInput { + /** `@scope/name` пакета в каталоге. */ + packageId: string; + /** Семантическая версия релиза. */ + version: string; + /** + * Внутренний URL subgraph'а внутри docker-сети + * (например `http://chatcoop:3000/graphql`). Используется и для + * healthcheck'а, и как `url` в registry. + */ + url: string; + /** + * OCI-ссылка образа `registry.host/scope/name:version`. Если задана — + * orchestrator делает docker pull. Если пусто — считается, что + * контейнер уже поднят сторонним образом (например core-subgraph). + */ + imageRef?: string; + /** + * Имя сервиса в docker-compose файле orchestrator'а. Если задано + * вместе с `composeFile` — orchestrator делает `compose up -d `. + */ + composeService?: string; + /** + * Путь к docker-compose файлу, описывающему extension-сервисы. + * Опциональный — если расширения деплоятся другим механизмом + * (k8s, swarm) — orchestrator только healthcheck'ает. + */ + composeFile?: string; + /** + * JWT кооператива для запроса OCI-токена у CA-auth. Нужен только + * если задан `imageRef`. + */ + cooperativeJwt?: string; + /** Healthcheck timeout. По умолчанию 60 сек — Nest-приложение успевает прогреться. */ + healthcheckTimeoutMs?: number; +} + +export type InstallOutcome = + | { status: 'applied'; packageId: string; healthAfterMs: number } + | { status: 'failed'; packageId: string; reason: InstallFailureReason; error: string }; + +export type InstallFailureReason = + | 'oci-token' + | 'docker-pull' + | 'compose-up' + | 'healthcheck' + | 'registry-write'; + +@Injectable() +export class InstallOrchestratorService { + private readonly logger = new Logger(InstallOrchestratorService.name); + + constructor( + @Inject(DOCKER_RUNNER) private readonly docker: DockerRunnerPort, + @Inject(HEALTH_PROBE) private readonly health: HealthProbePort, + @Inject(OCI_TOKEN_CLIENT) private readonly oci: OciTokenClientPort, + private readonly registry: SubgraphRegistryService, + ) {} + + async install(input: InstallExtensionInput): Promise { + const timeoutMs = input.healthcheckTimeoutMs ?? 60_000; + this.logger.log(`install start: ${input.packageId}@${input.version} → ${input.url}`); + + // 1. (опц.) docker pull через CA-auth OCI token. + if (input.imageRef !== undefined) { + if (input.cooperativeJwt === undefined) { + return this.failed(input.packageId, 'oci-token', 'cooperativeJwt обязателен при заданном imageRef'); + } + try { + const bearerToken = await this.oci.issueToken({ + packageId: input.packageId, + jwt: input.cooperativeJwt, + }); + await this.docker.pullImage({ imageRef: input.imageRef, bearerToken }); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + const reason: InstallFailureReason = msg.includes('docker') ? 'docker-pull' : 'oci-token'; + return this.failed(input.packageId, reason, msg); + } + } + + // 2. (опц.) docker compose up сервиса. + if (input.composeService !== undefined && input.composeFile !== undefined) { + try { + await this.docker.composeUp({ + composeFile: input.composeFile, + serviceName: input.composeService, + }); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + return this.failed(input.packageId, 'compose-up', msg); + } + } + + // 3. healthcheck poll по URL subgraph'а. + const health = await this.health.waitUntilHealthy({ url: input.url, timeoutMs }); + if (!health.ok) { + await this.rollbackComposeIfAny(input); + return this.failed( + input.packageId, + 'healthcheck', + `${health.reason}${health.lastError ? `: ${health.lastError}` : ''}`, + ); + } + + // 4. upsert в subgraph_registry — Apollo Gateway за ≤10 сек подхватит. + try { + await this.registry.upsert(input.packageId, input.version, input.url); + await this.registry.setHealthStatus(input.packageId, 'ok'); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + await this.rollbackComposeIfAny(input); + return this.failed(input.packageId, 'registry-write', msg); + } + + this.logger.log(`install applied: ${input.packageId}@${input.version} (health ${health.elapsedMs}ms)`); + return { status: 'applied', packageId: input.packageId, healthAfterMs: health.elapsedMs }; + } + + private async rollbackComposeIfAny(input: InstallExtensionInput): Promise { + if (input.composeService === undefined || input.composeFile === undefined) return; + try { + await this.docker.composeDown({ + composeFile: input.composeFile, + serviceName: input.composeService, + }); + } catch (e) { + this.logger.error( + `rollback composeDown failed for ${input.packageId}: ${e instanceof Error ? e.message : String(e)}`, + ); + } + } + + private failed( + packageId: string, + reason: InstallFailureReason, + error: string, + ): InstallOutcome { + this.logger.error(`install failed (${reason}) ${packageId}: ${error}`); + return { status: 'failed', packageId, reason, error }; + } +} diff --git a/components/orchestrator/src/orchestrator/install.controller.ts b/components/orchestrator/src/orchestrator/install.controller.ts new file mode 100644 index 00000000000..8f63d14a8eb --- /dev/null +++ b/components/orchestrator/src/orchestrator/install.controller.ts @@ -0,0 +1,42 @@ +/** + * @fileoverview REST API установки/снятия subgraph-расширений. + * + * Story 10.4: тонкая обёртка над {@link InstallOrchestratorService}. + * Реальный pipeline (OCI pull → compose up → healthcheck → registry) + * живёт в сервисе. Сюда приходят запросы из mono controller'а + * (mutation approvePackage → orchestrator REST install) и из E2E. + * + * `install` возвращает discriminated outcome (`applied | failed`); + * клиент должен switch'ить на `status` поле. + */ +import { Body, Controller, Delete, Param, Post } from '@nestjs/common'; +import { + InstallOrchestratorService, + InstallExtensionInput, + InstallOutcome, +} from './install-orchestrator.service'; +import { SubgraphRegistryService } from '../gateway/subgraph-registry.service'; + +interface UninstallResult { + ok: boolean; + deactivated: string; +} + +@Controller('v1/internal/extensions') +export class InstallController { + constructor( + private readonly orchestrator: InstallOrchestratorService, + private readonly registry: SubgraphRegistryService, + ) {} + + @Post('install') + async install(@Body() input: InstallExtensionInput): Promise { + return this.orchestrator.install(input); + } + + @Delete('uninstall/:packageId') + async uninstall(@Param('packageId') packageId: string): Promise { + await this.registry.deactivate(packageId); + return { ok: true, deactivated: packageId }; + } +} diff --git a/components/orchestrator/src/orchestrator/oci-token-client.impl.ts b/components/orchestrator/src/orchestrator/oci-token-client.impl.ts new file mode 100644 index 00000000000..4f0ed3faf1c --- /dev/null +++ b/components/orchestrator/src/orchestrator/oci-token-client.impl.ts @@ -0,0 +1,45 @@ +/** + * @fileoverview HTTP-импл `OciTokenClientPort`. Идёт за bearer-токеном + * в CA-auth по протоколу Docker Registry v2 token endpoint (Story 10.6). + * + * Эндпоинт: `GET {CA_AUTH_BASE}/v2/token?service=registry&scope=repository::pull` + * Авторизация: `Authorization: Bearer ` — это JWT + * кооператива, выданный CA-auth при онбординге; CA-auth маппит + * coopname → discharge OCI token со scope'ом pull. + * + * Ответ: `{ token: "..." }` — короткоживущий (15 мин); docker pull + * подставляет его как Bearer на каждом запросе к registry. + */ +import { Injectable } from '@nestjs/common'; +import { OciTokenClientPort } from './ports'; + +const REQUEST_TIMEOUT_MS = 10_000; + +@Injectable() +export class CaAuthOciTokenClient implements OciTokenClientPort { + constructor(private readonly caAuthBase: string) {} + + async issueToken(opts: { packageId: string; jwt: string }): Promise { + const scope = `repository:${opts.packageId}:pull`; + const url = `${this.caAuthBase}/v2/token?service=registry&scope=${encodeURIComponent(scope)}`; + const ctrl = new AbortController(); + const timer = setTimeout(() => ctrl.abort(), REQUEST_TIMEOUT_MS); + try { + const resp = await fetch(url, { + method: 'GET', + headers: { Authorization: `Bearer ${opts.jwt}` }, + signal: ctrl.signal, + }); + if (!resp.ok) { + throw new Error(`CA-auth /v2/token → HTTP ${resp.status}`); + } + const body = (await resp.json()) as { token?: string }; + if (typeof body.token !== 'string' || body.token.length === 0) { + throw new Error('CA-auth /v2/token: пустой token в ответе'); + } + return body.token; + } finally { + clearTimeout(timer); + } + } +} diff --git a/components/orchestrator/src/orchestrator/orchestrator.module.ts b/components/orchestrator/src/orchestrator/orchestrator.module.ts new file mode 100644 index 00000000000..7954f0a2fda --- /dev/null +++ b/components/orchestrator/src/orchestrator/orchestrator.module.ts @@ -0,0 +1,31 @@ +/** + * @fileoverview Nest-модуль orchestrator'а: REST-эндпоинты, install-pipeline, + * shell-out импленты Docker/Health/OCI портов. + * + * Story 10.4: добавлены InstallOrchestratorService + порты + shell-out + * импленты. CA-auth base URL берётся из ENV (CA_AUTH_BASE_URL). + */ +import { Module } from '@nestjs/common'; +import { GatewayModule } from '../gateway/gateway.module'; +import { CompositionController } from './composition.controller'; +import { InstallController } from './install.controller'; +import { InstallOrchestratorService } from './install-orchestrator.service'; +import { DOCKER_RUNNER, HEALTH_PROBE, OCI_TOKEN_CLIENT } from './ports'; +import { ShellDockerRunner } from './docker-runner.impl'; +import { HttpHealthProbe } from './health-probe.impl'; +import { CaAuthOciTokenClient } from './oci-token-client.impl'; + +@Module({ + imports: [GatewayModule], + controllers: [CompositionController, InstallController], + providers: [ + InstallOrchestratorService, + { provide: DOCKER_RUNNER, useClass: ShellDockerRunner }, + { provide: HEALTH_PROBE, useClass: HttpHealthProbe }, + { + provide: OCI_TOKEN_CLIENT, + useFactory: () => new CaAuthOciTokenClient(process.env.CA_AUTH_BASE_URL ?? 'http://ca-auth:3000'), + }, + ], +}) +export class OrchestratorModule {} diff --git a/components/orchestrator/src/orchestrator/ports.ts b/components/orchestrator/src/orchestrator/ports.ts new file mode 100644 index 00000000000..86b3a6b5830 --- /dev/null +++ b/components/orchestrator/src/orchestrator/ports.ts @@ -0,0 +1,60 @@ +/** + * @fileoverview Порты для install-pipeline'а. Сервис {@link InstallOrchestratorService} + * зависит только от этих интерфейсов; реальные shell-out / HTTP-импленты + * лежат рядом в `*.impl.ts` и подключаются в `OrchestratorModule`. + * + * Зачем интерфейсы: docker socket / сеть наружу — environment-specific. + * Тесты подменяют порты на in-memory моки, не запуская docker. Story 10.4. + */ + +/** + * Порт обращения к docker daemon хоста. MWP-набор операций: + * pull image (с авторизацией через OCI token) и compose up. + * + * Реальный impl шеллит `docker pull` / `docker compose up -d`. Тестовый + * impl возвращает заранее заданные результаты — без shell. + */ +export interface DockerRunnerPort { + /** Подтянуть образ из приватного registry. */ + pullImage(opts: { imageRef: string; bearerToken: string }): Promise; + /** Поднять сервис compose-проекта (idempotent — повторный вызов no-op если up). */ + composeUp(opts: { composeFile: string; serviceName: string }): Promise; + /** Снять сервис compose-проекта — используется на rollback. */ + composeDown(opts: { composeFile: string; serviceName: string }): Promise; +} + +export const DOCKER_RUNNER = Symbol('DockerRunnerPort'); + +/** + * Порт healthcheck'а subgraph'а. Делает GET к указанному URL и считает + * сервис здоровым, если получает 200 OK с непустым body в течение + * `timeoutMs`. Реальный impl поллит с интервалом ≤500ms; тестовый + * отвечает заранее. + * + * Зачем именно poll, а не один запрос: docker compose up возвращается + * сразу же по запуску контейнера; Nest-приложение внутри стартует + * 1-3 секунды (DI, TypeORM connect, GraphQL schema introspection). + * Без поллинга мы получили бы false negative. + */ +export interface HealthProbePort { + waitUntilHealthy(opts: { url: string; timeoutMs: number }): Promise; +} + +export type HealthOutcome = + | { ok: true; elapsedMs: number } + | { ok: false; reason: 'timeout' | 'badStatus' | 'transportError'; lastError?: string }; + +export const HEALTH_PROBE = Symbol('HealthProbePort'); + +/** + * Порт получения OCI token у CA-auth для docker pull из приватного + * registry. Возвращает bearer-токен, валидный для конкретного scope + * `repository::pull`. + * + * @see ca-auth/src/modules/registry/web/oci-token.controller.ts (Story 10.6) + */ +export interface OciTokenClientPort { + issueToken(opts: { packageId: string; jwt: string }): Promise; +} + +export const OCI_TOKEN_CLIENT = Symbol('OciTokenClientPort'); diff --git a/components/orchestrator/tsconfig.json b/components/orchestrator/tsconfig.json new file mode 100644 index 00000000000..8dc3d6f4dca --- /dev/null +++ b/components/orchestrator/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "declaration": false, + "outDir": "dist", + "rootDir": "src", + "types": ["node", "jest"] + }, + "include": ["src/**/*"], + "exclude": ["src/**/*.spec.ts", "dist", "node_modules"] +} diff --git a/components/sdk/src/queries/extensions/appsCatalogRemotePackages.ts b/components/sdk/src/queries/extensions/appsCatalogRemotePackages.ts new file mode 100644 index 00000000000..7c480ccc786 --- /dev/null +++ b/components/sdk/src/queries/extensions/appsCatalogRemotePackages.ts @@ -0,0 +1,30 @@ +import { appsCatalogRemotePackageSelector } from '../../selectors/extensions/appsCatalogRemotePackageSelector' +import { $, type GraphQLTypes, type InputType, Selector } from '../../zeus/index' + +export const name = 'appsCatalogRemotePackages' + +/** + * Публичный каталог remote-пакетов из apps-catalog (Story 9.5.b). + * + * Прокси-резолвер на mono controller'е дергает ca-admin + * `GET /v1/public/packages` и возвращает список с UI-полями + * (title / description / rubPerMonth — на MVP из дефолтов, в будущем из manifest). + */ +export const query = Selector('Query')({ + [name]: [ + { page: $('page', 'Float!'), pageSize: $('pageSize', 'Float!') }, + appsCatalogRemotePackageSelector, + ], +}) + +export interface IInput { + /** + * @private + */ + [key: string]: unknown + + page: number + pageSize: number +} + +export type IOutput = InputType diff --git a/components/sdk/src/queries/extensions/index.ts b/components/sdk/src/queries/extensions/index.ts index 7728deccb7b..f21c48d0b58 100644 --- a/components/sdk/src/queries/extensions/index.ts +++ b/components/sdk/src/queries/extensions/index.ts @@ -1,3 +1,6 @@ +/** Публичный каталог remote-пакетов apps-catalog (Story 9.5.b) */ +export * as AppsCatalogRemotePackages from './appsCatalogRemotePackages' + /** Получить логи расширений с фильтрацией и пагинацией */ export * as GetExtensionLogs from './getExtensionLogs' diff --git a/components/sdk/src/selectors/extensions/appsCatalogRemotePackageSelector.ts b/components/sdk/src/selectors/extensions/appsCatalogRemotePackageSelector.ts new file mode 100644 index 00000000000..00972c81970 --- /dev/null +++ b/components/sdk/src/selectors/extensions/appsCatalogRemotePackageSelector.ts @@ -0,0 +1,18 @@ +import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired' +import { type ModelTypes, Selector, type ValueTypes } from '../../zeus/index' + +const rawAppsCatalogRemotePackageSelector = { + packageId: true, + publisher: true, + compatibleSubnets: true, + lastActiveVersion: true, + title: true, + description: true, + rubPerMonth: true, +} + +const _validate: MakeAllFieldsRequired = rawAppsCatalogRemotePackageSelector +export type appsCatalogRemotePackageModel = ModelTypes['AppsCatalogRemotePackageDTO'] + +export const appsCatalogRemotePackageSelector = Selector('AppsCatalogRemotePackageDTO')(rawAppsCatalogRemotePackageSelector) +export { rawAppsCatalogRemotePackageSelector } diff --git a/components/sdk/src/selectors/extensions/index.ts b/components/sdk/src/selectors/extensions/index.ts index 277ace8926f..d0000747f1a 100644 --- a/components/sdk/src/selectors/extensions/index.ts +++ b/components/sdk/src/selectors/extensions/index.ts @@ -1 +1,2 @@ -export * from './extensionSelector' \ No newline at end of file +export * from './appsCatalogRemotePackageSelector' +export * from './extensionSelector' diff --git a/components/sdk/src/zeus/const.ts b/components/sdk/src/zeus/const.ts index 937e291626a..0cf3b414f14 100644 --- a/components/sdk/src/zeus/const.ts +++ b/components/sdk/src/zeus/const.ts @@ -1337,6 +1337,9 @@ export const AllTypesProps: Record = { agreements:{ filter:"AgreementFilter", options:"PaginationInput" + }, + appsCatalogRemotePackages:{ + }, buildInitialReportEdits:{ reportType:"ReportType" @@ -2040,6 +2043,15 @@ export const ReturnTypes: Record = { status:"ApprovalStatus", username:"String" }, + AppsCatalogRemotePackageDTO:{ + compatibleSubnets:"String", + description:"String", + lastActiveVersion:"String", + packageId:"String", + publisher:"String", + rubPerMonth:"Float", + title:"String" + }, AuthSequence:{ account:"String", sequence:"String" @@ -4118,6 +4130,7 @@ export const ReturnTypes: Record = { Query:{ agreementTemplates:"AgreementTemplate", agreements:"PaginatedAgreementsPaginationResult", + appsCatalogRemotePackages:"AppsCatalogRemotePackageDTO", buildInitialReportEdits:"BuildInitialReportEdits", candidates:"PaginatedCandidatesPaginationResult", capitalCandidates:"PaginatedCapitalCandidatesPaginationResult", diff --git a/components/sdk/src/zeus/index.ts b/components/sdk/src/zeus/index.ts index 53d826e7692..fcfd292b4af 100644 --- a/components/sdk/src/zeus/index.ts +++ b/components/sdk/src/zeus/index.ts @@ -1741,6 +1741,24 @@ export type ValueTypes = { }; /** Статус одобрения в системе CHAIRMAN */ ["ApprovalStatus"]:ApprovalStatus; + ["AppsCatalogRemotePackageDTO"]: AliasType<{ + /** Совместимые subnet (chain_id блокчейна ЦК) */ + compatibleSubnets?:boolean | `@${string}`, + /** Краткое описание (в MVP — заглушка, в будущем из manifest) */ + description?:boolean | `@${string}`, + /** Последняя активная версия (semver). null = релизов ещё нет. */ + lastActiveVersion?:boolean | `@${string}`, + /** Идентификатор пакета (например, @voskhod/demoapp) */ + packageId?:boolean | `@${string}`, + /** Имя владельца пакета (кооператив-разработчик) */ + publisher?:boolean | `@${string}`, + /** Стоимость подписки, RUB/месяц (в MVP — фиксированно из dev-pricing seed) */ + rubPerMonth?:boolean | `@${string}`, + /** Заголовок пакета для UI (в MVP — packageId, в будущем из manifest) */ + title?:boolean | `@${string}`, + __typename?: boolean | `@${string}`, + ['...on AppsCatalogRemotePackageDTO']?: Omit +}>; ["AssetContributionActGenerateDocumentInput"]: { /** Идентификатор акта */ act_id: string | Variable, @@ -7769,6 +7787,7 @@ walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable},V ["Query"]: AliasType<{ agreementTemplates?: [{ coopname: string | Variable},ValueTypes["AgreementTemplate"]], agreements?: [{ filter?: ValueTypes["AgreementFilter"] | undefined | null | Variable, options?: ValueTypes["PaginationInput"] | undefined | null | Variable},ValueTypes["PaginatedAgreementsPaginationResult"]], +appsCatalogRemotePackages?: [{ page: number | Variable, pageSize: number | Variable},ValueTypes["AppsCatalogRemotePackageDTO"]], buildInitialReportEdits?: [{ period?: number | undefined | null | Variable, reportType: ValueTypes["ReportType"] | Variable, year: number | Variable},ValueTypes["BuildInitialReportEdits"]], candidates?: [{ filter?: ValueTypes["CandidateFilterInput"] | undefined | null | Variable, options?: ValueTypes["PaginationInput"] | undefined | null | Variable},ValueTypes["PaginatedCandidatesPaginationResult"]], capitalCandidates?: [{ filter?: ValueTypes["CandidateFilterInput"] | undefined | null | Variable, options?: ValueTypes["PaginationInput"] | undefined | null | Variable},ValueTypes["PaginatedCapitalCandidatesPaginationResult"]], @@ -10208,6 +10227,23 @@ export type ResolverInputTypes = { }; /** Статус одобрения в системе CHAIRMAN */ ["ApprovalStatus"]:ApprovalStatus; + ["AppsCatalogRemotePackageDTO"]: AliasType<{ + /** Совместимые subnet (chain_id блокчейна ЦК) */ + compatibleSubnets?:boolean | `@${string}`, + /** Краткое описание (в MVP — заглушка, в будущем из manifest) */ + description?:boolean | `@${string}`, + /** Последняя активная версия (semver). null = релизов ещё нет. */ + lastActiveVersion?:boolean | `@${string}`, + /** Идентификатор пакета (например, @voskhod/demoapp) */ + packageId?:boolean | `@${string}`, + /** Имя владельца пакета (кооператив-разработчик) */ + publisher?:boolean | `@${string}`, + /** Стоимость подписки, RUB/месяц (в MVP — фиксированно из dev-pricing seed) */ + rubPerMonth?:boolean | `@${string}`, + /** Заголовок пакета для UI (в MVP — packageId, в будущем из manifest) */ + title?:boolean | `@${string}`, + __typename?: boolean | `@${string}` +}>; ["AssetContributionActGenerateDocumentInput"]: { /** Идентификатор акта */ act_id: string, @@ -16058,6 +16094,7 @@ walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputType ["Query"]: AliasType<{ agreementTemplates?: [{ coopname: string},ResolverInputTypes["AgreementTemplate"]], agreements?: [{ filter?: ResolverInputTypes["AgreementFilter"] | undefined | null, options?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["PaginatedAgreementsPaginationResult"]], +appsCatalogRemotePackages?: [{ page: number, pageSize: number},ResolverInputTypes["AppsCatalogRemotePackageDTO"]], buildInitialReportEdits?: [{ period?: number | undefined | null, reportType: ResolverInputTypes["ReportType"], year: number},ResolverInputTypes["BuildInitialReportEdits"]], candidates?: [{ filter?: ResolverInputTypes["CandidateFilterInput"] | undefined | null, options?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["PaginatedCandidatesPaginationResult"]], capitalCandidates?: [{ filter?: ResolverInputTypes["CandidateFilterInput"] | undefined | null, options?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["PaginatedCapitalCandidatesPaginationResult"]], @@ -18442,6 +18479,22 @@ export type ModelTypes = { username?: string | undefined | null }; ["ApprovalStatus"]:ApprovalStatus; + ["AppsCatalogRemotePackageDTO"]: { + /** Совместимые subnet (chain_id блокчейна ЦК) */ + compatibleSubnets: Array, + /** Краткое описание (в MVP — заглушка, в будущем из manifest) */ + description: string, + /** Последняя активная версия (semver). null = релизов ещё нет. */ + lastActiveVersion?: string | undefined | null, + /** Идентификатор пакета (например, @voskhod/demoapp) */ + packageId: string, + /** Имя владельца пакета (кооператив-разработчик) */ + publisher: string, + /** Стоимость подписки, RUB/месяц (в MVP — фиксированно из dev-pricing seed) */ + rubPerMonth: number, + /** Заголовок пакета для UI (в MVP — packageId, в будущем из manifest) */ + title: string +}; ["AssetContributionActGenerateDocumentInput"]: { /** Идентификатор акта */ act_id: string, @@ -24558,6 +24611,8 @@ export type ModelTypes = { agreementTemplates: Array, /** Получение списка соглашений с фильтрацией и пагинацией */ agreements: ModelTypes["PaginatedAgreementsPaginationResult"], + /** Список remote-пакетов из публичного каталога apps-catalog. Защищён JWT (видят только авторизованные пайщики). Источник — ca-admin /v1/public/packages; controller проксирует. */ + appsCatalogRemotePackages: Array, /** Построить предзаполненные edits для формы: дефолты (ledger2 + реквизиты + корректировки), с наложением dirty-полей существующего черновика (если он есть). Требуемые роли: chairman. */ @@ -27110,6 +27165,24 @@ export type GraphQLTypes = { }; /** Статус одобрения в системе CHAIRMAN */ ["ApprovalStatus"]: ApprovalStatus; + ["AppsCatalogRemotePackageDTO"]: { + __typename: "AppsCatalogRemotePackageDTO", + /** Совместимые subnet (chain_id блокчейна ЦК) */ + compatibleSubnets: Array, + /** Краткое описание (в MVP — заглушка, в будущем из manifest) */ + description: string, + /** Последняя активная версия (semver). null = релизов ещё нет. */ + lastActiveVersion?: string | undefined | null, + /** Идентификатор пакета (например, @voskhod/demoapp) */ + packageId: string, + /** Имя владельца пакета (кооператив-разработчик) */ + publisher: string, + /** Стоимость подписки, RUB/месяц (в MVP — фиксированно из dev-pricing seed) */ + rubPerMonth: number, + /** Заголовок пакета для UI (в MVP — packageId, в будущем из manifest) */ + title: string, + ['...on AppsCatalogRemotePackageDTO']: Omit +}; ["AssetContributionActGenerateDocumentInput"]: { /** Идентификатор акта */ act_id: string, @@ -33622,6 +33695,8 @@ export type GraphQLTypes = { agreementTemplates: Array, /** Получение списка соглашений с фильтрацией и пагинацией */ agreements: GraphQLTypes["PaginatedAgreementsPaginationResult"], + /** Список remote-пакетов из публичного каталога apps-catalog. Защищён JWT (видят только авторизованные пайщики). Источник — ca-admin /v1/public/packages; controller проксирует. */ + appsCatalogRemotePackages: Array, /** Построить предзаполненные edits для формы: дефолты (ledger2 + реквизиты + корректировки), с наложением dirty-полей существующего черновика (если он есть). Требуемые роли: chairman. */ diff --git a/docs/integration/native-to-catalog-research.md b/docs/integration/native-to-catalog-research.md new file mode 100644 index 00000000000..ccbef71ccd6 --- /dev/null +++ b/docs/integration/native-to-catalog-research.md @@ -0,0 +1,280 @@ +# Native extensions → Apps Catalog: research-доклад + +Документ выполнен на ветке `apps-catalog` от `dev` в mono. Цель — спроектировать **гармоничную** интеграцию каталога приложений (apps-catalog: ca-auth/ca-admin/Nexus + on-chain контракт `apps`) в существующий монолит, **не ломая** сегодняшнюю модель native-extensions (capital/chairman/chatcoop/participant/powerup/reports/soviet). + +Документ — план, не реализация. Stories 1.3–1.12 продолжатся в apps-catalog репозитории отдельно. + +## 0. Контекст и стейкхолдер + +- Пользователь хочет: 7 текущих native extensions **продолжают работать как есть** (bundle, in-memory installer), но **появляются в магазине** как «уже установлено». Новые extension'ы постепенно вытаскиваются из bundle и устанавливаются динамически через каталог по платной/бесплатной модели. +- Чисто заменять одно на другое («ломая всё») запрещено. +- В исследовании надо найти **минимальный шов**, который позволит сосуществовать `bundle` и `remote` режимам. + +## 1. As-is: что сейчас в mono + +### 1.1. Frontend (desktop) + +``` +components/desktop/extensions/ +├── capital/install.ts ← IWorkspaceConfig[] +├── chairman/install.ts +├── chatcoop/install.ts +├── participant/install.ts +├── powerup/install.ts +├── reports/install.ts +└── soviet/install.ts +``` + +- Каждый `install.ts` — экспорт `default async () => IWorkspaceConfig[]`, где `IWorkspaceConfig` = `{ workspace, extension_name, title?, icon?, defaultRoute?, routes }` (`components/desktop/src/shared/lib/types/workspace.ts:24`). +- Bundle-time registry: `components/desktop/src/processes/init-installed-extensions/extensions-registry.ts` импортирует все 7 install-функций и кладёт в `Record Promise>`. +- Process: `useInitExtensionsProcess(router)` (`processes/init-installed-extensions/index.ts:6`) на старте SSR-приложения вызывает **все** install-функции последовательно и пушит их routes в `desktopStore` + Vue Router. Никаких «не показывать» — все 7 столов всегда зарегистрированы в bundle. +- Видимость столов конкретному кооперативу / роли — отдельный слой (см. backend grants ниже + `meta.roles`/`meta.conditions` в IWorkspaceRoute). + +**Существующий UI «магазин»:** +- `components/desktop/src/pages/ExtensionStore/{BaseRoute, ExtensionPage, ExtensionsManagement, ExtensionsShowcase, InstalledExtensions}` — UI для просмотра/«управления» доступными расширениями кооператива. Данные подтягивает из coopback (AppRegistry → GraphQL). +- `components/desktop/src/pages/Marketplace/*` — это **Стол заказов** (Marketplace MVP, другое), путать нельзя. + +### 1.2. Backend (controller / coopback) + +- `components/controller/src/extensions/extensions.registry.ts` — `AppRegistry: INamedExtension` хардкодит NestJS-модули всех расширений (`soviet`, `chairman`, `capital`, `chatcoop`, `participant`, `powerup`, `reports`, `1ccoop`, `qrpay`, `sberpoll`, `yookassa`, `builtin`). +- Поля `IRegistryExtension`: `is_builtin`, `is_internal`, `is_available`, `desktops`, `external_url`, `title`, `description`, `image`, `class` (NestJS-модуль), `pluginClass` (миграции), `schema` (Zod конфига), `tags`, `readme`/`instructions` (async чтение README/INSTALL.md из директории). +- Концептуально это уже **полу-«пакетная»** модель: есть метаданные пакета, бинарь (тут — компилируемый внутрь monorepo), миграции, конфиг-схема. Просто всё in-process, без выгрузки и pull-tarball. +- Видимость и онбординг столов решает слой grants (см. memory `reference_extension_onboarding_gating`): backend через `getDesktop` отдаёт `DesktopWorkspace.grants`, фронт сверяет `meta.requires`. + +### 1.3. On-chain (apps contract) + +- Развёрнут в подсетях, в которых живёт Mono (на mono-ai-5 — chain_id `db79c84096…`). 17 actions / 8 tables — pricing, releases, subs, packages, coops, clients, globals. +- Source of truth для **внешнего** каталога; native extensions on-chain пока не существуют. + +### 1.4. Apps-catalog (внешний сервис) + +- Репозиторий `C9S/apps-catalog`, ветка `dev`. ca-auth (npm-pull-proxy) + ca-admin (publisher control) + Nexus + Postgres. Связь с mono — через `KE_RPC_URL=node:8888` и `MONO_BACKEND_URL=coopback:2998` (docker DNS `mono-shared` сеть). +- Концепция: tenant-кооператив (`romashka`, `voskhod`) → подключается → подписывается на пакет → ca-auth раздаёт tarballs из Nexus только когда on-chain `subs[tenant][package]` активна. + +## 2. To-be: целевая модель + +Цель — **два класса пакетов** в одном каталоге, разделённые по типу доставки: + +| Класс | Доставка | Жизненный цикл | Примеры | +|---|---|---|---| +| **bundle** | Компилируется в SSR-bundle desktop'а / NestJS-модуль coopback'а. Не качается из Nexus. | Версия = версия mono. Обновляется через `git pull && pnpm build`. | Сегодняшние 7 native: soviet, chairman, capital, … | +| **remote** | tarball в Nexus → ca-auth pull-proxy → dynamic import. NestJS-модуль динамически грузится в coopback. | Версия = `setrelease` on-chain. Обновляется без передеплоя mono. | Будущие: новые специализированные столы, форки текущих, частные пакеты кооперативов. | + +**Каталог отображает оба класса одинаково** в UI (`ExtensionStore`); внутри строки пакета — поле `kind: bundle | remote`. Для `bundle` кнопка «Установить» скрыта/заменена на «Уже установлено», для `remote` — реальный install-flow через подписку + pull. + +## 3. Промежуточный слой совместимости (dual mode) + +### 3.1. Регистрация bundle-пакетов в каталоге как «pre-installed» + +**Минимальный шов** на стороне apps-catalog: +- Добавить на on-chain контракт `apps` (или в admin-проекцию ca-admin) **флаг bundle** или отдельную таблицу `bundled_packages[coopname, package_id, mono_version]`. Запись означает: «этот кооператив получает пакет в составе bundle Mono, в каталоге показываем как already-installed, подписки не требуем». +- ca-auth при запросе на pull проверяет: bundle → отказ с понятной ошибкой (или редирект на «уже встроено»). + +**Источник истинности — каталог**, не Mono: +- Mono при старте `useInitExtensionsProcess` помимо bundle-загрузки **посылает в ca-admin** список своих bundled-пакетов (POST `/v1/admin/bundled-packages/sync` с api-key или mTLS). +- ca-admin записывает их с `kind='bundle'` и `compatible_subnets=[mono.chain_id]`. + +**Регистрация bundle-пакетов в каталоге** — однократная (idempotent на mono_version + chain_id), не требует Nexus-репозитория. + +### 3.2. Маркировка native extension'ов в коде + +В каждом из 7 extension'ов завести **manifest-файл** (mirror к [apps-catalog/libs/core/manifest/package-manifest.schema.ts](../../../../apps-catalog/libs/core/manifest/package-manifest.schema.ts)): + +```jsonc +// components/desktop/extensions/soviet/package.json +{ + "name": "@coopenomics/soviet", + "version": "0.1.0", + "catalog": { + "kind": "bundle", + "title": "Стол Совета", + "icon": "fa-solid fa-gavel", + "workspaces": ["soviet"], + "roles": ["chairman", "member"], + "mono_min_version": "0.0.0" + } +} +``` + +Скрипт `tools/sync-bundled-packages.ts` собирает все 7 manifest'ов + chain_id + mono version → POST в ca-admin. + +### 3.3. Динамическая загрузка (для remote-класса) + +**Frontend:** +- Расширить `extensionsRegistry` чтобы значения могли быть `() => Promise` (как сейчас, bundle) **или** `{ kind: 'remote', tarballUrl, version }` — резолвер сам качает tarball через ca-auth proxy, распаковывает в Quasar Boot-script хранилище, dynamic-import'ит `install.ts` из распакованного бандла, кэширует. +- Vue Router добавление routes остаётся то же (`router.addRoute('base', r)`). + +**Backend (coopback):** +- Аналогично — `extensions.registry.ts` принимает динамические записи. При первом запросе кооператива на remote-package coopback: + 1. Через `MonoBackendPort` (либо прямой HTTP к ca-auth) проверяет подписку on-chain. + 2. Качает tarball через ca-auth (с JWT кооперативного backend'а). + 3. Распаковывает в `/var/lib/coopback/extensions/@/`. + 4. `dynamic import` ESM-модуля; регистрирует Module через NestJS dynamic module pattern (тот же паттерн, что у текущих PluginModule). + +**Изоляция:** для безопасности — sandbox через VM2/Workers или хотя бы хеш-проверку tarball'а (Nexus отдаёт SHA-256, on-chain `releases` фиксирует ожидаемый hash). + +### 3.4. Coexistence rules + +- Один и тот же `package_id` не может одновременно быть `bundle` и `remote` для одного кооператива. Если on-chain появилась подписка на `@coopenomics/soviet` (remote), а в bundle есть тот же package_id — **bundle wins** до явного выключения через `bundled_packages.disabled_at`. +- Когда extension мигрирует из bundle → remote (см. п. 4): сначала выпускается `remote` версия в каталог, тестируется на одном пилоте, потом `disabled_at` ставится в `bundled_packages` Mono'а, и bundle-fallback гасится. + +## 4. План миграции по extension'ам (поштучно) + +| Extension | Сложность | Зависимости | Кандидат №… | +|---|---|---|---| +| **chatcoop** | низкая — изолированный чат-стол, минимум интеграции с core | core ws-bridge, member-info | **№1 (пилот)** | +| **reports** | низкая-средняя — read-only отчёты | analytics endpoints | №2 | +| **powerup** | средняя — затрагивает ledger | wallet | №3 | +| **chairman** | высокая — связан с onboarding + agreements | grants, agreements | №4 | +| **soviet** | высокая — управление повесткой совета, on-chain actions | proposals, agreements | №5 | +| **participant** | высокая — base extension для пайщика | accounts | №6 | +| **capital** | максимальная — финансовые операции, capital_issues | ledger2, time-tracking | №7 (последний) | + +Каждая миграция = отдельная story в apps-catalog репозитории (Epic 9 в дополнении), parallel — story в mono (вытаскивание extension'а в отдельный pnpm-workspace пакет + tarball-сборка через `pnpm publish` в Nexus). + +## 5. Bootstrap-пакет + +Для подтверждения трубы — `@voskhod/test-app`: +- Сегодня уже зарегистрирован через POST `/v1/admin/package` (smoke-стенд, request_id `98b1d8ac-…`). +- В рамках Bootstrap-стори: + - Сгенерировать минимальный tarball (1 page, 1 route) с `package.json` манифестом. + - `npm publish --registry=http://ca-auth:3001/v1/registry/` (с tenant JWT кооператива voskhod). + - `apps::setrelease` on-chain. + - Проверить, что desktop'у через `loadExtensionRoutes('@voskhod/test-app')` action раздаёт routes из remote tarball. + +Bootstrap не претендует на «настоящий» сценарий — это инфра-проверка трубы для последующей реальной миграции (Кандидат №1 = chatcoop). + +## 6. Открытые вопросы + +1. **Где живёт `package_id` для bundle?** Предложение: `@coopenomics/` (org scope = монорепозиторий). Если кооператив хочет форкнуть — он публикует под своим scope `@voskhod/` с другим bundle'ом. +2. **Можно ли остановить bundle-загрузку extension'а в Mono per кооператив?** Сегодня bundle грузится всегда; видимость регулируется backend grants. То есть мы можем ВЫПИЛИВАТЬ extension из bundle и заменить на remote — но не для одного кооператива из десяти. Возможный путь — feature-flag через ENV на инстансе mono (один Mono = один кооператив сегодня). +3. **Sandbox для remote-модулей.** Без изоляции remote-extension с правами NestJS-модуля = админ-доступ к coopback. Нужен или подписанный паблишер (allowlist on-chain `apps::pubpub`), или VM2-sandbox, или статический анализ tarball'а перед запуском. +4. **Миграции схемы БД для remote-extension.** Сегодня `pluginClass.runMigrations(schema)` запускается на старте монолита. Для remote — нужен hook на «новый extension установлен» с автоматическим прогоном миграций. Open question: что если миграция падает? Откат subscription on-chain невозможен (releases immutable). +5. **Версия Mono ↔ совместимость extension'а.** `catalog.mono_min_version` в манифесте; ca-admin при `setrelease` проверяет совместимость с активной mono_version подсетей `compatible_subnets`. +6. **UX миграции для уже-установленных native.** Когда extension переезжает bundle → remote, пользователь должен: + - Увидеть в каталоге, что обновлена доставка (info badge «теперь обновляется отдельно»). + - НЕ потерять данные / config (миграция identifiers in БД). + - Иметь fallback на bundle, если remote tarball недоступен (offline mode). + +## 7. Что НЕ входит в этот research + +- Конкретный код миграции какого-либо extension'а (это будут отдельные stories). +- Решение об IDE/sandbox/изоляции — это отдельное исследование безопасности. +- Дизайн UI «магазина» в ExtensionStore (мы переиспользуем существующие страницы, только добавляем kind/version поля). +- Pricing / биллинг внутри Mono — каталог уже это решает (контракт apps + ca-admin). + +## 8. Решение V1: не мигрируем, делаем демо-пилот + +**Утверждено 2026-06-02.** Native extensions трогать не будем. Вместо bundle→remote миграции собираем минимальное демо-приложение, ставим его сами как внешний разработчик и проводим полный цикл с оплатой. Это снимает риски с native и подтверждает контур end-to-end. + +### Решения по содержанию пилота + +| Тема | Решение | Замечание | +|---|---|---| +| Что делает demo-app | Вариант A — пустой Hello-стол с одним роутом + иконкой | Минимум контента, максимум инфра-трубы | +| `package_id` | `@voskhod/demo-app` | scope = coopname-разработчик | +| Разработчик (`owner_username`) | `voskhod` (кооператив-аккаунт) | Публикует **chairman voskhod** через `@active` | +| Pricing | `default` = **1000 RUB / месяц** prod / **60 сек dev** | period в `globals.min_payment_period_seconds` | +| Trial | **15 минут** в dev (нет prod-trial в V1) | `globals.lead_time_seconds=900` | +| Покупатель-пилот | `partner1` (поднимается через `pnpm boot:extra` в mono-ai-5) | Та же цепь voskhod'а, не отдельная chain | +| Self-subscription (voskhod) | Видит свой demo-app, подписывается без оплаты | Bypass: если `tenant_coop == package.owner_coop` → charge skip | +| Кэш tarball'ов | Без кэша — каждый desktop boot качает заново | Простой sync-loop, без storage | +| UI магазина | Переиспользуем `pages/ExtensionStore` (минимальные правки) | Добавляем поле `kind=bundle\|remote` | +| Стол разработчика | **Новый bundle-extension `developer`**, виден только chairman'у оператора | UI публикации + список подписчиков + pricing | + +### Инфра для одновременной работы двух кооперативов + +Сценарий «оператор-разработчик ↔ кооператив-потребитель» требует двух **независимых UI-окружений** в одной dev-машине: +- voskhod's desktop+coopback — на mono-ai-5 (`COOPNAME=voskhod`, порты 3038/3039). +- partner1's desktop+coopback — поднимаем **через docker-compose overlay в apps-catalog** (`COOPNAME=partner1`, порты 3048/3049), указывающие на ту же node mono-ai-5:8930 (`CHAIN_URL=http://node:8888`). +- Никакой второй chain не нужен. partner1 активируется на воскод-цепи через `pnpm boot:extra` (`mono-ai-5/components/boot/src/init/infra.ts:584`). + +Это компромисс: coopback/desktop single-coopname остаются как есть, multi-tenancy откладываем. + +## 9. Epic 9 «MVP demo-app pilot E2E» + +Зонтичный эпик. Параллельная работа в двух репозиториях: +- **apps-catalog** — `examples/demo-app/`, overlay для partner1, self-sub bypass, dev-seed. +- **mono-ai-5** ветка `apps-catalog` — стол разработчика, remote-loader в init-installed-extensions, минимальные правки ExtensionStore. + +### Stories + +**9.1 examples/demo-app/ в apps-catalog** +- Скаффолд: `package.json` с `catalog.kind=remote, workspaces=['demo']`, `install.ts` с одним IWorkspaceConfig (стол «Demo», роут `/:coopname/demo` с Hello-страницей). +- Builder: `pnpm pack` → `demo-app-0.1.0.tgz`. +- README с инструкцией публикации. + +**9.2 docker-compose.partner1.overlay.yml в apps-catalog** +- Overlay для dev-stack: `coopback-partner1` (порт 3048, COOPNAME=partner1, MONGO_PARTNER1_DB), `desktop-partner1` (порт 3049, тот же coopback-partner1). +- Подключение к `mono-shared` сети с aliases. +- Скрипт `pnpm dev:stack:up-partner1` поднимает overlay поверх базового. +- Документация: «партнерский desktop по `http://localhost:3049`, входить как admin partner1 (WIF из docs-harness/state/cooperatives/partner1.json)». + +**9.3 Стол Разработчика в mono (`extensions/developer`)** +- Bundle-extension в `components/desktop/extensions/developer/`: + - install.ts: workspace `developer`, роуты «Мои пакеты», «Опубликовать релиз», «Подписчики», «Pricing». + - Виден только chairman'у кооператива-оператора через `meta.roles=['chairman']` + grants на бэкенде. +- Backend модуль `controller/src/extensions/developer/`: + - Поднимается в `AppRegistry` как `is_internal=true, is_available=true`. + - GraphQL: `myPackages`, `publishPackage`, `setReleasePricing`, `packageSubscribers`. + - Делегирует в ca-admin через HTTP с админ-ключом. +- Self-sub bypass: «У вас уже доступ как разработчику» (без отдельной кнопки). + +**9.4 Remote-loader в `processes/init-installed-extensions`** +- После bundle-pass — запрос в coopback `getInstalledRemotePackages(coopname) → [{package_id, version, tarballUrl}]`. +- Для каждого: pull tarball через ca-auth pull-proxy с JWT кооператива. +- Распаковка в памяти (не на диск — без кэша). +- `Function('return ' + installSource)()` или dynamic `eval` install.ts → `IWorkspaceConfig[]` → `router.addRoute('base', ...)`. +- Ошибка на одном extension'е не валит остальные (try-catch вокруг каждого). + +**9.5 pages/ExtensionStore: поле `kind` + новые кнопки** +- Расширить DTO ответа `extensions catalogList` полями `kind: 'bundle'|'remote'`, `version`, `pricingRubPerMonth`, `isSelfOwned`. +- `kind=bundle` → «Уже установлено». +- `kind=remote && isSelfOwned` → «Доступно как разработчику» (без оплаты). +- `kind=remote && !isSelfOwned && !subscribed` → «Подписаться 1000 RUB/мес». +- `kind=remote && subscribed` → «Активная подписка до DD.MM». +- Резолвер дёргает coopback'овский новый endpoint, который ходит в ca-auth `GET /v1/registry/packages`. + +**9.6 Self-subscription bypass в pricing-watcher** +- В `apps-catalog/apps/ca-admin/src/modules/pricing-watcher/application/request-charge.use-case.ts`: + - Перед cadence-check — проверка `tenantCoopname === packageOwnerCoopname`. + - Если match → outcome=`'skip-self-subscription'`, sub продлевается через `extendsub` без charge. + - Audit-log: `pricing.self_subscription_skipped` с `package_id` + `coopname`. +- Тест: `request-charge.use-case.spec.ts` — voskhod подписан на свой `@voskhod/demo-app` → tick → no charge, sub.end_at += period. + +**9.7 dev-seed: globals + pricing** +- Скрипт `apps-catalog/scripts/seed-dev-pricing.ts`: + - `apps::setglobals` `lead_time=900`, `min_payment_period=60`, `retry_max=3`. + - `apps::setpricing @voskhod/demo-app default plan {RUB 1000}`. +- Запускается из `dev:stack:up`. + +**9.8 E2E smoke test demo-app pilot** +- Скрипт `test/e2e/demo-app-pilot.sh`: + 1. Поднимаем стенд + partner1 overlay. + 2. chairman voskhod публикует через стол разработчика → `apps::regpkg` + tarball в Nexus + `apps::setrelease`. + 3. partner1 admin подписывается через `pages/ExtensionStore` → `apps::regsub`. + 4. Ждём 70 секунд (один pricing-watcher тик) → проверка `apps::charge_intents` появилась запись → `extendsub` прошёл. + 5. Перезагрузка `desktop-partner1` → remote-loader тянет tarball → стол «Demo» в drawer'e. + 6. `apps::cancelsub` → следующий тик → стол исчезает. +- Pass criteria: все 6 шагов зелёные. + +### Order of execution + +``` +9.1 (examples/demo-app) ──┐ +9.2 (partner1 overlay) ─├─→ 9.7 (dev-seed) ─→ 9.8 (E2E smoke) +9.6 (self-sub bypass в ca-admin) ──┘ ↑ + │ +mono-сторона: │ +9.3 (стол разработчика) ─→ 9.5 (ExtensionStore) ─→ 9.4 (remote-loader) ─┘ +``` + +Все mono-stories идут в зонтичный PR `apps-catalog` → dev в mono'е (один PR на эпик). +Все apps-catalog-stories идут в зонтичный PR `feat/E9-demo-app-pilot` → dev в apps-catalog. + +### Не входит в Epic 9 + +- Реальная миграция native extensions (отложена на Epic 10+). +- Sandbox-изоляция remote-модулей (Epic 11, открытый вопрос #3). +- Production-pricing с реальным RUB-токеном / реальным биллингом — V1 крутит in-chain RUB-like asset. +- Multi-tenancy coopback'а (отложена; в V1 = два отдельных coopback'а). diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e95887691ca..5d0a4d1ffb9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -259,6 +259,9 @@ importers: '@a2seven/yoo-checkout': specifier: ^1.1.4 version: 1.1.4 + '@apollo/subgraph': + specifier: ^2 + version: 2.14.1(graphql@16.13.2) '@aws-sdk/client-s3': specifier: ^3.1035.0 version: 3.1046.0 @@ -294,7 +297,7 @@ importers: version: 0.13.24 '@nestjs/apollo': specifier: ^12.2.1 - version: 12.2.2(@apollo/server@4.13.0(encoding@0.1.13)(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.22)(@nestjs/graphql@12.2.2(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(graphql@16.13.2)(reflect-metadata@0.1.14))(graphql@16.13.2) + version: 12.2.2(@apollo/gateway@2.14.1(encoding@0.1.13)(graphql@16.13.2))(@apollo/server@4.13.0(encoding@0.1.13)(graphql@16.13.2))(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.22)(@nestjs/graphql@12.2.2(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(graphql@16.13.2)(reflect-metadata@0.1.14))(graphql@16.13.2) '@nestjs/bull': specifier: ^10.2.1 version: 10.2.3(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.22)(bull@4.16.5) @@ -312,7 +315,7 @@ importers: version: 3.0.1(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.22) '@nestjs/graphql': specifier: ^12.2.1 - version: 12.2.2(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(graphql@16.13.2)(reflect-metadata@0.1.14) + version: 12.2.2(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(graphql@16.13.2)(reflect-metadata@0.1.14) '@nestjs/jwt': specifier: ^11.0.0 version: 11.0.2(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2)) @@ -1064,6 +1067,100 @@ importers: specifier: ^0.34.5 version: 0.34.5 + components/extension-hello-subgraph: + dependencies: + '@apollo/subgraph': + specifier: ^2.9.0 + version: 2.14.1(graphql@16.13.2) + '@coopenomics/extension-sdk': + specifier: workspace:* + version: link:../extension-sdk + '@nestjs/apollo': + specifier: ^12.2.1 + version: 12.2.2(@apollo/gateway@2.14.1(encoding@0.1.13)(graphql@16.13.2))(@apollo/server@4.13.0(encoding@0.1.13)(graphql@16.13.2))(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(@nestjs/graphql@12.2.2(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(graphql@16.13.2)(reflect-metadata@0.2.2))(graphql@16.13.2) + '@nestjs/common': + specifier: ^10.4.0 + version: 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': + specifier: ^10.4.0 + version: 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/graphql': + specifier: ^12.2.1 + version: 12.2.2(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(graphql@16.13.2)(reflect-metadata@0.2.2) + '@nestjs/passport': + specifier: ^10.0.3 + version: 10.0.3(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) + '@nestjs/platform-express': + specifier: ^10.4.0 + version: 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22) + graphql: + specifier: ^16.9.0 + version: 16.13.2 + passport-jwt: + specifier: ^4.0.1 + version: 4.0.1 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.1 + version: 7.8.2 + devDependencies: + '@types/node': + specifier: ^20.0.0 + version: 20.19.37 + '@types/passport-jwt': + specifier: ^4.0.1 + version: 4.0.1 + ts-node-dev: + specifier: ^2.0.0 + version: 2.0.0(@swc/core@1.15.33(@swc/helpers@0.5.19))(@types/node@20.19.37)(typescript@5.9.3) + typescript: + specifier: ^5.4.0 + version: 5.9.3 + + components/extension-sdk: + devDependencies: + '@apollo/subgraph': + specifier: ^2.9.0 + version: 2.14.1(graphql@16.13.2) + '@nestjs/apollo': + specifier: ^12.2.1 + version: 12.2.2(@apollo/gateway@2.14.1(encoding@0.1.13)(graphql@16.13.2))(@apollo/server@4.13.0(encoding@0.1.13)(graphql@16.13.2))(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(@nestjs/graphql@12.2.2(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(graphql@16.13.2)(reflect-metadata@0.2.2))(graphql@16.13.2) + '@nestjs/common': + specifier: ^10.4.0 + version: 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': + specifier: ^10.4.0 + version: 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/graphql': + specifier: ^12.2.1 + version: 12.2.2(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(graphql@16.13.2)(reflect-metadata@0.2.2) + '@nestjs/passport': + specifier: ^10.0.3 + version: 10.0.3(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) + '@types/passport-jwt': + specifier: ^4 + version: 4.0.1 + graphql: + specifier: ^16.9.0 + version: 16.13.2 + passport-jwt: + specifier: ^4.0.1 + version: 4.0.1 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.1 + version: 7.8.2 + typescript: + specifier: ^5.4.0 + version: 5.9.3 + unbuild: + specifier: ^2.0.0 + version: 2.0.0(sass@1.98.0)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3)) + components/factory: dependencies: ajv: @@ -1167,6 +1264,64 @@ importers: specifier: ^1.6.0 version: 1.6.1(@types/node@20.19.37)(lightningcss@1.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1) + components/gateway-orchestrator: + dependencies: + '@apollo/gateway': + specifier: ^2.9.0 + version: 2.14.1(encoding@0.1.13)(graphql@16.13.2) + '@nestjs/apollo': + specifier: ^12.2.1 + version: 12.2.2(@apollo/gateway@2.14.1(encoding@0.1.13)(graphql@16.13.2))(@apollo/server@4.13.0(encoding@0.1.13)(graphql@16.13.2))(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(@nestjs/graphql@12.2.2(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(graphql@16.13.2)(reflect-metadata@0.2.2))(graphql@16.13.2) + '@nestjs/common': + specifier: ^10.4.0 + version: 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': + specifier: ^10.4.0 + version: 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/graphql': + specifier: ^12.2.1 + version: 12.2.2(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(graphql@16.13.2)(reflect-metadata@0.2.2) + '@nestjs/platform-express': + specifier: ^10.4.0 + version: 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22) + '@nestjs/typeorm': + specifier: ^10.0.2 + version: 10.0.2(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.20(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@swc/core@1.15.33(@swc/helpers@0.5.19))(@types/node@20.19.37)(typescript@5.9.3))) + graphql: + specifier: ^16.9.0 + version: 16.13.2 + pg: + specifier: ^8.13.0 + version: 8.20.0 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.1 + version: 7.8.2 + typeorm: + specifier: ^0.3.20 + version: 0.3.20(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@swc/core@1.15.33(@swc/helpers@0.5.19))(@types/node@20.19.37)(typescript@5.9.3)) + devDependencies: + '@types/jest': + specifier: ^29.5.0 + version: 29.5.14 + '@types/node': + specifier: ^20.0.0 + version: 20.19.37 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@20.19.37)(ts-node@10.9.2(@swc/core@1.15.33(@swc/helpers@0.5.19))(@types/node@20.19.37)(typescript@5.9.3)) + ts-jest: + specifier: ^29.1.0 + version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(esbuild@0.27.4)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.37)(ts-node@10.9.2(@swc/core@1.15.33(@swc/helpers@0.5.19))(@types/node@20.19.37)(typescript@5.9.3)))(typescript@5.9.3) + ts-node-dev: + specifier: ^2.0.0 + version: 2.0.0(@swc/core@1.15.33(@swc/helpers@0.5.19))(@types/node@20.19.37)(typescript@5.9.3) + typescript: + specifier: ^5.4.0 + version: 5.9.3 + components/inter: devDependencies: typescript: @@ -1247,6 +1402,64 @@ importers: specifier: ^4.0.18 version: 4.1.1(@opentelemetry/api@1.9.1)(@types/node@20.19.37)(vite@8.0.3(@types/node@20.19.37)(esbuild@0.27.4)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + components/orchestrator: + dependencies: + '@apollo/gateway': + specifier: ^2.9.0 + version: 2.14.1(encoding@0.1.13)(graphql@16.13.2) + '@nestjs/apollo': + specifier: ^12.2.1 + version: 12.2.2(@apollo/gateway@2.14.1(encoding@0.1.13)(graphql@16.13.2))(@apollo/server@4.13.0(encoding@0.1.13)(graphql@16.13.2))(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(@nestjs/graphql@12.2.2(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(graphql@16.13.2)(reflect-metadata@0.2.2))(graphql@16.13.2) + '@nestjs/common': + specifier: ^10.4.0 + version: 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': + specifier: ^10.4.0 + version: 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/graphql': + specifier: ^12.2.1 + version: 12.2.2(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(graphql@16.13.2)(reflect-metadata@0.2.2) + '@nestjs/platform-express': + specifier: ^10.4.0 + version: 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22) + '@nestjs/typeorm': + specifier: ^10.0.2 + version: 10.0.2(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.20(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@swc/core@1.15.33(@swc/helpers@0.5.19))(@types/node@20.19.37)(typescript@5.9.3))) + graphql: + specifier: ^16.9.0 + version: 16.13.2 + pg: + specifier: ^8.13.0 + version: 8.20.0 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.1 + version: 7.8.2 + typeorm: + specifier: ^0.3.20 + version: 0.3.20(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@swc/core@1.15.33(@swc/helpers@0.5.19))(@types/node@20.19.37)(typescript@5.9.3)) + devDependencies: + '@types/jest': + specifier: ^29.5.0 + version: 29.5.14 + '@types/node': + specifier: ^20.0.0 + version: 20.19.37 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@20.19.37)(ts-node@10.9.2(@swc/core@1.15.33(@swc/helpers@0.5.19))(@types/node@20.19.37)(typescript@5.9.3)) + ts-jest: + specifier: ^29.1.0 + version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(esbuild@0.27.4)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.37)(ts-node@10.9.2(@swc/core@1.15.33(@swc/helpers@0.5.19))(@types/node@20.19.37)(typescript@5.9.3)))(typescript@5.9.3) + ts-node-dev: + specifier: ^2.0.0 + version: 2.0.0(@swc/core@1.15.33(@swc/helpers@0.5.19))(@types/node@20.19.37)(typescript@5.9.3) + typescript: + specifier: ^5.4.0 + version: 5.9.3 + components/parser: dependencies: '@blockmatic/eosio-ship-reader': @@ -1627,6 +1840,24 @@ packages: subscriptions-transport-ws: optional: true + '@apollo/composition@2.14.1': + resolution: {integrity: sha512-VWtB2jb19lnR5JaD181x7g2oyCpNsLEAetYdB41McmxHIemFAv0AEs679bPmr0IL8dzf74Cm8dL5bxWCaHBnBQ==} + engines: {node: '>=18'} + peerDependencies: + graphql: ^16.5.0 + + '@apollo/federation-internals@2.14.1': + resolution: {integrity: sha512-BVgwDr9H9mR/9LEzRzhBXJqo2cYdjkwSK/2uEgMa+B45oXvMI5r69y6qdFvMjRyP1R6y4wjnR9UtYGpx5uhhdw==} + engines: {node: '>=18'} + peerDependencies: + graphql: ^16.5.0 + + '@apollo/gateway@2.14.1': + resolution: {integrity: sha512-iy7+67nXa8gmRoFyIBLDa5BfYzJJfCn/RFI2PJF/wTG55YJ82vGWdvQZ3gPRmO+mDFyza60ZMPhR3jKMG8EnEg==} + engines: {node: '>=14.15.0'} + peerDependencies: + graphql: ^16.5.0 + '@apollo/protobufjs@1.2.6': resolution: {integrity: sha512-Wqo1oSHNUj/jxmsVp4iR3I480p6qdqHikn38lKrFhfzcDJ7lwd7Ck7cHRl4JE81tWNArl77xhnG/OkZhxKBYOw==} hasBin: true @@ -1635,6 +1866,18 @@ packages: resolution: {integrity: sha512-Lahx5zntHPZia35myYDBRuF58tlwPskwHc5CWBZC/4bMKB6siTBWwtMrkqXcsNwQiFSzSx5hKdRPUmemrEp3Gg==} hasBin: true + '@apollo/query-graphs@2.14.1': + resolution: {integrity: sha512-gi3TWpkrr7Yq1SqpDdZp0yWtnIMQxrwd1K8wJJnp5KBiebH3qT20zcyBqKNDEGvE5Q9+4neH0g1I2xElwpLg5w==} + engines: {node: '>=18'} + peerDependencies: + graphql: ^16.5.0 + + '@apollo/query-planner@2.14.1': + resolution: {integrity: sha512-nUxh4VTdyqtMkncUkCtKQ9UmUCcm0RelIcGR9CXEn8iAbO/FM4wN9xeT5o5O2bVus+OLd8o9GmQ5UppqTwTqUw==} + engines: {node: '>=14.15.0'} + peerDependencies: + graphql: ^16.5.0 + '@apollo/server-gateway-interface@1.1.1': resolution: {integrity: sha512-pGwCl/po6+rxRmDMFgozKQo2pbsSwE91TpsDBAOgf74CRDPXHHtM88wbwjab0wMMZh95QfR45GGyDIdhY24bkQ==} deprecated: '@apollo/server-gateway-interface v1 is part of Apollo Server v4, which is deprecated and will transition to end-of-life on January 26, 2026. As long as you are already using a non-EOL version of Node.js, upgrading to v2 should take only a few minutes. See https://www.apollographql.com/docs/apollo-server/previous-versions for details.' @@ -1655,6 +1898,12 @@ packages: peerDependencies: graphql: ^16.6.0 + '@apollo/subgraph@2.14.1': + resolution: {integrity: sha512-8ixeiaaZDGOsqf1/KhOTrQcBYZquS0PWZhoSiak+7JZPzrc5RT+f9Mnsc0NWgdbdhqQHAxFay2rz/9Ij0SjCMg==} + engines: {node: '>=14.15.0'} + peerDependencies: + graphql: ^16.5.0 + '@apollo/usage-reporting-protobuf@4.1.1': resolution: {integrity: sha512-u40dIUePHaSKVshcedO7Wp+mPiZsaU6xjv9J+VyxpoU/zL6Jle+9zWeG98tr/+SZ0nZ4OXhrbb8SNr0rAPpIDA==} @@ -3855,6 +4104,10 @@ packages: '@fortawesome/fontawesome-svg-core': ~1 || ~6 || ~7 vue: '>= 3.0.0 < 4' + '@gar/promise-retry@1.0.3': + resolution: {integrity: sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==} + engines: {node: ^20.17.0 || >=22.9.0} + '@gerrit0/mini-shiki@3.23.0': resolution: {integrity: sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==} @@ -5433,6 +5686,10 @@ packages: resolution: {integrity: sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==} engines: {node: ^18.17.0 || >=20.5.0} + '@npmcli/agent@4.0.2': + resolution: {integrity: sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg==} + engines: {node: ^20.17.0 || >=22.9.0} + '@npmcli/arborist@7.5.4': resolution: {integrity: sha512-nWtIc6QwwoUORCRNzKx4ypHqCk3drI+5aeYdMTQQiRCcn4lOOgfQh7WyZobGYTxXPSq1VwV53lkpN/BRlRk08g==} engines: {node: ^16.14.0 || >=18.0.0} @@ -5446,6 +5703,10 @@ packages: resolution: {integrity: sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==} engines: {node: ^18.17.0 || >=20.5.0} + '@npmcli/fs@5.0.0': + resolution: {integrity: sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==} + engines: {node: ^20.17.0 || >=22.9.0} + '@npmcli/git@5.0.8': resolution: {integrity: sha512-liASfw5cqhjNW9UFd+ruwwdEf/lbOAQjLL2XY2dFW/bkJheXDYZgOyul/4gVvEV4BWkTXjYGmDqMw9uegdbJNQ==} engines: {node: ^16.14.0 || >=18.0.0} @@ -5487,6 +5748,10 @@ packages: resolution: {integrity: sha512-YgsR5jCQZhVmTJvjduTOIHph0L73pK8xwMVaDY0PatySqVM9AZj93jpoXYSJqfHFxFkN9dmqTw6OiqExsS3LPw==} engines: {node: ^16.14.0 || >=18.0.0} + '@npmcli/redact@4.0.0': + resolution: {integrity: sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==} + engines: {node: ^20.17.0 || >=22.9.0} + '@npmcli/run-script@8.1.0': resolution: {integrity: sha512-y7efHHwghQfk28G2z3tlZ67pLG0XdfYbcVG26r7YIXALRsrVQcTq4/tdenSmdOrEsNahIYA/eh8aEVROWGFUDg==} engines: {node: ^16.14.0 || >=18.0.0} @@ -5722,6 +5987,10 @@ packages: resolution: {integrity: sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==} engines: {node: '>=14'} + '@opentelemetry/api-logs@0.57.2': + resolution: {integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==} + engines: {node: '>=14'} + '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} @@ -5748,6 +6017,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@1.30.1': + resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.6.0': resolution: {integrity: sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==} engines: {node: ^18.19.0 || >=20.6.0} @@ -5760,6 +6035,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/exporter-metrics-otlp-http@0.57.2': + resolution: {integrity: sha512-ttb9+4iKw04IMubjm3t0EZsYRNWr3kg44uUuzfo9CaccYlOh8cDooe4QObDUkvx9d5qQUrbEckhrWKfJnKhemA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-trace-otlp-http@0.53.0': resolution: {integrity: sha512-m7F5ZTq+V9mKGWYpX8EnZ7NjoqAU7VemQ1E2HAG+W/u0wpY1x0OmbxAXfGKFHCspdJk8UKlwPGrpcB8nay3P8A==} engines: {node: '>=14'} @@ -5928,12 +6209,24 @@ packages: peerDependencies: '@opentelemetry/api': ^1.0.0 + '@opentelemetry/otlp-exporter-base@0.57.2': + resolution: {integrity: sha512-XdxEzL23Urhidyebg5E6jZoaiW5ygP/mRjxLHixogbqwDy2Faduzb5N0o/Oi+XTIJu+iyxXdVORjXax+Qgfxag==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-transformer@0.53.0': resolution: {integrity: sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-transformer@0.57.2': + resolution: {integrity: sha512-48IIRj49gbQVK52jYsw70+Jv+JbahT8BqT2Th7C4H7RCM9d0gZ5sgNPoMpWldmfjvIsSgiGJtjfk9MeZvjhoig==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/propagator-b3@1.26.0': resolution: {integrity: sha512-vvVkQLQ/lGGyEy9GT8uFnI047pajSOVnZI2poJqVGD3nJ+B9sFGdlHNnQKophE3lHfnIH0pw2ubrCTjZCgIj+Q==} engines: {node: '>=14'} @@ -5950,12 +6243,42 @@ packages: resolution: {integrity: sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==} engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/resource-detector-alibaba-cloud@0.30.1': + resolution: {integrity: sha512-9l0FVP3F4+Z6ax27vMzkmhZdNtxAbDqEfy7rduzya3xFLaRiJSvOpw6cru6Edl5LwO+WvgNui+VzHa9ViE8wCg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-aws@1.12.0': + resolution: {integrity: sha512-Cvi7ckOqiiuWlHBdA1IjS0ufr3sltex2Uws2RK6loVp4gzIJyOijsddAI6IZ5kiO8h/LgCWe8gxPmwkTKImd+Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-azure@0.6.1': + resolution: {integrity: sha512-Djr31QCExVfWViaf9cGJnH+bUInD72p0GEfgDGgjCAztyvyji6WJvKjs6qmkpPN+Ig6KLk0ho2VgzT5Kdl4L2Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-gcp@0.33.1': + resolution: {integrity: sha512-/aZJXI1rU6Eus04ih2vU0hxXAibXXMzH1WlDZ8bXcTJmhwmTY8cP392+6l7cWeMnTQOibBUz8UKV3nhcCBAefw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + '@opentelemetry/resources@1.26.0': resolution: {integrity: sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/resources@1.30.1': + resolution: {integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/resources@2.6.1': resolution: {integrity: sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==} engines: {node: ^18.19.0 || >=20.6.0} @@ -5968,18 +6291,36 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/sdk-logs@0.57.2': + resolution: {integrity: sha512-TXFHJ5c+BKggWbdEQ/inpgIzEmS2BGQowLE9UhsMd7YYlUfBQJ4uax0VF/B5NYigdM/75OoJGhAV3upEhK+3gg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/sdk-metrics@1.26.0': resolution: {integrity: sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-metrics@1.30.1': + resolution: {integrity: sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace-base@1.26.0': resolution: {integrity: sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/sdk-trace-base@1.30.1': + resolution: {integrity: sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/sdk-trace-base@2.6.1': resolution: {integrity: sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw==} engines: {node: ^18.19.0 || >=20.6.0} @@ -5996,6 +6337,10 @@ packages: resolution: {integrity: sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==} engines: {node: '>=14'} + '@opentelemetry/semantic-conventions@1.28.0': + resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} + engines: {node: '>=14'} + '@opentelemetry/semantic-conventions@1.40.0': resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==} engines: {node: '>=14'} @@ -7553,6 +7898,15 @@ packages: '@types/pako@2.0.4': resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==} + '@types/passport-jwt@4.0.1': + resolution: {integrity: sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==} + + '@types/passport-strategy@0.2.38': + resolution: {integrity: sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==} + + '@types/passport@1.0.17': + resolution: {integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==} + '@types/pg-pool@2.0.7': resolution: {integrity: sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==} @@ -7604,6 +7958,12 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/strip-bom@3.0.0': + resolution: {integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==} + + '@types/strip-json-comments@0.0.30': + resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==} + '@types/stylis@4.2.7': resolution: {integrity: sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA==} @@ -7631,6 +7991,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/uuid@9.0.8': + resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + '@types/validator@13.15.10': resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} @@ -8851,6 +9214,9 @@ packages: bigi@1.4.2: resolution: {integrity: sha512-ddkU+dFIuEIW8lE7ZwdIAf2UPoM90eaprg5m3YXAVVTmKlqV/9BX4A2M8BOK2yOq6/VgZFVhK6QAxJebhlbhzw==} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + bin-links@4.0.4: resolution: {integrity: sha512-cMtq4W5ZsEwcutJrVId+a/tjt8GSbS+h0oNkdl6+6rBuEv8Ot33Bevj5KPm40t309zuhVic8NjpuL42QCiJWWA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -9069,6 +9435,10 @@ packages: resolution: {integrity: sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==} engines: {node: ^18.17.0 || >=20.5.0} + cacache@20.0.4: + resolution: {integrity: sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==} + engines: {node: ^20.17.0 || >=22.9.0} + cacheable-lookup@5.0.4: resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} engines: {node: '>=10.6.0'} @@ -10252,6 +10622,10 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -10526,6 +10900,9 @@ packages: duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + dynamic-dedupe@0.3.0: + resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==} + earcut@2.2.4: resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} @@ -10708,6 +11085,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -11710,10 +12090,18 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + gaze@1.1.3: resolution: {integrity: sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==} engines: {node: '>= 4.0.0'} + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -11884,6 +12272,10 @@ packages: 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 hasBin: true + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + glob@5.0.15: resolution: {integrity: sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==} 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 @@ -11957,6 +12349,10 @@ packages: resolution: {integrity: sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==} engines: {node: '>= 0.10'} + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -12583,6 +12979,10 @@ packages: is-alphanumerical@1.0.4: resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -13201,6 +13601,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -13819,6 +14222,10 @@ packages: resolution: {integrity: sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==} engines: {node: ^18.17.0 || >=20.5.0} + make-fetch-happen@15.0.6: + resolution: {integrity: sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==} + engines: {node: ^20.17.0 || >=22.9.0} + make-iterator@1.0.1: resolution: {integrity: sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==} engines: {node: '>=0.10.0'} @@ -14219,6 +14626,10 @@ packages: resolution: {integrity: sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==} engines: {node: ^18.17.0 || >=20.5.0} + minipass-fetch@5.0.2: + resolution: {integrity: sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==} + engines: {node: ^20.17.0 || >=22.9.0} + minipass-flush@1.0.6: resolution: {integrity: sha512-7Uf5gMJZ2kTkFisE3toGxT991s+cg+vMh42nbZGM2bNxfYVpkpqRudf1QrcOy72a3PwcL4JYqL+4NY7t0Hdd0A==} engines: {node: '>=16 || 14 >=14.17'} @@ -14231,6 +14642,10 @@ packages: resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} engines: {node: '>=8'} + minipass-sized@2.0.0: + resolution: {integrity: sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==} + engines: {node: '>=8'} + minipass@3.3.6: resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} engines: {node: '>=8'} @@ -14553,6 +14968,9 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-cpu-count@0.1.1: + resolution: {integrity: sha512-YeKJLH1t8AXmBLqYv1HBqH8OF7VSLyxKxh97+RFHDAKW1c+E7fL5ElK+mHxl9dn/r+TsxExm8o5vruZ9f7s/QA==} + node-cron@3.0.3: resolution: {integrity: sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==} engines: {node: '>=6.0.0'} @@ -14816,6 +15234,10 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -15805,6 +16227,10 @@ packages: resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} engines: {node: ^18.17.0 || >=20.5.0} + proc-log@6.1.0: + resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} + engines: {node: ^20.17.0 || >=22.9.0} + process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -17191,6 +17617,10 @@ packages: resolution: {integrity: sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==} engines: {node: ^18.17.0 || >=20.5.0} + ssri@13.0.1: + resolution: {integrity: sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==} + engines: {node: ^20.17.0 || >=22.9.0} + stable-hash-x@0.2.0: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} @@ -17824,6 +18254,10 @@ packages: typescript: optional: true + ts-graphviz@1.8.2: + resolution: {integrity: sha512-5YhbFoHmjxa7pgQLkB07MtGnGJ/yhvjmc9uhsnDBEICME6gkPf83SBwLDQqGDoCa3XzUMWLk1AU2Wn1u1naDtA==} + engines: {node: '>=14.16'} + ts-jest@29.4.6: resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} @@ -17857,6 +18291,17 @@ packages: ts-map@1.0.3: resolution: {integrity: sha512-vDWbsl26LIcPGmDpoVzjEP6+hvHZkBkLW7JpvwbCv/5IYPJlsbzCVXY3wsCeAxAUeTclNOUZxnLdGh3VBD/J6w==} + ts-node-dev@2.0.0: + resolution: {integrity: sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==} + engines: {node: '>=0.8.0'} + hasBin: true + peerDependencies: + node-notifier: '*' + typescript: '*' + peerDependenciesMeta: + node-notifier: + optional: true + ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -17882,6 +18327,9 @@ packages: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} + tsconfig@7.0.0: + resolution: {integrity: sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==} + tsd@0.31.2: resolution: {integrity: sha512-VplBAQwvYrHzVihtzXiUVXu5bGcr7uH1juQZ1lmKgkuGNGT+FechUCqmx9/zk7wibcqR2xaNEwCkDyKh+VVZnQ==} engines: {node: '>=14.16'} @@ -19632,6 +20080,55 @@ snapshots: subscriptions-transport-ws: 0.11.0(graphql@16.13.2) optional: true + '@apollo/composition@2.14.1(graphql@16.13.2)': + dependencies: + '@apollo/federation-internals': 2.14.1(graphql@16.13.2) + '@apollo/query-graphs': 2.14.1(graphql@16.13.2) + graphql: 16.13.2 + + '@apollo/federation-internals@2.14.1(graphql@16.13.2)': + dependencies: + '@types/uuid': 9.0.8 + chalk: 4.1.2 + graphql: 16.13.2 + js-levenshtein: 1.1.6 + uuid: 9.0.1 + + '@apollo/gateway@2.14.1(encoding@0.1.13)(graphql@16.13.2)': + dependencies: + '@apollo/composition': 2.14.1(graphql@16.13.2) + '@apollo/federation-internals': 2.14.1(graphql@16.13.2) + '@apollo/query-planner': 2.14.1(graphql@16.13.2) + '@apollo/server-gateway-interface': 1.1.1(graphql@16.13.2) + '@apollo/usage-reporting-protobuf': 4.1.1 + '@apollo/utils.createhash': 2.0.2 + '@apollo/utils.fetcher': 2.0.1 + '@apollo/utils.isnodelike': 2.0.1 + '@apollo/utils.keyvaluecache': 2.1.1 + '@apollo/utils.logger': 2.0.1 + '@josephg/resolvable': 1.0.1 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/resource-detector-alibaba-cloud': 0.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resource-detector-aws': 1.12.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resource-detector-azure': 0.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resource-detector-gcp': 0.33.1(@opentelemetry/api@1.9.1)(encoding@0.1.13) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@types/node-fetch': 2.6.13 + async-retry: 1.3.3 + graphql: 16.13.2 + loglevel: 1.9.2 + make-fetch-happen: 15.0.6 + node-abort-controller: 3.1.1 + node-cpu-count: 0.1.1 + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + - supports-color + '@apollo/protobufjs@1.2.6': dependencies: '@protobufjs/aspromise': 1.1.2 @@ -19663,6 +20160,24 @@ snapshots: '@types/long': 4.0.2 long: 4.0.0 + '@apollo/query-graphs@2.14.1(graphql@16.13.2)': + dependencies: + '@apollo/federation-internals': 2.14.1(graphql@16.13.2) + deep-equal: 2.2.3 + graphql: 16.13.2 + ts-graphviz: 1.8.2 + uuid: 9.0.1 + + '@apollo/query-planner@2.14.1(graphql@16.13.2)': + dependencies: + '@apollo/federation-internals': 2.14.1(graphql@16.13.2) + '@apollo/query-graphs': 2.14.1(graphql@16.13.2) + '@apollo/utils.keyvaluecache': 2.1.1 + chalk: 4.1.2 + deep-equal: 2.2.3 + graphql: 16.13.2 + pretty-format: 29.7.0 + '@apollo/server-gateway-interface@1.1.1(graphql@16.13.2)': dependencies: '@apollo/usage-reporting-protobuf': 4.1.1 @@ -19708,6 +20223,12 @@ snapshots: - encoding - supports-color + '@apollo/subgraph@2.14.1(graphql@16.13.2)': + dependencies: + '@apollo/cache-control-types': 1.0.3(graphql@16.13.2) + '@apollo/federation-internals': 2.14.1(graphql@16.13.2) + graphql: 16.13.2 + '@apollo/usage-reporting-protobuf@4.1.1': dependencies: '@apollo/protobufjs': 1.2.7 @@ -20217,7 +20738,7 @@ snapshots: '@babel/types': 7.29.0 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -20269,7 +20790,7 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.11 transitivePeerDependencies: @@ -20978,7 +21499,7 @@ snapshots: '@babel/parser': 7.29.2 '@babel/template': 7.28.6 '@babel/types': 7.29.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -21513,7 +22034,7 @@ snapshots: '@electron/get@2.0.3': dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) env-paths: 2.2.1 fs-extra: 8.1.0 got: 11.8.6 @@ -21992,7 +22513,7 @@ snapshots: '@eslint/config-array@0.21.2': dependencies: '@eslint/object-schema': 2.1.7 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) minimatch: 3.1.5 transitivePeerDependencies: - supports-color @@ -22016,7 +22537,7 @@ snapshots: '@eslint/eslintrc@0.4.3': dependencies: ajv: 6.14.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) espree: 7.3.1 globals: 13.24.0 ignore: 4.0.6 @@ -22030,7 +22551,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.14.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -22044,7 +22565,7 @@ snapshots: '@eslint/eslintrc@3.3.5': dependencies: ajv: 6.14.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -22134,6 +22655,8 @@ snapshots: '@fortawesome/fontawesome-svg-core': 7.2.0 vue: 3.5.31(typescript@5.9.3) + '@gar/promise-retry@1.0.3': {} + '@gerrit0/mini-shiki@3.23.0': dependencies: '@shikijs/engine-oniguruma': 3.23.0 @@ -22654,7 +23177,7 @@ snapshots: '@types/js-yaml': 4.0.9 '@whatwg-node/fetch': 0.10.13 chalk: 4.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) dotenv: 16.6.1 graphql: 16.13.2 graphql-request: 6.1.0(encoding@0.1.13)(graphql@16.13.2) @@ -22694,7 +23217,7 @@ snapshots: '@graphql-tools/schema@10.0.10(graphql@16.13.2)': dependencies: - '@graphql-tools/merge': 9.0.11(graphql@16.13.2) + '@graphql-tools/merge': 9.1.7(graphql@16.13.2) '@graphql-tools/utils': 10.11.0(graphql@16.13.2) graphql: 16.13.2 tslib: 2.8.1 @@ -23027,7 +23550,7 @@ snapshots: '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) minimatch: 3.1.5 transitivePeerDependencies: - supports-color @@ -23035,7 +23558,7 @@ snapshots: '@humanwhocodes/config-array@0.5.0': dependencies: '@humanwhocodes/object-schema': 1.2.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) minimatch: 3.1.5 transitivePeerDependencies: - supports-color @@ -24236,17 +24759,35 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@nestjs/apollo@12.2.2(@apollo/server@4.13.0(encoding@0.1.13)(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.22)(@nestjs/graphql@12.2.2(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(graphql@16.13.2)(reflect-metadata@0.1.14))(graphql@16.13.2)': + '@nestjs/apollo@12.2.2(@apollo/gateway@2.14.1(encoding@0.1.13)(graphql@16.13.2))(@apollo/server@4.13.0(encoding@0.1.13)(graphql@16.13.2))(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.22)(@nestjs/graphql@12.2.2(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(graphql@16.13.2)(reflect-metadata@0.1.14))(graphql@16.13.2)': dependencies: '@apollo/server': 4.13.0(encoding@0.1.13)(graphql@16.13.2) '@apollo/server-plugin-landing-page-graphql-playground': 4.0.0(@apollo/server@4.13.0(encoding@0.1.13)(graphql@16.13.2)) '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/graphql': 12.2.2(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(graphql@16.13.2)(reflect-metadata@0.1.14) + '@nestjs/graphql': 12.2.2(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(graphql@16.13.2)(reflect-metadata@0.1.14) graphql: 16.13.2 iterall: 1.3.0 lodash.omit: 4.5.0 tslib: 2.8.1 + optionalDependencies: + '@apollo/gateway': 2.14.1(encoding@0.1.13)(graphql@16.13.2) + '@apollo/subgraph': 2.14.1(graphql@16.13.2) + + '@nestjs/apollo@12.2.2(@apollo/gateway@2.14.1(encoding@0.1.13)(graphql@16.13.2))(@apollo/server@4.13.0(encoding@0.1.13)(graphql@16.13.2))(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(@nestjs/graphql@12.2.2(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(graphql@16.13.2)(reflect-metadata@0.2.2))(graphql@16.13.2)': + dependencies: + '@apollo/server': 4.13.0(encoding@0.1.13)(graphql@16.13.2) + '@apollo/server-plugin-landing-page-graphql-playground': 4.0.0(@apollo/server@4.13.0(encoding@0.1.13)(graphql@16.13.2)) + '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/graphql': 12.2.2(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(graphql@16.13.2)(reflect-metadata@0.2.2) + graphql: 16.13.2 + iterall: 1.3.0 + lodash.omit: 4.5.0 + tslib: 2.8.1 + optionalDependencies: + '@apollo/gateway': 2.14.1(encoding@0.1.13)(graphql@16.13.2) + '@apollo/subgraph': 2.14.1(graphql@16.13.2) '@nestjs/bull-shared@10.2.3(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.22)': dependencies: @@ -24276,6 +24817,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + file-type: 20.4.1 + iterare: 1.2.1 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.4 + transitivePeerDependencies: + - supports-color + '@nestjs/config@3.3.0(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(rxjs@7.8.2)': dependencies: '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) @@ -24300,13 +24855,29 @@ snapshots: transitivePeerDependencies: - encoding + '@nestjs/core@10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nuxtjs/opencollective': 0.3.2(encoding@0.1.13) + fast-safe-stringify: 2.1.1 + iterare: 1.2.1 + path-to-regexp: 3.3.0 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + optionalDependencies: + '@nestjs/platform-express': 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22) + transitivePeerDependencies: + - encoding + '@nestjs/event-emitter@3.0.1(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.22)': dependencies: '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.1.14)(rxjs@7.8.2) eventemitter2: 6.4.9 - '@nestjs/graphql@12.2.2(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(graphql@16.13.2)(reflect-metadata@0.1.14)': + '@nestjs/graphql@12.2.2(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(graphql@16.13.2)(reflect-metadata@0.1.14)': dependencies: '@graphql-tools/merge': 9.0.11(graphql@16.13.2) '@graphql-tools/schema': 10.0.10(graphql@16.13.2) @@ -24327,6 +24898,35 @@ snapshots: uuid: 11.0.3 ws: 8.18.0 optionalDependencies: + '@apollo/subgraph': 2.14.1(graphql@16.13.2) + class-transformer: 0.5.1 + class-validator: 0.14.4 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@nestjs/graphql@12.2.2(@apollo/subgraph@2.14.1(graphql@16.13.2))(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(graphql@16.13.2)(reflect-metadata@0.2.2)': + dependencies: + '@graphql-tools/merge': 9.0.11(graphql@16.13.2) + '@graphql-tools/schema': 10.0.10(graphql@16.13.2) + '@graphql-tools/utils': 10.6.1(graphql@16.13.2) + '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.0.6(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) + chokidar: 4.0.1 + fast-glob: 3.3.2 + graphql: 16.13.2 + graphql-tag: 2.12.6(graphql@16.13.2) + graphql-ws: 5.16.0(graphql@16.13.2) + lodash: 4.17.21 + normalize-path: 3.0.0 + reflect-metadata: 0.2.2 + subscriptions-transport-ws: 0.11.0(graphql@16.13.2) + tslib: 2.8.1 + uuid: 11.0.3 + ws: 8.18.0 + optionalDependencies: + '@apollo/subgraph': 2.14.1(graphql@16.13.2) class-transformer: 0.5.1 class-validator: 0.14.4 transitivePeerDependencies: @@ -24347,6 +24947,14 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.4 + '@nestjs/mapped-types@2.0.6(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.4 + '@nestjs/mongoose@10.1.0(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.22)(mongoose@9.3.3(socks@2.8.7))(rxjs@7.8.2)': dependencies: '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) @@ -24359,6 +24967,11 @@ snapshots: '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) passport: 0.7.0 + '@nestjs/passport@10.0.3(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': + dependencies: + '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + passport: 0.7.0 + '@nestjs/platform-express@10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.22)': dependencies: '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) @@ -24371,6 +24984,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@nestjs/platform-express@10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)': + dependencies: + '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + body-parser: 1.20.4 + cors: 2.8.5 + express: 4.22.1 + multer: 2.0.2 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + '@nestjs/schedule@6.1.1(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@10.4.22)': dependencies: '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) @@ -24400,6 +25025,15 @@ snapshots: typeorm: 0.3.20(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@swc/core@1.15.33(@swc/helpers@0.5.19))(@types/node@20.19.37)(typescript@5.9.3)) uuid: 9.0.1 + '@nestjs/typeorm@10.0.2(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.20(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@swc/core@1.15.33(@swc/helpers@0.5.19))(@types/node@20.19.37)(typescript@5.9.3)))': + dependencies: + '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + typeorm: 0.3.20(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@swc/core@1.15.33(@swc/helpers@0.5.19))(@types/node@20.19.37)(typescript@5.9.3)) + uuid: 9.0.1 + '@noble/hashes@1.4.0': {} '@noble/hashes@1.8.0': {} @@ -24463,6 +25097,16 @@ snapshots: transitivePeerDependencies: - supports-color + '@npmcli/agent@4.0.2': + dependencies: + agent-base: 7.1.4 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 11.2.7 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + '@npmcli/arborist@7.5.4': dependencies: '@isaacs/string-locale-compare': 1.1.0 @@ -24512,6 +25156,10 @@ snapshots: dependencies: semver: 7.7.4 + '@npmcli/fs@5.0.0': + dependencies: + semver: 7.7.4 + '@npmcli/git@5.0.8': dependencies: '@npmcli/promise-spawn': 7.0.2 @@ -24575,6 +25223,8 @@ snapshots: '@npmcli/redact@2.0.1': {} + '@npmcli/redact@4.0.0': {} + '@npmcli/run-script@8.1.0': dependencies: '@npmcli/node-gyp': 3.0.0 @@ -24847,7 +25497,11 @@ snapshots: '@opentelemetry/api-logs@0.53.0': dependencies: - '@opentelemetry/api': 1.9.0 + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api-logs@0.57.2': + dependencies: + '@opentelemetry/api': 1.9.1 '@opentelemetry/api@1.9.0': {} @@ -24866,6 +25520,11 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.27.0 + '@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -24876,6 +25535,15 @@ snapshots: '@opentelemetry/api': 1.9.1 '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/exporter-metrics-otlp-http@0.57.2(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-http@0.53.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25117,6 +25785,12 @@ snapshots: '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-transformer': 0.53.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base@0.57.2(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer@0.53.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25128,6 +25802,17 @@ snapshots: '@opentelemetry/sdk-trace-base': 1.26.0(@opentelemetry/api@1.9.0) protobufjs: 7.5.4 + '@opentelemetry/otlp-transformer@0.57.2(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.1) + protobufjs: 7.5.4 + '@opentelemetry/propagator-b3@1.26.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25140,12 +25825,50 @@ snapshots: '@opentelemetry/redis-common@0.38.2': {} + '@opentelemetry/resource-detector-alibaba-cloud@0.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/resource-detector-aws@1.12.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/resource-detector-azure@0.6.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/resource-detector-gcp@0.33.1(@opentelemetry/api@1.9.1)(encoding@0.1.13)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + gcp-metadata: 6.1.1(encoding@0.1.13) + transitivePeerDependencies: + - encoding + - supports-color + '@opentelemetry/resources@1.26.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.27.0 + '@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -25159,12 +25882,25 @@ snapshots: '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs@0.57.2(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics@1.26.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25172,6 +25908,13 @@ snapshots: '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.27.0 + '@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -25191,6 +25934,8 @@ snapshots: '@opentelemetry/semantic-conventions@1.27.0': {} + '@opentelemetry/semantic-conventions@1.28.0': {} + '@opentelemetry/semantic-conventions@1.40.0': {} '@opentelemetry/sql-common@0.41.2(@opentelemetry/api@1.9.1)': @@ -25487,7 +26232,7 @@ snapshots: '@pm2/pm2-version-check@1.0.4': dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -26301,7 +27046,7 @@ snapshots: '@swc-node/sourcemap-support': 0.6.1 '@swc/core': 1.15.33(@swc/helpers@0.5.19) colorette: 2.0.20 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) oxc-resolver: 11.19.1 pirates: 4.0.7 tslib: 2.8.1 @@ -26400,7 +27145,7 @@ snapshots: '@tokenizer/inflate@0.2.7': dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) fflate: 0.8.2 token-types: 6.1.2 transitivePeerDependencies: @@ -26963,6 +27708,20 @@ snapshots: '@types/pako@2.0.4': {} + '@types/passport-jwt@4.0.1': + dependencies: + '@types/jsonwebtoken': 9.0.10 + '@types/passport-strategy': 0.2.38 + + '@types/passport-strategy@0.2.38': + dependencies: + '@types/express': 4.17.25 + '@types/passport': 1.0.17 + + '@types/passport@1.0.17': + dependencies: + '@types/express': 4.17.25 + '@types/pg-pool@2.0.7': dependencies: '@types/pg': 8.20.0 @@ -27026,6 +27785,10 @@ snapshots: '@types/stack-utils@2.0.3': {} + '@types/strip-bom@3.0.0': {} + + '@types/strip-json-comments@0.0.30': {} + '@types/stylis@4.2.7': {} '@types/superagent@8.1.9': @@ -27056,6 +27819,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/uuid@9.0.8': {} + '@types/validator@13.15.10': {} '@types/web-bluetooth@0.0.20': {} @@ -27092,7 +27857,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@7.32.0)(typescript@5.9.3) '@typescript-eslint/utils': 5.62.0(eslint@7.32.0)(typescript@5.9.3) - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 7.32.0 graphemer: 1.4.0 ignore: 5.3.2 @@ -27171,7 +27936,7 @@ snapshots: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 9.39.4(jiti@2.6.1) optionalDependencies: typescript: 5.9.3 @@ -27184,7 +27949,7 @@ snapshots: '@typescript-eslint/types': 8.57.2 '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.57.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 7.32.0 typescript: 5.9.3 transitivePeerDependencies: @@ -27196,7 +27961,7 @@ snapshots: '@typescript-eslint/types': 8.57.2 '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.57.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 8.57.1 typescript: 5.9.3 transitivePeerDependencies: @@ -27208,7 +27973,7 @@ snapshots: '@typescript-eslint/types': 8.57.2 '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.57.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 9.39.4(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -27218,7 +27983,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3) '@typescript-eslint/types': 8.57.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -27251,7 +28016,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.9.3) '@typescript-eslint/utils': 5.62.0(eslint@7.32.0)(typescript@5.9.3) - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 7.32.0 tsutils: 3.21.0(typescript@5.9.3) optionalDependencies: @@ -27264,7 +28029,7 @@ snapshots: '@typescript-eslint/types': 8.57.2 '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) '@typescript-eslint/utils': 8.57.2(eslint@7.32.0)(typescript@5.9.3) - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 7.32.0 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 @@ -27276,7 +28041,7 @@ snapshots: '@typescript-eslint/types': 8.57.2 '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) '@typescript-eslint/utils': 8.57.2(eslint@8.57.1)(typescript@5.9.3) - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 8.57.1 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 @@ -27288,7 +28053,7 @@ snapshots: '@typescript-eslint/types': 8.57.2 '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) '@typescript-eslint/utils': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 9.39.4(jiti@2.6.1) ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 @@ -27307,7 +28072,7 @@ snapshots: dependencies: '@typescript-eslint/types': 4.33.0 '@typescript-eslint/visitor-keys': 4.33.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.4 @@ -27321,7 +28086,7 @@ snapshots: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.4 @@ -27335,7 +28100,7 @@ snapshots: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.9 @@ -27352,7 +28117,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3) '@typescript-eslint/types': 8.57.2 '@typescript-eslint/visitor-keys': 8.57.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.4 semver: 7.7.4 tinyglobby: 0.2.15 @@ -28074,7 +28839,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -28480,19 +29245,19 @@ snapshots: axios@0.21.4: dependencies: - follow-redirects: 1.15.11 + follow-redirects: 1.15.11(debug@4.3.7) transitivePeerDependencies: - debug axios@0.26.1: dependencies: - follow-redirects: 1.15.11 + follow-redirects: 1.15.11(debug@4.3.7) transitivePeerDependencies: - debug axios@1.13.6: dependencies: - follow-redirects: 1.15.11 + follow-redirects: 1.15.11(debug@4.3.7) form-data: 4.0.5 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -28729,6 +29494,8 @@ snapshots: bigi@1.4.2: {} + bignumber.js@9.3.1: {} + bin-links@4.0.4: dependencies: cmd-shim: 6.0.3 @@ -28787,7 +29554,7 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) http-errors: 2.0.1 iconv-lite: 0.7.2 on-finished: 2.4.1 @@ -29035,6 +29802,19 @@ snapshots: tar: 7.5.13 unique-filename: 4.0.0 + cacache@20.0.4: + dependencies: + '@npmcli/fs': 5.0.0 + fs-minipass: 3.0.3 + glob: 13.0.6 + lru-cache: 11.2.7 + minipass: 7.1.3 + minipass-collect: 2.0.1 + minipass-flush: 1.0.6 + minipass-pipeline: 1.2.4 + p-map: 7.0.4 + ssri: 13.0.1 + cacheable-lookup@5.0.4: {} cacheable-request@7.0.4: @@ -30393,10 +31173,6 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.3: - dependencies: - ms: 2.1.3 - debug@4.4.3(supports-color@5.5.0): dependencies: ms: 2.1.3 @@ -30438,6 +31214,27 @@ snapshots: deep-eql@5.0.2: {} + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + es-get-iterator: 1.1.3 + get-intrinsic: 1.3.0 + is-arguments: 1.2.0 + is-array-buffer: 3.0.5 + is-date-object: 1.1.0 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.7 + regexp.prototype.flags: 1.5.4 + side-channel: 1.1.0 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + deep-extend@0.6.0: {} deep-is@0.1.4: {} @@ -30565,7 +31362,7 @@ snapshots: docker-modem@5.0.7: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) readable-stream: 3.6.2 split-ca: 1.0.1 ssh2: 1.17.0 @@ -30691,6 +31488,10 @@ snapshots: duplexer@0.1.2: {} + dynamic-dedupe@0.3.0: + dependencies: + xtend: 4.0.2 + earcut@2.2.4: {} eastasianwidth@0.2.0: {} @@ -30801,7 +31602,7 @@ snapshots: engine.io-client@6.6.4: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) engine.io-parser: 5.2.3 ws: 8.18.3 xmlhttprequest-ssl: 2.1.2 @@ -30821,7 +31622,7 @@ snapshots: base64id: 2.0.0 cookie: 0.7.2 cors: 2.8.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) engine.io-parser: 5.2.3 ws: 8.18.3 transitivePeerDependencies: @@ -30962,6 +31763,18 @@ snapshots: es-errors@1.3.0: {} + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.8 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + is-arguments: 1.2.0 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.1 + isarray: 2.0.5 + stop-iteration-iterator: 1.1.0 + es-module-lexer@1.7.0: {} es-module-lexer@2.0.0: {} @@ -31346,7 +32159,7 @@ snapshots: '@package-json/types': 0.0.12 '@typescript-eslint/types': 8.57.2 comment-parser: 1.4.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 7.32.0 eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 @@ -31365,7 +32178,7 @@ snapshots: '@package-json/types': 0.0.12 '@typescript-eslint/types': 8.57.2 comment-parser: 1.4.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 8.57.1 eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 @@ -31384,7 +32197,7 @@ snapshots: '@package-json/types': 0.0.12 '@typescript-eslint/types': 8.57.2 comment-parser: 1.4.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 9.39.4(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 @@ -31442,7 +32255,7 @@ snapshots: '@es-joy/jsdoccomment': 0.50.2 are-docs-informative: 0.0.2 comment-parser: 1.4.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint: 7.32.0 espree: 10.4.0 @@ -31458,7 +32271,7 @@ snapshots: '@es-joy/jsdoccomment': 0.50.2 are-docs-informative: 0.0.2 comment-parser: 1.4.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint: 8.57.1 espree: 10.4.0 @@ -31474,7 +32287,7 @@ snapshots: '@es-joy/jsdoccomment': 0.50.2 are-docs-informative: 0.0.2 comment-parser: 1.4.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint: 9.39.4(jiti@2.6.1) espree: 10.4.0 @@ -31701,7 +32514,7 @@ snapshots: eslint-plugin-toml@0.11.1(eslint@7.32.0): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 7.32.0 eslint-compat-utils: 0.5.1(eslint@7.32.0) lodash: 4.17.23 @@ -31711,7 +32524,7 @@ snapshots: eslint-plugin-toml@0.11.1(eslint@8.57.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 8.57.1 eslint-compat-utils: 0.5.1(eslint@8.57.1) lodash: 4.17.23 @@ -31721,7 +32534,7 @@ snapshots: eslint-plugin-toml@0.11.1(eslint@9.39.4(jiti@2.6.1)): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 9.39.4(jiti@2.6.1) eslint-compat-utils: 0.5.1(eslint@9.39.4(jiti@2.6.1)) lodash: 4.17.23 @@ -31731,7 +32544,7 @@ snapshots: eslint-plugin-toml@0.12.0(eslint@9.39.4(jiti@2.6.1)): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 9.39.4(jiti@2.6.1) eslint-compat-utils: 0.6.5(eslint@9.39.4(jiti@2.6.1)) lodash: 4.17.23 @@ -31888,7 +32701,7 @@ snapshots: eslint-plugin-yml@1.19.1(eslint@7.32.0): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) diff-sequences: 27.5.1 escape-string-regexp: 4.0.0 eslint: 7.32.0 @@ -31900,7 +32713,7 @@ snapshots: eslint-plugin-yml@1.19.1(eslint@8.57.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) diff-sequences: 27.5.1 escape-string-regexp: 4.0.0 eslint: 8.57.1 @@ -31912,7 +32725,7 @@ snapshots: eslint-plugin-yml@1.19.1(eslint@9.39.4(jiti@2.6.1)): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) diff-sequences: 27.5.1 escape-string-regexp: 4.0.0 eslint: 9.39.4(jiti@2.6.1) @@ -31986,7 +32799,7 @@ snapshots: ajv: 6.14.0 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) doctrine: 3.0.0 enquirer: 2.4.1 escape-string-regexp: 4.0.0 @@ -32036,7 +32849,7 @@ snapshots: ajv: 6.14.0 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -32083,7 +32896,7 @@ snapshots: ajv: 6.14.0 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -32340,7 +33153,7 @@ snapshots: content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 @@ -32393,7 +33206,7 @@ snapshots: extract-zip@2.0.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -32618,7 +33431,7 @@ snapshots: finalhandler@2.1.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -32693,15 +33506,13 @@ snapshots: fn.name@1.1.0: {} - follow-redirects@1.15.11: {} - follow-redirects@1.15.11(debug@4.3.7): optionalDependencies: debug: 4.3.7 follow-redirects@1.15.11(debug@4.4.3): optionalDependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) for-each@0.3.5: dependencies: @@ -32838,10 +33649,30 @@ snapshots: functions-have-names@1.2.3: {} + gaxios@6.7.1(encoding@0.1.13): + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0(encoding@0.1.13) + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + gaze@1.1.3: dependencies: globule: 1.3.4 + gcp-metadata@6.1.1(encoding@0.1.13): + dependencies: + gaxios: 6.7.1(encoding@0.1.13) + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + generator-function@2.0.1: {} gensync@1.0.0-beta.2: {} @@ -32931,7 +33762,7 @@ snapshots: dependencies: basic-ftp: 5.2.0 data-uri-to-buffer: 6.0.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -33035,6 +33866,12 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 2.0.2 + glob@13.0.6: + dependencies: + minimatch: 10.2.4 + minipass: 7.1.3 + path-scurry: 2.0.2 + glob@5.0.15: dependencies: inflight: 1.0.6 @@ -33153,6 +33990,8 @@ snapshots: lodash: 4.17.23 minimatch: 3.0.8 + google-logging-utils@0.0.2: {} + gopd@1.2.0: {} got@11.8.6: @@ -33645,14 +34484,14 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color http-proxy-middleware@3.0.5: dependencies: '@types/http-proxy': 1.17.17 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) http-proxy: 1.18.1(debug@4.4.3) is-glob: 4.0.3 is-plain-object: 5.0.0 @@ -33697,14 +34536,14 @@ snapshots: https-proxy-agent@5.0.0: dependencies: agent-base: 6.0.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -33941,7 +34780,7 @@ snapshots: dependencies: '@ioredis/commands': 1.5.1 cluster-key-slot: 1.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -33973,6 +34812,11 @@ snapshots: is-alphabetical: 1.0.4 is-decimal: 1.0.4 + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -34293,7 +35137,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -34408,7 +35252,7 @@ snapshots: jest-diff@29.7.0: dependencies: - chalk: 4.1.0 + chalk: 4.1.2 diff-sequences: 29.6.3 jest-get-type: 29.6.3 pretty-format: 29.7.0 @@ -34736,6 +35580,10 @@ snapshots: jsesc@3.1.0: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + json-buffer@3.0.1: {} json-parse-better-errors@1.0.2: {} @@ -35130,7 +35978,7 @@ snapshots: dependencies: chalk: 5.6.2 commander: 13.1.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) execa: 8.0.1 lilconfig: 3.1.3 listr2: 8.3.3 @@ -35459,6 +36307,23 @@ snapshots: transitivePeerDependencies: - supports-color + make-fetch-happen@15.0.6: + dependencies: + '@gar/promise-retry': 1.0.3 + '@npmcli/agent': 4.0.2 + '@npmcli/redact': 4.0.0 + cacache: 20.0.4 + http-cache-semantics: 4.2.0 + minipass: 7.1.3 + minipass-fetch: 5.0.2 + minipass-flush: 1.0.6 + minipass-pipeline: 1.2.4 + negotiator: 1.0.0 + proc-log: 6.1.0 + ssri: 13.0.1 + transitivePeerDependencies: + - supports-color + make-iterator@1.0.1: dependencies: kind-of: 6.0.3 @@ -35941,7 +36806,7 @@ snapshots: micromark@2.11.4: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) parse-entities: 2.0.0 transitivePeerDependencies: - supports-color @@ -35949,7 +36814,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.13 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) decode-named-character-reference: 1.3.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -36076,6 +36941,14 @@ snapshots: optionalDependencies: encoding: 0.1.13 + minipass-fetch@5.0.2: + dependencies: + minipass: 7.1.3 + minipass-sized: 2.0.0 + minizlib: 3.1.0 + optionalDependencies: + iconv-lite: 0.7.2 + minipass-flush@1.0.6: dependencies: minipass: 7.1.3 @@ -36088,6 +36961,10 @@ snapshots: dependencies: minipass: 3.3.6 + minipass-sized@2.0.0: + dependencies: + minipass: 7.1.3 + minipass@3.3.6: dependencies: yallist: 4.0.0 @@ -36383,6 +37260,8 @@ snapshots: node-addon-api@7.1.1: optional: true + node-cpu-count@0.1.1: {} + node-cron@3.0.3: dependencies: uuid: 8.3.2 @@ -36637,7 +37516,7 @@ snapshots: '@yarnpkg/parsers': 3.0.2 '@zkochan/js-yaml': 0.0.7 axios: 1.13.6 - chalk: 4.1.0 + chalk: 4.1.2 cli-cursor: 3.1.0 cli-spinners: 2.6.1 cliui: 8.0.1 @@ -36736,6 +37615,11 @@ snapshots: object-inspect@1.13.4: {} + object-is@1.1.6: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + object-keys@1.1.1: {} object-refs@0.4.0: {} @@ -37066,7 +37950,7 @@ snapshots: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) get-uri: 6.0.5 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -37506,7 +38390,7 @@ snapshots: pm2-axon-rpc@0.7.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -37514,7 +38398,7 @@ snapshots: dependencies: amp: 0.3.1 amp-message: 0.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 transitivePeerDependencies: - supports-color @@ -37531,7 +38415,7 @@ snapshots: pm2-sysmonit@1.2.8: dependencies: async: 3.2.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) pidusage: 2.0.21 systeminformation: 5.31.5 tx2: 1.0.5 @@ -37553,7 +38437,7 @@ snapshots: commander: 2.15.1 croner: 4.1.97 dayjs: 1.11.20 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) enquirer: 2.3.6 eventemitter2: 5.0.1 fclone: 1.0.11 @@ -37845,6 +38729,8 @@ snapshots: proc-log@5.0.0: {} + proc-log@6.1.0: {} + process-nextick-args@2.0.1: {} process-warning@5.0.0: {} @@ -38021,7 +38907,7 @@ snapshots: proxy-agent@6.3.1: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -38656,7 +39542,7 @@ snapshots: require-in-the-middle@5.2.0: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) module-details-from-path: 1.0.4 resolve: 1.22.11 transitivePeerDependencies: @@ -38664,7 +39550,7 @@ snapshots: require-in-the-middle@8.0.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) module-details-from-path: 1.0.4 transitivePeerDependencies: - supports-color @@ -38873,7 +39759,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -39097,7 +39983,7 @@ snapshots: send@1.2.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -39355,7 +40241,7 @@ snapshots: simple-websocket@9.1.0: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) queue-microtask: 1.2.3 randombytes: 2.1.0 readable-stream: 3.6.2 @@ -39406,7 +40292,7 @@ snapshots: socket.io-adapter@2.5.6: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) ws: 8.18.3 transitivePeerDependencies: - bufferutil @@ -39427,7 +40313,7 @@ snapshots: socket.io-client@4.8.3: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) engine.io-client: 6.6.4 socket.io-parser: 4.2.6 transitivePeerDependencies: @@ -39438,7 +40324,7 @@ snapshots: socket.io-parser@4.2.6: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -39447,7 +40333,7 @@ snapshots: accepts: 1.3.8 base64id: 2.0.0 cors: 2.8.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) engine.io: 6.6.6 socket.io-adapter: 2.5.6 socket.io-parser: 4.2.6 @@ -39459,7 +40345,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) socks: 2.8.7 transitivePeerDependencies: - supports-color @@ -39649,6 +40535,10 @@ snapshots: dependencies: minipass: 7.1.3 + ssri@13.0.1: + dependencies: + minipass: 7.1.3 + stable-hash-x@0.2.0: {} stack-trace@0.0.10: {} @@ -39889,7 +40779,7 @@ snapshots: sumchecker@3.0.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -39897,7 +40787,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) fast-safe-stringify: 2.1.1 form-data: 4.0.5 formidable: 3.5.4 @@ -40176,7 +41066,7 @@ snapshots: threads@1.7.0: dependencies: callsites: 3.1.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) is-observable: 2.1.0 observable-fns: 0.6.1 optionalDependencies: @@ -40337,6 +41227,8 @@ snapshots: optionalDependencies: typescript: 5.9.3 + ts-graphviz@1.8.2: {} + ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(esbuild@0.27.4)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.37)(ts-node@10.9.2(@swc/core@1.15.33(@swc/helpers@0.5.19))(@types/node@20.19.37)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 @@ -40362,6 +41254,24 @@ snapshots: ts-map@1.0.3: {} + ts-node-dev@2.0.0(@swc/core@1.15.33(@swc/helpers@0.5.19))(@types/node@20.19.37)(typescript@5.9.3): + dependencies: + chokidar: 3.6.0 + dynamic-dedupe: 0.3.0 + minimist: 1.2.8 + mkdirp: 1.0.4 + resolve: 1.22.11 + rimraf: 2.7.1 + source-map-support: 0.5.21 + tree-kill: 1.2.2 + ts-node: 10.9.2(@swc/core@1.15.33(@swc/helpers@0.5.19))(@types/node@20.19.37)(typescript@5.9.3) + tsconfig: 7.0.0 + typescript: 5.9.3 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - '@types/node' + ts-node@10.9.2(@swc/core@1.15.33(@swc/helpers@0.5.19))(@types/node@20.19.37)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -40404,6 +41314,13 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 + tsconfig@7.0.0: + dependencies: + '@types/strip-bom': 3.0.0 + '@types/strip-json-comments': 0.0.30 + strip-bom: 3.0.0 + strip-json-comments: 2.0.1 + tsd@0.31.2: dependencies: '@tsd/typescript': 5.4.5 @@ -40450,7 +41367,7 @@ snapshots: tuf-js@2.2.1: dependencies: '@tufjs/models': 2.0.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) make-fetch-happen: 13.0.1 transitivePeerDependencies: - supports-color @@ -40584,7 +41501,7 @@ snapshots: chalk: 4.1.2 cli-highlight: 2.1.11 dayjs: 1.11.20 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) dotenv: 16.6.1 glob: 10.5.0 mkdirp: 2.1.6 @@ -40950,7 +41867,7 @@ snapshots: vite-node@1.6.1(@types/node@20.19.37)(lightningcss@1.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1): dependencies: cac: 6.7.14 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) pathe: 1.1.2 picocolors: 1.1.1 vite: 5.4.21(@types/node@20.19.37)(lightningcss@1.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1) @@ -40968,7 +41885,7 @@ snapshots: vite-node@2.1.9(@types/node@22.19.15)(lightningcss@1.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1): dependencies: cac: 6.7.14 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) es-module-lexer: 1.7.0 pathe: 1.1.2 vite: 5.4.21(@types/node@22.19.15)(lightningcss@1.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1) @@ -41090,7 +42007,7 @@ snapshots: '@vitest/utils': 1.6.1 acorn-walk: 8.3.5 chai: 4.5.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) execa: 8.0.1 local-pkg: 0.5.1 magic-string: 0.30.21 @@ -41125,7 +42042,7 @@ snapshots: '@vitest/spy': 2.1.9 '@vitest/utils': 2.1.9 chai: 5.3.3 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) expect-type: 1.3.0 magic-string: 0.30.21 pathe: 1.1.2 @@ -41263,7 +42180,7 @@ snapshots: vue-eslint-parser@9.4.3(eslint@7.32.0): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 7.32.0 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 @@ -41276,7 +42193,7 @@ snapshots: vue-eslint-parser@9.4.3(eslint@8.57.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 8.57.1 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 @@ -41289,7 +42206,7 @@ snapshots: vue-eslint-parser@9.4.3(eslint@9.39.4(jiti@2.6.1)): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 9.39.4(jiti@2.6.1) eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3