fix: 0.1.0 사전 점검 차단 결함을 보정

공유 archive 마이그레이션과 런타임 설정 보존, AVIF 판별 및 문서 생성을 수정한다. 조건부 통합 격리를 보강하고 취약 의존성을 갱신하며 릴리스 메타데이터를 정리한다.
This commit is contained in:
2026-08-19 11:20:44 +00:00
parent b43d7601e7
commit 769d81f894
20 changed files with 1257 additions and 1888 deletions
+3 -3
View File
@@ -30,7 +30,7 @@
},
"dependencies": {
"@fastify/cors": "^11.2.0",
"@fastify/static": "^9.0.0",
"@fastify/static": "^10.1.3",
"@prisma/client": "^7.9.1",
"@sammo-ts/common": "workspace:*",
"@sammo-ts/game-engine": "workspace:*",
@@ -40,10 +40,10 @@
"date-fns": "^4.1.0",
"es-toolkit": "^1.43.0",
"fastify": "^5.6.2",
"pm2": "^5.4.3",
"pm2": "^7.0.3",
"redis": "^5.10.0",
"sanitize-html": "2.17.6",
"sharp": "^0.34.4",
"sharp": "^0.35.0",
"zod": "^4.3.5"
}
}
+3 -2
View File
@@ -202,7 +202,8 @@ export const accountRouter = router({
const profiles = await listIconSyncProfiles(ctx, user.id);
const buffer = decodeImage(input.imageData);
const metadata = await sharp(buffer, { animated: true }).metadata();
if (!metadata.format || !ALLOWED_ICON_FORMATS.has(metadata.format)) {
const detectedFormat = metadata.mediaType === 'image/avif' ? 'avif' : metadata.format;
if (!detectedFormat || !ALLOWED_ICON_FORMATS.has(detectedFormat)) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'avif, webp, jpg, gif, png 아이콘만 사용할 수 있습니다.',
@@ -214,7 +215,7 @@ export const accountRouter = router({
message: '아이콘은 64x64~128x128 범위의 정사각형이어야 합니다.',
});
}
const extension = metadata.format === 'jpeg' ? 'jpg' : metadata.format;
const extension = detectedFormat === 'jpeg' ? 'jpg' : detectedFormat;
const filename = `${randomBytes(16).toString('hex')}.${extension}`;
if (!ctx.userIconUpload) {
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: '이미지 저장소가 설정되지 않았습니다.' });
+31
View File
@@ -1359,6 +1359,37 @@ describe('account self service', () => {
}
});
it('stores AVIF account icons with the public AVIF extension and media type', async () => {
const iconDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sammo-account-icon-avif-'));
try {
const { caller, users, sessions, userIconUpload } = buildCaller({ userIconDir: iconDir });
const user = await users.createUser({
username: 'icon-avif',
password: 'current-password',
});
const session = await sessions.createSession(user);
const avif = await sharp({
create: { width: 64, height: 64, channels: 4, background: '#334455' },
})
.avif()
.toBuffer();
const result = await caller.account.changeIcon({
sessionToken: session.sessionToken,
imageData: `data:image/avif;base64,${avif.toString('base64')}`,
});
expect(result.iconUrl).toMatch(
/^https:\/\/sam-image\.hided\.net\/icons\/users\/core2026\/[a-f0-9]{32}\.avif$/
);
expect(userIconUpload.upload).toHaveBeenCalledWith(
expect.objectContaining({ contentType: 'image/avif', body: avif })
);
} finally {
await fs.rm(iconDir, { recursive: true, force: true });
}
});
it('atomically allows only one icon change per KST day and removes the losing file', async () => {
const iconDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sammo-account-icon-race-'));
try {