[105-9][@ant] refactor: перевести account self-service auth-v2 с REST на GraphQL/SDK — единый типизированный фасад фронта

Корзина C Фазы 2: sessions/2fa/recovery-strategy/security-not-me →
AccountSecurityResolver (9 операций, GqlJwtAuthGuard), а
critical-actions/keys/force-recovery → CriticalActionsResolver (6 операций,
тот же CASL @CheckAbility+AuthorizationGuard). Движок Эпика 6 не тронут —
резолверы поверх тех же сервисов. 5 REST-контролёров сняты целиком, 2 урезаны
до magic-link :token (корзина D). SDK-домены AccountSecurity/CriticalActions +
codegen (SDL 62 резолвера) + Zeus закоммичен. Транспорт (IP/refresh-токен) —
request-meta декораторы, не GraphQL-переменные. Unit 24/24, ESLint 0.

Зачем: наружу на фронт смотрит только @coopenomics/sdk — нового способа
взаимодействия с бэкендом не появляется, bearer живёт только в SDK. OIDC
(.well-known), webhook (coop/internal) и login-контур (Фаза 3) остаются REST.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
coopops
2026-06-14 06:22:15 +00:00
parent d4a34bb067
commit f5bcaa3a69
51 changed files with 3241 additions and 354 deletions
@@ -0,0 +1,135 @@
import type { SessionsService } from '../sessions/sessions.service';
import type { TwoFactorService } from '../two-factor/two-factor.service';
import type { RecoveryStrategyService } from '../recovery/recovery-strategy.service';
import type { SecurityIncidentService } from '../security/security-incident.service';
import { RecoveryStrategy } from '~/domain/auth-v2/recovery-strategy/recovery-strategy.types';
import { AccountSecurityResolver } from './account-security.resolver';
const USER = { id: 'u1', username: 'payer1', role: 'user' };
const IP = '203.0.113.7';
function build() {
const sessions = {
list: jest.fn(async () => [
{
id: 's1',
device: 'Firefox',
ip: '10.0.0.1',
createdAt: '2026-06-13T00:00:00Z',
lastSeenAt: '2026-06-14T00:00:00Z',
current: true,
},
]),
revoke: jest.fn(async () => undefined),
revokeAll: jest.fn(async () => ({ revoked: 3 })),
};
const twoFactor = {
beginEnrollment: jest.fn(async () => ({ secret: 'BASE32SECRET', otpauthUri: 'otpauth://totp/x' })),
activate: jest.fn(async () => undefined),
disable: jest.fn(async () => undefined),
};
const recoveryStrategy = {
getStrategy: jest.fn(async () => RecoveryStrategy.OfflineCode),
setStrategy: jest.fn(async () => undefined),
};
const incidents = {
report: jest.fn(async () => ({ revoked: 2 })),
};
const resolver = new AccountSecurityResolver(
sessions as unknown as SessionsService,
twoFactor as unknown as TwoFactorService,
recoveryStrategy as unknown as RecoveryStrategyService,
incidents as unknown as SecurityIncidentService,
);
return { resolver, sessions, twoFactor, recoveryStrategy, incidents };
}
describe('AccountSecurityResolver', () => {
it('getSessions маппит сессии в snake_case и передаёт refresh-токен текущей сессии', async () => {
const { resolver, sessions } = build();
const out = await resolver.getSessions(USER, 'rt-current');
expect(sessions.list).toHaveBeenCalledWith('u1', 'rt-current');
expect(out).toEqual([
{
id: 's1',
device: 'Firefox',
ip: '10.0.0.1',
created_at: '2026-06-13T00:00:00Z',
last_seen_at: '2026-06-14T00:00:00Z',
current: true,
},
]);
});
it('getRecoveryStrategy возвращает стратегию пайщика', async () => {
const { resolver, recoveryStrategy } = build();
const out = await resolver.getRecoveryStrategy(USER);
expect(recoveryStrategy.getStrategy).toHaveBeenCalledWith('u1');
expect(out).toBe(RecoveryStrategy.OfflineCode);
});
it('revokeSession завершает сессию по id с IP для аудита', async () => {
const { resolver, sessions } = build();
const ok = await resolver.revokeSession({ session_id: 's1' }, USER, IP);
expect(ok).toBe(true);
expect(sessions.revoke).toHaveBeenCalledWith('u1', 's1', IP);
});
it('revokeAllSessions возвращает число завершённых', async () => {
const { resolver, sessions } = build();
const out = await resolver.revokeAllSessions(USER, IP);
expect(sessions.revokeAll).toHaveBeenCalledWith('u1', IP);
expect(out).toEqual({ revoked: 3 });
});
it('enrollTwoFactor маппит otpauthUri в otpauth_uri', async () => {
const { resolver, twoFactor } = build();
const out = await resolver.enrollTwoFactor(USER);
expect(twoFactor.beginEnrollment).toHaveBeenCalledWith('u1', 'payer1');
expect(out).toEqual({ secret: 'BASE32SECRET', otpauth_uri: 'otpauth://totp/x' });
});
it('activateTwoFactor подтверждает второй фактор кодом', async () => {
const { resolver, twoFactor } = build();
const ok = await resolver.activateTwoFactor({ code: '123456' }, USER, IP);
expect(ok).toBe(true);
expect(twoFactor.activate).toHaveBeenCalledWith('u1', '123456', IP);
});
it('disableTwoFactor отключает второй фактор кодом', async () => {
const { resolver, twoFactor } = build();
const ok = await resolver.disableTwoFactor({ code: '654321' }, USER, IP);
expect(ok).toBe(true);
expect(twoFactor.disable).toHaveBeenCalledWith('u1', '654321', IP);
});
it('setRecoveryStrategy меняет стратегию со step-up кодом', async () => {
const { resolver, recoveryStrategy } = build();
const ok = await resolver.setRecoveryStrategy({ strategy: RecoveryStrategy.Council, code: '000111' }, USER, IP);
expect(ok).toBe(true);
expect(recoveryStrategy.setStrategy).toHaveBeenCalledWith('u1', RecoveryStrategy.Council, '000111', IP);
});
it('reportNotMe зовёт report с source=settings и подозрительной сессией', async () => {
const { resolver, incidents } = build();
const out = await resolver.reportNotMe({ session_id: 's9' }, USER, IP);
expect(incidents.report).toHaveBeenCalledWith({
subjectId: 'u1',
ip: IP,
source: 'settings',
reportedSessionId: 's9',
});
expect(out).toEqual({ revoked: 2 });
});
it('reportNotMe без session_id передаёт reportedSessionId=null', async () => {
const { resolver, incidents } = build();
await resolver.reportNotMe({}, USER, IP);
expect(incidents.report).toHaveBeenCalledWith({
subjectId: 'u1',
ip: IP,
source: 'settings',
reportedSessionId: null,
});
});
});
@@ -0,0 +1,170 @@
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard';
import { CurrentUser } from '~/application/auth/decorators/current-user.decorator';
import { ClientIp, RefreshTokenHeader } from '~/application/auth/decorators/request-meta.decorator';
import { RecoveryStrategy } from '~/domain/auth-v2/recovery-strategy/recovery-strategy.types';
import { SessionsService } from '../sessions/sessions.service';
import { TwoFactorService } from '../two-factor/two-factor.service';
import { RecoveryStrategyService } from '../recovery/recovery-strategy.service';
import { SecurityIncidentService } from '../security/security-incident.service';
import {
AccountSessionDTO,
ReportNotMeInputDTO,
RevokeSessionInputDTO,
RevokedSessionsResultDTO,
SetRecoveryStrategyInputDTO,
TwoFactorCodeInputDTO,
TwoFactorEnrollmentDTO,
} from './dto/account-security.dto';
interface ICurrentUser {
id: string;
username: string;
role?: string;
}
/**
* GraphQL-фасад самообслуживания безопасности аккаунта (Фаза 2 миграции REST→GraphQL/SDK).
* Заменяет REST-контроллеры `coop/sessions`, `coop/2fa`, `coop/recovery/strategy` и
* JWT-метод `coop/security/not-me` — фронт ходит через @coopenomics/sdk (Zeus), нового
* способа взаимодействия с бэкендом наружу не появляется.
*
* Все операции — для текущего залогиненного пайщика (subject = `user.id`) под
* `GqlJwtAuthGuard`. IP и refresh-токен текущей сессии — транспорт, берутся из
* request-meta декораторов, не из GraphQL-переменных.
*/
@Resolver()
export class AccountSecurityResolver {
constructor(
private readonly sessions: SessionsService,
private readonly twoFactor: TwoFactorService,
private readonly recoveryStrategy: RecoveryStrategyService,
private readonly incidents: SecurityIncidentService,
) {}
@Query(() => [AccountSessionDTO], {
name: 'getSessions',
description: 'Активные сессии текущего пайщика (текущая помечается current)',
})
@UseGuards(GqlJwtAuthGuard)
async getSessions(
@CurrentUser() user: ICurrentUser,
@RefreshTokenHeader() currentRefreshToken: string | null,
): Promise<AccountSessionDTO[]> {
const sessions = await this.sessions.list(user.id, currentRefreshToken);
return sessions.map((s) => ({
id: s.id,
device: s.device,
ip: s.ip,
created_at: s.createdAt,
last_seen_at: s.lastSeenAt,
current: s.current,
}));
}
@Query(() => RecoveryStrategy, {
name: 'getRecoveryStrategy',
description: 'Текущая стратегия восстановления доступа пайщика',
})
@UseGuards(GqlJwtAuthGuard)
async getRecoveryStrategy(@CurrentUser() user: ICurrentUser): Promise<RecoveryStrategy> {
return this.recoveryStrategy.getStrategy(user.id);
}
@Mutation(() => Boolean, {
name: 'revokeSession',
description: 'Завершить конкретную сессию пайщика',
})
@UseGuards(GqlJwtAuthGuard)
async revokeSession(
@Args('data', { type: () => RevokeSessionInputDTO }) data: RevokeSessionInputDTO,
@CurrentUser() user: ICurrentUser,
@ClientIp() ip: string | null,
): Promise<boolean> {
await this.sessions.revoke(user.id, data.session_id, ip);
return true;
}
@Mutation(() => RevokedSessionsResultDTO, {
name: 'revokeAllSessions',
description: 'Завершить все сессии пайщика',
})
@UseGuards(GqlJwtAuthGuard)
async revokeAllSessions(
@CurrentUser() user: ICurrentUser,
@ClientIp() ip: string | null,
): Promise<RevokedSessionsResultDTO> {
return this.sessions.revokeAll(user.id, ip);
}
@Mutation(() => TwoFactorEnrollmentDTO, {
name: 'enrollTwoFactor',
description: 'Начать подключение второго фактора: выпустить секрет и otpauth-URI для QR',
})
@UseGuards(GqlJwtAuthGuard)
async enrollTwoFactor(@CurrentUser() user: ICurrentUser): Promise<TwoFactorEnrollmentDTO> {
const challenge = await this.twoFactor.beginEnrollment(user.id, user.username);
return { secret: challenge.secret, otpauth_uri: challenge.otpauthUri };
}
@Mutation(() => Boolean, {
name: 'activateTwoFactor',
description: 'Подтвердить подключение второго фактора первым кодом',
})
@UseGuards(GqlJwtAuthGuard)
async activateTwoFactor(
@Args('data', { type: () => TwoFactorCodeInputDTO }) data: TwoFactorCodeInputDTO,
@CurrentUser() user: ICurrentUser,
@ClientIp() ip: string | null,
): Promise<boolean> {
await this.twoFactor.activate(user.id, data.code, ip);
return true;
}
@Mutation(() => Boolean, {
name: 'disableTwoFactor',
description: 'Отключить второй фактор (требует валидный код)',
})
@UseGuards(GqlJwtAuthGuard)
async disableTwoFactor(
@Args('data', { type: () => TwoFactorCodeInputDTO }) data: TwoFactorCodeInputDTO,
@CurrentUser() user: ICurrentUser,
@ClientIp() ip: string | null,
): Promise<boolean> {
await this.twoFactor.disable(user.id, data.code, ip);
return true;
}
@Mutation(() => Boolean, {
name: 'setRecoveryStrategy',
description: 'Сменить стратегию восстановления (требует step-up второго фактора)',
})
@UseGuards(GqlJwtAuthGuard)
async setRecoveryStrategy(
@Args('data', { type: () => SetRecoveryStrategyInputDTO }) data: SetRecoveryStrategyInputDTO,
@CurrentUser() user: ICurrentUser,
@ClientIp() ip: string | null,
): Promise<boolean> {
await this.recoveryStrategy.setStrategy(user.id, data.strategy, data.code, ip);
return true;
}
@Mutation(() => RevokedSessionsResultDTO, {
name: 'reportNotMe',
description: 'Сигнал «Это не я»: немедленно завершить все сессии пайщика',
})
@UseGuards(GqlJwtAuthGuard)
async reportNotMe(
@Args('data', { type: () => ReportNotMeInputDTO }) data: ReportNotMeInputDTO,
@CurrentUser() user: ICurrentUser,
@ClientIp() ip: string | null,
): Promise<RevokedSessionsResultDTO> {
return this.incidents.report({
subjectId: user.id,
ip,
source: 'settings',
reportedSessionId: data.session_id ?? null,
});
}
}
@@ -0,0 +1,90 @@
import { Field, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql';
import { RecoveryStrategy } from '~/domain/auth-v2/recovery-strategy/recovery-strategy.types';
/**
* GraphQL-контракт самообслуживания безопасности аккаунта пайщика (Фаза 2 миграции
* REST→GraphQL/SDK): активные сессии (Story 3.7), второй фактор (3.6), стратегия
* восстановления (3.5), сигнал «Это не я» (3.10). Зеркало доменных типов auth-v2,
* поля — snake_case по канону GraphQL этого репозитория; резолвер маппит camelCase
* сервисов в snake_case этих DTO.
*
* Зачем GraphQL, а не REST: единый типизированный фасад фронта — @coopenomics/sdk
* (Zeus). Нового способа взаимодействия с бэкендом наружу не появляется; bearer
* живёт только в SDK. Транспорт (IP, refresh-токен текущей сессии) — через
* request-meta декораторы, не как GraphQL-переменные.
*/
registerEnumType(RecoveryStrategy, {
name: 'RecoveryStrategy',
description: 'Разрешённый канал восстановления доступа пайщика (активен ровно один)',
});
/** Активная сессия пайщика — устройство, с которого выполнен вход. */
@ObjectType('AccountSession')
export class AccountSessionDTO {
@Field(() => String, { description: 'Идентификатор сессии (для точечного завершения)' })
id!: string;
@Field(() => String, { description: 'Устройство входа (User-Agent); заглушка, если метаданные не сохранялись' })
device!: string;
@Field(() => String, { description: 'IP входа; заглушка, если метаданные не сохранялись' })
ip!: string;
@Field(() => String, { description: 'Время создания сессии (ISO)' })
created_at!: string;
@Field(() => String, { description: 'Последняя зафиксированная активность (ISO)' })
last_seen_at!: string;
@Field(() => Boolean, { description: 'Текущая сессия (с которой выполнен запрос)' })
current!: boolean;
}
/** Результат массового завершения сессий. */
@ObjectType('RevokedSessionsResult')
export class RevokedSessionsResultDTO {
@Field(() => Int, { description: 'Сколько активных сессий завершено' })
revoked!: number;
}
/** Вызов на подключение второго фактора: данные для ручного ввода и QR. */
@ObjectType('TwoFactorEnrollment')
export class TwoFactorEnrollmentDTO {
@Field(() => String, { description: 'Base32-секрет для ручного ввода в приложение-аутентификатор' })
secret!: string;
@Field(() => String, { description: 'otpauth://-URI для QR-кода' })
otpauth_uri!: string;
}
/** Вход на завершение конкретной сессии. */
@InputType('RevokeSessionInput')
export class RevokeSessionInputDTO {
@Field(() => String, { description: 'Идентификатор завершаемой сессии' })
session_id!: string;
}
/** Вход на операции второго фактора, требующие TOTP-кода (активация/отключение). */
@InputType('TwoFactorCodeInput')
export class TwoFactorCodeInputDTO {
@Field(() => String, { description: 'Одноразовый код из приложения-аутентификатора' })
code!: string;
}
/** Вход на смену стратегии восстановления (требует step-up второго фактора). */
@InputType('SetRecoveryStrategyInput')
export class SetRecoveryStrategyInputDTO {
@Field(() => RecoveryStrategy, { description: 'Новая стратегия восстановления' })
strategy!: RecoveryStrategy;
@Field(() => String, { description: 'TOTP-код для подтверждения смены (step-up)' })
code!: string;
}
/** Вход на сигнал «Это не я» из настроек ЛК. */
@InputType('ReportNotMeInput')
export class ReportNotMeInputDTO {
@Field(() => String, { nullable: true, description: 'Идентификатор подозрительной сессии (опционально, из настроек)' })
session_id?: string | null;
}
@@ -32,23 +32,20 @@ import { RecoveryService } from './recovery/recovery.service';
import { RecoveryConfirmService } from './recovery/recovery-confirm.service';
import { OfflineRecoveryService } from './recovery/offline-recovery.service';
import { RecoveryStrategyService } from './recovery/recovery-strategy.service';
import { RecoveryStrategyController } from './recovery/recovery-strategy.controller';
import { RecoveryFinalizationService } from './recovery/recovery-finalization.service';
import { RecoveryController } from './recovery/recovery.controller';
import { TwoFactorService } from './two-factor/two-factor.service';
import { TwoFactorController } from './two-factor/two-factor.controller';
import { SessionsService } from './sessions/sessions.service';
import { SessionsController } from './sessions/sessions.controller';
import { SecurityIncidentService } from './security/security-incident.service';
import { SecurityIncidentController } from './security/security-incident.controller';
import { CriticalActionsService } from './critical-actions/critical-actions.service';
import { CriticalActionsController } from './critical-actions/critical-actions.controller';
import { ForceRecoveryService } from './force-recovery/force-recovery.service';
import { ForceRecoveryController } from './force-recovery/force-recovery.controller';
import { KeyRevocationService } from './key-revocation/key-revocation.service';
import { KeyRevocationController } from './key-revocation/key-revocation.controller';
import { CapabilitySetService } from './authorization/capability-set.service';
import { AuthorizationResolver } from './authorization/authorization.resolver';
import { AccountSecurityResolver } from './account-security/account-security.resolver';
import { CriticalActionsResolver } from './critical-actions/critical-actions.resolver';
/**
* auth-v2 (CoopID): новый контур аутентификации. Живёт рядом с legacy `auth/`
@@ -58,9 +55,12 @@ import { AuthorizationResolver } from './authorization/authorization.resolver';
*/
@Module({
imports: [RedisModule, AuthV2InfrastructureModule, TokenApplicationModule, AuthorizationModule],
controllers: [AuthentikEventsController, SessionBindingController, VaultController, VerifyTimestampController, CoopIdClaimsPolicyController, CoopIdSchemaPolicyController, LogoutController, RecoveryController, RecoveryStrategyController, TwoFactorController, SessionsController, SecurityIncidentController, CriticalActionsController, ForceRecoveryController, KeyRevocationController],
// SecurityIncidentController/ForceRecoveryController остаются REST только ради magic-link
// `:token`-эндпоинтов (клик из письма без SDK-контекста); их JWT-методы переведены в
// GraphQL/SDK (AccountSecurityResolver/CriticalActionsResolver, Фаза 2 миграции).
controllers: [AuthentikEventsController, SessionBindingController, VaultController, VerifyTimestampController, CoopIdClaimsPolicyController, CoopIdSchemaPolicyController, LogoutController, RecoveryController, SecurityIncidentController, ForceRecoveryController],
providers: [
AuditService, AuditActionInterceptor, SessionBindingService, VaultService, VerifyTimestampService, CertificateService, CertSettingsService, VerificationTypesService, VerificationRulesService, VerificationRuleGuard, LogoutService, AuthRateLimitGuard, RecoveryService, RecoveryConfirmService, OfflineRecoveryService, RecoveryStrategyService, RecoveryFinalizationService, TwoFactorService, DeviceTrackingService, NewDeviceNotificationService, SecurityEventNotificationService, SessionsService, SecurityIncidentService, CriticalActionsService, ForceRecoveryService, KeyRevocationService, CapabilitySetService, AuthorizationResolver, CertificateResolver,
AuditService, AuditActionInterceptor, SessionBindingService, VaultService, VerifyTimestampService, CertificateService, CertSettingsService, VerificationTypesService, VerificationRulesService, VerificationRuleGuard, LogoutService, AuthRateLimitGuard, RecoveryService, RecoveryConfirmService, OfflineRecoveryService, RecoveryStrategyService, RecoveryFinalizationService, TwoFactorService, DeviceTrackingService, NewDeviceNotificationService, SecurityEventNotificationService, SessionsService, SecurityIncidentService, CriticalActionsService, ForceRecoveryService, KeyRevocationService, CapabilitySetService, AuthorizationResolver, CertificateResolver, AccountSecurityResolver, CriticalActionsResolver,
// Узкий verifier-порт для потребителей (recovery Story 3.2, 2FA-вход) → тот же сервис.
{ provide: TWO_FACTOR_VERIFIER, useExisting: TwoFactorService },
// Финализация recovery (Story 3.3): ротация ключа через registrator::changekey + vault + отзыв сессий + аудит.
@@ -1,69 +0,0 @@
import { BadRequestException, Body, Controller, Get, Param, Post, Req, UnauthorizedException, UseFilters, UseGuards } from '@nestjs/common';
import type { Request } from 'express';
import { HttpJwtAuthGuard } from '~/application/auth/guards/http-jwt-auth.guard';
import { CriticalActionType, type PendingCriticalAction } from '~/domain/auth-v2/ports/pending-critical-actions.port';
import { AuthorizationGuard } from '../authorization/authorization.guard';
import { CheckAbility } from '../authorization/check-ability.decorator';
import { AuthV2ExceptionFilter } from '../exceptions/auth-v2-exception.filter';
import { CriticalActionsService, type CriticalActionAuditEntry } from './critical-actions.service';
interface AuthedRequest extends Request {
user?: { id: string; username: string; role?: string };
}
interface InitiateBody {
actionType: CriticalActionType;
targetId: string;
payload?: Record<string, unknown>;
}
/**
* Critical actions (CoopID, Story 6.8) — первый боевой `@CheckAbility` + `AuthorizationGuard`
* (Story 6.4/6.5): инициация под capability `create CriticalAction` (председатель),
* подтверждение под `confirm CriticalAction` (член совета). Различимость подписантов,
* кворум и окно — в сервисе.
*/
@Controller('coop/critical-actions')
@UseFilters(AuthV2ExceptionFilter)
export class CriticalActionsController {
constructor(private readonly service: CriticalActionsService) {}
@Post()
@UseGuards(HttpJwtAuthGuard, AuthorizationGuard)
@CheckAbility('create', 'CriticalAction')
async initiate(@Body() body: InitiateBody, @Req() req: AuthedRequest): Promise<PendingCriticalAction> {
const actorId = req.user?.username;
if (!actorId) throw new UnauthorizedException();
if (!(Object.values(CriticalActionType) as string[]).includes(body?.actionType))
throw new BadRequestException('Недопустимый тип критического действия');
if (!body?.targetId) throw new BadRequestException('targetId обязателен');
return this.service.initiate({
actionType: body.actionType,
actorId,
targetId: body.targetId,
payload: body.payload,
});
}
@Post(':id/confirm')
@UseGuards(HttpJwtAuthGuard, AuthorizationGuard)
@CheckAbility('confirm', 'CriticalAction')
async confirm(@Param('id') id: string, @Req() req: AuthedRequest): Promise<PendingCriticalAction> {
const confirmerId = req.user?.username;
if (!confirmerId) throw new UnauthorizedException();
return this.service.confirm(id, confirmerId);
}
/**
* Audit-trail критических действий пайщика (Story 6.10): полная атрибуция
* (инициатор + подтверждающие совета + payload_hash) для контролирующего органа.
* Право чтения — `read CriticalAction` (член совета / председатель).
*/
@Get('audit-trail/:targetId')
@UseGuards(HttpJwtAuthGuard, AuthorizationGuard)
@CheckAbility('read', 'CriticalAction')
async auditTrail(@Param('targetId') targetId: string): Promise<CriticalActionAuditEntry[]> {
if (!targetId) throw new BadRequestException('targetId обязателен');
return this.service.getAuditTrail(targetId);
}
}
@@ -0,0 +1,181 @@
import type { CriticalActionsService } from './critical-actions.service';
import type { KeyRevocationService } from '../key-revocation/key-revocation.service';
import type { ForceRecoveryService } from '../force-recovery/force-recovery.service';
import { CriticalActionStatus, CriticalActionType } from '~/domain/auth-v2/ports/pending-critical-actions.port';
import { ForceRecoveryConsentVia } from '../force-recovery/force-recovery.service';
import { CriticalActionsResolver } from './critical-actions.resolver';
const CHAIRMAN = { id: 'u1', username: 'chairman1', role: 'chairman' };
const IP = '203.0.113.7';
const PENDING = {
id: 'ca1',
actionType: CriticalActionType.ExcludeParticipant,
actorId: 'chairman1',
targetId: 'payer1',
payload: { reason: 'x' },
status: CriticalActionStatus.Pending,
confirmations: [{ by: 'chairman1', at: '2026-06-14T00:00:00Z' }],
createdAt: '2026-06-14T00:00:00Z',
expiresAt: '2026-06-15T00:00:00Z',
finalizedAt: null,
};
function build() {
const criticalActions = {
initiate: jest.fn(async () => PENDING),
confirm: jest.fn(async () => ({ ...PENDING, status: CriticalActionStatus.Confirmed })),
getAuditTrail: jest.fn(async () => [
{
id: 'ca1',
actionType: CriticalActionType.ExcludeParticipant,
targetId: 'payer1',
status: CriticalActionStatus.Confirmed,
createdAt: '2026-06-14T00:00:00Z',
finalizedAt: '2026-06-14T01:00:00Z',
initiatorId: 'chairman1',
initiatedAt: '2026-06-14T00:00:00Z',
confirmerIds: [{ by: 'member1', at: '2026-06-14T00:30:00Z' }],
payloadHash: 'deadbeef',
},
]),
};
const keyRevocation = {
revoke: jest.fn(async () => ({ status: 'revoked', targetId: 'payer1', sessionsRevoked: 2, mustRecover: true })),
};
const forceRecovery = {
requestConsent: jest.fn(async () => undefined),
authorize: jest.fn(async () => ({
authorized: true,
consentVia: ForceRecoveryConsentVia.AssemblyDecision,
triggeredBy: 'chairman',
})),
};
const resolver = new CriticalActionsResolver(
criticalActions as unknown as CriticalActionsService,
keyRevocation as unknown as KeyRevocationService,
forceRecovery as unknown as ForceRecoveryService,
);
return { resolver, criticalActions, keyRevocation, forceRecovery };
}
describe('CriticalActionsResolver', () => {
it('initiateCriticalAction ставит actorId = текущий пользователь и маппит pending в snake_case', async () => {
const { resolver, criticalActions } = build();
const out = await resolver.initiateCriticalAction(
{ action_type: CriticalActionType.ExcludeParticipant, target_id: 'payer1', payload: { reason: 'x' } },
CHAIRMAN,
);
expect(criticalActions.initiate).toHaveBeenCalledWith({
actionType: CriticalActionType.ExcludeParticipant,
actorId: 'chairman1',
targetId: 'payer1',
payload: { reason: 'x' },
});
expect(out).toEqual({
id: 'ca1',
action_type: CriticalActionType.ExcludeParticipant,
actor_id: 'chairman1',
target_id: 'payer1',
payload: { reason: 'x' },
status: CriticalActionStatus.Pending,
confirmations: [{ by: 'chairman1', at: '2026-06-14T00:00:00Z' }],
created_at: '2026-06-14T00:00:00Z',
expires_at: '2026-06-15T00:00:00Z',
finalized_at: null,
});
});
it('initiateCriticalAction без payload передаёт undefined', async () => {
const { resolver, criticalActions } = build();
await resolver.initiateCriticalAction(
{ action_type: CriticalActionType.ForceRecovery, target_id: 'payer1' },
CHAIRMAN,
);
expect(criticalActions.initiate).toHaveBeenCalledWith({
actionType: CriticalActionType.ForceRecovery,
actorId: 'chairman1',
targetId: 'payer1',
payload: undefined,
});
});
it('confirmCriticalAction подтверждает от имени текущего пользователя', async () => {
const { resolver, criticalActions } = build();
const out = await resolver.confirmCriticalAction('ca1', CHAIRMAN);
expect(criticalActions.confirm).toHaveBeenCalledWith('ca1', 'chairman1');
expect(out.status).toBe(CriticalActionStatus.Confirmed);
});
it('getCriticalActionAuditTrail маппит атрибуцию в snake_case', async () => {
const { resolver, criticalActions } = build();
const out = await resolver.getCriticalActionAuditTrail('payer1');
expect(criticalActions.getAuditTrail).toHaveBeenCalledWith('payer1');
expect(out).toEqual([
{
id: 'ca1',
action_type: CriticalActionType.ExcludeParticipant,
target_id: 'payer1',
status: CriticalActionStatus.Confirmed,
created_at: '2026-06-14T00:00:00Z',
finalized_at: '2026-06-14T01:00:00Z',
initiator_id: 'chairman1',
initiated_at: '2026-06-14T00:00:00Z',
confirmer_ids: [{ by: 'member1', at: '2026-06-14T00:30:00Z' }],
payload_hash: 'deadbeef',
},
]);
});
it('revokeParticipantKey ставит chairmanId = текущий пользователь и маппит результат', async () => {
const { resolver, keyRevocation } = build();
const out = await resolver.revokeParticipantKey({ target_id: 'payer1', reason: 'compromise' }, CHAIRMAN, IP);
expect(keyRevocation.revoke).toHaveBeenCalledWith({
targetId: 'payer1',
reason: 'compromise',
chairmanId: 'chairman1',
ip: IP,
});
expect(out).toEqual({ status: 'revoked', target_id: 'payer1', sessions_revoked: 2, must_recover: true });
});
it('requestForceRecoveryConsent зовёт сервис с инициатором и возвращает true', async () => {
const { resolver, forceRecovery } = build();
const ok = await resolver.requestForceRecoveryConsent({ target_id: 'payer1' }, CHAIRMAN, IP);
expect(ok).toBe(true);
expect(forceRecovery.requestConsent).toHaveBeenCalledWith('payer1', 'chairman1', IP);
});
it('authorizeForceRecovery маппит результат и прокидывает основания', async () => {
const { resolver, forceRecovery } = build();
const out = await resolver.authorizeForceRecovery(
{ target_id: 'payer1', assembly_decision_tx_id: 'tx9', critical_action_id: 'ca1' },
CHAIRMAN,
IP,
);
expect(forceRecovery.authorize).toHaveBeenCalledWith({
targetId: 'payer1',
initiatorId: 'chairman1',
assemblyDecisionTxId: 'tx9',
criticalActionId: 'ca1',
ip: IP,
});
expect(out).toEqual({
authorized: true,
consent_via: ForceRecoveryConsentVia.AssemblyDecision,
triggered_by: 'chairman',
});
});
it('authorizeForceRecovery без необязательных полей передаёт undefined', async () => {
const { resolver, forceRecovery } = build();
await resolver.authorizeForceRecovery({ target_id: 'payer1' }, CHAIRMAN, IP);
expect(forceRecovery.authorize).toHaveBeenCalledWith({
targetId: 'payer1',
initiatorId: 'chairman1',
assemblyDecisionTxId: undefined,
criticalActionId: undefined,
ip: IP,
});
});
});
@@ -0,0 +1,183 @@
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard';
import { CurrentUser } from '~/application/auth/decorators/current-user.decorator';
import { ClientIp } from '~/application/auth/decorators/request-meta.decorator';
import type { PendingCriticalAction } from '~/domain/auth-v2/ports/pending-critical-actions.port';
import { AuthorizationGuard } from '../authorization/authorization.guard';
import { CheckAbility } from '../authorization/check-ability.decorator';
import { CriticalActionsService, type CriticalActionAuditEntry } from './critical-actions.service';
import { KeyRevocationService } from '../key-revocation/key-revocation.service';
import { ForceRecoveryService } from '../force-recovery/force-recovery.service';
import {
AuthorizeForceRecoveryInputDTO,
CriticalActionAuditEntryDTO,
ForceRecoveryAuthorizationDTO,
InitiateCriticalActionInputDTO,
PendingCriticalActionDTO,
RequestForceRecoveryConsentInputDTO,
RevokeKeyResultDTO,
RevokeParticipantKeyInputDTO,
} from './dto/critical-actions.dto';
interface ICurrentUser {
id: string;
username: string;
role?: string;
}
function mapPending(p: PendingCriticalAction): PendingCriticalActionDTO {
return {
id: p.id,
action_type: p.actionType,
actor_id: p.actorId,
target_id: p.targetId,
payload: p.payload,
status: p.status,
confirmations: p.confirmations.map((c) => ({ by: c.by, at: c.at })),
created_at: p.createdAt,
expires_at: p.expiresAt,
finalized_at: p.finalizedAt ?? null,
};
}
/**
* GraphQL-фасад критических действий совета (Фаза 2 миграции REST→GraphQL/SDK).
* Заменяет REST-контроллеры `coop/critical-actions`, `coop/keys` и JWT-методы
* `coop/force-recovery` (request-consent/authorize) — фронт ходит через
* @coopenomics/sdk (Zeus), нового способа взаимодействия с бэкендом наружу не появляется.
*
* Авторизация — тот же субстрат Эпика 6: `@CheckAbility` + `AuthorizationGuard`
* (уже GraphQL-aware), те же способности, что были на REST. Magic-link
* `coop/force-recovery/consent/:token` остаётся REST (клик из письма без SDK-контекста).
*/
@Resolver()
export class CriticalActionsResolver {
constructor(
private readonly criticalActions: CriticalActionsService,
private readonly keyRevocation: KeyRevocationService,
private readonly forceRecovery: ForceRecoveryService,
) {}
@Mutation(() => PendingCriticalActionDTO, {
name: 'initiateCriticalAction',
description: 'Инициировать критическое действие совета (председатель)',
})
@UseGuards(GqlJwtAuthGuard, AuthorizationGuard)
@CheckAbility('create', 'CriticalAction')
async initiateCriticalAction(
@Args('data', { type: () => InitiateCriticalActionInputDTO }) data: InitiateCriticalActionInputDTO,
@CurrentUser() user: ICurrentUser,
): Promise<PendingCriticalActionDTO> {
const action = await this.criticalActions.initiate({
actionType: data.action_type,
actorId: user.username,
targetId: data.target_id,
payload: data.payload ?? undefined,
});
return mapPending(action);
}
@Mutation(() => PendingCriticalActionDTO, {
name: 'confirmCriticalAction',
description: 'Подтвердить критическое действие совета (член совета)',
})
@UseGuards(GqlJwtAuthGuard, AuthorizationGuard)
@CheckAbility('confirm', 'CriticalAction')
async confirmCriticalAction(
@Args('id', { type: () => String }) id: string,
@CurrentUser() user: ICurrentUser,
): Promise<PendingCriticalActionDTO> {
const action = await this.criticalActions.confirm(id, user.username);
return mapPending(action);
}
@Query(() => [CriticalActionAuditEntryDTO], {
name: 'getCriticalActionAuditTrail',
description: 'Audit-trail критических действий, затрагивающих пайщика (для контролирующего органа)',
})
@UseGuards(GqlJwtAuthGuard, AuthorizationGuard)
@CheckAbility('read', 'CriticalAction')
async getCriticalActionAuditTrail(
@Args('target_id', { type: () => String }) targetId: string,
): Promise<CriticalActionAuditEntryDTO[]> {
const entries: CriticalActionAuditEntry[] = await this.criticalActions.getAuditTrail(targetId);
return entries.map((e) => ({
id: e.id,
action_type: e.actionType,
target_id: e.targetId,
status: e.status,
created_at: e.createdAt,
finalized_at: e.finalizedAt,
initiator_id: e.initiatorId,
initiated_at: e.initiatedAt,
confirmer_ids: e.confirmerIds.map((c) => ({ by: c.by, at: c.at })),
payload_hash: e.payloadHash,
}));
}
@Mutation(() => RevokeKeyResultDTO, {
name: 'revokeParticipantKey',
description: 'Отозвать скомпрометированный ключ пайщика (председатель)',
})
@UseGuards(GqlJwtAuthGuard, AuthorizationGuard)
@CheckAbility('update', 'Participant')
async revokeParticipantKey(
@Args('data', { type: () => RevokeParticipantKeyInputDTO }) data: RevokeParticipantKeyInputDTO,
@CurrentUser() user: ICurrentUser,
@ClientIp() ip: string | null,
): Promise<RevokeKeyResultDTO> {
const result = await this.keyRevocation.revoke({
targetId: data.target_id,
reason: data.reason,
chairmanId: user.username,
ip,
});
return {
status: result.status,
target_id: result.targetId,
sessions_revoked: result.sessionsRevoked,
must_recover: result.mustRecover,
};
}
@Mutation(() => Boolean, {
name: 'requestForceRecoveryConsent',
description: 'Запросить согласие пайщика на принудительное восстановление (председатель)',
})
@UseGuards(GqlJwtAuthGuard, AuthorizationGuard)
@CheckAbility('create', 'CriticalAction')
async requestForceRecoveryConsent(
@Args('data', { type: () => RequestForceRecoveryConsentInputDTO }) data: RequestForceRecoveryConsentInputDTO,
@CurrentUser() user: ICurrentUser,
@ClientIp() ip: string | null,
): Promise<boolean> {
await this.forceRecovery.requestConsent(data.target_id, user.username, ip);
return true;
}
@Mutation(() => ForceRecoveryAuthorizationDTO, {
name: 'authorizeForceRecovery',
description: 'Авторизовать принудительное восстановление доступа пайщика (председатель)',
})
@UseGuards(GqlJwtAuthGuard, AuthorizationGuard)
@CheckAbility('create', 'CriticalAction')
async authorizeForceRecovery(
@Args('data', { type: () => AuthorizeForceRecoveryInputDTO }) data: AuthorizeForceRecoveryInputDTO,
@CurrentUser() user: ICurrentUser,
@ClientIp() ip: string | null,
): Promise<ForceRecoveryAuthorizationDTO> {
const auth = await this.forceRecovery.authorize({
targetId: data.target_id,
initiatorId: user.username,
assemblyDecisionTxId: data.assembly_decision_tx_id ?? undefined,
criticalActionId: data.critical_action_id ?? undefined,
ip,
});
return {
authorized: auth.authorized,
consent_via: auth.consentVia,
triggered_by: auth.triggeredBy,
};
}
}
@@ -0,0 +1,181 @@
import { Field, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import { CriticalActionStatus, CriticalActionType } from '~/domain/auth-v2/ports/pending-critical-actions.port';
import { ForceRecoveryConsentVia } from '../../force-recovery/force-recovery.service';
/**
* GraphQL-контракт критических действий совета (Фаза 2 миграции REST→GraphQL/SDK):
* multi-party critical actions (Story 6.8), audit-trail (6.10), manual key-revoke (4.7),
* force-recovery председателем (6.9). Зеркало доменных типов auth-v2, поля — snake_case
* по канону GraphQL этого репозитория; резолвер маппит camelCase сервисов в snake_case.
*
* Зачем GraphQL, а не REST: единый типизированный фасад фронта — @coopenomics/sdk
* (Zeus). Авторизация — тот же субстрат Эпика 6 (`@CheckAbility` + `AuthorizationGuard`,
* уже GraphQL-aware). Magic-link `:token`-эндпоинты (согласие пайщика, one-click из
* письма) остаются REST — клик без SDK-контекста.
*/
registerEnumType(CriticalActionType, {
name: 'CriticalActionType',
description: 'Тип критического действия совета',
});
registerEnumType(CriticalActionStatus, {
name: 'CriticalActionStatus',
description: 'Состояние критического действия',
});
registerEnumType(ForceRecoveryConsentVia, {
name: 'ForceRecoveryConsentVia',
description: 'Чем подтверждено принудительное восстановление: согласием пайщика или решением собрания',
});
/** Одно подтверждение критического действия: кто и когда. */
@ObjectType('CriticalActionConfirmation')
export class CriticalActionConfirmationDTO {
@Field(() => String, { description: 'Кто подтвердил (имя аккаунта)' })
by!: string;
@Field(() => String, { description: 'Когда подтвердил (ISO)' })
at!: string;
}
/** Pending-запись критического действия (ждёт кворума подтверждений в окне ≤24ч). */
@ObjectType('PendingCriticalAction')
export class PendingCriticalActionDTO {
@Field(() => String, { description: 'Идентификатор критического действия' })
id!: string;
@Field(() => CriticalActionType, { description: 'Тип действия' })
action_type!: CriticalActionType;
@Field(() => String, { description: 'Инициатор (председатель)' })
actor_id!: string;
@Field(() => String, { description: 'Кого/что затрагивает действие' })
target_id!: string;
@Field(() => GraphQLJSON, { description: 'Содержимое действия (зависит от типа)' })
payload!: Record<string, unknown>;
@Field(() => CriticalActionStatus, { description: 'Состояние действия' })
status!: CriticalActionStatus;
@Field(() => [CriticalActionConfirmationDTO], { description: 'Накопленные подтверждения' })
confirmations!: CriticalActionConfirmationDTO[];
@Field(() => String, { description: 'Момент инициации (ISO)' })
created_at!: string;
@Field(() => String, { description: 'Крайний срок подтверждения (ISO)' })
expires_at!: string;
@Field(() => String, { nullable: true, description: 'Момент финализации (ISO); пусто, пока не финализировано' })
finalized_at!: string | null;
}
/** Запись audit-trail критического действия: полная атрибуция для контролирующего органа. */
@ObjectType('CriticalActionAuditEntry')
export class CriticalActionAuditEntryDTO {
@Field(() => String, { description: 'Идентификатор критического действия' })
id!: string;
@Field(() => CriticalActionType, { description: 'Тип действия' })
action_type!: CriticalActionType;
@Field(() => String, { description: 'Кого/что затрагивает действие' })
target_id!: string;
@Field(() => CriticalActionStatus, { description: 'Состояние действия' })
status!: CriticalActionStatus;
@Field(() => String, { description: 'Момент инициации (ISO)' })
created_at!: string;
@Field(() => String, { nullable: true, description: 'Момент финализации (ISO); пусто, пока не финализировано' })
finalized_at!: string | null;
@Field(() => String, { description: 'Инициатор (председатель)' })
initiator_id!: string;
@Field(() => String, { nullable: true, description: 'Момент первой подписи инициатора (ISO)' })
initiated_at!: string | null;
@Field(() => [CriticalActionConfirmationDTO], { description: 'Подтверждающие совета (≠ инициатор) со своими timestamp' })
confirmer_ids!: CriticalActionConfirmationDTO[];
@Field(() => String, { description: 'SHA-256 от payload — гарантия неподменяемости содержимого' })
payload_hash!: string;
}
/** Результат отзыва ключа пайщика председателем. */
@ObjectType('RevokeKeyResult')
export class RevokeKeyResultDTO {
@Field(() => String, { description: 'Статус операции' })
status!: string;
@Field(() => String, { description: 'Пайщик, чей ключ отозван' })
target_id!: string;
@Field(() => Int, { description: 'Сколько активных сессий пайщика отозвано' })
sessions_revoked!: number;
@Field(() => Boolean, { description: 'Пайщик обязан пройти recovery для получения нового ключа' })
must_recover!: boolean;
}
/** Результат авторизации принудительного восстановления председателем. */
@ObjectType('ForceRecoveryAuthorization')
export class ForceRecoveryAuthorizationDTO {
@Field(() => Boolean, { description: 'Восстановление авторизовано' })
authorized!: boolean;
@Field(() => ForceRecoveryConsentVia, { description: 'Чем подтверждено восстановление' })
consent_via!: ForceRecoveryConsentVia;
@Field(() => String, { description: 'Кто инициировал (председатель)' })
triggered_by!: string;
}
/** Вход на инициацию критического действия. */
@InputType('InitiateCriticalActionInput')
export class InitiateCriticalActionInputDTO {
@Field(() => CriticalActionType, { description: 'Тип критического действия' })
action_type!: CriticalActionType;
@Field(() => String, { description: 'Кого/что затрагивает действие' })
target_id!: string;
@Field(() => GraphQLJSON, { nullable: true, description: 'Содержимое действия (зависит от типа)' })
payload?: Record<string, unknown> | null;
}
/** Вход на отзыв ключа пайщика. */
@InputType('RevokeParticipantKeyInput')
export class RevokeParticipantKeyInputDTO {
@Field(() => String, { description: 'Пайщик, чей ключ отзывается' })
target_id!: string;
@Field(() => String, { description: 'Обоснование отзыва' })
reason!: string;
}
/** Вход на запрос согласия пайщика на принудительное восстановление. */
@InputType('RequestForceRecoveryConsentInput')
export class RequestForceRecoveryConsentInputDTO {
@Field(() => String, { description: 'Пайщик, для которого запрашивается согласие' })
target_id!: string;
}
/** Вход на авторизацию принудительного восстановления председателем. */
@InputType('AuthorizeForceRecoveryInput')
export class AuthorizeForceRecoveryInputDTO {
@Field(() => String, { description: 'Пайщик, для которого авторизуется восстановление' })
target_id!: string;
@Field(() => String, { nullable: true, description: 'Идентификатор транзакции решения собрания (если основание — собрание)' })
assembly_decision_tx_id?: string | null;
@Field(() => String, { nullable: true, description: 'Идентификатор связанного критического действия' })
critical_action_id?: string | null;
}
@@ -1,58 +1,25 @@
import { BadRequestException, Body, Controller, Param, Post, Req, UnauthorizedException, UseFilters, UseGuards } from '@nestjs/common';
import { Controller, Param, Post, Req, UseFilters } from '@nestjs/common';
import type { Request } from 'express';
import { HttpJwtAuthGuard } from '~/application/auth/guards/http-jwt-auth.guard';
import { AuthorizationGuard } from '../authorization/authorization.guard';
import { CheckAbility } from '../authorization/check-ability.decorator';
import { AuthV2ExceptionFilter } from '../exceptions/auth-v2-exception.filter';
import { ForceRecoveryService, type ForceRecoveryAuthorization } from './force-recovery.service';
interface AuthedRequest extends Request {
user?: { id: string; username: string; role?: string };
}
import { ForceRecoveryService } from './force-recovery.service';
/**
* Force-recovery (CoopID, Story 6.9). Председатель (capability `create CriticalAction`)
* запрашивает согласие пайщика и авторизует сброс; пайщик подтверждает согласие по
* magic-link без auth-guard (доступ мог быть утрачен). Гейт — в сервисе.
* Согласие пайщика на принудительное восстановление по magic-link (CoopID, Story 6.9).
* Остаётся REST: пайщик подтверждает согласие кликом из письма без auth-guard и без
* SDK-контекста (доступ мог быть утрачен), авторизация — по одноразовому токену из ссылки.
*
* Запрос согласия и авторизация председателем (под JWT+CASL) переведены в GraphQL/SDK —
* см. `CriticalActionsResolver.requestForceRecoveryConsent` / `authorizeForceRecovery`
* (Фаза 2 миграции REST→GraphQL/SDK).
*/
@Controller('coop/force-recovery')
@UseFilters(AuthV2ExceptionFilter)
export class ForceRecoveryController {
constructor(private readonly service: ForceRecoveryService) {}
@Post('request-consent')
@UseGuards(HttpJwtAuthGuard, AuthorizationGuard)
@CheckAbility('create', 'CriticalAction')
async requestConsent(@Body() body: { targetId: string }, @Req() req: AuthedRequest): Promise<{ status: 'requested' }> {
const initiatorId = req.user?.username;
if (!initiatorId) throw new UnauthorizedException();
if (!body?.targetId) throw new BadRequestException('targetId обязателен');
await this.service.requestConsent(body.targetId, initiatorId, req.ip ?? null);
return { status: 'requested' };
}
@Post('consent/:token')
async grantConsent(@Param('token') token: string, @Req() req: AuthedRequest): Promise<{ status: 'granted'; targetId: string }> {
async grantConsent(@Param('token') token: string, @Req() req: Request): Promise<{ status: 'granted'; targetId: string }> {
const { targetId } = await this.service.grantConsent(token, req.ip ?? null);
return { status: 'granted', targetId };
}
@Post('authorize')
@UseGuards(HttpJwtAuthGuard, AuthorizationGuard)
@CheckAbility('create', 'CriticalAction')
async authorize(
@Body() body: { targetId: string; assemblyDecisionTxId?: string; criticalActionId?: string },
@Req() req: AuthedRequest,
): Promise<ForceRecoveryAuthorization> {
const initiatorId = req.user?.username;
if (!initiatorId) throw new UnauthorizedException();
if (!body?.targetId) throw new BadRequestException('targetId обязателен');
return this.service.authorize({
targetId: body.targetId,
initiatorId,
assemblyDecisionTxId: body.assemblyDecisionTxId,
criticalActionId: body.criticalActionId,
ip: req.ip ?? null,
});
}
}
@@ -1,39 +0,0 @@
import { BadRequestException, Body, Controller, Post, Req, UnauthorizedException, UseFilters, UseGuards } from '@nestjs/common';
import type { Request } from 'express';
import { HttpJwtAuthGuard } from '~/application/auth/guards/http-jwt-auth.guard';
import { AuthorizationGuard } from '../authorization/authorization.guard';
import { CheckAbility } from '../authorization/check-ability.decorator';
import { AuthV2ExceptionFilter } from '../exceptions/auth-v2-exception.filter';
import { KeyRevocationService, type RevokeKeyResult } from './key-revocation.service';
interface AuthedRequest extends Request {
user?: { id: string; username: string; role?: string };
}
interface RevokeBody {
targetId: string;
reason: string;
}
/**
* Manual revoke (CoopID, Story 4.7). Председатель отзывает скомпрометированный ключ
* пайщика. Право — `update Participant` (председатель администрирует security-состояние
* пайщика; отдельной CASL-способности `revoke` не вводим — матрица не расширяется).
* Сессии, pending-state и аудит — в сервисе.
*/
@Controller('coop/keys')
@UseFilters(AuthV2ExceptionFilter)
export class KeyRevocationController {
constructor(private readonly service: KeyRevocationService) {}
@Post('revoke')
@UseGuards(HttpJwtAuthGuard, AuthorizationGuard)
@CheckAbility('update', 'Participant')
async revoke(@Body() body: RevokeBody, @Req() req: AuthedRequest): Promise<RevokeKeyResult> {
const chairmanId = req.user?.username;
if (!chairmanId) throw new UnauthorizedException();
if (!body?.targetId) throw new BadRequestException('targetId обязателен');
if (!body?.reason) throw new BadRequestException('reason обязателен (обоснование отзыва)');
return this.service.revoke({ targetId: body.targetId, reason: body.reason, chairmanId, ip: req.ip ?? null });
}
}
@@ -1,57 +0,0 @@
import {
BadRequestException,
Body,
Controller,
Get,
HttpCode,
Post,
Req,
UnauthorizedException,
UseFilters,
UseGuards,
} from '@nestjs/common';
import type { Request } from 'express';
import { HttpJwtAuthGuard } from '~/application/auth/guards/http-jwt-auth.guard';
import { isRecoveryStrategy, RecoveryStrategy } from '~/domain/auth-v2/recovery-strategy/recovery-strategy.types';
import { AuthV2ExceptionFilter } from '../exceptions/auth-v2-exception.filter';
import { RecoveryStrategyService } from './recovery-strategy.service';
interface AuthedRequest extends Request {
user?: { id: string; username: string };
}
interface SetStrategyBody {
strategy?: string;
code?: string;
}
/**
* Настройка стратегии восстановления (CoopID, Story 3.5). Под `HttpJwtAuthGuard` —
* для текущего залогиненного пайщика (subject = `user.id`). Смена требует step-up
* второго фактора (TOTP) — см. отступление от AC в спеке.
*/
@Controller('coop/recovery/strategy')
@UseFilters(AuthV2ExceptionFilter)
@UseGuards(HttpJwtAuthGuard)
export class RecoveryStrategyController {
constructor(private readonly strategy: RecoveryStrategyService) {}
/** Текущая стратегия пайщика. */
@Get()
async get(@Req() req: AuthedRequest): Promise<{ strategy: RecoveryStrategy }> {
const user = req.user;
if (!user?.id) throw new UnauthorizedException();
return { strategy: await this.strategy.getStrategy(user.id) };
}
/** Сменить стратегию (требует TOTP-код step-up). */
@Post()
@HttpCode(204)
async set(@Body() body: SetStrategyBody, @Req() req: AuthedRequest): Promise<void> {
const user = req.user;
if (!user?.id) throw new UnauthorizedException();
if (!isRecoveryStrategy(body?.strategy)) throw new BadRequestException('Недопустимая стратегия восстановления');
if (!body?.code || typeof body.code !== 'string') throw new BadRequestException('Требуется code');
await this.strategy.setStrategy(user.id, body.strategy, body.code, req.ip ?? null);
}
}
@@ -1,41 +1,24 @@
import { Body, Controller, Param, Post, Req, UnauthorizedException, UseFilters, UseGuards } from '@nestjs/common';
import { Controller, Param, Post, Req, UseFilters } from '@nestjs/common';
import type { Request } from 'express';
import { HttpJwtAuthGuard } from '~/application/auth/guards/http-jwt-auth.guard';
import { AuthV2ExceptionFilter } from '../exceptions/auth-v2-exception.filter';
import { SecurityIncidentService } from './security-incident.service';
interface AuthedRequest extends Request {
user?: { id: string; username: string };
}
interface NotMeBody {
/** id сессии, помеченной подозрительной (опц., из настроек). */
sessionId?: string;
}
/**
* Флаг «Это не я» (CoopID, Story 3.10) — немедленный отзыв всех сессий пайщика.
* One-click «Это не я» из письма о новом устройстве (CoopID, Story 3.9) — немедленный
* отзыв всех сессий пайщика. Остаётся REST: клик по magic-link из письма приходит без
* аутентификации и без SDK-контекста (своя сессия могла быть скомпрометирована),
* авторизация — по одноразовому токену из ссылки.
*
* `POST /coop/security/not-me` — из настроек ЛК (под JWT-guard, subject = user.id).
* `POST /coop/security/not-me/:token` — one-click из письма о новом устройстве (3.9):
* без аутентификации (своя сессия могла быть скомпрометирована), авторизация — по
* одноразовому токену из ссылки.
* Сигнал «Это не я» из настроек ЛК (под JWT) переведён в GraphQL/SDK —
* см. `AccountSecurityResolver.reportNotMe` (Фаза 2 миграции REST→GraphQL/SDK).
*/
@Controller('coop/security')
@UseFilters(AuthV2ExceptionFilter)
export class SecurityIncidentController {
constructor(private readonly incidents: SecurityIncidentService) {}
@Post('not-me')
@UseGuards(HttpJwtAuthGuard)
async notMe(@Body() body: NotMeBody, @Req() req: AuthedRequest): Promise<{ revoked: number }> {
const user = req.user;
if (!user?.id) throw new UnauthorizedException();
return this.incidents.report({ subjectId: user.id, ip: req.ip ?? null, source: 'settings', reportedSessionId: body?.sessionId ?? null });
}
@Post('not-me/:token')
async notMeOneClick(@Param('token') token: string, @Req() req: AuthedRequest): Promise<{ revoked: number }> {
async notMeOneClick(@Param('token') token: string, @Req() req: Request): Promise<{ revoked: number }> {
return this.incidents.reportByToken(token, req.ip ?? null);
}
}
@@ -1,50 +0,0 @@
import { Controller, Delete, Get, HttpCode, Param, Req, UnauthorizedException, UseFilters, UseGuards } from '@nestjs/common';
import type { Request } from 'express';
import { HttpJwtAuthGuard } from '~/application/auth/guards/http-jwt-auth.guard';
import type { ActiveSession } from '~/domain/auth-v2/sessions/session.types';
import { AuthV2ExceptionFilter } from '../exceptions/auth-v2-exception.filter';
import { SessionsService } from './sessions.service';
interface AuthedRequest extends Request {
user?: { id: string; username: string };
}
/** Заголовок, которым SPA передаёт свой refresh-токен, чтобы пометить текущую сессию в списке. */
const CURRENT_SESSION_HEADER = 'x-coop-refresh-token';
/**
* Активные сессии пайщика (Story 3.7) под `HttpJwtAuthGuard` — subject = `user.id`.
* GET — список устройств; DELETE /:id — отозвать одну; DELETE — отозвать все.
*/
@Controller('coop/sessions')
@UseFilters(AuthV2ExceptionFilter)
@UseGuards(HttpJwtAuthGuard)
export class SessionsController {
constructor(private readonly sessions: SessionsService) {}
/** Список активных сессий; текущая помечается, если передан X-Coop-Refresh-Token. */
@Get()
async list(@Req() req: AuthedRequest): Promise<{ sessions: ActiveSession[] }> {
const user = req.user;
if (!user?.id) throw new UnauthorizedException();
const current = req.header(CURRENT_SESSION_HEADER) ?? null;
return { sessions: await this.sessions.list(user.id, current) };
}
/** Завершить конкретную сессию. */
@Delete(':id')
@HttpCode(204)
async revoke(@Param('id') id: string, @Req() req: AuthedRequest): Promise<void> {
const user = req.user;
if (!user?.id) throw new UnauthorizedException();
await this.sessions.revoke(user.id, id, req.ip ?? null);
}
/** Завершить все сессии пайщика. */
@Delete()
async revokeAll(@Req() req: AuthedRequest): Promise<{ revoked: number }> {
const user = req.user;
if (!user?.id) throw new UnauthorizedException();
return this.sessions.revokeAll(user.id, req.ip ?? null);
}
}
@@ -1,64 +0,0 @@
import {
BadRequestException,
Body,
Controller,
HttpCode,
Post,
Req,
UnauthorizedException,
UseFilters,
UseGuards,
} from '@nestjs/common';
import type { Request } from 'express';
import { HttpJwtAuthGuard } from '~/application/auth/guards/http-jwt-auth.guard';
import { AuthV2ExceptionFilter } from '../exceptions/auth-v2-exception.filter';
import { TwoFactorService } from './two-factor.service';
import type { EnrollmentChallenge } from './two-factor.service';
interface AuthedRequest extends Request {
user?: { id: string; username: string };
}
interface CodeBody {
code?: string;
}
/**
* Управление вторым фактором (TOTP / Google Authenticator) — CoopID Story 3.6.
* Все операции — для текущего залогиненного пайщика (HttpJwtAuthGuard → req.user).
* Subject второго фактора — `user.id` (как `sub` сертификата, Story 1.8).
*/
@Controller('coop/2fa')
@UseFilters(AuthV2ExceptionFilter)
@UseGuards(HttpJwtAuthGuard)
export class TwoFactorController {
constructor(private readonly twoFactor: TwoFactorService) {}
/** Начать подключение: выпустить секрет + otpauth-URI для QR. */
@Post('enroll')
async enroll(@Req() req: AuthedRequest): Promise<EnrollmentChallenge> {
const user = req.user;
if (!user?.id) throw new UnauthorizedException();
return this.twoFactor.beginEnrollment(user.id, user.username);
}
/** Подтвердить подключение первым кодом. */
@Post('activate')
@HttpCode(204)
async activate(@Body() body: CodeBody, @Req() req: AuthedRequest): Promise<void> {
const user = req.user;
if (!user?.id) throw new UnauthorizedException();
if (!body?.code) throw new BadRequestException('Требуется code');
await this.twoFactor.activate(user.id, body.code, req.ip ?? null);
}
/** Отключить второй фактор (требует валидный код). */
@Post('disable')
@HttpCode(204)
async disable(@Body() body: CodeBody, @Req() req: AuthedRequest): Promise<void> {
const user = req.user;
if (!user?.id) throw new UnauthorizedException();
if (!body?.code) throw new BadRequestException('Требуется code');
await this.twoFactor.disable(user.id, body.code, req.ip ?? null);
}
}
@@ -0,0 +1,29 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
/**
* Транспортные детали запроса для GraphQL-резолверов auth-v2 (Фаза 2 миграции
* REST→GraphQL/SDK). Доменные аргументы идут через `@Args`/Input-DTO; IP и
* заголовок текущей сессии — это транспорт, не домен, поэтому достаются здесь
* из express-`req` в GraphQL-контексте (по образцу {@link CurrentUser}), а не
* прокидываются как GraphQL-переменные. Так IP/refresh-токен не светятся в
* запросе/логах GraphQL, а резолвер остаётся тестируемым (значение передаётся
* в метод напрямую — декоратор в unit-тесте не исполняется).
*/
/** IP-адрес клиента (для аудита security-операций); null, если недоступен. */
export const ClientIp = createParamDecorator((_data: unknown, context: ExecutionContext): string | null => {
const ctx = GqlExecutionContext.create(context);
const req = ctx.getContext().req;
return req?.ip ?? null;
});
/** Заголовок, которым SPA передаёт свой refresh-токен, чтобы пометить текущую сессию в списке. */
export const CURRENT_SESSION_HEADER = 'x-coop-refresh-token';
/** Refresh-токен текущей сессии из заголовка (пометка `current` в списке сессий); null, если не передан. */
export const RefreshTokenHeader = createParamDecorator((_data: unknown, context: ExecutionContext): string | null => {
const ctx = GqlExecutionContext.create(context);
const req = ctx.getContext().req;
return req?.headers?.[CURRENT_SESSION_HEADER] ?? null;
});
+131
View File
@@ -127,6 +127,9 @@ export const AllTypesProps: Record<string,any> = {
},
AuthorizeDecisionInput:{
document:"SignedDigitalDocumentInput"
},
AuthorizeForceRecoveryInput:{
},
BankAccountDetailsInput:{
@@ -364,6 +367,8 @@ export const AllTypesProps: Record<string,any> = {
CreateWithdrawInput:{
statement:"ReturnByMoneySignedDocumentInput"
},
CriticalActionStatus: "enum" as const,
CriticalActionType: "enum" as const,
CurrentTableStatesFiltersInput:{
},
@@ -444,6 +449,7 @@ export const AllTypesProps: Record<string,any> = {
FinalizeProjectInput:{
},
ForceRecoveryConsentVia: "enum" as const,
FreeDecisionGenerateDocumentInput:{
},
@@ -605,6 +611,10 @@ export const AllTypesProps: Record<string,any> = {
Init:{
organization_data:"CreateInitOrganizationDataInput"
},
InitiateCriticalActionInput:{
action_type:"CriticalActionType",
payload:"JSON"
},
Install:{
soviet:"SovietMemberInput",
vars:"SetVarsInput"
@@ -647,6 +657,9 @@ export const AllTypesProps: Record<string,any> = {
acceptChildOrder:{
data:"AcceptChildOrderInput"
},
activateTwoFactor:{
data:"TwoFactorCodeInput"
},
addParticipant:{
data:"AddParticipantInput"
},
@@ -662,6 +675,9 @@ export const AllTypesProps: Record<string,any> = {
authorizeDecision:{
data:"AuthorizeDecisionInput"
},
authorizeForceRecovery:{
data:"AuthorizeForceRecoveryInput"
},
cancelRequest:{
data:"CancelRequestInput"
},
@@ -953,6 +969,9 @@ export const AllTypesProps: Record<string,any> = {
},
confirmAgreement:{
data:"ConfirmAgreementInput"
},
confirmCriticalAction:{
},
confirmReceiveOnRequest:{
data:"ConfirmReceiveOnRequestInput"
@@ -1017,6 +1036,9 @@ export const AllTypesProps: Record<string,any> = {
deliverOnRequest:{
data:"DeliverOnRequestInput"
},
disableTwoFactor:{
data:"TwoFactorCodeInput"
},
disputeOnRequest:{
data:"DisputeOnRequestInput"
},
@@ -1127,6 +1149,9 @@ export const AllTypesProps: Record<string,any> = {
initSystem:{
data:"Init"
},
initiateCriticalAction:{
data:"InitiateCriticalActionInput"
},
installExtension:{
data:"ExtensionInput"
},
@@ -1178,6 +1203,12 @@ export const AllTypesProps: Record<string,any> = {
registerParticipant:{
data:"RegisterParticipantInput"
},
reportNotMe:{
data:"ReportNotMeInput"
},
requestForceRecoveryConsent:{
data:"RequestForceRecoveryConsentInput"
},
resendNotification:{
},
@@ -1190,6 +1221,12 @@ export const AllTypesProps: Record<string,any> = {
revokeCapabilitySet:{
data:"RevokeCapabilitySetInput"
},
revokeParticipantKey:{
data:"RevokeParticipantKeyInput"
},
revokeSession:{
data:"RevokeSessionInput"
},
saveReportDraft:{
input:"SaveReportDraftInput"
},
@@ -1202,6 +1239,9 @@ export const AllTypesProps: Record<string,any> = {
setPaymentStatus:{
data:"SetPaymentStatusInput"
},
setRecoveryStrategy:{
data:"SetRecoveryStrategyInput"
},
setWif:{
data:"SetWifInput"
},
@@ -1554,6 +1594,9 @@ export const AllTypesProps: Record<string,any> = {
},
getCapitalProjectLogs:{
data:"GetCapitalLogsInput"
},
getCriticalActionAuditTrail:{
},
getCurrentTableStates:{
filters:"CurrentTableStatesFiltersInput",
@@ -1686,6 +1729,7 @@ export const AllTypesProps: Record<string,any> = {
ReceiveOnRequestInput:{
document:"ReturnByAssetActSignedDocumentInput"
},
RecoveryStrategy: "enum" as const,
RefreshInput:{
},
@@ -1719,6 +1763,9 @@ export const AllTypesProps: Record<string,any> = {
},
ReportHistoryFilterInput:{
reportType:"ReportType"
},
ReportNotMeInput:{
},
ReportPreviewInput:{
reportType:"ReportType"
@@ -1727,6 +1774,9 @@ export const AllTypesProps: Record<string,any> = {
ReportType: "enum" as const,
RepresentedByInput:{
},
RequestForceRecoveryConsentInput:{
},
RequisiteSource: "enum" as const,
ResetKeyInput:{
@@ -1788,6 +1838,12 @@ export const AllTypesProps: Record<string,any> = {
},
RevokeCapabilitySetInput:{
},
RevokeParticipantKeyInput:{
},
RevokeSessionInput:{
},
RoomMessageKind: "enum" as const,
SaveReportDraftInput:{
@@ -1833,6 +1889,9 @@ export const AllTypesProps: Record<string,any> = {
},
SetPlanInput:{
},
SetRecoveryStrategyInput:{
strategy:"RecoveryStrategy"
},
SetVarsInput:{
coopenomics_agreement:"AgreementVarInput",
@@ -1897,6 +1956,9 @@ export const AllTypesProps: Record<string,any> = {
TriggerNotificationWorkflowInput:{
payload:"JSONObject",
to:"NotificationWorkflowRecipientInput"
},
TwoFactorCodeInput:{
},
UninstallExtensionInput:{
@@ -2019,6 +2081,14 @@ export const ReturnTypes: Record<string,any> = {
max:"String",
used:"String"
},
AccountSession:{
created_at:"String",
current:"Boolean",
device:"String",
id:"String",
ip:"String",
last_seen_at:"String"
},
AccountsPaginationResult:{
currentPage:"Int",
items:"Account",
@@ -3112,6 +3182,22 @@ export const ReturnTypes: Record<string,any> = {
question:"String",
title:"String"
},
CriticalActionAuditEntry:{
action_type:"CriticalActionType",
confirmer_ids:"CriticalActionConfirmation",
created_at:"String",
finalized_at:"String",
id:"String",
initiated_at:"String",
initiator_id:"String",
payload_hash:"String",
status:"CriticalActionStatus",
target_id:"String"
},
CriticalActionConfirmation:{
at:"String",
by:"String"
},
CurrentInstanceDTO:{
blockchain_status:"String",
description:"String",
@@ -3286,6 +3372,11 @@ export const ReturnTypes: Record<string,any> = {
message:"String",
path:"String"
},
ForceRecoveryAuthorization:{
authorized:"Boolean",
consent_via:"ForceRecoveryConsentVia",
triggered_by:"String"
},
GatewayPayment:{
blockchain_data:"JSON",
can_change_status:"Boolean",
@@ -3602,11 +3693,13 @@ export const ReturnTypes: Record<string,any> = {
},
Mutation:{
acceptChildOrder:"Transaction",
activateTwoFactor:"Boolean",
addParticipant:"Account",
addPaymentMethod:"PaymentMethod",
addTrustedAccount:"Branch",
assignCapabilitySet:"Boolean",
authorizeDecision:"Transaction",
authorizeForceRecovery:"ForceRecoveryAuthorization",
cancelRequest:"Transaction",
capitalAddAuthor:"CapitalProject",
capitalApproveCommit:"CapitalCommit",
@@ -3698,6 +3791,7 @@ export const ReturnTypes: Record<string,any> = {
completeExtensionOnboardingStep:"ExtensionOnboardingState",
completeRequest:"Transaction",
confirmAgreement:"Transaction",
confirmCriticalAction:"PendingCriticalAction",
confirmReceiveOnRequest:"Transaction",
confirmSupplyOnRequest:"Transaction",
createAnnualGeneralMeet:"MeetAggregate",
@@ -3719,8 +3813,10 @@ export const ReturnTypes: Record<string,any> = {
deleteReportDraft:"Boolean",
deleteTrustedAccount:"Branch",
deliverOnRequest:"Transaction",
disableTwoFactor:"Boolean",
disputeOnRequest:"Transaction",
editBranch:"Branch",
enrollTwoFactor:"TwoFactorEnrollment",
generateAnnualGeneralMeetAgendaDocument:"GeneratedDocument",
generateAnnualGeneralMeetDecisionDocument:"GeneratedDocument",
generateAnnualGeneralMeetNotificationDocument:"GeneratedDocument",
@@ -3748,6 +3844,7 @@ export const ReturnTypes: Record<string,any> = {
generateUserAgreement:"GeneratedDocument",
generateWalletAgreement:"GeneratedDocument",
initSystem:"SystemInfo",
initiateCriticalAction:"PendingCriticalAction",
installExtension:"Extension",
installSystem:"SystemInfo",
login:"RegisteredAccount",
@@ -3765,15 +3862,21 @@ export const ReturnTypes: Record<string,any> = {
refresh:"RegisteredAccount",
registerAccount:"RegisteredAccount",
registerParticipant:"Account",
reportNotMe:"RevokedSessionsResult",
requestForceRecoveryConsent:"Boolean",
resendNotification:"Notification",
resetKey:"Boolean",
resetRegistration:"Account",
restartAnnualGeneralMeet:"MeetAggregate",
revokeAllSessions:"RevokedSessionsResult",
revokeCapabilitySet:"Boolean",
revokeParticipantKey:"RevokeKeyResult",
revokeSession:"Boolean",
saveReportDraft:"ReportDraft",
selectBranch:"Boolean",
sendAgreement:"Transaction",
setPaymentStatus:"GatewayPayment",
setRecoveryStrategy:"Boolean",
setWif:"Boolean",
signByPresiderOnAnnualGeneralMeet:"MeetAggregate",
signBySecretaryOnAnnualGeneralMeet:"MeetAggregate",
@@ -4085,6 +4188,18 @@ export const ReturnTypes: Record<string,any> = {
totalCount:"Int",
totalPages:"Int"
},
PendingCriticalAction:{
action_type:"CriticalActionType",
actor_id:"String",
confirmations:"CriticalActionConfirmation",
created_at:"String",
expires_at:"String",
finalized_at:"String",
id:"String",
payload:"JSON",
status:"CriticalActionStatus",
target_id:"String"
},
Permission:{
parent:"String",
perm_name:"String",
@@ -4331,6 +4446,7 @@ export const ReturnTypes: Record<string,any> = {
getCapitalOnboardingState:"CapitalOnboardingState",
getCapitalProjectLogs:"PaginatedCapitalLogsPaginationResult",
getChairmanOnboardingState:"ChairmanOnboardingState",
getCriticalActionAuditTrail:"CriticalActionAuditEntry",
getCurrentInstance:"CurrentInstanceDTO",
getCurrentTableStates:"PaginatedCurrentTableStatesPaginationResult",
getDeltas:"PaginatedDeltasPaginationResult",
@@ -4360,6 +4476,7 @@ export const ReturnTypes: Record<string,any> = {
getProgramWallets:"ProgramWalletsPaginationResult",
getProviderSubscriptionById:"ProviderSubscription",
getProviderSubscriptions:"ProviderSubscription",
getRecoveryStrategy:"RecoveryStrategy",
getRegistrationAgreements:"RegistrationAgreement",
getRegistrationConfig:"RegistrationConfig",
getReport:"GeneratedReport",
@@ -4368,6 +4485,7 @@ export const ReturnTypes: Record<string,any> = {
getReportHistory:"ReportHistoryPage",
getReportPreview:"ReportPreview",
getReportRequisites:"ReportRequisitesView",
getSessions:"AccountSession",
getSystemInfo:"SystemInfo",
getUnreadNotificationsCount:"UnreadNotificationsCount",
getUserWebPushSubscriptions:"WebPushSubscriptionDto",
@@ -4539,6 +4657,15 @@ export const ReturnTypes: Record<string,any> = {
owner:"String",
ram_bytes:"Int"
},
RevokeKeyResult:{
must_recover:"Boolean",
sessions_revoked:"Int",
status:"String",
target_id:"String"
},
RevokedSessionsResult:{
revoked:"Int"
},
SbpAccount:{
phone:"String"
},
@@ -4656,6 +4783,10 @@ export const ReturnTypes: Record<string,any> = {
startOffset:"Float",
text:"String"
},
TwoFactorEnrollment:{
otpauth_uri:"String",
secret:"String"
},
UnreadNotificationsCount:{
count:"Int"
},
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
export const name = 'activateTwoFactor'
/**
* Подтвердить подключение второго фактора первым кодом.
*/
export const mutation = Selector('Mutation')({
[name]: [{ data: $('data', 'TwoFactorCodeInput!') }, true],
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
data: ModelTypes['TwoFactorCodeInput']
}
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
@@ -0,0 +1,21 @@
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
export const name = 'disableTwoFactor'
/**
* Отключить второй фактор (требует валидный код).
*/
export const mutation = Selector('Mutation')({
[name]: [{ data: $('data', 'TwoFactorCodeInput!') }, true],
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
data: ModelTypes['TwoFactorCodeInput']
}
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
@@ -0,0 +1,20 @@
import { twoFactorEnrollmentSelector } from '../../selectors/accountSecurity/twoFactorEnrollmentSelector'
import { type GraphQLTypes, type InputType, Selector } from '../../zeus/index'
export const name = 'enrollTwoFactor'
/**
* Начать подключение второго фактора: выпустить секрет и otpauth-URI для QR.
*/
export const mutation = Selector('Mutation')({
[name]: twoFactorEnrollmentSelector,
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
}
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
@@ -0,0 +1,20 @@
/** Подтвердить подключение второго фактора первым кодом */
export * as ActivateTwoFactor from './activateTwoFactor'
/** Отключить второй фактор */
export * as DisableTwoFactor from './disableTwoFactor'
/** Начать подключение второго фактора */
export * as EnrollTwoFactor from './enrollTwoFactor'
/** Сигнал «Это не я»: немедленно завершить все сессии пайщика */
export * as ReportNotMe from './reportNotMe'
/** Завершить все сессии пайщика */
export * as RevokeAllSessions from './revokeAllSessions'
/** Завершить конкретную сессию пайщика */
export * as RevokeSession from './revokeSession'
/** Сменить стратегию восстановления */
export * as SetRecoveryStrategy from './setRecoveryStrategy'
@@ -0,0 +1,22 @@
import { revokedSessionsResultSelector } from '../../selectors/accountSecurity/revokedSessionsResultSelector'
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
export const name = 'reportNotMe'
/**
* Сигнал «Это не я»: немедленно завершить все сессии пайщика.
*/
export const mutation = Selector('Mutation')({
[name]: [{ data: $('data', 'ReportNotMeInput!') }, revokedSessionsResultSelector],
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
data: ModelTypes['ReportNotMeInput']
}
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
@@ -0,0 +1,20 @@
import { revokedSessionsResultSelector } from '../../selectors/accountSecurity/revokedSessionsResultSelector'
import { type GraphQLTypes, type InputType, Selector } from '../../zeus/index'
export const name = 'revokeAllSessions'
/**
* Завершить все сессии пайщика.
*/
export const mutation = Selector('Mutation')({
[name]: revokedSessionsResultSelector,
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
}
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
@@ -0,0 +1,21 @@
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
export const name = 'revokeSession'
/**
* Завершить конкретную сессию пайщика.
*/
export const mutation = Selector('Mutation')({
[name]: [{ data: $('data', 'RevokeSessionInput!') }, true],
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
data: ModelTypes['RevokeSessionInput']
}
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
@@ -0,0 +1,21 @@
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
export const name = 'setRecoveryStrategy'
/**
* Сменить стратегию восстановления (требует step-up второго фактора).
*/
export const mutation = Selector('Mutation')({
[name]: [{ data: $('data', 'SetRecoveryStrategyInput!') }, true],
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
data: ModelTypes['SetRecoveryStrategyInput']
}
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
@@ -0,0 +1,22 @@
import { forceRecoveryAuthorizationSelector } from '../../selectors/criticalActions/forceRecoveryAuthorizationSelector'
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
export const name = 'authorizeForceRecovery'
/**
* Авторизовать принудительное восстановление доступа пайщика (председатель).
*/
export const mutation = Selector('Mutation')({
[name]: [{ data: $('data', 'AuthorizeForceRecoveryInput!') }, forceRecoveryAuthorizationSelector],
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
data: ModelTypes['AuthorizeForceRecoveryInput']
}
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
@@ -0,0 +1,22 @@
import { pendingCriticalActionSelector } from '../../selectors/criticalActions/pendingCriticalActionSelector'
import { $, type GraphQLTypes, type InputType, Selector } from '../../zeus/index'
export const name = 'confirmCriticalAction'
/**
* Подтвердить критическое действие совета (член совета).
*/
export const mutation = Selector('Mutation')({
[name]: [{ id: $('id', 'String!') }, pendingCriticalActionSelector],
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
id: string
}
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
@@ -0,0 +1,14 @@
/** Авторизовать принудительное восстановление доступа пайщика */
export * as AuthorizeForceRecovery from './authorizeForceRecovery'
/** Подтвердить критическое действие совета */
export * as ConfirmCriticalAction from './confirmCriticalAction'
/** Инициировать критическое действие совета */
export * as InitiateCriticalAction from './initiateCriticalAction'
/** Запросить согласие пайщика на принудительное восстановление */
export * as RequestForceRecoveryConsent from './requestForceRecoveryConsent'
/** Отозвать скомпрометированный ключ пайщика */
export * as RevokeParticipantKey from './revokeParticipantKey'
@@ -0,0 +1,22 @@
import { pendingCriticalActionSelector } from '../../selectors/criticalActions/pendingCriticalActionSelector'
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
export const name = 'initiateCriticalAction'
/**
* Инициировать критическое действие совета (председатель).
*/
export const mutation = Selector('Mutation')({
[name]: [{ data: $('data', 'InitiateCriticalActionInput!') }, pendingCriticalActionSelector],
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
data: ModelTypes['InitiateCriticalActionInput']
}
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
@@ -0,0 +1,21 @@
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
export const name = 'requestForceRecoveryConsent'
/**
* Запросить согласие пайщика на принудительное восстановление (председатель).
*/
export const mutation = Selector('Mutation')({
[name]: [{ data: $('data', 'RequestForceRecoveryConsentInput!') }, true],
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
data: ModelTypes['RequestForceRecoveryConsentInput']
}
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
@@ -0,0 +1,22 @@
import { revokeKeyResultSelector } from '../../selectors/criticalActions/revokeKeyResultSelector'
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
export const name = 'revokeParticipantKey'
/**
* Отозвать скомпрометированный ключ пайщика (председатель).
*/
export const mutation = Selector('Mutation')({
[name]: [{ data: $('data', 'RevokeParticipantKeyInput!') }, revokeKeyResultSelector],
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
data: ModelTypes['RevokeParticipantKeyInput']
}
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
+2
View File
@@ -1,4 +1,5 @@
export * as Accounts from './accounts'
export * as AccountSecurity from './accountSecurity'
export * as Agreements from './agreements'
export * as Auth from './auth'
export * as Authorization from './authorization'
@@ -7,6 +8,7 @@ export * as Capital from './capital'
export * as Chairman from './chairman'
export * as ChatCoop from './chatcoop'
export * as Cooplace from './cooplace'
export * as CriticalActions from './criticalActions'
export * as Decisions from './decisions'
export * as Documents from './documents'
export * as Extensions from './extensions'
@@ -0,0 +1,19 @@
import { type GraphQLTypes, type InputType, Selector } from '../../zeus/index'
export const name = 'getRecoveryStrategy'
/**
* Текущая стратегия восстановления доступа пайщика (enum-значение).
*/
export const query = Selector('Query')({
[name]: true,
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
}
export type IOutput = InputType<GraphQLTypes['Query'], typeof query>
@@ -0,0 +1,20 @@
import { accountSessionSelector } from '../../selectors/accountSecurity/accountSessionSelector'
import { type GraphQLTypes, type InputType, Selector } from '../../zeus/index'
export const name = 'getSessions'
/**
* Активные сессии текущего пайщика (текущая помечается current).
*/
export const query = Selector('Query')({
[name]: accountSessionSelector,
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
}
export type IOutput = InputType<GraphQLTypes['Query'], typeof query>
@@ -0,0 +1,5 @@
/** Текущая стратегия восстановления доступа пайщика */
export * as GetRecoveryStrategy from './getRecoveryStrategy'
/** Активные сессии текущего пайщика */
export * as GetSessions from './getSessions'
@@ -0,0 +1,22 @@
import { criticalActionAuditEntrySelector } from '../../selectors/criticalActions/criticalActionAuditEntrySelector'
import { $, type GraphQLTypes, type InputType, Selector } from '../../zeus/index'
export const name = 'getCriticalActionAuditTrail'
/**
* Audit-trail критических действий, затрагивающих пайщика (для контролирующего органа).
*/
export const query = Selector('Query')({
[name]: [{ target_id: $('target_id', 'String!') }, criticalActionAuditEntrySelector],
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
target_id: string
}
export type IOutput = InputType<GraphQLTypes['Query'], typeof query>
@@ -0,0 +1,2 @@
/** Audit-trail критических действий, затрагивающих пайщика */
export * as GetCriticalActionAuditTrail from './getCriticalActionAuditTrail'
+2
View File
@@ -1,4 +1,5 @@
export * as Accounts from './accounts'
export * as AccountSecurity from './accountSecurity'
export * as Agenda from './agenda'
export * as Agreements from './agreements'
export * as Authorization from './authorization'
@@ -8,6 +9,7 @@ export * as Capital from './capital'
export * as Certificate from './certificate'
export * as Chairman from './chairman'
export * as ChatCoop from './chatcoop'
export * as CriticalActions from './criticalActions'
export * as Desktop from './desktop'
export * as Documents from './documents'
export * as Extensions from './extensions'
@@ -0,0 +1,16 @@
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
import { Selector, type ValueTypes } from '../../zeus/index'
export const rawAccountSessionSelector = {
id: true,
device: true,
ip: true,
created_at: true,
last_seen_at: true,
current: true,
}
// Проверка валидности
const _validate: MakeAllFieldsRequired<ValueTypes['AccountSession']> = rawAccountSessionSelector
export const accountSessionSelector = Selector('AccountSession')(rawAccountSessionSelector)
@@ -0,0 +1,3 @@
export * from './accountSessionSelector'
export * from './revokedSessionsResultSelector'
export * from './twoFactorEnrollmentSelector'
@@ -0,0 +1,11 @@
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
import { Selector, type ValueTypes } from '../../zeus/index'
export const rawRevokedSessionsResultSelector = {
revoked: true,
}
// Проверка валидности
const _validate: MakeAllFieldsRequired<ValueTypes['RevokedSessionsResult']> = rawRevokedSessionsResultSelector
export const revokedSessionsResultSelector = Selector('RevokedSessionsResult')(rawRevokedSessionsResultSelector)
@@ -0,0 +1,12 @@
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
import { Selector, type ValueTypes } from '../../zeus/index'
export const rawTwoFactorEnrollmentSelector = {
secret: true,
otpauth_uri: true,
}
// Проверка валидности
const _validate: MakeAllFieldsRequired<ValueTypes['TwoFactorEnrollment']> = rawTwoFactorEnrollmentSelector
export const twoFactorEnrollmentSelector = Selector('TwoFactorEnrollment')(rawTwoFactorEnrollmentSelector)
@@ -0,0 +1,21 @@
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
import { Selector, type ValueTypes } from '../../zeus/index'
import { rawCriticalActionConfirmationSelector } from './criticalActionConfirmationSelector'
export const rawCriticalActionAuditEntrySelector = {
id: true,
action_type: true,
target_id: true,
status: true,
created_at: true,
finalized_at: true,
initiator_id: true,
initiated_at: true,
confirmer_ids: rawCriticalActionConfirmationSelector,
payload_hash: true,
}
// Проверка валидности
const _validate: MakeAllFieldsRequired<ValueTypes['CriticalActionAuditEntry']> = rawCriticalActionAuditEntrySelector
export const criticalActionAuditEntrySelector = Selector('CriticalActionAuditEntry')(rawCriticalActionAuditEntrySelector)
@@ -0,0 +1,12 @@
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
import { Selector, type ValueTypes } from '../../zeus/index'
export const rawCriticalActionConfirmationSelector = {
by: true,
at: true,
}
// Проверка валидности
const _validate: MakeAllFieldsRequired<ValueTypes['CriticalActionConfirmation']> = rawCriticalActionConfirmationSelector
export const criticalActionConfirmationSelector = Selector('CriticalActionConfirmation')(rawCriticalActionConfirmationSelector)
@@ -0,0 +1,13 @@
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
import { Selector, type ValueTypes } from '../../zeus/index'
export const rawForceRecoveryAuthorizationSelector = {
authorized: true,
consent_via: true,
triggered_by: true,
}
// Проверка валидности
const _validate: MakeAllFieldsRequired<ValueTypes['ForceRecoveryAuthorization']> = rawForceRecoveryAuthorizationSelector
export const forceRecoveryAuthorizationSelector = Selector('ForceRecoveryAuthorization')(rawForceRecoveryAuthorizationSelector)
@@ -0,0 +1,5 @@
export * from './criticalActionAuditEntrySelector'
export * from './criticalActionConfirmationSelector'
export * from './forceRecoveryAuthorizationSelector'
export * from './pendingCriticalActionSelector'
export * from './revokeKeyResultSelector'
@@ -0,0 +1,21 @@
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
import { Selector, type ValueTypes } from '../../zeus/index'
import { rawCriticalActionConfirmationSelector } from './criticalActionConfirmationSelector'
export const rawPendingCriticalActionSelector = {
id: true,
action_type: true,
actor_id: true,
target_id: true,
payload: true,
status: true,
confirmations: rawCriticalActionConfirmationSelector,
created_at: true,
expires_at: true,
finalized_at: true,
}
// Проверка валидности
const _validate: MakeAllFieldsRequired<ValueTypes['PendingCriticalAction']> = rawPendingCriticalActionSelector
export const pendingCriticalActionSelector = Selector('PendingCriticalAction')(rawPendingCriticalActionSelector)
@@ -0,0 +1,14 @@
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
import { Selector, type ValueTypes } from '../../zeus/index'
export const rawRevokeKeyResultSelector = {
status: true,
target_id: true,
sessions_revoked: true,
must_recover: true,
}
// Проверка валидности
const _validate: MakeAllFieldsRequired<ValueTypes['RevokeKeyResult']> = rawRevokeKeyResultSelector
export const revokeKeyResultSelector = Selector('RevokeKeyResult')(rawRevokeKeyResultSelector)
+2
View File
@@ -1,4 +1,5 @@
export * from './accounts'
export * from './accountSecurity'
export * from './agreements'
export * from './authorization'
export * from './branches'
@@ -7,6 +8,7 @@ export * from './certificate'
export * from './chatcoop'
export * from './common'
export * from './cooplace'
export * from './criticalActions'
export * from './extensions'
export * from './freeDecisions'
export * from './gateway'
+131
View File
@@ -127,6 +127,9 @@ export const AllTypesProps: Record<string,any> = {
},
AuthorizeDecisionInput:{
document:"SignedDigitalDocumentInput"
},
AuthorizeForceRecoveryInput:{
},
BankAccountDetailsInput:{
@@ -364,6 +367,8 @@ export const AllTypesProps: Record<string,any> = {
CreateWithdrawInput:{
statement:"ReturnByMoneySignedDocumentInput"
},
CriticalActionStatus: "enum" as const,
CriticalActionType: "enum" as const,
CurrentTableStatesFiltersInput:{
},
@@ -444,6 +449,7 @@ export const AllTypesProps: Record<string,any> = {
FinalizeProjectInput:{
},
ForceRecoveryConsentVia: "enum" as const,
FreeDecisionGenerateDocumentInput:{
},
@@ -605,6 +611,10 @@ export const AllTypesProps: Record<string,any> = {
Init:{
organization_data:"CreateInitOrganizationDataInput"
},
InitiateCriticalActionInput:{
action_type:"CriticalActionType",
payload:"JSON"
},
Install:{
soviet:"SovietMemberInput",
vars:"SetVarsInput"
@@ -647,6 +657,9 @@ export const AllTypesProps: Record<string,any> = {
acceptChildOrder:{
data:"AcceptChildOrderInput"
},
activateTwoFactor:{
data:"TwoFactorCodeInput"
},
addParticipant:{
data:"AddParticipantInput"
},
@@ -662,6 +675,9 @@ export const AllTypesProps: Record<string,any> = {
authorizeDecision:{
data:"AuthorizeDecisionInput"
},
authorizeForceRecovery:{
data:"AuthorizeForceRecoveryInput"
},
cancelRequest:{
data:"CancelRequestInput"
},
@@ -953,6 +969,9 @@ export const AllTypesProps: Record<string,any> = {
},
confirmAgreement:{
data:"ConfirmAgreementInput"
},
confirmCriticalAction:{
},
confirmReceiveOnRequest:{
data:"ConfirmReceiveOnRequestInput"
@@ -1017,6 +1036,9 @@ export const AllTypesProps: Record<string,any> = {
deliverOnRequest:{
data:"DeliverOnRequestInput"
},
disableTwoFactor:{
data:"TwoFactorCodeInput"
},
disputeOnRequest:{
data:"DisputeOnRequestInput"
},
@@ -1127,6 +1149,9 @@ export const AllTypesProps: Record<string,any> = {
initSystem:{
data:"Init"
},
initiateCriticalAction:{
data:"InitiateCriticalActionInput"
},
installExtension:{
data:"ExtensionInput"
},
@@ -1178,6 +1203,12 @@ export const AllTypesProps: Record<string,any> = {
registerParticipant:{
data:"RegisterParticipantInput"
},
reportNotMe:{
data:"ReportNotMeInput"
},
requestForceRecoveryConsent:{
data:"RequestForceRecoveryConsentInput"
},
resendNotification:{
},
@@ -1190,6 +1221,12 @@ export const AllTypesProps: Record<string,any> = {
revokeCapabilitySet:{
data:"RevokeCapabilitySetInput"
},
revokeParticipantKey:{
data:"RevokeParticipantKeyInput"
},
revokeSession:{
data:"RevokeSessionInput"
},
saveReportDraft:{
input:"SaveReportDraftInput"
},
@@ -1202,6 +1239,9 @@ export const AllTypesProps: Record<string,any> = {
setPaymentStatus:{
data:"SetPaymentStatusInput"
},
setRecoveryStrategy:{
data:"SetRecoveryStrategyInput"
},
setWif:{
data:"SetWifInput"
},
@@ -1554,6 +1594,9 @@ export const AllTypesProps: Record<string,any> = {
},
getCapitalProjectLogs:{
data:"GetCapitalLogsInput"
},
getCriticalActionAuditTrail:{
},
getCurrentTableStates:{
filters:"CurrentTableStatesFiltersInput",
@@ -1686,6 +1729,7 @@ export const AllTypesProps: Record<string,any> = {
ReceiveOnRequestInput:{
document:"ReturnByAssetActSignedDocumentInput"
},
RecoveryStrategy: "enum" as const,
RefreshInput:{
},
@@ -1719,6 +1763,9 @@ export const AllTypesProps: Record<string,any> = {
},
ReportHistoryFilterInput:{
reportType:"ReportType"
},
ReportNotMeInput:{
},
ReportPreviewInput:{
reportType:"ReportType"
@@ -1727,6 +1774,9 @@ export const AllTypesProps: Record<string,any> = {
ReportType: "enum" as const,
RepresentedByInput:{
},
RequestForceRecoveryConsentInput:{
},
RequisiteSource: "enum" as const,
ResetKeyInput:{
@@ -1788,6 +1838,12 @@ export const AllTypesProps: Record<string,any> = {
},
RevokeCapabilitySetInput:{
},
RevokeParticipantKeyInput:{
},
RevokeSessionInput:{
},
RoomMessageKind: "enum" as const,
SaveReportDraftInput:{
@@ -1833,6 +1889,9 @@ export const AllTypesProps: Record<string,any> = {
},
SetPlanInput:{
},
SetRecoveryStrategyInput:{
strategy:"RecoveryStrategy"
},
SetVarsInput:{
coopenomics_agreement:"AgreementVarInput",
@@ -1897,6 +1956,9 @@ export const AllTypesProps: Record<string,any> = {
TriggerNotificationWorkflowInput:{
payload:"JSONObject",
to:"NotificationWorkflowRecipientInput"
},
TwoFactorCodeInput:{
},
UninstallExtensionInput:{
@@ -2019,6 +2081,14 @@ export const ReturnTypes: Record<string,any> = {
max:"String",
used:"String"
},
AccountSession:{
created_at:"String",
current:"Boolean",
device:"String",
id:"String",
ip:"String",
last_seen_at:"String"
},
AccountsPaginationResult:{
currentPage:"Int",
items:"Account",
@@ -3112,6 +3182,22 @@ export const ReturnTypes: Record<string,any> = {
question:"String",
title:"String"
},
CriticalActionAuditEntry:{
action_type:"CriticalActionType",
confirmer_ids:"CriticalActionConfirmation",
created_at:"String",
finalized_at:"String",
id:"String",
initiated_at:"String",
initiator_id:"String",
payload_hash:"String",
status:"CriticalActionStatus",
target_id:"String"
},
CriticalActionConfirmation:{
at:"String",
by:"String"
},
CurrentInstanceDTO:{
blockchain_status:"String",
description:"String",
@@ -3286,6 +3372,11 @@ export const ReturnTypes: Record<string,any> = {
message:"String",
path:"String"
},
ForceRecoveryAuthorization:{
authorized:"Boolean",
consent_via:"ForceRecoveryConsentVia",
triggered_by:"String"
},
GatewayPayment:{
blockchain_data:"JSON",
can_change_status:"Boolean",
@@ -3602,11 +3693,13 @@ export const ReturnTypes: Record<string,any> = {
},
Mutation:{
acceptChildOrder:"Transaction",
activateTwoFactor:"Boolean",
addParticipant:"Account",
addPaymentMethod:"PaymentMethod",
addTrustedAccount:"Branch",
assignCapabilitySet:"Boolean",
authorizeDecision:"Transaction",
authorizeForceRecovery:"ForceRecoveryAuthorization",
cancelRequest:"Transaction",
capitalAddAuthor:"CapitalProject",
capitalApproveCommit:"CapitalCommit",
@@ -3698,6 +3791,7 @@ export const ReturnTypes: Record<string,any> = {
completeExtensionOnboardingStep:"ExtensionOnboardingState",
completeRequest:"Transaction",
confirmAgreement:"Transaction",
confirmCriticalAction:"PendingCriticalAction",
confirmReceiveOnRequest:"Transaction",
confirmSupplyOnRequest:"Transaction",
createAnnualGeneralMeet:"MeetAggregate",
@@ -3719,8 +3813,10 @@ export const ReturnTypes: Record<string,any> = {
deleteReportDraft:"Boolean",
deleteTrustedAccount:"Branch",
deliverOnRequest:"Transaction",
disableTwoFactor:"Boolean",
disputeOnRequest:"Transaction",
editBranch:"Branch",
enrollTwoFactor:"TwoFactorEnrollment",
generateAnnualGeneralMeetAgendaDocument:"GeneratedDocument",
generateAnnualGeneralMeetDecisionDocument:"GeneratedDocument",
generateAnnualGeneralMeetNotificationDocument:"GeneratedDocument",
@@ -3748,6 +3844,7 @@ export const ReturnTypes: Record<string,any> = {
generateUserAgreement:"GeneratedDocument",
generateWalletAgreement:"GeneratedDocument",
initSystem:"SystemInfo",
initiateCriticalAction:"PendingCriticalAction",
installExtension:"Extension",
installSystem:"SystemInfo",
login:"RegisteredAccount",
@@ -3765,15 +3862,21 @@ export const ReturnTypes: Record<string,any> = {
refresh:"RegisteredAccount",
registerAccount:"RegisteredAccount",
registerParticipant:"Account",
reportNotMe:"RevokedSessionsResult",
requestForceRecoveryConsent:"Boolean",
resendNotification:"Notification",
resetKey:"Boolean",
resetRegistration:"Account",
restartAnnualGeneralMeet:"MeetAggregate",
revokeAllSessions:"RevokedSessionsResult",
revokeCapabilitySet:"Boolean",
revokeParticipantKey:"RevokeKeyResult",
revokeSession:"Boolean",
saveReportDraft:"ReportDraft",
selectBranch:"Boolean",
sendAgreement:"Transaction",
setPaymentStatus:"GatewayPayment",
setRecoveryStrategy:"Boolean",
setWif:"Boolean",
signByPresiderOnAnnualGeneralMeet:"MeetAggregate",
signBySecretaryOnAnnualGeneralMeet:"MeetAggregate",
@@ -4085,6 +4188,18 @@ export const ReturnTypes: Record<string,any> = {
totalCount:"Int",
totalPages:"Int"
},
PendingCriticalAction:{
action_type:"CriticalActionType",
actor_id:"String",
confirmations:"CriticalActionConfirmation",
created_at:"String",
expires_at:"String",
finalized_at:"String",
id:"String",
payload:"JSON",
status:"CriticalActionStatus",
target_id:"String"
},
Permission:{
parent:"String",
perm_name:"String",
@@ -4331,6 +4446,7 @@ export const ReturnTypes: Record<string,any> = {
getCapitalOnboardingState:"CapitalOnboardingState",
getCapitalProjectLogs:"PaginatedCapitalLogsPaginationResult",
getChairmanOnboardingState:"ChairmanOnboardingState",
getCriticalActionAuditTrail:"CriticalActionAuditEntry",
getCurrentInstance:"CurrentInstanceDTO",
getCurrentTableStates:"PaginatedCurrentTableStatesPaginationResult",
getDeltas:"PaginatedDeltasPaginationResult",
@@ -4360,6 +4476,7 @@ export const ReturnTypes: Record<string,any> = {
getProgramWallets:"ProgramWalletsPaginationResult",
getProviderSubscriptionById:"ProviderSubscription",
getProviderSubscriptions:"ProviderSubscription",
getRecoveryStrategy:"RecoveryStrategy",
getRegistrationAgreements:"RegistrationAgreement",
getRegistrationConfig:"RegistrationConfig",
getReport:"GeneratedReport",
@@ -4368,6 +4485,7 @@ export const ReturnTypes: Record<string,any> = {
getReportHistory:"ReportHistoryPage",
getReportPreview:"ReportPreview",
getReportRequisites:"ReportRequisitesView",
getSessions:"AccountSession",
getSystemInfo:"SystemInfo",
getUnreadNotificationsCount:"UnreadNotificationsCount",
getUserWebPushSubscriptions:"WebPushSubscriptionDto",
@@ -4539,6 +4657,15 @@ export const ReturnTypes: Record<string,any> = {
owner:"String",
ram_bytes:"Int"
},
RevokeKeyResult:{
must_recover:"Boolean",
sessions_revoked:"Int",
status:"String",
target_id:"String"
},
RevokedSessionsResult:{
revoked:"Int"
},
SbpAccount:{
phone:"String"
},
@@ -4656,6 +4783,10 @@ export const ReturnTypes: Record<string,any> = {
startOffset:"Float",
text:"String"
},
TwoFactorEnrollment:{
otpauth_uri:"String",
secret:"String"
},
UnreadNotificationsCount:{
count:"Int"
},
File diff suppressed because it is too large Load Diff