Files
core2026/app/gateway-api/src/lobby/profileStatusInternalRoute.ts
T
Hide_D 48d94e5ff2 feat: Ref 호환 접속 제한과 벌점 초기화 구현
실행 중인 프로필에서만 접속 벌점을 누적하고 제한 임계값과 대상 경로를 Ref 순서에 맞춘다. 자기 턴 명령 성공 시 순간 점수를 같은 flush에서 초기화하며 월간 누적 감쇠는 유지한다. 제한 중 메인 자동 갱신과 실시간 구독을 중지하고 수동 갱신 성공 시 복구한다.
2026-08-15 18:49:41 +00:00

45 lines
1.8 KiB
TypeScript

import { createHmac, timingSafeEqual } from 'node:crypto';
import type { FastifyInstance } from 'fastify';
import type { GatewayProfileRepository } from '../orchestrator/profileRepository.js';
const INTERNAL_TOKEN_HEADER = 'x-sammo-internal-token';
const INTERNAL_TOKEN_CONTEXT = 'sammo:profile-status-source:v1';
const deriveInternalToken = (secret: string): string =>
createHmac('sha256', secret).update(INTERNAL_TOKEN_CONTEXT).digest('hex');
const matchesSecret = (provided: string | string[] | undefined, expected: string): boolean => {
const candidate = Array.isArray(provided) ? provided[0] : provided;
if (!candidate) {
return false;
}
const candidateBuffer = Buffer.from(candidate);
const expectedBuffer = Buffer.from(expected);
return candidateBuffer.length === expectedBuffer.length && timingSafeEqual(candidateBuffer, expectedBuffer);
};
export const registerProfileStatusInternalRoute = (
app: FastifyInstance,
options: {
profiles: GatewayProfileRepository;
secret: string;
}
): void => {
app.get<{ Params: { profileName: string } }>('/internal/profile-status/:profileName', async (request, reply) => {
void reply.header('Cache-Control', 'no-store');
if (!matchesSecret(request.headers[INTERNAL_TOKEN_HEADER], deriveInternalToken(options.secret))) {
await reply.status(401).send({ ok: false, error: 'unauthorized' });
return;
}
const profileName = request.params.profileName;
const profile = await options.profiles.getProfile(profileName);
if (!profile) {
await reply.status(404).send({ ok: false, error: 'not_found' });
return;
}
await reply.send({ profileName: profile.profileName, status: profile.status });
});
};