fix: 0.1.0 사전 점검 차단 결함을 보정
공유 archive 마이그레이션과 런타임 설정 보존, AVIF 판별 및 문서 생성을 수정한다. 조건부 통합 격리를 보강하고 취약 의존성을 갱신하며 릴리스 메타데이터를 정리한다.
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: '이미지 저장소가 설정되지 않았습니다.' });
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user