실행 중인 프로필에서만 접속 벌점을 누적하고 제한 임계값과 대상 경로를 Ref 순서에 맞춘다. 자기 턴 명령 성공 시 순간 점수를 같은 flush에서 초기화하며 월간 누적 감쇠는 유지한다. 제한 중 메인 자동 갱신과 실시간 구독을 중지하고 수동 갱신 성공 시 복구한다.
45 lines
1.8 KiB
TypeScript
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 });
|
|
});
|
|
};
|