From b48779efc7158e4bad4791ccd64822d9005c8391 Mon Sep 17 00:00:00 2001 From: saltbo Date: Wed, 22 Jul 2026 23:48:33 -0400 Subject: [PATCH] fix(downloads): decode task tokens as UTF-8 --- server/adapters/repos/download-tokens.ts | 4 +++- server/http/objects.integration.test.ts | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/server/adapters/repos/download-tokens.ts b/server/adapters/repos/download-tokens.ts index 94f437cb..724fbcb1 100644 --- a/server/adapters/repos/download-tokens.ts +++ b/server/adapters/repos/download-tokens.ts @@ -147,5 +147,7 @@ function base64UrlDecode(value: string): string { .replace(/-/g, '+') .replace(/_/g, '/') .padEnd(Math.ceil(value.length / 4) * 4, '=') - return atob(padded) + const binary = atob(padded) + const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)) + return new TextDecoder('utf-8', { fatal: true }).decode(bytes) } diff --git a/server/http/objects.integration.test.ts b/server/http/objects.integration.test.ts index 9eb5afbd..1c56d676 100644 --- a/server/http/objects.integration.test.ts +++ b/server/http/objects.integration.test.ts @@ -2496,6 +2496,30 @@ async function mintTaskUploadContext( } describe('Objects API — error branches', () => { + it('accepts a download-task-upload token scoped to a Unicode target folder', async () => { + const { app, db } = await createTestApp({ DOWNLOAD_TOKEN_SECRET: 'test-download-token-secret' }) + await insertStorage(db) + const targetFolder = 'Media/Music/欧阳娜娜/NANA II (2020)' + const { uploadToken } = await mintTaskUploadContext(app, db, { targetFolder }) + + const res = await app.request('/api/objects', { + method: 'POST', + headers: { Authorization: `Bearer ${uploadToken}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: '藏.mp3', + type: 'audio/mpeg', + size: 1, + parent: targetFolder, + }), + }) + + expect(res.status).toBe(201) + await expect(res.json()).resolves.toMatchObject({ + name: '藏.mp3', + parent: targetFolder, + }) + }) + it('returns 403 for a user-scoped API key with no active org on write', async () => { const { app, db, auth } = await createTestApp() await authedHeaders(app)