From 46c2b39aae001b294bfde686c3ae522b344d3f31 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Fri, 26 Sep 2025 22:58:13 -0400 Subject: [PATCH] Ensure R2 CORS when using manual access keys --- js/app.js | 34 +++++++++++++++++++++++++++++++--- js/storage/r2-s3.js | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/js/app.js b/js/app.js index 3960845d..a18669a8 100644 --- a/js/app.js +++ b/js/app.js @@ -32,7 +32,11 @@ import { attachCustomDomainAndWait, setManagedDomain, } from "./storage/r2-mgmt.js"; -import { makeR2Client, multipartUpload } from "./storage/r2-s3.js"; +import { + makeR2Client, + multipartUpload, + ensureBucketCors, +} from "./storage/r2-s3.js"; import { initQuickR2Upload } from "./r2-quick.js"; /** @@ -1568,6 +1572,10 @@ class bitvidApp { const accountId = (this.cloudflareSettings.accountId || "").trim(); const apiToken = (this.cloudflareSettings.apiToken || "").trim(); const zoneId = (this.cloudflareSettings.zoneId || "").trim(); + const accessKeyId = (this.cloudflareSettings.accessKeyId || "").trim(); + const secretAccessKey = + (this.cloudflareSettings.secretAccessKey || "").trim(); + const corsOrigins = this.getCorsOrigins(); const baseDomain = this.cloudflareSettings.baseDomain || ""; if (!accountId) { @@ -1588,7 +1596,7 @@ class bitvidApp { accountId, bucket: entry.bucket, token: apiToken, - origins: this.getCorsOrigins(), + origins: corsOrigins, }); } catch (err) { console.warn("Failed to refresh bucket configuration:", err); @@ -1625,6 +1633,26 @@ class bitvidApp { lastUpdated: Date.now(), }; + if (accessKeyId && secretAccessKey && corsOrigins.length > 0) { + try { + const s3 = makeR2Client({ + accountId, + accessKeyId, + secretAccessKey, + }); + await ensureBucketCors({ + s3, + bucket: bucketName, + origins: corsOrigins, + }); + } catch (corsErr) { + console.warn( + "Failed to ensure R2 CORS rules via access keys. Configure the bucket's CORS policy manually if uploads continue to fail.", + corsErr + ); + } + } + let savedEntry = entry; if ( !entry || @@ -1656,7 +1684,7 @@ class bitvidApp { accountId, bucket: bucketName, token: apiToken, - origins: this.getCorsOrigins(), + origins: corsOrigins, }); } catch (err) { console.warn("Failed to apply R2 CORS rules:", err); diff --git a/js/storage/r2-s3.js b/js/storage/r2-s3.js index e71f948b..56745b22 100644 --- a/js/storage/r2-s3.js +++ b/js/storage/r2-s3.js @@ -5,6 +5,7 @@ import { UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, + PutBucketCorsCommand, } from "https://esm.sh/@aws-sdk/client-s3@3.637.0?target=es2022&bundle"; function computeCacheControl(key) { @@ -41,6 +42,42 @@ export function makeR2Client({ accountId, accessKeyId, secretAccessKey }) { }); } +export async function ensureBucketCors({ s3, bucket, origins }) { + if (!s3) { + throw new Error("S3 client is required to configure CORS"); + } + if (!bucket) { + throw new Error("Bucket name is required to configure CORS"); + } + + const allowedOrigins = (origins || []).filter(Boolean); + if (allowedOrigins.length === 0) { + return; + } + + const command = new PutBucketCorsCommand({ + Bucket: bucket, + CORSConfiguration: { + CORSRules: [ + { + AllowedHeaders: ["*"], + AllowedMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "OPTIONS"], + AllowedOrigins: allowedOrigins, + ExposeHeaders: [ + "ETag", + "Content-Length", + "Content-Range", + "Accept-Ranges", + ], + MaxAgeSeconds: 3600, + }, + ], + }, + }); + + await s3.send(command); +} + export async function multipartUpload({ s3, bucket,