mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-06 12:52:26 +00:00
[ui] #38: make the CORS setup helper provider-aware (R2 / B2 / Custom S3)
The first cut showed for Custom S3 too but always gave B2-only instructions (b2 CLI +
B2-native JSON), and was hidden for R2. Now the helper shows for all bucket providers
and tailors its content:
- Backblaze B2 -> B2-native rules + `b2 update-bucket --corsRules ...`
- Custom S3 -> standard S3 CORSConfiguration + `aws s3api put-bucket-cors
--endpoint-url <endpoint> ...`
- Cloudflare R2 -> standard S3 JSON + Cloudflare dashboard guidance (R2 -> bucket ->
Settings -> CORS Policy) and the AWS-CLI command vs the derived R2
endpoint.
Origins pre-filled from getCorsOrigins(); intro/command-label/notes set per provider;
copy buttons for both blocks. bitvid still auto-applies CORS on Save (R2/S3 via
ensureBucketCors) — this is the manual fallback when the key lacks CORS-write perms.
buildCorsHelpContent() switches on provider; modal text is now data-driven. Tests
extended to 10 (per-provider rules + command + a DOM provider-switch). Build + lint
clean; storage suites 40/40.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
d7ad151698
commit
6006813b7e
5 files changed
+242
-115
No files matched your search
@@ -973,7 +973,7 @@
|
||||
<button id="storageClearBtn" type="button" class="btn-ghost focus-ring text-status-danger">
|
||||
Clear
|
||||
</button>
|
||||
<button id="storageCorsHelpBtn" type="button" class="btn-ghost focus-ring text-xs hidden">
|
||||
<button id="storageCorsHelpBtn" type="button" class="btn-ghost focus-ring text-xs">
|
||||
CORS setup help
|
||||
</button>
|
||||
</div>
|
||||
@@ -1875,10 +1875,8 @@
|
||||
|
||||
<div class="modal-body overflow-y-auto p-5 space-y-4 text-sm text-muted-strong">
|
||||
<p id="storageCorsModalIntro">
|
||||
Backblaze B2's web console CORS presets ("Share everything…") only allow
|
||||
<strong>downloads</strong>. Uploading from your browser uses an S3
|
||||
<code>PUT</code>/<code>POST</code>, which needs the <strong>custom</strong>
|
||||
rules below. Backblaze can only apply custom rules via its command-line tool.
|
||||
Uploading from your browser uses an S3 <code>PUT</code>/<code>POST</code>,
|
||||
which your bucket must allow via CORS.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
@@ -1897,7 +1895,7 @@
|
||||
|
||||
<div>
|
||||
<div class="mb-1 flex items-center justify-between gap-2">
|
||||
<span class="font-medium text-text">Apply it (B2 command-line tool)</span>
|
||||
<span id="storageCorsCmdLabel" class="font-medium text-text">Apply it</span>
|
||||
<button id="storageCorsCopyCmdBtn" type="button" class="btn-ghost focus-ring text-xs">
|
||||
Copy command
|
||||
</button>
|
||||
@@ -1905,18 +1903,7 @@
|
||||
<pre class="card border border-default bg-surface-strong/40 p-3 overflow-x-auto text-xs leading-relaxed"><code id="storageCorsCli" class="whitespace-pre text-text"></code></pre>
|
||||
</div>
|
||||
|
||||
<ol class="list-decimal space-y-1 pl-5 text-xs text-muted">
|
||||
<li>Install Backblaze's <strong>B2 CLI</strong> (<code>pip install b2</code>) on your computer.</li>
|
||||
<li>Run the command above (it authorizes with your key, then sets the rules).</li>
|
||||
<li>Wait ~1 minute, then retry your upload in bitvid.</li>
|
||||
</ol>
|
||||
|
||||
<p class="text-xs text-muted">
|
||||
Prefer the web console? Its presets are fine for <strong>playback</strong> of
|
||||
already-uploaded files — only browser <strong>uploads</strong> need the custom
|
||||
rules above. (Tip: a B2 application key with the <code>writeBucketCors</code>
|
||||
capability lets bitvid set this for you automatically when you Save.)
|
||||
</p>
|
||||
<p id="storageCorsNotes" class="whitespace-pre-line text-xs text-muted"></p>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer flex items-center justify-end gap-3 border-t border-border/60 p-4">
|
||||
|
||||
@@ -35,9 +35,12 @@ export class ProfileStorageController {
|
||||
this.storageR2Helper = null;
|
||||
this.storageB2Helper = null;
|
||||
this.storageS3Helper = null;
|
||||
// CORS setup helper modal (B2 / Custom S3) — owns its own DOM + behavior.
|
||||
// Provider-aware CORS setup helper modal — owns its own DOM + behavior.
|
||||
this.corsHelp = new StorageCorsHelp({
|
||||
getProvider: () => this.storageProviderInput?.value || "cloudflare_r2",
|
||||
getBucket: () => this.storageBucketInput?.value?.trim() || "",
|
||||
getRegion: () => this.storageRegionInput?.value?.trim() || "",
|
||||
getEndpoint: () => this.storageEndpointInput?.value?.trim() || "",
|
||||
});
|
||||
this.storageForcePathStyleInput = null;
|
||||
this.storageForcePathStyleLabel = null;
|
||||
@@ -447,7 +450,6 @@ export class ProfileStorageController {
|
||||
}
|
||||
}
|
||||
|
||||
// Region matters for B2 (drives the endpoint) and AWS-style S3; nudge the example.
|
||||
if (this.storageRegionInput && isB2) {
|
||||
this.storageRegionInput.placeholder = "us-west-004";
|
||||
} else if (this.storageRegionInput) {
|
||||
@@ -480,8 +482,7 @@ export class ProfileStorageController {
|
||||
);
|
||||
}
|
||||
|
||||
// Manual CORS helper is for S3-compatible providers (B2 / Custom S3), not R2.
|
||||
this.corsHelp.setVisible(!isR2);
|
||||
this.corsHelp.setVisible(true); // provider-aware; useful for all bucket providers
|
||||
}
|
||||
|
||||
getStorageUnlockFailureMessage(error) {
|
||||
@@ -814,8 +815,7 @@ export class ProfileStorageController {
|
||||
secretAccessKey,
|
||||
bucket,
|
||||
forcePathStyle,
|
||||
// Honor an explicit Public Access URL (custom domain / CDN); when blank,
|
||||
// validateS3Connection derives it from the endpoint + bucket.
|
||||
// Explicit Public Access URL (custom domain/CDN); blank → derived.
|
||||
publicBaseUrl: prefix || undefined,
|
||||
origins: getCorsOrigins(),
|
||||
});
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
// Storage CORS setup helper (B2 / S3-compatible providers).
|
||||
// Storage CORS setup helper (all bucket providers).
|
||||
//
|
||||
// Browser uploads to a bucket use an S3 PUT/POST, which the bucket must allow via
|
||||
// CORS. Backblaze B2's web-console presets only "share" (download) files, so uploads
|
||||
// stay CORS-blocked until custom rules are applied with B2's CLI. This module renders
|
||||
// a small modal with the exact rules (origins pre-filled) + the command to apply them.
|
||||
// CORS. bitvid tries to set this for you on Save, but that needs a key with CORS-write
|
||||
// permission and a provider that allows it (Backblaze B2's web presets, for example,
|
||||
// only "share" downloads). This module renders a small, PROVIDER-AWARE modal with the
|
||||
// exact rules (origins pre-filled) + how to apply them for the selected provider.
|
||||
|
||||
import { devLogger } from "../../utils/logger.js";
|
||||
import { getCorsOrigins } from "../../services/s3Service.js";
|
||||
import { deriveB2Endpoint, getCorsOrigins } from "../../services/s3Service.js";
|
||||
|
||||
// The CORS rules bitvid needs for in-browser uploads + ranged playback, in B2's
|
||||
// native rules shape. Mirrors ensureBucketCors (GET/HEAD/PUT/POST/DELETE + ranged-
|
||||
// playback expose headers) but adds the upload operations the B2 web-console presets
|
||||
// omit (they only share downloads).
|
||||
const PROVIDER_B2 = "backblaze_b2";
|
||||
const PROVIDER_R2 = "cloudflare_r2";
|
||||
|
||||
// Backblaze B2 native rules (corsRuleName + allowedOperations). The web-console
|
||||
// presets omit the upload operations (s3_put/s3_post), which is why uploads fail.
|
||||
export function buildBucketCorsRules(origins) {
|
||||
const allowedOrigins =
|
||||
Array.isArray(origins) && origins.length ? origins : ["*"];
|
||||
@@ -27,21 +29,102 @@ export function buildBucketCorsRules(origins) {
|
||||
];
|
||||
}
|
||||
|
||||
// The exact B2 CLI command that applies the rules (the path B2's web console points
|
||||
// custom-rule users to). Compact JSON so it pastes as one line.
|
||||
// Standard S3 CORSConfiguration — accepted by the AWS CLI, S3-compatible provider
|
||||
// dashboards, and Cloudflare R2's CORS-policy editor. (OPTIONS is implicit in S3 CORS
|
||||
// and not a valid AllowedMethod, so it's omitted on purpose.)
|
||||
export function buildS3CorsConfig(origins) {
|
||||
const allowedOrigins =
|
||||
Array.isArray(origins) && origins.length ? origins : ["*"];
|
||||
return {
|
||||
CORSRules: [
|
||||
{
|
||||
AllowedOrigins: allowedOrigins,
|
||||
AllowedMethods: ["GET", "HEAD", "PUT", "POST", "DELETE"],
|
||||
AllowedHeaders: ["*"],
|
||||
ExposeHeaders: ["ETag", "Content-Length", "Content-Range", "Accept-Ranges"],
|
||||
MaxAgeSeconds: 3600,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// B2 CLI command (the path B2's web console points custom-rule users to).
|
||||
export function buildB2CorsCommand(bucket, rules) {
|
||||
const bucketName = (bucket && bucket.trim()) || "YOUR_BUCKET";
|
||||
const compact = JSON.stringify(rules);
|
||||
return `b2 update-bucket --corsRules '${compact}' ${bucketName} allPublic`;
|
||||
return `b2 update-bucket --corsRules '${JSON.stringify(rules)}' ${bucketName} allPublic`;
|
||||
}
|
||||
|
||||
// AWS CLI command against the provider's S3 endpoint (works for Custom S3 and R2).
|
||||
export function buildAwsCorsCommand(bucket, endpoint, config) {
|
||||
const bucketName = (bucket && bucket.trim()) || "YOUR_BUCKET";
|
||||
const ep = endpoint && endpoint.trim() ? ` --endpoint-url ${endpoint.trim()}` : "";
|
||||
return `aws s3api put-bucket-cors --bucket ${bucketName}${ep} --cors-configuration '${JSON.stringify(config)}'`;
|
||||
}
|
||||
|
||||
function resolveEndpointForCors(provider, region, rawEndpoint) {
|
||||
if (provider === PROVIDER_B2) {
|
||||
return deriveB2Endpoint(region);
|
||||
}
|
||||
if (provider === PROVIDER_R2) {
|
||||
return rawEndpoint ? `https://${rawEndpoint}.r2.cloudflarestorage.com` : "";
|
||||
}
|
||||
return rawEndpoint || "";
|
||||
}
|
||||
|
||||
// Build the provider-specific modal content (intro / JSON / command + label / notes).
|
||||
export function buildCorsHelpContent({ provider, origins, bucket, endpoint } = {}) {
|
||||
if (provider === PROVIDER_B2) {
|
||||
const rules = buildBucketCorsRules(origins);
|
||||
return {
|
||||
intro:
|
||||
"Backblaze B2's web-console CORS presets (\"Share everything…\") only allow " +
|
||||
"downloads. Uploading from your browser needs the custom rules below, applied " +
|
||||
"with B2's command-line tool.",
|
||||
json: JSON.stringify(rules, null, 2),
|
||||
cmdLabel: "Apply it (B2 command-line tool)",
|
||||
cmd: buildB2CorsCommand(bucket, rules),
|
||||
notes:
|
||||
"1. Install the B2 CLI (pip install b2).\n" +
|
||||
"2. Run the command above (it authorizes, then sets the rules).\n" +
|
||||
"3. Wait ~1 minute, then retry your upload.\n" +
|
||||
"Tip: a B2 application key with the writeBucketCors capability lets bitvid set " +
|
||||
"this for you automatically on Save.",
|
||||
};
|
||||
}
|
||||
|
||||
const isR2 = provider === PROVIDER_R2;
|
||||
const config = buildS3CorsConfig(origins);
|
||||
return {
|
||||
intro:
|
||||
"bitvid tries to set CORS for you when you Save. If uploads still fail (the key " +
|
||||
"lacks CORS-write permission, or your provider needs it set manually), apply the " +
|
||||
"rules below.",
|
||||
json: JSON.stringify(config, null, 2),
|
||||
cmdLabel: isR2
|
||||
? "Apply it (Cloudflare dashboard, or AWS CLI)"
|
||||
: "Apply it (provider dashboard, or AWS CLI)",
|
||||
cmd: buildAwsCorsCommand(bucket, endpoint, config),
|
||||
notes: isR2
|
||||
? "Cloudflare dashboard: R2 → your bucket → Settings → CORS Policy → paste the JSON.\n" +
|
||||
"Or run the AWS CLI command above (it targets your R2 endpoint)."
|
||||
: "Paste the JSON into your provider's bucket CORS settings, or run the AWS CLI " +
|
||||
"command above against your endpoint.",
|
||||
};
|
||||
}
|
||||
|
||||
export class StorageCorsHelp {
|
||||
constructor({ getBucket } = {}) {
|
||||
constructor({ getProvider, getBucket, getRegion, getEndpoint } = {}) {
|
||||
this.getProvider = typeof getProvider === "function" ? getProvider : () => "";
|
||||
this.getBucket = typeof getBucket === "function" ? getBucket : () => "";
|
||||
this.getRegion = typeof getRegion === "function" ? getRegion : () => "";
|
||||
this.getEndpoint = typeof getEndpoint === "function" ? getEndpoint : () => "";
|
||||
this.helpBtn = null;
|
||||
this.modal = null;
|
||||
this.introEl = null;
|
||||
this.jsonEl = null;
|
||||
this.cliEl = null;
|
||||
this.cmdLabelEl = null;
|
||||
this.notesEl = null;
|
||||
this.copyJsonBtn = null;
|
||||
this.copyCmdBtn = null;
|
||||
}
|
||||
@@ -49,8 +132,11 @@ export class StorageCorsHelp {
|
||||
cacheDom(doc = document) {
|
||||
this.helpBtn = doc.getElementById("storageCorsHelpBtn") || null;
|
||||
this.modal = doc.getElementById("storageCorsModal") || null;
|
||||
this.introEl = doc.getElementById("storageCorsModalIntro") || null;
|
||||
this.jsonEl = doc.getElementById("storageCorsJson") || null;
|
||||
this.cliEl = doc.getElementById("storageCorsCli") || null;
|
||||
this.cmdLabelEl = doc.getElementById("storageCorsCmdLabel") || null;
|
||||
this.notesEl = doc.getElementById("storageCorsNotes") || null;
|
||||
this.copyJsonBtn = doc.getElementById("storageCorsCopyJsonBtn") || null;
|
||||
this.copyCmdBtn = doc.getElementById("storageCorsCopyCmdBtn") || null;
|
||||
}
|
||||
@@ -81,8 +167,6 @@ export class StorageCorsHelp {
|
||||
}
|
||||
}
|
||||
|
||||
// R2 auto-applies CORS via its own flow; the manual helper is for the S3-compatible
|
||||
// providers (B2 / Custom S3) where the bucket owner sets it.
|
||||
setVisible(show) {
|
||||
if (this.helpBtn instanceof HTMLElement) {
|
||||
this.helpBtn.classList.toggle("hidden", !show);
|
||||
@@ -99,13 +183,20 @@ export class StorageCorsHelp {
|
||||
} catch (error) {
|
||||
origins = [];
|
||||
}
|
||||
const rules = buildBucketCorsRules(origins);
|
||||
if (this.jsonEl) {
|
||||
this.jsonEl.textContent = JSON.stringify(rules, null, 2);
|
||||
}
|
||||
if (this.cliEl) {
|
||||
this.cliEl.textContent = buildB2CorsCommand(this.getBucket(), rules);
|
||||
}
|
||||
const provider = this.getProvider();
|
||||
const content = buildCorsHelpContent({
|
||||
provider,
|
||||
origins,
|
||||
bucket: this.getBucket(),
|
||||
endpoint: resolveEndpointForCors(provider, this.getRegion(), this.getEndpoint()),
|
||||
});
|
||||
|
||||
if (this.introEl) this.introEl.textContent = content.intro;
|
||||
if (this.jsonEl) this.jsonEl.textContent = content.json;
|
||||
if (this.cmdLabelEl) this.cmdLabelEl.textContent = content.cmdLabel;
|
||||
if (this.cliEl) this.cliEl.textContent = content.cmd;
|
||||
if (this.notesEl) this.notesEl.textContent = content.notes;
|
||||
|
||||
this.modal.classList.remove("hidden");
|
||||
const sheet = this.modal.querySelector(".modal-sheet");
|
||||
if (sheet instanceof HTMLElement) {
|
||||
|
||||
@@ -1,29 +1,35 @@
|
||||
// Storage CORS setup helper (B2 / S3-compatible): the rules + command bitvid shows a
|
||||
// user so in-browser uploads work. The crucial property: the rules include the UPLOAD
|
||||
// operations (s3_put / s3_post) that B2's web-console "share" presets omit — that gap
|
||||
// is exactly why uploads CORS-fail until these custom rules are applied.
|
||||
// Provider-aware storage CORS setup helper. The helper must give the RIGHT rules +
|
||||
// apply-command for each provider: B2-native rules + the b2 CLI for Backblaze B2, and
|
||||
// standard S3 CORS + AWS CLI / dashboard guidance for Custom S3 and Cloudflare R2.
|
||||
// The invariant across all of them: the rules allow browser uploads (PUT/POST), which
|
||||
// is what makes uploads stop CORS-failing.
|
||||
|
||||
import test, { beforeEach, afterEach } from "node:test";
|
||||
import { strict as assert } from "node:assert";
|
||||
import { JSDOM } from "jsdom";
|
||||
import {
|
||||
buildBucketCorsRules,
|
||||
buildS3CorsConfig,
|
||||
buildB2CorsCommand,
|
||||
buildAwsCorsCommand,
|
||||
buildCorsHelpContent,
|
||||
StorageCorsHelp,
|
||||
} from "../js/ui/profileModal/storageCorsHelp.js";
|
||||
|
||||
test("CORS rules include the upload operations the B2 presets omit", () => {
|
||||
test("B2-native rules include the upload operations the B2 presets omit", () => {
|
||||
const [rule] = buildBucketCorsRules(["https://bitvid.network"]);
|
||||
for (const op of ["s3_put", "s3_post"]) {
|
||||
assert.ok(
|
||||
rule.allowedOperations.includes(op),
|
||||
`rule must allow ${op} (browser upload) — the whole point of this helper`,
|
||||
);
|
||||
}
|
||||
// ...and still allow ranged playback reads.
|
||||
assert.ok(rule.allowedOperations.includes("s3_put"));
|
||||
assert.ok(rule.allowedOperations.includes("s3_post"));
|
||||
assert.ok(rule.allowedOperations.includes("s3_get"));
|
||||
assert.ok(rule.allowedOperations.includes("s3_head"));
|
||||
assert.deepEqual(rule.exposeHeaders, [
|
||||
});
|
||||
|
||||
test("standard S3 CORS config allows upload + ranged playback methods", () => {
|
||||
const cfg = buildS3CorsConfig(["https://bitvid.network"]);
|
||||
const rule = cfg.CORSRules[0];
|
||||
assert.deepEqual(rule.AllowedMethods, ["GET", "HEAD", "PUT", "POST", "DELETE"]);
|
||||
// OPTIONS is not a valid S3 AllowedMethod and must not be present.
|
||||
assert.ok(!rule.AllowedMethods.includes("OPTIONS"));
|
||||
assert.deepEqual(rule.ExposeHeaders, [
|
||||
"ETag",
|
||||
"Content-Length",
|
||||
"Content-Range",
|
||||
@@ -31,41 +37,83 @@ test("CORS rules include the upload operations the B2 presets omit", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("CORS rules use the given origins, and fall back to every-origin when none", () => {
|
||||
assert.deepEqual(
|
||||
buildBucketCorsRules(["https://a.example", "https://b.example"])[0]
|
||||
.allowedOrigins,
|
||||
["https://a.example", "https://b.example"],
|
||||
);
|
||||
test("origins fall back to every-origin when none are provided", () => {
|
||||
assert.deepEqual(buildBucketCorsRules([])[0].allowedOrigins, ["*"]);
|
||||
assert.deepEqual(buildBucketCorsRules(undefined)[0].allowedOrigins, ["*"]);
|
||||
assert.deepEqual(buildS3CorsConfig(undefined).CORSRules[0].AllowedOrigins, ["*"]);
|
||||
});
|
||||
|
||||
test("the B2 CLI command targets the bucket as allPublic with compact one-line JSON", () => {
|
||||
const rules = buildBucketCorsRules(["https://bitvid.network"]);
|
||||
const cmd = buildB2CorsCommand("bitvid", rules);
|
||||
test("B2 command targets the bucket allPublic with compact one-line JSON", () => {
|
||||
const cmd = buildB2CorsCommand("bitvid", buildBucketCorsRules(["https://x"]));
|
||||
assert.match(cmd, /^b2 update-bucket --corsRules '.*' bitvid allPublic$/);
|
||||
assert.ok(!cmd.includes("\n"), "command must be a single pasteable line");
|
||||
// The embedded JSON is valid and carries the upload op.
|
||||
const json = cmd.slice(cmd.indexOf("'") + 1, cmd.lastIndexOf("'"));
|
||||
assert.ok(JSON.parse(json)[0].allowedOperations.includes("s3_put"));
|
||||
assert.ok(!cmd.includes("\n"));
|
||||
});
|
||||
|
||||
test("a missing bucket name degrades to an obvious placeholder (not an empty arg)", () => {
|
||||
assert.match(buildB2CorsCommand("", buildBucketCorsRules([])), /YOUR_BUCKET allPublic$/);
|
||||
test("AWS command includes the endpoint + bucket, one line", () => {
|
||||
const cmd = buildAwsCorsCommand(
|
||||
"vids",
|
||||
"https://s3.example.com",
|
||||
buildS3CorsConfig(["https://x"]),
|
||||
);
|
||||
assert.match(
|
||||
cmd,
|
||||
/^aws s3api put-bucket-cors --bucket vids --endpoint-url https:\/\/s3\.example\.com --cors-configuration '.*'$/,
|
||||
);
|
||||
});
|
||||
|
||||
// --- DOM behavior ---
|
||||
test("content for Backblaze B2 uses B2-native rules + the b2 CLI", () => {
|
||||
const c = buildCorsHelpContent({
|
||||
provider: "backblaze_b2",
|
||||
origins: ["https://bitvid.network"],
|
||||
bucket: "bitvid",
|
||||
endpoint: "https://s3.us-west-004.backblazeb2.com",
|
||||
});
|
||||
assert.match(c.cmd, /^b2 update-bucket /);
|
||||
assert.match(c.cmdLabel, /B2 command-line/i);
|
||||
assert.ok(JSON.parse(c.json)[0].allowedOperations.includes("s3_put"));
|
||||
assert.match(c.notes, /b2 CLI|pip install b2/i);
|
||||
});
|
||||
|
||||
test("content for Custom S3 uses standard S3 CORS + the AWS CLI", () => {
|
||||
const c = buildCorsHelpContent({
|
||||
provider: "generic_s3",
|
||||
origins: ["https://bitvid.network"],
|
||||
bucket: "vids",
|
||||
endpoint: "https://s3.example.com",
|
||||
});
|
||||
assert.match(c.cmd, /^aws s3api put-bucket-cors /);
|
||||
assert.match(c.cmd, /--endpoint-url https:\/\/s3\.example\.com/);
|
||||
assert.ok(JSON.parse(c.json).CORSRules[0].AllowedMethods.includes("PUT"));
|
||||
assert.doesNotMatch(c.cmd, /backblaze|b2 update-bucket/i);
|
||||
});
|
||||
|
||||
test("content for Cloudflare R2 uses S3 CORS and points at the R2 dashboard", () => {
|
||||
const c = buildCorsHelpContent({
|
||||
provider: "cloudflare_r2",
|
||||
origins: ["https://bitvid.network"],
|
||||
bucket: "vids",
|
||||
endpoint: "https://acct.r2.cloudflarestorage.com",
|
||||
});
|
||||
assert.match(c.cmd, /^aws s3api put-bucket-cors /);
|
||||
assert.match(c.notes, /Cloudflare dashboard/i);
|
||||
assert.match(c.cmdLabel, /Cloudflare/i);
|
||||
assert.ok(JSON.parse(c.json).CORSRules);
|
||||
});
|
||||
|
||||
// --- DOM behavior (provider switch reflected in the modal) ---
|
||||
let dom;
|
||||
beforeEach(() => {
|
||||
dom = new JSDOM(
|
||||
`<!doctype html><html><body>
|
||||
<button id="storageCorsHelpBtn" class="hidden"></button>
|
||||
<select id="prov"><option value="cloudflare_r2">r2</option></select>
|
||||
<button id="storageCorsHelpBtn"></button>
|
||||
<div id="storageCorsModal" class="hidden">
|
||||
<div class="modal-sheet" tabindex="-1"></div>
|
||||
<button data-storage-cors-dismiss id="x"></button>
|
||||
<p id="storageCorsModalIntro"></p>
|
||||
<code id="storageCorsJson"></code>
|
||||
<span id="storageCorsCmdLabel"></span>
|
||||
<code id="storageCorsCli"></code>
|
||||
<p id="storageCorsNotes"></p>
|
||||
<button id="storageCorsCopyJsonBtn"></button>
|
||||
<button id="storageCorsCopyCmdBtn"></button>
|
||||
</div>
|
||||
@@ -82,30 +130,33 @@ afterEach(() => {
|
||||
delete globalThis.window;
|
||||
});
|
||||
|
||||
test("open() fills the JSON + command from the current origin and reveals the modal", () => {
|
||||
const help = new StorageCorsHelp({ getBucket: () => "bitvid" });
|
||||
test("open() renders provider-specific content and pre-fills the current origin", () => {
|
||||
let provider = "backblaze_b2";
|
||||
const help = new StorageCorsHelp({
|
||||
getProvider: () => provider,
|
||||
getBucket: () => "bitvid",
|
||||
getRegion: () => "us-west-004",
|
||||
getEndpoint: () => "",
|
||||
});
|
||||
help.cacheDom(dom.window.document);
|
||||
help.registerEventListeners();
|
||||
|
||||
help.open();
|
||||
const doc = dom.window.document;
|
||||
assert.ok(!doc.getElementById("storageCorsModal").classList.contains("hidden"));
|
||||
assert.match(doc.getElementById("storageCorsCli").textContent, /^b2 update-bucket /);
|
||||
const b2Rule = JSON.parse(doc.getElementById("storageCorsJson").textContent)[0];
|
||||
assert.deepEqual(b2Rule.allowedOrigins, ["https://unstable.bitvid.network"]);
|
||||
|
||||
const modal = dom.window.document.getElementById("storageCorsModal");
|
||||
assert.ok(!modal.classList.contains("hidden"), "modal is shown");
|
||||
|
||||
const json = dom.window.document.getElementById("storageCorsJson").textContent;
|
||||
const rule = JSON.parse(json)[0];
|
||||
assert.deepEqual(
|
||||
rule.allowedOrigins,
|
||||
["https://unstable.bitvid.network"],
|
||||
"origins pre-filled from getCorsOrigins()",
|
||||
);
|
||||
|
||||
const cli = dom.window.document.getElementById("storageCorsCli").textContent;
|
||||
assert.match(cli, /bitvid allPublic$/);
|
||||
// Switch provider → reopening yields the AWS-CLI form.
|
||||
provider = "generic_s3";
|
||||
help.open();
|
||||
assert.match(doc.getElementById("storageCorsCli").textContent, /^aws s3api put-bucket-cors /);
|
||||
assert.ok(JSON.parse(doc.getElementById("storageCorsJson").textContent).CORSRules);
|
||||
});
|
||||
|
||||
test("dismiss controls hide the modal again", () => {
|
||||
const help = new StorageCorsHelp({ getBucket: () => "bitvid" });
|
||||
const help = new StorageCorsHelp({ getProvider: () => "generic_s3" });
|
||||
help.cacheDom(dom.window.document);
|
||||
help.registerEventListeners();
|
||||
help.open();
|
||||
@@ -114,13 +165,3 @@ test("dismiss controls hide the modal again", () => {
|
||||
dom.window.document.getElementById("storageCorsModal").classList.contains("hidden"),
|
||||
);
|
||||
});
|
||||
|
||||
test("setVisible toggles the help button for S3-compatible vs R2", () => {
|
||||
const help = new StorageCorsHelp({ getBucket: () => "" });
|
||||
help.cacheDom(dom.window.document);
|
||||
const btn = dom.window.document.getElementById("storageCorsHelpBtn");
|
||||
help.setVisible(true);
|
||||
assert.ok(!btn.classList.contains("hidden"));
|
||||
help.setVisible(false);
|
||||
assert.ok(btn.classList.contains("hidden"));
|
||||
});
|
||||
@@ -865,16 +865,24 @@ Reported 2026-06-25. Relates to #17 (NIP-71 interop) / the bitvid→NIP-71 mirro
|
||||
`s3ApiUrl` = `https://s3.us-west-004.backblazeb2.com` = `deriveB2Endpoint("us-west-004")`;
|
||||
bucket is `allPublic` → S3-style playback URL works. Key file caps
|
||||
(read/write/list/delete) ✓.
|
||||
- [x] **CORS helper modal — DONE 2026-06-30.** Empirically confirmed the gotcha: B2's
|
||||
web-console "Share everything…" presets write DOWNLOAD-only CORS (`s3_get/s3_head`,
|
||||
`b2_download_*`) — **no `s3_put/s3_post`** — so browser uploads CORS-fail. Added a
|
||||
"CORS setup help" button (shown for B2 / Custom S3) opening a modal that shows the
|
||||
exact B2-native CORS rules JSON (upload + ranged-playback ops, origins pre-filled
|
||||
from `getCorsOrigins()`) + the `b2 update-bucket --corsRules …` command to apply
|
||||
them, with copy buttons and step-by-step (incl. the "use a key with
|
||||
`writeBucketCors` and bitvid sets it for you" tip). Extracted to
|
||||
`js/ui/profileModal/storageCorsHelp.js` (keeps the controller <1000 lines). Tests:
|
||||
`tests/storage-cors-help.test.mjs` (8). Build + lint clean.
|
||||
- [x] **CORS helper modal (provider-aware) — DONE 2026-06-30.** Empirically confirmed
|
||||
the gotcha: B2's web-console "Share everything…" presets write DOWNLOAD-only CORS
|
||||
(`s3_get/s3_head`, `b2_download_*`) — **no `s3_put/s3_post`** — so browser uploads
|
||||
CORS-fail. Added a "CORS setup help" button (shown for **all** bucket providers)
|
||||
opening a **provider-aware** modal:
|
||||
- **Backblaze B2** → B2-native rules JSON (upload + ranged-playback ops) + the
|
||||
`b2 update-bucket --corsRules …` command.
|
||||
- **Custom S3** → standard S3 `CORSConfiguration` JSON + `aws s3api put-bucket-cors
|
||||
--endpoint-url <endpoint> …`.
|
||||
- **Cloudflare R2** → standard S3 JSON + Cloudflare-dashboard guidance (R2 → bucket
|
||||
→ Settings → CORS Policy) and the AWS-CLI command against the derived R2 endpoint.
|
||||
Origins pre-filled from `getCorsOrigins()`; copy buttons for JSON + command. bitvid
|
||||
still auto-applies CORS on Save (R2/S3 via `ensureBucketCors`) — this is the manual
|
||||
fallback for when the key lacks CORS-write permission. Logic in
|
||||
`js/ui/profileModal/storageCorsHelp.js` (`buildCorsHelpContent` switches on
|
||||
provider; keeps the controller <1000 lines). Tests:
|
||||
`tests/storage-cors-help.test.mjs` (10 — per-provider rules/command + DOM switch).
|
||||
Build + lint clean.
|
||||
- [ ] **VERIFY live** on unstable: pick Backblaze B2, enter region (`us-west-004`) +
|
||||
bucket + key/secret, Test → passes; apply the CORS helper's rules; upload a video →
|
||||
plays from `https://<bucket>.s3.<region>.backblazeb2.com/...`.
|
||||
|
||||
Reference in new issue
Block a user