fix: harden auth and investment synchronization
Backend security / security (push) Successful in 8s
Backend verify / verify (push) Successful in 11m9s
Conventional commits / commits (pull_request) Successful in 28s
Backend security / security (pull_request) Successful in 7s
Backend verify / verify (pull_request) Successful in 10m38s

Resolve A01-A10 and C01-C07 from the backend audit.
Preserve auth failures, forced candle reconciliation and forecast freshness.
Validate MOEX responses and nested HTTP errors; remove unused legacy paths.

Verified: 209 unit tests, 262 SQLite e2e tests on Linux, OpenAPI and durability.
Record the remaining macOS native SQLite abort in the audit.
This commit is contained in:
2026-09-27 14:09:05 +03:00
parent 5d3835e796
commit 4796d9d3f0
55 changed files with 1954 additions and 569 deletions
@@ -15,6 +15,12 @@ export class ProblemDetailsDto {
@ApiProperty({
required: false,
type: 'array',
items: {
type: 'object',
required: ['pointer', 'detail'],
properties: { pointer: { type: 'string' }, detail: { type: 'string' } },
},
example: [{ pointer: '#/email', detail: 'must be a valid email address' }],
})
errors?: { pointer: string; detail: string }[];
@@ -14,7 +14,10 @@ describe('HttpExceptionFilter', () => {
}),
} as unknown as ArgumentsHost;
filter.catch(new Error('database password: secret'), host);
filter.catch(
new Error('operation failed\npassword: SYNTHETIC_AUDIT_SECRET'),
host,
);
expect(status).toHaveBeenCalledWith(500);
expect(type).toHaveBeenCalledWith('application/problem+json');
@@ -23,7 +26,6 @@ describe('HttpExceptionFilter', () => {
code: 'INTERNAL_SERVER_ERROR',
});
expect(locals.publicErrorCode).toBe('INTERNAL_SERVER_ERROR');
expect(typeof locals.errorStack).toBe('string');
expect(locals.errorStack).not.toContain('database password: secret');
expect(locals.errorStack).toBeUndefined();
});
});
+12 -14
View File
@@ -27,14 +27,6 @@ export class HttpExceptionFilter implements ExceptionFilter {
? 'VALIDATION_ERROR'
: this.codeFor(status, payload);
(response.locals ??= {}).publicErrorCode = code;
if (
status >= Number(HttpStatus.INTERNAL_SERVER_ERROR) &&
exception instanceof Error
)
response.locals.errorStack = exception.stack
?.split('\n')
.slice(1)
.join('\n');
response
.status(status)
.type('application/problem+json')
@@ -69,12 +61,18 @@ export class HttpExceptionFilter implements ExceptionFilter {
private validationErrors(
errors: ValidationError[],
prefix = '#',
): { pointer: string; detail: string }[] {
return errors.flatMap((error) =>
Object.values(error.constraints ?? {}).map((detail) => ({
pointer: `#/${error.property}`,
detail,
})),
);
return errors.flatMap((error) => {
if (!error || typeof error.property !== 'string') return [];
const pointer = `${prefix}/${error.property.replaceAll('~', '~0').replaceAll('/', '~1')}`;
return [
...Object.values(error.constraints ?? {}).map((detail) => ({
pointer,
detail,
})),
...this.validationErrors(error.children ?? [], pointer),
];
});
}
}
@@ -42,9 +42,6 @@ export class RequestLoggingMiddleware implements NestMiddleware {
...(response.locals.publicErrorCode
? { code: response.locals.publicErrorCode as string }
: {}),
...(response.locals.errorStack
? { stack: response.locals.errorStack as string }
: {}),
};
const level =
response.statusCode >= httpStatus.HTTP_STATUS_INTERNAL_SERVER_ERROR
@@ -17,7 +17,6 @@ const allowedFields = new Set([
'status',
'durationMs',
'code',
'stack',
'operation',
'grpcCode',
'attempts',