01 · Masalahnya
File besar tidak bisa lewat server aplikasi.
CMS-nya berjalan di Vercel, yang membatasi body request di 4,5 MB. Video jauh lebih besar dari itu, jadi browser mengunggah langsung ke Cloudflare Worker di depan bucket R2. Worker yang menerima upload dari browser adalah endpoint publik. Tanpa pengecekan, siapa pun yang menemukan URL-nya bisa upload, melihat isi, atau menghapus.
02 · Satu token per aksi
CMS menandatangani token baru untuk setiap aksi.
Setelah sesi dicek, server CMS menandatangani JWT. Issuer-nya CMS, audience-nya Worker penyimpanan, dan kedaluwarsa dalam 300 detik. Isinya tepat satu izin: upload ke satu folder, hapus satu key, atau list satu prefix. Token yang bocor tidak bisa dipakai ke mana-mana.
type Grant =
| { scope: "upload"; folder: "images" | "files" | "videos" }
| { scope: "delete"; key: string }
| { scope: "list"; prefix: string };
export function mintStorageToken(userId: number, grant: Grant) {
return new SignJWT({ ...grant })
.setProtectedHeader({ alg: "HS256" })
.setSubject(String(userId))
.setIssuer("cms")
.setAudience("storage-worker")
.setIssuedAt()
.setExpirationTime("300s")
.sign(secret);
}03 · Worker mengecek semuanya
Worker percaya pada token, bukan pada request.
Worker memverifikasi tanda tangan, issuer, audience, dan masa berlaku. Lalu scope harus cocok dengan route: token upload yang dikirim ke route hapus mendapat 403. Lalu targetnya harus cocok: folder, key, atau prefix diambil dari token, bukan dari body request.
const { payload } = await jwtVerify(token, secret, {
algorithms: ["HS256"],
issuer: "cms",
audience: "storage-worker",
});
if (payload.scope !== scope) {
throw new AuthError(403, `Token scope "${payload.scope}" cannot be used here`);
}04 · Jangan percaya browser soal jenis file
file.type adalah apa pun yang dikatakan browser.
Worker menentukan content type dari ekstensi file dan mengabaikan kiriman client. Ekstensi yang bisa membawa script, seperti .svg dan .html, diblokir. Setiap folder punya batas ukuran sendiri: 10 MB untuk gambar, 25 MB untuk file, 200 MB untuk video. Key harus berada langsung di bawah folder yang diizinkan, dan apa pun yang mengandung .. ditolak.
05 · Satu pintu masuk
Codebase-nya hanya punya satu jalur upload.
Kode fitur tidak boleh merender <input type="file"> sendiri. Semua upload lewat satu asset picker, yang meminta token lalu berbicara dengan Worker. Aturan file di Worker dan di CMS ada di dua file yang harus diubah bersamaan, dan aturan proyeknya menuliskan itu secara eksplisit.
06 · Harganya
Satu round trip tambahan, dan satu secret yang harus dijaga.
Setiap upload meminta token ke CMS dulu, satu request kecil sebelum file yang bisa mencapai 200 MB. Token ditandatangani dengan secret yang sama dengan sesi CMS, jadi merotasinya juga mengeluarkan semua editor dari sesi. Lain kali saya akan memberi penyimpanan secret-nya sendiri.
