fix(blossom): upload directly per-server instead of the failing /mirror flow [LOCAL] (#30)

Real-device failure: multiServerUpload uploads to one server then has the OTHERS
fetch it server-to-server via PUT /mirror — those /mirror requests fail from a
browser (net::ERR_FAILED / CORS), so the whole upload returned no blob URL
("Blossom upload did not return a usable blob URL").

Switch blossomService.uploadFile to upload DIRECTLY to each server (BUD-02
PUT /upload) in parallel via uploadBlob, resilient: first success wins, per-server
failures are collected, and if ALL fail the error names each server + the likely
cause (CORS / size / payment) and suggests blossom.band. A fresh signed upload
auth (BUD-11) is created per request.

Tests: blossom-service 11/11 (added partial-failure fallback + all-fail error).
Lint + build clean. Local only.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-07-09 21:49:50 -04:00
1 parent ba62b35ada
commit 18ea5428eb
2 files changed
+74 -22

No files matched your search

+33 -10
View File
@@ -80,26 +80,49 @@ export class BlossomService {
}
const sdk = await this.loadSdk();
const { multiServerUpload, createUploadAuth } = sdk;
const { uploadBlob, createUploadAuth } = sdk;
if (
typeof multiServerUpload !== "function" ||
typeof uploadBlob !== "function" ||
typeof createUploadAuth !== "function"
) {
throw new Error("Blossom SDK is missing expected exports.");
}
// One auth per action, scoped to the blob's sha256; the SDK reuses it across
// the mirror set so a multi-server upload isn't N signer prompts.
const results = await multiServerUpload(serverList, file, {
onAuth: (server, sha256) => createUploadAuth(signer, sha256, { type }),
// Upload DIRECTLY to each server (BUD-02 `PUT /upload`), in parallel, rather
// than the SDK's upload-one-then-`/mirror` flow — the server-to-server mirror
// step fails from a browser (CORS) on many servers. A fresh signed upload auth
// (BUD-11, scoped to the blob's sha256) is created per request.
const onAuth = (server, sha256) =>
createUploadAuth(signer, sha256, { type });
const settled = await Promise.allSettled(
serverList.map((server) =>
uploadBlob(server, file, {
onAuth,
onProgress: typeof onProgress === "function" ? onProgress : undefined,
}),
),
);
const descriptors = [];
const failures = [];
settled.forEach((r, i) => {
if (r.status === "fulfilled" && r.value && typeof r.value.url === "string") {
descriptors.push(r.value);
} else {
const reason = r.reason;
failures.push(
`${serverList[i]}: ${reason?.message || reason || "no blob URL returned"}`,
);
}
});
const descriptors = Array.from(results?.values?.() || []);
const primary =
descriptors.find((d) => typeof d?.url === "string" && d.url) || null;
const primary = descriptors.find((d) => typeof d?.url === "string" && d.url);
if (!primary) {
throw new Error("Blossom upload did not return a usable blob URL.");
throw new Error(
`Blossom upload failed on all server(s) — ${failures.join("; ")}. ` +
`Common causes: the server blocks browser uploads (CORS), rejects the ` +
`file size, or requires payment/allowlisting. Try blossom.band.`,
);
}
return {
+41 -12
View File
@@ -16,23 +16,25 @@ import { FEATURE_BLOSSOM_STORAGE } from "../js/constants.js";
const SHA = "a".repeat(64);
const signer = async (draft) => ({ ...draft, id: "id", sig: "sig", pubkey: "pk" });
// A BlossomService whose SDK is mocked: multiServerUpload returns a descriptor
// map keyed by server, echoing the file so we can assert which blob was sent.
function mockService() {
// A BlossomService whose SDK is mocked: uploadBlob uploads directly to each
// server (BUD-02 /upload) and returns a descriptor, echoing the blob so we can
// assert which servers were hit. `failServers` simulates per-server failures.
function mockService({ failServers = [] } = {}) {
const uploads = [];
const svc = new BlossomService();
svc.loadSdk = async () => ({
createUploadAuth: async (s, sha256, opts) => ({ kind: 24242, sha256, opts }),
multiServerUpload: async (servers, file, opts) => {
uploads.push({ servers: [...servers], file });
uploadBlob: async (server, file, opts) => {
// Give each blob a distinct sha256 so video vs .torrent URLs differ.
const sha = file?.name?.includes(".torrent") ? "b".repeat(64) : SHA;
if (typeof opts?.onAuth === "function") await opts.onAuth(servers[0], sha);
const map = new Map();
for (const server of servers) {
map.set(server, { url: `${server}/${sha}`, sha256: sha, size: 1, type: "x" });
if (typeof opts?.onAuth === "function") {
await opts.onAuth(server, sha, "upload", file);
}
return map;
uploads.push({ server, file });
if (failServers.includes(server)) {
throw new Error("CORS blocked");
}
return { url: `${server}/${sha}`, sha256: sha, size: 1, type: "x" };
},
});
return { svc, uploads };
@@ -70,7 +72,7 @@ test("uploadFile guards: file, servers, signer required", async () => {
);
});
test("uploadFile returns the primary descriptor url + sha256 key, mirrored to all servers", async () => {
test("uploadFile uploads directly to every server and returns the primary url", async () => {
const { svc, uploads } = mockService();
const out = await svc.uploadFile({
file: new File([new Uint8Array([1])], "v.mp4"),
@@ -79,7 +81,34 @@ test("uploadFile returns the primary descriptor url + sha256 key, mirrored to al
});
assert.equal(out.url, `https://a/${SHA}`);
assert.equal(out.key, SHA);
assert.deepEqual(uploads[0].servers, ["https://a", "https://b"], "mirrored to both");
assert.deepEqual(
uploads.map((u) => u.server).sort(),
["https://a", "https://b"],
"uploaded to both servers directly (no /mirror)",
);
});
test("uploadFile survives partial failure — one server down, another succeeds", async () => {
const { svc } = mockService({ failServers: ["https://a"] });
const out = await svc.uploadFile({
file: new File([new Uint8Array([1])], "v.mp4"),
servers: ["https://a", "https://b"],
signer,
});
assert.equal(out.url, `https://b/${SHA}`, "falls back to the working server");
});
test("uploadFile throws a descriptive error when ALL servers fail", async () => {
const { svc } = mockService({ failServers: ["https://a", "https://b"] });
await assert.rejects(
() =>
svc.uploadFile({
file: new File([new Uint8Array([1])], "v.mp4"),
servers: ["https://a", "https://b"],
signer,
}),
/failed on all server\(s\).*CORS blocked/s,
);
});
test("uploadVideo without a torrent returns a blossom storagePointer and no magnet", async () => {