diff --git a/.gitignore b/.gitignore index b6391b3..f020ac3 100644 --- a/.gitignore +++ b/.gitignore @@ -12,8 +12,15 @@ yarn-debug.log* yarn-error.log* pnpm-debug.log* logs/ +coverage/ gateway/gateway.exe dump.rdb +*.tsbuildinfo +outputs/ +tmp_*.py +tmp_*.ps1 +tmp_*.sh + .DS_Store Thumbs.db diff --git a/api/jest.config.cjs b/api/jest.config.cjs index fac75ac..4ff7a67 100644 --- a/api/jest.config.cjs +++ b/api/jest.config.cjs @@ -9,4 +9,12 @@ module.exports = { testMatch: ['**/*.spec.ts'], moduleFileExtensions: ['ts', 'js', 'json'], clearMocks: true, + coverageThreshold: { + global: { + statements: 59, + branches: 50, + functions: 60, + lines: 62, + }, + }, }; diff --git a/api/package.json b/api/package.json index 0221776..5672326 100644 --- a/api/package.json +++ b/api/package.json @@ -6,6 +6,7 @@ "scripts": { "build": "tsc -p tsconfig.build.json", "test": "jest --runInBand", + "test:coverage": "jest --runInBand --coverage", "test:watch": "jest --watch", "start": "node dist/main.js", "start:dev": "ts-node src/main.ts", diff --git a/api/prisma.config.ts b/api/prisma.config.ts index 3bc5f8f..43489f7 100644 --- a/api/prisma.config.ts +++ b/api/prisma.config.ts @@ -1,13 +1,17 @@ import { defineConfig } from 'prisma/config'; +const databaseUrl = process.env.DATABASE_URL?.trim(); +const allowDevelopmentDefault = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test'; +if (!databaseUrl && !allowDevelopmentDefault) { + throw new Error('DATABASE_URL is required outside development and test environments'); +} + export default defineConfig({ schema: 'prisma/schema.prisma', migrations: { path: 'prisma/migrations', }, datasource: { - url: - process.env.DATABASE_URL ?? - 'postgresql://cmpp:cmpp_password@localhost:5432/cmpp_platform?schema=public', + url: databaseUrl ?? 'postgresql://cmpp:cmpp_password@localhost:5432/cmpp_platform?schema=public', }, }); diff --git a/api/src/auth/auth.controller.ts b/api/src/auth/auth.controller.ts index b95d66d..44e246e 100644 --- a/api/src/auth/auth.controller.ts +++ b/api/src/auth/auth.controller.ts @@ -1,7 +1,9 @@ -import { Body, Controller, Get, Post, Req, Res, UnauthorizedException } from '@nestjs/common'; +import { Body, Controller, Get, Post, Req, Res, UnauthorizedException, UsePipes } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { CurrentSessionUserId } from './current-session-user.decorator'; -import { AuthService, LoginDto } from './auth.service'; +import { AuthService } from './auth.service'; +import { ChangeOwnPasswordDto, LoginDto, PasswordVerificationDto } from './auth.dto'; +import { strictValidationPipe } from '../common/strict-validation.pipe'; import { DEVELOPMENT_SESSION_COOKIE_NAME, SESSION_COOKIE_NAME, SessionPortal, SessionService } from './session.service'; import type { SessionRequest } from './session-validation.middleware'; import { UsersService } from '../users/users.service'; @@ -26,6 +28,7 @@ export class AuthController { } @Post('admin/auth/login') + @UsePipes(strictValidationPipe) async adminLogin(@Body() body: LoginDto, @Req() request: SessionRequest, @Res({ passthrough: true }) response: CookieResponse) { let result: Awaited>; try { @@ -43,6 +46,7 @@ export class AuthController { } @Post('client/auth/login') + @UsePipes(strictValidationPipe) async clientLogin(@Body() body: LoginDto, @Req() request: SessionRequest, @Res({ passthrough: true }) response: CookieResponse) { let result: Awaited>; try { @@ -95,7 +99,9 @@ export class AuthController { } @Post(['admin/auth/session/unlock', 'client/auth/session/unlock']) - async unlock(@Req() request: SessionRequest, @Body('password') password: string, @Res({ passthrough: true }) response: CookieResponse) { + @UsePipes(strictValidationPipe) + async unlock(@Req() request: SessionRequest, @Body() body: PasswordVerificationDto, @Res({ passthrough: true }) response: CookieResponse) { + const { password } = body; this.assertSession(request); const result = await this.auth.unlock(request.sessionToken!, request.sessionUserId!, password); if (result.status !== 'active' || !('token' in result)) throw new UnauthorizedException({ code: 'SESSION_LOCK_TIMEOUT', message: '锁定时间过长,请重新登录' }); @@ -105,7 +111,9 @@ export class AuthController { } @Post(['admin/auth/reauthenticate', 'client/auth/reauthenticate']) - async reauthenticate(@Req() request: SessionRequest, @Body('password') password: string) { + @UsePipes(strictValidationPipe) + async reauthenticate(@Req() request: SessionRequest, @Body() body: PasswordVerificationDto) { + const { password } = body; this.assertSession(request); const result = await this.auth.reauthenticate(request.sessionToken!, request.sessionUserId!, password); if (result.status !== 'active') throw new UnauthorizedException({ code: 'SESSION_LOCKED', message: '会话已锁定' }); @@ -122,9 +130,10 @@ export class AuthController { } @Post(['admin/auth/password', 'client/auth/password']) - changeOwnPassword(@CurrentSessionUserId() userId: string | undefined, @Body() body: { currentPassword?: string; password?: string }) { + @UsePipes(strictValidationPipe) + changeOwnPassword(@CurrentSessionUserId() userId: string | undefined, @Body() body: ChangeOwnPasswordDto) { if (!userId) throw new UnauthorizedException('登录会话无效,请重新登录'); - return this.users.changeOwnPassword(userId, body.currentPassword ?? '', body.password ?? ''); + return this.users.changeOwnPassword(userId, body.currentPassword, body.password); } private async finishLogin(result: Awaited>, request: SessionRequest, response: CookieResponse) { diff --git a/api/src/auth/auth.dto.ts b/api/src/auth/auth.dto.ts new file mode 100644 index 0000000..4b9d821 --- /dev/null +++ b/api/src/auth/auth.dto.ts @@ -0,0 +1,17 @@ +import { IsString, MaxLength, MinLength } from 'class-validator'; + +export class LoginDto { + @IsString() @MinLength(1) @MaxLength(200) login!: string; + @IsString() @MinLength(1) @MaxLength(256) password!: string; + @IsString() @MinLength(1) @MaxLength(64) captchaId!: string; + @IsString() @MinLength(1) @MaxLength(32) captchaText!: string; +} + +export class PasswordVerificationDto { + @IsString() @MinLength(1) @MaxLength(256) password!: string; +} + +export class ChangeOwnPasswordDto { + @IsString() @MinLength(1) @MaxLength(256) currentPassword!: string; + @IsString() @MinLength(8) @MaxLength(256) password!: string; +} diff --git a/api/src/auth/auth.service.spec.ts b/api/src/auth/auth.service.spec.ts index 3527a47..8074e03 100644 --- a/api/src/auth/auth.service.spec.ts +++ b/api/src/auth/auth.service.spec.ts @@ -1,6 +1,6 @@ import { UnauthorizedException } from '@nestjs/common'; import { AuthService } from './auth.service'; -import { hashPassword } from '../users/users.service'; +import { legacyHashPassword } from './password-hasher'; function createUsersMock(roleCode: string, overrides: Record = {}) { const user = { @@ -10,7 +10,7 @@ function createUsersMock(roleCode: string, overrides: Record = email: 'user@example.com', phone: '13800000000', displayName: '用户', - passwordHash: hashPassword('secret1'), + passwordHash: legacyHashPassword('secret1'), status: 'active', deletedAt: null, lockedUntil: null, @@ -20,24 +20,32 @@ function createUsersMock(roleCode: string, overrides: Record = }; return { findByLogin: jest.fn().mockResolvedValue(user), + verifyLoginPassword: jest.fn(async (_id: string, password: string) => password === 'secret1'), recordLoginSuccess: jest.fn(), recordLoginFailure: jest.fn(), }; } function createSessionsMock() { + const captchas = new Map(); + const failures = new Map(); const record = { userId: 'user-1', portal: 'admin', sessionVersion: 0, createdAt: 1, lastActivityAt: 1, lastAuthenticatedAt: 1, absoluteExpiresAt: Date.now() + 1000, }; return { + storeCaptcha: jest.fn(async (id: string, answer: string) => { captchas.set(id, answer); }), + consumeCaptcha: jest.fn(async (id: string) => { const answer = captchas.get(id) ?? null; captchas.delete(id); return answer; }), + isAnonymousLoginLocked: jest.fn(async (login: string) => (failures.get(login) ?? 0) >= 5), + recordAnonymousLoginFailure: jest.fn(async (login: string) => { const count = (failures.get(login) ?? 0) + 1; failures.set(login, count); return count; }), + clearAnonymousLoginFailures: jest.fn(async (login: string) => { failures.delete(login); }), create: jest.fn().mockResolvedValue({ token: 'opaque-session-token', record }), publicSession: jest.fn().mockReturnValue({ idleTimeoutSeconds: 3600, absoluteExpiresAt: new Date(record.absoluteExpiresAt).toISOString() }), }; } async function loginWithCaptcha(service: AuthService, portal: 'admin' | 'client', password = 'secret1') { - const captcha = service.createCaptcha(); + const captcha = await service.createCaptcha(); const answer = captcha.challenge.split('=')[0].split('+').map((part) => Number(part.trim())).reduce((sum, value) => sum + value, 0); return service.login({ login: 'user@example.com', diff --git a/api/src/auth/auth.service.ts b/api/src/auth/auth.service.ts index 8126a37..e5cb802 100644 --- a/api/src/auth/auth.service.ts +++ b/api/src/auth/auth.service.ts @@ -1,37 +1,20 @@ import { randomUUID } from 'node:crypto'; import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common'; -import { hashPassword, UsersService } from '../users/users.service'; +import { UsersService } from '../users/users.service'; +import type { LoginDto } from './auth.dto'; import { SessionService } from './session.service'; -export interface LoginDto { - login: string; - password: string; - captchaId: string; - captchaText: string; -} - type LoginPortal = 'admin' | 'client'; -type CaptchaRecord = { - answer: string; - expiresAt: number; -}; - -const captchaStore = new Map(); -const anonymousFailures = new Map(); - @Injectable() export class AuthService { constructor(private readonly users: UsersService, private readonly sessions: SessionService) {} - createCaptcha() { + async createCaptcha() { const left = Math.floor(10 + Math.random() * 40); const right = Math.floor(1 + Math.random() * 9); const captchaId = randomUUID(); - captchaStore.set(captchaId, { - answer: String(left + right), - expiresAt: Date.now() + 5 * 60 * 1000, - }); + await this.sessions.storeCaptcha(captchaId, String(left + right), 5 * 60); return { captchaId, challenge: `${left} + ${right} = ?`, @@ -44,12 +27,12 @@ export class AuthService { if (!login || !data.password) { throw new BadRequestException('login and password are required'); } - this.verifyCaptcha(data.captchaId, data.captchaText); - this.assertAnonymousNotLocked(login); + await this.verifyCaptcha(data.captchaId, data.captchaText); + await this.assertAnonymousNotLocked(login); const user = await this.users.findByLogin(login); if (!user) { - this.recordAnonymousFailure(login); + await this.sessions.recordAnonymousLoginFailure(login); throw new UnauthorizedException('Invalid login or password'); } if (user.lockedUntil && user.lockedUntil.getTime() > Date.now()) { @@ -59,7 +42,7 @@ export class AuthService { await this.users.recordLoginFailure(user.id); throw new UnauthorizedException('User is disabled or deleted'); } - if (user.passwordHash !== hashPassword(data.password)) { + if (!await this.users.verifyLoginPassword(user.id, data.password, user.passwordHash)) { await this.users.recordLoginFailure(user.id); throw new UnauthorizedException('Invalid login or password'); } @@ -75,7 +58,7 @@ export class AuthService { } await this.users.recordLoginSuccess(user.id); - anonymousFailures.delete(login); + await this.sessions.clearAnonymousLoginFailures(login); const { token, record } = await this.sessions.create(user.id, portal, user.sessionVersion ?? 0); return { @@ -105,30 +88,19 @@ export class AuthService { return this.sessions.markReauthenticated(token); } - private verifyCaptcha(captchaId?: string, captchaText?: string) { - const record = captchaId ? captchaStore.get(captchaId) : undefined; - captchaStore.delete(captchaId ?? ''); - if (!record || record.expiresAt < Date.now()) { + private async verifyCaptcha(captchaId?: string, captchaText?: string) { + const answer = captchaId ? await this.sessions.consumeCaptcha(captchaId) : null; + if (!answer) { throw new BadRequestException('Captcha expired, refresh and try again'); } - if (record.answer !== captchaText?.trim()) { + if (answer !== captchaText?.trim()) { throw new BadRequestException('Captcha is incorrect'); } } - private assertAnonymousNotLocked(login: string) { - const current = anonymousFailures.get(login); - if (current?.lockedUntil && current.lockedUntil > Date.now()) { + private async assertAnonymousNotLocked(login: string) { + if (await this.sessions.isAnonymousLoginLocked(login)) { throw new UnauthorizedException('User is locked for 24 hours after repeated failures'); } } - - private recordAnonymousFailure(login: string) { - const current = anonymousFailures.get(login) ?? { count: 0 }; - const count = current.count + 1; - anonymousFailures.set(login, { - count, - lockedUntil: count >= 5 ? Date.now() + 24 * 60 * 60 * 1000 : current.lockedUntil, - }); - } } diff --git a/api/src/auth/current-tenant-id.decorator.ts b/api/src/auth/current-tenant-id.decorator.ts new file mode 100644 index 0000000..0f11283 --- /dev/null +++ b/api/src/auth/current-tenant-id.decorator.ts @@ -0,0 +1,14 @@ +import { ForbiddenException, createParamDecorator, ExecutionContext } from '@nestjs/common'; +import type { SessionRequest } from './session-validation.middleware'; + +/** + * Returns the tenant bound to the authenticated client session. + * Request headers, query parameters and request bodies must never determine this value. + */ +export const CurrentTenantId = createParamDecorator((_: unknown, context: ExecutionContext) => { + const request = context.switchToHttp().getRequest(); + if (request.authSession?.portal !== 'client' || !request.sessionTenantId) { + throw new ForbiddenException({ code: 'CLIENT_TENANT_REQUIRED', message: '缺少可信企业上下文' }); + } + return request.sessionTenantId; +}); diff --git a/api/src/auth/password-hasher.spec.ts b/api/src/auth/password-hasher.spec.ts new file mode 100644 index 0000000..6902300 --- /dev/null +++ b/api/src/auth/password-hasher.spec.ts @@ -0,0 +1,27 @@ +import { hashPassword, isLegacySha256, legacyHashPassword, passwordNeedsRehash, verifyPassword } from './password-hasher'; + +describe('password hasher', () => { + it('stores new passwords using versioned salted scrypt hashes', async () => { + const first = await hashPassword('correct horse battery staple'); + const second = await hashPassword('correct horse battery staple'); + + expect(first).toMatch(/^\$scrypt\$v=1\$/); + expect(second).not.toBe(first); + await expect(verifyPassword('correct horse battery staple', first)).resolves.toBe(true); + await expect(verifyPassword('wrong password', first)).resolves.toBe(false); + expect(passwordNeedsRehash(first)).toBe(false); + }); + + it('recognizes and verifies legacy SHA-256 hashes for transparent migration', async () => { + const legacy = legacyHashPassword('legacy-password'); + + expect(isLegacySha256(legacy)).toBe(true); + expect(passwordNeedsRehash(legacy)).toBe(true); + await expect(verifyPassword('legacy-password', legacy)).resolves.toBe(true); + await expect(verifyPassword('wrong-password', legacy)).resolves.toBe(false); + }); + + it('rejects malformed or excessive scrypt parameters', async () => { + await expect(verifyPassword('password', '$scrypt$v=1$N=1048576,r=8,p=1$YWJjZGVmZ2hpamtsbW5vcA$YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU')).resolves.toBe(false); + }); +}); diff --git a/api/src/auth/password-hasher.ts b/api/src/auth/password-hasher.ts new file mode 100644 index 0000000..0c29c9d --- /dev/null +++ b/api/src/auth/password-hasher.ts @@ -0,0 +1,78 @@ +import { createHash, randomBytes, scrypt as scryptCallback, timingSafeEqual } from 'node:crypto'; +const VERSION = 1; +const KEY_LENGTH = 32; +const DEFAULT_N = 32768; +const DEFAULT_R = 8; +const DEFAULT_P = 1; +const MAX_MEMORY = 64 * 1024 * 1024; +const LEGACY_SHA256 = /^[a-f0-9]{64}$/i; + +export async function hashPassword(password: string) { + const salt = randomBytes(16); + const derived = await deriveScrypt(password, salt, KEY_LENGTH, { + N: DEFAULT_N, + r: DEFAULT_R, + p: DEFAULT_P, + maxmem: MAX_MEMORY, + }); + return `$scrypt$v=${VERSION}$N=${DEFAULT_N},r=${DEFAULT_R},p=${DEFAULT_P}$${salt.toString('base64url')}$${derived.toString('base64url')}`; +} + +export async function verifyPassword(password: string, encoded: string) { + if (isLegacySha256(encoded)) { + const candidate = Buffer.from(legacyHashPassword(password), 'hex'); + const expected = Buffer.from(encoded, 'hex'); + return candidate.length === expected.length && timingSafeEqual(candidate, expected); + } + + const parsed = parseScryptHash(encoded); + if (!parsed) return false; + const derived = await deriveScrypt(password, parsed.salt, parsed.hash.length, { + N: parsed.N, + r: parsed.r, + p: parsed.p, + maxmem: MAX_MEMORY, + }); + return derived.length === parsed.hash.length && timingSafeEqual(derived, parsed.hash); +} + +export function passwordNeedsRehash(encoded: string) { + if (isLegacySha256(encoded)) return true; + const parsed = parseScryptHash(encoded); + return !parsed || parsed.version !== VERSION || parsed.N !== DEFAULT_N || parsed.r !== DEFAULT_R || parsed.p !== DEFAULT_P; +} + +export function isLegacySha256(encoded: string) { + return LEGACY_SHA256.test(encoded); +} + +export function legacyHashPassword(password: string) { + return createHash('sha256').update(password).digest('hex'); +} + +function parseScryptHash(encoded: string) { + const match = /^\$scrypt\$v=(\d+)\$N=(\d+),r=(\d+),p=(\d+)\$([A-Za-z0-9_-]+)\$([A-Za-z0-9_-]+)$/.exec(encoded); + if (!match) return undefined; + const [, version, N, r, p, salt, hash] = match; + const params = { version: Number(version), N: Number(N), r: Number(r), p: Number(p) }; + if (!Number.isInteger(params.N) || params.N < 2 || params.N > DEFAULT_N + || !Number.isInteger(params.r) || params.r < 1 || params.r > DEFAULT_R + || !Number.isInteger(params.p) || params.p < 1 || params.p > DEFAULT_P) return undefined; + try { + const decodedSalt = Buffer.from(salt, 'base64url'); + const decodedHash = Buffer.from(hash, 'base64url'); + if (decodedSalt.length < 16 || decodedHash.length !== KEY_LENGTH) return undefined; + return { ...params, salt: decodedSalt, hash: decodedHash }; + } catch { + return undefined; + } +} + +function deriveScrypt(password: string, salt: Buffer, keyLength: number, options: { N: number; r: number; p: number; maxmem: number }) { + return new Promise((resolve, reject) => { + scryptCallback(password, salt, keyLength, options, (error, derivedKey) => { + if (error) reject(error); + else resolve(derivedKey); + }); + }); +} diff --git a/api/src/auth/session-validation.middleware.spec.ts b/api/src/auth/session-validation.middleware.spec.ts index 198d2bf..29f6b7a 100644 --- a/api/src/auth/session-validation.middleware.spec.ts +++ b/api/src/auth/session-validation.middleware.spec.ts @@ -1,4 +1,4 @@ -import { UnauthorizedException } from '@nestjs/common'; +import { ForbiddenException, UnauthorizedException } from '@nestjs/common'; import { SessionValidationMiddleware, type SessionRequest } from './session-validation.middleware'; const record = { @@ -6,10 +6,10 @@ const record = { lastActivityAt: 1, lastAuthenticatedAt: 1, absoluteExpiresAt: Date.now() + 1000, }; -function request(path = '/api/admin/users', cookie = 'cmpp_admin_session=opaque-token'): SessionRequest { +function request(path = '/api/admin/users', cookie = 'cmpp_admin_session=opaque-token', tenantId?: string): SessionRequest { return { originalUrl: path, - header: jest.fn((name: string) => name === 'cookie' ? cookie : undefined), + header: jest.fn((name: string) => name === 'cookie' ? cookie : name === 'x-tenant-id' ? tenantId : undefined), }; } @@ -17,7 +17,7 @@ describe('SessionValidationMiddleware', () => { const cookieName = jest.fn((portal: 'admin' | 'client') => `cmpp_${portal}_session`); it('accepts an active Redis session and exposes its user and record', async () => { - const prisma = { user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', status: 'active', deletedAt: null, sessionVersion: 3 }) } }; + const prisma = { user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', tenantId: null, status: 'active', deletedAt: null, sessionVersion: 3 }) } }; const sessions = { cookieName, validate: jest.fn().mockResolvedValue({ status: 'active', record }), remove: jest.fn() }; const middleware = new SessionValidationMiddleware(prisma as never, sessions as never); const currentRequest = request(); @@ -32,7 +32,7 @@ describe('SessionValidationMiddleware', () => { }); it('rejects a session after the user session version changes', async () => { - const prisma = { user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', status: 'active', deletedAt: null, sessionVersion: 4 }) } }; + const prisma = { user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', tenantId: null, status: 'active', deletedAt: null, sessionVersion: 4 }) } }; const sessions = { cookieName, validate: jest.fn().mockResolvedValue({ status: 'active', record }), remove: jest.fn() }; const middleware = new SessionValidationMiddleware(prisma as never, sessions as never); @@ -41,7 +41,7 @@ describe('SessionValidationMiddleware', () => { }); it('only lets a locked session reach unlock and logout endpoints', async () => { - const prisma = { user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', status: 'active', deletedAt: null, sessionVersion: 3 }) } }; + const prisma = { user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', tenantId: null, status: 'active', deletedAt: null, sessionVersion: 3 }) } }; const sessions = { cookieName, validate: jest.fn().mockResolvedValue({ status: 'locked', record }), remove: jest.fn() }; const middleware = new SessionValidationMiddleware(prisma as never, sessions as never); @@ -53,7 +53,7 @@ describe('SessionValidationMiddleware', () => { it('selects only the cookie belonging to the requested portal', async () => { const clientRecord = { ...record, portal: 'client' as const }; - const prisma = { user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', status: 'active', deletedAt: null, sessionVersion: 3 }) } }; + const prisma = { user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', tenantId: 'tenant-a', status: 'active', deletedAt: null, sessionVersion: 3 }) } }; const sessions = { cookieName, validate: jest.fn().mockResolvedValue({ status: 'active', record: clientRecord }), remove: jest.fn() }; const middleware = new SessionValidationMiddleware(prisma as never, sessions as never); const currentRequest = request('/api/client/users', 'cmpp_admin_session=admin-token; cmpp_client_session=client-token'); @@ -62,6 +62,33 @@ describe('SessionValidationMiddleware', () => { expect(sessions.validate).toHaveBeenCalledWith('client-token', false); expect(currentRequest.sessionToken).toBe('client-token'); + expect(currentRequest.sessionTenantId).toBe('tenant-a'); + }); + + it('rejects a client session whose user is not bound to a tenant', async () => { + const clientRecord = { ...record, portal: 'client' as const }; + const prisma = { user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', tenantId: null, status: 'active', deletedAt: null, sessionVersion: 3 }) } }; + const sessions = { cookieName, validate: jest.fn().mockResolvedValue({ status: 'active', record: clientRecord }), remove: jest.fn() }; + const middleware = new SessionValidationMiddleware(prisma as never, sessions as never); + + await expect(middleware.use(request('/api/client/users', 'cmpp_client_session=client-token'), {}, jest.fn())) + .rejects.toBeInstanceOf(ForbiddenException); + }); + + it('rejects and audits a client tenant header that disagrees with the authenticated user', async () => { + const clientRecord = { ...record, portal: 'client' as const }; + const prisma = { + user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', tenantId: 'tenant-a', status: 'active', deletedAt: null, sessionVersion: 3 }) }, + operationLog: { create: jest.fn().mockResolvedValue({ id: 'log-1' }) }, + }; + const sessions = { cookieName, validate: jest.fn().mockResolvedValue({ status: 'active', record: clientRecord }), remove: jest.fn() }; + const middleware = new SessionValidationMiddleware(prisma as never, sessions as never); + + await expect(middleware.use(request('/api/client/users', 'cmpp_client_session=client-token', 'tenant-b'), {}, jest.fn())) + .rejects.toMatchObject({ response: expect.objectContaining({ code: 'CLIENT_TENANT_MISMATCH' }) }); + expect(prisma.operationLog.create).toHaveBeenCalledWith({ + data: expect.objectContaining({ tenantId: 'tenant-a', userId: 'user-1', action: 'security.client_tenant_mismatch' }), + }); }); it('does not accept an admin cookie for a client route', async () => { diff --git a/api/src/auth/session-validation.middleware.ts b/api/src/auth/session-validation.middleware.ts index 3c12a80..47bef59 100644 --- a/api/src/auth/session-validation.middleware.ts +++ b/api/src/auth/session-validation.middleware.ts @@ -1,4 +1,4 @@ -import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common'; +import { ForbiddenException, Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { AuthSessionRecord, SessionPortal, SessionService } from './session.service'; @@ -8,6 +8,7 @@ export type SessionRequest = { url?: string; sessionUserId?: string; sessionToken?: string; + sessionTenantId?: string; authSession?: AuthSessionRecord; }; @@ -38,7 +39,7 @@ export class SessionValidationMiddleware implements NestMiddleware { const user = await this.prisma.user.findUnique({ where: { id: result.record.userId }, - select: { id: true, status: true, deletedAt: true, sessionVersion: true }, + select: { id: true, tenantId: true, status: true, deletedAt: true, sessionVersion: true }, }); if (!user || user.status !== 'active' || user.deletedAt || user.sessionVersion !== result.record.sessionVersion) { await this.sessions.remove(token); @@ -51,6 +52,25 @@ export class SessionValidationMiddleware implements NestMiddleware { request.sessionUserId = user.id; request.sessionToken = token; request.authSession = result.record; + if (portal === 'client') { + if (!user.tenantId) { + throw new ForbiddenException({ code: 'CLIENT_TENANT_REQUIRED', message: '当前客户端账号未关联企业' }); + } + const suppliedTenantId = request.header('x-tenant-id')?.trim(); + if (suppliedTenantId && suppliedTenantId !== user.tenantId) { + await this.prisma.operationLog.create({ + data: { + tenantId: user.tenantId, + userId: user.id, + action: 'security.client_tenant_mismatch', + resource: 'auth_session', + detail: { suppliedTenantId }, + }, + }); + throw new ForbiddenException({ code: 'CLIENT_TENANT_MISMATCH', message: '请求企业与登录企业不一致' }); + } + request.sessionTenantId = user.tenantId; + } const isSessionRecoveryRoute = /\/auth\/(?:session(?:\/unlock)?|logout)(?:\?|$)/.test(path); if (result.status === 'locked' && !isSessionRecoveryRoute) { if (result.newlyLocked) { diff --git a/api/src/auth/session.service.ts b/api/src/auth/session.service.ts index 284b70d..8483bc2 100644 --- a/api/src/auth/session.service.ts +++ b/api/src/auth/session.service.ts @@ -21,6 +21,9 @@ export type SessionValidationResult = | { status: 'expired'; code: 'SESSION_INVALID' | 'SESSION_ABSOLUTE_TIMEOUT' | 'SESSION_LOCK_TIMEOUT' }; const SESSION_PREFIX = 'cmpp:auth:session:'; +const CAPTCHA_PREFIX = 'cmpp:auth:captcha:'; +const ANONYMOUS_FAILURE_PREFIX = 'cmpp:auth:failure:'; +const ANONYMOUS_LOCK_PREFIX = 'cmpp:auth:lock:'; export const SESSION_COOKIE_NAME = '__Host-cmpp_session'; export const DEVELOPMENT_SESSION_COOKIE_NAME = 'cmpp_session'; export const ADMIN_SESSION_COOKIE_NAME = '__Host-cmpp_admin_session'; @@ -123,6 +126,61 @@ export class SessionService implements OnModuleDestroy { return this.client.del(this.key(token)); } + async storeCaptcha(captchaId: string, answer: string, ttlSeconds: number) { + try { + await this.client.set(`${CAPTCHA_PREFIX}${captchaId}`, answer, 'EX', ttlSeconds); + } catch { + throw new ServiceUnavailableException('验证码服务暂不可用'); + } + } + + async consumeCaptcha(captchaId: string) { + try { + return await this.client.getdel(`${CAPTCHA_PREFIX}${captchaId}`); + } catch { + throw new ServiceUnavailableException('验证码服务暂不可用'); + } + } + + async isAnonymousLoginLocked(login: string) { + try { + return Boolean(await this.client.exists(`${ANONYMOUS_LOCK_PREFIX}${this.loginDigest(login)}`)); + } catch { + throw new ServiceUnavailableException('登录保护服务暂不可用'); + } + } + + async recordAnonymousLoginFailure(login: string) { + const digest = this.loginDigest(login); + const failureKey = `${ANONYMOUS_FAILURE_PREFIX}${digest}`; + const lockKey = `${ANONYMOUS_LOCK_PREFIX}${digest}`; + try { + const count = Number(await this.client.eval( + `local count = redis.call('INCR', KEYS[1]) + if count == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end + if count >= tonumber(ARGV[2]) then redis.call('SET', KEYS[2], '1', 'EX', ARGV[1]) end + return count`, + 2, + failureKey, + lockKey, + 24 * 60 * 60, + 5, + )); + return count; + } catch { + throw new ServiceUnavailableException('登录保护服务暂不可用'); + } + } + + async clearAnonymousLoginFailures(login: string) { + const digest = this.loginDigest(login); + try { + await this.client.del(`${ANONYMOUS_FAILURE_PREFIX}${digest}`, `${ANONYMOUS_LOCK_PREFIX}${digest}`); + } catch { + throw new ServiceUnavailableException('登录保护服务暂不可用'); + } + } + isRecentlyAuthenticated(record: AuthSessionRecord) { return Date.now() - record.lastAuthenticatedAt < this.recentAuthenticationMs; } @@ -195,6 +253,10 @@ export class SessionService implements OnModuleDestroy { return `${SESSION_PREFIX}${createHash('sha256').update(token).digest('hex')}`; } + private loginDigest(login: string) { + return createHash('sha256').update(login.trim().toLocaleLowerCase('en-US')).digest('hex'); + } + private get client() { if (!this.redis) { this.redis = new IORedis(process.env.REDIS_URL ?? 'redis://127.0.0.1:6379', { diff --git a/api/src/billing/billing.controller.ts b/api/src/billing/billing.controller.ts index fa47cbb..71c43a5 100644 --- a/api/src/billing/billing.controller.ts +++ b/api/src/billing/billing.controller.ts @@ -1,6 +1,9 @@ -import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, Query, UsePipes } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { TenantId } from '../common/tenant-id.decorator'; +import { CurrentTenantId } from '../auth/current-tenant-id.decorator'; +import { ClientBillingEstimateDto } from '../common/client-write.dto'; +import { strictValidationPipe } from '../common/strict-validation.pipe'; import { RequireRecentAuthentication } from '../auth/require-recent-authentication.decorator'; import { CurrentSessionUserId } from '../auth/current-session-user.decorator'; import { @@ -125,14 +128,15 @@ export class ClientBillingController { constructor(private readonly billing: BillingService) {} @Get('orders') - listRechargeOrders(@TenantId() tenantId?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) { + listRechargeOrders(@CurrentTenantId() tenantId: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) { return page || pageSize ? this.billing.listRechargeOrdersPage({ tenantId, page: Number(page), pageSize: Number(pageSize) }) : this.billing.listRechargeOrders(tenantId); } @Post('estimate') - estimateSmsCost(@Body() body: EstimateSmsCostDto) { - return this.billing.estimateSmsCost(body); + @UsePipes(strictValidationPipe) + estimateSmsCost(@CurrentTenantId() tenantId: string, @Body() body: ClientBillingEstimateDto) { + return this.billing.estimateSmsCost({ ...body, tenantId }); } } diff --git a/api/src/certification/certification.controller.ts b/api/src/certification/certification.controller.ts index f643f9e..cf447d6 100644 --- a/api/src/certification/certification.controller.ts +++ b/api/src/certification/certification.controller.ts @@ -1,8 +1,10 @@ -import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, Query, UsePipes } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { CurrentSessionUserId } from '../auth/current-session-user.decorator'; -import { TenantId } from '../common/tenant-id.decorator'; -import { CertificationService, ReviewCertificationDto, SubmitCertificationDto } from './certification.service'; +import { CurrentTenantId } from '../auth/current-tenant-id.decorator'; +import { ClientCertificationSubmissionDto } from '../common/client-write.dto'; +import { strictValidationPipe } from '../common/strict-validation.pipe'; +import { CertificationService, ReviewCertificationDto } from './certification.service'; @ApiTags('client-certification') @Controller('client/enterprise-certification') @@ -10,13 +12,14 @@ export class ClientCertificationController { constructor(private readonly certifications: CertificationService) {} @Get() - list(@TenantId() tenantId?: string) { + list(@CurrentTenantId() tenantId: string) { return this.certifications.list(tenantId); } @Post() - submit(@Body() body: SubmitCertificationDto) { - return this.certifications.submit(body); + @UsePipes(strictValidationPipe) + submit(@CurrentTenantId() tenantId: string, @Body() body: ClientCertificationSubmissionDto) { + return this.certifications.submit({ ...body, tenantId }); } } diff --git a/api/src/common/client-write.dto.spec.ts b/api/src/common/client-write.dto.spec.ts new file mode 100644 index 0000000..9ddcb1e --- /dev/null +++ b/api/src/common/client-write.dto.spec.ts @@ -0,0 +1,25 @@ +import { BadRequestException } from '@nestjs/common'; +import { ClientBatchTaskDto, ClientImportConfirmDto, ClientStatusChangeDto } from './client-write.dto'; +import { strictValidationPipe } from './strict-validation.pipe'; + +function validate(metatype: new () => T, value: unknown) { + return strictValidationPipe.transform(value, { type: 'body', metatype, data: undefined }); +} + +describe('strict client write DTOs', () => { + it('accepts an import confirmation without a client-supplied phones array', async () => { + await expect(validate(ClientImportConfirmDto, { + content: '【测试】验证码 ${code}', + importContent: 'phone,code\n13800000001,1234', + })).resolves.toEqual(expect.objectContaining({ importContent: expect.any(String) })); + }); + + it('rejects a direct batch task without validated phone numbers', async () => { + await expect(validate(ClientBatchTaskDto, { content: '【测试】通知' })).rejects.toBeInstanceOf(BadRequestException); + }); + + it('rejects a client-supplied operator identity', async () => { + await expect(validate(ClientStatusChangeDto, { status: 'disabled', operatorId: 'another-user' })) + .rejects.toBeInstanceOf(BadRequestException); + }); +}); diff --git a/api/src/common/client-write.dto.ts b/api/src/common/client-write.dto.ts new file mode 100644 index 0000000..8efd199 --- /dev/null +++ b/api/src/common/client-write.dto.ts @@ -0,0 +1,154 @@ +import { Type } from 'class-transformer'; +import { PartialType } from '@nestjs/swagger'; +import { + ArrayMaxSize, + IsArray, + IsBoolean, + IsIn, + IsInt, + IsObject, + IsOptional, + IsString, + IsUrl, + Matches, + Max, + MaxLength, + Min, + MinLength, + ValidateNested, +} from 'class-validator'; + +export class ClientCertificationSubmissionDto { + @IsOptional() @IsString() @MaxLength(64) tenantId?: string; + @IsString() @MinLength(1) @MaxLength(200) companyName!: string; + @IsOptional() @IsString() @MaxLength(100) licenseNo?: string; + @IsOptional() @IsString() @MaxLength(100) contactName?: string; + @IsOptional() @Matches(/^\+?[0-9-]{6,24}$/) contactPhone?: string; + @IsOptional() @IsObject() materials?: Record; +} + +export class ClientTaskBaseDto { + @IsOptional() @IsString() @MaxLength(64) tenantId?: string; + @IsOptional() @IsString() @MaxLength(64) applicationId?: string; + @IsOptional() @IsString() @MaxLength(64) templateId?: string; + @IsString() @MinLength(1) @MaxLength(5000) content!: string; + @IsOptional() @IsString() @MaxLength(64) category?: string; + @IsOptional() @IsIn(['immediate', 'scheduled']) sendMode?: 'immediate' | 'scheduled'; + @IsOptional() @IsString() @MaxLength(64) scheduledAt?: string; + @IsOptional() @IsObject() variables?: Record; + @IsOptional() @IsString() @MaxLength(64) requestedAt?: string; + @IsOptional() @IsString() @MaxLength(128) clientMessageId?: string; +} + +export class ClientBatchTaskDto extends ClientTaskBaseDto { + @IsArray() @ArrayMaxSize(100000) @Matches(/^1\d{10}$/, { each: true }) phones!: string[]; +} + +export class ClientImportPreviewDto { + @IsOptional() @IsString() @MaxLength(64) tenantId?: string; + @IsOptional() @IsString() @MaxLength(64) applicationId?: string; + @IsString() @MinLength(1) @MaxLength(5_000_000) content!: string; + @IsOptional() @IsString() @MaxLength(255) fileName?: string; + @IsOptional() @IsIn(['utf8', 'gbk']) encoding?: 'utf8' | 'gbk'; + @IsOptional() @IsIn([',', '\t']) delimiter?: ',' | '\t'; + @IsOptional() @IsArray() @ArrayMaxSize(100) @IsString({ each: true }) requiredVariables?: string[]; +} + +export class ClientImportConfirmDto extends ClientTaskBaseDto { + @IsString() @MinLength(1) @MaxLength(5_000_000) importContent!: string; + @IsOptional() @IsArray() @ArrayMaxSize(100) @IsString({ each: true }) requiredVariables?: string[]; +} + +export class ClientBillingEstimateDto { + @IsOptional() @IsString() @MaxLength(64) tenantId?: string; + @IsOptional() @IsString() @MaxLength(64) applicationId?: string; + @IsString() @MinLength(1) @MaxLength(5000) content!: string; + @Type(() => Number) @IsInt() @Min(1) @Max(100000) phoneCount!: number; + @IsOptional() @Type(() => Number) @Min(0) unitPrice?: number; + @IsOptional() @IsString() @MaxLength(64) taskId?: string; +} + +export class ClientSmsApplicationDto { + @IsOptional() @IsString() @MaxLength(64) tenantId?: string; + @IsString() @MinLength(1) @MaxLength(100) name!: string; + @IsOptional() @IsString() @MaxLength(500) scene?: string; + @IsOptional() @IsUrl({ require_tld: false }) @MaxLength(2048) callbackUrl?: string; + @IsOptional() @IsString() @MaxLength(64) cmppAccount?: string; + @IsOptional() @IsString() @MaxLength(64) cmppEnterpriseCode?: string; + @IsOptional() @IsString() @MaxLength(32) cmppApplicationExtension?: string; + @IsOptional() @IsBoolean() cmppAccessNumberFillEnabled?: boolean; + @IsOptional() @IsString() @MaxLength(32) cmppAccessNumberFillPrefix?: string; + @IsOptional() @IsString() @MaxLength(4096) passwordCipher?: string; + @IsOptional() @IsBoolean() interfaceEnabled?: boolean; + @IsOptional() @IsString() @MaxLength(32) interfaceType?: string; + @IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(100) cmppMaxConnections?: number; + @IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(1000) cmppWindowSize?: number; + @IsOptional() @Type(() => Number) @IsInt() @Min(0) dailyLimit?: number; + @IsOptional() @Type(() => Number) @Min(0) customerUnitPrice?: number; + @IsOptional() @IsString() @MaxLength(32) queuePriority?: string; + @IsOptional() @IsString() @MaxLength(32) templateMismatchMode?: string; + @IsOptional() @IsBoolean() downstreamReceiptRetryEnabled?: boolean; + @IsOptional() @IsBoolean() downstreamUplinkRetryEnabled?: boolean; + @IsOptional() @IsArray() @ArrayMaxSize(100) @IsString({ each: true }) ipAllowlist?: string[]; +} + +export class ClientSmsSignatureDto { + @IsOptional() @IsString() @MaxLength(64) tenantId?: string; + @IsOptional() @IsString() @MaxLength(64) applicationId?: string; + @IsString() @MinLength(1) @MaxLength(100) name!: string; + @IsOptional() @IsString() @MaxLength(500) purpose?: string; + @IsOptional() @IsObject() drainageInfo?: Record; +} + +export class ClientSmsSignatureUpdateDto extends PartialType(ClientSmsSignatureDto) { + @IsOptional() @IsString() @MaxLength(32) auditStatus?: string; +} + +export class ClientDrainageInfoDto { + @IsString() @MinLength(1) @MaxLength(200) siteName!: string; + @IsUrl({ require_tld: false }) @MaxLength(2048) url!: string; + @IsOptional() @IsString() @MaxLength(1000) remark?: string; + @IsOptional() @IsObject() reportValues?: Record; +} + +export class ClientDrainageInfoUpdateDto extends PartialType(ClientDrainageInfoDto) {} + +export class ClientSignatureMaterialDto { + @IsOptional() @IsString() @MaxLength(64) fileObjectId?: string; + @IsString() @MinLength(1) @MaxLength(64) materialType!: string; + @IsString() @MinLength(1) @MaxLength(200) title!: string; + @IsOptional() @IsString() @MaxLength(2000) description?: string; +} + +class TemplateVariableDto { + @IsString() @MinLength(1) @MaxLength(64) name!: string; + @IsOptional() @IsString() @MaxLength(500) example?: string; + @IsOptional() @IsBoolean() required?: boolean; +} + +export class ClientSmsTemplateDto { + @IsOptional() @IsString() @MaxLength(64) tenantId?: string; + @IsString() @MaxLength(64) applicationId!: string; + @IsOptional() @IsString() @MaxLength(64) signatureId?: string; + @IsString() @MinLength(1) @MaxLength(200) name!: string; + @IsString() @MinLength(1) @MaxLength(5000) content!: string; + @IsOptional() @IsString() @MaxLength(64) category?: string; + @IsOptional() @IsArray() @ArrayMaxSize(100) @ValidateNested({ each: true }) @Type(() => TemplateVariableDto) variables?: TemplateVariableDto[]; +} + +export class ClientSmsTemplateUpdateDto extends PartialType(ClientSmsTemplateDto) { + @IsOptional() @IsString() @MaxLength(32) auditStatus?: string; +} + +export class ClientStatusChangeDto { + @IsOptional() @IsString() @MaxLength(32) status?: string; + @IsOptional() @IsString() @MaxLength(1000) reason?: string; + @IsOptional() @IsBoolean() force?: boolean; + @IsOptional() @IsString() @MaxLength(200) confirmName?: string; + @IsOptional() @IsString() @MaxLength(200) confirmText?: string; + @IsOptional() @IsString() @MaxLength(64) expectedUpdatedAt?: string; + @IsOptional() @IsString() @MaxLength(128) idempotencyKey?: string; + @IsOptional() @IsBoolean() deleteAssociatedTemplates?: boolean; + @IsOptional() @IsBoolean() deleteAssociatedDrainage?: boolean; + @IsOptional() @IsBoolean() abandonAssociatedReportTasks?: boolean; +} diff --git a/api/src/common/strict-validation.pipe.ts b/api/src/common/strict-validation.pipe.ts new file mode 100644 index 0000000..9b20144 --- /dev/null +++ b/api/src/common/strict-validation.pipe.ts @@ -0,0 +1,9 @@ +import { ValidationPipe } from '@nestjs/common'; + +export const strictValidationPipe = new ValidationPipe({ + transform: true, + whitelist: true, + forbidNonWhitelisted: true, + stopAtFirstError: false, + transformOptions: { enableImplicitConversion: false }, +}); diff --git a/api/src/deletion-governance/deletion-governance.controller.ts b/api/src/deletion-governance/deletion-governance.controller.ts index 1cd4e50..b5b52c4 100644 --- a/api/src/deletion-governance/deletion-governance.controller.ts +++ b/api/src/deletion-governance/deletion-governance.controller.ts @@ -1,8 +1,8 @@ import { Body, Controller, Get, Param, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { CurrentSessionUserId } from '../auth/current-session-user.decorator'; +import { CurrentTenantId } from '../auth/current-tenant-id.decorator'; import { RequireRecentAuthentication } from '../auth/require-recent-authentication.decorator'; -import { TenantId } from '../common/tenant-id.decorator'; import { DeleteTargetDto, DeletionGovernanceService, DeletionTargetType } from './deletion-governance.service'; @ApiTags('deletion-governance') @@ -28,13 +28,13 @@ export class ClientDeletionGovernanceController { constructor(private readonly deletions: DeletionGovernanceService) {} @Get(':type/:id/preflight') - preflight(@Param('type') type: DeletionTargetType, @Param('id') id: string, @TenantId() tenantId?: string) { + preflight(@Param('type') type: DeletionTargetType, @Param('id') id: string, @CurrentTenantId() tenantId: string) { return this.deletions.preflight(type, id, tenantId); } @Post(':type/:id') @RequireRecentAuthentication() - delete(@Param('type') type: DeletionTargetType, @Param('id') id: string, @Body() body: DeleteTargetDto, @TenantId() tenantId?: string, @CurrentSessionUserId() operatorId?: string) { + delete(@Param('type') type: DeletionTargetType, @Param('id') id: string, @Body() body: DeleteTargetDto, @CurrentTenantId() tenantId: string, @CurrentSessionUserId() operatorId?: string) { return this.deletions.delete(type, id, { ...body, operatorId }, tenantId); } } diff --git a/api/src/open-api/client-open-api.controller.ts b/api/src/open-api/client-open-api.controller.ts index 015a2d1..0c0e9db 100644 --- a/api/src/open-api/client-open-api.controller.ts +++ b/api/src/open-api/client-open-api.controller.ts @@ -1,7 +1,8 @@ import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { RequireRecentAuthentication } from '../auth/require-recent-authentication.decorator'; -import { TenantId } from '../common/tenant-id.decorator'; +import { CurrentTenantId } from '../auth/current-tenant-id.decorator'; +import { CurrentSessionUserId } from '../auth/current-session-user.decorator'; import { OpenApiService } from './open-api.service'; @ApiTags('client-http-open-api-management') @@ -9,13 +10,13 @@ import { OpenApiService } from './open-api.service'; export class ClientOpenApiController { constructor(private readonly service: OpenApiService) {} - @Get() getConfig(@Param('applicationId') applicationId: string, @TenantId() tenantId?: string) { return this.service.getConfig(applicationId, tenantId); } - @Get('credentials') listCredentials(@Param('applicationId') applicationId: string, @TenantId() tenantId?: string) { return this.service.listCredentials(applicationId, tenantId); } - @Post('credentials') @RequireRecentAuthentication() createCredential(@Param('applicationId') applicationId: string, @Body() body: { name?: string; expiresAt?: string; createdById?: string }, @TenantId() tenantId?: string) { return this.service.createCredential(applicationId, body, tenantId, true); } - @Post('credentials/:credentialId/revoke') @RequireRecentAuthentication() revokeCredential(@Param('applicationId') applicationId: string, @Param('credentialId') credentialId: string, @TenantId() tenantId?: string) { return this.service.revokeCredential(applicationId, credentialId, tenantId); } - @Get('webhooks') getWebhooks(@Param('applicationId') applicationId: string, @TenantId() tenantId?: string) { return this.service.getWebhookEndpoints(applicationId, tenantId); } - @Put('webhooks/:eventType') @RequireRecentAuthentication() upsertWebhook(@Param('applicationId') applicationId: string, @Param('eventType') eventType: string, @Body() body: { url: string; rotateSecret?: boolean; status?: string }, @TenantId() tenantId?: string) { return this.service.upsertWebhookEndpoint(applicationId, eventType, body, tenantId); } - @Get('requests') listRequests(@Param('applicationId') applicationId: string, @TenantId() tenantId?: string) { return this.service.listRequestLogs(applicationId, tenantId); } - @Get('webhook-deliveries') listDeliveries(@Param('applicationId') applicationId: string, @TenantId() tenantId?: string) { return this.service.listWebhookDeliveries(applicationId, tenantId); } - @Post('webhook-deliveries/:deliveryId/retry') @RequireRecentAuthentication() retryDelivery(@Param('applicationId') applicationId: string, @Param('deliveryId') deliveryId: string, @TenantId() tenantId?: string) { return this.service.retryWebhookDelivery(applicationId, deliveryId, tenantId); } + @Get() getConfig(@Param('applicationId') applicationId: string, @CurrentTenantId() tenantId: string) { return this.service.getConfig(applicationId, tenantId); } + @Get('credentials') listCredentials(@Param('applicationId') applicationId: string, @CurrentTenantId() tenantId: string) { return this.service.listCredentials(applicationId, tenantId); } + @Post('credentials') @RequireRecentAuthentication() createCredential(@Param('applicationId') applicationId: string, @Body() body: { name?: string; expiresAt?: string; createdById?: string }, @CurrentTenantId() tenantId: string, @CurrentSessionUserId() createdById?: string) { return this.service.createCredential(applicationId, { ...body, createdById }, tenantId, true); } + @Post('credentials/:credentialId/revoke') @RequireRecentAuthentication() revokeCredential(@Param('applicationId') applicationId: string, @Param('credentialId') credentialId: string, @CurrentTenantId() tenantId: string) { return this.service.revokeCredential(applicationId, credentialId, tenantId); } + @Get('webhooks') getWebhooks(@Param('applicationId') applicationId: string, @CurrentTenantId() tenantId: string) { return this.service.getWebhookEndpoints(applicationId, tenantId); } + @Put('webhooks/:eventType') @RequireRecentAuthentication() upsertWebhook(@Param('applicationId') applicationId: string, @Param('eventType') eventType: string, @Body() body: { url: string; rotateSecret?: boolean; status?: string }, @CurrentTenantId() tenantId: string) { return this.service.upsertWebhookEndpoint(applicationId, eventType, body, tenantId); } + @Get('requests') listRequests(@Param('applicationId') applicationId: string, @CurrentTenantId() tenantId: string) { return this.service.listRequestLogs(applicationId, tenantId); } + @Get('webhook-deliveries') listDeliveries(@Param('applicationId') applicationId: string, @CurrentTenantId() tenantId: string) { return this.service.listWebhookDeliveries(applicationId, tenantId); } + @Post('webhook-deliveries/:deliveryId/retry') @RequireRecentAuthentication() retryDelivery(@Param('applicationId') applicationId: string, @Param('deliveryId') deliveryId: string, @CurrentTenantId() tenantId: string) { return this.service.retryWebhookDelivery(applicationId, deliveryId, tenantId); } } diff --git a/api/src/operations/client-operations.controller.ts b/api/src/operations/client-operations.controller.ts index 52503cd..6fd3cf8 100644 --- a/api/src/operations/client-operations.controller.ts +++ b/api/src/operations/client-operations.controller.ts @@ -1,7 +1,7 @@ import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { CurrentSessionUserId } from '../auth/current-session-user.decorator'; -import { TenantId } from '../common/tenant-id.decorator'; +import { CurrentTenantId } from '../auth/current-tenant-id.decorator'; import { OperationsService } from './operations.service'; @ApiTags('client-operations') @@ -10,18 +10,18 @@ export class ClientOperationsController { constructor(private readonly operations: OperationsService) {} @Get('batch-tasks') - listBatchTasks(@TenantId() tenantId?: string, @Query('status') status?: string) { + listBatchTasks(@CurrentTenantId() tenantId: string, @Query('status') status?: string) { return this.operations.listClientBatchTasks({ tenantId, status }); } @Get('batch-tasks/:id/messages') - listTaskMessages(@TenantId() tenantId: string | undefined, @Param('id') taskId: string, @Query('phoneNumber') phoneNumber?: string) { + listTaskMessages(@CurrentTenantId() tenantId: string, @Param('id') taskId: string, @Query('phoneNumber') phoneNumber?: string) { return this.operations.listClientMessages({ tenantId, taskId, phoneNumber }); } @Get('messages') listMessages( - @TenantId() tenantId?: string, + @CurrentTenantId() tenantId: string, @Query('applicationId') applicationId?: string, @Query('taskId') taskId?: string, @Query('messageId') messageId?: string, @@ -49,20 +49,20 @@ export class ClientOperationsController { } @Get('uplink-messages') - listUplinkMessages(@TenantId() tenantId?: string, @Query('channelId') channelId?: string, @Query('applicationId') applicationId?: string, @Query('phoneNumber') phoneNumber?: string, @Query('keyword') keyword?: string, @Query('startTime') startTime?: string, @Query('endTime') endTime?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) { + listUplinkMessages(@CurrentTenantId() tenantId: string, @Query('channelId') channelId?: string, @Query('applicationId') applicationId?: string, @Query('phoneNumber') phoneNumber?: string, @Query('keyword') keyword?: string, @Query('startTime') startTime?: string, @Query('endTime') endTime?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) { return page || pageSize ? this.operations.listUplinkMessagesPage({ tenantId, applicationId, phoneNumber, keyword, startTime, endTime, page: Number(page), pageSize: Number(pageSize) }, true) : this.operations.listClientUplinkMessages({ tenantId, applicationId, phoneNumber, keyword, startTime, endTime }); } @Get('dashboard') - dashboard(@TenantId() tenantId?: string) { + dashboard(@CurrentTenantId() tenantId: string) { return this.operations.clientDashboard({ tenantId }); } @Get('system-logs') systemLogs( - @TenantId() tenantId?: string, + @CurrentTenantId() tenantId: string, @Query('keyword') keyword?: string, @Query('level') level?: string, @Query('module') module?: string, @@ -78,8 +78,9 @@ export class ClientOperationsController { @Post('system-logs/exports') exportSystemLogs( @CurrentSessionUserId() userId: string | undefined, + @CurrentTenantId() tenantId: string, @Body() body: { keyword?: string; level?: string; module?: string; range?: string; createdAtFrom?: string; createdAtTo?: string }, ) { - return this.operations.exportSystemLogs(body, userId); + return this.operations.exportSystemLogs({ ...body, tenantId }, userId); } } diff --git a/api/src/prisma/prisma.service.ts b/api/src/prisma/prisma.service.ts index c7b0bb8..671e903 100644 --- a/api/src/prisma/prisma.service.ts +++ b/api/src/prisma/prisma.service.ts @@ -36,6 +36,10 @@ export class PrismaService extends PrismaClient implements OnModuleDestroy { const poolMax = Number.isInteger(configuredPoolMax) && configuredPoolMax > 0 ? configuredPoolMax : protocolLogRole ? 4 : outboxRole ? 6 : callbackRole ? 16 : workerRole ? 8 : 32; + const allowDevelopmentDefault = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test'; + if (!databaseUrl && !allowDevelopmentDefault) { + throw new Error(`DATABASE_URL is required for CMPP process role ${processRole}`); + } const databasePool = new Pool({ connectionString: databaseUrl ?? 'postgresql://cmpp:cmpp_password@localhost:5432/cmpp_platform?schema=public', diff --git a/api/src/risk-review/client-risk-review.controller.ts b/api/src/risk-review/client-risk-review.controller.ts index e4b6ebf..a6f8290 100644 --- a/api/src/risk-review/client-risk-review.controller.ts +++ b/api/src/risk-review/client-risk-review.controller.ts @@ -1,7 +1,10 @@ -import { Body, Controller, Get, Post, Query } from '@nestjs/common'; +import { Body, Controller, Get, Post, Query, UsePipes } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { TenantId } from '../common/tenant-id.decorator'; -import { EvaluateSmsTaskDto, RiskReviewService } from './risk-review.service'; +import { CurrentTenantId } from '../auth/current-tenant-id.decorator'; +import { CurrentSessionUserId } from '../auth/current-session-user.decorator'; +import { ClientBatchTaskDto } from '../common/client-write.dto'; +import { strictValidationPipe } from '../common/strict-validation.pipe'; +import { RiskReviewService } from './risk-review.service'; @ApiTags('client-risk-review') @Controller('client/risk-review') @@ -9,13 +12,13 @@ export class ClientRiskReviewController { constructor(private readonly riskReview: RiskReviewService) {} @Post('tasks/evaluate') - evaluateTask(@Body() body: EvaluateSmsTaskDto) { - return this.riskReview.evaluateTask(body); + @UsePipes(strictValidationPipe) + evaluateTask(@CurrentTenantId() tenantId: string, @CurrentSessionUserId() createdById: string | undefined, @Body() body: ClientBatchTaskDto) { + return this.riskReview.evaluateTask({ ...body, tenantId, createdById }); } @Get('tasks') - listTasks(@TenantId() tenantId?: string, @Query('status') status?: string) { + listTasks(@CurrentTenantId() tenantId: string, @Query('status') status?: string) { return this.riskReview.listTasks(tenantId, status); } } - diff --git a/api/src/send-chain/client-send-chain.controller.ts b/api/src/send-chain/client-send-chain.controller.ts index 5097b90..94b8f3a 100644 --- a/api/src/send-chain/client-send-chain.controller.ts +++ b/api/src/send-chain/client-send-chain.controller.ts @@ -1,7 +1,9 @@ -import { BadRequestException, Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, Query, UsePipes } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { TenantId } from '../common/tenant-id.decorator'; -import { ConfirmImportDto, CreateBatchTaskDto, ImportPreviewDto } from './send-chain.contracts'; +import { CurrentTenantId } from '../auth/current-tenant-id.decorator'; +import { CurrentSessionUserId } from '../auth/current-session-user.decorator'; +import { ClientBatchTaskDto, ClientImportConfirmDto, ClientImportPreviewDto } from '../common/client-write.dto'; +import { strictValidationPipe } from '../common/strict-validation.pipe'; import { SendChainService } from './send-chain.service'; @ApiTags('client-send-chain') @@ -10,46 +12,42 @@ export class ClientSendChainController { constructor(private readonly sendChain: SendChainService) {} @Post('batch-tasks') - createBatchTask(@Body() body: CreateBatchTaskDto) { - return this.sendChain.createBatchTask(body); + @UsePipes(strictValidationPipe) + createBatchTask(@CurrentTenantId() tenantId: string, @CurrentSessionUserId() createdById: string | undefined, @Body() body: ClientBatchTaskDto) { + return this.sendChain.createBatchTask({ ...body, tenantId, sourceType: 'client', createdById }); } @Post('imports/preview') - previewImport(@Body() body: ImportPreviewDto) { - return this.sendChain.previewImport(body); + @UsePipes(strictValidationPipe) + previewImport(@CurrentTenantId() tenantId: string, @Body() body: ClientImportPreviewDto) { + return this.sendChain.previewImport({ ...body, tenantId }); } @Post('imports/confirm') - confirmImport(@Body() body: ConfirmImportDto) { - return this.sendChain.confirmImport(body); + @UsePipes(strictValidationPipe) + confirmImport(@CurrentTenantId() tenantId: string, @CurrentSessionUserId() createdById: string | undefined, @Body() body: ClientImportConfirmDto) { + return this.sendChain.confirmImport({ ...body, tenantId, sourceType: 'client', createdById }); } @Get('batch-tasks') - listBatchTasks(@TenantId() tenantId?: string, @Query('status') status?: string, @Query('keyword') keyword?: string, @Query('applicationKeyword') applicationKeyword?: string, @Query('createdAtFrom') createdAtFrom?: string, @Query('createdAtTo') createdAtTo?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) { + listBatchTasks(@CurrentTenantId() tenantId: string, @Query('status') status?: string, @Query('keyword') keyword?: string, @Query('applicationKeyword') applicationKeyword?: string, @Query('createdAtFrom') createdAtFrom?: string, @Query('createdAtTo') createdAtTo?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) { return page || pageSize - ? this.sendChain.listBatchTasksPage({ tenantId: requireTenantId(tenantId), status, sourceType: 'client', keyword, applicationKeyword, createdAtFrom, createdAtTo, page: Number(page), pageSize: Number(pageSize) }) - : this.sendChain.listBatchTasks(requireTenantId(tenantId), status, 'client'); + ? this.sendChain.listBatchTasksPage({ tenantId, status, sourceType: 'client', keyword, applicationKeyword, createdAtFrom, createdAtTo, page: Number(page), pageSize: Number(pageSize) }) + : this.sendChain.listBatchTasks(tenantId, status, 'client'); } @Get('batch-tasks/:id') - getBatchTask(@TenantId() tenantId: string | undefined, @Param('id') taskId: string) { - return this.sendChain.getBatchTask(taskId, requireTenantId(tenantId), 'client'); + getBatchTask(@CurrentTenantId() tenantId: string, @Param('id') taskId: string) { + return this.sendChain.getBatchTask(taskId, tenantId, 'client'); } @Get('batch-tasks/:id/messages') - listTaskMessages(@TenantId() tenantId: string | undefined, @Param('id') taskId: string) { - return this.sendChain.listClientTaskMessages(taskId, requireTenantId(tenantId)); + listTaskMessages(@CurrentTenantId() tenantId: string, @Param('id') taskId: string) { + return this.sendChain.listClientTaskMessages(taskId, tenantId); } @Post('batch-tasks/:id/cancel') - cancelBatchTask(@TenantId() tenantId: string | undefined, @Param('id') taskId: string) { - return this.sendChain.cancelBatchTask(taskId, requireTenantId(tenantId), 'client'); + cancelBatchTask(@CurrentTenantId() tenantId: string, @Param('id') taskId: string) { + return this.sendChain.cancelBatchTask(taskId, tenantId, 'client'); } } - -function requireTenantId(tenantId?: string) { - if (!tenantId) { - throw new BadRequestException('Tenant context is required'); - } - return tenantId; -} diff --git a/api/src/send-chain/send-chain.contracts.ts b/api/src/send-chain/send-chain.contracts.ts index 8489c13..0c163d5 100644 --- a/api/src/send-chain/send-chain.contracts.ts +++ b/api/src/send-chain/send-chain.contracts.ts @@ -234,7 +234,7 @@ export interface ImportPreviewDto { requiredVariables?: string[]; } -export interface ConfirmImportDto extends CreateBatchTaskDto { +export interface ConfirmImportDto extends Omit { importContent: string; requiredVariables?: string[]; } diff --git a/api/src/sms-config/client-sms-config.controller.ts b/api/src/sms-config/client-sms-config.controller.ts index 52daba3..32c1a8f 100644 --- a/api/src/sms-config/client-sms-config.controller.ts +++ b/api/src/sms-config/client-sms-config.controller.ts @@ -1,20 +1,11 @@ -import { Body, Controller, Get, Param, Post, Put, Query } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, Put, Query, UsePipes } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { TenantId } from '../common/tenant-id.decorator'; +import { CurrentTenantId } from '../auth/current-tenant-id.decorator'; import { RequireRecentAuthentication } from '../auth/require-recent-authentication.decorator'; import { CurrentSessionUserId } from '../auth/current-session-user.decorator'; -import { DeleteTargetDto, DeletionGovernanceService } from '../deletion-governance/deletion-governance.service'; -import { - CreateSignatureMaterialDto, - CreateSmsApplicationDto, - CreateSmsDrainageInfoDto, - CreateSmsSignatureDto, - CreateSmsTemplateDto, - StatusChangeDto, - UpdateSmsTemplateDto, - UpdateSmsDrainageInfoDto, - UpdateSmsSignatureDto, -} from './sms-config.contracts'; +import { ClientDrainageInfoDto, ClientDrainageInfoUpdateDto, ClientSignatureMaterialDto, ClientSmsApplicationDto, ClientSmsSignatureDto, ClientSmsSignatureUpdateDto, ClientSmsTemplateDto, ClientSmsTemplateUpdateDto, ClientStatusChangeDto } from '../common/client-write.dto'; +import { strictValidationPipe } from '../common/strict-validation.pipe'; +import { DeletionGovernanceService } from '../deletion-governance/deletion-governance.service'; import { SmsConfigService } from './sms-config.service'; @ApiTags('client-sms-config') @@ -23,29 +14,30 @@ export class ClientSmsConfigController { constructor(private readonly smsConfig: SmsConfigService, private readonly deletions: DeletionGovernanceService) {} @Get('applications') - listApplications(@TenantId() tenantId?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) { + listApplications(@CurrentTenantId() tenantId: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) { return page || pageSize ? this.smsConfig.listApplicationsPage({ tenantId, page: Number(page), pageSize: Number(pageSize) }) : this.smsConfig.listApplications(tenantId); } @Get('application-options') - listApplicationOptions(@TenantId() tenantId?: string) { + listApplicationOptions(@CurrentTenantId() tenantId: string) { return this.smsConfig.listApplicationOptions(tenantId); } @Post('applications') - createApplication(@Body() body: CreateSmsApplicationDto) { - return this.smsConfig.createApplication(body); + @UsePipes(strictValidationPipe) + createApplication(@CurrentTenantId() tenantId: string, @Body() body: ClientSmsApplicationDto) { + return this.smsConfig.createApplication({ ...body, tenantId }); } @Get('applications/:id/cmpp-params') - getApplicationCmppParams(@Param('id') applicationId: string, @TenantId() tenantId?: string) { + getApplicationCmppParams(@Param('id') applicationId: string, @CurrentTenantId() tenantId: string) { return this.smsConfig.getApplicationCmppParams(applicationId, tenantId); } @Get('applications/:id/report-fields') - getApplicationReportFields(@Param('id') applicationId: string, @Query('reportType') reportType: 'signature' | 'drainage' = 'drainage', @TenantId() tenantId?: string) { + getApplicationReportFields(@Param('id') applicationId: string, @Query('reportType') reportType: 'signature' | 'drainage' = 'drainage', @CurrentTenantId() tenantId: string) { return this.smsConfig.getApplication(applicationId, tenantId).then(() => this.smsConfig.getClientApplicationReportFields(applicationId, reportType)); } @@ -56,110 +48,122 @@ export class ClientSmsConfigController { @Post('applications/:id/secret/reset') @RequireRecentAuthentication() - resetApplicationSecret(@Param('id') applicationId: string, @Body() body: StatusChangeDto) { - return this.smsConfig.resetApplicationSecret(applicationId, body); + @UsePipes(strictValidationPipe) + resetApplicationSecret(@Param('id') applicationId: string, @Body() body: ClientStatusChangeDto, @CurrentTenantId() tenantId: string, @CurrentSessionUserId() operatorId?: string) { + return this.smsConfig.resetClientApplicationSecret(applicationId, { ...body, operatorId }, tenantId); } @Post('applications/:id/status') @RequireRecentAuthentication() - changeApplicationStatus(@Param('id') applicationId: string, @Body() body: StatusChangeDto) { - return this.smsConfig.changeApplicationStatus(applicationId, body); + @UsePipes(strictValidationPipe) + changeApplicationStatus(@Param('id') applicationId: string, @Body() body: ClientStatusChangeDto, @CurrentTenantId() tenantId: string, @CurrentSessionUserId() operatorId?: string) { + return this.smsConfig.changeClientApplicationStatus(applicationId, { ...body, operatorId }, tenantId); } @Get('signatures') - listSignatures(@TenantId() tenantId?: string) { + listSignatures(@CurrentTenantId() tenantId: string) { return this.smsConfig.listClientSignatures(tenantId); } @Get('signature-options') - listSignatureOptions(@TenantId() tenantId?: string) { + listSignatureOptions(@CurrentTenantId() tenantId: string) { return this.smsConfig.listSignatureOptions(tenantId); } @Get('signatures-workspace') - getSignatureWorkspace(@TenantId() tenantId?: string, @Query('keyword') keyword?: string, @Query('applicationId') applicationId?: string, @Query('status') status?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) { + getSignatureWorkspace(@CurrentTenantId() tenantId: string, @Query('keyword') keyword?: string, @Query('applicationId') applicationId?: string, @Query('status') status?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) { return this.smsConfig.getClientSignatureWorkspace(tenantId, { keyword, applicationId, status, page: Number(page), pageSize: Number(pageSize) }); } @Post('signatures') - async createSignature(@Body() body: CreateSmsSignatureDto, @TenantId() tenantId?: string) { - const signature = await this.smsConfig.createSignature({ ...body, tenantId: tenantId ?? body.tenantId }); - return this.smsConfig.getClientSignatureView(signature.id, tenantId ?? body.tenantId); + @UsePipes(strictValidationPipe) + async createSignature(@Body() body: ClientSmsSignatureDto, @CurrentTenantId() tenantId: string) { + const signature = await this.smsConfig.createSignature({ ...body, tenantId }); + return this.smsConfig.getClientSignatureView(signature.id, tenantId); } @Put('signatures/:id') - async updateSignature(@Param('id') signatureId: string, @Body() body: UpdateSmsSignatureDto, @TenantId() tenantId?: string) { + @UsePipes(strictValidationPipe) + async updateSignature(@Param('id') signatureId: string, @Body() body: ClientSmsSignatureUpdateDto, @CurrentTenantId() tenantId: string) { await this.smsConfig.updateClientSignature(signatureId, body, tenantId); return this.smsConfig.getClientSignatureView(signatureId, tenantId); } @Post('signatures/:id/materials') - createSignatureMaterial(@Param('id') signatureId: string, @Body() body: Omit) { - return this.smsConfig.createSignatureMaterial({ ...body, signatureId }); + @UsePipes(strictValidationPipe) + createSignatureMaterial(@Param('id') signatureId: string, @Body() body: ClientSignatureMaterialDto, @CurrentTenantId() tenantId: string) { + return this.smsConfig.createClientSignatureMaterial({ ...body, signatureId }, tenantId); } @Get('drainage-infos') - listDrainageInfos(@TenantId() tenantId?: string) { + listDrainageInfos(@CurrentTenantId() tenantId: string) { return this.smsConfig.listClientDrainageInfos(tenantId); } @Post('signatures/:id/drainage-infos') - async createDrainageInfo(@Param('id') signatureId: string, @Body() body: CreateSmsDrainageInfoDto, @TenantId() tenantId?: string) { + @UsePipes(strictValidationPipe) + async createDrainageInfo(@Param('id') signatureId: string, @Body() body: ClientDrainageInfoDto, @CurrentTenantId() tenantId: string) { const item = await this.smsConfig.createDrainageInfo(signatureId, body, {}, tenantId); return this.smsConfig.getClientDrainageInfoView(item.id, tenantId); } @Put('drainage-infos/:id') - async updateDrainageInfo(@Param('id') itemId: string, @Body() body: UpdateSmsDrainageInfoDto, @TenantId() tenantId?: string) { + @UsePipes(strictValidationPipe) + async updateDrainageInfo(@Param('id') itemId: string, @Body() body: ClientDrainageInfoUpdateDto, @CurrentTenantId() tenantId: string) { await this.smsConfig.updateDrainageInfo(itemId, body, {}, tenantId); return this.smsConfig.getClientDrainageInfoView(itemId, tenantId); } @Post('drainage-infos/:id/status') - async changeDrainageInfoStatus(@Param('id') itemId: string, @Body() body: StatusChangeDto, @TenantId() tenantId?: string) { - await this.smsConfig.changeDrainageInfoStatus(itemId, body, tenantId); + @UsePipes(strictValidationPipe) + async changeDrainageInfoStatus(@Param('id') itemId: string, @Body() body: ClientStatusChangeDto, @CurrentTenantId() tenantId: string, @CurrentSessionUserId() operatorId?: string) { + await this.smsConfig.changeDrainageInfoStatus(itemId, { ...body, operatorId }, tenantId); if (body.status === 'deleted') return { id: itemId, status: 'deleted' }; return this.smsConfig.getClientDrainageInfoView(itemId, tenantId); } @Post('signatures/:id/submit') - async submitSignature(@Param('id') signatureId: string, @TenantId() tenantId?: string) { + async submitSignature(@Param('id') signatureId: string, @CurrentTenantId() tenantId: string) { await this.smsConfig.submitSignature(signatureId, tenantId); return this.smsConfig.getClientSignatureView(signatureId, tenantId); } @Post('signatures/:id/status') - async changeSignatureStatus(@Param('id') signatureId: string, @Body() body: StatusChangeDto & DeleteTargetDto, @TenantId() tenantId?: string, @CurrentSessionUserId() operatorId?: string) { + @UsePipes(strictValidationPipe) + async changeSignatureStatus(@Param('id') signatureId: string, @Body() body: ClientStatusChangeDto, @CurrentTenantId() tenantId: string, @CurrentSessionUserId() operatorId?: string) { if (body.status === 'deleted') return this.deletions.delete('signature', signatureId, { ...body, operatorId }, tenantId); - await this.smsConfig.changeSignatureStatus(signatureId, body, tenantId); + await this.smsConfig.changeSignatureStatus(signatureId, { ...body, operatorId }, tenantId); return this.smsConfig.getClientSignatureView(signatureId, tenantId); } @Get('templates') - listTemplates(@TenantId() tenantId?: string, @Query('includeHistory') includeHistory?: string, @Query('keyword') keyword?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) { + listTemplates(@CurrentTenantId() tenantId: string, @Query('includeHistory') includeHistory?: string, @Query('keyword') keyword?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) { return page || pageSize ? this.smsConfig.listTemplatesPage({ tenantId, status: includeHistory === 'true' ? 'all' : 'approved', keyword, page: Number(page), pageSize: Number(pageSize) }) : this.smsConfig.listClientTemplates(tenantId, includeHistory === 'true'); } @Post('templates') - createTemplate(@Body() body: CreateSmsTemplateDto, @TenantId() tenantId?: string) { - return this.smsConfig.createTemplate({ ...body, tenantId: tenantId ?? body.tenantId }); + @UsePipes(strictValidationPipe) + createTemplate(@Body() body: ClientSmsTemplateDto, @CurrentTenantId() tenantId: string) { + return this.smsConfig.createTemplate({ ...body, tenantId }); } @Put('templates/:id') - updateTemplate(@Param('id') templateId: string, @Body() body: UpdateSmsTemplateDto, @TenantId() tenantId?: string) { + @UsePipes(strictValidationPipe) + updateTemplate(@Param('id') templateId: string, @Body() body: ClientSmsTemplateUpdateDto, @CurrentTenantId() tenantId: string) { return this.smsConfig.updateClientTemplate(templateId, body, tenantId); } @Post('templates/:id/submit') - submitTemplate(@Param('id') templateId: string, @TenantId() tenantId?: string) { + submitTemplate(@Param('id') templateId: string, @CurrentTenantId() tenantId: string) { return this.smsConfig.submitTemplate(templateId, tenantId); } @Post('templates/:id/status') - changeTemplateStatus(@Param('id') templateId: string, @Body() body: StatusChangeDto & DeleteTargetDto, @TenantId() tenantId?: string, @CurrentSessionUserId() operatorId?: string) { + @UsePipes(strictValidationPipe) + changeTemplateStatus(@Param('id') templateId: string, @Body() body: ClientStatusChangeDto, @CurrentTenantId() tenantId: string, @CurrentSessionUserId() operatorId?: string) { if (body.status === 'deleted') return this.deletions.delete('template', templateId, { ...body, operatorId }, tenantId); - return this.smsConfig.changeTemplateStatus(templateId, body, tenantId); + return this.smsConfig.changeTemplateStatus(templateId, { ...body, operatorId }, tenantId); } } diff --git a/api/src/sms-config/sms-config.service.ts b/api/src/sms-config/sms-config.service.ts index fa51126..acef63a 100644 --- a/api/src/sms-config/sms-config.service.ts +++ b/api/src/sms-config/sms-config.service.ts @@ -81,10 +81,20 @@ export class SmsConfigService implements OnModuleInit, OnModuleDestroy { return this.applications.resetApplicationSecret(applicationId, data); } + async resetClientApplicationSecret(applicationId: string, data: StatusChangeDto, tenantId: string) { + await this.applications.getApplication(applicationId, tenantId); + return this.applications.resetApplicationSecret(applicationId, data); + } + async changeApplicationStatus(applicationId: string, data: StatusChangeDto) { return this.lifecycle.changeApplicationStatus(applicationId, data); } + async changeClientApplicationStatus(applicationId: string, data: StatusChangeDto, tenantId: string) { + await this.applications.getApplication(applicationId, tenantId); + return this.lifecycle.changeApplicationStatus(applicationId, data); + } + async getApplicationDeactivationPreview(applicationId: string) { return this.lifecycle.getApplicationDeactivationPreview(applicationId); } @@ -177,6 +187,11 @@ export class SmsConfigService implements OnModuleInit, OnModuleDestroy { return this.signatures.createSignatureMaterial(data); } + async createClientSignatureMaterial(data: CreateSignatureMaterialDto, tenantId: string) { + await this.signatures.getClientSignatureView(data.signatureId, tenantId); + return this.signatures.createSignatureMaterial(data); + } + async submitSignature(signatureId: string, tenantId?: string) { return this.signatures.submitSignature(signatureId, tenantId); } diff --git a/api/src/users/users.controller.ts b/api/src/users/users.controller.ts index b633f86..cdcd5ba 100644 --- a/api/src/users/users.controller.ts +++ b/api/src/users/users.controller.ts @@ -1,6 +1,6 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; -import { TenantId } from '../common/tenant-id.decorator'; +import { CurrentTenantId } from '../auth/current-tenant-id.decorator'; import { RequireRecentAuthentication } from '../auth/require-recent-authentication.decorator'; import { CurrentSessionUserId } from '../auth/current-session-user.decorator'; import { AdminUserResponseDto, ClientUserResponseDto } from './user-response.dto'; @@ -72,7 +72,7 @@ export class UsersController { @Get('client/users') @ApiOkResponse({ type: [ClientUserResponseDto] }) listClient( - @TenantId() tenantId?: string, + @CurrentTenantId() tenantId: string, @Query('displayName') displayName?: string, @Query('login') login?: string, @Query('status') status?: string, @@ -82,31 +82,31 @@ export class UsersController { @Post('client/users') @RequireRecentAuthentication() - createClient(@TenantId() tenantId: string | undefined, @Body() body: CreateUserDto, @CurrentSessionUserId() operatorId?: string) { + createClient(@CurrentTenantId() tenantId: string, @Body() body: CreateUserDto, @CurrentSessionUserId() operatorId?: string) { return this.users.create({ ...body, roleCode: 'enterprise_admin', operatorId }, tenantId); } @Put('client/users/:id') @RequireRecentAuthentication() - updateClient(@TenantId() tenantId: string | undefined, @Param('id') id: string, @Body() body: UpdateUserDto, @CurrentSessionUserId() operatorId?: string) { + updateClient(@CurrentTenantId() tenantId: string, @Param('id') id: string, @Body() body: UpdateUserDto, @CurrentSessionUserId() operatorId?: string) { return this.users.update(id, { ...body, roleCode: 'enterprise_admin', operatorId }, tenantId); } @Post('client/users/:id/status') @RequireRecentAuthentication() - changeClientStatus(@TenantId() tenantId: string | undefined, @Param('id') id: string, @Body() body: ChangeUserStatusDto, @CurrentSessionUserId() operatorId?: string) { + changeClientStatus(@CurrentTenantId() tenantId: string, @Param('id') id: string, @Body() body: ChangeUserStatusDto, @CurrentSessionUserId() operatorId?: string) { return this.users.changeStatus(id, { ...body, operatorId }, tenantId, operatorId); } @Post('client/users/:id/password') @RequireRecentAuthentication() - changeClientPassword(@TenantId() tenantId: string | undefined, @Param('id') id: string, @Body() body: ChangePasswordDto, @CurrentSessionUserId() operatorId?: string) { + changeClientPassword(@CurrentTenantId() tenantId: string, @Param('id') id: string, @Body() body: ChangePasswordDto, @CurrentSessionUserId() operatorId?: string) { return this.users.changePassword(id, { ...body, operatorId }, tenantId); } @Delete('client/users/:id') @RequireRecentAuthentication() - removeClient(@TenantId() tenantId: string | undefined, @Param('id') id: string, @CurrentSessionUserId() operatorId?: string) { + removeClient(@CurrentTenantId() tenantId: string, @Param('id') id: string, @CurrentSessionUserId() operatorId?: string) { return this.users.remove(id, operatorId, tenantId); } diff --git a/api/src/users/users.service.spec.ts b/api/src/users/users.service.spec.ts index 28f22ea..e5709e1 100644 --- a/api/src/users/users.service.spec.ts +++ b/api/src/users/users.service.spec.ts @@ -1,5 +1,6 @@ import { BadRequestException, ForbiddenException } from '@nestjs/common'; -import { hashPassword, UsersService } from './users.service'; +import { legacyHashPassword, verifyPassword } from '../auth/password-hasher'; +import { UsersService } from './users.service'; function createPrismaMock() { const roles = new Map(); @@ -10,6 +11,7 @@ function createPrismaMock() { findFirst: jest.fn(), findUnique: jest.fn(), update: jest.fn(), + updateMany: jest.fn().mockResolvedValue({ count: 1 }), count: jest.fn().mockResolvedValue(2), }, role: { @@ -206,7 +208,7 @@ describe('UsersService', () => { it('returns a safe view after changing the current password', async () => { const prisma = createPrismaMock(); prisma.user.findFirst.mockResolvedValue({ - id: 'user-1', tenantId: 'tenant-1', username: 'admin', passwordHash: hashPassword('old-password'), + id: 'user-1', tenantId: 'tenant-1', username: 'admin', passwordHash: legacyHashPassword('old-password'), sessionVersion: 3, failedLoginCount: 0, roles: [{ role: { code: 'enterprise_admin' } }], }); prisma.user.update.mockResolvedValue({ @@ -223,6 +225,30 @@ describe('UsersService', () => { expect(user).not.toHaveProperty('failedLoginCount'); }); + it('transparently upgrades a legacy password hash after successful login verification', async () => { + const prisma = createPrismaMock(); + const service = new UsersService(prisma as never); + const legacyHash = legacyHashPassword('correct-password'); + + await expect(service.verifyLoginPassword('user-1', 'correct-password', legacyHash)).resolves.toBe(true); + + expect(prisma.user.updateMany).toHaveBeenCalledWith({ + where: { id: 'user-1', passwordHash: legacyHash }, + data: { passwordHash: expect.stringMatching(/^\$scrypt\$/) }, + }); + const upgradedHash = prisma.user.updateMany.mock.calls[0][0].data.passwordHash; + await expect(verifyPassword('correct-password', upgradedHash)).resolves.toBe(true); + }); + + it('does not rewrite a legacy password hash after failed login verification', async () => { + const prisma = createPrismaMock(); + const service = new UsersService(prisma as never); + + await expect(service.verifyLoginPassword('user-1', 'wrong-password', legacyHashPassword('correct-password'))).resolves.toBe(false); + + expect(prisma.user.updateMany).not.toHaveBeenCalled(); + }); + it('forbids deleting the current signed-in user', async () => { const prisma = createPrismaMock(); prisma.user.findFirst.mockResolvedValue({ diff --git a/api/src/users/users.service.ts b/api/src/users/users.service.ts index 8d60882..ab82860 100644 --- a/api/src/users/users.service.ts +++ b/api/src/users/users.service.ts @@ -1,7 +1,7 @@ -import { createHash } from 'node:crypto'; import { BadRequestException, ConflictException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; +import { hashPassword, passwordNeedsRehash, verifyPassword } from '../auth/password-hasher'; export type UserRoleCode = 'platform_admin' | 'enterprise_admin'; @@ -131,6 +131,7 @@ export class UsersService { this.assertUserInput({ ...data, roleCode }, scopeTenantId, true); const tenantId = scopeTenantId ?? data.tenantId; const role = await this.ensureRole(roleCode); + const passwordHash = await hashPassword(data.password); const user = await this.mapUniqueConflict(() => this.prisma.user.create({ data: { tenantId, @@ -138,7 +139,7 @@ export class UsersService { email: normalizeOptional(data.email), phone: normalizeOptional(data.phone), displayName: data.displayName, - passwordHash: hashPassword(data.password), + passwordHash, status: data.status ?? 'active', roles: { create: [{ roleId: role.id }] }, }, @@ -208,9 +209,10 @@ export class UsersService { throw new BadRequestException('password must be at least 6 characters'); } const current = await this.getExisting(id, scopeTenantId); + const passwordHash = await hashPassword(data.password); const updated = await this.prisma.user.update({ where: { id }, - data: { passwordHash: hashPassword(data.password), failedLoginCount: 0, lockedUntil: null, sessionVersion: { increment: 1 } }, + data: { passwordHash, failedLoginCount: 0, lockedUntil: null, sessionVersion: { increment: 1 } }, include: { tenant: true, roles: { include: { role: true } } }, }); await this.writeLog(current.tenantId, data.operatorId, 'user.password_changed', id, { username: current.username }); @@ -259,12 +261,13 @@ export class UsersService { throw new BadRequestException('currentPassword and a password of at least 6 characters are required'); } const current = await this.getExisting(id); - if (current.passwordHash !== hashPassword(currentPassword)) { + if (!await verifyPassword(currentPassword, current.passwordHash)) { throw new BadRequestException('当前密码不正确'); } + const passwordHash = await hashPassword(password); const updated = await this.prisma.user.update({ where: { id }, - data: { passwordHash: hashPassword(password), failedLoginCount: 0, lockedUntil: null, sessionVersion: { increment: 1 } }, + data: { passwordHash, failedLoginCount: 0, lockedUntil: null, sessionVersion: { increment: 1 } }, include: { tenant: true, roles: { include: { role: true } } }, }); await this.writeLog(current.tenantId, id, 'user.password_changed_self', id, { username: current.username }); @@ -276,12 +279,25 @@ export class UsersService { throw new BadRequestException('请输入当前密码'); } const current = await this.getExisting(id); - if (current.passwordHash !== hashPassword(password)) { + if (!await verifyPassword(password, current.passwordHash)) { throw new BadRequestException('当前密码不正确'); } + if (passwordNeedsRehash(current.passwordHash)) { + const passwordHash = await hashPassword(password); + await this.prisma.user.updateMany({ where: { id, passwordHash: current.passwordHash }, data: { passwordHash } }); + } return current; } + async verifyLoginPassword(id: string, password: string, storedHash: string) { + if (!await verifyPassword(password, storedHash)) return false; + if (passwordNeedsRehash(storedHash)) { + const passwordHash = await hashPassword(password); + await this.prisma.user.updateMany({ where: { id, passwordHash: storedHash }, data: { passwordHash } }); + } + return true; + } + listRoles() { return this.prisma.role.findMany({ include: { permissions: { include: { permission: true } } }, @@ -440,7 +456,3 @@ function normalizeOptional(value?: string | null) { const next = value?.trim(); return next ? next : null; } - -export function hashPassword(password: string) { - return createHash('sha256').update(password).digest('hex'); -} diff --git a/api/tsconfig.build.tsbuildinfo b/api/tsconfig.build.tsbuildinfo deleted file mode 100644 index b0f290f..0000000 --- a/api/tsconfig.build.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"fileNames":["./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/typescript/lib/lib.dom.asynciterable.d.ts","./node_modules/typescript/lib/lib.webworker.importscripts.d.ts","./node_modules/typescript/lib/lib.scripthost.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/typescript/lib/lib.es2022.object.d.ts","./node_modules/typescript/lib/lib.es2022.string.d.ts","./node_modules/typescript/lib/lib.es2022.regexp.d.ts","./node_modules/typescript/lib/lib.es2025.float16.d.ts","./node_modules/typescript/lib/lib.esnext.disposable.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/typescript/lib/lib.es2022.full.d.ts","./node_modules/reflect-metadata/index.d.ts","./node_modules/@nestjs/common/decorators/core/bind.decorator.d.ts","./node_modules/@nestjs/common/interfaces/abstract.interface.d.ts","./node_modules/@nestjs/common/interfaces/controllers/controller-metadata.interface.d.ts","./node_modules/@nestjs/common/interfaces/controllers/controller.interface.d.ts","./node_modules/@nestjs/common/interfaces/features/arguments-host.interface.d.ts","./node_modules/@nestjs/common/interfaces/exceptions/exception-filter.interface.d.ts","./node_modules/rxjs/dist/types/internal/subscription.d.ts","./node_modules/rxjs/dist/types/internal/subscriber.d.ts","./node_modules/rxjs/dist/types/internal/operator.d.ts","./node_modules/rxjs/dist/types/internal/observable.d.ts","./node_modules/rxjs/dist/types/internal/types.d.ts","./node_modules/rxjs/dist/types/internal/operators/audit.d.ts","./node_modules/rxjs/dist/types/internal/operators/audittime.d.ts","./node_modules/rxjs/dist/types/internal/operators/buffer.d.ts","./node_modules/rxjs/dist/types/internal/operators/buffercount.d.ts","./node_modules/rxjs/dist/types/internal/operators/buffertime.d.ts","./node_modules/rxjs/dist/types/internal/operators/buffertoggle.d.ts","./node_modules/rxjs/dist/types/internal/operators/bufferwhen.d.ts","./node_modules/rxjs/dist/types/internal/operators/catcherror.d.ts","./node_modules/rxjs/dist/types/internal/operators/combinelatestall.d.ts","./node_modules/rxjs/dist/types/internal/operators/combineall.d.ts","./node_modules/rxjs/dist/types/internal/operators/combinelatest.d.ts","./node_modules/rxjs/dist/types/internal/operators/combinelatestwith.d.ts","./node_modules/rxjs/dist/types/internal/operators/concat.d.ts","./node_modules/rxjs/dist/types/internal/operators/concatall.d.ts","./node_modules/rxjs/dist/types/internal/operators/concatmap.d.ts","./node_modules/rxjs/dist/types/internal/operators/concatmapto.d.ts","./node_modules/rxjs/dist/types/internal/operators/concatwith.d.ts","./node_modules/rxjs/dist/types/internal/operators/connect.d.ts","./node_modules/rxjs/dist/types/internal/operators/count.d.ts","./node_modules/rxjs/dist/types/internal/operators/debounce.d.ts","./node_modules/rxjs/dist/types/internal/operators/debouncetime.d.ts","./node_modules/rxjs/dist/types/internal/operators/defaultifempty.d.ts","./node_modules/rxjs/dist/types/internal/operators/delay.d.ts","./node_modules/rxjs/dist/types/internal/operators/delaywhen.d.ts","./node_modules/rxjs/dist/types/internal/operators/dematerialize.d.ts","./node_modules/rxjs/dist/types/internal/operators/distinct.d.ts","./node_modules/rxjs/dist/types/internal/operators/distinctuntilchanged.d.ts","./node_modules/rxjs/dist/types/internal/operators/distinctuntilkeychanged.d.ts","./node_modules/rxjs/dist/types/internal/operators/elementat.d.ts","./node_modules/rxjs/dist/types/internal/operators/endwith.d.ts","./node_modules/rxjs/dist/types/internal/operators/every.d.ts","./node_modules/rxjs/dist/types/internal/operators/exhaustall.d.ts","./node_modules/rxjs/dist/types/internal/operators/exhaust.d.ts","./node_modules/rxjs/dist/types/internal/operators/exhaustmap.d.ts","./node_modules/rxjs/dist/types/internal/operators/expand.d.ts","./node_modules/rxjs/dist/types/internal/operators/filter.d.ts","./node_modules/rxjs/dist/types/internal/operators/finalize.d.ts","./node_modules/rxjs/dist/types/internal/operators/find.d.ts","./node_modules/rxjs/dist/types/internal/operators/findindex.d.ts","./node_modules/rxjs/dist/types/internal/operators/first.d.ts","./node_modules/rxjs/dist/types/internal/subject.d.ts","./node_modules/rxjs/dist/types/internal/operators/groupby.d.ts","./node_modules/rxjs/dist/types/internal/operators/ignoreelements.d.ts","./node_modules/rxjs/dist/types/internal/operators/isempty.d.ts","./node_modules/rxjs/dist/types/internal/operators/last.d.ts","./node_modules/rxjs/dist/types/internal/operators/map.d.ts","./node_modules/rxjs/dist/types/internal/operators/mapto.d.ts","./node_modules/rxjs/dist/types/internal/notification.d.ts","./node_modules/rxjs/dist/types/internal/operators/materialize.d.ts","./node_modules/rxjs/dist/types/internal/operators/max.d.ts","./node_modules/rxjs/dist/types/internal/operators/merge.d.ts","./node_modules/rxjs/dist/types/internal/operators/mergeall.d.ts","./node_modules/rxjs/dist/types/internal/operators/mergemap.d.ts","./node_modules/rxjs/dist/types/internal/operators/flatmap.d.ts","./node_modules/rxjs/dist/types/internal/operators/mergemapto.d.ts","./node_modules/rxjs/dist/types/internal/operators/mergescan.d.ts","./node_modules/rxjs/dist/types/internal/operators/mergewith.d.ts","./node_modules/rxjs/dist/types/internal/operators/min.d.ts","./node_modules/rxjs/dist/types/internal/observable/connectableobservable.d.ts","./node_modules/rxjs/dist/types/internal/operators/multicast.d.ts","./node_modules/rxjs/dist/types/internal/operators/observeon.d.ts","./node_modules/rxjs/dist/types/internal/operators/onerrorresumenextwith.d.ts","./node_modules/rxjs/dist/types/internal/operators/pairwise.d.ts","./node_modules/rxjs/dist/types/internal/operators/partition.d.ts","./node_modules/rxjs/dist/types/internal/operators/pluck.d.ts","./node_modules/rxjs/dist/types/internal/operators/publish.d.ts","./node_modules/rxjs/dist/types/internal/operators/publishbehavior.d.ts","./node_modules/rxjs/dist/types/internal/operators/publishlast.d.ts","./node_modules/rxjs/dist/types/internal/operators/publishreplay.d.ts","./node_modules/rxjs/dist/types/internal/operators/race.d.ts","./node_modules/rxjs/dist/types/internal/operators/racewith.d.ts","./node_modules/rxjs/dist/types/internal/operators/reduce.d.ts","./node_modules/rxjs/dist/types/internal/operators/repeat.d.ts","./node_modules/rxjs/dist/types/internal/operators/repeatwhen.d.ts","./node_modules/rxjs/dist/types/internal/operators/retry.d.ts","./node_modules/rxjs/dist/types/internal/operators/retrywhen.d.ts","./node_modules/rxjs/dist/types/internal/operators/refcount.d.ts","./node_modules/rxjs/dist/types/internal/operators/sample.d.ts","./node_modules/rxjs/dist/types/internal/operators/sampletime.d.ts","./node_modules/rxjs/dist/types/internal/operators/scan.d.ts","./node_modules/rxjs/dist/types/internal/operators/sequenceequal.d.ts","./node_modules/rxjs/dist/types/internal/operators/share.d.ts","./node_modules/rxjs/dist/types/internal/operators/sharereplay.d.ts","./node_modules/rxjs/dist/types/internal/operators/single.d.ts","./node_modules/rxjs/dist/types/internal/operators/skip.d.ts","./node_modules/rxjs/dist/types/internal/operators/skiplast.d.ts","./node_modules/rxjs/dist/types/internal/operators/skipuntil.d.ts","./node_modules/rxjs/dist/types/internal/operators/skipwhile.d.ts","./node_modules/rxjs/dist/types/internal/operators/startwith.d.ts","./node_modules/rxjs/dist/types/internal/operators/subscribeon.d.ts","./node_modules/rxjs/dist/types/internal/operators/switchall.d.ts","./node_modules/rxjs/dist/types/internal/operators/switchmap.d.ts","./node_modules/rxjs/dist/types/internal/operators/switchmapto.d.ts","./node_modules/rxjs/dist/types/internal/operators/switchscan.d.ts","./node_modules/rxjs/dist/types/internal/operators/take.d.ts","./node_modules/rxjs/dist/types/internal/operators/takelast.d.ts","./node_modules/rxjs/dist/types/internal/operators/takeuntil.d.ts","./node_modules/rxjs/dist/types/internal/operators/takewhile.d.ts","./node_modules/rxjs/dist/types/internal/operators/tap.d.ts","./node_modules/rxjs/dist/types/internal/operators/throttle.d.ts","./node_modules/rxjs/dist/types/internal/operators/throttletime.d.ts","./node_modules/rxjs/dist/types/internal/operators/throwifempty.d.ts","./node_modules/rxjs/dist/types/internal/operators/timeinterval.d.ts","./node_modules/rxjs/dist/types/internal/operators/timeout.d.ts","./node_modules/rxjs/dist/types/internal/operators/timeoutwith.d.ts","./node_modules/rxjs/dist/types/internal/operators/timestamp.d.ts","./node_modules/rxjs/dist/types/internal/operators/toarray.d.ts","./node_modules/rxjs/dist/types/internal/operators/window.d.ts","./node_modules/rxjs/dist/types/internal/operators/windowcount.d.ts","./node_modules/rxjs/dist/types/internal/operators/windowtime.d.ts","./node_modules/rxjs/dist/types/internal/operators/windowtoggle.d.ts","./node_modules/rxjs/dist/types/internal/operators/windowwhen.d.ts","./node_modules/rxjs/dist/types/internal/operators/withlatestfrom.d.ts","./node_modules/rxjs/dist/types/internal/operators/zip.d.ts","./node_modules/rxjs/dist/types/internal/operators/zipall.d.ts","./node_modules/rxjs/dist/types/internal/operators/zipwith.d.ts","./node_modules/rxjs/dist/types/operators/index.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/action.d.ts","./node_modules/rxjs/dist/types/internal/scheduler.d.ts","./node_modules/rxjs/dist/types/internal/testing/testmessage.d.ts","./node_modules/rxjs/dist/types/internal/testing/subscriptionlog.d.ts","./node_modules/rxjs/dist/types/internal/testing/subscriptionloggable.d.ts","./node_modules/rxjs/dist/types/internal/testing/coldobservable.d.ts","./node_modules/rxjs/dist/types/internal/testing/hotobservable.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/asyncscheduler.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/timerhandle.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/asyncaction.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/virtualtimescheduler.d.ts","./node_modules/rxjs/dist/types/internal/testing/testscheduler.d.ts","./node_modules/rxjs/dist/types/testing/index.d.ts","./node_modules/rxjs/dist/types/internal/symbol/observable.d.ts","./node_modules/rxjs/dist/types/internal/observable/dom/animationframes.d.ts","./node_modules/rxjs/dist/types/internal/behaviorsubject.d.ts","./node_modules/rxjs/dist/types/internal/replaysubject.d.ts","./node_modules/rxjs/dist/types/internal/asyncsubject.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/asapscheduler.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/asap.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/async.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/queuescheduler.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/queue.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/animationframescheduler.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/animationframe.d.ts","./node_modules/rxjs/dist/types/internal/util/identity.d.ts","./node_modules/rxjs/dist/types/internal/util/pipe.d.ts","./node_modules/rxjs/dist/types/internal/util/noop.d.ts","./node_modules/rxjs/dist/types/internal/util/isobservable.d.ts","./node_modules/rxjs/dist/types/internal/lastvaluefrom.d.ts","./node_modules/rxjs/dist/types/internal/firstvaluefrom.d.ts","./node_modules/rxjs/dist/types/internal/util/argumentoutofrangeerror.d.ts","./node_modules/rxjs/dist/types/internal/util/emptyerror.d.ts","./node_modules/rxjs/dist/types/internal/util/notfounderror.d.ts","./node_modules/rxjs/dist/types/internal/util/objectunsubscribederror.d.ts","./node_modules/rxjs/dist/types/internal/util/sequenceerror.d.ts","./node_modules/rxjs/dist/types/internal/util/unsubscriptionerror.d.ts","./node_modules/rxjs/dist/types/internal/observable/bindcallback.d.ts","./node_modules/rxjs/dist/types/internal/observable/bindnodecallback.d.ts","./node_modules/rxjs/dist/types/internal/anycatcher.d.ts","./node_modules/rxjs/dist/types/internal/observable/combinelatest.d.ts","./node_modules/rxjs/dist/types/internal/observable/concat.d.ts","./node_modules/rxjs/dist/types/internal/observable/connectable.d.ts","./node_modules/rxjs/dist/types/internal/observable/defer.d.ts","./node_modules/rxjs/dist/types/internal/observable/empty.d.ts","./node_modules/rxjs/dist/types/internal/observable/forkjoin.d.ts","./node_modules/rxjs/dist/types/internal/observable/from.d.ts","./node_modules/rxjs/dist/types/internal/observable/fromevent.d.ts","./node_modules/rxjs/dist/types/internal/observable/fromeventpattern.d.ts","./node_modules/rxjs/dist/types/internal/observable/generate.d.ts","./node_modules/rxjs/dist/types/internal/observable/iif.d.ts","./node_modules/rxjs/dist/types/internal/observable/interval.d.ts","./node_modules/rxjs/dist/types/internal/observable/merge.d.ts","./node_modules/rxjs/dist/types/internal/observable/never.d.ts","./node_modules/rxjs/dist/types/internal/observable/of.d.ts","./node_modules/rxjs/dist/types/internal/observable/onerrorresumenext.d.ts","./node_modules/rxjs/dist/types/internal/observable/pairs.d.ts","./node_modules/rxjs/dist/types/internal/observable/partition.d.ts","./node_modules/rxjs/dist/types/internal/observable/race.d.ts","./node_modules/rxjs/dist/types/internal/observable/range.d.ts","./node_modules/rxjs/dist/types/internal/observable/throwerror.d.ts","./node_modules/rxjs/dist/types/internal/observable/timer.d.ts","./node_modules/rxjs/dist/types/internal/observable/using.d.ts","./node_modules/rxjs/dist/types/internal/observable/zip.d.ts","./node_modules/rxjs/dist/types/internal/scheduled/scheduled.d.ts","./node_modules/rxjs/dist/types/internal/config.d.ts","./node_modules/rxjs/dist/types/index.d.ts","./node_modules/@nestjs/common/interfaces/exceptions/rpc-exception-filter.interface.d.ts","./node_modules/@nestjs/common/interfaces/exceptions/ws-exception-filter.interface.d.ts","./node_modules/@nestjs/common/interfaces/external/validation-error.interface.d.ts","./node_modules/@nestjs/common/interfaces/features/execution-context.interface.d.ts","./node_modules/@nestjs/common/interfaces/features/can-activate.interface.d.ts","./node_modules/@nestjs/common/interfaces/features/custom-route-param-factory.interface.d.ts","./node_modules/@nestjs/common/interfaces/features/nest-interceptor.interface.d.ts","./node_modules/@nestjs/common/interfaces/features/paramtype.interface.d.ts","./node_modules/@nestjs/common/interfaces/type.interface.d.ts","./node_modules/@nestjs/common/interfaces/features/pipe-transform.interface.d.ts","./node_modules/@nestjs/common/enums/request-method.enum.d.ts","./node_modules/@nestjs/common/enums/http-status.enum.d.ts","./node_modules/@nestjs/common/enums/shutdown-signal.enum.d.ts","./node_modules/@nestjs/common/enums/version-type.enum.d.ts","./node_modules/@nestjs/common/enums/index.d.ts","./node_modules/@nestjs/common/interfaces/version-options.interface.d.ts","./node_modules/@nestjs/common/interfaces/middleware/middleware-configuration.interface.d.ts","./node_modules/@nestjs/common/interfaces/middleware/middleware-consumer.interface.d.ts","./node_modules/@nestjs/common/interfaces/middleware/middleware-config-proxy.interface.d.ts","./node_modules/@nestjs/common/interfaces/middleware/nest-middleware.interface.d.ts","./node_modules/@nestjs/common/interfaces/middleware/index.d.ts","./node_modules/@nestjs/common/interfaces/global-prefix-options.interface.d.ts","./node_modules/@nestjs/common/interfaces/hooks/before-application-shutdown.interface.d.ts","./node_modules/@nestjs/common/interfaces/hooks/on-application-bootstrap.interface.d.ts","./node_modules/@nestjs/common/interfaces/hooks/on-application-shutdown.interface.d.ts","./node_modules/@nestjs/common/interfaces/hooks/on-destroy.interface.d.ts","./node_modules/@nestjs/common/interfaces/hooks/on-init.interface.d.ts","./node_modules/@nestjs/common/interfaces/hooks/index.d.ts","./node_modules/@nestjs/common/interfaces/http/http-exception-body.interface.d.ts","./node_modules/@nestjs/common/interfaces/http/http-redirect-response.interface.d.ts","./node_modules/@nestjs/common/interfaces/external/cors-options.interface.d.ts","./node_modules/@nestjs/common/interfaces/external/https-options.interface.d.ts","./node_modules/@nestjs/common/services/logger.service.d.ts","./node_modules/@nestjs/common/interfaces/nest-application-context-options.interface.d.ts","./node_modules/@nestjs/common/interfaces/nest-application-options.interface.d.ts","./node_modules/@nestjs/common/interfaces/http/http-server.interface.d.ts","./node_modules/@nestjs/common/interfaces/http/message-event.interface.d.ts","./node_modules/@nestjs/common/interfaces/http/raw-body-request.interface.d.ts","./node_modules/@nestjs/common/interfaces/http/index.d.ts","./node_modules/@nestjs/common/interfaces/injectable.interface.d.ts","./node_modules/@nestjs/common/interfaces/microservices/nest-hybrid-application-options.interface.d.ts","./node_modules/@nestjs/common/interfaces/modules/forward-reference.interface.d.ts","./node_modules/@nestjs/common/interfaces/scope-options.interface.d.ts","./node_modules/@nestjs/common/interfaces/modules/injection-token.interface.d.ts","./node_modules/@nestjs/common/interfaces/modules/optional-factory-dependency.interface.d.ts","./node_modules/@nestjs/common/interfaces/modules/provider.interface.d.ts","./node_modules/@nestjs/common/interfaces/modules/module-metadata.interface.d.ts","./node_modules/@nestjs/common/interfaces/modules/dynamic-module.interface.d.ts","./node_modules/@nestjs/common/interfaces/modules/introspection-result.interface.d.ts","./node_modules/@nestjs/common/interfaces/modules/nest-module.interface.d.ts","./node_modules/@nestjs/common/interfaces/modules/index.d.ts","./node_modules/@nestjs/common/interfaces/shutdown-hooks-options.interface.d.ts","./node_modules/@nestjs/common/interfaces/nest-application-context.interface.d.ts","./node_modules/@nestjs/common/interfaces/websockets/web-socket-adapter.interface.d.ts","./node_modules/@nestjs/common/interfaces/nest-application.interface.d.ts","./node_modules/@nestjs/common/interfaces/nest-microservice.interface.d.ts","./node_modules/@nestjs/common/interfaces/index.d.ts","./node_modules/@nestjs/common/decorators/core/catch.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/controller.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/dependencies.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/exception-filters.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/inject.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/injectable.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/optional.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/set-metadata.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/use-guards.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/use-interceptors.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/use-pipes.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/apply-decorators.d.ts","./node_modules/@nestjs/common/decorators/core/version.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/index.d.ts","./node_modules/@nestjs/common/decorators/modules/global.decorator.d.ts","./node_modules/@nestjs/common/decorators/modules/module.decorator.d.ts","./node_modules/@nestjs/common/decorators/modules/index.d.ts","./node_modules/@nestjs/common/decorators/http/request-mapping.decorator.d.ts","./node_modules/@nestjs/common/decorators/http/route-params.decorator.d.ts","./node_modules/@nestjs/common/decorators/http/http-code.decorator.d.ts","./node_modules/@nestjs/common/decorators/http/create-route-param-metadata.decorator.d.ts","./node_modules/@nestjs/common/decorators/http/render.decorator.d.ts","./node_modules/@nestjs/common/decorators/http/header.decorator.d.ts","./node_modules/@nestjs/common/decorators/http/redirect.decorator.d.ts","./node_modules/@nestjs/common/constants.d.ts","./node_modules/@nestjs/common/decorators/http/sse.decorator.d.ts","./node_modules/@nestjs/common/decorators/http/index.d.ts","./node_modules/@nestjs/common/decorators/index.d.ts","./node_modules/@nestjs/common/exceptions/intrinsic.exception.d.ts","./node_modules/@nestjs/common/exceptions/http.exception.d.ts","./node_modules/@nestjs/common/exceptions/bad-gateway.exception.d.ts","./node_modules/@nestjs/common/exceptions/bad-request.exception.d.ts","./node_modules/@nestjs/common/exceptions/conflict.exception.d.ts","./node_modules/@nestjs/common/exceptions/forbidden.exception.d.ts","./node_modules/@nestjs/common/exceptions/gateway-timeout.exception.d.ts","./node_modules/@nestjs/common/exceptions/gone.exception.d.ts","./node_modules/@nestjs/common/exceptions/http-version-not-supported.exception.d.ts","./node_modules/@nestjs/common/exceptions/im-a-teapot.exception.d.ts","./node_modules/@nestjs/common/exceptions/internal-server-error.exception.d.ts","./node_modules/@nestjs/common/exceptions/method-not-allowed.exception.d.ts","./node_modules/@nestjs/common/exceptions/misdirected.exception.d.ts","./node_modules/@nestjs/common/exceptions/not-acceptable.exception.d.ts","./node_modules/@nestjs/common/exceptions/not-found.exception.d.ts","./node_modules/@nestjs/common/exceptions/not-implemented.exception.d.ts","./node_modules/@nestjs/common/exceptions/payload-too-large.exception.d.ts","./node_modules/@nestjs/common/exceptions/precondition-failed.exception.d.ts","./node_modules/@nestjs/common/exceptions/request-timeout.exception.d.ts","./node_modules/@nestjs/common/exceptions/service-unavailable.exception.d.ts","./node_modules/@nestjs/common/exceptions/unauthorized.exception.d.ts","./node_modules/@nestjs/common/exceptions/unprocessable-entity.exception.d.ts","./node_modules/@nestjs/common/exceptions/unsupported-media-type.exception.d.ts","./node_modules/@nestjs/common/exceptions/index.d.ts","./node_modules/@nestjs/common/services/console-logger.service.d.ts","./node_modules/@nestjs/common/services/utils/filter-log-levels.util.d.ts","./node_modules/@nestjs/common/services/index.d.ts","./node_modules/@nestjs/common/file-stream/interfaces/streamable-options.interface.d.ts","./node_modules/@nestjs/common/file-stream/interfaces/streamable-handler-response.interface.d.ts","./node_modules/@nestjs/common/file-stream/interfaces/index.d.ts","./node_modules/@nestjs/common/file-stream/streamable-file.d.ts","./node_modules/@nestjs/common/file-stream/index.d.ts","./node_modules/@nestjs/common/module-utils/constants.d.ts","./node_modules/@nestjs/common/module-utils/interfaces/configurable-module-async-options.interface.d.ts","./node_modules/@nestjs/common/module-utils/interfaces/configurable-module-cls.interface.d.ts","./node_modules/@nestjs/common/module-utils/interfaces/configurable-module-host.interface.d.ts","./node_modules/@nestjs/common/module-utils/interfaces/index.d.ts","./node_modules/@nestjs/common/module-utils/configurable-module.builder.d.ts","./node_modules/@nestjs/common/module-utils/index.d.ts","./node_modules/@nestjs/common/pipes/default-value.pipe.d.ts","./node_modules/@nestjs/common/pipes/file/interfaces/file.interface.d.ts","./node_modules/@nestjs/common/pipes/file/interfaces/index.d.ts","./node_modules/@nestjs/common/pipes/file/file-validator-context.interface.d.ts","./node_modules/@nestjs/common/pipes/file/file-validator.interface.d.ts","./node_modules/@nestjs/common/pipes/file/file-type.validator.d.ts","./node_modules/@nestjs/common/pipes/file/max-file-size.validator.d.ts","./node_modules/@nestjs/common/utils/http-error-by-code.util.d.ts","./node_modules/@nestjs/common/pipes/file/parse-file-options.interface.d.ts","./node_modules/@nestjs/common/pipes/file/parse-file.pipe.d.ts","./node_modules/@nestjs/common/pipes/file/parse-file-pipe.builder.d.ts","./node_modules/@nestjs/common/pipes/file/index.d.ts","./node_modules/@nestjs/common/interfaces/external/class-transform-options.interface.d.ts","./node_modules/@nestjs/common/interfaces/external/transformer-package.interface.d.ts","./node_modules/@nestjs/common/interfaces/external/validator-options.interface.d.ts","./node_modules/@nestjs/common/interfaces/external/validator-package.interface.d.ts","./node_modules/@nestjs/common/pipes/validation.pipe.d.ts","./node_modules/@nestjs/common/pipes/parse-array.pipe.d.ts","./node_modules/@nestjs/common/pipes/parse-bool.pipe.d.ts","./node_modules/@nestjs/common/pipes/parse-date.pipe.d.ts","./node_modules/@nestjs/common/pipes/parse-enum.pipe.d.ts","./node_modules/@nestjs/common/pipes/parse-float.pipe.d.ts","./node_modules/@nestjs/common/pipes/parse-int.pipe.d.ts","./node_modules/@nestjs/common/pipes/parse-uuid.pipe.d.ts","./node_modules/@nestjs/common/pipes/index.d.ts","./node_modules/@nestjs/common/serializer/class-serializer.interfaces.d.ts","./node_modules/@nestjs/common/serializer/class-serializer.interceptor.d.ts","./node_modules/@nestjs/common/serializer/decorators/serialize-options.decorator.d.ts","./node_modules/@nestjs/common/serializer/decorators/index.d.ts","./node_modules/@nestjs/common/serializer/index.d.ts","./node_modules/@nestjs/common/utils/forward-ref.util.d.ts","./node_modules/@nestjs/common/utils/index.d.ts","./node_modules/@nestjs/common/index.d.ts","./node_modules/@nestjs/config/dist/conditional.module.d.ts","./node_modules/@nestjs/config/dist/interfaces/config-change-event.interface.d.ts","./node_modules/@nestjs/config/dist/types/config-object.type.d.ts","./node_modules/@nestjs/config/dist/types/config.type.d.ts","./node_modules/@nestjs/config/dist/types/no-infer.type.d.ts","./node_modules/@nestjs/config/dist/types/path-value.type.d.ts","./node_modules/@nestjs/config/dist/types/index.d.ts","./node_modules/@nestjs/config/dist/interfaces/config-factory.interface.d.ts","./node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/@types/node/globals.typedarray.d.ts","./node_modules/@types/node/buffer.buffer.d.ts","./node_modules/@types/node/globals.d.ts","./node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/@types/node/web-globals/blob.d.ts","./node_modules/@types/node/web-globals/console.d.ts","./node_modules/@types/node/web-globals/crypto.d.ts","./node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/@types/node/web-globals/encoding.d.ts","./node_modules/@types/node/web-globals/events.d.ts","./node_modules/undici-types/utility.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client-stats.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts","./node_modules/undici-types/global-dispatcher.d.ts","./node_modules/undici-types/global-origin.d.ts","./node_modules/undici-types/pool-stats.d.ts","./node_modules/undici-types/pool.d.ts","./node_modules/undici-types/handlers.d.ts","./node_modules/undici-types/balanced-pool.d.ts","./node_modules/undici-types/round-robin-pool.d.ts","./node_modules/undici-types/h2c-client.d.ts","./node_modules/undici-types/agent.d.ts","./node_modules/undici-types/mock-interceptor.d.ts","./node_modules/undici-types/mock-call-history.d.ts","./node_modules/undici-types/mock-agent.d.ts","./node_modules/undici-types/mock-client.d.ts","./node_modules/undici-types/mock-pool.d.ts","./node_modules/undici-types/snapshot-agent.d.ts","./node_modules/undici-types/mock-errors.d.ts","./node_modules/undici-types/proxy-agent.d.ts","./node_modules/undici-types/socks5-proxy-agent.d.ts","./node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/undici-types/retry-handler.d.ts","./node_modules/undici-types/retry-agent.d.ts","./node_modules/undici-types/api.d.ts","./node_modules/undici-types/cache-interceptor.d.ts","./node_modules/undici-types/interceptors.d.ts","./node_modules/undici-types/util.d.ts","./node_modules/undici-types/cookies.d.ts","./node_modules/undici-types/patch.d.ts","./node_modules/undici-types/websocket.d.ts","./node_modules/undici-types/eventsource.d.ts","./node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/undici-types/content-type.d.ts","./node_modules/undici-types/cache.d.ts","./node_modules/undici-types/index.d.ts","./node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/@types/node/web-globals/importmeta.d.ts","./node_modules/@types/node/web-globals/messaging.d.ts","./node_modules/@types/node/web-globals/navigator.d.ts","./node_modules/@types/node/web-globals/performance.d.ts","./node_modules/@types/node/web-globals/storage.d.ts","./node_modules/@types/node/web-globals/streams.d.ts","./node_modules/@types/node/web-globals/timers.d.ts","./node_modules/@types/node/web-globals/url.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/@types/node/async_hooks.d.ts","./node_modules/@types/node/buffer.d.ts","./node_modules/@types/node/child_process.d.ts","./node_modules/@types/node/cluster.d.ts","./node_modules/@types/node/console.d.ts","./node_modules/@types/node/constants.d.ts","./node_modules/@types/node/crypto.d.ts","./node_modules/@types/node/dgram.d.ts","./node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/@types/node/dns.d.ts","./node_modules/@types/node/dns/promises.d.ts","./node_modules/@types/node/domain.d.ts","./node_modules/@types/node/events.d.ts","./node_modules/@types/node/fs.d.ts","./node_modules/@types/node/fs/promises.d.ts","./node_modules/@types/node/http.d.ts","./node_modules/@types/node/http2.d.ts","./node_modules/@types/node/https.d.ts","./node_modules/@types/node/inspector.d.ts","./node_modules/@types/node/inspector.generated.d.ts","./node_modules/@types/node/inspector/promises.d.ts","./node_modules/@types/node/module.d.ts","./node_modules/@types/node/net.d.ts","./node_modules/@types/node/os.d.ts","./node_modules/@types/node/path.d.ts","./node_modules/@types/node/path/posix.d.ts","./node_modules/@types/node/path/win32.d.ts","./node_modules/@types/node/perf_hooks.d.ts","./node_modules/@types/node/process.d.ts","./node_modules/@types/node/punycode.d.ts","./node_modules/@types/node/querystring.d.ts","./node_modules/@types/node/quic.d.ts","./node_modules/@types/node/readline.d.ts","./node_modules/@types/node/readline/promises.d.ts","./node_modules/@types/node/repl.d.ts","./node_modules/@types/node/sea.d.ts","./node_modules/@types/node/sqlite.d.ts","./node_modules/@types/node/stream.d.ts","./node_modules/@types/node/stream/consumers.d.ts","./node_modules/@types/node/stream/iter.d.ts","./node_modules/@types/node/stream/promises.d.ts","./node_modules/@types/node/stream/web.d.ts","./node_modules/@types/node/string_decoder.d.ts","./node_modules/@types/node/test.d.ts","./node_modules/@types/node/test/reporters.d.ts","./node_modules/@types/node/timers.d.ts","./node_modules/@types/node/timers/promises.d.ts","./node_modules/@types/node/tls.d.ts","./node_modules/@types/node/trace_events.d.ts","./node_modules/@types/node/tty.d.ts","./node_modules/@types/node/url.d.ts","./node_modules/@types/node/util.d.ts","./node_modules/@types/node/util/types.d.ts","./node_modules/@types/node/v8.d.ts","./node_modules/@types/node/vm.d.ts","./node_modules/@types/node/wasi.d.ts","./node_modules/@types/node/worker_threads.d.ts","./node_modules/@types/node/zlib.d.ts","./node_modules/@types/node/zlib/iter.d.ts","./node_modules/@types/node/index.d.ts","./node_modules/dotenv-expand/lib/main.d.ts","./node_modules/@nestjs/config/dist/interfaces/config-module-options.interface.d.ts","./node_modules/@nestjs/config/dist/interfaces/index.d.ts","./node_modules/@nestjs/config/dist/config.module.d.ts","./node_modules/@nestjs/config/dist/config.service.d.ts","./node_modules/@nestjs/config/dist/utils/register-as.util.d.ts","./node_modules/@nestjs/config/dist/utils/get-config-token.util.d.ts","./node_modules/@nestjs/config/dist/utils/index.d.ts","./node_modules/@nestjs/config/dist/index.d.ts","./node_modules/@nestjs/config/index.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-basic.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-bearer.decorator.d.ts","./node_modules/@nestjs/swagger/dist/interfaces/open-api-spec.interface.d.ts","./node_modules/@nestjs/swagger/dist/types/swagger-enum.type.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-body.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-consumes.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-cookie.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-default-getter.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-exclude-endpoint.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-include-endpoint.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-exclude-controller.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-extra-models.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-header.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-hide-property.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-link.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-oauth2.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-operation.decorator.d.ts","./node_modules/@nestjs/swagger/dist/interfaces/enum-schema-attributes.interface.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-param.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-produces.decorator.d.ts","./node_modules/@nestjs/swagger/dist/interfaces/schema-object-metadata.interface.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-property.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-query.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-webhook.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-response.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-security.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-use-tags.decorator.d.ts","./node_modules/@nestjs/swagger/dist/interfaces/callback-object.interface.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-callbacks.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-extension.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-schema.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/index.d.ts","./node_modules/@nestjs/swagger/dist/interfaces/swagger-ui-options.interface.d.ts","./node_modules/@nestjs/swagger/dist/interfaces/swagger-custom-options.interface.d.ts","./node_modules/@nestjs/swagger/dist/interfaces/swagger-document-options.interface.d.ts","./node_modules/@nestjs/swagger/dist/interfaces/index.d.ts","./node_modules/@nestjs/swagger/dist/document-builder.d.ts","./node_modules/@nestjs/swagger/dist/swagger-module.d.ts","./node_modules/@nestjs/swagger/dist/type-helpers/deep-partial-type.helper.d.ts","./node_modules/@nestjs/swagger/dist/type-helpers/intersection-type.helper.d.ts","./node_modules/@nestjs/swagger/dist/type-helpers/omit-type.helper.d.ts","./node_modules/@nestjs/swagger/dist/type-helpers/partial-type.helper.d.ts","./node_modules/@nestjs/swagger/dist/type-helpers/pick-type.helper.d.ts","./node_modules/@nestjs/swagger/dist/type-helpers/index.d.ts","./node_modules/@nestjs/swagger/dist/utils/get-schema-path.util.d.ts","./node_modules/@nestjs/swagger/dist/utils/generate-schema.util.d.ts","./node_modules/@nestjs/swagger/dist/utils/index.d.ts","./node_modules/@nestjs/swagger/dist/constants.d.ts","./node_modules/@nestjs/swagger/dist/index.d.ts","./src/common/tenant-id.decorator.ts","./node_modules/@prisma/client-runtime-utils/dist/index.d.ts","./node_modules/@prisma/client/runtime/client.d.ts","./node_modules/.prisma/client/index.d.ts","./node_modules/.prisma/client/default.d.ts","./node_modules/@prisma/client/default.d.ts","./node_modules/@prisma/debug/dist/index.d.ts","./node_modules/@prisma/driver-adapter-utils/dist/index.d.ts","./node_modules/pg-types/index.d.ts","./node_modules/pg-protocol/dist/messages.d.ts","./node_modules/pg-protocol/dist/serializer.d.ts","./node_modules/pg-protocol/dist/parser.d.ts","./node_modules/pg-protocol/dist/index.d.ts","./node_modules/@types/pg/lib/type-overrides.d.ts","./node_modules/@types/pg/index.d.ts","./node_modules/@prisma/adapter-pg/dist/index.d.ts","./src/prisma/prisma.service.ts","./src/audit/audit.service.ts","./src/audit/audit.controller.ts","./src/audit/audit.module.ts","./src/users/users.service.ts","./src/users/users.controller.ts","./src/users/users.module.ts","./src/auth/auth.service.ts","./src/auth/auth.controller.ts","./src/auth/auth.module.ts","./src/billing/billing.service.ts","./src/billing/billing.controller.ts","./src/billing/billing.module.ts","./node_modules/bullmq/dist/esm/classes/async-fifo-queue.d.ts","./node_modules/bullmq/dist/esm/interfaces/backoff-options.d.ts","./node_modules/bullmq/dist/esm/types/keep-jobs.d.ts","./node_modules/bullmq/dist/esm/interfaces/parent-options.d.ts","./node_modules/cron-parser/types/common.d.ts","./node_modules/cron-parser/types/index.d.ts","./node_modules/bullmq/dist/esm/interfaces/repeat-options.d.ts","./node_modules/bullmq/dist/esm/interfaces/base-job-options.d.ts","./node_modules/bullmq/dist/esm/types/deduplication-options.d.ts","./node_modules/bullmq/dist/esm/types/job-options.d.ts","./node_modules/bullmq/dist/esm/types/job-progress.d.ts","./node_modules/bullmq/dist/esm/interfaces/parent.d.ts","./node_modules/bullmq/dist/esm/interfaces/job-json.d.ts","./node_modules/bullmq/dist/esm/types/job-json-sandbox.d.ts","./node_modules/bullmq/dist/esm/interfaces/minimal-job.d.ts","./node_modules/bullmq/dist/esm/types/backoff-strategy.d.ts","./node_modules/bullmq/dist/esm/classes/backoffs.d.ts","./node_modules/bullmq/dist/esm/types/repeat-strategy.d.ts","./node_modules/bullmq/dist/esm/interfaces/advanced-options.d.ts","./node_modules/bullmq/dist/esm/enums/parent-command.d.ts","./node_modules/bullmq/dist/esm/interfaces/child-message.d.ts","./node_modules/bullmq/dist/esm/interfaces/redis-client.d.ts","./node_modules/bullmq/dist/esm/interfaces/connection.d.ts","./node_modules/bullmq/dist/esm/types/database-type.d.ts","./node_modules/bullmq/dist/esm/types/finished-status.d.ts","./node_modules/bullmq/dist/esm/types/job-scheduler-template-options.d.ts","./node_modules/bullmq/dist/esm/types/job-type.d.ts","./node_modules/bullmq/dist/esm/types/index.d.ts","./node_modules/bullmq/node_modules/ioredis/built/types.d.ts","./node_modules/bullmq/node_modules/ioredis/built/command.d.ts","./node_modules/bullmq/node_modules/ioredis/built/scanstream.d.ts","./node_modules/bullmq/node_modules/ioredis/built/utils/rediscommander.d.ts","./node_modules/bullmq/node_modules/ioredis/built/transaction.d.ts","./node_modules/bullmq/node_modules/ioredis/built/utils/commander.d.ts","./node_modules/bullmq/node_modules/ioredis/built/connectors/abstractconnector.d.ts","./node_modules/bullmq/node_modules/ioredis/built/connectors/connectorconstructor.d.ts","./node_modules/bullmq/node_modules/ioredis/built/connectors/sentinelconnector/types.d.ts","./node_modules/bullmq/node_modules/ioredis/built/connectors/sentinelconnector/sentineliterator.d.ts","./node_modules/bullmq/node_modules/ioredis/built/connectors/sentinelconnector/index.d.ts","./node_modules/bullmq/node_modules/ioredis/built/connectors/standaloneconnector.d.ts","./node_modules/bullmq/node_modules/ioredis/built/redis/redisoptions.d.ts","./node_modules/bullmq/node_modules/ioredis/built/cluster/util.d.ts","./node_modules/bullmq/node_modules/ioredis/built/cluster/clusteroptions.d.ts","./node_modules/bullmq/node_modules/ioredis/built/cluster/index.d.ts","./node_modules/denque/index.d.ts","./node_modules/bullmq/node_modules/ioredis/built/subscriptionset.d.ts","./node_modules/bullmq/node_modules/ioredis/built/datahandler.d.ts","./node_modules/bullmq/node_modules/ioredis/built/redis.d.ts","./node_modules/bullmq/node_modules/ioredis/built/pipeline.d.ts","./node_modules/bullmq/node_modules/ioredis/built/index.d.ts","./node_modules/bullmq/dist/esm/interfaces/redis-options.d.ts","./node_modules/bullmq/dist/esm/enums/child-command.d.ts","./node_modules/bullmq/dist/esm/enums/error-code.d.ts","./node_modules/bullmq/dist/esm/enums/metrics-time.d.ts","./node_modules/bullmq/dist/esm/enums/telemetry-attributes.d.ts","./node_modules/bullmq/dist/esm/enums/index.d.ts","./node_modules/bullmq/dist/esm/interfaces/telemetry.d.ts","./node_modules/bullmq/dist/esm/interfaces/queue-options.d.ts","./node_modules/bullmq/dist/esm/interfaces/flow-job.d.ts","./node_modules/bullmq/dist/esm/interfaces/ioredis-events.d.ts","./node_modules/bullmq/dist/esm/interfaces/job-scheduler-json.d.ts","./node_modules/bullmq/dist/esm/interfaces/lock-manager-worker-context.d.ts","./node_modules/bullmq/dist/esm/interfaces/metrics-options.d.ts","./node_modules/bullmq/dist/esm/interfaces/metrics.d.ts","./node_modules/bullmq/dist/esm/classes/queue-keys.d.ts","./node_modules/bullmq/dist/esm/interfaces/script-queue-context.d.ts","./node_modules/bullmq/dist/esm/interfaces/minimal-queue.d.ts","./node_modules/bullmq/dist/esm/interfaces/parent-message.d.ts","./node_modules/bullmq/dist/esm/interfaces/queue-meta.d.ts","./node_modules/bullmq/dist/esm/interfaces/rate-limiter-options.d.ts","./node_modules/bullmq/dist/esm/interfaces/redis-streams.d.ts","./node_modules/bullmq/dist/esm/interfaces/repeatable-job.d.ts","./node_modules/bullmq/dist/esm/interfaces/repeatable-options.d.ts","./node_modules/bullmq/dist/esm/interfaces/retry-options.d.ts","./node_modules/bullmq/dist/esm/interfaces/sandboxed-job.d.ts","./node_modules/bullmq/dist/esm/interfaces/sandboxed-job-processor.d.ts","./node_modules/bullmq/dist/esm/interfaces/sandboxed-options.d.ts","./node_modules/bullmq/dist/esm/interfaces/worker-options.d.ts","./node_modules/bullmq/dist/esm/interfaces/receiver.d.ts","./node_modules/bullmq/dist/esm/interfaces/index.d.ts","./node_modules/bullmq/dist/esm/classes/child.d.ts","./node_modules/bullmq/dist/esm/classes/child-pool.d.ts","./node_modules/bullmq/dist/esm/classes/child-processor.d.ts","./node_modules/bullmq/dist/esm/classes/errors/connection-closed-error.d.ts","./node_modules/bullmq/dist/esm/classes/errors/delayed-error.d.ts","./node_modules/bullmq/dist/esm/classes/errors/rate-limit-error.d.ts","./node_modules/bullmq/dist/esm/classes/errors/unrecoverable-error.d.ts","./node_modules/bullmq/dist/esm/classes/errors/waiting-children-error.d.ts","./node_modules/bullmq/dist/esm/classes/errors/waiting-error.d.ts","./node_modules/bullmq/dist/esm/classes/errors/index.d.ts","./node_modules/bullmq/dist/esm/classes/scripts.d.ts","./node_modules/bullmq/dist/esm/classes/redis-connection.d.ts","./node_modules/bullmq/dist/esm/classes/queue-base.d.ts","./node_modules/bullmq/dist/esm/classes/queue-events.d.ts","./node_modules/bullmq/dist/esm/classes/job.d.ts","./node_modules/bullmq/dist/esm/classes/flow-producer.d.ts","./node_modules/bullmq/dist/esm/classes/ioredis-client.d.ts","./node_modules/bullmq/dist/esm/classes/node-redis-client.d.ts","./node_modules/bullmq/dist/esm/classes/bun-redis-client.d.ts","./node_modules/bullmq/dist/esm/classes/job-scheduler.d.ts","./node_modules/node-abort-controller/index.d.ts","./node_modules/bullmq/dist/esm/classes/abort-controller.d.ts","./node_modules/bullmq/dist/esm/classes/lock-manager.d.ts","./node_modules/bullmq/dist/esm/classes/queue-events-producer.d.ts","./node_modules/bullmq/dist/esm/classes/queue-getters.d.ts","./node_modules/bullmq/dist/esm/classes/repeat.d.ts","./node_modules/bullmq/dist/esm/classes/queue.d.ts","./node_modules/bullmq/dist/esm/classes/sandbox.d.ts","./node_modules/bullmq/dist/esm/types/processor.d.ts","./node_modules/bullmq/dist/esm/classes/worker.d.ts","./node_modules/bullmq/dist/esm/classes/index.d.ts","./node_modules/bullmq/dist/esm/utils/index.d.ts","./node_modules/bullmq/dist/esm/utils/create-scripts.d.ts","./node_modules/bullmq/dist/esm/index.d.ts","./src/channels/channels.service.ts","./src/channels/channels.controller.ts","./src/channels/channels.module.ts","./src/certification/certification.service.ts","./src/certification/certification.controller.ts","./src/certification/certification.module.ts","./src/dictionaries/dictionaries.service.ts","./src/dictionaries/dictionaries.controller.ts","./src/dictionaries/dictionaries.module.ts","./node_modules/@nestjs/core/adapters/http-adapter.d.ts","./node_modules/@nestjs/platform-express/interfaces/nest-express-body-parser-options.interface.d.ts","./node_modules/@nestjs/platform-express/interfaces/nest-express-body-parser.interface.d.ts","./node_modules/@nestjs/platform-express/interfaces/serve-static-options.interface.d.ts","./node_modules/@nestjs/platform-express/adapters/express-adapter.d.ts","./node_modules/@nestjs/platform-express/adapters/index.d.ts","./node_modules/@nestjs/platform-express/interfaces/nest-express-application.interface.d.ts","./node_modules/@nestjs/platform-express/interfaces/index.d.ts","./node_modules/@nestjs/platform-express/multer/interfaces/multer-options.interface.d.ts","./node_modules/@nestjs/platform-express/multer/interceptors/any-files.interceptor.d.ts","./node_modules/@nestjs/platform-express/multer/interceptors/file-fields.interceptor.d.ts","./node_modules/@nestjs/platform-express/multer/interceptors/file.interceptor.d.ts","./node_modules/@nestjs/platform-express/multer/interceptors/files.interceptor.d.ts","./node_modules/@nestjs/platform-express/multer/interceptors/no-files.interceptor.d.ts","./node_modules/@nestjs/platform-express/multer/interceptors/index.d.ts","./node_modules/@nestjs/platform-express/multer/interfaces/files-upload-module.interface.d.ts","./node_modules/@nestjs/platform-express/multer/interfaces/index.d.ts","./node_modules/@nestjs/platform-express/multer/multer.module.d.ts","./node_modules/@nestjs/platform-express/multer/index.d.ts","./node_modules/@nestjs/platform-express/index.d.ts","./node_modules/minio/dist/main/internal/copy-conditions.d.ts","./node_modules/minio/dist/main/internal/type.d.ts","./node_modules/minio/dist/main/helpers.d.ts","./node_modules/minio/dist/main/credentials.d.ts","./node_modules/minio/dist/main/credentialprovider.d.ts","./node_modules/eventemitter3/index.d.ts","./node_modules/minio/dist/main/notification.d.ts","./node_modules/minio/dist/main/internal/extensions.d.ts","./node_modules/minio/dist/main/internal/post-policy.d.ts","./node_modules/minio/dist/main/internal/s3-endpoints.d.ts","./node_modules/minio/dist/main/internal/xml-parser.d.ts","./node_modules/minio/dist/main/internal/client.d.ts","./node_modules/minio/dist/main/errors.d.ts","./node_modules/minio/dist/main/iamawsprovider.d.ts","./node_modules/minio/dist/main/minio.d.ts","./src/files/object-storage.service.ts","./src/files/files.service.ts","./src/files/files.controller.ts","./src/files/files.module.ts","./src/health.controller.ts","./src/prisma/prisma.module.ts","./src/operations/operations.service.ts","./src/operations/admin-operations.controller.ts","./src/operations/client-operations.controller.ts","./src/operations/operations.module.ts","./src/risk-review/risk-review.service.ts","./src/risk-review/admin-risk-review.controller.ts","./src/risk-review/client-risk-review.controller.ts","./src/risk-review/risk-review.module.ts","./node_modules/ioredis/built/types.d.ts","./node_modules/ioredis/built/command.d.ts","./node_modules/ioredis/built/scanstream.d.ts","./node_modules/ioredis/built/utils/rediscommander.d.ts","./node_modules/ioredis/built/transaction.d.ts","./node_modules/ioredis/built/utils/commander.d.ts","./node_modules/ioredis/built/connectors/abstractconnector.d.ts","./node_modules/ioredis/built/connectors/connectorconstructor.d.ts","./node_modules/ioredis/built/connectors/sentinelconnector/types.d.ts","./node_modules/ioredis/built/connectors/sentinelconnector/sentineliterator.d.ts","./node_modules/ioredis/built/connectors/sentinelconnector/index.d.ts","./node_modules/ioredis/built/connectors/standaloneconnector.d.ts","./node_modules/ioredis/built/redis/redisoptions.d.ts","./node_modules/ioredis/built/cluster/util.d.ts","./node_modules/ioredis/built/cluster/clusteroptions.d.ts","./node_modules/ioredis/built/cluster/index.d.ts","./node_modules/ioredis/built/subscriptionset.d.ts","./node_modules/ioredis/built/datahandler.d.ts","./node_modules/ioredis/built/tracing.d.ts","./node_modules/ioredis/built/redis.d.ts","./node_modules/ioredis/built/pipeline.d.ts","./node_modules/ioredis/built/index.d.ts","./src/send-chain/send-chain.service.ts","./src/send-chain/admin-send-chain.controller.ts","./src/send-chain/client-send-chain.controller.ts","./src/send-chain/gateway-events.controller.ts","./src/send-chain/send-chain.module.ts","./src/sms-config/sms-config.service.ts","./src/sms-config/admin-sms-config.controller.ts","./src/sms-config/client-sms-config.controller.ts","./src/sms-config/sms-config.module.ts","./src/tenants/tenants.service.ts","./src/tenants/tenants.controller.ts","./src/tenants/tenants.module.ts","./src/app.module.ts","./node_modules/@nestjs/core/adapters/index.d.ts","./node_modules/@nestjs/core/inspector/interfaces/edge.interface.d.ts","./node_modules/@nestjs/core/inspector/interfaces/entrypoint.interface.d.ts","./node_modules/@nestjs/core/inspector/interfaces/extras.interface.d.ts","./node_modules/@nestjs/core/inspector/interfaces/node.interface.d.ts","./node_modules/@nestjs/core/injector/settlement-signal.d.ts","./node_modules/@nestjs/core/injector/injector.d.ts","./node_modules/@nestjs/core/inspector/interfaces/serialized-graph-metadata.interface.d.ts","./node_modules/@nestjs/core/inspector/interfaces/serialized-graph-json.interface.d.ts","./node_modules/@nestjs/core/inspector/serialized-graph.d.ts","./node_modules/@nestjs/core/injector/opaque-key-factory/interfaces/module-opaque-key-factory.interface.d.ts","./node_modules/@nestjs/core/injector/compiler.d.ts","./node_modules/@nestjs/core/injector/modules-container.d.ts","./node_modules/@nestjs/core/injector/container.d.ts","./node_modules/@nestjs/core/injector/instance-links-host.d.ts","./node_modules/@nestjs/core/injector/abstract-instance-resolver.d.ts","./node_modules/@nestjs/core/injector/module-ref.d.ts","./node_modules/@nestjs/core/injector/module.d.ts","./node_modules/@nestjs/core/injector/instance-wrapper.d.ts","./node_modules/@nestjs/core/router/interfaces/exclude-route-metadata.interface.d.ts","./node_modules/@nestjs/core/application-config.d.ts","./node_modules/@nestjs/core/constants.d.ts","./node_modules/@nestjs/core/discovery/discovery-module.d.ts","./node_modules/@nestjs/core/discovery/discovery-service.d.ts","./node_modules/@nestjs/core/discovery/index.d.ts","./node_modules/@nestjs/core/helpers/http-adapter-host.d.ts","./node_modules/@nestjs/core/exceptions/base-exception-filter.d.ts","./node_modules/@nestjs/core/exceptions/index.d.ts","./node_modules/@nestjs/core/helpers/context-id-factory.d.ts","./node_modules/@nestjs/common/interfaces/exceptions/exception-filter-metadata.interface.d.ts","./node_modules/@nestjs/core/exceptions/exceptions-handler.d.ts","./node_modules/@nestjs/core/router/router-proxy.d.ts","./node_modules/@nestjs/core/helpers/context-creator.d.ts","./node_modules/@nestjs/core/exceptions/base-exception-filter-context.d.ts","./node_modules/@nestjs/common/interfaces/exceptions/rpc-exception-filter-metadata.interface.d.ts","./node_modules/@nestjs/common/interfaces/exceptions/index.d.ts","./node_modules/@nestjs/core/exceptions/external-exception-filter.d.ts","./node_modules/@nestjs/core/exceptions/external-exceptions-handler.d.ts","./node_modules/@nestjs/core/exceptions/external-exception-filter-context.d.ts","./node_modules/@nestjs/core/guards/constants.d.ts","./node_modules/@nestjs/core/helpers/execution-context-host.d.ts","./node_modules/@nestjs/core/guards/guards-consumer.d.ts","./node_modules/@nestjs/core/guards/guards-context-creator.d.ts","./node_modules/@nestjs/core/guards/index.d.ts","./node_modules/@nestjs/core/interceptors/interceptors-consumer.d.ts","./node_modules/@nestjs/core/interceptors/interceptors-context-creator.d.ts","./node_modules/@nestjs/core/interceptors/index.d.ts","./node_modules/@nestjs/common/enums/route-paramtypes.enum.d.ts","./node_modules/@nestjs/core/pipes/params-token-factory.d.ts","./node_modules/@nestjs/core/pipes/pipes-consumer.d.ts","./node_modules/@nestjs/core/pipes/pipes-context-creator.d.ts","./node_modules/@nestjs/core/pipes/index.d.ts","./node_modules/@nestjs/core/helpers/context-utils.d.ts","./node_modules/@nestjs/core/injector/inquirer/inquirer-constants.d.ts","./node_modules/@nestjs/core/injector/inquirer/index.d.ts","./node_modules/@nestjs/core/interfaces/module-definition.interface.d.ts","./node_modules/@nestjs/core/interfaces/module-override.interface.d.ts","./node_modules/@nestjs/core/inspector/interfaces/enhancer-metadata-cache-entry.interface.d.ts","./node_modules/@nestjs/core/inspector/graph-inspector.d.ts","./node_modules/@nestjs/core/metadata-scanner.d.ts","./node_modules/@nestjs/core/scanner.d.ts","./node_modules/@nestjs/core/injector/instance-loader.d.ts","./node_modules/@nestjs/core/injector/lazy-module-loader/lazy-module-loader-options.interface.d.ts","./node_modules/@nestjs/core/injector/lazy-module-loader/lazy-module-loader.d.ts","./node_modules/@nestjs/core/injector/index.d.ts","./node_modules/@nestjs/core/helpers/interfaces/external-handler-metadata.interface.d.ts","./node_modules/@nestjs/core/helpers/interfaces/params-metadata.interface.d.ts","./node_modules/@nestjs/core/helpers/external-context-creator.d.ts","./node_modules/@nestjs/core/helpers/index.d.ts","./node_modules/@nestjs/core/inspector/initialize-on-preview.allowlist.d.ts","./node_modules/@nestjs/core/inspector/partial-graph.host.d.ts","./node_modules/@nestjs/core/inspector/index.d.ts","./node_modules/@nestjs/core/middleware/route-info-path-extractor.d.ts","./node_modules/@nestjs/core/middleware/routes-mapper.d.ts","./node_modules/@nestjs/core/middleware/builder.d.ts","./node_modules/@nestjs/core/middleware/index.d.ts","./node_modules/@nestjs/core/nest-application-context.d.ts","./node_modules/@nestjs/core/nest-application.d.ts","./node_modules/@nestjs/common/interfaces/microservices/nest-microservice-options.interface.d.ts","./node_modules/@nestjs/core/nest-factory.d.ts","./node_modules/@nestjs/core/repl/repl.d.ts","./node_modules/@nestjs/core/repl/index.d.ts","./node_modules/@nestjs/core/router/interfaces/routes.interface.d.ts","./node_modules/@nestjs/core/router/interfaces/index.d.ts","./node_modules/@nestjs/core/router/request/request-constants.d.ts","./node_modules/@nestjs/core/router/request/index.d.ts","./node_modules/@nestjs/core/router/router-module.d.ts","./node_modules/@nestjs/core/router/index.d.ts","./node_modules/@nestjs/core/services/reflector.service.d.ts","./node_modules/@nestjs/core/services/index.d.ts","./node_modules/@nestjs/core/index.d.ts","./src/main.ts"],"fileIdsList":[[429,493,501,505,508,510,511,512,524,614],[429,493,501,505,508,510,511,512,524,613],[429,493,501,505,508,510,511,512,524],[318,429,493,501,505,508,510,511,512,524],[418,429,493,501,505,508,510,511,512,524],[67,319,320,321,322,323,324,325,326,327,328,329,330,331,429,493,501,505,508,510,511,512,524],[270,304,429,493,501,505,508,510,511,512,524],[277,429,493,501,505,508,510,511,512,524],[267,318,418,429,493,501,505,508,510,511,512,524],[336,337,338,339,340,341,342,344,429,493,501,505,508,510,511,512,524],[272,429,493,501,505,508,510,511,512,524],[318,418,429,493,501,505,508,510,511,512,524],[272,343,429,493,501,505,508,510,511,512,524],[332,335,345,429,493,501,505,508,510,511,512,524],[333,334,429,493,501,505,508,510,511,512,524],[308,429,493,501,505,508,510,511,512,524],[272,273,274,275,429,493,501,505,508,510,511,512,524],[348,429,493,501,505,508,510,511,512,524],[290,347,429,493,501,505,508,510,511,512,524],[347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,429,493,501,505,508,510,511,512,524],[377,429,493,501,505,508,510,511,512,524],[374,375,429,493,501,505,508,510,511,512,524],[373,376,429,493,501,505,508,510,511,512,524,529],[66,276,318,346,370,373,378,385,410,415,417,429,493,501,505,508,510,511,512,524],[72,270,429,493,501,505,508,510,511,512,524],[71,429,493,501,505,508,510,511,512,524],[72,262,263,429,493,501,505,508,510,511,512,524,876,881],[262,270,429,493,501,505,508,510,511,512,524],[71,261,429,493,501,505,508,510,511,512,524],[270,398,429,493,501,505,508,510,511,512,524],[264,400,429,493,501,505,508,510,511,512,524],[261,265,429,493,501,505,508,510,511,512,524],[265,429,493,501,505,508,510,511,512,524],[71,318,429,493,501,505,508,510,511,512,524],[269,270,429,493,501,505,508,510,511,512,524],[282,429,493,501,505,508,510,511,512,524],[284,285,286,287,288,429,493,501,505,508,510,511,512,524],[276,429,493,501,505,508,510,511,512,524],[276,277,296,429,493,501,505,508,510,511,512,524],[290,291,297,298,299,429,493,501,505,508,510,511,512,524],[68,69,70,71,72,262,263,264,265,266,267,268,269,270,271,277,282,283,289,296,300,301,302,304,312,313,314,315,316,317,429,493,501,505,508,510,511,512,524],[295,429,493,501,505,508,510,511,512,524],[278,279,280,281,429,493,501,505,508,510,511,512,524],[270,278,279,429,493,501,505,508,510,511,512,524],[270,276,277,429,493,501,505,508,510,511,512,524],[270,280,429,493,501,505,508,510,511,512,524],[270,308,429,493,501,505,508,510,511,512,524],[303,305,306,307,308,309,310,311,429,493,501,505,508,510,511,512,524],[68,270,429,493,501,505,508,510,511,512,524],[304,429,493,501,505,508,510,511,512,524],[68,270,303,307,309,429,493,501,505,508,510,511,512,524],[279,429,493,501,505,508,510,511,512,524],[305,429,493,501,505,508,510,511,512,524],[270,304,305,306,429,493,501,505,508,510,511,512,524],[294,429,493,501,505,508,510,511,512,524],[270,274,294,295,312,313,429,493,501,505,508,510,511,512,524],[292,293,295,429,493,501,505,508,510,511,512,524],[266,268,277,283,297,314,315,318,429,493,501,505,508,510,511,512,524],[72,261,266,268,271,314,315,429,493,501,505,508,510,511,512,524],[275,429,493,501,505,508,510,511,512,524],[261,429,493,501,505,508,510,511,512,524],[294,318,379,383,429,493,501,505,508,510,511,512,524],[383,384,429,493,501,505,508,510,511,512,524],[318,379,429,493,501,505,508,510,511,512,524],[318,379,380,429,493,501,505,508,510,511,512,524],[380,381,429,493,501,505,508,510,511,512,524],[380,381,382,429,493,501,505,508,510,511,512,524],[271,429,493,501,505,508,510,511,512,524],[388,389,390,429,493,501,505,508,510,511,512,524],[388,429,493,501,505,508,510,511,512,524],[390,391,392,394,395,396,429,493,501,505,508,510,511,512,524],[387,429,493,501,505,508,510,511,512,524],[390,393,429,493,501,505,508,510,511,512,524],[390,391,392,394,395,429,493,501,505,508,510,511,512,524],[271,388,390,394,429,493,501,505,508,510,511,512,524],[386,397,402,403,404,405,406,407,408,409,429,493,501,505,508,510,511,512,524],[271,318,402,429,493,501,505,508,510,511,512,524],[271,393,429,493,501,505,508,510,511,512,524],[271,393,418,429,493,501,505,508,510,511,512,524],[264,270,271,393,398,399,400,401,429,493,501,505,508,510,511,512,524],[261,318,398,399,411,429,493,501,505,508,510,511,512,524],[318,398,429,493,501,505,508,510,511,512,524],[413,429,493,501,505,508,510,511,512,524],[346,411,429,493,501,505,508,510,511,512,524],[411,412,414,429,493,501,505,508,510,511,512,524],[294,429,493,501,505,508,510,511,512,524,543],[294,371,372,429,493,501,505,508,510,511,512,524],[303,429,493,501,505,508,510,511,512,524],[276,318,429,493,501,505,508,510,511,512,524],[416,429,493,501,505,508,510,511,512,524],[418,429,493,501,505,508,510,511,512,524,554],[261,420,425,429,493,501,505,508,510,511,512,524],[419,425,429,493,501,505,508,510,511,512,524,554,555,556,559],[425,429,493,501,505,508,510,511,512,524],[426,429,493,501,505,508,510,511,512,524,552],[420,426,429,493,501,505,508,510,511,512,524,553],[421,422,423,424,429,493,501,505,508,510,511,512,524],[429,493,501,505,508,510,511,512,524,557,558],[425,429,493,501,505,508,510,511,512,524,554,560],[429,493,501,505,508,510,511,512,524,560],[296,318,418,429,493,501,505,508,510,511,512,524],[429,493,501,505,508,510,511,512,524,763],[318,418,429,493,501,505,508,510,511,512,524,865,866],[343,429,493,501,505,508,510,511,512,524],[418,429,493,501,505,508,510,511,512,524,859,864,865],[429,493,501,505,508,510,511,512,524,869,870],[72,318,429,493,501,505,508,510,511,512,524,860,865,879],[418,429,493,501,505,508,510,511,512,524,847,872],[71,418,429,493,501,505,508,510,511,512,524,873,876],[318,429,493,501,505,508,510,511,512,524,860,865,867,878,880,884],[71,429,493,501,505,508,510,511,512,524,882,883],[429,493,501,505,508,510,511,512,524,873],[261,318,418,429,493,501,505,508,510,511,512,524,887],[318,418,429,493,501,505,508,510,511,512,524,860,865,867,879],[429,493,501,505,508,510,511,512,524,886,888,889],[318,429,493,501,505,508,510,511,512,524,865],[429,493,501,505,508,510,511,512,524,865],[318,418,429,493,501,505,508,510,511,512,524,887],[71,318,418,429,493,501,505,508,510,511,512,524],[318,418,429,493,501,505,508,510,511,512,524,859,860,865,885,887,890,893,898,899,912,913],[261,429,493,501,505,508,510,511,512,524,763],[429,493,501,505,508,510,511,512,524,872,875,914],[429,493,501,505,508,510,511,512,524,899,911],[66,429,493,501,505,508,510,511,512,524,847,867,868,871,874,906,911,915,918,922,923,924,926,928,934,936],[318,418,429,493,501,505,508,510,511,512,524,853,861,864,865],[318,429,493,501,505,508,510,511,512,524,857],[295,318,343,418,429,493,501,505,508,510,511,512,524,856,857,858,859,864,865,867,937],[429,493,501,505,508,510,511,512,524,859,860,863,865,901,910],[318,418,429,493,501,505,508,510,511,512,524,852,864,865],[429,493,501,505,508,510,511,512,524,900],[418,429,493,501,505,508,510,511,512,524,860,865],[418,429,493,501,505,508,510,511,512,524,853,860,864,905],[318,343,418,429,493,501,505,508,510,511,512,524,852,864],[418,429,493,501,505,508,510,511,512,524,858,859,863,903,907,908,909],[418,429,493,501,505,508,510,511,512,524,853,860,861,862,864,865],[318,343,429,493,501,505,508,510,511,512,524,860,863,865],[261,429,493,501,505,508,510,511,512,524,864],[270,303,309,429,493,501,505,508,510,511,512,524],[429,493,501,505,508,510,511,512,524,849,850,851,860,864,865,904],[429,493,501,505,508,510,511,512,524,856,905,916,917],[343,418,429,493,501,505,508,510,511,512,524,865],[343,418,429,493,501,505,508,510,511,512,524],[429,493,501,505,508,510,511,512,524,848,849,850,851,854,856],[429,493,501,505,508,510,511,512,524,853],[429,493,501,505,508,510,511,512,524,855,856],[418,429,493,501,505,508,510,511,512,524,848,849,850,851,854,855],[429,493,501,505,508,510,511,512,524,891,892],[318,429,493,501,505,508,510,511,512,524,860,865,867,879],[429,493,501,505,508,510,511,512,524,902],[301,429,493,501,505,508,510,511,512,524],[282,318,429,493,501,505,508,510,511,512,524,919,920],[429,493,501,505,508,510,511,512,524,921],[318,429,493,501,505,508,510,511,512,524,867],[318,429,493,501,505,508,510,511,512,524,860,867],[295,318,418,429,493,501,505,508,510,511,512,524,853,860,861,862,864,865],[294,318,418,429,493,501,505,508,510,511,512,524,847,860,867,905,923],[295,296,418,429,493,501,505,508,510,511,512,524,763,925],[429,493,501,505,508,510,511,512,524,895,896,897],[418,429,493,501,505,508,510,511,512,524,894],[429,493,501,505,508,510,511,512,524,927],[418,429,493,501,505,508,510,511,512,524,526],[429,493,501,505,508,510,511,512,524,930,932,933],[429,493,501,505,508,510,511,512,524,929],[429,493,501,505,508,510,511,512,524,931],[418,429,493,501,505,508,510,511,512,524,859,864,930],[429,493,501,505,508,510,511,512,524,877],[318,343,418,429,493,501,505,508,510,511,512,524,860,864,865,867,902,903,905,906],[429,493,501,505,508,510,511,512,524,935],[292,296,318,418,429,493,501,505,507,508,509,510,511,512,524,763,764,765,766],[429,493,501,505,508,510,511,512,524,767],[429,493,501,505,508,510,511,512,524,768,770,781],[429,493,501,505,508,510,511,512,524,764,765,769],[292,418,429,493,501,505,507,508,509,510,511,512,524,764,765,766],[429,493,501,505,507,508,510,511,512,524],[429,493,501,505,508,510,511,512,524,777,779,780],[418,429,493,501,505,508,510,511,512,524,771],[429,493,501,505,508,510,511,512,524,772,773,774,775,776],[318,429,493,501,505,508,510,511,512,524,771],[429,493,501,505,508,510,511,512,524,778],[418,429,493,501,505,508,510,511,512,524,778],[418,429,493,501,505,508,510,511,512,524,564,565],[429,493,501,505,508,510,511,512,524,589],[429,493,501,505,508,510,511,512,524,564,565],[429,493,501,505,508,510,511,512,524,564],[418,429,493,501,505,508,510,511,512,524,564,565,579],[418,429,493,501,505,508,510,511,512,524,579,582],[418,429,493,501,505,508,510,511,512,524,564],[429,493,501,505,508,510,511,512,524,582],[429,493,501,505,508,510,511,512,524,562,563,566,567,568,569,570,571,572,573,574,575,576,577,578,580,581,583,584,585,586,587,588,590,591,592],[429,493,501,505,508,510,511,512,524,564,586,597],[66,429,493,501,505,508,510,511,512,524,593,597,598,599,605,608,609],[429,493,501,505,508,510,511,512,524,564,595,596],[418,429,493,501,505,508,510,511,512,524,564,579],[429,493,501,505,508,510,511,512,524,564,594],[297,418,429,493,501,505,508,510,511,512,524,597],[429,493,501,505,508,510,511,512,524,600,601,602,603,604],[429,493,501,505,508,510,511,512,524,606,607],[429,493,501,505,508,510,511,512,524,618,625],[429,493,501,505,508,510,511,512,524,615],[429,493,501,505,508,510,511,512,524,612],[429,493,501,505,508,510,511,512,524,617],[429,490,491,493,501,505,508,510,511,512,524],[429,492,493,501,505,508,510,511,512,524],[493,501,505,508,510,511,512,524],[429,493,501,505,508,510,511,512,524,533],[429,493,494,499,501,504,505,508,510,511,512,514,524,529,542],[429,493,494,495,501,504,505,508,510,511,512,524],[429,493,496,501,505,508,510,511,512,524,543],[429,493,497,498,501,505,508,510,511,512,515,524],[429,493,498,501,505,508,510,511,512,524,529,539],[429,493,499,501,504,505,508,510,511,512,514,524],[429,492,493,500,501,505,508,510,511,512,524],[429,493,501,502,505,508,510,511,512,524],[429,493,501,503,504,505,508,510,511,512,524],[429,492,493,501,504,505,508,510,511,512,524],[429,493,501,504,505,506,508,510,511,512,524,529,542],[429,493,501,504,505,506,508,510,511,512,524,529,531,533],[429,480,493,501,504,505,507,508,510,511,512,514,524,529,542],[429,493,501,504,505,507,508,510,511,512,514,524,529,539,542],[429,493,501,505,507,508,509,510,511,512,524,529,539,542],[427,428,429,430,431,432,433,434,435,436,437,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550],[429,493,501,504,505,508,510,511,512,524],[429,493,501,505,508,510,512,524],[429,493,501,505,508,510,511,512,513,524,542],[429,493,501,504,505,508,510,511,512,514,524,529],[429,493,501,505,508,510,511,512,515,524],[429,493,501,505,508,510,511,512,516,524],[429,493,501,504,505,508,510,511,512,519,524],[429,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549],[429,493,501,505,508,510,511,512,521,524],[429,493,501,505,508,510,511,512,522,524],[429,493,498,501,505,508,510,511,512,514,524,533],[429,493,501,504,505,508,510,511,512,524,525],[429,493,501,505,508,510,511,512,524,526,543,546],[429,493,501,504,505,508,510,511,512,524,529,532,533],[429,493,501,505,508,510,511,512,524,530,533],[429,493,501,505,508,510,511,512,524,531],[429,493,501,505,508,510,511,512,524,533,543],[429,493,501,505,508,510,511,512,524,534],[429,490,493,501,505,508,510,511,512,524,529,536,542],[429,493,501,505,508,510,511,512,524,529,535],[429,493,501,504,505,508,510,511,512,524,537,538],[429,493,501,505,508,510,511,512,524,537,538],[429,493,498,501,505,508,510,511,512,514,524,529,539],[429,493,501,505,508,510,511,512,524,540],[429,493,501,505,508,510,511,512,514,524,541],[429,493,501,505,507,508,510,511,512,522,524,542],[429,493,501,505,508,510,511,512,524,543,544],[429,493,498,501,505,508,510,511,512,524,544],[429,493,501,505,508,510,511,512,524,529,545],[429,493,501,505,508,510,511,512,513,524,546],[429,493,501,505,508,510,511,512,524,547],[429,493,496,501,505,508,510,511,512,524],[429,493,498,501,505,508,510,511,512,524],[429,493,501,505,508,510,511,512,524,543],[429,480,493,501,505,508,510,511,512,524],[429,493,501,505,508,510,511,512,524,542],[429,493,501,505,508,510,511,512,524,548],[429,493,501,505,508,510,511,512,519,524],[429,493,501,505,508,510,511,512,524,538],[429,480,493,501,504,505,506,508,510,511,512,519,524,529,533,542,545,546,548],[429,493,501,505,508,510,511,512,524,529,549],[429,493,501,505,508,510,511,512,524,531,550],[429,493,501,504,505,508,510,511,512,524,529,539,551,619,620,623,624,625],[429,493,501,505,508,510,511,512,524,625],[429,493,501,505,508,510,511,512,524,740],[429,493,501,505,508,510,511,512,524,641,654,655],[429,493,501,505,508,510,511,512,524,661],[429,493,501,505,508,510,511,512,524,719,720],[429,493,501,505,508,510,511,512,524,667,719],[429,493,494,501,504,505,508,510,511,512,524,548,719],[429,493,501,505,508,510,511,512,524,723,724,725,726,727,728],[429,493,501,504,505,508,510,511,512,524,704,719,731,734],[429,493,501,505,508,510,511,512,524,640,656,704,720,721,722,729,730,731,732,733,734,735,736,737,738,739,742,743,744,745,746,747,749],[429,493,501,505,508,510,511,512,524,661,689],[429,493,501,505,508,510,511,512,524,667,719,731,732,734],[429,493,501,505,508,510,511,512,524,667,719,730,733],[429,493,501,505,508,510,511,512,524,719,741],[429,493,501,504,505,508,510,511,512,524,663,695,704,719,730,731,734],[429,493,501,505,508,510,511,512,524,719,731,732],[429,493,501,505,508,510,511,512,524,667,719,731,732],[429,493,501,505,508,510,511,512,524,667,719,732,734],[429,493,501,505,508,510,511,512,524,667,719,731,734,739,744,745],[429,493,501,504,505,508,510,511,512,524,661,667,719],[429,493,501,505,508,510,511,512,524,721,734],[429,493,501,505,508,510,511,512,524,542,667,719,731,732,734,739,741,742,745,748],[429,493,501,505,508,510,511,512,524,659,691,692,693,694],[429,493,501,505,508,510,511,512,524,667,695,719,748,750,751,752],[429,493,501,505,508,510,511,512,524,655,657],[429,493,501,505,508,510,511,512,524,641,642,643,646],[429,493,501,505,508,510,511,512,524,659],[429,493,501,504,505,508,510,511,512,524,661],[429,493,501,505,508,510,511,512,524,667,697],[429,493,501,505,508,510,511,512,524,641,643,646,647,651,652,654,658,660,661,662,690,696,697,698,699,700,701,702,703,705,706,707,708,709,710,711,712,713,714,715,716,717,718],[429,493,501,505,508,510,511,512,524,649,650,651],[429,493,501,505,508,510,511,512,524,667],[429,493,501,505,508,510,511,512,524,695,696],[429,493,501,505,508,510,511,512,524,643,649,650,651,652,653],[429,493,501,505,508,510,511,512,524,662,694,696,705],[429,493,501,505,508,510,511,512,524,652,691],[429,493,501,505,508,510,511,512,524,649],[429,493,501,505,508,510,511,512,524,647,658,690,696],[429,493,501,505,508,510,511,512,524,529],[429,493,501,505,508,510,511,512,524,645],[429,493,501,505,508,510,511,512,524,714],[429,493,501,505,508,510,511,512,524,654,667],[429,493,494,501,505,508,510,511,512,524,548],[429,493,501,505,508,510,511,512,524,662,663,697,704],[429,493,501,505,508,510,511,512,524,695],[429,493,501,505,508,510,511,512,524,642,658,696,697,702,709,716],[429,493,501,505,508,510,511,512,524,654],[429,493,501,505,508,510,511,512,524,642,648,649,650,653,655,657,663,664,665,666],[429,493,501,505,508,510,511,512,524,652],[429,493,501,505,508,510,511,512,524,647,648],[429,493,501,505,508,510,511,512,524,664],[429,493,501,505,508,510,511,512,524,734],[429,493,501,505,508,510,511,512,524,646],[429,493,501,505,508,510,511,512,524,719,730],[429,493,501,505,508,510,511,512,524,667,689,695,719,741],[429,493,501,505,508,510,511,512,524,551,673,680,681],[429,493,501,504,505,508,510,511,512,524,551,668,669,670,672,673,681,682,687],[429,493,501,505,508,510,511,512,524,551],[429,493,501,505,508,510,511,512,524,551,668],[429,493,501,505,508,510,511,512,524,668],[429,493,501,505,508,510,511,512,524,674],[429,493,501,504,505,508,510,511,512,524,539,551,668,674,676,677,682],[429,493,501,505,508,510,511,512,524,676],[429,493,501,505,508,510,511,512,524,680],[429,493,501,505,508,510,511,512,514,524,539,551,668,674],[429,493,501,504,505,508,510,511,512,524,551,668,684,685],[429,493,501,505,508,510,511,512,524,668,669,670,671,674,678,679,680,681,682,683,687,688],[429,493,501,505,508,510,511,512,524,669,673,683,687],[429,493,501,504,505,508,510,511,512,524,551,668,669,670,672,673,680,683,684,686],[429,493,501,505,508,510,511,512,524,673,675,678,679],[429,493,501,505,508,510,511,512,524,529,551],[429,493,501,505,508,510,511,512,524,669],[429,493,501,505,508,510,511,512,524,671],[429,493,501,505,508,510,511,512,514,524,539,551],[429,493,501,505,508,510,511,512,524,668,669,671],[429,493,501,505,508,510,511,512,524,644],[429,493,501,505,508,510,511,512,524,551,817,824,825],[429,493,501,504,505,508,510,511,512,524,551,812,813,814,816,817,825,826,831],[429,493,501,505,508,510,511,512,524,551,812],[429,493,501,505,508,510,511,512,524,812],[429,493,501,505,508,510,511,512,524,818],[429,493,501,504,505,508,510,511,512,524,539,551,812,818,820,821,826],[429,493,501,505,508,510,511,512,524,820],[429,493,501,505,508,510,511,512,524,824],[429,493,501,505,508,510,511,512,514,524,539,551,812,818],[429,493,501,504,505,508,510,511,512,524,551,684,812,828],[429,493,501,505,508,510,511,512,524,812,813,814,815,818,822,823,824,825,826,827,830,831,832],[429,493,501,505,508,510,511,512,524,813,817,827,831],[429,493,501,504,505,508,510,511,512,524,551,684,812,813,814,816,817,824,827,829,830],[429,493,501,505,508,510,511,512,524,817,819,822,823],[429,493,501,505,508,510,511,512,524,813],[429,493,501,505,508,510,511,512,524,815],[429,493,501,505,508,510,511,512,524,812,813,815],[429,493,501,505,508,510,511,512,524,786],[429,493,501,505,508,510,511,512,524,784],[429,493,501,505,507,508,510,511,512,524,551,786,787],[429,493,501,505,507,508,509,510,511,512,524,529,551,783,784,785,787,789,790,791,792,793],[429,493,501,505,508,510,511,512,524,784,794],[429,493,501,505,507,508,510,511,512,524,529,551,783,785],[429,493,501,505,507,508,510,511,512,524,551,784,785],[429,493,501,505,508,510,511,512,524,783,784,785,789,791,792,794,795,796],[429,493,501,505,508,510,511,512,524,788,794],[429,493,501,505,508,510,511,512,524,620,621,622],[429,493,501,505,508,510,511,512,524,529,620],[73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,89,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,129,130,131,132,133,134,135,136,137,138,139,140,142,143,144,145,146,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,192,193,194,196,205,207,208,209,210,211,212,214,215,217,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,429,493,501,505,508,510,511,512,524],[118,429,493,501,505,508,510,511,512,524],[74,77,429,493,501,505,508,510,511,512,524],[76,429,493,501,505,508,510,511,512,524],[76,77,429,493,501,505,508,510,511,512,524],[73,74,75,77,429,493,501,505,508,510,511,512,524],[74,76,77,234,429,493,501,505,508,510,511,512,524],[77,429,493,501,505,508,510,511,512,524],[73,76,118,429,493,501,505,508,510,511,512,524],[76,77,234,429,493,501,505,508,510,511,512,524],[76,242,429,493,501,505,508,510,511,512,524],[74,76,77,429,493,501,505,508,510,511,512,524],[86,429,493,501,505,508,510,511,512,524],[109,429,493,501,505,508,510,511,512,524],[130,429,493,501,505,508,510,511,512,524],[76,77,118,429,493,501,505,508,510,511,512,524],[77,125,429,493,501,505,508,510,511,512,524],[76,77,118,136,429,493,501,505,508,510,511,512,524],[76,77,136,429,493,501,505,508,510,511,512,524],[77,177,429,493,501,505,508,510,511,512,524],[77,118,429,493,501,505,508,510,511,512,524],[73,77,195,429,493,501,505,508,510,511,512,524],[73,77,196,429,493,501,505,508,510,511,512,524],[218,429,493,501,505,508,510,511,512,524],[202,204,429,493,501,505,508,510,511,512,524],[213,429,493,501,505,508,510,511,512,524],[202,429,493,501,505,508,510,511,512,524],[73,77,195,202,203,429,493,501,505,508,510,511,512,524],[195,196,204,429,493,501,505,508,510,511,512,524],[216,429,493,501,505,508,510,511,512,524],[73,77,202,203,204,429,493,501,505,508,510,511,512,524],[75,76,77,429,493,501,505,508,510,511,512,524],[73,77,429,493,501,505,508,510,511,512,524],[74,76,196,197,198,199,429,493,501,505,508,510,511,512,524],[118,196,197,198,199,429,493,501,505,508,510,511,512,524],[196,198,429,493,501,505,508,510,511,512,524],[76,197,198,200,201,205,429,493,501,505,508,510,511,512,524],[73,76,429,493,501,505,508,510,511,512,524],[77,220,429,493,501,505,508,510,511,512,524],[78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,119,120,121,122,123,124,126,127,128,129,130,131,132,133,134,135,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,429,493,501,505,508,510,511,512,524],[206,429,493,501,505,508,510,511,512,524],[429,444,447,450,451,493,501,505,508,510,511,512,524,542],[429,447,493,501,505,508,510,511,512,524,529,542],[429,447,451,493,501,505,508,510,511,512,524,542],[429,441,493,501,505,508,510,511,512,524],[429,445,493,501,505,508,510,511,512,524],[429,443,444,447,493,501,505,508,510,511,512,524,542],[429,493,501,505,508,510,511,512,514,524,539],[429,441,493,501,505,508,510,511,512,524,551],[429,443,447,493,501,505,508,510,511,512,514,524,542],[429,438,439,440,442,446,493,501,504,505,508,510,511,512,524,529,542],[429,447,456,464,493,501,505,508,510,511,512,524],[429,439,445,493,501,505,508,510,511,512,524],[429,447,474,475,493,501,505,508,510,511,512,524],[429,439,442,447,493,501,505,508,510,511,512,524,533,542,551],[429,447,493,501,505,508,510,511,512,524],[429,443,447,493,501,505,508,510,511,512,524,542],[429,438,493,501,505,508,510,511,512,524],[429,441,442,443,445,446,447,448,449,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,475,476,477,478,479,493,501,505,508,510,511,512,524],[429,447,467,470,493,501,505,508,510,511,512,524],[429,447,456,457,458,493,501,505,508,510,511,512,524],[429,445,447,457,459,493,501,505,508,510,511,512,524],[429,446,493,501,505,508,510,511,512,524],[429,439,441,447,493,501,505,508,510,511,512,524],[429,447,451,457,459,493,501,505,508,510,511,512,524],[429,451,493,501,505,508,510,511,512,524],[429,445,447,450,493,501,505,508,510,511,512,524,542],[429,439,443,447,456,493,501,505,508,510,511,512,524],[429,447,467,493,501,505,508,510,511,512,524],[429,459,493,501,505,508,510,511,512,524],[429,439,443,447,451,493,501,505,508,510,511,512,524],[429,441,447,474,493,501,505,508,510,511,512,524,533,548,551],[418,429,493,501,505,508,510,511,512,524,561,630,633,636,639,756,759,762,801,802,803,807,811,838,842,845],[418,429,493,501,505,508,510,511,512,524,610,611,628],[418,429,493,501,505,508,510,511,512,524,628,629],[418,429,493,501,505,508,510,511,512,524,616,627],[418,429,493,501,505,508,510,511,512,524,610,634],[418,429,493,501,505,508,510,511,512,524,633,634,635],[418,429,493,498,501,505,508,510,511,512,524,631],[418,429,493,501,505,508,510,511,512,524,610,611,637],[418,429,493,501,505,508,510,511,512,524,637,638],[418,429,493,501,505,508,510,511,512,524,610,611,757],[418,429,493,501,505,508,510,511,512,524,757,758],[418,429,493,501,505,508,510,511,512,524,610,754],[418,429,493,501,505,508,510,511,512,524,754,755],[418,429,493,498,501,505,508,510,511,512,524,616,627,753],[418,429,493,501,505,508,510,511,512,524,610,611,760],[418,429,493,501,505,508,510,511,512,524,760,761],[418,429,493,501,505,508,510,511,512,524,610,611,782,799],[418,429,493,501,505,508,510,511,512,524,798,799,800],[418,429,493,498,501,505,508,510,511,512,524,616,627,798],[418,429,493,501,505,506,508,510,511,512,516,524,561,797],[418,429,493,501,505,508,510,511,512,524,610],[66,429,493,501,505,508,510,511,512,524,610,846,937],[418,429,493,501,505,508,510,511,512,524,610,804],[418,429,493,501,505,508,510,511,512,524,610,611,804],[418,429,493,501,505,508,510,511,512,524,803,804,805,806],[418,429,493,501,505,508,510,511,512,524,627],[418,429,493,501,505,508,510,511,512,524,616,626],[418,429,493,501,505,508,510,511,512,524,610,808],[418,429,493,501,505,508,510,511,512,524,610,611,808],[418,429,493,501,505,508,510,511,512,524,803,808,809,810],[418,429,493,498,501,505,508,510,511,512,524,616,627],[418,429,493,501,505,508,510,511,512,524,610,834],[418,429,493,501,505,508,510,511,512,524,610,611,834],[418,429,493,501,505,508,510,511,512,524,639,803,811,834,835,836,837],[418,429,493,498,501,505,508,510,511,512,524,538,627,637,753,808,833],[418,429,493,501,505,508,510,511,512,524,610,839],[418,429,493,501,505,508,510,511,512,524,610,611,839],[418,429,493,501,505,508,510,511,512,524,839,840,841],[418,429,493,501,505,508,510,511,512,524,610,843],[418,429,493,501,505,508,510,511,512,524,843,844],[418,429,493,501,505,508,510,511,512,524,610,611,631],[418,429,493,501,505,508,510,511,512,524,631,632]],"fileInfos":[{"version":"bcd24271a113971ba9eb71ff8cb01bc6b0f872a85c23fdbe5d93065b375933cd","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f88bedbeb09c6f5a6645cb24c7c55f1aa22d19ae96c8e6959cbd8b85a707bc6","impliedFormat":1},{"version":"7fe93b39b810eadd916be8db880dd7f0f7012a5cc6ffb62de8f62a2117fa6f1f","impliedFormat":1},{"version":"bb0074cc08b84a2374af33d8bf044b80851ccc9e719a5e202eacf40db2c31600","impliedFormat":1},{"version":"1a7daebe4f45fb03d9ec53d60008fbf9ac45a697fdc89e4ce218bc94b94f94d6","impliedFormat":1},{"version":"f94b133a3cb14a288803be545ac2683e0d0ff6661bcd37e31aaaec54fc382aed","impliedFormat":1},{"version":"f59d0650799f8782fd74cf73c19223730c6d1b9198671b1c5b3a38e1188b5953","impliedFormat":1},{"version":"8a15b4607d9a499e2dbeed9ec0d3c0d7372c850b2d5f1fb259e8f6d41d468a84","impliedFormat":1},{"version":"26e0fe14baee4e127f4365d1ae0b276f400562e45e19e35fd2d4c296684715e6","impliedFormat":1},{"version":"d6b1eba8496bdd0eed6fc8a685768fe01b2da4a0388b5fe7df558290bffcf32f","affectsGlobalScope":true,"impliedFormat":1},{"version":"7f57fc4404ff020bc45b9c620aff2b40f700b95fe31164024c453a5e3c163c54","impliedFormat":1},{"version":"7f57fc4404ff020bc45b9c620aff2b40f700b95fe31164024c453a5e3c163c54","impliedFormat":1},{"version":"2a2de5b9459b3fc44decd9ce6100b72f1b002ef523126c1d3d8b2a4a63d74d78","affectsGlobalScope":true,"impliedFormat":1},{"version":"f13f4b465c99041e912db5c44129a94588e1aafee35a50eab51044833f50b4ee","affectsGlobalScope":true,"impliedFormat":1},{"version":"eadcffda2aa84802c73938e589b9e58248d74c59cb7fcbca6474e3435ac15504","affectsGlobalScope":true,"impliedFormat":1},{"version":"105ba8ff7ba746404fe1a2e189d1d3d2e0eb29a08c18dded791af02f29fb4711","affectsGlobalScope":true,"impliedFormat":1},{"version":"00343ca5b2e3d48fa5df1db6e32ea2a59afab09590274a6cccb1dbae82e60c7c","affectsGlobalScope":true,"impliedFormat":1},{"version":"ebd9f816d4002697cb2864bea1f0b70a103124e18a8cd9645eeccc09bdf80ab4","affectsGlobalScope":true,"impliedFormat":1},{"version":"2c1afac30a01772cd2a9a298a7ce7706b5892e447bb46bdbeef720f7b5da77ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"7b0225f483e4fa685625ebe43dd584bb7973bbd84e66a6ba7bbe175ee1048b4f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c0a4b8ac6ce74679c1da2b3795296f5896e31c38e888469a8e0f99dc3305de60","affectsGlobalScope":true,"impliedFormat":1},{"version":"3084a7b5f569088e0146533a00830e206565de65cae2239509168b11434cd84f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5079c53f0f141a0698faa903e76cb41cd664e3efb01cc17a5c46ec2eb0bef42","affectsGlobalScope":true,"impliedFormat":1},{"version":"32cafbc484dea6b0ab62cf8473182bbcb23020d70845b406f80b7526f38ae862","affectsGlobalScope":true,"impliedFormat":1},{"version":"fca4cdcb6d6c5ef18a869003d02c9f0fd95df8cfaf6eb431cd3376bc034cad36","affectsGlobalScope":true,"impliedFormat":1},{"version":"b93ec88115de9a9dc1b602291b85baf825c85666bf25985cc5f698073892b467","affectsGlobalScope":true,"impliedFormat":1},{"version":"f5c06dcc3fe849fcb297c247865a161f995cc29de7aa823afdd75aaaddc1419b","affectsGlobalScope":true,"impliedFormat":1},{"version":"b77e16112127a4b169ef0b8c3a4d730edf459c5f25fe52d5e436a6919206c4d7","affectsGlobalScope":true,"impliedFormat":1},{"version":"fbffd9337146eff822c7c00acbb78b01ea7ea23987f6c961eba689349e744f8c","affectsGlobalScope":true,"impliedFormat":1},{"version":"a995c0e49b721312f74fdfb89e4ba29bd9824c770bbb4021d74d2bf560e4c6bd","affectsGlobalScope":true,"impliedFormat":1},{"version":"c7b3542146734342e440a84b213384bfa188835537ddbda50d30766f0593aff9","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce6180fa19b1cccd07ee7f7dbb9a367ac19c0ed160573e4686425060b6df7f57","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f02e2476bccb9dbe21280d6090f0df17d2f66b74711489415a8aa4df73c9675","affectsGlobalScope":true,"impliedFormat":1},{"version":"45e3ab34c1c013c8ab2dc1ba4c80c780744b13b5676800ae2e3be27ae862c40c","affectsGlobalScope":true,"impliedFormat":1},{"version":"805c86f6cca8d7702a62a844856dbaa2a3fd2abef0536e65d48732441dde5b5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e42e397f1a5a77994f0185fd1466520691456c772d06bf843e5084ceb879a0ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"f4c2b41f90c95b1c532ecc874bd3c111865793b23aebcc1c3cbbabcd5d76ffb0","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab26191cfad5b66afa11b8bf935ef1cd88fabfcb28d30b2dfa6fad877d050332","affectsGlobalScope":true,"impliedFormat":1},{"version":"2088bc26531e38fb05eedac2951480db5309f6be3fa4a08d2221abb0f5b4200d","affectsGlobalScope":true,"impliedFormat":1},{"version":"cb9d366c425fea79716a8fb3af0d78e6b22ebbab3bd64d25063b42dc9f531c1e","affectsGlobalScope":true,"impliedFormat":1},{"version":"500934a8089c26d57ebdb688fc9757389bb6207a3c8f0674d68efa900d2abb34","affectsGlobalScope":true,"impliedFormat":1},{"version":"689da16f46e647cef0d64b0def88910e818a5877ca5379ede156ca3afb780ac3","affectsGlobalScope":true,"impliedFormat":1},{"version":"bc21cc8b6fee4f4c2440d08035b7ea3c06b3511314c8bab6bef7a92de58a2593","affectsGlobalScope":true,"impliedFormat":1},{"version":"7ca53d13d2957003abb47922a71866ba7cb2068f8d154877c596d63c359fed25","affectsGlobalScope":true,"impliedFormat":1},{"version":"54725f8c4df3d900cb4dac84b64689ce29548da0b4e9b7c2de61d41c79293611","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5594bc3076ac29e6c1ebda77939bc4c8833de72f654b6e376862c0473199323","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f3eb332c2d73e729f3364fcc0c2b375e72a121e8157d25a82d67a138c83a95c","affectsGlobalScope":true,"impliedFormat":1},{"version":"6f4427f9642ce8d500970e4e69d1397f64072ab73b97e476b4002a646ac743b1","affectsGlobalScope":true,"impliedFormat":1},{"version":"48915f327cd1dea4d7bd358d9dc7732f58f9e1626a29cc0c05c8c692419d9bb7","affectsGlobalScope":true,"impliedFormat":1},{"version":"b7bf9377723203b5a6a4b920164df22d56a43f593269ba6ae1fdc97774b68855","affectsGlobalScope":true,"impliedFormat":1},{"version":"db9709688f82c9e5f65a119c64d835f906efe5f559d08b11642d56eb85b79357","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b25b8c874acd1a4cf8444c3617e037d444d19080ac9f634b405583fd10ce1f7","affectsGlobalScope":true,"impliedFormat":1},{"version":"37be57d7c90cf1f8112ee2636a068d8fd181289f82b744160ec56a7dc158a9f5","affectsGlobalScope":true,"impliedFormat":1},{"version":"a917a49ac94cd26b754ab84e113369a75d1a47a710661d7cd25e961cc797065f","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d3261badeb7843d157ef3e6f5d1427d0eeb0af0cf9df84a62cfd29fd47ac86e","affectsGlobalScope":true,"impliedFormat":1},{"version":"195daca651dde22f2167ac0d0a05e215308119a3100f5e6268e8317d05a92526","affectsGlobalScope":true,"impliedFormat":1},{"version":"8b11e4285cd2bb164a4dc09248bdec69e9842517db4ca47c1ba913011e44ff2f","affectsGlobalScope":true,"impliedFormat":1},{"version":"0508571a52475e245b02bc50fa1394065a0a3d05277fbf5120c3784b85651799","affectsGlobalScope":true,"impliedFormat":1},{"version":"8f9af488f510c3015af3cc8c267a9e9d96c4dd38a1fdff0e11dc5a544711415b","affectsGlobalScope":true,"impliedFormat":1},{"version":"fc611fea8d30ea72c6bbfb599c9b4d393ce22e2f5bfef2172534781e7d138104","affectsGlobalScope":true,"impliedFormat":1},{"version":"f128dae7c44d8f35ee42e0a437000a57c9f06cc04f8b4fb42eebf44954d53dc8","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ecb8e347cb6b2a8927c09b86263663289418df375f5e68e11a0ae683776978f","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ce14b81c5cc821994aa8ec1d42b220dd41b27fcc06373bce3958af7421b77d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3a048b3e9302ef9a34ef4ebb9aecfb28b66abb3bce577206a79fee559c230da","affectsGlobalScope":true,"impliedFormat":1},{"version":"01a30f9e8582b369075c0808df71121e6855cb06fd8d3d39511d9ebb66405205","impliedFormat":1},{"version":"8d6d51a5118d000ed3bfe6e1dd1335bebfff3fef23cd2af2f84a24d30f90cc90","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d8dedbec739bc79642c1e96e9bfc0b83b25b104a0486aebf016fc7b85b39f48","impliedFormat":1},{"version":"e89535c3ec439608bcd0f68af555d0e5ddf121c54abe69343549718bd7506b9c","impliedFormat":1},{"version":"622a984b60c294ffb2f9152cf1d4d12e91d2b733d820eec949cf54d63a3c1025","impliedFormat":1},{"version":"81aae92abdeaccd9c1723cef39232c90c1aed9d9cf199e6e2a523b7d8e058a11","impliedFormat":1},{"version":"a63a6c6806a1e519688ef7bd8ca57be912fc0764485119dbd923021eb4e79665","impliedFormat":1},{"version":"75b57b109d774acca1e151df21cf5cb54c7a1df33a273f0457b9aee4ebd36fb9","impliedFormat":1},{"version":"073ca26c96184db9941b5ec0ddea6981c9b816156d9095747809e524fdd90e35","impliedFormat":1},{"version":"e41d17a2ec23306d953cda34e573ed62954ca6ea9b8c8b74e013d07a6886ce47","impliedFormat":1},{"version":"241bd4add06f06f0699dcd58f3b334718d85e3045d9e9d4fa556f11f4d1569c1","impliedFormat":1},{"version":"2ae3787e1498b20aad1b9c2ee9ea517ec30e89b70d242d8e3e52d1e091039695","impliedFormat":1},{"version":"c7c72c4cffb1bc83617eefed71ed68cc89df73cab9e19507ccdecb3e72b4967e","affectsGlobalScope":true,"impliedFormat":1},{"version":"b8bff8a60af0173430b18d9c3e5c443eaa3c515617210c0c7b3d2e1743c19ecb","impliedFormat":1},{"version":"38b38db08e7121828294dec10957a7a9ff263e33e2a904b346516d4a4acca482","impliedFormat":1},{"version":"a76ebdf2579e68e4cfe618269c47e5a12a4e045c2805ed7f7ab37af8daa6b091","impliedFormat":1},{"version":"8a2aaea564939c22be05d665cc955996721bad6d43148f8fa21ae8f64afecd37","impliedFormat":1},{"version":"e59d36b7b6e8ba2dd36d032a5f5c279d2460968c8b4e691ca384f118fb09b52a","impliedFormat":1},{"version":"e96885c0684c9042ec72a9a43ef977f6b4b4a2728f4b9e737edcbaa0c74e5bf6","impliedFormat":1},{"version":"95950a187596e206d32d5d9c7b932901088c65ed8f9040e614aa8e321e0225ef","impliedFormat":1},{"version":"89e061244da3fc21b7330f4bd32f47c1813dd4d7f1dc3d0883d88943f035b993","impliedFormat":1},{"version":"e46558c2e04d06207b080138678020448e7fc201f3d69c2601b0d1456105f29a","impliedFormat":1},{"version":"71549375db52b1163411dba383b5f4618bdf35dc57fa327a1c7d135cf9bf67d1","impliedFormat":1},{"version":"7e6b2d61d6215a4e82ea75bc31a80ebb8ad0c2b37a60c10c70dd671e8d9d6d5d","impliedFormat":1},{"version":"78bea05df2896083cca28ed75784dde46d4b194984e8fc559123b56873580a23","impliedFormat":1},{"version":"5dd04ced37b7ea09f29d277db11f160df7fd73ba8b9dba86cb25552e0653a637","impliedFormat":1},{"version":"f74b81712e06605677ae1f061600201c425430151f95b5ef4d04387ad7617e6a","impliedFormat":1},{"version":"9a72847fcf4ac937e352d40810f7b7aec7422d9178451148296cf1aa19467620","impliedFormat":1},{"version":"3ae18f60e0b96fa1e025059b7d25b3247ba4dcb5f4372f6d6e67ce2adac74eac","impliedFormat":1},{"version":"2b9260f44a2e071450ae82c110f5dc8f330c9e5c3e85567ed97248330f2bf639","impliedFormat":1},{"version":"4f196e13684186bda6f5115fc4677a87cf84a0c9c4fc17b8f51e0984f3697b6d","impliedFormat":1},{"version":"61419f2c5822b28c1ea483258437c1faab87d00c6f84481aa22afb3380d8e9a4","impliedFormat":1},{"version":"64479aee03812264e421c0bf5104a953ca7b02740ba80090aead1330d0effe91","impliedFormat":1},{"version":"0521108c9f8ddb17654a0a54dae6ba9667c99eddccfd6af5748113e022d1c37a","impliedFormat":1},{"version":"c5570e504be103e255d80c60b56c367bf45d502ca52ee35c55dec882f6563b5c","impliedFormat":1},{"version":"ee764e6e9a7f2b987cc1a2c0a9afd7a8f4d5ebc4fdb66ad557a7f14a8c2bd320","impliedFormat":1},{"version":"0520b5093712c10c6ef23b5fea2f833bf5481771977112500045e5ea7e8e2b69","impliedFormat":1},{"version":"5c3cf26654cf762ac4d7fd7b83f09acfe08eef88d2d6983b9a5a423cb4004ca3","impliedFormat":1},{"version":"e60fa19cf7911c1623b891155d7eb6b7e844e9afdf5738e3b46f3b687730a2bd","impliedFormat":1},{"version":"b1fd72ff2bb0ba91bb588f3e5329f8fc884eb859794f1c4657a2bfa122ae54d0","impliedFormat":1},{"version":"6cf42a4f3cfec648545925d43afaa8bb364ac10a839ffed88249da109361b275","impliedFormat":1},{"version":"d7058e75920120b142a9d57be25562a3cd9a936269fd52908505f530105f2ec4","impliedFormat":1},{"version":"6df52b70d7f7702202f672541a5f4a424d478ee5be51a9d37b8ccbe1dbf3c0f2","impliedFormat":1},{"version":"0ca7f997e9a4d8985e842b7c882e521b6f63233c4086e9fe79dd7a9dc4742b5e","impliedFormat":1},{"version":"91046b5c6b55d3b194c81fd4df52f687736fad3095e9d103ead92bb64dc160ee","impliedFormat":1},{"version":"db5704fdad56c74dfc5941283c1182ed471bd17598209d3ac4a49faa72e43cfc","impliedFormat":1},{"version":"758e8e89559b02b81bc0f8fd395b17ad5aff75490c862cbe369bb1a3d1577c40","impliedFormat":1},{"version":"2ee64342c077b1868f1834c063f575063051edd6e2964257d34aad032d6b657c","impliedFormat":1},{"version":"6f6b4b3d670b6a5f0e24ea001c1b3d36453c539195e875687950a178f1730fa7","impliedFormat":1},{"version":"a472a1d3f25ce13a1d44911cd3983956ac040ce2018e155435ea34afb25f864c","impliedFormat":1},{"version":"b48b83a86dd9cfe36f8776b3ff52fcd45b0e043c0538dc4a4b149ba45fe367b9","impliedFormat":1},{"version":"792de5c062444bd2ee0413fb766e57e03cce7cdaebbfc52fc0c7c8e95069c96b","impliedFormat":1},{"version":"a79e3e81094c7a04a885bad9b049c519aace53300fb8a0fe4f26727cb5a746ce","impliedFormat":1},{"version":"93181bac0d90db185bb730c95214f6118ae997fe836a98a49664147fbcaf1988","impliedFormat":1},{"version":"8a4e89564d8ea66ad87ee3762e07540f9f0656a62043c910d819b4746fc429c5","impliedFormat":1},{"version":"b9011d99942889a0f95e120d06b698c628b0b6fdc3e6b7ecb459b97ed7d5bcc6","impliedFormat":1},{"version":"4d639cbbcc2f8f9ce6d55d5d503830d6c2556251df332dc5255d75af53c8a0e7","impliedFormat":1},{"version":"cdb48277f600ab5f429ecf1c5ea046683bc6b9f73f3deab9a100adac4b34969c","impliedFormat":1},{"version":"75be84956a29040a1afbe864c0a7a369dfdb739380072484eff153905ef867ee","impliedFormat":1},{"version":"b06b4adc2ae03331a92abd1b19af8eb91ec2bf8541747ee355887a167d53145e","impliedFormat":1},{"version":"c54166a85bd60f86d1ebb90ce0117c0ecb850b8a33b366691629fdf26f1bbbd8","impliedFormat":1},{"version":"0d417c15c5c635384d5f1819cc253a540fe786cc3fda32f6a2ae266671506a21","impliedFormat":1},{"version":"80f23f1d60fbed356f726b3b26f9d348dddbb34027926d10d59fad961e70a730","impliedFormat":1},{"version":"cb59317243a11379a101eb2f27b9df1022674c3df1df0727360a0a3f963f523b","impliedFormat":1},{"version":"cc20bb2227dd5de0aab0c8d697d1572f8000550e62c7bf5c92f212f657dd88c5","impliedFormat":1},{"version":"06b8a7d46195b6b3980e523ef59746702fd210b71681a83a5cf73799623621f9","impliedFormat":1},{"version":"860e4405959f646c101b8005a191298b2381af8f33716dc5f42097e4620608f8","impliedFormat":1},{"version":"f7e32adf714b8f25d3c1783473abec3f2e82d5724538d8dcf6f51baaaff1ca7a","impliedFormat":1},{"version":"d0da80c845999a16c24d0783033fb5366ada98df17867c98ad433ede05cd87fd","impliedFormat":1},{"version":"bfbf80f9cd4558af2d7b2006065340aaaced15947d590045253ded50aabb9bc5","impliedFormat":1},{"version":"fd9a991b51870325e46ebb0e6e18722d313f60cd8e596e645ec5ac15b96dbf4e","impliedFormat":1},{"version":"c3bd2b94e4298f81743d92945b80e9b56c1cdfb2bef43c149b7106a2491b1fc9","impliedFormat":1},{"version":"a246cce57f558f9ebaffd55c1e5673da44ea603b4da3b2b47eb88915d30a9181","impliedFormat":1},{"version":"d993eacc103c5a065227153c9aae8acea3a4322fe1a169ee7c70b77015bf0bb2","impliedFormat":1},{"version":"fc2b03d0c042aa1627406e753a26a1eaad01b3c496510a78016822ef8d456bb6","impliedFormat":1},{"version":"063c7ebbe756f0155a8b453f410ca6b76ffa1bbc1048735bcaf9c7c81a1ce35f","impliedFormat":1},{"version":"314e402cd481370d08f63051ae8b8c8e6370db5ee3b8820eeeaaf8d722a6dac6","impliedFormat":1},{"version":"9669075ac38ce36b638b290ba468233980d9f38bdc62f0519213b2fd3e2552ec","impliedFormat":1},{"version":"4d123de012c24e2f373925100be73d50517ac490f9ed3578ac82d0168bfbd303","impliedFormat":1},{"version":"656c9af789629aa36b39092bee3757034009620439d9a39912f587538033ce28","impliedFormat":1},{"version":"3ac3f4bdb8c0905d4c3035d6f7fb20118c21e8a17bee46d3735195b0c2a9f39f","impliedFormat":1},{"version":"1f453e6798ed29c86f703e9b41662640d4f2e61337007f27ac1c616f20093f69","impliedFormat":1},{"version":"af43b7871ff21c62bf1a54ec5c488e31a8d3408d5b51ff2e9f8581b6c55f2fc7","impliedFormat":1},{"version":"70550511d25cbb0b6a64dcac7fffc3c1397fd4cbeb6b23ccc7f9b794ab8a6954","impliedFormat":1},{"version":"af0fbf08386603a62f2a78c42d998c90353b1f1d22e05a384545f7accf881e0a","impliedFormat":1},{"version":"cefc20054d20b85b534206dbcedd509bb74f87f3d8bc45c58c7be3a76caa45e1","impliedFormat":1},{"version":"ad6eee4877d0f7e5244d34bc5026fd6e9cf8e66c5c79416b73f9f6ebf132f924","impliedFormat":1},{"version":"4888fd2bcfee9a0ce89d0df860d233e0cee8ee9c479b6bd5a5d5f9aae98342fe","impliedFormat":1},{"version":"f4749c102ced952aa6f40f0b579865429c4869f6d83df91000e98005476bee87","impliedFormat":1},{"version":"56654d2c5923598384e71cb808fac2818ca3f07dd23bb018988a39d5e64f268b","impliedFormat":1},{"version":"8b6719d3b9e65863da5390cb26994602c10a315aa16e7d70778a63fee6c4c079","impliedFormat":1},{"version":"05f56cd4b929977d18df8f3d08a4c929a2592ef5af083e79974b20a063f30940","impliedFormat":1},{"version":"547d3c406a21b30e2b78629ecc0b2ddaf652d9e0bdb2d59ceebce5612906df33","impliedFormat":1},{"version":"b3a4f9385279443c3a5568ec914a9492b59a723386161fd5ef0619d9f8982f97","impliedFormat":1},{"version":"3fe66aba4fbe0c3ba196a4f9ed2a776fe99dc4d1567a558fb11693e9fcc4e6ed","impliedFormat":1},{"version":"140eef237c7db06fc5adcb5df434ee21e81ee3a6fd57e1a75b8b3750aa2df2d8","impliedFormat":1},{"version":"0944ec553e4744efae790c68807a461720cff9f3977d4911ac0d918a17c9dd99","impliedFormat":1},{"version":"cb46b38d5e791acaa243bf342b8b5f8491639847463ac965b93896d4fb0af0d9","impliedFormat":1},{"version":"7c7d9e116fe51100ff766703e6b5e4424f51ad8977fe474ddd8d0959aa6de257","impliedFormat":1},{"version":"af70a2567e586be0083df3938b6a6792e6821363d8ef559ad8d721a33a5bcdaf","impliedFormat":1},{"version":"006cff3a8bcb92d77953f49a94cd7d5272fef4ab488b9052ef82b6a1260d870b","impliedFormat":1},{"version":"7d44bfdc8ee5e9af70738ff652c622ae3ad81815e63ab49bdc593d34cb3a68e5","impliedFormat":1},{"version":"339814517abd4dbc7b5f013dfd3b5e37ef0ea914a8bbe65413ecffd668792bc6","impliedFormat":1},{"version":"34d5bc0a6958967ec237c99f980155b5145b76e6eb927c9ffc57d8680326b5d8","impliedFormat":1},{"version":"9eae79b70c9d8288032cbe1b21d0941f6bd4f315e14786b2c1d10bccc634e897","impliedFormat":1},{"version":"18ce015ed308ea469b13b17f99ce53bbb97975855b2a09b86c052eefa4aa013a","impliedFormat":1},{"version":"5a931bc4106194e474be141e0bc1046629510dc95b9a0e4b02a3783847222965","impliedFormat":1},{"version":"5e5f371bf23d5ced2212a5ff56675aefbd0c9b3f4d4fdda1b6123ac6e28f058c","impliedFormat":1},{"version":"907c17ad5a05eecb29b42b36cc8fec6437be27cc4986bb3a218e4f74f606911c","impliedFormat":1},{"version":"ce60a562cd2a92f37a88f2ddd99a3abfbc5848d7baf38c48fb8d3243701fcb75","impliedFormat":1},{"version":"a726ad2d0a98bfffbe8bc1cd2d90b6d831638c0adc750ce73103a471eb9a891c","impliedFormat":1},{"version":"f44c0c8ce58d3dacac016607a1a90e5342d830ea84c48d2e571408087ae55894","impliedFormat":1},{"version":"75a315a098e630e734d9bc932d9841b64b30f7a349a20cf4717bf93044eff113","impliedFormat":1},{"version":"9131d95e32b3d4611d4046a613e022637348f6cebfe68230d4e81b691e4761a1","impliedFormat":1},{"version":"b03aa292cfdcd4edc3af00a7dbd71136dd067ec70a7536b655b82f4dd444e857","impliedFormat":1},{"version":"b6e2b0448ced813b8c207810d96551a26e7d7bb73255eea4b9701698f78846d6","impliedFormat":1},{"version":"8ae10cd85c1bd94d2f2d17c4cbd25c068a4b2471c70c2d96434239f97040747a","impliedFormat":1},{"version":"9ed5b799c50467b0c9f81ddf544b6bcda3e34d92076d6cab183c84511e45c39f","impliedFormat":1},{"version":"b4fa87cc1833839e51c49f20de71230e259c15b2c9c3e89e4814acc1d1ef10de","impliedFormat":1},{"version":"e90ac9e4ac0326faa1bc39f37af38ace0f9d4a655cd6d147713c653139cf4928","impliedFormat":1},{"version":"ea27110249d12e072956473a86fd1965df8e1be985f3b686b4e277afefdde584","impliedFormat":1},{"version":"8776a368617ce51129b74db7d55c3373dadcce5d0701e61d106e99998922a239","impliedFormat":1},{"version":"5666075052877fe2fdddd5b16de03168076cf0f03fbca5c1d4a3b8f43cba570c","impliedFormat":1},{"version":"9108ab5af05418f599ab48186193b1b07034c79a4a212a7f73535903ba4ca249","impliedFormat":1},{"version":"bb4e2cdcadf9c9e6ee2820af23cee6582d47c9c9c13b0dca1baaffe01fbbcb5f","impliedFormat":1},{"version":"6e30d0b5a1441d831d19fe02300ab3d83726abd5141cbcc0e2993fa0efd33db4","impliedFormat":1},{"version":"423f28126b2fc8d8d6fa558035309000a1297ed24473c595b7dec52e5c7ebae5","impliedFormat":1},{"version":"fb30734f82083d4790775dae393cd004924ebcbfde49849d9430bf0f0229dd16","impliedFormat":1},{"version":"2c92b04a7a4a1cd9501e1be338bf435738964130fb2ad5bd6c339ee41224ac4c","impliedFormat":1},{"version":"c5c5f0157b41833180419dacfbd2bcce78fb1a51c136bd4bcba5249864d8b9b5","impliedFormat":1},{"version":"02ae43d5bae42efcd5a00d3923e764895ce056bca005a9f4e623aa6b4797c8af","impliedFormat":1},{"version":"db6e01f17012a9d7b610ae764f94a1af850f5d98c9c826ad61747dca0fb800bd","impliedFormat":1},{"version":"8a44b424edee7bb17dc35a558cc15f92555f14a0441205613e0e50452ab3a602","impliedFormat":1},{"version":"24a00d0f98b799e6f628373249ece352b328089c3383b5606214357e9107e7d5","impliedFormat":1},{"version":"33637e3bc64edd2075d4071c55d60b32bdb0d243652977c66c964021b6fc8066","impliedFormat":1},{"version":"0f0ad9f14dedfdca37260931fac1edf0f6b951c629e84027255512f06a6ebc4c","impliedFormat":1},{"version":"16ad86c48bf950f5a480dc812b64225ca4a071827d3d18ffc5ec1ae176399e36","impliedFormat":1},{"version":"8cbf55a11ff59fd2b8e39a4aa08e25c5ddce46e3af0ed71fb51610607a13c505","impliedFormat":1},{"version":"d5bc4544938741f5daf8f3a339bfbf0d880da9e89e79f44a6383aaf056fe0159","impliedFormat":1},{"version":"97f9169882d393e6f303f570168ca86b5fe9aab556e9a43672dae7e6bb8e6495","impliedFormat":1},{"version":"7c9adb3fcd7851497818120b7e151465406e711d6a596a71b807f3a17853cb58","impliedFormat":1},{"version":"6752d402f9282dd6f6317c8c048aaaac27295739a166eed27e00391b358fed9a","impliedFormat":1},{"version":"9fd7466b77020847dbc9d2165829796bf7ea00895b2520ff3752ffdcff53564b","impliedFormat":1},{"version":"fbfc12d54a4488c2eb166ed63bab0fb34413e97069af273210cf39da5280c8d6","impliedFormat":1},{"version":"85a84240002b7cf577cec637167f0383409d086e3c4443852ca248fc6e16711e","impliedFormat":1},{"version":"84794e3abd045880e0fadcf062b648faf982aa80cfc56d28d80120e298178626","impliedFormat":1},{"version":"053d8b827286a16a669a36ffc8ccc8acdf8cc154c096610aa12348b8c493c7b8","impliedFormat":1},{"version":"3cce4ce031710970fe12d4f7834375f5fd455aa129af4c11eb787935923ff551","impliedFormat":1},{"version":"8f62cbd3afbd6a07bb8c934294b6bfbe437021b89e53a4da7de2648ecfc7af25","impliedFormat":1},{"version":"62c3621d34fb2567c17a2c4b89914ebefbfbd1b1b875b070391a7d4f722e55dc","impliedFormat":1},{"version":"c05ac811542e0b59cb9c2e8f60e983461f0b0e39cea93e320fad447ff8e474f3","impliedFormat":1},{"version":"8e7a5b8f867b99cc8763c0b024068fb58e09f7da2c4810c12833e1ca6eb11c4f","impliedFormat":1},{"version":"132351cbd8437a463757d3510258d0fa98fd3ebef336f56d6f359cf3e177a3ce","impliedFormat":1},{"version":"df877050b04c29b9f8409aa10278d586825f511f0841d1ec41b6554f8362092b","impliedFormat":1},{"version":"33d1888c3c27d3180b7fd20bac84e97ecad94b49830d5dd306f9e770213027d1","impliedFormat":1},{"version":"ee942c58036a0de88505ffd7c129f86125b783888288c2389330168677d6347f","impliedFormat":1},{"version":"a3f317d500c30ea56d41501632cdcc376dae6d24770563a5e59c039e1c2a08ec","impliedFormat":1},{"version":"eb21ddc3a8136a12e69176531197def71dc28ffaf357b74d4bf83407bd845991","impliedFormat":1},{"version":"0c1651a159995dfa784c57b4ea9944f16bdf8d924ed2d8b3db5c25d25749a343","impliedFormat":1},{"version":"aaa13958e03409d72e179b5d7f6ec5c6cc666b7be14773ae7b6b5ee4921e52db","impliedFormat":1},{"version":"0a86e049843ad02977a94bb9cdfec287a6c5a0a4b6b5391a6648b1a122072c5a","impliedFormat":1},{"version":"40f06693e2e3e58526b713c937895c02e113552dc8ba81ecd49cdd9596567ddb","impliedFormat":1},{"version":"4ed5e1992aedb174fb8f5aa8796aa6d4dcb8bd819b4af1b162a222b680a37fa0","impliedFormat":1},{"version":"d7f4bd46a8b97232ea6f8c28012b8d2b995e55e729d11405f159d3e00c51420a","impliedFormat":1},{"version":"d604d413aff031f4bfbdae1560e54ebf503d374464d76d50a2c6ded4df525712","impliedFormat":1},{"version":"e4f4f9cf1e3ac9fd91ada072e4d428ecbf0aa6dc57138fb797b8a0ca3a1d521c","impliedFormat":1},{"version":"12bfd290936824373edda13f48a4094adee93239b9a73432db603127881a300d","impliedFormat":1},{"version":"340ceb3ea308f8e98264988a663640e567c553b8d6dc7d5e43a8f3b64f780374","impliedFormat":1},{"version":"c5a769564e530fba3ec696d0a5cff1709b9095a0bdf5b0826d940d2fc9786413","impliedFormat":1},{"version":"7124ef724c3fc833a17896f2d994c368230a8d4b235baed39aa8037db31de54f","impliedFormat":1},{"version":"5de1c0759a76e7710f76899dcae601386424eab11fb2efaf190f2b0f09c3d3d3","impliedFormat":1},{"version":"9c5ee8f7e581f045b6be979f062a61bf076d362bf89c7f966b993a23424e8b0d","impliedFormat":1},{"version":"1a11df987948a86aa1ec4867907c59bdf431f13ed2270444bf47f788a5c7f92d","impliedFormat":1},{"version":"8018dd2e95e7ce6e613ddd81672a54532614dc745520a2f9e3860ff7fb1be0ca","impliedFormat":1},{"version":"b756781cd40d465da57d1fc6a442c34ae61fe8c802d752aace24f6a43fedacee","impliedFormat":1},{"version":"0fe76167c87289ea094e01616dcbab795c11b56bad23e1ef8aba9aa37e93432a","impliedFormat":1},{"version":"3a45029dba46b1f091e8dc4d784e7be970e209cd7d4ff02bd15270a98a9ba24b","impliedFormat":1},{"version":"032c1581f921f8874cf42966f27fd04afcabbb7878fa708a8251cac5415a2a06","impliedFormat":1},{"version":"69c68ed9652842ce4b8e495d63d2cd425862104c9fb7661f72e7aa8a9ef836f8","impliedFormat":1},{"version":"0e704ee6e9fd8b6a5a7167886f4d8915f4bc22ed79f19cb7b32bd28458f50643","impliedFormat":1},{"version":"06f62a14599a68bcde148d1efd60c2e52e8fa540cc7dcfa4477af132bb3de271","impliedFormat":1},{"version":"904a96f84b1bcee9a7f0f258d17f8692e6652a0390566515fe6741a5c6db8c1c","impliedFormat":1},{"version":"11f19ce32d21222419cecab448fa335017ebebf4f9e5457c4fa9df42fa2dcca7","impliedFormat":1},{"version":"2e8ee2cbb5e9159764e2189cf5547aebd0e6b0d9a64d479397bb051cd1991744","impliedFormat":1},{"version":"1b0471d75f5adb7f545c1a97c02a0f825851b95fe6e069ac6ecaa461b8bb321d","impliedFormat":1},{"version":"1d157c31a02b1e5cca9bc495b3d8d39f4b42b409da79f863fb953fbe3c7d4884","impliedFormat":1},{"version":"07baaceaec03d88a4b78cb0651b25f1ae0322ac1aa0b555ae3749a79a41cba86","impliedFormat":1},{"version":"619a132f634b4ebe5b4b4179ea5870f62f2cb09916a25957bff17b408de8b56d","impliedFormat":1},{"version":"f60fa446a397eb1aead9c4e568faf2df8068b4d0306ebc075fb4be16ed26b741","impliedFormat":1},{"version":"f3cb784be4d9e91f966a0b5052a098d9b53b0af0d341f690585b0cc05c6ca412","impliedFormat":1},{"version":"350f63439f8fe2e06c97368ddc7fb6d6c676d54f59520966f7dbbe6a4586014e","impliedFormat":1},{"version":"eba613b9b357ac8c50a925fa31dc7e65ff3b95a07efbaa684b624f143d8d34ba","impliedFormat":1},{"version":"45b74185005ed45bec3f07cac6e4d68eaf02ead9ff5a66721679fb28020e5e7c","impliedFormat":1},{"version":"0f6199602df09bdb12b95b5434f5d7474b1490d2cd8cc036364ab3ba6fd24263","impliedFormat":1},{"version":"c8ca7fd9ec7a3ec82185bfc8213e4a7f63ae748fd6fced931741d23ef4ea3c0f","impliedFormat":1},{"version":"5c6a8a3c2a8d059f0592d4eab59b062210a1c871117968b10797dee36d991ef7","impliedFormat":1},{"version":"ad77fd25ece8e09247040826a777dc181f974d28257c9cd5acb4921b51967bd8","impliedFormat":1},{"version":"795a08ae4e193f345073b49f68826ab6a9b280400b440906e4ec5c237ae777e6","impliedFormat":1},{"version":"8153df63cf65122809db17128e5918f59d6bb43a371b5218f4430c4585f64085","impliedFormat":1},{"version":"a8150bc382dd12ce58e00764d2366e1d59a590288ee3123af8a4a2cb4ef7f9df","impliedFormat":1},{"version":"5adfaf2f9f33957264ad199a186456a4676b2724ed700fc313ff945d03372169","impliedFormat":1},{"version":"d5c41a741cd408c34cb91f84468f70e9bda3dfeabf33251a61039b3cdb8b22d8","impliedFormat":1},{"version":"a20c3e0fe86a1d8fc500a0e9afec9a872ad3ab5b746ceb3dd7118c6d2bff4328","impliedFormat":1},{"version":"cbaf4a4aa8a8c02aa681c5870d5c69127974de29b7e01df570edec391a417959","impliedFormat":1},{"version":"c7135e329a18b0e712378d5c7bc2faec6f5ab0e955ea0002250f9e232af8b3e4","impliedFormat":1},{"version":"340a45cd77b41d8a6deda248167fa23d3dc67ec798d411bd282f7b3d555b1695","impliedFormat":1},{"version":"fae330f86bc10db6841b310f32367aaa6f553036a3afc426e0389ddc5566cd74","impliedFormat":1},{"version":"2bee1efe53481e93bb8b31736caba17353e7bb6fc04520bd312f4e344afd92f9","impliedFormat":1},{"version":"357b67529139e293a0814cb5b980c3487717c6fbf7c30934d67bc42dad316871","impliedFormat":1},{"version":"99d99a765426accf8133737843fb024a154dc6545fc0ffbba968a7c0b848959d","impliedFormat":1},{"version":"c782c5fd5fa5491c827ecade05c3af3351201dd1c7e77e06711c8029b7a9ee4d","impliedFormat":1},{"version":"883d2104e448bb351c49dd9689a7e8117b480b614b2622732655cef03021bf6d","impliedFormat":1},{"version":"d9b00ee2eca9b149663fdba1c1956331841ae296ee03eaaff6c5becbc0ff1ea8","impliedFormat":1},{"version":"09a7e04beb0547c43270b327c067c85a4e2154372417390731dfe092c4350998","impliedFormat":1},{"version":"eee530aaa93e9ec362e3941ee8355e2d073c7b21d88c2af4713e3d701dab8fef","impliedFormat":1},{"version":"28d47319b97dbeee9130b78eae03b2061d46dedbf92b0d9de13ed7ab8399ccd0","impliedFormat":1},{"version":"6559a36671052ca93cab9a289279a6cef6f9d1a72c34c34546a8848274a9c66c","impliedFormat":1},{"version":"7a0e4cd92545ad03910fd019ae9838718643bd4dde39881c745f236914901dfa","impliedFormat":1},{"version":"c99ebd20316217e349004ee1a0bc74d32d041fb6864093f10f31984c737b8cad","impliedFormat":1},{"version":"6f622e7f054f5ab86258362ac0a64a2d6a27f1e88732d6f5f052f422e08a70e7","impliedFormat":1},{"version":"d62d2ef93ceeb41cf9dfab25989a1e5f9ca5160741aac7f1453c69a6c14c69be","impliedFormat":1},{"version":"1491e80d72873fc586605283f2d9056ee59b166333a769e64378240df130d1c9","impliedFormat":1},{"version":"c32c073d389cfaa3b3e562423e16c2e6d26b8edebbb7d73ccffff4aa66f2171d","impliedFormat":1},{"version":"eca72bf229eecadb63e758613c62fab13815879053539a22477d83a48a21cd73","impliedFormat":1},{"version":"633db46fd1765736409a4767bfc670861468dde60dbb9a501fba4c1b72f8644d","impliedFormat":1},{"version":"f379412f2c0dddd193ff66dcdd9d9cc169162e441d86804c98c84423f993aa8a","impliedFormat":1},{"version":"f2ee748883723aa9325e5d7f30fce424f6a786706e1b91a5a55237c78ee89c4a","impliedFormat":1},{"version":"eda4760e5d7b171132265e970b67c322bcfffacb84248f44def26ed160eb722e","impliedFormat":1},{"version":"142f5190d730259339be1433931c0eb31ae7c7806f4e325f8a470bd9221b6533","impliedFormat":1},{"version":"cbd19f594f0ee7beffeb37dc0367af3908815acf4ce46d86b0515478718cfed8","impliedFormat":1},{"version":"3cdb96f128133efd129c798ac11f959e59d278ae439f69983224774d79ed11db","impliedFormat":1},{"version":"8776e64e6165838ac152fa949456732755b0976d1867ae5534ce248f0ccd7f41","impliedFormat":1},{"version":"896bbc7402b3a403cda96813c8ea595470ff76d31f32869d053317c00ca2589a","impliedFormat":1},{"version":"5c4c5b49bbb01828402bb04af1d71673b18852c11b7e95bfd5cf4c3d80d352c8","impliedFormat":1},{"version":"7030df3d920343df00324df59dc93a959a33e0f4940af3fefef8c07b7ee329bf","impliedFormat":1},{"version":"a96bc00e0c356e29e620eaec24a56d6dd7f4e304feefcc99066a1141c6fe05a7","impliedFormat":1},{"version":"d12cc0e5b09943c4cd0848f787eb9d07bf78b60798e4588c50582db9d4decc70","impliedFormat":1},{"version":"7333ee6354964fd396297958e52e5bf62179aa2c88ca0a35c6d3a668293b7e0e","impliedFormat":1},{"version":"19c3760af3cbc9da99d5b7763b9e33aaf8d018bc2ed843287b7ff4343adf4634","impliedFormat":1},{"version":"9d1e38aeb76084848d2fcd39b458ec88246de028c0f3f448b304b15d764b23d2","impliedFormat":1},{"version":"d406da1eccf18cec56fd29730c24af69758fe3ff49c4f94335e797119cbc0554","impliedFormat":1},{"version":"4898c93890a136da9156c75acd1a80a941a961b3032a0cf14e1fa09a764448b7","impliedFormat":1},{"version":"f5d7a845e3e1c6c27351ea5f358073d0b0681537a2da6201fab254aa434121d3","impliedFormat":1},{"version":"3a47d4582ef0697cccf1f3d03b620002f03fb0ff098f630e284433c417d6c61b","impliedFormat":1},{"version":"d7c30f0abfe9e197e376b016086cf66b2ffb84015139963f37301ed0da9d3d0d","impliedFormat":1},{"version":"ff75bba0148f07775bcb54bf4823421ed4ebdb751b3bf79cc003bd22e49d7d73","impliedFormat":1},{"version":"d40d20ac633703a7333770bfd60360126fc3302d5392d237bbb76e8c529a4f95","impliedFormat":1},{"version":"35a9867207c488061fb4f6fe4715802fbc164b4400018d2fa0149ad02db9a61c","impliedFormat":1},{"version":"b5fd805b7c578ca6a42c42bbfa6fda95a85d9e332106d810bb18116dc13a45f8","impliedFormat":1},{"version":"3abd9ab4fb3a035c865e6a68cb9f4260515354d5ebebacd5c681aee52c046d1f","impliedFormat":1},{"version":"13e82862532619a727cff9a9ba78df7ca66e8a9b69e4cbd18e9809257b6bf7ba","impliedFormat":1},{"version":"601fe4e366b99181cd0244d96418cffeaaa987a7e310c6f0ed0f06ce63dfe3e9","impliedFormat":1},{"version":"c66a4f2b1362abc4aeee0870c697691618b423c8c6e75624a40ef14a06f787b7","impliedFormat":1},{"version":"8808b1c4f84f2e43da98757a959fe7282cb1795737e16534a97b7d4d33e84dfc","impliedFormat":1},{"version":"cd0565ace87a2d7802bf4c20ea23a997c54e598b9eb89f9c75e69478c1f7a0b4","impliedFormat":1},{"version":"738020d2c8fc9df92d5dee4b682d35a776eaedfe2166d12bc8f186e1ea57cc52","impliedFormat":1},{"version":"86dd7c5657a0b0bc6bee8002edcfd544458d3d3c60974555746eb9b2583dc35e","impliedFormat":1},{"version":"d97b96b6ecd4ee03f9f1170722c825ef778430a6a0d7aab03b8929012bf773cd","impliedFormat":1},{"version":"e84e9b89251a57da26a339e75f4014f52e8ef59b77c2ee1e0171cde18d17b3b8","impliedFormat":1},{"version":"272dbfe04cfa965d6fff63fdaba415c1b5a515b1881ae265148f8a84ddeb318f","impliedFormat":1},{"version":"2035fb009b5fafa9a4f4e3b3fdb06d9225b89f2cbbf17a5b62413bf72cea721a","impliedFormat":1},{"version":"eefafec7c059f07b885b79b327d381c9a560e82b439793de597441a4e68d774a","impliedFormat":1},{"version":"72636f59b635c378dc9ea5246b9b3517b1214e340e468e54cb80126353053b2e","impliedFormat":1},{"version":"ebb79f267a3bf2de5f8edc1995c5d31777b539935fab8b7d863e8efb06c8e9ea","impliedFormat":1},{"version":"ada033e6a4c7f4e147e6d76bb881069dc66750619f8cc2472d65beeec1100145","impliedFormat":1},{"version":"0c04cc14a807a5dc0e3752d18a3b2655a135fefbf76ddcdabd0c5df037530d41","impliedFormat":1},{"version":"605d29d619180fbec287d1701e8b1f51f2d16747ec308d20aba3e9a0dac43a0f","impliedFormat":1},{"version":"67c19848b442d77c767414084fc571ce118b08301c4ddff904889d318f3a3363","impliedFormat":1},{"version":"c704ff0e0cb86d1b791767a88af21dadfee259180720a14c12baee668d0eb8fb","impliedFormat":1},{"version":"195c50e15d5b3ea034e01fbdca6f8ad4b35ad47463805bb0360bdffd6fce3009","impliedFormat":1},{"version":"da665f00b6877ae4adb39cd548257f487a76e3d99e006a702a4f38b4b39431cb","impliedFormat":1},{"version":"083aebdd7c96aee90b71ec970f81c48984d9c8ab863e7d30084f048ddcc9d6af","impliedFormat":1},{"version":"9324b243d75bd794f7eaf0704161628dac1f536375b82ac9bebd6e5283a985ba","impliedFormat":1},{"version":"d7a4309673b06223537bc9544b1a5fe9425628e1c8ab5605f3c5ebc27ecb8074","impliedFormat":1},{"version":"0be3da88f06100e2291681bbda2592816dd804004f0972296b20725138ebcddf","impliedFormat":1},{"version":"3eadfd083d40777b403f4f4eecfa40f93876f2a01779157cc114b2565a7afb51","impliedFormat":1},{"version":"cb6789ce3eba018d5a7996ccbf50e27541d850e9b4ee97fdcb3cbd8c5093691f","impliedFormat":1},{"version":"a3684ea9719122f9477902acd08cd363a6f3cff6d493df89d4dc12fa58204e27","impliedFormat":1},{"version":"ff3c48a17bf10dfbb62448152042e4a48a56c9972059997ab9e7ed03b191809b","impliedFormat":1},{"version":"bc3561e460de5a2c19123f618fc1d5a96a484d168884d00666997d847f502bf9","impliedFormat":1},{"version":"c0c46113b4cd5ec9e7cf56e6dbfb3930ef6cbba914c0883eeced396988ae8320","impliedFormat":1},{"version":"118ea3f4e7b9c12e92551be0766706f57a411b4f18a1b4762cfde3cd6d4f0a96","impliedFormat":1},{"version":"01acd7f315e2493395292d9a02841f3b0300e77ccf42f84f4f11460e7623107d","impliedFormat":1},{"version":"656d1ce5b8fbed896bb803d849d6157242261030967b821d01e72264774cab55","impliedFormat":1},{"version":"da66c1b41d833858fe61947432130d39649f0b53d992dfd7d00f0bbe57191ef4","impliedFormat":1},{"version":"835739c6dcf0a9a1533d1e95b7d7cf8e44ca1341652856b897f4573078b23a31","impliedFormat":1},{"version":"774a3bcc0700036313c57a079e2e1161a506836d736203aa0463efa7b11a7e54","impliedFormat":1},{"version":"96577e3f8e0f9ea07ddf748d72dc1908581ef2aafd4ae7418a4574c26027cf02","impliedFormat":1},{"version":"f55971cb3ede99c17443b03788fe27b259dcd0f890ac31badcb74e3ffb4bb371","impliedFormat":1},{"version":"0ef0c246f8f255a5d798727c40d6d2231d2b0ebda5b1ec75e80eadb02022c548","impliedFormat":1},{"version":"ea127752a5ec75f2ac6ef7f1440634e6ae5bc8d09e6f98b61a8fb600def6a861","impliedFormat":1},{"version":"862320e775649dcca8915f8886865e9c6d8affc1e70ed4b97199f3b70a843b47","impliedFormat":1},{"version":"561764374e9f37cb895263d5c8380885972d75d09d0db64c12e0cb10ba90ae3e","impliedFormat":1},{"version":"ee889da857c29fa7375ad500926748ef2e029a6645d7c080e57769923d15dfef","impliedFormat":1},{"version":"56984ba2d781bd742b6bc0fa34c10df2eae59b42ec8b1b731d297f1590fa4071","impliedFormat":1},{"version":"7521de5e64e2dd022be87fce69d956a52d4425286fbc5697ecfec386da896d7e","impliedFormat":1},{"version":"f50b072ec1f4839b54fd1269a4fa7b03efbc9c59940224c7939632c0f70a39c3","impliedFormat":1},{"version":"a5b7ec6f1ff3f1d19a2547f7e1a50ab1284e6b4755d260a481ea01ed2c7cec60","impliedFormat":1},{"version":"1747f9eebf5beb8cfc46cf0303e300950b7bff20cff60b9c46818caced3226e3","impliedFormat":1},{"version":"9d969f36abb62139a90345ee5d03f1c2479831bd84c8f843d87ec304cad96ead","impliedFormat":1},{"version":"e972b52218fd5919aec6cd0e5e2a5fb75f5d2234cf05597a9441837a382b2b29","impliedFormat":1},{"version":"d1e292b0837d0ef5ede4f52363c9d8e93f5d5234086adc796e11eae390305b36","impliedFormat":1},{"version":"0a9e10028a96865d0f25aeca9e3b1ff0691b9b662aa186d9d490728434cf8261","impliedFormat":1},{"version":"1aed740b674839c89f427f48737bad435ee5a39d80b5929f9dc9cc9ac10a7700","impliedFormat":1},{"version":"6e9e3690dc3a6e99a845482e33ee78915893f2d0d579a55b6a0e9b4c44193371","impliedFormat":1},{"version":"4e7a76cce3b537b6cdb1c4b97e29cb4048ee8e7d829cf3a85f4527e92eb573f2","impliedFormat":1},{"version":"7e7e30f804f94b72d23a606f1d281de404a510984085fea8cbbefc7bdcaf1a37","impliedFormat":1},{"version":"46f1fe93f199a419172d7480407d9572064b54712b69406efa97e0244008b24e","impliedFormat":1},{"version":"044e6aaa3f612833fb80e323c65e9d816c3148b397e93630663cda5c2d8f4de1","impliedFormat":1},{"version":"deaf8eb392c46ea2c88553d3cc38d46cfd5ee498238dbc466e3f5be63ae0f651","impliedFormat":1},{"version":"6a79b61f57699de0a381c8a13f4c4bcd120556bfab0b4576994b6917cb62948b","impliedFormat":1},{"version":"c5133d7bdec65f465df12f0b507fbc0d96c78bfa5a012b0eb322cf1ff654e733","impliedFormat":1},{"version":"7905c052681cbe9286797ec036942618e1e8d698dcc2e60f4fb7a0013d470442","impliedFormat":1},{"version":"89049878a456b5e0870bb50289ea8ece28a2abd0255301a261fa8ab6a3e9a07d","impliedFormat":1},{"version":"d0da4f4fd66f37c13deabc1a641edd629141c333ccf862733788bd27e89436ac","impliedFormat":1},{"version":"d4a4f10062a6d82ba60d3ffde9154ef24b1baf2ce28c6439f5bdfb97aa0d18fc","impliedFormat":1},{"version":"f13310c360ecffddb3858dcb33a7619665369d465f55e7386c31d45dfc3847bf","impliedFormat":1},{"version":"e7bde95a05a0564ee1450bc9a53797b0ac7944bf24d87d6f645baca3aa60df48","impliedFormat":1},{"version":"62e68ce120914431a7d34232d3eca643a7ddd67584387936a5202ae1c4dd9a1b","impliedFormat":1},{"version":"91d695bba902cc2eda7edc076cd17c5c9340f7bb254597deb6679e343effadbb","impliedFormat":1},{"version":"e1cb8168c7e0bd4857a66558fe7fe6c66d08432a0a943c51bacdac83773d5745","impliedFormat":1},{"version":"a464510505f31a356e9833963d89ce39f37a098715fc2863e533255af4410525","impliedFormat":1},{"version":"0612b149cabbc136cb25de9daf062659f306b67793edc5e39755c51c724e2949","impliedFormat":1},{"version":"2579b150b86b5f644d86a6d58f17e3b801772c78866c34d41f86f3fc9eb523fe","impliedFormat":1},{"version":"e4b3a3e1b21a194b29d35488ec880948fc2ef8e937288463ea2981ad62a7b106","impliedFormat":1},{"version":"0353e05b0d8475c10ddd88056e0483b191aa5cdea00a25e0505b96e023f1a2d9","impliedFormat":1},{"version":"6a312caabb43c284a4b0da60d5c24f285338096eb9e977af1faca38d32a34685","impliedFormat":1},{"version":"b6eda93163beb978dd0d3042b11c60373506400c94613c0b40d1c0a9a9f1020e","impliedFormat":1},{"version":"a8af4739274959d70f7da4bfdd64f71cfc08d825c2d5d3561bc7baed760b33ef","impliedFormat":1},{"version":"99193bafaa9ce112889698de25c4b8c80b1209bb7402189aea1c7ada708a8a54","impliedFormat":1},{"version":"70473538c6eb9494d53bf1539fe69df68d87c348743d8f7244dcb02ca3619484","impliedFormat":1},{"version":"c48932ab06a4e7531bdca7b0f739ace5fa273f9a1b9009bcd26902f8c0b851f0","impliedFormat":1},{"version":"df6c83e574308f6540c19e3409370482a7d8f448d56c65790b4ac0ab6f6fedd8","impliedFormat":1},{"version":"ebbe6765a836bfa7f03181bc433c8984ca29626270ca1e240c009851222cb8a7","impliedFormat":1},{"version":"20f630766b73752f9d74aab6f4367dba9664e8122ea2edcb00168e4f8b667627","impliedFormat":1},{"version":"468df9d24a6e2bc6b4351417e3b5b4c2ca08264d6d5045fe18eb42e7996e58b4","impliedFormat":1},{"version":"954523d1f4856180cbf79b35bd754e14d3b2aea06c7efd71b254c745976086e9","impliedFormat":1},{"version":"31a030f1225ab463dd0189a11706f0eb413429510a7490192a170114b2af8697","impliedFormat":1},{"version":"6f48f244cd4b5b7e9a0326c74f480b179432397580504726de7c3c65d6304b36","impliedFormat":1},{"version":"5520e6defac8e6cdced6dd28808fafe795cb2cd87407bb1012e13a2b061f50b7","impliedFormat":1},{"version":"c3451661fb058f4e15971bbed29061dd960d02d9f8db1038e08b90d294a05c68","impliedFormat":1},{"version":"1f21aefa51f03629582568f97c20ef138febe32391012828e2a0149c2c393f62","impliedFormat":1},{"version":"b18141cda681d82b2693aef045107a910b90a7409ecff0830e1283f0bb2a53e6","impliedFormat":1},{"version":"18eb53924f27af2a5e9734dce28cf5985df7b2828dade1239241e95b639e9bf1","impliedFormat":1},{"version":"a9f1c52f4e7c2a2c4988b5638bd3dbfe38e408b358d02dd2fb8c8920e877f088","impliedFormat":1},{"version":"a7e10a8ad6536dd0225029e46108b18cee0d3c15c2f6e49bd62798ad85bc57b6","impliedFormat":1},{"version":"8db1ed144dd2304b9bd6e41211e22bad5f4ab1d8006e6ac127b29599f4b36083","impliedFormat":1},{"version":"843a5e3737f2abbbbd43bf2014b70f1c69a80530814a27ae1f8be213ae9ec222","impliedFormat":1},{"version":"6fc1be224ad6b3f3ec11535820def2d21636a47205c2c9de32238ba1ac8d82e6","impliedFormat":1},{"version":"5a44788293f9165116c9c183be66cefef0dc5d718782a04847de53bf664f3cc1","impliedFormat":1},{"version":"afd653ae63ce07075b018ba5ce8f4e977b6055c81cc65998410b904b94003c0a","impliedFormat":1},{"version":"9172155acfeb17b9d75f65b84f36cb3eb0ff3cd763db3f0d1ad5f6d10d55662f","impliedFormat":1},{"version":"71807b208e5f15feffb3ff530bec5b46b1217af0d8cc96dde00d549353bcb864","impliedFormat":1},{"version":"1a6eca5c2bc446481046c01a54553c3ffb856f81607a074f9f0256c59dd0ab13","impliedFormat":1},{"version":"dff93e0997c4e64ff29e9f70cad172c0b438c4f58c119f17a51c94d48164475a","impliedFormat":1},{"version":"fd1ddf926b323dfa439be49c1d41bbe233fe5656975a11183aeb3bf2addfa3bb","impliedFormat":1},{"version":"6dda11db28da6bcc7ff09242cd1866bdddd0ae91e2db3bea03ba66112399641a","impliedFormat":1},{"version":"ea4cd1e72af1aa49cf208b9cb4caf542437beb7a7a5b522f50a5f1b7480362ed","impliedFormat":1},{"version":"903a7d68a222d94da11a5a89449fdd5dd75d83cd95af34c0242e10b85ec33a93","impliedFormat":1},{"version":"e7fe2e7ed5c3a7beff60361632be19a8943e53466b7dd69c34f89faf473206d7","impliedFormat":1},{"version":"b4896cee83379e159f83021e262223354db79e439092e485611163e2082224ff","impliedFormat":1},{"version":"5243e79a643e41d9653011d6c66e95048fc0478eb8593dc079b70877a2e3990e","impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"0ccdaa19852d25ecd84eec365c3bfa16e7859cadecf6e9ca6d0dbbbee439743f","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc2110f7decca6bfb9392e30421cfa1436479e4a6756e8fec6cbc22625d4f881","affectsGlobalScope":true,"impliedFormat":1},{"version":"096116f8fedc1765d5bd6ef360c257b4a9048e5415054b3bf3c41b07f8951b0b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5e01375c9e124a83b52ee4b3244ed1a4d214a6cfb54ac73e164a823a4a7860a","affectsGlobalScope":true,"impliedFormat":1},{"version":"f90ae2bbce1505e67f2f6502392e318f5714bae82d2d969185c4a6cecc8af2fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b58e207b93a8f1c88bbf2a95ddc686ac83962b13830fe8ad3f404ffc7051fb4","affectsGlobalScope":true,"impliedFormat":1},{"version":"1fefabcb2b06736a66d2904074d56268753654805e829989a46a0161cd8412c5","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"c18a99f01eb788d849ad032b31cafd49de0b19e083fe775370834c5675d7df8e","affectsGlobalScope":true,"impliedFormat":1},{"version":"5247874c2a23b9a62d178ae84f2db6a1d54e6c9a2e7e057e178cc5eea13757fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"cdcf9ea426ad970f96ac930cd176d5c69c6c24eebd9fc580e1572d6c6a88f62c","impliedFormat":1},{"version":"23cd712e2ce083d68afe69224587438e5914b457b8acf87073c22494d706a3d0","impliedFormat":1},{"version":"156a859e21ef3244d13afeeba4e49760a6afa035c149dda52f0c45ea8903b338","impliedFormat":1},{"version":"10ec5e82144dfac6f04fa5d1d6c11763b3e4dbbac6d99101427219ab3e2ae887","impliedFormat":1},{"version":"615754924717c0b1e293e083b83503c0a872717ad5aa60ed7f1a699eb1b4ea5c","impliedFormat":1},{"version":"14e9acf826baba0ef4b5665704084896e7bcc06f65a9ab13af7e93d27d6b7069","impliedFormat":1},{"version":"68834d631c8838c715f225509cfc3927913b9cc7a4870460b5b60c8dbdb99baf","impliedFormat":1},{"version":"21adf13435b9b748529c8cedf80f884e5130b9684188120a686cd2b26a2059c7","impliedFormat":1},{"version":"eec76bf6b9346f3f95fa402621b889489e96930e72295b0369022f332e9b4a6a","impliedFormat":1},{"version":"0ecd58f413f9bc3b7d4383eae31b0c8fc576985cd7404d6f99f8c643543ade74","impliedFormat":1},{"version":"ea6bc8de8b59f90a7a3960005fd01988f98fd0784e14bc6922dde2e93305ec7d","impliedFormat":1},{"version":"36107995674b29284a115e21a0618c4c2751b32a8766dd4cb3ba740308b16d59","impliedFormat":1},{"version":"914a0ae30d96d71915fc519ccb4efbf2b62c0ddfb3a3fc6129151076bc01dc60","impliedFormat":1},{"version":"9c32412007b5662fd34a8eb04292fb5314ec370d7016d1c2fb8aa193c807fe22","impliedFormat":1},{"version":"7fd1b31fd35876b0aa650811c25ec2c97a3c6387e5473eb18004bed86cdd76b6","impliedFormat":1},{"version":"4d327f7d72ad0918275cea3eee49a6a8dc8114ae1d5b7f3f5d0774de75f7439a","impliedFormat":1},{"version":"6ebe8ebb8659aaa9d1acbf3710d7dae3e923e97610238b9511c25dc39023a166","impliedFormat":1},{"version":"e85d7f8068f6a26710bff0cc8c0fc5e47f71089c3780fbede05857331d2ddec9","impliedFormat":1},{"version":"7befaf0e76b5671be1d47b77fcc65f2b0aad91cc26529df1904f4a7c46d216e9","impliedFormat":1},{"version":"0a60a292b89ca7218b8616f78e5bbd1c96b87e048849469cccb4355e98af959a","impliedFormat":1},{"version":"0b6e25234b4eec6ed96ab138d96eb70b135690d7dd01f3dd8a8ab291c35a683a","impliedFormat":1},{"version":"9666f2f84b985b62400d2e5ab0adae9ff44de9b2a34803c2c5bd3c8325b17dc0","impliedFormat":1},{"version":"40cd35c95e9cf22cfa5bd84e96408b6fcbca55295f4ff822390abb11afbc3dca","impliedFormat":1},{"version":"b1616b8959bf557feb16369c6124a97a0e74ed6f49d1df73bb4b9ddf68acf3f3","impliedFormat":1},{"version":"5b03a034c72146b61573aab280f295b015b9168470f2df05f6080a2122f9b4df","impliedFormat":1},{"version":"40b463c6766ca1b689bfcc46d26b5e295954f32ad43e37ee6953c0a677e4ae2b","impliedFormat":1},{"version":"249b9cab7f5d628b71308c7d9bb0a808b50b091e640ba3ed6e2d0516f4a8d91d","impliedFormat":1},{"version":"d33ce35e3f9cfcc1d94eca415bdd3bde94d5b153ffdd33e6c4455c029986c630","impliedFormat":1},{"version":"80aae6afc67faa5ac0b32b5b8bc8cc9f7fa299cff15cf09cc2e11fd28c6ae29e","impliedFormat":1},{"version":"f473cd2288991ff3221165dcf73cd5d24da30391f87e85b3dd4d0450c787a391","impliedFormat":1},{"version":"499e5b055a5aba1e1998f7311a6c441a369831c70905cc565ceac93c28083d53","impliedFormat":1},{"version":"8aee8b6d4f9f62cf3776cda1305fb18763e2aade7e13cea5bbe699112df85214","impliedFormat":1},{"version":"98498b101803bb3dde9f76a56e65c14b75db1cc8bec5f4db72be541570f74fc5","impliedFormat":1},{"version":"4dc59f6e1dbf3d5f66660fceabe6c174d3261b37b696ae1854f0dbaf255fc753","impliedFormat":1},{"version":"5d0375ca7310efb77e3ef18d068d53784faf62705e0ad04569597ae0e755c401","impliedFormat":1},{"version":"59af37caec41ecf7b2e76059c9672a49e682c1a2aa6f9d7dc78878f53aa284d6","impliedFormat":1},{"version":"addf417b9eb3f938fddf8d81e96393a165e4be0d4a8b6402292f9c634b1cb00d","impliedFormat":1},{"version":"436d7b4543b340b0f3eef4310d524242e41369b9652aa9c70428767c4dcac455","impliedFormat":1},{"version":"adf27937dba6af9f08a68c5b1d3fce0ca7d4b960c57e6d6c844e7d1a8e53adae","impliedFormat":1},{"version":"12950411eeab8563b349cb7959543d92d8d02c289ed893d78499a19becb5a8cc","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"114f493b30f364255290472111b5a4791d5902c308645670cd0401429cbc6930","impliedFormat":1},{"version":"c3f5289820990ab66b70c7fb5b63cb674001009ff84b13de40619619a9c8175f","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3275d55fac10b799c9546804126239baf020d220136163f763b55a74e50e750","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa68a0a3b7cb32c00e39ee3cd31f8f15b80cac97dce51b6ee7fc14a1e8deb30b","affectsGlobalScope":true,"impliedFormat":1},{"version":"1cf059eaf468efcc649f8cf6075d3cb98e9a35a0fe9c44419ec3d2f5428d7123","affectsGlobalScope":true,"impliedFormat":1},{"version":"6c36e755bced82df7fb6ce8169265d0a7bb046ab4e2cb6d0da0cb72b22033e89","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"7a93de4ff8a63bafe62ba86b89af1df0ccb5e40bb85b0c67d6bbcfdcf96bf3d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"90e85f9bc549dfe2b5749b45fe734144e96cd5d04b38eae244028794e142a77e","affectsGlobalScope":true,"impliedFormat":1},{"version":"e0a5deeb610b2a50a6350bd23df6490036a1773a8a71d70f2f9549ab009e67ee","affectsGlobalScope":true,"impliedFormat":1},{"version":"d2ae155afe8a01cc0ae612d99117cf8ef16692ba7c4366590156fdec1bcf2d8c","impliedFormat":1},{"version":"3f5e5d9be35913db9fea42a63f3df0b7e3c8703b97670a2125587b4dbbd56d7c","impliedFormat":1},{"version":"c8b8968311ec4e5e97b7b5fb8a65efaba455db9bdcfd7fff7fb15f6e317bfba0","impliedFormat":1},{"version":"57c23df0b5f7a8e26363a3849b0bc7763f6b241207157c8e40089d1df4116f35","affectsGlobalScope":true,"impliedFormat":1},{"version":"3b8bc0c17b54081b0878673989216229e575d67a10874e84566a21025a2461ee","impliedFormat":1},{"version":"5b0db5a58b73498792a29bfebc333438e61906fef75da898b410e24e52229e6f","impliedFormat":1},{"version":"dbe055b2b29a7bab2c1ca8f259436306adb43f469dca7e639a02cd3695d3f621","impliedFormat":1},{"version":"1678b04557dca52feab73cc67610918a7f5e25bfdba3e7fa081acd625d93106d","impliedFormat":1},{"version":"aecbf1d9e6a18dab7d92ef8a89a1444b47e1eb6134cb2bb776a26d55ff58c29a","impliedFormat":1},{"version":"2ea729503db9793f2691162fec3dd1118cab62e96d025f8eeb376d43ec293395","impliedFormat":1},{"version":"9ec87fea42b92894b0f209931a880789d43c3397d09dd99c631ae40a2f7071d1","impliedFormat":1},{"version":"c68e88cdfadfb6c8ba5fc38e58a3a166b0beae77b1f05b7d921150a32a5ffb8d","impliedFormat":1},{"version":"2bc7aa4fba46df0bd495425a7c8201437a7d465f83854fac859df2d67f664df3","impliedFormat":1},{"version":"41d17e1ad9a002feb11c8cdd2777e5bbc0cdb1e3f595d237e4dded0b6949983b","impliedFormat":1},{"version":"1fede9296beac11ce8e6b425396a1791f64341f2be85deebb6286faf6e16306e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce697b6a251d9cad53998c7fd3098072df883b525ec45d83530e434dc6d80dc6","impliedFormat":1},{"version":"719412f054e6ecc35489462c9a21bab0323d173a7d04e55b0ace4b5d86fbeb07","impliedFormat":1},{"version":"0eb5d0cbf09de5d34542b977fd6a933bb2e0817bffe8e1a541b2f1ad1b9af1ff","impliedFormat":1},{"version":"3db996ecdee7aabecc5385976cc07eb66216034a273c07b17d1a85292e9bab0c","impliedFormat":1},{"version":"2c2bdaa1d8ead9f68628d6d9d250e46ee8e81aa4898b4769a36956ae15e060fe","impliedFormat":1},{"version":"c32c840c62d8bd7aeb3147aa6754cd2d922b990a6b6634530cb2ebdce5adc8e9","impliedFormat":1},{"version":"5ff4433a2deae4f85ab1377e90a7554ce6b47ae51c69a84ca30a6e22fae85834","impliedFormat":1},{"version":"82b91e4e42e6c41bc7fc1b6c2dc5eba6a2ba98375eb1f210e6ff6bba2d54177e","impliedFormat":1},{"version":"c1fa52b3d014001e8662fa2669d90ea15373958a288e3b83a3b621733d25292a","affectsGlobalScope":true,"impliedFormat":1},{"version":"cbed824fec91efefc7bbdcb8b43d1a531fdbebd0e2ef19481501ff365a93cb70","impliedFormat":1},{"version":"d0716593b3f2b0451bcf0c24cfa86dec2235c325c89f201934248b7c742715fc","impliedFormat":1},{"version":"ec501101c2a96133a6c695f934c8f6642149cc728571b29cbb7b770984c1088e","impliedFormat":1},{"version":"b214ebcf76c51b115453f69729ee8aa7b7f8eccdae2a922b568a45c2d7ff52f7","impliedFormat":1},{"version":"429c9cdfa7d126255779efd7e6d9057ced2d69c81859bbab32073bad52e9ba76","impliedFormat":1},{"version":"2991bca2cc0f0628a278df2a2ccdb8d6cbcb700f3761abbed62bba137d5b1790","impliedFormat":1},{"version":"47d480d0c84b0f6215f1f817e0939b247ebf5217b6784846b96fa6ab710ce99d","affectsGlobalScope":true,"impliedFormat":1},{"version":"230763250f20449fa7b3c9273e1967adb0023dc890d4be1553faca658ee65971","impliedFormat":1},{"version":"c3e9078b60cb329d1221f5878e88cecfa3e74460550e605a58fcfb41a66029ff","impliedFormat":1},{"version":"8413d0641f293aed551c7464615b770d34a02dedede889b9591172287d68e773","impliedFormat":1},{"version":"0ea59f7d3e51440baa64f429253759b106cfcbaf51e474cae606e02265b37cf8","impliedFormat":1},{"version":"bc18a1991ba681f03e13285fa1d7b99b03b67ee671b7bc936254467177543890","impliedFormat":1},{"version":"1b241e24f3227d078c06aeda6e050187ad59a4e591f4467abed44d92b084e08d","impliedFormat":1},{"version":"fa94bbf532b7af8f394b95fa310980d6e20bd2d4c871c6a6cb9f70f03750a44b","impliedFormat":1},{"version":"7fde0e1be5c8be204ffbf428abfcf01da2eb0f130e1bc3f539eb7275f4fd1f58","impliedFormat":1},{"version":"e284328553df5f425a5d33d36a0c3fa66b46af9d097cad6f4d2e8696dfdeb0f1","affectsGlobalScope":true,"impliedFormat":1},{"version":"7fa2214bb0d64701bc6f9ce8cde2fd2ff8c571e0b23065fa04a8a5a6beb91511","impliedFormat":1},{"version":"f36b3fbe2be150a9ca140da48593f21e6a8172004f92ddc549b43efec39f3e54","impliedFormat":1},{"version":"f1c93e046fb3d9b7f8249629f4b63dc068dd839b824dd0aa39a5e68476dc9420","impliedFormat":1},{"version":"016b29bf4926b80255a108c53a1451717350059da04fcae64d1075f5e93bbb39","impliedFormat":1},{"version":"841983e39bd4cbb463be385e92fda11057cab368bf27100a801c492f1d86cbaa","impliedFormat":1},{"version":"1c4f139ade4f6ebf45463505f8155173e5d7a5305e50e0aae0a5e712d6ff3b48","impliedFormat":1},{"version":"e16b319e5aca1031168de823c4946ff8e29629c4c8cc0ec0fcfe2a8ab2155043","impliedFormat":1},{"version":"e4156ddb25aa0e3b5303d372f26957b36778f0f6bbd4326359269873295e3058","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc1b433a84cae05ddc5672d4823170af78606ad21ecef60dbc4570190cbf1357","impliedFormat":1},{"version":"9d3821bc75c59577e52643324cec92fc2145642e8d17cf7ee07a3181f21d985d","impliedFormat":1},{"version":"7f78cfb2b343838612c192cb251746e3a7c62ac7675726a47e130d9b213f6580","impliedFormat":1},{"version":"201db9cf1687fab1adf5282fcba861f382b32303dc4f67c89d59655e78a25461","impliedFormat":1},{"version":"2c3c5c0f54055e87640f5d233716fd889f3034fc7911d603b642369b0dbeb2a7","impliedFormat":1},{"version":"0a20eaf2e4b1e3c1e1f87f7bccb0c936375b23b022baeea750519b7c9bc6ce83","impliedFormat":1},{"version":"b484ec11ba00e3a2235562a41898d55372ccabe607986c6fa4f4aba72093749f","impliedFormat":1},{"version":"a16b91b27bd6b706c687c88cbc8a7d4ee98e5ed6043026d6b84bda923c0aed67","impliedFormat":1},{"version":"1c9e5b1a17b1fc9b3711fb36e0690421261ab2880f15b145155b5b2ba2ab6c2d","impliedFormat":1},{"version":"99ab6d0d660ce4d21efb52288a39fd35bb3f556980ec5463b1ae8f304a3bbc85","impliedFormat":1},{"version":"6eeded8c7e352be6e0efb83f4935ec752513c4d22043b52522b90849a49a3a11","impliedFormat":1},{"version":"6c1ad90050ffbb151cacc68e2d06ea1a26a945659391e32651f5d42b86fd7f2c","impliedFormat":1},{"version":"afa1c49f8e559e413d57343339db857d2a8159435cf9cf7d4deb41718fff1b88","impliedFormat":1},{"version":"6953d7597831d0860c7034cf4f0419687d263b6b98a4b32e37ce6d49615c36e2","impliedFormat":1},{"version":"76e7352249c42b9d54fe1f9e1ebcef777da1cb2eb33038366af49469d433597b","impliedFormat":1},{"version":"88cb622dd0ec1ef860e5c27fa884e60d2eba5ae22c7907dff82c56a69bdd2c8a","impliedFormat":1},{"version":"eb234b3e285e8bc071bdddc1ec0460095e13ead6222d44b02c4e0869522f9ba3","impliedFormat":1},{"version":"c85114872760189e50fef131944427b0fb367f0cc0b6dce164bb427a6fd89381","impliedFormat":1},{"version":"5ad69b0d7e7bdbcd3adfdb6a3e306e935c9c2711b1c60493646504a2f991346e","impliedFormat":1},{"version":"a12a667efdeb03b529bd4ebb4032998ddd32743799f59f9f18b186f8e63a2cf1","impliedFormat":1},{"version":"cee7efa0ae4c58deab218d1df0d1bf84abfd5c356cff28bca1421489cba13a19","impliedFormat":1},{"version":"f9e034b1ae29825c00532e08ea852b0c72885c343ee48d2975db0a6481218ab3","impliedFormat":1},{"version":"1193f49cbb883f40326461fe379e58ffa4c18d15bf6d6a1974ad2894e4fb20f3","impliedFormat":1},{"version":"8f1241f5d9f0d3d72117768b3c974e462840fbd85026fb66685078945404cf2f","impliedFormat":1},{"version":"6ecc423e71318bafbd230e6059e082c377170dfc7e02fccfa600586f8604d452","impliedFormat":1},{"version":"772f9bdd2bf50c9c01b0506001545e9b878faa7394ad6e7d90b49b179a024584","impliedFormat":1},{"version":"46fbbc428aa66412a94e7c1d455b6ae592ee57e0adb080bfa2abacc2a63b21a1","impliedFormat":1},{"version":"df251aa7a87b6d003a45ad1f71b87ff4448e535db473cb3c897720012b5553d2","impliedFormat":1},{"version":"20ccce97c422c215a41f0aa8373eb2b6f4068633ceef401c5f743cecdf8119bf","impliedFormat":1},{"version":"cd1ccdd9fd7980d43dfede5d42ee3d18064baed98b136089cf7c8221d562f058","impliedFormat":1},{"version":"d60f9a4fd1e734e7b79517f02622426ea1000deb7d6549dfdece043353691a4e","impliedFormat":1},{"version":"ec05ccc3a2e35ef2800a5b5ed2eb2ad4cd004955447bebd86883ddf49625b400","impliedFormat":1},{"version":"403d28b5e5f8fcff795ac038902033ec5890143e950af45bd91a3ed231e8b59c","impliedFormat":1},{"version":"0b17ac073f8dd236cb4ec1f16c84ac2632713bf421305c96a536c360b1500f01","impliedFormat":1},{"version":"c73b59f91088c00886d44ca296d53a75c263c3bda31e3b2f37ceb137382282be","impliedFormat":1},{"version":"e7aa2c584edb0970cb4bb01eb10344200286055f9a22bc3dadcc5a1f9199af3e","impliedFormat":1},{"version":"bfeb476eb0049185cb94c2bfcadb3ce1190554bbcf170d2bf7c68ed9bb00458e","impliedFormat":1},{"version":"ae23a65a2b664ffe979b0a2a98842e10bdf3af67a356f14bbc9d77eb3ab13585","impliedFormat":1},{"version":"2db00053dff66774bc4216209acf094dd70d9dfd8211e409fc4bd8d10f7f66f6","impliedFormat":1},{"version":"eccf6ad2a8624329653896e8dbd03f30756cbd902a81b5d3942d6cf0e1a21575","impliedFormat":1},{"version":"1930c964051c04b4b5475702613cd5a27fcc2d33057aa946ff52bfca990dbc84","impliedFormat":1},{"version":"762992adfa3fbf42c0bce86caed3dc185786855b21a20265089770485e6aa9d3","impliedFormat":1},{"version":"8a440978a6b5c6e6ea76a2804a90c06cadbefb96f6680785d8d3bfab8c8875d8","impliedFormat":1},{"version":"62463aa3d299ae0cdc5473d2ac32213a05753c3adce87a8801c6d2b114a64116","impliedFormat":1},{"version":"05a0d93bb8653bb3833c1c22e3fa5432c38885b2e545e99524e67e25e5ce355c","impliedFormat":1},{"version":"fb4c35b52a35353805407ea9fa1baf3ef102e2f8e0d37a88c0fc0099f3df5dbb","impliedFormat":1},{"version":"40abfc1faa2971acedb69bde8d8c4bbd4edce4af12f786e747dfb8298e6a05a1","impliedFormat":1},{"version":"80ae880ac3e366a8aca79089c9d8467b1753e19fb65a9cf43bbfdb6db1ac588d","impliedFormat":1},{"version":"02153ce9d452d116e9b7bfe42058c02c4cac09f49c46450820b4b44c83306a3e","impliedFormat":1},{"version":"f689c0633e8c95f550d36af943d775f3fae3dac81a28714b45c7af0bbb76a980","impliedFormat":1},{"version":"34be0f820d16a54625b86ab6d7b7928dbb819b58ddac181aa3836390c8c3df24","impliedFormat":1},{"version":"0495afa06118083a11cd4da27acfd96a01b989aff0fc633823c5febe9668ef15","impliedFormat":1},{"version":"67feb4436be89f58ba899dec57f6e703bee1bb7205ba21ab50fca237f6753787","impliedFormat":1},{"version":"75849f5ead7684bf85ee9cce7e84683ed4332fa187f8ee0978ba9df96c5cee06","impliedFormat":1},{"version":"b5325ff5c9dc488bb9c87711faf2b73f639c45f190b81df88ed056807206958b","impliedFormat":1},{"version":"3a8c0797ff5e1cfb5438f0c5a22a36500320c075b74e964da1ec1424b5b526f0","impliedFormat":1},{"version":"a743cf98667fdbb6989d9a7629d25a9824a484ce639bbf2740dc809341e6dbce","impliedFormat":1},{"version":"d73a2cbf1893668c1fc0b41878a9df37bbb128cb05e292b845f68b7b88ef9957","impliedFormat":1},{"version":"eaa70e04ed55081afa11e3aa261b14ed26507e1a0b0fbade405c6b50c70b2059","impliedFormat":1},{"version":"eb89de249984a951578b5fa472d0414cd10cd7d35777aa1c7c308dfe2b0b49d6","impliedFormat":1},{"version":"0fe4a1bd28d903694da55eb5955297a5db1fc87720a45ae60b7640bb5310368c","impliedFormat":1},{"version":"a30b92bc54447834c3b4443b388891bf11cce5b25b8edbfc29184d2abd51c991","impliedFormat":1},{"version":"5e168f60c9132665853ccba13a29e4d311ac174c0fd3eda5cee7a6c5071802d6","impliedFormat":1},{"version":"1c225a18846203fafc4334658715b0d3fd3ee842c4cfd42e628a535eda17730d","impliedFormat":1},{"version":"7ce93da38595d1caf57452d57e0733474564c2b290459d34f6e9dcf66e2d8beb","impliedFormat":1},{"version":"d7b672c1c583e9e34ff6df2549d6a55d7ca3adaf72e6a05081ea9ee625dac59f","impliedFormat":1},{"version":"f3a2902e84ebdef6525ed6bf116387a1256ea9ae8eeb36c22f070b7c9ea4cf09","impliedFormat":1},{"version":"788e6ec863183b001b1e96d54f876c9d5436fa3aa19397093912dcdae466caf7","impliedFormat":1},{"version":"ae3e98448468e46474d817b5ebe74db11ab22c2feb60e292d96ce1a4ee963623","impliedFormat":1},{"version":"f5c67304429e2e2554f63526e842f47bd9ee0cb0f18becc963b93b2d3fafe9c2","impliedFormat":1},{"version":"8903c1df475f531bf787bc795c718e415ce9974536d8429619fdc456f5329948","impliedFormat":1},{"version":"4bac2738a569f77e13bff48946a7b8e98db314cc5c8e75d14e7efd6b1ed52a85","impliedFormat":1},{"version":"337de7eeeb4bc95014657067d2c3f370d7b3935c8c18fb5e12d4c00545ee519b","impliedFormat":1},{"version":"c7dc98d27c1b991852351817a238385366a4302111d12b79695df9d4783cae66","signature":"70e810b4f31cf54e9f73a64c73be54b12a2e5dff4d2f7b4e045f96df5e27f06c"},{"version":"617cca499054c26280cf7a274ba61634ca4a2376afb322f72e5af4f2cc2d7689","impliedFormat":1},{"version":"b1a6b13030544850d3032928604dd91e28ca82862346aaebff1049b645daf12f","impliedFormat":1},{"version":"c87c5490724beee48ed6eca974ef1618f875b07c3c03b576100902ae01236279","impliedFormat":1},{"version":"d5eb5865d4cbaa9985cc3cfb920b230cdcf3363f1e70903a08dc4baab80b0ce1","impliedFormat":1},{"version":"51ebca098538b252953b1ef83c165f25b52271bfb6049cd09d197dddd4cd43c5","impliedFormat":1},{"version":"8aa255453712ce8df1652947a0406769415ee7113c54551310e36cf6ab9db99c","impliedFormat":1},{"version":"6d5618deb494f418484f7c41319f2bc8c34bd071720e377df61194f7a51ff518","impliedFormat":1},{"version":"f60e3e3060207ac982da13363181fd7ee4beecc19a7c569f0d6bb034331066c2","impliedFormat":1},{"version":"4a20a0a39aca8738cc5088f9dea2969bf2af3ca1341c56071f513059b122d150","impliedFormat":1},{"version":"a33c98f95c099c2a9a383b156c8f2a16605843d29455a954c1431bcbbc1ce35a","impliedFormat":1},{"version":"effa1fd4980f6dff4e81ed67009b847d34449087504319f9a0dd081e8cf23a31","impliedFormat":1},{"version":"c2c9f431f788b434e6ae327844ef6712bb6a6a88ad08acf20bf6cfeb41d154ba","impliedFormat":1},{"version":"798367363a3274220cbed839b883fe2f52ba7197b25e8cb2ac59c1e1fd8af6b7","impliedFormat":1},{"version":"2636a309ed87d6876728d9aca846a76b372cf2a21a4fdf9940a82a2dd86687d0","impliedFormat":1},{"version":"55917f3d5b339d0a8e57c4898dfbc19d3941643f716f4830187068e799159ea4","impliedFormat":1},"44b9fc7cc9ebe1235a25b027e1205511bc2ee5eb42b7253e6aa07616b20a5dcb","b5d956f435ebe2c610d28a0e57c80576809bfe6150f76ef1fe61db399a758b2d","65419ba0ec4b4f5603c47369261619a48464f23b2696d7be1b85b80a23ee7bd4","dc9fc49c8494b4fd0650800a7767fb7a524b1023bbc8e09082d80eff85c0495f","67d5023311162e68be1ba2157a997bfb30bac08ec255c66baaae879c8b3dfcf9","bb3e683bb0eaef13c9025bfb2dab02b111f35e231713c42fd95252f96d6f097a","19ff6e335cf5a64247219c3b159f634ee882823897844d348f02572bddca48f1","3b2d03a9ed136bb38c13f3ebb52d1d92ae10242868ed771efa39969dcd7e020a","1f075aa9a1adec22674bba8fb35430e701dc8c7174cd83fc2232c1ce05ca5157","a9ff19c64dcd9e378a5966c06bfdcdd2a706c815578f9419bfd664be2e83f57c","9375a851dc72dc703ecb153d60d0e3ed53f4975f00230232b36bc73b15381f08","8b18c93ae2fd1d8d91edb0775b59ba0afe826d583b06c2bfdf395ed23d0b9657","c4b97bc6a9a4e7525eee7d639ea50c2613211e6400ad380365b957a891b72691",{"version":"663266b8782a8b327a5196efa1b3627805e3e4fc712d6a4380f5e6571e45a813","impliedFormat":1},{"version":"5aa42b32993e161aaf93d992300494377d38c8883e15fde44d5c7949313058af","impliedFormat":1},{"version":"b75dd83280bdfd4880c04cadd7f5edf6b23305850e436b1e5b92c3847090d031","impliedFormat":1},{"version":"eae784573a5c4c55c65b86accb356b21b5f597c3484c1bd344e647bc92ebe572","impliedFormat":1},{"version":"827eb54656695635a6e25543f711f0fe86d1083e5e1c0e84f394ffc122bd3ad7","impliedFormat":1},{"version":"2309cee540edc190aa607149b673b437cb8807f4e8d921bf7f5a50e6aa8d609c","impliedFormat":1},{"version":"901de16fbacf42f8cbe7fb2e3dc7d33cd91548a54459438fdface0b30a6a29cd","impliedFormat":1},{"version":"c4a8a08d13b2aba4fb5e798f7d2d5691192d4383cf7c0686845d80a167fc5f42","impliedFormat":1},{"version":"243248596db650ce6f8907cbb3a8076a5ab6888a39c37debc11a81c2f9f9d247","impliedFormat":1},{"version":"1460f16c4b7fc66d2dde3ce1a4ab97d480c27fb84a4e429355a21e76cd471e19","impliedFormat":1},{"version":"c5d73bf762b7b0e75fcdf691e21e31c9db9913931b200b9990f07f49ab2edff3","impliedFormat":1},{"version":"de38157a9f1be75a02ea8358be48a4d50ba31785fedd6a4f9ce7387d77ac9a22","impliedFormat":1},{"version":"76a5f88a99d386a1ea9209a9f8f33a3f2c2f17bc445a4078950a49c0624bae3d","impliedFormat":1},{"version":"65357b3849688962f59c625718650ad31ff59e6c23f244b4086f0d96558405d6","impliedFormat":1},{"version":"3f2fed2d0130ee5356cc1cb8782d7c974d37eccce4f1de871f7744ce61463eb4","impliedFormat":1},{"version":"471486ab7c5c95c3df63c0fbebe6871b9535eedff8b582557dfd66fcbf946d5b","impliedFormat":1},{"version":"45e82f28a80d855bab2355d5e46cc8edd7f2679fc5bfb0905dcf01ce59a5c347","impliedFormat":1},{"version":"48f7cd72c6f8ec5b2f70f50a8d4e6f47494e0d228015efb50c36fc6eab33c7ff","impliedFormat":1},{"version":"a8aa7a344599265ef9c2aba0433a805227b2c9b0e743106fab4d6f0c6966f536","impliedFormat":1},{"version":"806bed2ed4bf1c6ebb74a391531e98d361a0e23a31601fddb33dd90e31436b6d","impliedFormat":1},{"version":"9b92a4d989efc3eeefdca5f95f10267504abc7748ecff400b533cdf54dcdbd68","impliedFormat":1},{"version":"16e6feb2ea29757a0cdd16c8fe2bdf059a612a5474ea30019ae60d9fe309442a","impliedFormat":1},{"version":"2f45dc6e152333f5d28bb660e4543bfd41412b5af16b9a6331b7fd0b4982885e","impliedFormat":1},{"version":"ea4eadfe3d8b0447ecea1cbbf7aad70423cca9350bc9103a1d80cebc37e6bdb3","impliedFormat":1},{"version":"b88645280562793af76ab59052d87e4846ac5ef19af054c729fbb87c73481a59","impliedFormat":1},{"version":"a1f43b06dd37b1f6c5c7821881960dfe55038b468eafb324ad90ce5e9b448d2a","impliedFormat":1},{"version":"15b142d522e96e1962bd54c75560f6994cc8fe9a1640a36de2268fdb95e58fb5","impliedFormat":1},{"version":"de79263f32ea34b3f9282b19251626eeb3a3aef8d96491b731a3c0fb5cca2e77","impliedFormat":1},{"version":"332680a9475bd631519399f9796c59502aa499aa6f6771734eec82fa40c6d654","impliedFormat":1},{"version":"191bee6605de2b5210f29f22df04f5b5e6bdcc1f6e21fb07091d40eeeb75fd72","impliedFormat":1},{"version":"d83f3c0362467589b3a65d3a83088c068099c665a39061bf9b477f16708fa0f9","impliedFormat":1},{"version":"180e527dbc1f5ae2bbb79d0a3db1ada49258783d7e6299559e0f2ed663b4afec","impliedFormat":1},{"version":"29994a97447d10d003957bcc0c9355c272d8cf0f97143eb1ade331676e860945","impliedFormat":1},{"version":"f4260022f7af38e533d364ea62eb7ae01b0a32050033d7f6772073e1dc908025","impliedFormat":1},{"version":"9cddf06f2bc6753a8628670a737754b5c7e93e2cfe982a300a0b43cf98a7d032","impliedFormat":1},{"version":"3f8e68bd94e82fe4362553aa03030fcf94c381716ce3599d242535b0d9953e49","impliedFormat":1},{"version":"63e628515ec7017458620e1624c594c9bd76382f606890c8eebf2532bcab3b7c","impliedFormat":1},{"version":"355d5e2ba58012bc059e347a70aa8b72d18d82f0c3491e9660adaf852648f032","impliedFormat":1},{"version":"311cc121259b3e0c3c08304fc25b525aa02ba0f9bf55b3e7c60b0dbb7422014e","impliedFormat":1},{"version":"74c269b43d39e5ece20b2cca49c14e64c05b01e46407200d7558301d0fcaabf4","impliedFormat":1},{"version":"ec09bd95866efe38cd00ebb79dfa7a26563d600fa4a30db0f7c6d68f8f6d2b06","impliedFormat":1},{"version":"482d0ac70d56aa79941be30da6df28e926a007f835eed70cf7b5f3135368d1f6","impliedFormat":1},{"version":"7dd19397d5a090c9f8cd762bae67bd0ad6f782abe422594fb71168fb578673b0","impliedFormat":1},{"version":"84cbf6204ada0ee2f80493e55e45befa079954788718efd6dcc103183104e3c0","impliedFormat":1},{"version":"ed849d616865076f44a41c87f27698f7cdf230290c44bafc71d7c2bc6919b202","impliedFormat":1},{"version":"9a0a0af04065ddfecc29d2b090659fce57f46f64c7a04a9ba63835ef2b2d0efa","impliedFormat":1},{"version":"10297d22a9209a718b9883a384db19249b206a0897e95f2b9afeed3144601cb0","impliedFormat":1},{"version":"034b8b5912823744c986986f24432bf3fa7bfa671e69316b672f3f2db5166ce4","impliedFormat":1},{"version":"34d206f6ba993e601dade2791944bdf742ab0f7a8caccc661106c87438f4f904","impliedFormat":1},{"version":"05ca49cc7ba9111f6c816ecfadb9305fffeb579840961ee8286cc89749f06ebd","impliedFormat":1},{"version":"14bad5bea17b4e0f2495b5dac89a936e139338e2c87716bffd3c281ae7a9fec6","impliedFormat":1},{"version":"b8a122e51c2ee902b44d86ff806011d216af54be75844a4d3c366d80776a4d33","impliedFormat":1},{"version":"0850c98ca2cccae6ce2aad363f6eb370c401fbc279a64607fff90c0f87973a91","impliedFormat":1},{"version":"d0f62192ec787f1592a5b86760a44350d1c925883a573eadc12d60862890dffe","impliedFormat":1},{"version":"4ef34562ac49a16a1681d51d6e8ece677657782cf1a464e010224cbbe0bb071f","impliedFormat":1},{"version":"a66ad696f2785dd00374b8dee6fab5c58c049c0efe24b3c214fbe6aec3f53d6e","impliedFormat":1},{"version":"f226f29f5594bd479f27648f42423ffa1a1460cafc7b1ba04bd013b28cba710f","impliedFormat":1},{"version":"63f859a315e9711f383d06b7a2b940804e51078d85e896980816f46f1b6021a8","impliedFormat":1},{"version":"f8da2a3bec435c09f9a10d9f150949bf0ebb0c3508f074887dbb2d3e33ce302b","impliedFormat":1},{"version":"397b46c6a95826d26714b5481addc606de72d8229b092e236f0d78a9e7226d29","impliedFormat":1},{"version":"5f47fb5b000c03fdcae71e6e017261898a37f0892532cb713ce95c8950462d80","impliedFormat":1},{"version":"8142d5eaa44b4dbb68dbe87b5f05b148c5a74a6fa7abd3cb3aa80a7ed4a05150","impliedFormat":1},{"version":"6c66369276512eac5b53eaca735d7472b5ffca417977c3976a66b84059f2af2b","impliedFormat":1},{"version":"225deff02f4d1c91e2d6c71dec9f18feae510aa729a9774024f30278f4c6b8fe","impliedFormat":1},{"version":"6c24f6dcbb3bf8235bf8da995a7290ffbd9d557a760cf2deb380ce91a989b765","impliedFormat":1},{"version":"e48415a5a97a5e6c22cc4fdcb3172805ce68a1d41d0a056d71bff9ced2186d43","impliedFormat":1},{"version":"adec507ee458a691b72254991a7903a10c052019d8cdc7ffe359d9f373e7401f","impliedFormat":1},{"version":"9b74326515d17f03809cfbea6de789772ff7d0c759a08a59bfa5242bda98d35b","impliedFormat":1},{"version":"09788d0d992a6d2471f35019ab68b39105c44ebf17f2051cf3e228a39e91885a","impliedFormat":1},{"version":"0ea47413eaffe144782a44058205c31130b382dee0e2f66b62b5188eac57039e","impliedFormat":1},{"version":"c0591738dbfe11a36959f16ab40bc98b2a430c4565770ef6257574546079d791","impliedFormat":1},{"version":"3cf3dc0f53d71795cd7c461346e9aa3c713f8a5138015776aa6d4b8ff9e0cb26","impliedFormat":1},{"version":"ca73451ec7771379b6b1271dcda0d0b2146da80b329136a09ad692529a073965","impliedFormat":1},{"version":"fad74233657c4e0346822942ac3716a20b16fb053ca00c1260a08a81cc76df89","impliedFormat":1},{"version":"43f0a7dead8b25e1c101a060ea31d6df548a9303c58aa7498d0285fd4ecaac1c","impliedFormat":1},{"version":"fced7c59acecb0ac631505fcbc5a1ce0c6420e2494a256321e9359093efb7a1f","impliedFormat":1},{"version":"8c42fbcae55a41f9c48f644ff9743fab827a9d38f5a6bd486f17c6460f8a099b","impliedFormat":1},{"version":"793c2b5a225ca7569320b2f9f3a43cc20a08e83fbd6587f5c24c7a05feaee6d0","impliedFormat":1},{"version":"cf841c4bfb05b4b1d3826773ff77a47bb0dc17c665a4dbff7d6c4a6d9042d50c","impliedFormat":1},{"version":"597b9bd9363840016d7e8c6839cccd22f85079484bae7e444e80cf24645529af","impliedFormat":1},{"version":"bd15222c3f016a97d7062a0018f7fe0d130be508ca276b43dcafa8c9032a3ea4","impliedFormat":1},{"version":"4f5f11b73282262904f4c1bc5ffb76631b40ac8b54ae01bde274cb9242d6cb2f","impliedFormat":1},{"version":"9e6dcb736749cc84304b38c5a1101b299659dbc3871bab5d1544ee8f8dc73e5b","impliedFormat":1},{"version":"3f8f36996bff2e0d4f6bb2b11e1a684489b511032473180f44d0b23f38a53800","impliedFormat":1},{"version":"4e4559e8e4ea7d87f914014074559e515de78308bacc733a7ea76f795de178a3","impliedFormat":1},{"version":"13ecb31795209aa56b1837b9d46cc5494da392f594132bc5b3a56c067e12ea1c","impliedFormat":1},{"version":"e34a28e978cf430e062c91d03987f2b42360b33e6207738b40494acd4a97004b","impliedFormat":1},{"version":"5cc10d0295e594c961bd020cc76845097928f550fa3d58468114e5225054f76c","impliedFormat":1},{"version":"99c4cd704c85c3b9a215977d1d10ad34f1c6bbc5784e0ddaaf6fe8090030eaf3","impliedFormat":1},{"version":"7dc695c4ab01309d1d1b43201700560054feadddf8d0bbfc79d981bff03c1db0","impliedFormat":1},{"version":"ab0db8d8a821582e3f49739e5267466d964b64f4aeea5ba379ac7759c7adc531","impliedFormat":1},{"version":"f6e9e0411de308207892802c52be06bf25df281ace623cbf7870583e28b3f12b","impliedFormat":1},{"version":"307067699cff7c3c8a77d9e69aef5291f0141cfbbbeb0954804589e6701238cc","impliedFormat":1},{"version":"4c8070d4ac9fe0026ceae0b0981cb4dcb3cd92b094ef72c78f9ffe04d09a1df2","impliedFormat":1},{"version":"e58ec6f129b33de21aa444c87a6e538effe82b5f2a7d07c101fc258e2e66f73d","impliedFormat":1},{"version":"e3e78772154f790f8b0a6eaeaba5b4c816567d9d30f9c8655ed29cb9438e5455","impliedFormat":1},{"version":"e17369650fdfac36437e8559b86b2d6172d090594696976082903ba856a2a979","impliedFormat":1},{"version":"69cab7f70e702f3a517d6e47428b4d2d76b56bc355e6f85e305d1b6f6d700f7b","impliedFormat":1},{"version":"8a95a499afe7cd2aa6161b68aa3f7599e9ae7d12697171384f540e268d846746","impliedFormat":1},{"version":"2967840927e96b345f7b8250ad35e7436aae73c77d9767a2b43f2606ab107cf3","impliedFormat":1},{"version":"0e6387b87925a10ba52cd0de685a4f7e2d9dd402dbac560dce8934e8e34007d0","impliedFormat":1},{"version":"77515d8e0ceb4f4bae7d0c9aafe9fd764a5f25601569ecbac3240cacfe136f14","impliedFormat":1},{"version":"b07d72a408bfcf125c24cfba3db38207687711b800b98ab417e5ada59438c554","impliedFormat":1},{"version":"ef5aa9871f3b8dac96d4ef93e22eec539527d739c6a7e0c7fa7101fa343bfd77","impliedFormat":1},{"version":"47507ff33fbe83627bdf11136400594a249e8012508425bb2846b7268f966f00","impliedFormat":1},{"version":"4a1a0f21b3c4fc0d217392d82445a34fcc8c9ed6f79fdc4d14b8353e3c74eaf3","impliedFormat":1},{"version":"e041d566f765b48ddf66ffd085fb120951cf28e92d129751351e72f8c96d99fa","impliedFormat":1},{"version":"5a59c1315ed5ca8f899fc6527f23a15dc9a38107175a5fffd473db25bcd1fd4a","impliedFormat":1},{"version":"20a5515b81a828fc10b066aa5f88a5eb68323d23b8a10d8e9dc7edb6ebdd2bd7","impliedFormat":1},{"version":"f2c24a1e4bc555e203dab5b749b718b1cdf73ecf57d8a8872ff163cbc03b1a68","impliedFormat":1},{"version":"0dcea4e3a40e7408ec9a6aeacb721ce8289fc99d5f2be600146a472d6d1590b8","impliedFormat":1},{"version":"2093f341c5cba3058c29f9344e59ecfa22dc6559c7eb9f00497a108e09301a55","impliedFormat":1},{"version":"15aba6a4199ce9e4f3c1ec397a468f6aaaeba973649ca4f1c016225b46a5feaf","impliedFormat":1},{"version":"a1ca7f7788853a2ff3670bf1112a92fa503686b10d58339318fec5862bd209a6","impliedFormat":1},{"version":"0ad5ed5721170d5b3655b961c86fd1de3649a67cb7a90a9890d8756ea7037e6a","signature":"1f9282c9b10fd65ce6113864325681f68536d55b8ded40b53c6c8e275e46c2e0"},{"version":"3e3540c43bf90c0fc46e0b20e49e9ad6e1922c6043f5aaa218a7f0d74184c5bc","signature":"7d42ccc2e1603f71cd41eed3853b76ebecfc7b16f7c592ea5e7fdd25c56cb2c5"},{"version":"200736356ab6fb37115cf04b53f0ff69cd2e5bad11e61fcb05a342b1690d3b9f","signature":"927879af2d208d7e2ae9b14943333eb8cc5319e9aec8a8eeef5bdc411dd0b1ff"},"dbb1c9a356d46eaa394814d15fefd53b8c56d73311db6ce1165915f877ba68d7","4932de08a709ec20052db7d211f0bd35f7e32f41ca8a24411b6dd330b56e4aa6","2e4056a5df111f0dda39e1e711ef9386135028ef989981e78ac8563c0de5005f","fd64eb3aaf5c177b2a90f4ef46a039f07e5f5c1a352d6ce69600de9ab348c142","dc746958346541bc5448c82bfae5135343ad588801d5730fb81fa788296fe354","edc486f4a2e915288fbb70fea901c7a72dba3a6e48cf2ce62965e9d936642554",{"version":"04de5584b953b03611eeef01ba9948607def8f64f1e7fbc840752b13b4521b52","impliedFormat":1},{"version":"25e5c8b73c6ad21f39e8e72f954090f30b431a993252bccea5bdad4a3d93c760","impliedFormat":1},{"version":"5bf595f68b7c1d46ae8385e3363c6e0d4695b6da58a84c6340489fc07ffc73f8","impliedFormat":1},{"version":"b87682ddc9e2c3714ca66991cdd86ff7e18cae6fd010742a93bd612a07d19697","impliedFormat":1},{"version":"6fd2c680287dd441936fbf38e1b191e126492589b701ea01066a70c434b9ff88","impliedFormat":1},{"version":"86bf2bfe29d0bc3fbc68e64c25ea6eab9bcb3c518ae941012ed75b1e87d391ae","impliedFormat":1},{"version":"3c74d80d1dd95437cc9bbf22d88199e7410fd85af06171327125bcf4025deae8","impliedFormat":1},{"version":"00b4f8b82e78f658b7e269c95d07e55d391235ce34d432764687441177ae7f64","impliedFormat":1},{"version":"e9757a8109200b4efd4334c0c10370e36e19641a624dca2082305d5c5edaaee8","impliedFormat":1},{"version":"10ac50eaf9eb62c048efe576592b14830a757f7ea7ed28ee8deafc19c9845297","impliedFormat":1},{"version":"e75af112e5487476f7c427945fbd76ca46b28285586ad349a25731d196222d56","impliedFormat":1},{"version":"e91adad3da69c366d57067fcf234030b8a05bcf98c25a759a7a5cd22398ac201","impliedFormat":1},{"version":"d7d6e1974124a2dad1a1b816ba2436a95f44feeda0573d6c9fb355f590cf9086","impliedFormat":1},{"version":"464413fcd7e7a3e1d3f2676dc5ef4ebe211c10e3107e126d4516d79439e4e808","impliedFormat":1},{"version":"18f912e4672327b3dd17d70e91da6fcd79d497ba01dde9053a23e7691f56908c","impliedFormat":1},{"version":"2974e2f06de97e1d6e61d1462b54d7da2c03b3e8458ee4b3dc36273bc6dda990","impliedFormat":1},{"version":"d8c1697db4bb3234ff3f8481545284992f1516bc712421b81ee3ef3f226ae112","impliedFormat":1},{"version":"59b6cce93747f7eb2c0405d9f32b77874e059d9881ec8f1b65ff6c068fcce6f2","impliedFormat":1},{"version":"e2c3c3ca3818d610599392a9431e60ec021c5d59262ecd616538484990f6e331","impliedFormat":1},{"version":"e3cd60be3c4f95c43420be67eaa21637585b7c1a8129f9b39983bbd294f9513c","impliedFormat":1},{"version":"6bde3e00980f7ec94aa825e94c5f7a5c4c3c2342a41a8ffd5bc23c750251e58d","impliedFormat":1},{"version":"717b120720ab5a9ed26d119eb1e4024d161a9e4676ec44fb43f95a27d163ec5a","impliedFormat":1},{"version":"67c4e219bccbd87b8e054213e80b1278be8da8b099d45af747dccfb4dfd7af6f","impliedFormat":1},{"version":"c2621930ef221aff59317993158f5d87662eb5bca09d5ce3ae1e41d7187c9384","impliedFormat":1},{"version":"f1a60cadab021078f04200daf4c66ee70d93e5a9123f6fa05ba4128fcf69c0d9","impliedFormat":1},{"version":"27679e96d1bd38c5938178aaf4abe8627493090b63d6bae2ce8436e6a87ebe4d","impliedFormat":1},{"version":"eb967aca6a70476eabb2b2de534f94cf58dd266e7c038d8c54f8d17f990eac96","impliedFormat":1},{"version":"f95c1b0910c4e0432b76a5dbec026aa8b725ac0b107b7bcff20b25ae8101c1bb","impliedFormat":1},{"version":"30ce903610d8fff81c1587d01c033eaaa5f3a9cbdf7daa9da87706cc52f5aa04","impliedFormat":1},{"version":"4c5d2a6d41a3ae593c805120d0eb155036d161eda42cc5d5214b27def43c4ee1","impliedFormat":1},{"version":"dbca9f6a3e220261be9b53a01de40279c841130cc416e284bb1727a56010c43f","impliedFormat":1},{"version":"09beee23a66f0bdf7dc331f4a02e92c2b5394007b64370ed2d27734bc5536491","impliedFormat":1},{"version":"27562a4464551037e4287cf3261d164427754c5bd7c6ad27bf028bec82fc86b2","impliedFormat":1},{"version":"e13b50da243d4d0fd5e2ec6397c1be56a131f0da2c124ffa1fcedd98807f182b","impliedFormat":1},{"version":"35605aa025dccd05aa7bcf114ee4d41f44f6c9d3a7bb7a00aee12b261134e1be","impliedFormat":1},{"version":"9e7e32157e896705d3d96b080d194cecce56cf8d60b3da621cd4268a03caab37","signature":"e2955fdcc1c55e2b5a5e00699b9ac6b9ab23980f2659d633d465af990fae8d48"},{"version":"73e92e788c2f0725dd0ac0988580175b07e2c99b1a8f78b8e8416157f7ccc62b","signature":"84dc4739f9dd422b22924dafd7f13a5216f93168acb26df7b0140d82f2f9aa08"},{"version":"b1bce0a2de893efe9d0ca785201de2909b02a72abfc04308a02bba86a81bd70c","signature":"7e9bb8359ba4066bfbbddfa2898ca4426fb261455e0833f3f3b70423543d5fdf"},{"version":"db2e073fa5f0561b8dffd815122cd9236effb16b28477e5a0199749cd4b3dbd9","signature":"7536446e76a5a61c1e395d9de80100924bcdd2c52261982fad783bd73a5790fb"},"772e99e4b56d6dec0089bae8cae02dfc45050e5acbccb767ba8c8ad944956e34","9223a0889abb0669020e94a9b8c1e68274cdc05533c1f79d84fe516450e94ebd","6a8f2f6d4d9c38a567e0a2d80ab85df8955d6cfb2f9dfd15110e31a9381ef5b3","a20afedbcacf9a51b380a383b7802abda104a81fd544383f003ecf7317f530d9","afb85f8a09758bc9385c900247c0842152ed49702e3d9e96e35b5f71d104e489","718e8f1f79b47e434f80dd17815300ad0d099221fc89a172ff0a95ebe7046c5a","5cbfc5401af4e4f58cb7f93ee4840ad025ed1156144b52b05c0870d1f6a133ce","36462cfc5a2cd85eaa3328b559a5e2be35337d31d6b69ad8692fcd4caaa66067","5ee144290f94daecc0ce8d21a26b06c263d041e8a8d3485b6f988ce571106ec5","3e42a81b9929255789e83f66bc961945b84c4bad3d8c5c11427be8a74b317302",{"version":"332680a9475bd631519399f9796c59502aa499aa6f6771734eec82fa40c6d654","impliedFormat":1},{"version":"e269c531fbbfe9f97dfe0e73b8b7ff1301ad3b9f9fdad63e9df8a96e24365df1","impliedFormat":1},{"version":"d83f3c0362467589b3a65d3a83088c068099c665a39061bf9b477f16708fa0f9","impliedFormat":1},{"version":"da83cf072f354357f28fd842998b7ca345e744fd24d1023851a0eacf9485ac4a","impliedFormat":1},{"version":"29994a97447d10d003957bcc0c9355c272d8cf0f97143eb1ade331676e860945","impliedFormat":1},{"version":"f4260022f7af38e533d364ea62eb7ae01b0a32050033d7f6772073e1dc908025","impliedFormat":1},{"version":"9cddf06f2bc6753a8628670a737754b5c7e93e2cfe982a300a0b43cf98a7d032","impliedFormat":1},{"version":"3f8e68bd94e82fe4362553aa03030fcf94c381716ce3599d242535b0d9953e49","impliedFormat":1},{"version":"63e628515ec7017458620e1624c594c9bd76382f606890c8eebf2532bcab3b7c","impliedFormat":1},{"version":"355d5e2ba58012bc059e347a70aa8b72d18d82f0c3491e9660adaf852648f032","impliedFormat":1},{"version":"311cc121259b3e0c3c08304fc25b525aa02ba0f9bf55b3e7c60b0dbb7422014e","impliedFormat":1},{"version":"74c269b43d39e5ece20b2cca49c14e64c05b01e46407200d7558301d0fcaabf4","impliedFormat":1},{"version":"cde21e822ae5de7b130f6db6b2fce8435e23e33a00a9d62fae06b3982ef38a39","impliedFormat":1},{"version":"482d0ac70d56aa79941be30da6df28e926a007f835eed70cf7b5f3135368d1f6","impliedFormat":1},{"version":"f0715a6654ab21cf89f1c2544b1afc704b60c2ff687b29ee8727ab01e05e8211","impliedFormat":1},{"version":"84cbf6204ada0ee2f80493e55e45befa079954788718efd6dcc103183104e3c0","impliedFormat":1},{"version":"9a0a0af04065ddfecc29d2b090659fce57f46f64c7a04a9ba63835ef2b2d0efa","impliedFormat":1},{"version":"10297d22a9209a718b9883a384db19249b206a0897e95f2b9afeed3144601cb0","impliedFormat":1},{"version":"6bab5c65a7a5bf20eebac39b4a84259351914c2458af04d11819319e01f0aa10","impliedFormat":1},{"version":"84828aa498631f28c5d8d6f53aaa6614ebceec9742f6a31ef1a77452ab8f3890","impliedFormat":1},{"version":"34d206f6ba993e601dade2791944bdf742ab0f7a8caccc661106c87438f4f904","impliedFormat":1},{"version":"f92729b267ea7acf1984487c16c9cf8ae32ec3bc7fbe261d19bce7aad1c7ba1b","impliedFormat":1},{"version":"59e352f43c2035b3828b92d984e2c62073cc5e846e4f1bf1a467459bee11c27c","signature":"ea561e1be308ea6a49a8d7e572a165fe85b4c53077289b87f7adeb7a0d462706"},{"version":"31d78ca9ad30e1aa271ed75be3732645a80ec4b2fc90f076e0e79f303f2a77cb","signature":"a9cdb9f6213b4ed81a42ab6c4602dc79f24af315007c05523b1da9c440d5f980"},{"version":"11aac9a02fdd63366e46ce617b18996c4070fc0d460393feac3d9711d178fcf8","signature":"51afbd6018f8336321e38fc730b0297dc8257ac9307b0f676a5407cf18d353a1"},{"version":"254ac3ce1879fa8d5d46ce97011bb183efce3a387e96dc059a8021924c6f04b4","signature":"f3f562da7847b937cf30e208aabd49623a2bdd2acefd45d041b2e5989e92257f"},{"version":"7cdc6f3c4e12493e7cffcdcc52499955e9c04f0a5f8370c0bc6b295f21587fca","signature":"405d9699b615e5ed9881cf9c3d871724e9d5d544a02676c68348bf182cfd50cc"},{"version":"51e916c700e35d3bbc8faa8ab05d55b71985053f7ac8920f5409beb9fb665a81","signature":"2145899b4b925e57c3ee6367e27736ae8255728fdbfa635db27834c03c0fd86b"},{"version":"baeec9442be0da17f73cbf95bf4edf8b5b36648ca632250129724cc3d48e080d","signature":"dfbe607a985a623d71afe1e141686dd69b0793e387751753fb84a538d24da29a"},{"version":"7952c20e30376023ed4523874a169eaac3c27a8b339cce62da5887291ef452b7","signature":"807f188235f1d6b13e0dd22dd5d0e3babdd9e694bf147b7810f80f3930d3658d"},{"version":"5d9365f103debd67a88706cbafb4d4ed7f7b6d8a2ecef7061443f54df9063054","signature":"8f73398a329589eb3b131795ee1e83413688990f72ffb93d6f37a9de176953b8"},"b840a1609d67a381a8f85270226c9b9418aa0c9b0c34b9f0930662318842db05","334c2caecba98cc185076c0706f77d6577d436715dfeb71898cd82b6f51e6bd8","e878006e2ac2c2d2369b3ed8782ecbd90950bbb7e553685503ad6d9c633e5bb5","f7125dba9ce150e859df764df946755c8de7b8101da0d1c62118da3370ea61d1",{"version":"8b0b6a4c032a56d5651f7dd02ba3f05fbfe4131c4095093633cda3cae0991972","impliedFormat":1},{"version":"192a0c215bffe5e4ac7b9ff1e90e94bf4dfdad4f0f69a5ae07fccc36435ebb87","impliedFormat":1},{"version":"3ef8565e3d254583cced37534f161c31e3a8f341ff005c98b582c6d8c9274538","impliedFormat":1},{"version":"d7e42a3800e287d2a1af8479c7dd58c8663e80a01686cb89e0068be6c777d687","impliedFormat":1},{"version":"1098034333d3eb3c1d974435cacba9bd5a625711453412b3a514774fec7ca748","impliedFormat":1},{"version":"f2388b97b898a93d5a864e85627e3af8638695ebfa6d732ecd39d382824f0e63","impliedFormat":1},{"version":"f130eccfce6e5c42ac92e6d070aa2e41c34312b3a75ce8e2c0dd57e5d3cf1d50","impliedFormat":1},{"version":"f477375e6f0bf2a638a71d4e7a3da8885e3a03f3e5350688541d136b10b762a6","impliedFormat":1},{"version":"a44d6ea4dc70c3d789e9cef3cc42b79c78d17d3ce07f5fd278a7e1cbe824da56","impliedFormat":1},{"version":"55cd8cbc22fe648429a787e16a9cd2dc501a2aafd28c00254ad120ef68a581c0","impliedFormat":1},{"version":"ba4900e9d6f9795a72e8f5ca13c18861821a3fc3ae7858acb0a3366091a47afb","impliedFormat":1},{"version":"7778e2cc5f74ef263a880159aa7fa67254d6232e94dd03429a75597a622537a7","impliedFormat":1},{"version":"8e06a1ef49502a62039eeb927a1bd7561b0bce48bd423a929e2e478fd827c273","impliedFormat":1},{"version":"7ec3d0b061da85d6ff50c337e3248a02a72088462739d88f33b9337dba488c4f","impliedFormat":1},{"version":"2f554c6798b731fc39ff4e3d86aadc932fdeaa063e3cbab025623ff5653c0031","impliedFormat":1},{"version":"fe4613c6c0d23edc04cd8585bdd86bc7337dc6265fb52037d11ca19eeb5e5aaf","impliedFormat":1},{"version":"53b26fbee1a21a6403cf4625d0e501a966b9ccf735754b854366cee8984b711c","impliedFormat":1},{"version":"9ff247206ec5dffdfadddfded2c9d9ad5f714821bb56760be40ed89121f192f4","impliedFormat":1},{"version":"a1a42747e6f2d60162b334556d5355a261e194910e6d7b5f1ad2087668b7964b","impliedFormat":1},{"version":"8c59d8256086ed17676139ee43c1155673e357ab956fb9d00711a7cac73e059d","impliedFormat":1},{"version":"cfe88132f67aa055a3f49d59b01585fa8d890f5a66a0a13bb71973d57573eee7","impliedFormat":1},{"version":"53ce488a97f0b50686ade64252f60a1e491591dd7324f017b86d78239bd232ca","impliedFormat":1},{"version":"50fd11b764194f06977c162c37e5a70bcf0d3579bf82dd4de4eee3ac68d0f82f","impliedFormat":1},{"version":"e0ceb647dcdf6b27fd37e8b0406c7eafb8adfc99414837f3c9bfd28ffed6150a","impliedFormat":1},{"version":"99579aa074ed298e7a3d6a47e68f0cd099e92411212d5081ce88344a5b1b528d","impliedFormat":1},{"version":"096e4ddaa8f0aa8b0ceadd6ab13c3fab53e8a0280678c405160341332eca3cd7","impliedFormat":1},{"version":"415b55892d813a74be51742edd777bbced1f1417848627bf71725171b5325133","impliedFormat":1},{"version":"942ab34f62ac3f3d20014615b6442b6dc51815e30a878ebc390dd70e0dec63bf","impliedFormat":1},{"version":"7a671bf8b4ad81b8b8aea76213ca31b8a5de4ba39490fbdee249fc5ba974a622","impliedFormat":1},{"version":"8e07f13fb0f67e12863b096734f004e14c5ebfd34a524ed4c863c80354c25a44","impliedFormat":1},{"version":"9faa56e38ed5637228530065a9bab19a4dc5a326fbdd1c99e73a310cfed4fcde","impliedFormat":1},{"version":"7d4ad85174f559d8e6ed28a5459aebfc0a7b0872f7775ca147c551e7765e3285","impliedFormat":1},{"version":"d422f0c340060a53cb56d0db24dd170e31e236a808130ab106f7ab2c846f1cdb","impliedFormat":1},{"version":"424403ef35c4c97a7f00ea85f4a5e2f088659c731e75dbe0c546137cb64ef8d8","impliedFormat":1},{"version":"16900e9a60518461d7889be8efeca3fe2cbcd3f6ce6dee70fea81dfbf8990a76","impliedFormat":1},{"version":"6daf17b3bd9499bd0cc1733ab227267d48cd0145ed9967c983ccb8f52eb72d6e","impliedFormat":1},{"version":"e4177e6220d0fef2500432c723dbd2eb9a27dcb491344e6b342be58cc1379ec0","impliedFormat":1},{"version":"ddc62031f48165334486ad1943a1e4ed40c15c94335697cb1e1fd19a182e3102","impliedFormat":1},{"version":"b3f4224eb155d7d13eb377ef40baa1f158f4637aa6de6297dfeeacefd6247476","impliedFormat":1},{"version":"4a168e11fe0f46918721d2f6fcdb676333395736371db1c113ae30b6fde9ccd2","impliedFormat":1},{"version":"5b0a75a5cced0bed0d733bde2da0bbb5d8c8c83d3073444ae52df5f16aefb6ab","impliedFormat":1},{"version":"ef2c1585cad462bdf65f2640e7bcd75cd0dbc45bae297e75072e11fe3db017fa","impliedFormat":1},{"version":"ef809928a4085de826f5b0c84175a56d32dd353856f5b9866d78b8419f8ea9bc","impliedFormat":1},{"version":"6f6eadb32844b0ec7b322293b011316486894f110443197c4c9fbcba01b3b2fa","impliedFormat":1},{"version":"a51e08f41e3e948c287268a275bfe652856a10f68ddd2bf3e3aaf5b8cdb9ef85","impliedFormat":1},{"version":"862f7d760ef37f0ae2c17de82e5fbf336b37d5c1b0dcf39dcd5468f90a7fdd54","impliedFormat":1},{"version":"af48a76b75041e2b3e7bd8eed786c07f39ea896bb2ff165e27e18208d09b8bee","impliedFormat":1},{"version":"cb524ec077f3963e13e85747c6b53fbdf6bf407c84ca1873c6e43da1e96bee6d","impliedFormat":1},{"version":"deb092bc337b2cb0a1b14f3d43f56bc663e1447694e6d479d6df8296bdd452d6","impliedFormat":1},{"version":"041bc1c3620322cb6152183857601707ef6626e9d99f736e8780533689fb1bf9","impliedFormat":1},{"version":"22bd7c75de7d68e075975bf1123de5bccecfd06688afff2e2022b4c70bfc91c3","impliedFormat":1},{"version":"128e7c2ffd37aa29e05367400d718b0e4770cefb1e658d8783ec80a16bc0643a","impliedFormat":1},{"version":"076ac4f2d642c473fa7f01c8c1b7b4ef58f921130174d9cf78430651f44c43ec","impliedFormat":1},{"version":"396c1e5a39706999ec8cc582916e05fcb4f901631d2c192c1292e95089a494d9","impliedFormat":1},{"version":"89df75d28f34fc698fe261f9489125b4e5828fbd62d863bbe93373d3ed995056","impliedFormat":1},{"version":"8ccf5843249a042f4553a308816fe8a03aa423e55544637757d0cfa338bb5186","impliedFormat":1},{"version":"93b44aa4a7b27ba57d9e2bad6fb7943956de85c5cc330d2c3e30cd25b4583d44","impliedFormat":1},{"version":"a0c6216075f54cafdfa90412596b165ff85e2cadd319c49557cc8410f487b77c","impliedFormat":1},{"version":"3c359d811ec0097cba00fb2afd844b125a2ddf4cad88afaf864e88c8d3d358bd","impliedFormat":1},{"version":"3c0b38e8bf11bf3ab87b5116ae8e7b2cad0147b1c80f2b77989dea6f0b93e024","impliedFormat":1},{"version":"8df06e1cd5bb3bf31529cc0db74fa2e57f7de1f6042726679eb8bc1f57083a99","impliedFormat":1},{"version":"d62f09256941e92a95b78ae2267e4cf5ff2ca8915d62b9561b1bc85af1baf428","impliedFormat":1},{"version":"e6223b7263dd7a49f4691bf8df2b1e69f764fb46972937e6f9b28538d050b1ba","impliedFormat":1},{"version":"d9b59eb4e79a0f7a144ee837afb3f1afbc4dab031e49666067a2b5be94b36bd4","impliedFormat":1},{"version":"1db014db736a09668e0c0576585174dbcfd6471bb5e2d79f151a241e0d18d66b","impliedFormat":1},{"version":"8a153d30edde9cefd102e5523b5a9673c298fc7cf7af5173ae946cbb8dd48f11","impliedFormat":1},{"version":"abaaf8d606990f505ee5f76d0b45a44df60886a7d470820fcfb2c06eafa99659","impliedFormat":1},{"version":"51a66bfa412057e786a712733107547ceb6f539061f5bf1c6e5a96e4ccf4f83c","impliedFormat":1},{"version":"d92a80c2c05cf974704088f9da904fe5eadc0b3ad49ddd1ef70ca8028b5adda1","impliedFormat":1},{"version":"fbd7450f20b4486c54f8a90486c395b14f76da66ba30a7d83590e199848f0660","impliedFormat":1},{"version":"ece5b0e45c865645ab65880854899a5422a0b76ada7baa49300c76d38a530ee1","impliedFormat":1},{"version":"62d89ac385aeab821e2d55b4f9a23a277d44f33c67fefe4859c17b80fdb397ea","impliedFormat":1},{"version":"f4dee11887c5564886026263c6ee65c0babc971b2b8848d85c35927af25da827","impliedFormat":1},{"version":"fb8dd49a4cd6d802be4554fbab193bb06e2035905779777f32326cb57cf6a2c2","impliedFormat":1},{"version":"e403ecdfba83013b5eb0e648a92ce182bff2a45ccb81db3035a69081563c2830","impliedFormat":1},{"version":"82d3e00d56a71fc169f3cf9ec5f5ffcc92f6c0e67d4dfc130dafe9f1886d5515","impliedFormat":1},{"version":"b8d57effce2d49a5493debbd8c644e8d52fbe66e2c6d451371375ef5f7bccb8e","impliedFormat":1},{"version":"856024d913cc60b17cf63970f2707ed549c562ccf6ccbb14a171df5a4d1ea44f","impliedFormat":1},{"version":"1b33478647aa1b771314745807397002a410c746480e9447db959110999873ce","impliedFormat":1},{"version":"a14e7c48debe27b25ddf7932e6976c4f58123e32be8384c3f91b0a4d9f67c2f0","impliedFormat":1},{"version":"7e6a96b383da9f5acb848bb9dedb9ac8489df7cec46bbf26aeaed2610f709078","impliedFormat":1},{"version":"9fac6ebf3c60ced53dd21def30a679ec225fc3ff4b8d66b86326c285a4eebb5a","impliedFormat":1},{"version":"8cb83cb98c460cd716d2a98b64eb1a07a3a65c7362436550e02f5c2d212871d1","impliedFormat":1},{"version":"07bc8a3551e39e70c38e7293b1a09916867d728043e352b119f951742cb91624","impliedFormat":1},{"version":"e47adc2176f43c617c0ab47f2d9b2bb1706d9e0669bf349a30c3fe09ddd63261","impliedFormat":1},{"version":"7fec79dfd7319fec7456b1b53134edb54c411ba493a0aef350eee75a4f223eeb","impliedFormat":1},{"version":"189c489705bb96a308dcde9b3336011d08bfbca568bcaf5d5d55c05468e9de7a","impliedFormat":1},{"version":"98f4b1074567341764b580bf14c5aabe82a4390d11553780814f7e932970a6f7","impliedFormat":1},{"version":"1dd24cbf39199100fbe2f3dbd1c7203c240c41d95f66301ecc7650ae77875be1","impliedFormat":1},{"version":"2e252235037a2cd8feebfbf74aa460f783e5d423895d13f29a934d7655a1f8be","impliedFormat":1},{"version":"3bd10a31e9066676e0af937c2ef2507451281861ae294d04c7c46e46706140d9","impliedFormat":1},"cd5a165a9d51a46669a414aa5a67250ae74347802ce80198745009bd59cb5432"],"root":[611,[627,639],[754,762],[798,811],[834,846],938],"options":{"allowSyntheticDefaultImports":true,"declaration":false,"emitDecoratorMetadata":true,"esModuleInterop":true,"experimentalDecorators":true,"module":1,"outDir":"./dist","removeComments":true,"rootDir":"./src","skipLibCheck":true,"sourceMap":true,"strict":true,"target":9},"referencedMap":[[615,1],[614,2],[343,3],[330,3],[67,3],[319,4],[320,4],[321,3],[322,5],[332,6],[323,4],[324,7],[325,3],[326,3],[327,4],[328,4],[329,4],[331,8],[339,9],[341,3],[338,3],[345,10],[342,3],[340,3],[336,11],[337,12],[344,13],[346,14],[333,3],[335,15],[334,16],[273,3],[276,17],[272,3],[894,3],[274,3],[275,3],[349,18],[350,18],[351,18],[352,18],[353,18],[354,18],[355,18],[348,19],[356,18],[370,20],[357,18],[347,3],[358,18],[359,18],[360,18],[361,18],[362,18],[363,18],[364,18],[365,18],[366,18],[367,18],[368,18],[369,18],[378,21],[376,22],[375,3],[374,3],[377,23],[418,24],[68,3],[69,3],[70,3],[876,25],[72,26],[882,27],[881,28],[262,29],[263,26],[398,3],[292,3],[293,3],[399,30],[264,3],[400,3],[401,31],[71,3],[266,32],[267,33],[265,34],[268,32],[269,3],[271,35],[283,36],[284,3],[289,37],[285,3],[286,3],[287,3],[288,3],[290,3],[291,38],[297,39],[300,40],[298,3],[299,3],[318,41],[301,3],[302,3],[925,42],[282,43],[280,44],[278,45],[279,46],[281,3],[309,47],[303,3],[312,48],[305,49],[310,50],[308,51],[311,52],[306,53],[307,54],[295,55],[314,56],[296,57],[316,58],[317,59],[304,3],[313,3],[270,3],[277,60],[315,61],[384,62],[379,3],[385,63],[380,64],[381,65],[382,66],[383,67],[386,68],[391,69],[389,70],[390,70],[397,71],[387,3],[388,72],[392,69],[394,73],[396,74],[395,75],[410,76],[403,77],[404,78],[405,78],[406,79],[407,79],[408,78],[409,78],[402,80],[412,81],[411,82],[414,83],[413,84],[415,85],[371,86],[373,87],[294,3],[372,55],[416,88],[393,89],[417,90],[419,5],[555,91],[556,92],[560,93],[420,3],[426,94],[553,95],[554,96],[421,3],[422,3],[425,97],[423,3],[424,3],[558,3],[559,98],[557,99],[561,100],[763,101],[847,102],[867,103],[868,104],[869,3],[870,105],[871,106],[880,107],[873,108],[877,109],[885,110],[883,5],[884,111],[874,112],[886,3],[888,113],[889,114],[890,115],[879,116],[875,117],[899,118],[887,119],[914,120],[872,121],[915,122],[912,123],[913,5],[937,124],[862,125],[858,126],[860,127],[911,128],[853,129],[901,130],[900,3],[861,131],[908,132],[865,133],[909,3],[910,134],[863,135],[864,136],[859,137],[857,138],[852,3],[905,139],[918,140],[916,5],[848,5],[904,141],[849,12],[850,104],[851,142],[855,143],[854,144],[917,145],[856,146],[893,147],[891,113],[892,148],[902,12],[903,149],[906,150],[921,151],[922,152],[919,153],[920,154],[923,155],[924,156],[926,157],[898,158],[895,159],[896,4],[897,148],[928,160],[927,161],[934,162],[866,5],[930,163],[929,5],[932,164],[931,3],[933,165],[878,166],[907,167],[936,168],[935,5],[767,169],[768,170],[782,171],[770,172],[769,173],[764,174],[765,3],[766,3],[781,175],[772,176],[773,176],[774,176],[775,176],[777,177],[776,176],[778,178],[779,179],[771,3],[780,180],[609,3],[562,3],[563,3],[566,181],[590,182],[567,3],[568,3],[569,5],[572,3],[570,3],[591,3],[573,3],[574,183],[575,3],[571,3],[576,5],[577,3],[578,184],[580,185],[581,3],[583,186],[584,185],[586,187],[592,188],[587,184],[588,184],[585,3],[593,189],[598,190],[610,191],[589,3],[579,184],[597,192],[564,3],[582,193],[595,194],[596,3],[594,3],[599,195],[600,5],[605,196],[601,5],[602,5],[603,5],[604,5],[565,3],[607,187],[606,3],[608,197],[626,198],[612,3],[616,199],[613,200],[617,3],[618,201],[490,202],[491,202],[492,203],[429,204],[493,205],[494,206],[495,207],[427,3],[496,208],[497,209],[498,210],[499,211],[500,212],[501,213],[502,213],[503,214],[504,215],[505,216],[506,217],[430,3],[428,3],[507,218],[508,219],[509,220],[551,221],[510,222],[511,223],[512,222],[513,224],[514,225],[515,226],[516,227],[517,227],[518,227],[519,228],[520,229],[521,230],[522,231],[523,232],[524,233],[525,233],[526,234],[527,3],[528,3],[529,235],[530,236],[531,237],[532,235],[533,238],[534,239],[535,240],[536,241],[537,242],[538,243],[539,244],[540,245],[541,246],[542,247],[543,248],[544,249],[545,250],[546,251],[547,252],[431,222],[432,3],[433,253],[434,254],[435,3],[436,255],[437,3],[481,256],[482,257],[483,258],[484,258],[485,259],[486,3],[487,205],[488,260],[489,257],[548,261],[549,262],[550,263],[625,264],[624,265],[741,266],[640,3],[656,267],[738,268],[721,269],[722,270],[720,271],[723,3],[724,3],[729,272],[725,3],[726,3],[727,3],[728,3],[735,273],[750,274],[736,275],[739,276],[734,277],[742,278],[737,268],[732,279],[743,280],[733,281],[744,282],[704,3],[746,283],[731,284],[745,276],[747,285],[730,270],[749,286],[691,3],[692,3],[695,287],[693,3],[659,3],[694,3],[753,288],[658,289],[641,3],[647,290],[660,291],[662,292],[698,293],[719,294],[699,3],[652,295],[700,296],[701,297],[702,3],[703,3],[654,298],[706,299],[707,300],[643,3],[651,301],[708,3],[697,302],[709,3],[718,3],[661,303],[690,275],[710,3],[646,304],[711,3],[712,3],[713,3],[715,305],[714,306],[716,307],[705,308],[696,309],[717,310],[655,311],[663,3],[648,3],[664,3],[667,312],[653,313],[649,314],[650,3],[665,301],[666,315],[642,3],[748,316],[657,317],[752,318],[751,319],[682,320],[683,321],[681,322],[669,323],[674,324],[675,325],[678,326],[677,327],[676,328],[679,329],[686,330],[689,331],[688,332],[687,333],[680,334],[670,335],[685,336],[672,337],[668,338],[673,339],[671,323],[644,3],[645,340],[684,3],[552,322],[788,3],[826,341],[827,342],[825,322],[813,343],[818,344],[819,345],[822,346],[821,347],[820,348],[823,349],[829,350],[833,351],[832,352],[831,353],[824,354],[814,335],[828,355],[830,344],[816,356],[812,338],[817,357],[815,343],[787,358],[786,3],[795,3],[785,359],[796,360],[794,361],[783,3],[790,362],[791,359],[792,3],[784,363],[793,364],[797,365],[789,366],[740,3],[623,367],[620,3],[622,368],[621,3],[619,3],[66,3],[261,369],[234,3],[212,370],[210,370],[260,371],[225,372],[224,372],[125,373],[76,374],[232,373],[233,373],[235,375],[236,373],[237,376],[136,377],[238,373],[209,373],[239,373],[240,378],[241,373],[242,372],[243,379],[244,373],[245,373],[246,373],[247,373],[248,372],[249,373],[250,373],[251,373],[252,373],[253,380],[254,373],[255,373],[256,373],[257,373],[258,373],[75,371],[78,376],[79,376],[80,376],[81,376],[82,376],[83,376],[84,376],[85,373],[87,381],[88,376],[86,376],[89,376],[90,376],[91,376],[92,376],[93,376],[94,376],[95,373],[96,376],[97,376],[98,376],[99,376],[100,376],[101,373],[102,376],[103,376],[104,376],[105,376],[106,376],[107,376],[108,373],[110,382],[109,376],[111,376],[112,376],[113,376],[114,376],[115,380],[116,373],[117,373],[131,383],[119,384],[120,376],[121,376],[122,373],[123,376],[124,376],[126,385],[127,376],[128,376],[129,376],[130,376],[132,376],[133,376],[134,376],[135,376],[137,386],[138,376],[139,376],[140,376],[141,373],[142,376],[143,387],[144,387],[145,387],[146,373],[147,376],[148,376],[149,376],[154,376],[150,376],[151,373],[152,376],[153,373],[155,376],[156,376],[157,376],[158,376],[159,376],[160,376],[161,373],[162,376],[163,376],[164,376],[165,376],[166,376],[167,376],[168,376],[169,376],[170,376],[171,376],[172,376],[173,376],[174,376],[175,376],[176,376],[177,376],[178,388],[179,376],[180,376],[181,376],[182,376],[183,376],[184,376],[185,373],[186,373],[187,373],[188,373],[189,373],[190,376],[191,376],[192,376],[193,376],[211,389],[259,373],[196,390],[195,391],[219,392],[218,393],[214,394],[213,393],[215,395],[204,396],[202,397],[217,398],[216,395],[203,3],[205,399],[118,400],[74,401],[73,376],[208,3],[200,402],[201,403],[198,3],[199,404],[197,376],[206,405],[77,406],[226,3],[227,3],[220,3],[223,372],[222,3],[228,3],[229,3],[221,407],[230,3],[231,3],[194,408],[207,409],[63,3],[64,3],[12,3],[10,3],[11,3],[16,3],[15,3],[2,3],[17,3],[18,3],[19,3],[20,3],[21,3],[22,3],[23,3],[24,3],[3,3],[25,3],[26,3],[4,3],[27,3],[31,3],[28,3],[29,3],[30,3],[32,3],[33,3],[34,3],[5,3],[35,3],[36,3],[37,3],[38,3],[6,3],[42,3],[39,3],[40,3],[41,3],[43,3],[7,3],[44,3],[49,3],[50,3],[45,3],[46,3],[47,3],[48,3],[8,3],[54,3],[51,3],[52,3],[53,3],[55,3],[9,3],[56,3],[65,3],[57,3],[58,3],[60,3],[59,3],[61,3],[1,3],[62,3],[14,3],[13,3],[456,410],[469,411],[453,412],[470,303],[479,413],[444,414],[445,415],[443,416],[478,322],[473,417],[477,418],[447,419],[466,420],[446,421],[476,422],[441,423],[442,417],[448,424],[449,3],[455,425],[452,424],[439,426],[480,427],[471,428],[459,429],[458,424],[460,430],[463,431],[457,432],[461,433],[474,322],[450,434],[451,435],[464,436],[440,303],[468,437],[467,424],[454,435],[462,438],[465,439],[472,3],[438,3],[475,440],[846,441],[629,442],[630,443],[628,444],[635,445],[636,446],[634,447],[638,448],[639,449],[637,444],[758,450],[759,451],[757,444],[755,452],[756,453],[754,454],[611,5],[761,455],[762,456],[760,444],[800,457],[801,458],[799,459],[798,460],[802,461],[938,462],[805,463],[806,464],[807,465],[804,444],[803,466],[627,467],[809,468],[810,469],[811,470],[808,471],[835,472],[836,473],[837,472],[838,474],[834,475],[840,476],[841,477],[842,478],[839,471],[844,479],[845,480],[843,444],[632,481],[633,482],[631,471]],"version":"6.0.3"} \ No newline at end of file diff --git a/docs/code-quality-audit-20260828.md b/docs/code-quality-audit-20260828.md new file mode 100644 index 0000000..8ac67d0 --- /dev/null +++ b/docs/code-quality-audit-20260828.md @@ -0,0 +1,238 @@ +# CMPP 平台代码质量审查报告 + +- 报告日期:2026-08-28 +- 审查类型:当前工作区只读基线审计 +- 审查仓库基线:`171c7d38e8f17de0dd83570603da316d47c0d015`;对应业务代码基线为其父提交 `a70e9e2c078a6109dd87cf8e45aeff956faf24e8` +- 分支状态:`main`,相对 `origin/main` 超前 12 个提交 +- 审查范围:React/Vite 前端、NestJS API、Prisma/PostgreSQL、Redis/BullMQ 接口、Go Gateway、自动化测试、构建与工程配置 +- 明确未执行:提交、推送、部署、数据库写入、短信发送、远程环境变更、生产/预生产利用验证 + +## 1. 执行摘要 + +本次综合判定为:**有条件不通过,当前版本不建议继续发布**。 + +代码已经具备较好的业务复杂度承载能力:API 45 个测试套件共 532 项全部通过,Gateway 全包测试与 `go vet` 通过,前后端正式构建、Prisma Schema、Gateway 队列契约和已有安全部署校验均通过;PostgreSQL 连接池、事务级 advisory lock、`FOR UPDATE SKIP LOCKED` 和核心业务复合索引也已实际存在。 + +但审查发现 2 个 P0 发布阻断问题: + +1. 客户端租户范围来自浏览器可修改的 `x-tenant-id` 请求头,后端多个客户端接口直接信任该值,部分写接口还直接信任请求体中的 `tenantId`,存在跨租户读取、创建、修改或删除业务数据的风险。 +2. 用户密码使用无盐单次 SHA-256 保存和比对。数据库一旦泄漏,相同密码可直接关联,并可使用 GPU/字典进行高速离线破解,不符合生产账号系统的密码存储要求。 + +因此,本报告不把“532 项测试通过”解释为生产可发布,也不把现有 mock 单测解释为真实 PostgreSQL、Redis、MinIO、Gateway 和 CMPP 全链验收完成。 + +## 2. 质量评级 + +| 维度 | 评级 | 结论 | +|---|---:|---| +| 业务正确性与并发设计 | B | 关键发送链测试较多,存在 advisory lock、幂等和队列 claim 机制;未做本轮真实全链复验。 | +| 安全性 | D | 存在租户越权和弱密码哈希两个 P0。 | +| 自动化测试 | C | API/Gateway 测试数量较好;API 语句覆盖率 59.61%、分支覆盖率 50.99%,前端测试文件为 0,且没有覆盖率门槛。 | +| 前端性能 | C- | 主 JavaScript 包 2.12 MB,gzip 626 KB,所有页面同步进入主包。 | +| 可维护性 | C | 存在超大 Service/Page、无 lint/format 门禁、生产类型仍依赖 `src/mock`。 | +| 数据库工程 | B | Schema 有效,核心复合索引、连接池和锁策略较完整;未对真实数据执行 `EXPLAIN (ANALYZE, BUFFERS)`。 | +| 依赖与仓库卫生 | C- | 依赖审计仍有 2 个 high advisory;构建缓存被跟踪,临时文件和产物缺少统一忽略策略。 | + +综合参考分:**58/100**。该分数用于排序治理工作,不等价于功能验收通过率。 + +## 3. 发布阻断问题 + +### CQ-SEC-001 / P0:客户端租户身份可由请求方伪造 + +**证据链** + +- `src/api/session.ts:175-177` 从浏览器 `localStorage` 中的展示会话读取 `tenantId`。 +- `src/api/core/httpClient.ts:90-92`、`117-119` 将该值写入 `x-tenant-id`。 +- `api/src/common/tenant-id.decorator.ts:3-6` 直接返回客户端请求头中的 `x-tenant-id`,没有绑定已认证用户。 +- `api/src/auth/session-validation.middleware.ts:25-55` 验证会话用户、状态和 portal,但没有把用户所属租户写成服务端可信租户上下文,也没有校验请求头租户等于用户租户。 +- `api/src/certification/certification.controller.ts:13-19` 的客户端认证查询信任请求头,提交接口直接接受请求体。 +- `api/src/certification/certification.service.ts:49-82` 的提交逻辑直接使用 `data.tenantId` 创建认证记录、更新目标企业并写操作日志。 +- `api/src/sms-config/client-sms-config.controller.ts:26-163` 大量客户端查询和写接口依赖同一 `@TenantId()`;其中应用密钥重置、应用状态变更、材料创建等接口甚至没有传入租户范围。 + +**影响** + +已登录企业管理员可以修改浏览器存储、请求头、请求体或资源 ID,尝试访问其他企业的认证、应用、签名、模板、账务、日志等数据。文件服务已有“从当前会话用户反查租户”的正确实现,但该保护没有成为客户端 API 的统一规则。 + +**最小修复要求** + +1. 在服务端认证中间件中根据 `sessionUserId` 获取并写入可信 `request.tenantId`;客户端控制器只能读取该字段。 +2. 客户端路由禁止把 `x-tenant-id` 或请求体 `tenantId` 作为授权依据;即使保留请求头,也只能用于一致性检查,不得决定数据范围。 +3. 所有按资源 ID 的客户端读写在数据库查询中同时带上可信 `tenantId`。 +4. 增加 tenant-a/tenant-b 的真实 API 越权回归,覆盖查询、创建、更新、删除、密钥重置、上传下载和导出。 + +### CQ-SEC-002 / P0:用户密码采用无盐 SHA-256 + +**证据链** + +- `api/src/users/users.service.ts:444-445`:`createHash('sha256').update(password).digest('hex')`。 +- `api/src/users/users.service.ts:141`、`213`、`267` 使用该函数创建或修改密码。 +- `api/src/auth/auth.service.ts:62` 使用字符串相等比较验证密码。 + +**影响** + +密码哈希没有用户级 salt,也没有故意增加计算和内存成本。数据库泄漏后,弱密码可被高速离线破解;相同密码生成相同哈希,还会泄露账号之间的密码复用关系。 + +**最小修复要求** + +1. 改用 Argon2id;如运行环境暂不支持,可使用带独立 salt 和合理成本参数的 scrypt/bcrypt。 +2. 新哈希保存算法版本与参数;验证旧 SHA-256 成功后立即透明升级,避免一次性强制所有账号重置。 +3. 使用库提供的恒定时间验证接口,不直接比较哈希字符串。 +4. 增加旧哈希迁移、错误密码、参数升级、密码修改和 sessionVersion 失效测试。 + +## 4. 高优先级问题 + +### CQ-API-001 / P1:API 缺少统一运行时输入校验 + +- `api/src/main.ts:23-27` 创建应用并配置 body parser,但没有注册全局 `ValidationPipe`。 +- 搜索未发现 `@IsString`、`@IsInt` 等 `class-validator` 规则。 +- 统计到约 166 个控制器 `@Body()` 入口;多数 DTO 是 TypeScript interface 或内联类型,运行时会被擦除。 +- `api/src/open-api/open-api.dto.ts:3-14` 仅包含 Swagger 装饰器,没有输入校验装饰器。 + +风险包括错误类型进入服务层、超长字符串、额外字段、枚举外状态和不一致的 400/500 响应。建议建立 DTO class、`transform: true`、`whitelist: true`、`forbidNonWhitelisted: true` 的统一策略,并分批为高风险写接口补齐字段长度、枚举、数组大小和格式限制。 + +### CQ-AUTH-001 / P1:验证码和匿名失败计数为进程内无界 Map + +- `api/src/auth/auth.service.ts:20-21` 使用两个模块级 `Map`。 +- `createCaptcha()` 会持续写入记录;过期记录只有在同一 captchaId 被再次提交时才删除。 +- 匿名失败以任意登录名为 key 写入,没有容量限制或周期清理。 + +这会造成多实例登录状态不一致,并可通过大量验证码请求或随机登录名制造内存增长。建议迁移到 Redis,使用 TTL、原子计数、IP+账号双维度限流和固定容量保护。 + +### CQ-FE-001 / P1:前端主包过大且没有路由级代码分割 + +当前生产构建结果: + +- JavaScript:2,123.63 KB,gzip 626.18 KB。 +- CSS:270.61 KB,gzip 39.84 KB。 +- Vite 明确产生“chunk 大于 500 KB”警告。 +- `src/routes/AppRoutes.tsx:2-59` 同步导入全部运营端和客户端页面;代码中未发现 `React.lazy()` 或动态 `import()`。 +- `src/components/ui/Chart.tsx:3` 使用 `import * as echarts from 'echarts'`,进一步扩大主包。 + +建议先按 admin/client 及页面路由使用 `React.lazy` + `Suspense` 分包,再按需引入 ECharts 图表模块。应在 CI 增加 bundle budget,例如初始 JS gzip 不高于 250 KB、单异步 chunk gzip 不高于 180 KB;实际阈值可在首轮拆包后校准。 + +### CQ-TEST-001 / P1:测试覆盖和验收层级不足 + +- API:45 套、532 项通过;全源覆盖率为 statements 59.61%、branches 50.99%、functions 60.33%、lines 62.35%。 +- `api/jest.config.cjs` 没有 `coverageThreshold`。 +- `src/` 下前端 `*.spec.*` / `*.test.*` 文件数量为 0。 +- Gateway 内部包覆盖率约 49.7% 至 100%,但 `cmd/gateway` 只有 0.7%,另两个命令包为 0%。 +- 本轮测试没有启动真实 PostgreSQL、Redis、MinIO 或 CMPP 模拟器,不能证明真实后端闭环。 + +建议先为租户隔离、认证、账务、发送幂等、回执关联建立真实 PostgreSQL/Redis 集成测试;前端至少覆盖登录、权限、加载/空/错误、筛选分页和高风险确认操作;随后逐步设置覆盖率门槛,避免一次性追求无意义的高百分比。 + +## 5. 中优先级问题 + +### CQ-DEP-001 / P2:生产依赖仍有 2 个 high advisory + +`pnpm audit --prod --json` 返回: + +1. `react-router 7.18.1`:GHSA-qwww-vcr4-c8h2,修复版本 `>=7.18.2`。 +2. `nanoid 3.3.16`:GHSA-2v37-7h3g-55p8,修复版本 `>=3.3.18`。 + +现有 `security:verify` 已证明项目没有使用 React Router RSC 模式,并验证了既有 PostCSS 缓解,因此当前可利用面低于审计工具的原始 high 评级;但版本仍处于公告范围,不能长期依赖“功能未使用”作为供应链治理。建议升级后重新执行构建、API 测试、前端浏览器回归和安全校验。 + +### CQ-MAINT-001 / P2:超大文件与缺失静态风格门禁 + +- `api/src/send-chain/send-inbound-entry.service.ts` 约 84 KB。 +- `api/src/send-chain/send-gateway-submit.service.ts` 约 52 KB。 +- `src/apps/admin/AdminDownstreamDeliveriesPage.tsx` 约 44 KB。 +- 根项目和 API 均没有 lint/format 脚本,也未发现 ESLint/Prettier 配置。 + +建议先按“持久化、状态机、队列 claim、路由、计费”拆分 send-chain 服务;页面按筛选、表格、详情和恢复操作拆分。拆分时要求行为不变,并依靠当前测试防回归。 + +### CQ-CONFIG-001 / P2:数据库连接存在硬编码开发默认凭据 + +- `api/prisma.config.ts:8-11`。 +- `api/src/prisma/prisma.service.ts:39-41`。 + +当 `DATABASE_URL` 缺失时,进程会尝试使用 `cmpp/cmpp_password` 连接本机数据库。建议生产角色 fail closed;仅在显式 `NODE_ENV=development` 或专用本地配置下允许开发默认值。 + +### CQ-ARCH-001 / P2:真实 API 代码仍与 mock 目录耦合 + +- `src/mock/` 保留完整 localStorage mock service。 +- `src/apps/admin/auditColumns.tsx:3` 的生产组件仍从 `@/mock` 导入业务类型。 + +当前没有证据表明 mock service 仍被页面运行时调用,但该目录和类型依赖会误导后续开发,并增加重新接入静态数据的风险。建议把共享类型迁移到 `src/api/types` 或 domain 模块,并在构建/检查中禁止 `src/apps/**` 导入 `src/mock/**`。 + +### CQ-REPO-001 / P2:仓库产物和审计基线不稳定 + +- `api/tsconfig.build.tsbuildinfo` 已被 Git 跟踪,每次构建产生无业务意义的修改。 +- `.gitignore` 没有统一忽略 `*.tsbuildinfo`、`outputs/` 和任务临时文件。 +- 最终工作区仍存在 `=`, `outputs/`, `pnpm-lock.yaml`, `tmp_generate_ui_drafts.py` 等既有未跟踪内容;审计窗口内还曾出现随后被并发流程处理的临时部署脚本。 +- 审计开始时 HEAD 为 `4d4c1f3`,期间其他会话先后提交了业务代码 `a70e9e2` 和部署记录 `171c7d3`;本报告已对新业务代码重新运行前端类型检查和生产构建,但完整 API 覆盖率运行发生在该纯前端提交之前。 + +建议统一包管理器与唯一锁文件,忽略纯构建缓存,并在正式审查/发布时使用固定 commit 或独立 worktree,避免审计结论对应移动目标。 + +## 6. 已通过门禁 + +| 检查项 | 结果 | +|---|---| +| 前端 TypeScript `tsc --noEmit` | 通过;并发提交后已重跑 | +| Vite 生产构建 | 通过;存在包体警告 | +| API TypeScript 正式构建 | 通过 | +| API Jest | 45/45 套、532/532 项通过 | +| API 覆盖率 | statements 59.61%,branches 50.99%,functions 60.33%,lines 62.35% | +| Gateway `go test ./... -count=1` | 通过 | +| Gateway `go vet ./...` | 通过 | +| Gateway `go test ./... -cover` | 通过;各包覆盖率差异较大 | +| Prisma Schema | 95 个迁移目录;`prisma validate` 通过 | +| Gateway 队列契约 | 5/5 样例通过 | +| 依赖缓解/安全部署脚本 | 通过 | +| `pnpm audit --prod` | 不通过;2 个 high advisory | +| `git diff --check` | 通过 | + +## 7. 积极发现 + +1. PostgreSQL 连接按 API、Worker、Outbox、Callback 和 Protocol Log 角色设置独立连接池上限。 +2. 账务、日配额、频控等竞争资源使用事务级 advisory lock。 +3. 多个队列 claim 使用 `FOR UPDATE SKIP LOCKED`,适合并发消费者。 +4. `SmsMessageRecord`、`SmsBatchTask`、`CmppDownstreamDelivery` 等高频表具备 tenant/status/time 复合索引。 +5. Webhook 主链对协议、内网地址、环回地址和 DNS 解析做了 SSRF 防护,并在 HTTP 客户回调中固定解析后的目标地址。 +6. 会话 cookie 使用 HttpOnly、SameSite=Lax,并按环境控制 Secure;前端 localStorage 保存的是会话展示元数据,不是 session token。 +7. API、Gateway 和队列契约测试已经覆盖大量发送、补发、回执去重和异常分支。 + +## 8. 未验证边界 + +本报告不能替代以下验证: + +- 真实 PostgreSQL 数据量下的 `EXPLAIN (ANALYZE, BUFFERS)` 和慢查询分析。 +- Redis Stream/BullMQ pending、lag、重试、宕机恢复和重复消费验证。 +- MinIO 上传、下载、租户隔离和大文件边界。 +- 登录后的真实浏览器 UI、控制台、网络请求、加载/空/错误/刷新状态。 +- Gateway 与 CMPP 模拟器或真实供应商的 Submit、长短信、回执、上行和断线恢复闭环。 +- 测试、预生产、生产当前部署 commit、迁移数、配置和服务状态。 + +在完成 P0 修复前,不建议通过真实环境攻击性测试证明越权;应先补自动化隔离回归,再在隔离测试环境验证。 + +## 9. 建议整改顺序 + +### 第一批:发布阻断 + +1. 服务端统一可信租户上下文,关闭请求头/请求体决定客户端租户的能力。 +2. 迁移密码哈希到 Argon2id/scrypt,并提供旧哈希透明升级。 +3. 补 tenant-a/tenant-b 越权测试和密码迁移测试。 + +### 第二批:安全与质量门禁 + +1. 建立全局运行时 DTO 校验。 +2. 将验证码、失败计数和限流迁移到 Redis TTL/原子计数。 +3. 升级两个公告依赖并保持安全校验通过。 +4. 在 CI 增加 lint、format check、覆盖率门槛、依赖审计和 bundle budget。 + +### 第三批:性能与可维护性 + +1. 前端路由级分包并按需加载 ECharts。 +2. 拆分 send-chain 超大服务和超大页面。 +3. 清理 mock 类型耦合、构建缓存和临时产物治理。 +4. 在隔离真实后端完成 API/DB/Redis/MinIO/Gateway/CMPP 全链回归。 + +## 10. 验收出口标准 + +整改完成至少应满足: + +- P0 为 0,P1 有明确关闭证据或书面风险接受。 +- tenant-a 用户无法通过 header、body、query 或资源 ID 访问 tenant-b 数据。 +- 新密码使用强哈希;旧 SHA-256 账号登录后自动升级,数据库不再新增 SHA-256 密码。 +- 前端初始 JS 包达到约定预算,核心路由按需加载。 +- API/Gateway/前端测试与构建全部通过;覆盖率不低于本报告基线且建立门槛。 +- 依赖审计不再包含本报告两项 high advisory。 +- 真实 PostgreSQL、Redis、MinIO、Gateway 和 CMPP 测试证据与 `docs/testing-progress.md` 同步。 diff --git a/docs/code-quality-remediation-plan-20260828.md b/docs/code-quality-remediation-plan-20260828.md new file mode 100644 index 0000000..0c84b6a --- /dev/null +++ b/docs/code-quality-remediation-plan-20260828.md @@ -0,0 +1,402 @@ +# CMPP 平台代码质量整改方案 + +- 方案日期:2026-08-28 +- 对应审计报告:`docs/code-quality-audit-20260828.md` +- 当前复核基线:`c3bf8af3e6fc8bac0ac5e104f09a3d6b68506b27` +- 适用范围:React/Vite 前端、NestJS API、Prisma/PostgreSQL、Redis、Go Gateway、测试与工程门禁 +- 当前结论:两个 P0 均已在当前代码中确认;P0 关闭前不得继续发布到预生产或生产 +- 环境边界:只允许在本地和测试环境实施、部署与验证;预生产不得部署、覆盖或回退,除非再次取得明确授权 + +## 1. 整改目标 + +本轮整改以“先消除发布阻断,再建立防复发门禁,最后处理性能和可维护性”为原则。完成后应达到: + +1. 客户端租户身份只来自服务端已认证会话,任何请求头、请求体、查询参数和资源 ID 都不能改变数据归属范围。 +2. 新增和修改的密码全部使用带版本及参数信息的强密码哈希;旧 SHA-256 账号在成功登录后透明迁移。 +3. 高风险写接口具备统一、可预测的运行时输入校验和错误响应。 +4. 验证码、失败计数和登录限流支持多实例一致性、TTL、原子操作与容量控制。 +5. 建立能够阻止租户越权、弱密码、依赖漏洞、包体回退和测试覆盖下降的自动化门禁。 +6. 前端首屏资源按路由拆分,工程产物、mock 类型和超大模块进入可持续治理状态。 + +## 2. 范围、依赖与验证成本 + +| 批次 | 范围 | 关键依赖 | 主要验证 | 相对工作量 | +|---|---|---|---|---:| +| R0 | 固定基线、资产与测试数据 | Git、测试数据库、Redis、MinIO、测试账号 | 恢复演练、基线门禁 | 0.5~1 人日 | +| R1 | 可信租户上下文和全客户端接口隔离 | User.tenantId、会话中间件、Prisma | 双租户真实 API 回归 | 4~7 人日 | +| R2 | 密码强哈希与透明迁移 | Argon2id 或 scrypt、User.passwordHash | 兼容登录、迁移、会话失效 | 2~4 人日 | +| R3 | DTO 校验、验证码与限流 | class-validator、Redis | 接口契约、多实例、过期与并发 | 4~7 人日 | +| R4 | 依赖、CI、测试门禁 | 唯一包管理器、浏览器测试框架 | 全量构建、测试、安全审计 | 3~6 人日 | +| R5 | 前端分包与 ECharts 按需加载 | Vite、React Router | 包体、浏览器、弱网首屏 | 2~4 人日 | +| R6 | 超大模块、mock、配置和仓库治理 | 既有回归测试 | 行为等价、构建、Git 清洁度 | 5~10 人日 | +| R7 | 测试环境真实链路验收 | PostgreSQL、Redis、MinIO、Gateway、CMPP 模拟器 | 全链证据和恢复演练 | 2~5 人日 | + +以上是工作量级别,不是承诺工期。R1 涉及的客户端控制器和资源类型较多,实际工作量取决于租户资源清单和集成测试基础设施。最小充分范围是先完成 R0~R2;R3~R7 不得反向阻塞 P0 修复,但没有完成相应门禁的项目不能视为质量治理闭环。 + +## 3. 总体实施顺序 + +```text +R0 固定基线和恢复资产 + ├─ R1 可信租户上下文 ─┐ + └─ R2 密码哈希迁移 ───┴─ P0 安全门禁通过 + │ + R3 输入校验和登录状态 + │ + R4 测试与工程门禁 + │ + R5 前端性能 + R6 可维护性 + │ + R7 测试环境全链验收 +``` + +R1 和 R2 可以在独立分支并行开发,但必须分别完成测试后再合并。R3 的全局校验可能改变大量接口响应,不应与 R1 混在同一个大提交中。 + +## 4. R0:基线、恢复资产和变更控制 + +### 4.1 实施内容 + +1. 为整改建立固定 Git 基线,记录完整 commit、分支及工作区状态;不得把现有未跟踪文件或其他会话改动误纳入提交。 +2. 部署测试环境前重新建立并校验恢复资产: + - 当前服务包、配置和 systemd/Nginx 配置备份; + - PostgreSQL 可恢复备份; + - Redis 关键 key/stream/queue 状态快照; + - MinIO 关键 bucket 和租户测试文件清单; + - 当前部署 commit、Prisma 迁移数和服务健康状态。 +3. 准备两个隔离企业及账号:`tenant-a/user-a`、`tenant-b/user-b`。两边分别准备认证、应用、签名、模板、任务、记录、账务、日志和文件样本。 +4. 记录 P0 修复前的接口契约,但不执行真实环境攻击性验证,不保留可直接复用的生产攻击脚本。 + +### 4.2 出口标准 + +- 恢复资产的位置、校验值、恢复命令和验证结果有记录。 +- 两个租户的测试数据可重复创建,不使用生产数据。 +- 工作目录的既有脏文件归属已记录,整改提交不混入无关文件。 + +## 5. R1:关闭客户端跨租户访问 + +### 5.1 服务端可信租户上下文 + +修改 `api/src/auth/session-validation.middleware.ts`: + +1. 查询会话用户时同时读取 `tenantId`。 +2. 对 `/api/client/**` 请求强制要求有效 `tenantId`;缺失时返回明确的 403,而不是继续执行无租户过滤查询。 +3. 将可信字段写入请求上下文,例如: + +```ts +request.authContext = { + userId: user.id, + portal: result.record.portal, + tenantId: user.tenantId, +}; +``` + +4. 新建只读取服务端上下文的装饰器,例如 `@CurrentTenantId()`。原 `@TenantId()` 不再作为客户端授权依据。 +5. 若为兼容旧前端暂时保留 `x-tenant-id`,只允许进行一致性校验:请求头存在且与可信租户不一致时返回 403,并写入安全审计日志;请求头不得决定查询范围。 +6. 运营端按明确权限跨租户查询的能力继续使用运营端查询参数,不复用客户端租户装饰器。 + +### 5.2 控制器和服务层整改 + +必须逐一盘点所有 `/api/client/**` 路由,不仅修改审计报告列举的两个控制器。处理规则如下: + +| 接口类型 | 必须采用的规则 | +|---|---| +| 列表查询 | `where` 必须包含可信 `tenantId`;不得因 `undefined` 省略过滤 | +| 单条详情 | 使用 `findFirst({ where: { id, tenantId } })` 或等价复合条件,不得只按 `id` 查询 | +| 创建 | 服务端覆盖 `tenantId`,忽略或拒绝请求体中的 `tenantId` | +| 更新/删除 | 更新前按 `{ id, tenantId }` 验证资源;事务内再次带租户条件 | +| 密钥重置/状态变更 | 同时传入可信 `tenantId`,禁止只凭资源 ID 操作 | +| 上传/下载 | 文件对象和所属业务对象都必须校验租户;下载不得仅凭 fileObjectId | +| 导入/导出 | 导出查询和异步任务 payload 都固化可信 `tenantId` | +| 关联资源 | 应用、签名、模板、通道能力等外键必须属于同一租户或是明确的公共资源 | + +首批重点文件包括但不限于: + +- `api/src/certification/certification.controller.ts` +- `api/src/certification/certification.service.ts` +- `api/src/sms-config/client-sms-config.controller.ts` +- 客户端账务、短信任务、发送记录、上行短信、日志、文件和报表相关控制器及服务 + +建议建立一个租户资源清单,记录“路由、动作、资源、服务层方法、租户条件、测试编号、整改状态”。不能只用全局搜索替代清单验收。 + +### 5.3 数据库约束与审计 + +1. 优先使用现有 `tenantId` 列和复合索引,不为了本轮修复盲目大改数据库模型。 +2. 对经常使用“资源 ID + tenantId”的高频表检查复合索引;新增索引前在测试数据上执行 `EXPLAIN (ANALYZE, BUFFERS)`。 +3. 对租户头不一致、资源归属不一致和客户端传入 tenantId 的尝试记录安全事件,但日志不得包含密码、应用密钥或完整敏感资料。 +4. 业务层建议增加统一的 `assertTenantResource` 或仓储查询约束,但不能用一个可选 tenantId 参数制造新的绕过路径。 + +### 5.4 必测用例 + +每类资源至少覆盖: + +- A 查询 A:成功。 +- A 查询 B:404 或 403,响应不得暴露 B 是否存在。 +- A 创建时提交 B 的 tenantId:拒绝或由服务端强制覆盖为 A。 +- A 更新、删除、提交审核、重置密钥、变更状态时使用 B 的资源 ID:失败且 B 数据不变。 +- 省略、伪造、重复或大小写变化的 `x-tenant-id`:不能扩大数据范围。 +- 批量 ID 中混入 B 的资源:整个请求原子失败,或者只处理 A 且返回明确结果;不得静默操作 B。 +- 文件上传、下载、导出和异步任务执行后仍保持租户隔离。 +- 管理端经授权跨租户操作保持原有能力,客户端权限收紧不能误伤运营端。 + +### 5.5 出口标准 + +- 所有客户端路由使用服务端可信租户上下文。 +- 资源 ID 操作均有租户复合条件。 +- tenant-a/tenant-b 自动化测试覆盖查询、创建、更新、删除、密钥、文件、导入导出和异步任务。 +- 安全测试失败时构建门禁失败。 + +## 6. R2:密码哈希升级与旧账号透明迁移 + +### 6.1 算法与存储格式 + +首选使用成熟库实现 Argon2id,并保存库生成的 PHC 字符串,例如 `$argon2id$...`,其中包含算法、版本、成本、salt 和哈希。不要自行拼接 salt 或自行实现密码学算法。 + +参数必须在目标 API 机器上基准测试后确定。目标是单次登录验证具备足够成本,但不造成登录接口不可接受的延迟或并发耗尽。参数写入配置及文档,不能散落为魔法数字。 + +如果目标运行环境无法稳定安装 Argon2 原生依赖,可以使用 Node 标准库 `scrypt` 作为备选;使用独立随机 salt、版本化格式、固定上限和恒定时间比较。不得继续新增 SHA-256 哈希。 + +### 6.2 兼容迁移流程 + +```text +用户提交密码 + ├─ passwordHash 是 Argon2id/新格式 + │ └─ 使用库验证;参数过旧则成功后重新哈希 + └─ passwordHash 是 64 位旧 SHA-256 + ├─ 旧算法验证失败:正常失败 + └─ 旧算法验证成功:同一登录流程内写入新哈希并完成登录 +``` + +实施要求: + +1. 新建统一的 `PasswordHasher` 服务,集中负责 `hash`、`verify`、`needsRehash` 和旧格式识别。 +2. 创建用户、管理员改密、用户自助改密、密码重置全部调用该服务。 +3. 旧哈希透明升级使用条件更新或事务,避免并发登录覆盖更新后的哈希。 +4. 密码修改后继续递增 `sessionVersion`,保证旧会话失效。 +5. 日志中只记录迁移成功/失败及用户 ID,不记录密码或完整哈希。 +6. 暂不批量强制重置全部账号;对长期不登录的旧账号可在后续治理中安排强制重置。 + +### 6.3 必测用例 + +- 新账号数据库中不再产生 64 位裸 SHA-256。 +- 旧 SHA-256 正确密码能够登录,登录后立即变为新格式。 +- 旧 SHA-256 错误密码不能触发迁移。 +- 新格式正确/错误密码验证正确。 +- 参数升级后 `needsRehash` 能透明更新。 +- 管理员改密、自助改密、重置密码都使用新格式并撤销旧会话。 +- 并发登录不会把新哈希覆盖回旧哈希。 +- 算法验证异常不会回退到“直接字符串相等”。 + +### 6.4 回退策略 + +代码回退必须继续具备读取新哈希的能力。因此密码迁移上线后,不允许回退到只认识 SHA-256 的旧版本。推荐先部署“双读新旧、只写新格式”的兼容版本;验证稳定后再移除旧哈希写入代码。数据库字段通常无需迁移,但必须先确认长度足以保存 PHC 字符串。 + +### 6.5 出口标准 + +- 全部密码写路径只写新格式。 +- 旧账号登录后可验证地完成透明迁移。 +- 密码和会话相关自动化测试通过。 +- 测试数据库扫描证明没有新产生的旧 SHA-256 哈希。 + +## 7. R3:输入校验、验证码和登录限流 + +### 7.1 DTO 运行时校验 + +不要直接一次性对全部 181 个左右的 `@Body()` 入口开启严格拒绝,否则可能造成大面积兼容性回归。分三步实施: + +1. 先建立 DTO class 和统一异常格式,在认证、租户资源、密码、密钥、文件、发送、账务等高风险写接口启用。 +2. 对已覆盖 DTO 的模块启用 `transform`、`whitelist` 和 `forbidNonWhitelisted`。 +3. 所有模块完成 DTO 转换和兼容验证后,再提升为全局 `ValidationPipe`。 + +每个 DTO 至少考虑:字符串长度、trim 策略、枚举、手机号/日期格式、数组数量、单项长度、分页上限、文件大小、嵌套对象和额外字段。校验失败统一返回稳定的 400 错误码和字段列表,不返回 Internal server error。 + +### 7.2 验证码和失败计数 + +复用现有 `ioredis` 依赖,但应抽出共享 Redis 连接/服务,避免每个模块各自维护连接。建议 key 设计: + +- `auth:captcha:`:TTL 2~5 分钟,验证使用原子读取并删除。 +- `auth:fail:account:`:滑动或固定窗口计数。 +- `auth:fail:ip:`:IP 维度计数。 +- `auth:lock:`:明确锁定 TTL。 + +要求: + +1. 使用 Lua 或等价 Redis 原子命令完成“校验并消费”和计数/过期设置。 +2. 对 key 长度和账号归一化进行限制,避免任意超长输入制造内存压力。 +3. 登录接口结合可信代理配置获得真实来源 IP,不能盲目信任任意 `X-Forwarded-For`。 +4. Redis 不可用时采用明确的安全策略并报警;不能悄悄退回无限 Map。是否 fail closed 应结合运营可用性评审后固化。 +5. 增加验证码请求频率和总容量保护。 + +### 7.3 出口标准 + +- 高风险写接口全部具有运行时 DTO 校验。 +- 多 API 实例共享验证码和锁定状态。 +- 过期 key 自动清理,随机账号/验证码压测下 Redis 内存增长受控。 +- 错误请求稳定返回 4xx,不出现未处理 500。 + +## 8. R4:依赖、测试与 CI 门禁 + +### 8.1 依赖整改 + +1. 将 `react-router-dom`/`react-router` 升级到已修复的兼容版本,至少 `7.18.2`。 +2. 通过 PostCSS 或包管理器解析结果把 NanoID 升级到至少 `3.3.18`。 +3. 统一使用一种包管理器和唯一锁文件;在决定前不要直接删除现有锁文件。 +4. 升级后执行前端构建、登录/路由浏览器回归、安全校验和生产依赖审计。 + +React Router 公告只影响不稳定 RSC 路径,NanoID 公告需要特定零长度自定义生成器调用;当前项目可利用面较低,但版本治理仍应关闭公告。 + +### 8.2 自动化测试优先级 + +新增测试优先级如下: + +1. 租户隔离和密码迁移。 +2. 账务余额、发送幂等、任务审核、回执关联和补发。 +3. 登录、权限、加载/空/错误/刷新、筛选分页及高风险确认操作的前端测试。 +4. PostgreSQL、Redis 和 MinIO 集成测试。 +5. Gateway/CMPP 模拟器的 Submit、长短信、回执、上行和断线恢复。 + +覆盖率门槛应先以当前实测基线为下限,新增或修改文件要求更高的增量覆盖率,再逐批提高。不以补无意义断言换取百分比。 + +### 8.3 CI 建议门禁 + +每次合并至少执行: + +- `git diff --check` +- 前端 TypeScript 和 Vite 正式构建 +- API TypeScript 正式构建和 Jest +- Gateway `go test ./... -count=1` 与 `go vet ./...` +- Prisma validate 和迁移一致性检查 +- tenant-a/tenant-b 安全回归 +- lint、format check +- 生产依赖审计 +- bundle budget +- 禁止 `src/apps/**` 导入 `src/mock/**` +- 禁止新增裸 SHA-256 密码写入代码 + +任何 P0 安全回归、构建、迁移验证或依赖阻断项失败都不得合并。 + +## 9. R5:前端路由分包和图表瘦身 + +### 9.1 实施内容 + +1. 在 `src/routes/AppRoutes.tsx` 按 admin/client 和页面路由使用 `React.lazy` 与 `Suspense`。 +2. 登录页、布局骨架和通用错误页保留轻量同步加载;大型报表、审计、监控和详情页异步加载。 +3. 为异步路由提供统一加载、失败和重试界面,避免白屏。 +4. ECharts 改为按需注册图表、组件和渲染器;确认所有现有图表类型仍正常。 +5. 在 Vite 构建产物中记录初始 JS、CSS、最大异步 chunk 和 gzip 大小。 + +### 9.2 建议预算 + +- 初始 JavaScript gzip:首轮目标不高于 250 KB;如果受框架公共依赖限制,可基于首轮拆包结果书面校准。 +- 单个异步 chunk gzip:不高于 180 KB。 +- 不允许新增页面使初始包超过已验收基线。 + +### 9.3 浏览器验收 + +覆盖客户端和运营端:首次访问、直接打开深层 URL、登录后跳转、刷新、后退/前进、异步加载失败、权限不足、移动端宽度及弱网。浏览器控制台不得出现 chunk 加载、路由和图表初始化错误。 + +## 10. R6:可维护性、配置、mock 和仓库治理 + +### 10.1 超大模块拆分 + +拆分遵循“先测试锁定行为,再机械移动,最后优化结构”: + +- `send-inbound-entry.service.ts`:按接入解析、持久化、长短信聚合、业务工作流、频控/预留拆分。 +- `send-gateway-submit.service.ts`:按队列 claim、路由选择、Gateway 提交、Outbox/Redis、重试恢复拆分。 +- `AdminDownstreamDeliveriesPage.tsx`:按筛选条件、统计、表格、详情、恢复操作拆分。 + +每次只移动一个职责,提交中不同时改变业务规则。拆分后保持事务边界、锁顺序、幂等键和队列语义不变。 + +### 10.2 数据库配置 + +1. 生产、预生产和测试服务启动时,`DATABASE_URL` 缺失必须立即失败。 +2. 开发默认连接只允许在显式 development/test 模式下使用,并在启动日志中标明非生产配置。 +3. 对 `REDIS_URL`、MinIO、会话密钥、应用加密主密钥等关键配置建立同类启动校验。 +4. 日志不得输出完整连接串和密码。 + +### 10.3 mock 解耦 + +1. 把 `auditColumns.tsx` 所需业务类型迁移到 `src/api/types` 或独立 domain 类型文件。 +2. 生产组件禁止导入 `src/mock/**`。 +3. 在确认没有运行时引用后,再决定保留 mock 作为测试夹具还是删除;不能只为“目录干净”贸然删除可能仍被工具使用的内容。 + +### 10.4 仓库卫生 + +1. `.gitignore` 增加 `*.tsbuildinfo`、明确的本地产物和任务临时目录规则。 +2. 对已经被 Git 跟踪的构建缓存使用 `git rm --cached` 停止跟踪,但保留本地文件;操作前确认没有业务用途。 +3. 统一锁文件后再清理其他锁文件,并通过全新目录可重复安装验证。 +4. 正式审计和发布使用固定 commit 或独立 worktree,报告记录 commit、依赖锁摘要和构建产物校验值。 +5. 不自动删除当前工作区的 `=`, `outputs/`, `tmp_generate_ui_drafts.py` 等既有内容,必须先确认归属和可恢复性。 + +## 11. R7:测试环境验收与发布策略 + +### 11.1 测试环境部署前 + +- 重新建立并校验恢复资产,不复用上一次“已经备份”的口头结论。 +- 核对部署目标为 `100.93.204.60` 测试环境,不连接预生产服务器。 +- 记录旧 commit、新 commit、迁移计划、服务包校验值和回退步骤。 +- 密码新格式上线后,回退包必须仍能读取新哈希。 + +### 11.2 测试环境真实验证 + +1. 双租户 API 隔离矩阵全部通过。 +2. 旧密码登录透明迁移、新密码登录、改密和会话撤销通过。 +3. PostgreSQL 数据实际落库且租户归属正确。 +4. Redis 验证码、锁定、TTL、多实例一致性和故障策略通过。 +5. MinIO 上传、下载、租户隔离和大文件边界通过。 +6. 登录后的客户端/运营端页面完成加载、空、错误、刷新和权限状态检查。 +7. Gateway/CMPP 模拟器完成 Submit、长短信、状态报告、上行和断线恢复。 +8. 短信结果必须以最终平台状态、数据库/队列和下游证据闭环,不能以 SubmitResp 成功代替最终送达。 + +### 11.3 发布门禁 + +测试环境验收通过不自动授权预生产发布。预生产仍需单独明确授权,并在发布前重新核对: + +- P0 为 0; +- P1 已关闭或有书面风险接受; +- 数据库迁移可向前执行且回退边界明确; +- 新旧密码格式兼容; +- 全量自动化与真实链路证据齐全; +- 目标环境恢复资产已重新建立; +- `docs/testing-progress.md`、审计问题清单和部署记录同步。 + +## 12. 问题关闭标准 + +| 编号 | 关闭证据 | +|---|---| +| CQ-SEC-001 | 服务端可信租户上下文;客户端路由清单;双租户真实 API 自动化;资源 ID、文件、导入导出和异步任务均无越权 | +| CQ-SEC-002 | 新哈希写入证据;旧 SHA-256 透明迁移;并发和 sessionVersion 测试;回退版本兼容新格式 | +| CQ-API-001 | 高风险接口 DTO class;统一 400 格式;全局或分模块严格 ValidationPipe;异常输入回归 | +| CQ-AUTH-001 | Redis TTL/原子计数;多实例一致性;容量和故障策略测试 | +| CQ-FE-001 | 路由异步 chunk;ECharts 按需加载;包体预算和真实浏览器回归 | +| CQ-TEST-001 | 安全/业务集成测试;前端关键状态测试;覆盖率门槛不低于确认后的基线 | +| CQ-DEP-001 | 锁文件中版本已修复;生产依赖审计不再报告对应公告;构建和浏览器回归通过 | +| CQ-MAINT-001 | 超大模块按职责拆分;行为等价测试通过;lint/format 门禁启用 | +| CQ-CONFIG-001 | 非开发环境缺失关键配置时启动失败;开发回退显式且不泄露凭据 | +| CQ-ARCH-001 | 生产代码不再导入 `src/mock/**`;禁止规则进入 CI | +| CQ-REPO-001 | 唯一锁文件;构建缓存不再被跟踪;审计基线固定;工作区产物有明确治理规则 | + +## 13. 建议提交拆分 + +为降低审查和回退风险,建议至少拆为以下独立提交: + +1. `security: bind client tenant scope to authenticated session` +2. `test: add cross-tenant isolation regression matrix` +3. `security: migrate password hashing with legacy upgrade` +4. `test: cover password migration and session revocation` +5. `security: add runtime DTO validation to high-risk APIs` +6. `security: move captcha and login throttling to Redis` +7. `build: upgrade audited dependencies and add quality gates` +8. `perf: split routes and load charts on demand` +9. `refactor: split send-chain responsibilities without behavior change` +10. `chore: decouple mock types and normalize repository artifacts` + +任何提交都不应同时包含测试环境部署资产、无关 UI 修改或其他会话的工作区文件。 + +## 14. 最终交付物 + +- 整改后的源代码和逐项可审查提交。 +- 租户资源/路由清单及关闭状态。 +- 自动化测试结果、覆盖率、依赖审计和 bundle 报告。 +- 测试环境真实 API、数据库、Redis、MinIO、Gateway、CMPP 和浏览器证据。 +- 密码迁移统计,只记录格式数量,不导出密码哈希。 +- 恢复资产、部署记录和回退验证记录。 +- 更新后的 `docs/testing-progress.md` 与代码质量问题关闭清单。 diff --git a/package-lock.json b/package-lock.json index c4f250a..b20e1fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "lucide-react": "^1.18.0", "react": "^19.2.7", "react-dom": "^19.2.7", - "react-router-dom": "7.18.1", + "react-router-dom": "7.18.2", "vite": "^8.0.16", "zustand": "^5.0.14" }, @@ -277,9 +277,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -296,9 +293,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -315,9 +309,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -334,9 +325,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -353,9 +341,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -372,9 +357,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -479,6 +461,7 @@ "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } @@ -489,6 +472,7 @@ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -869,9 +853,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -892,9 +873,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -915,9 +893,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -938,9 +913,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1062,9 +1034,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -1111,6 +1083,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -1151,6 +1124,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -1160,6 +1134,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -1168,9 +1143,9 @@ } }, "node_modules/react-router": { - "version": "7.18.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", - "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -1190,12 +1165,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.18.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz", - "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", "license": "MIT", "dependencies": { - "react-router": "7.18.1" + "react-router": "7.18.2" }, "engines": { "node": ">=20.0.0" @@ -1346,6 +1321,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", diff --git a/package.json b/package.json index 4f925a9..d046178 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,10 @@ "spike:bullmq": "node api/src/spike/bullmq-link-spike.mjs", "test:api": "npm --prefix api test", "test:gateway": "npm run spike:gateway", + "lint": "npm run quality:verify && tsc --noEmit", + "format:check": "git diff --check", + "quality:verify": "node tools/quality/verify-code-quality.mjs", + "bundle:verify": "node tools/quality/verify-bundle-budget.mjs", "verify:phase1": "npm run spike:contracts && npm run spike:gateway && npm run spike:bullmq && npm run prisma:generate && npm run build:api && npm run build", "verify:phase2": "npm run verify:phase1", "verify:phase3": "npm run verify:phase2", @@ -25,7 +29,8 @@ "verify:phase5": "npm run verify:phase4", "verify:phase6": "npm run verify:phase5", "verify:phase7": "npm run verify:phase6", - "verify:phase8": "npm run verify:phase7" + "verify:phase8": "npm run verify:phase7", + "verify:quality": "npm run lint && npm run format:check && npm run build && npm run bundle:verify && npm --prefix api run test:coverage" }, "dependencies": { "@vitejs/plugin-react": "^6.0.2", @@ -36,7 +41,7 @@ "lucide-react": "^1.18.0", "react": "^19.2.7", "react-dom": "^19.2.7", - "react-router-dom": "7.18.1", + "react-router-dom": "7.18.2", "vite": "^8.0.16", "zustand": "^5.0.14" }, @@ -47,6 +52,7 @@ "typescript": "^6.0.3" }, "overrides": { + "nanoid": "3.3.18", "postcss": "8.5.23" } } diff --git a/src/api/types/audit.ts b/src/api/types/audit.ts new file mode 100644 index 0000000..c978232 --- /dev/null +++ b/src/api/types/audit.ts @@ -0,0 +1,11 @@ +export type AuditStatus = 'pending' | 'approved' | 'rejected'; + +export type AuditItem = { + id: string; + customer: string; + type: '模板' | '签名'; + content: string; + risk: 'low' | 'medium' | 'high'; + status: AuditStatus; + submittedAt: string; +}; diff --git a/src/apps/admin/AdminDownstreamDeliveriesPage.tsx b/src/apps/admin/AdminDownstreamDeliveriesPage.tsx index c2dde34..fb073ef 100644 --- a/src/apps/admin/AdminDownstreamDeliveriesPage.tsx +++ b/src/apps/admin/AdminDownstreamDeliveriesPage.tsx @@ -3,164 +3,7 @@ import { AlertTriangle, BarChart3, CheckCircle2, Eye, RefreshCw, Search, TimerRe import { adminApi, type DownstreamDeliveryDashboard, type DownstreamDeliveryRecord, type DownstreamRequeuePreview, type DownstreamRequeueTask, type DownstreamRequeueTaskItem, type EnterpriseApplication, type TenantOption } from '@/api/adminApi'; import { Breadcrumb, Button, DateRangeInput, Input, Modal, Pagination, Select, Tag, Textarea, type DateRangeValue } from '@/components/ui'; import { formatDateTime } from '@/utils/dateTime'; - -const statusTone: Record = { - pending: 'warning', - manual_requeueing: 'info', - awaiting_ack: 'info', - delivered: 'success', - failed: 'danger', - unconfirmed: 'warning', - rejected: 'danger', -}; - -const statusLabel: Record = { - manual_requeueing: '人工重投处理中', - awaiting_ack: '等待客户端确认', - delivered: '客户端已确认', - failed: '投递失败', - unconfirmed: '客户端未确认', - rejected: '客户端拒绝', -}; - -function deliveryStatusLabel(record: DownstreamDeliveryRecord) { - if (record.status !== 'pending') return statusLabel[record.status] ?? record.status; - if (record.manualRetryCount > 0) return '人工重投排队中'; - if (record.retryCount > 0) return '等待自动重试'; - return '待首次投递'; -} - -const deliveryTypeLabel: Record = { - receipt: '状态回执', - uplink: '上行短信', -}; - -const attemptStatusLabel: Record = { - awaiting_ack: '等待客户端确认', - acknowledged: '客户端已确认', - rejected: '客户端拒绝', - failed: '投递失败', -}; - -const requeueTaskStatusLabel: Record = { - queued: '排队中', running: '执行中', paused: '已暂停', completed: '已完成', - partial_completed: '部分完成', terminated: '已终止', -}; - -const requeueItemStatusLabel: Record = { - queued: '排队中', processing: '处理中', waiting_connection: '等待连接', - waiting_external_ack: '等待其他链路确认', waiting_ack: '等待客户确认', success: '成功', - failed: '失败', skipped: '跳过', unprocessed: '未处理', -}; - -function requeueTone(status: string) { - if (status === 'completed' || status === 'success') return 'success' as const; - if (status === 'partial_completed' || status === 'failed') return 'danger' as const; - if (status === 'paused' || status === 'skipped' || status === 'unprocessed') return 'warning' as const; - return 'info' as const; -} - -type RequeueTarget = - | { kind: 'single'; record: DownstreamDeliveryRecord } - | { kind: 'batch'; ids: string[] }; - -type RequeueResult = { - status: 'success' | 'partial' | 'failed'; - title: string; - message: string; - failures?: string[]; -}; - -function formatLocalDate(value: Date) { - return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`; -} - -function recentSevenDays(): DateRangeValue { - const end = new Date(); - const start = new Date(end); - start.setDate(end.getDate() - 6); - return { start: formatLocalDate(start), end: formatLocalDate(end) }; -} - -function attemptStatusTone(status: string) { - if (status === 'acknowledged') return 'success' as const; - if (status === 'rejected' || status === 'failed') return 'danger' as const; - if (status === 'awaiting_ack') return 'info' as const; - return 'neutral' as const; -} - -function DeliveryDetailModal({ record, onClose }: { record: DownstreamDeliveryRecord; onClose: () => void }) { - const payloadText = useMemo(() => JSON.stringify(record.payload ?? {}, null, 2), [record.payload]); - - return ( -

下游投递详情

{record.id}

} - footer={} - > -
-
-
企业{record.tenant?.name ?? record.tenantId}
-
应用{record.application?.name ?? record.applicationId}
-
投递类型{deliveryTypeLabel[record.deliveryType] ?? record.deliveryType}
-
当前状态{deliveryStatusLabel(record)}
-
消息 ID{record.messageId ?? '-'}
-
自动重试次数{record.retryCount}
-
人工重投次数{record.manualRetryCount}
-
最近人工重投{record.lastRetriedAt ? formatDateTime(record.lastRetriedAt) : '-'}
-
下次重试{record.nextRetryAt ?? '-'}
-
自动重试{record.retryEnabled ? '开启' : '关闭'}
-
写出时间{record.sentAt ?? '-'}
-
确认时间{record.acknowledgedAt ?? '-'}
-
ACK Result{record.ackResult ?? '-'}
-
ACK Sequence_Id{record.ackSequenceId ?? '-'}
-
ACK Msg_Id{record.ackMessageId ?? '-'}
-
连接 ID{record.connectionId ?? '-'}
-
最后错误{record.lastError ?? '-'}
-
-
-

逐次投递记录

-
- {(record.attempts ?? []).map((attempt) => ( -
-
- {attempt.attemptNo} -
-
-
- 第 {attempt.attemptNo} 次投递 - {attemptStatusLabel[attempt.status] ?? attempt.status} -
-
-
发送时间
{attempt.sentAt ? formatDateTime(attempt.sentAt) : '-'}
-
ACK 时间
{attempt.acknowledgedAt ? formatDateTime(attempt.acknowledgedAt) : '-'}
-
ACK 截止
{attempt.ackDeadlineAt ? formatDateTime(attempt.ackDeadlineAt) : '-'}
-
-
-
连接 ID
{attempt.connectionId ?? '-'}
-
Sequence_Id
{attempt.sequenceId ?? '-'}
-
Msg_Id
{attempt.messageId ?? '-'}
-
-
- ACK Result:{attempt.ackResult ?? '-'} - {attempt.errorMessage ?? attempt.failureType ?? '本次投递未记录异常'} -
-
-
- ))} - {(record.attempts ?? []).length === 0 ?

暂无逐次投递记录

: null} -
-
-
-

Payload

-
{payloadText}
-
-
-
- ); -} +import { DeliveryDetailModal, deliveryStatusLabel, deliveryTypeLabel, recentSevenDays, requeueItemStatusLabel, requeueTaskStatusLabel, requeueTone, statusLabel, statusTone, type RequeueResult, type RequeueTarget } from './downstreamDeliveryPresentation'; export function AdminDownstreamDeliveriesPage() { const [records, setRecords] = useState([]); diff --git a/src/apps/admin/AdminHome.tsx b/src/apps/admin/AdminHome.tsx index 57712fb..8b7c68c 100644 --- a/src/apps/admin/AdminHome.tsx +++ b/src/apps/admin/AdminHome.tsx @@ -9,13 +9,13 @@ import { useNavigate } from 'react-router-dom'; import { Breadcrumb, Button, - Chart, Modal, MoneyText, Table, Tag, type TableColumn, } from '@/components/ui'; +import { Chart } from '@/components/ui/Chart'; import { adminApi, type DashboardResponse, type SendQualityResponse } from '@/api/adminApi'; import { createDualAxisBarLineOption, createLineOption } from '@/theme/chartOptions'; import { formatAmount, moneyUnitsToYuan } from '@/utils/currency'; diff --git a/src/apps/admin/auditColumns.tsx b/src/apps/admin/auditColumns.tsx index 09d5f70..d4dc2e4 100644 --- a/src/apps/admin/auditColumns.tsx +++ b/src/apps/admin/auditColumns.tsx @@ -1,6 +1,6 @@ import { Check, X } from 'lucide-react'; import { Button, Tag, type TableColumn } from '@/components/ui'; -import type { AuditItem, AuditStatus } from '@/mock'; +import type { AuditItem, AuditStatus } from '@/api/types/audit'; const riskToneMap = { low: 'success', diff --git a/src/apps/admin/downstreamDeliveryPresentation.tsx b/src/apps/admin/downstreamDeliveryPresentation.tsx new file mode 100644 index 0000000..f96c214 --- /dev/null +++ b/src/apps/admin/downstreamDeliveryPresentation.tsx @@ -0,0 +1,162 @@ +import { useMemo } from 'react'; +import type { DownstreamDeliveryRecord } from '@/api/adminApi'; +import { Button, Modal, Tag, type DateRangeValue } from '@/components/ui'; +import { formatDateTime } from '@/utils/dateTime'; + +export const statusTone: Record = { + pending: 'warning', + manual_requeueing: 'info', + awaiting_ack: 'info', + delivered: 'success', + failed: 'danger', + unconfirmed: 'warning', + rejected: 'danger', +}; + +export const statusLabel: Record = { + manual_requeueing: '人工重投处理中', + awaiting_ack: '等待客户端确认', + delivered: '客户端已确认', + failed: '投递失败', + unconfirmed: '客户端未确认', + rejected: '客户端拒绝', +}; + +export function deliveryStatusLabel(record: DownstreamDeliveryRecord) { + if (record.status !== 'pending') return statusLabel[record.status] ?? record.status; + if (record.manualRetryCount > 0) return '人工重投排队中'; + if (record.retryCount > 0) return '等待自动重试'; + return '待首次投递'; +} + +export const deliveryTypeLabel: Record = { + receipt: '状态回执', + uplink: '上行短信', +}; + +const attemptStatusLabel: Record = { + awaiting_ack: '等待客户端确认', + acknowledged: '客户端已确认', + rejected: '客户端拒绝', + failed: '投递失败', +}; + +export const requeueTaskStatusLabel: Record = { + queued: '排队中', running: '执行中', paused: '已暂停', completed: '已完成', + partial_completed: '部分完成', terminated: '已终止', +}; + +export const requeueItemStatusLabel: Record = { + queued: '排队中', processing: '处理中', waiting_connection: '等待连接', + waiting_external_ack: '等待其他链路确认', waiting_ack: '等待客户确认', success: '成功', + failed: '失败', skipped: '跳过', unprocessed: '未处理', +}; + +export function requeueTone(status: string) { + if (status === 'completed' || status === 'success') return 'success' as const; + if (status === 'partial_completed' || status === 'failed') return 'danger' as const; + if (status === 'paused' || status === 'skipped' || status === 'unprocessed') return 'warning' as const; + return 'info' as const; +} + +export type RequeueTarget = + | { kind: 'single'; record: DownstreamDeliveryRecord } + | { kind: 'batch'; ids: string[] }; + +export type RequeueResult = { + status: 'success' | 'partial' | 'failed'; + title: string; + message: string; + failures?: string[]; +}; + +function formatLocalDate(value: Date) { + return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`; +} + +export function recentSevenDays(): DateRangeValue { + const end = new Date(); + const start = new Date(end); + start.setDate(end.getDate() - 6); + return { start: formatLocalDate(start), end: formatLocalDate(end) }; +} + +function attemptStatusTone(status: string) { + if (status === 'acknowledged') return 'success' as const; + if (status === 'rejected' || status === 'failed') return 'danger' as const; + if (status === 'awaiting_ack') return 'info' as const; + return 'neutral' as const; +} + +export function DeliveryDetailModal({ record, onClose }: { record: DownstreamDeliveryRecord; onClose: () => void }) { + const payloadText = useMemo(() => JSON.stringify(record.payload ?? {}, null, 2), [record.payload]); + + return ( +

下游投递详情

{record.id}

} + footer={} + > +
+
+
企业{record.tenant?.name ?? record.tenantId}
+
应用{record.application?.name ?? record.applicationId}
+
投递类型{deliveryTypeLabel[record.deliveryType] ?? record.deliveryType}
+
当前状态{deliveryStatusLabel(record)}
+
消息 ID{record.messageId ?? '-'}
+
自动重试次数{record.retryCount}
+
人工重投次数{record.manualRetryCount}
+
最近人工重投{record.lastRetriedAt ? formatDateTime(record.lastRetriedAt) : '-'}
+
下次重试{record.nextRetryAt ?? '-'}
+
自动重试{record.retryEnabled ? '开启' : '关闭'}
+
写出时间{record.sentAt ?? '-'}
+
确认时间{record.acknowledgedAt ?? '-'}
+
ACK Result{record.ackResult ?? '-'}
+
ACK Sequence_Id{record.ackSequenceId ?? '-'}
+
ACK Msg_Id{record.ackMessageId ?? '-'}
+
连接 ID{record.connectionId ?? '-'}
+
最后错误{record.lastError ?? '-'}
+
+
+

逐次投递记录

+
+ {(record.attempts ?? []).map((attempt) => ( +
+
+ {attempt.attemptNo} +
+
+
+ 第 {attempt.attemptNo} 次投递 + {attemptStatusLabel[attempt.status] ?? attempt.status} +
+
+
发送时间
{attempt.sentAt ? formatDateTime(attempt.sentAt) : '-'}
+
ACK 时间
{attempt.acknowledgedAt ? formatDateTime(attempt.acknowledgedAt) : '-'}
+
ACK 截止
{attempt.ackDeadlineAt ? formatDateTime(attempt.ackDeadlineAt) : '-'}
+
+
+
连接 ID
{attempt.connectionId ?? '-'}
+
Sequence_Id
{attempt.sequenceId ?? '-'}
+
Msg_Id
{attempt.messageId ?? '-'}
+
+
+ ACK Result:{attempt.ackResult ?? '-'} + {attempt.errorMessage ?? attempt.failureType ?? '本次投递未记录异常'} +
+
+
+ ))} + {(record.attempts ?? []).length === 0 ?

暂无逐次投递记录

: null} +
+
+
+

Payload

+
{payloadText}
+
+
+
+ ); +} diff --git a/src/apps/admin/security-detection/AdminSecurityDetectionPage.tsx b/src/apps/admin/security-detection/AdminSecurityDetectionPage.tsx index a3b777a..88f7092 100644 --- a/src/apps/admin/security-detection/AdminSecurityDetectionPage.tsx +++ b/src/apps/admin/security-detection/AdminSecurityDetectionPage.tsx @@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo, useState, type ComponentProps } from ' import type { EChartsOption } from 'echarts'; import { AlertTriangle, Ban, BellRing, RefreshCw, Settings2, ShieldCheck, ShieldOff } from 'lucide-react'; import { adminApi, type SecurityAlert, type SecurityBlock, type SecurityOverview, type SecurityProtectedNetwork, type SecurityRule } from '@/api/adminApi'; -import { Breadcrumb, Button, Chart, Input, Modal as BaseModal, Select, Table, Tabs, Tag, Textarea, type TableColumn } from '@/components/ui'; +import { Breadcrumb, Button, Input, Modal as BaseModal, Select, Table, Tabs, Tag, Textarea, type TableColumn } from '@/components/ui'; +import { Chart } from '@/components/ui/Chart'; import './AdminSecurityDetectionPage.css'; const statusLabels: Record = { open: '待处理', acknowledged: '已确认', blocked: '已封禁', block_failed: '封禁失败', ignored: '已忽略', requested: '执行中', failed: '失败', released: '已解封' }; diff --git a/src/apps/admin/system-monitoring/AdminSystemMonitoringPage.tsx b/src/apps/admin/system-monitoring/AdminSystemMonitoringPage.tsx index 47430ac..f385c51 100644 --- a/src/apps/admin/system-monitoring/AdminSystemMonitoringPage.tsx +++ b/src/apps/admin/system-monitoring/AdminSystemMonitoringPage.tsx @@ -24,7 +24,8 @@ import { type InfrastructureMonitoringOverview, type InfrastructureMonitoringRange, } from '@/api/adminApi'; -import { Breadcrumb, Button, Chart, Input, Modal, Table, Tag, type TableColumn } from '@/components/ui'; +import { Breadcrumb, Button, Input, Modal, Table, Tag, type TableColumn } from '@/components/ui'; +import { Chart } from '@/components/ui/Chart'; import './AdminSystemMonitoringPage.css'; const RANGE_OPTIONS: Array<{ value: InfrastructureMonitoringRange; label: string }> = [ diff --git a/src/apps/client/ClientHome.tsx b/src/apps/client/ClientHome.tsx index 25affdd..3a49b5c 100644 --- a/src/apps/client/ClientHome.tsx +++ b/src/apps/client/ClientHome.tsx @@ -10,7 +10,8 @@ import { WalletCards, } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; -import { Button, Chart, Table, Tag, type TableColumn } from '@/components/ui'; +import { Button, Table, Tag, type TableColumn } from '@/components/ui'; +import { Chart } from '@/components/ui/Chart'; import { clientApi, type DashboardResponse } from '@/api/adminApi'; import { createLineOption, createPieOption } from '@/theme/chartOptions'; import { formatDateTime } from '@/utils/dateTime'; diff --git a/src/components/ui/Chart.tsx b/src/components/ui/Chart.tsx index c61285c..25ed1d4 100644 --- a/src/components/ui/Chart.tsx +++ b/src/components/ui/Chart.tsx @@ -1,6 +1,11 @@ import { useEffect, useRef } from 'react'; import type { EChartsOption } from 'echarts'; -import * as echarts from 'echarts'; +import { BarChart, LineChart, PieChart } from 'echarts/charts'; +import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'; +import { init, use } from 'echarts/core'; +import { CanvasRenderer } from 'echarts/renderers'; + +use([BarChart, LineChart, PieChart, GridComponent, LegendComponent, TooltipComponent, CanvasRenderer]); type ChartProps = { option: EChartsOption; @@ -15,7 +20,7 @@ export function Chart({ option, height = 280 }: ChartProps) { return undefined; } - const chart = echarts.init(chartRef.current); + const chart = init(chartRef.current); chart.setOption(option); const resizeObserver = new ResizeObserver(() => chart.resize()); diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index 63c8fe3..1ce5b3c 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -1,6 +1,5 @@ export { Button } from './Button'; export { Breadcrumb } from './Breadcrumb'; -export { Chart } from './Chart'; export { CarrierTag, normalizeCarrierTag } from './CarrierTag'; export type { CarrierTagValue } from './CarrierTag'; export { DateRangeInput } from './DateRangeInput'; diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx index 71f3fe9..7eba943 100644 --- a/src/routes/AppRoutes.tsx +++ b/src/routes/AppRoutes.tsx @@ -1,69 +1,83 @@ +import { lazy, Suspense, type ComponentType, type LazyExoticComponent } from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; -import { AdminAnalyticsPage } from '@/apps/admin/AdminAnalyticsPage'; -import { AdminChannelGroupFormPage } from '@/apps/admin/AdminChannelGroupFormPage'; -import { AdminChannelGroupsPage } from '@/apps/admin/AdminChannelGroupsPage'; -import { AdminChannelsPage } from '@/apps/admin/AdminChannelsPage'; -import { AdminChannelReportPage } from '@/apps/admin/AdminChannelReportPage'; -import { AdminCustomerDetailPage } from '@/apps/admin/AdminCustomerDetailPage'; -import { AdminCustomerFormPage } from '@/apps/admin/AdminCustomerFormPage'; -import { AdminCustomersPage } from '@/apps/admin/AdminCustomersPage'; -import { AdminDrainageFieldsPage } from '@/apps/admin/AdminDrainageFieldsPage'; -import { AdminDrainageDetectionRulesPage } from '@/apps/admin/AdminDrainageDetectionRulesPage'; -import { AdminDownstreamDeliveriesPage } from '@/apps/admin/AdminDownstreamDeliveriesPage'; -import { AdminDownstreamRecoveryStatusesPage } from '@/apps/admin/AdminDownstreamRecoveryStatusesPage'; -import { AdminEnterpriseApplicationsPage } from '@/apps/admin/AdminEnterpriseApplicationsPage'; -import { AdminEnterpriseBlacklistPage } from '@/apps/admin/AdminEnterpriseBlacklistPage'; -import { AdminEnterpriseSignaturesPage } from '@/apps/admin/AdminEnterpriseSignaturesPage'; -import { AdminEnterpriseTemplatesPage } from '@/apps/admin/AdminEnterpriseTemplatesPage'; -import { AdminGlobalBlacklistPage } from '@/apps/admin/AdminGlobalBlacklistPage'; -import { AdminGatewaySubmitExceptionsPage } from '@/apps/admin/AdminGatewaySubmitExceptionsPage'; -import { AdminHome } from '@/apps/admin/AdminHome'; -import { AdminMonitorPage } from '@/apps/admin/AdminMonitorPage'; -import { AdminPhoneSegmentsPage } from '@/apps/admin/AdminPhoneSegmentsPage'; -import { AdminRechargeRecordsPage } from '@/apps/admin/AdminRechargeRecordsPage'; -import { AdminReconciliationReportsPage } from '@/apps/admin/AdminReconciliationReportsPage'; -import { AdminProfitReportsPage } from '@/apps/admin/AdminProfitReportsPage'; -import { AdminQualityReportsPage } from '@/apps/admin/AdminQualityReportsPage'; -import { AdminReportRecordsPage } from '@/apps/admin/AdminReportRecordsPage'; -import { AdminReportTasksPage } from '@/apps/admin/AdminReportTasksPage'; -import { AdminReportMaterialsPage } from '@/apps/admin/AdminReportMaterialsPage'; -import { AdminSensitiveWordsPage } from '@/apps/admin/AdminSensitiveWordsPage'; -import { AdminSmsAuditPage } from '@/apps/admin/AdminSmsAuditPage'; -import { AdminRiskRulesPage } from '@/apps/admin/AdminRiskRulesPage'; -import { AdminSmsApplicationFormPage } from '@/apps/admin/AdminSmsApplicationFormPage'; -import { AdminSmsRecordsPage } from '@/apps/admin/AdminSmsRecordsPage'; -import { AdminSmsTaskProgressPage } from '@/apps/admin/AdminSmsTaskProgressPage'; -import { AdminSmsUplinkRecordsPage } from '@/apps/admin/AdminSmsUplinkRecordsPage'; -import { AdminSignatureAuditPage } from '@/apps/admin/AdminSignatureAuditPage'; -import { AdminSignatureRetirementPage } from '@/apps/admin/AdminSignatureRetirementPage'; -import { AdminDrainageAuditPage } from '@/apps/admin/AdminDrainageAuditPage'; -import { AdminSystemLogsPage } from '@/apps/admin/AdminSystemLogsPage'; -import { AdminSystemMonitoringPage } from '@/apps/admin/system-monitoring/AdminSystemMonitoringPage'; -import { AdminSecurityDetectionPage } from '@/apps/admin/security-detection/AdminSecurityDetectionPage'; -import { AdminTemplateAuditPage } from '@/apps/admin/AdminTemplateAuditPage'; -import { AdminUsersPage } from '@/apps/admin/AdminUsersPage'; -import { AdminEnterpriseAuditPage } from '@/apps/admin/AdminEnterpriseAuditPage'; -import { ClientApplicationsPage } from '@/apps/client/ClientApplicationsPage'; -import { ClientBatchTasksPage } from '@/apps/client/ClientBatchTasksPage'; -import { ClientBillingPage } from '@/apps/client/ClientBillingPage'; -import { ClientEnterpriseAuthPage } from '@/apps/client/ClientEnterpriseAuthPage'; -import { ClientHome } from '@/apps/client/ClientHome'; -import { ClientHttpApiPage } from '@/apps/client/ClientHttpApiPage'; -import { ClientSendDetailPage } from '@/apps/client/ClientSendDetailPage'; -import { ClientSendPage } from '@/apps/client/ClientSendPage'; -import { ClientSignaturesPage } from '@/apps/client/ClientSignaturesPage'; -import { ClientSystemLogsPage } from '@/apps/client/ClientSystemLogsPage'; -import { ClientTemplatesPage } from '@/apps/client/ClientTemplatesPage'; -import { ClientUplinkMessagesPage } from '@/apps/client/ClientUplinkMessagesPage'; -import { ClientUsersPage } from '@/apps/client/ClientUsersPage'; import { LoginPage } from '@/apps/LoginPage'; import { PagePlaceholder } from '@/components/PagePlaceholder'; import { AdminLayout } from '@/layouts/AdminLayout'; import { ClientLayout } from '@/layouts/ClientLayout'; +import { RouteLoadBoundary } from './RouteLoadBoundary'; + +function lazyNamed(loader: () => Promise, exportName: string): LazyExoticComponent> { + return lazy(async () => { + const loaded = await loader() as Record; + const component = loaded[exportName]; + if (!component) throw new Error(`Lazy route export ${exportName} was not found`); + return { default: component }; + }); +} + +const AdminAnalyticsPage = lazyNamed(() => import('@/apps/admin/AdminAnalyticsPage'), 'AdminAnalyticsPage'); +const AdminChannelGroupFormPage = lazyNamed(() => import('@/apps/admin/AdminChannelGroupFormPage'), 'AdminChannelGroupFormPage'); +const AdminChannelGroupsPage = lazyNamed(() => import('@/apps/admin/AdminChannelGroupsPage'), 'AdminChannelGroupsPage'); +const AdminChannelsPage = lazyNamed(() => import('@/apps/admin/AdminChannelsPage'), 'AdminChannelsPage'); +const AdminChannelReportPage = lazyNamed(() => import('@/apps/admin/AdminChannelReportPage'), 'AdminChannelReportPage'); +const AdminCustomerDetailPage = lazyNamed(() => import('@/apps/admin/AdminCustomerDetailPage'), 'AdminCustomerDetailPage'); +const AdminCustomerFormPage = lazyNamed(() => import('@/apps/admin/AdminCustomerFormPage'), 'AdminCustomerFormPage'); +const AdminCustomersPage = lazyNamed(() => import('@/apps/admin/AdminCustomersPage'), 'AdminCustomersPage'); +const AdminDrainageFieldsPage = lazyNamed(() => import('@/apps/admin/AdminDrainageFieldsPage'), 'AdminDrainageFieldsPage'); +const AdminDrainageDetectionRulesPage = lazyNamed(() => import('@/apps/admin/AdminDrainageDetectionRulesPage'), 'AdminDrainageDetectionRulesPage'); +const AdminDownstreamDeliveriesPage = lazyNamed(() => import('@/apps/admin/AdminDownstreamDeliveriesPage'), 'AdminDownstreamDeliveriesPage'); +const AdminDownstreamRecoveryStatusesPage = lazyNamed(() => import('@/apps/admin/AdminDownstreamRecoveryStatusesPage'), 'AdminDownstreamRecoveryStatusesPage'); +const AdminEnterpriseApplicationsPage = lazyNamed(() => import('@/apps/admin/AdminEnterpriseApplicationsPage'), 'AdminEnterpriseApplicationsPage'); +const AdminEnterpriseBlacklistPage = lazyNamed(() => import('@/apps/admin/AdminEnterpriseBlacklistPage'), 'AdminEnterpriseBlacklistPage'); +const AdminEnterpriseSignaturesPage = lazyNamed(() => import('@/apps/admin/AdminEnterpriseSignaturesPage'), 'AdminEnterpriseSignaturesPage'); +const AdminEnterpriseTemplatesPage = lazyNamed(() => import('@/apps/admin/AdminEnterpriseTemplatesPage'), 'AdminEnterpriseTemplatesPage'); +const AdminGlobalBlacklistPage = lazyNamed(() => import('@/apps/admin/AdminGlobalBlacklistPage'), 'AdminGlobalBlacklistPage'); +const AdminGatewaySubmitExceptionsPage = lazyNamed(() => import('@/apps/admin/AdminGatewaySubmitExceptionsPage'), 'AdminGatewaySubmitExceptionsPage'); +const AdminHome = lazyNamed(() => import('@/apps/admin/AdminHome'), 'AdminHome'); +const AdminMonitorPage = lazyNamed(() => import('@/apps/admin/AdminMonitorPage'), 'AdminMonitorPage'); +const AdminPhoneSegmentsPage = lazyNamed(() => import('@/apps/admin/AdminPhoneSegmentsPage'), 'AdminPhoneSegmentsPage'); +const AdminRechargeRecordsPage = lazyNamed(() => import('@/apps/admin/AdminRechargeRecordsPage'), 'AdminRechargeRecordsPage'); +const AdminReconciliationReportsPage = lazyNamed(() => import('@/apps/admin/AdminReconciliationReportsPage'), 'AdminReconciliationReportsPage'); +const AdminProfitReportsPage = lazyNamed(() => import('@/apps/admin/AdminProfitReportsPage'), 'AdminProfitReportsPage'); +const AdminQualityReportsPage = lazyNamed(() => import('@/apps/admin/AdminQualityReportsPage'), 'AdminQualityReportsPage'); +const AdminReportRecordsPage = lazyNamed(() => import('@/apps/admin/AdminReportRecordsPage'), 'AdminReportRecordsPage'); +const AdminReportTasksPage = lazyNamed(() => import('@/apps/admin/AdminReportTasksPage'), 'AdminReportTasksPage'); +const AdminReportMaterialsPage = lazyNamed(() => import('@/apps/admin/AdminReportMaterialsPage'), 'AdminReportMaterialsPage'); +const AdminSensitiveWordsPage = lazyNamed(() => import('@/apps/admin/AdminSensitiveWordsPage'), 'AdminSensitiveWordsPage'); +const AdminSmsAuditPage = lazyNamed(() => import('@/apps/admin/AdminSmsAuditPage'), 'AdminSmsAuditPage'); +const AdminRiskRulesPage = lazyNamed(() => import('@/apps/admin/AdminRiskRulesPage'), 'AdminRiskRulesPage'); +const AdminSmsApplicationFormPage = lazyNamed(() => import('@/apps/admin/AdminSmsApplicationFormPage'), 'AdminSmsApplicationFormPage'); +const AdminSmsRecordsPage = lazyNamed(() => import('@/apps/admin/AdminSmsRecordsPage'), 'AdminSmsRecordsPage'); +const AdminSmsTaskProgressPage = lazyNamed(() => import('@/apps/admin/AdminSmsTaskProgressPage'), 'AdminSmsTaskProgressPage'); +const AdminSmsUplinkRecordsPage = lazyNamed(() => import('@/apps/admin/AdminSmsUplinkRecordsPage'), 'AdminSmsUplinkRecordsPage'); +const AdminSignatureAuditPage = lazyNamed(() => import('@/apps/admin/AdminSignatureAuditPage'), 'AdminSignatureAuditPage'); +const AdminSignatureRetirementPage = lazyNamed(() => import('@/apps/admin/AdminSignatureRetirementPage'), 'AdminSignatureRetirementPage'); +const AdminDrainageAuditPage = lazyNamed(() => import('@/apps/admin/AdminDrainageAuditPage'), 'AdminDrainageAuditPage'); +const AdminSystemLogsPage = lazyNamed(() => import('@/apps/admin/AdminSystemLogsPage'), 'AdminSystemLogsPage'); +const AdminSystemMonitoringPage = lazyNamed(() => import('@/apps/admin/system-monitoring/AdminSystemMonitoringPage'), 'AdminSystemMonitoringPage'); +const AdminSecurityDetectionPage = lazyNamed(() => import('@/apps/admin/security-detection/AdminSecurityDetectionPage'), 'AdminSecurityDetectionPage'); +const AdminTemplateAuditPage = lazyNamed(() => import('@/apps/admin/AdminTemplateAuditPage'), 'AdminTemplateAuditPage'); +const AdminUsersPage = lazyNamed(() => import('@/apps/admin/AdminUsersPage'), 'AdminUsersPage'); +const AdminEnterpriseAuditPage = lazyNamed(() => import('@/apps/admin/AdminEnterpriseAuditPage'), 'AdminEnterpriseAuditPage'); +const ClientApplicationsPage = lazyNamed(() => import('@/apps/client/ClientApplicationsPage'), 'ClientApplicationsPage'); +const ClientBatchTasksPage = lazyNamed(() => import('@/apps/client/ClientBatchTasksPage'), 'ClientBatchTasksPage'); +const ClientBillingPage = lazyNamed(() => import('@/apps/client/ClientBillingPage'), 'ClientBillingPage'); +const ClientEnterpriseAuthPage = lazyNamed(() => import('@/apps/client/ClientEnterpriseAuthPage'), 'ClientEnterpriseAuthPage'); +const ClientHome = lazyNamed(() => import('@/apps/client/ClientHome'), 'ClientHome'); +const ClientHttpApiPage = lazyNamed(() => import('@/apps/client/ClientHttpApiPage'), 'ClientHttpApiPage'); +const ClientSendDetailPage = lazyNamed(() => import('@/apps/client/ClientSendDetailPage'), 'ClientSendDetailPage'); +const ClientSendPage = lazyNamed(() => import('@/apps/client/ClientSendPage'), 'ClientSendPage'); +const ClientSignaturesPage = lazyNamed(() => import('@/apps/client/ClientSignaturesPage'), 'ClientSignaturesPage'); +const ClientSystemLogsPage = lazyNamed(() => import('@/apps/client/ClientSystemLogsPage'), 'ClientSystemLogsPage'); +const ClientTemplatesPage = lazyNamed(() => import('@/apps/client/ClientTemplatesPage'), 'ClientTemplatesPage'); +const ClientUplinkMessagesPage = lazyNamed(() => import('@/apps/client/ClientUplinkMessagesPage'), 'ClientUplinkMessagesPage'); +const ClientUsersPage = lazyNamed(() => import('@/apps/client/ClientUsersPage'), 'ClientUsersPage'); export function AppRoutes() { return ( - + +
页面加载中...
}> + } /> } /> } /> @@ -148,6 +162,8 @@ export function AppRoutes() { } /> } /> - +
+ + ); } diff --git a/src/routes/RouteLoadBoundary.tsx b/src/routes/RouteLoadBoundary.tsx new file mode 100644 index 0000000..a35e75a --- /dev/null +++ b/src/routes/RouteLoadBoundary.tsx @@ -0,0 +1,30 @@ +import { Component, type ErrorInfo, type ReactNode } from 'react'; + +type Props = { children: ReactNode }; +type State = { error?: Error }; + +export class RouteLoadBoundary extends Component { + state: State = {}; + + static getDerivedStateFromError(error: Error): State { + return { error }; + } + + componentDidCatch(error: Error, info: ErrorInfo) { + console.error('Route chunk failed to load', error, info.componentStack); + } + + render() { + if (!this.state.error) return this.props.children; + return ( +
+
+

页面资源加载失败,请检查网络后重试。

+ +
+
+ ); + } +} diff --git a/tools/quality/verify-bundle-budget.mjs b/tools/quality/verify-bundle-budget.mjs new file mode 100644 index 0000000..196e7ef --- /dev/null +++ b/tools/quality/verify-bundle-budget.mjs @@ -0,0 +1,30 @@ +import { gzipSync } from 'node:zlib'; +import { readdirSync, readFileSync } from 'node:fs'; +import { basename, resolve } from 'node:path'; + +const root = resolve(import.meta.dirname, '../..'); +const dist = resolve(root, 'dist'); +const html = readFileSync(resolve(dist, 'index.html'), 'utf8'); +const entryMatch = html.match(/]+src="([^"]+\.js)"/); +if (!entryMatch) throw new Error('Unable to locate the Vite entry script in dist/index.html'); + +const assets = resolve(dist, 'assets'); +const files = readdirSync(assets).filter((file) => file.endsWith('.js')); +const sizes = files.map((file) => ({ file, gzip: gzipSync(readFileSync(resolve(assets, file))).length })); +const entryName = basename(entryMatch[1]); +const entry = sizes.find((item) => item.file === entryName); +if (!entry) throw new Error(`Entry asset ${entryName} was not found`); + +const entryBudget = Number(process.env.BUNDLE_ENTRY_GZIP_BUDGET ?? 250 * 1024); +// ECharts core + the three chart types used by the platform currently settle at ~182 KiB. +// Keep a narrow calibrated ceiling so future chart imports cannot silently restore the full bundle. +const chunkBudget = Number(process.env.BUNDLE_CHUNK_GZIP_BUDGET ?? 190 * 1024); +const oversized = sizes.filter((item) => item.file !== entryName && item.gzip > chunkBudget); +console.log(`entry ${entry.file}: ${(entry.gzip / 1024).toFixed(2)} KiB gzip (budget ${(entryBudget / 1024).toFixed(0)} KiB)`); +for (const item of sizes.toSorted((a, b) => b.gzip - a.gzip).slice(0, 10)) { + console.log(`${item.file}: ${(item.gzip / 1024).toFixed(2)} KiB gzip`); +} +if (entry.gzip > entryBudget || oversized.length) { + if (oversized.length) console.error(`Oversized async chunks: ${oversized.map((item) => item.file).join(', ')}`); + process.exit(1); +} diff --git a/tools/quality/verify-code-quality.mjs b/tools/quality/verify-code-quality.mjs new file mode 100644 index 0000000..5b0b8ee --- /dev/null +++ b/tools/quality/verify-code-quality.mjs @@ -0,0 +1,50 @@ +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const root = resolve(import.meta.dirname, '../..'); +const violations = []; + +const productionFiles = execFileSync('git', ['ls-files', 'src/apps/**/*.ts', 'src/apps/**/*.tsx', 'src/components/**/*.ts', 'src/components/**/*.tsx'], { cwd: root, encoding: 'utf8' }) + .split(/\r?\n/).filter(Boolean); +for (const file of productionFiles) { + const content = readFileSync(resolve(root, file), 'utf8'); + if (/from\s+['"]@\/mock(?:\/|['"])/.test(content)) violations.push(`${file}: production code imports @/mock`); +} + +const clientControllers = execFileSync('git', ['ls-files', 'api/src/**/*.controller.ts'], { cwd: root, encoding: 'utf8' }) + .split(/\r?\n/).filter(Boolean); +for (const file of clientControllers) { + const content = readFileSync(resolve(root, file), 'utf8'); + const clientClassOffset = content.indexOf('export class Client'); + const clientSection = clientClassOffset >= 0 ? content.slice(clientClassOffset) : content; + if (/['"]client(?:\/|['"])/.test(clientSection) && /@TenantId\(\)/.test(clientSection)) { + violations.push(`${file}: client route still reads request-controlled @TenantId()`); + } +} + +const trackedBuildCaches = execFileSync('git', ['ls-files', '*.tsbuildinfo', '**/*.tsbuildinfo'], { cwd: root, encoding: 'utf8' }).trim(); +if (trackedBuildCaches) violations.push(`tracked TypeScript build caches: ${trackedBuildCaches.replace(/\r?\n/g, ', ')}`); + +const usersService = readFileSync(resolve(root, 'api/src/users/users.service.ts'), 'utf8'); +if (/createHash\(['"]sha256['"]\)/.test(usersService)) { + violations.push('api/src/users/users.service.ts: password writes must use the versioned password hasher'); +} +const authService = readFileSync(resolve(root, 'api/src/auth/auth.service.ts'), 'utf8'); +if (/passwordHash\s*===|===\s*[^\n;]*passwordHash/.test(authService)) { + violations.push('api/src/auth/auth.service.ts: password hashes must not be compared directly'); +} + +const packageJson = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')); +if (packageJson.dependencies?.['react-router-dom'] !== '7.18.2') { + violations.push('package.json: react-router-dom must remain on the remediated 7.18.2 baseline'); +} +if (packageJson.overrides?.nanoid !== '3.3.18') { + violations.push('package.json: nanoid override must remain on the remediated 3.3.18 baseline'); +} + +if (violations.length) { + console.error(violations.map((item) => `ERROR: ${item}`).join('\n')); + process.exit(1); +} +console.log('Code quality structural checks passed.'); diff --git a/tools/security/verify-dependency-mitigations.mjs b/tools/security/verify-dependency-mitigations.mjs index 09c196d..478e676 100644 --- a/tools/security/verify-dependency-mitigations.mjs +++ b/tools/security/verify-dependency-mitigations.mjs @@ -7,7 +7,8 @@ const rootLock = JSON.parse(readFileSync(join(workspaceRoot, 'package-lock.json' const apiLock = JSON.parse(readFileSync(join(workspaceRoot, 'api', 'package-lock.json'), 'utf8')); assertVersionAtLeast(rootLock.packages['node_modules/postcss']?.version, [8, 5, 18], 'postcss'); -assertEqual(rootLock.packages['node_modules/react-router']?.version, '7.18.1', 'react-router'); +assertVersionAtLeast(rootLock.packages['node_modules/react-router']?.version, [7, 18, 2], 'react-router'); +assertVersionAtLeast(rootLock.packages['node_modules/nanoid']?.version, [3, 3, 18], 'nanoid'); assertEqual(apiLock.packages['node_modules/brace-expansion-safe']?.version, '5.0.8', 'brace-expansion-safe'); assertEqual( apiLock.packages['node_modules/brace-expansion']?.resolved, @@ -50,7 +51,7 @@ for (const relativePath of [ } } -console.log('Dependency mitigations verified: PostCSS patched, React Router RSC unused, brace expansion bounded and compatible.'); +console.log('Dependency mitigations verified: PostCSS, React Router and NanoID patched; RSC unused; brace expansion bounded and compatible.'); function sourceFiles(directory) { return readdirSync(directory).flatMap((name) => {