SSE 헤더 인증과 연결 수명 검증 및 HTTP 오류 정보 보호

This commit is contained in:
2026-10-09 02:35:45 +09:00
parent 87f810a878
commit 9dceaf8e4e
22 changed files with 1023 additions and 170 deletions
+31
View File
@@ -0,0 +1,31 @@
/** Preserve public validation/permission messages while hiding internal causes and stacks. */
export const safeTrpcErrorFormatter = <T extends { message: string; data: { code: string; stack?: string } }>({
shape,
}: {
shape: T;
}): T => ({
...shape,
message: shape.data.code === 'INTERNAL_SERVER_ERROR' ? 'Internal server error' : shape.message,
data: { ...shape.data, stack: undefined },
});
export const safeHttpErrorHandler = (
error: unknown,
request: { log: { error: (context: object, message: string) => void } },
reply: { code: (status: number) => { send: (body: object) => unknown } }
): void => {
const value = error && typeof error === 'object' ? (error as { statusCode?: number; message?: string }) : {};
const status =
typeof value.statusCode === 'number' &&
Number.isInteger(value.statusCode) &&
value.statusCode >= 400 &&
value.statusCode < 500
? value.statusCode
: 500;
request.log.error({ err: error }, 'Request handling failed');
reply.code(status).send({
statusCode: status,
error: status >= 500 ? 'Internal Server Error' : 'Request Error',
message: status >= 500 ? 'Internal server error' : (value.message ?? 'Request failed'),
});
};
+75
View File
@@ -0,0 +1,75 @@
const errorCodes = /^(?:P\d{4}|FST_ERR_[A-Z_]+|ERR_[A-Z_]+|UNAUTHORIZED|FORBIDDEN|BAD_REQUEST|INTERNAL_SERVER_ERROR)$/;
const errorSummary = (
error: unknown
): { type: string; message: string; stack: string; code?: string; statusCode?: number } => {
const value = error && typeof error === 'object' ? (error as Record<string, unknown>) : {};
const code =
typeof value.code === 'string' && value.code.length < 80 && errorCodes.test(value.code)
? value.code
: undefined;
const statusCode =
typeof value.statusCode === 'number' && value.statusCode >= 400 && value.statusCode <= 599
? value.statusCode
: undefined;
return {
type: 'RequestError',
message: 'Request handling failed',
stack: '',
...(code ? { code } : {}),
...(statusCode ? { statusCode } : {}),
};
};
/** HTTP diagnostics carry static route/status/code, never credentials or error payloads. */
export const safeHttpLoggerOptions = {
serializers: {
req: (request: { method?: string; routeOptions?: { url?: string } }) => ({
method: request.method,
// URLs can carry OAuth/session/input values, including unmatched path segments.
url: request.routeOptions?.url ?? '[unmatched]',
}),
res: (response: { statusCode?: number }) => ({ statusCode: response.statusCode }),
err: errorSummary,
},
redact: {
paths: [
'authorization',
'cookie',
'password',
'sessionToken',
'accessToken',
'token',
'secret',
'body',
'headers',
'req.headers',
'req.body',
'res.headers',
],
remove: true,
},
hooks: {
logMethod(args: unknown[], method: (...values: unknown[]) => void): void {
const first = args[0];
const error =
first instanceof Error
? first
: first && typeof first === 'object' && 'err' in first
? first.err
: undefined;
if (error !== undefined) {
// Fastify also copies err.message into msg; serializers alone do not protect it.
method.apply(this, [{ err: errorSummary(error) }, 'Request handling failed']);
} else {
// Fastify's default 404 handler also embeds the raw URL in a message.
method.apply(
this,
args.map((value) =>
typeof value === 'string' && /^Route .+ not found$/.test(value) ? 'Route not found' : value
)
);
}
},
},
};
+2
View File
@@ -31,6 +31,8 @@ export * from './gateway/profileStatus.js';
export * from './gateway/runtimeDiagnostics.js';
export * from './game/accessPenalty.js';
export * from './http/trpcTransport.js';
export * from './http/safeLogger.js';
export * from './http/errorPrivacy.js';
export * from './webPush/types.js';
export { canReadPlayAudit, canReadPlayAuditAccounts } from './auth/playAudit.js';