[storage] #39: fix profile image/banner upload (resolveConnection rejected hex pubkeys)

The Edit Profile image + banner upload was wired (resolve connection -> uploadFile ->
auto-fill URL) but silently broken: ProfileEditController passes a HEX pubkey to
r2Service.resolveConnection(), which only decoded npub1... strings via safeDecodeNpub.
A hex pubkey therefore resolved to null -> "Storage configuration missing" and the
Upload button stayed disabled even with storage configured. (Thumbnails worked because
mediaUploader calls listConnections(pubkey) directly.)

Fix: resolveConnection() now accepts a hex pubkey OR an npub. The rest of the chain is
already provider-agnostic (resolveConnection returns the defaultForUploads connection's
endpoint/baseDomain/forcePathStyle, uploadFile routes R2 vs generic-S3 by provider,
buildPublicUrl builds the URL), so profile/banner upload now works for R2 / B2 /
Custom S3.

Test: tests/r2-resolve-connection-hex.test.mjs (3 — nip19.decode throws on hex to prove
the hex path is used, plus npub still works and empty returns null). Build + lint clean;
r2/storage suites green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-30 11:07:11 -04:00
1 parent f0beab2713
commit a673ee96aa
3 files changed
+109 -8

No files matched your search

+7 -2
View File
@@ -447,8 +447,13 @@ export class R2Service {
async resolveConnection(npub) {
if (!npub) return null;
// 1. Try StorageService
const pubkey = safeDecodeNpub(npub);
// 1. Try StorageService. Accept either a hex pubkey or an npub — some callers
// (e.g. ProfileEditController's image upload) pass hex, and safeDecodeNpub only
// decodes npub1… strings, so passing hex used to silently resolve no connection.
const trimmed = String(npub).trim();
const pubkey = /^[0-9a-f]{64}$/i.test(trimmed)
? trimmed.toLowerCase()
: safeDecodeNpub(trimmed);
if (pubkey && storageService) {
try {
const connections = await storageService.listConnections(pubkey);
+83
View File
@@ -0,0 +1,83 @@
// #39: profile image/banner upload silently found "no storage" because
// ProfileEditController passes a HEX pubkey to r2Service.resolveConnection(), which
// only decoded npub1… strings (safeDecodeNpub) — so a hex pubkey resolved to null and
// the upload button stayed disabled even with storage configured. resolveConnection
// must accept a hex pubkey OR an npub.
import { describe, it, beforeEach, afterEach, mock } from "node:test";
import assert from "node:assert";
import r2ServiceDefault from "../js/services/r2Service.js";
import storageService from "../js/services/storageService.js";
const R2Service = r2ServiceDefault.constructor;
const HEX = "a".repeat(64);
let originalWindow;
beforeEach(() => {
originalWindow = globalThis.window;
// nip19.decode THROWS on anything that isn't a real npub — exactly how the live
// decoder behaves for a hex string. So if resolveConnection still works with hex,
// it's using the hex path, not npub decoding.
globalThis.window = {
location: { origin: "" },
NostrTools: {
nip19: {
decode: (value) => {
if (typeof value === "string" && value.startsWith("npub1")) {
return { type: "npub", data: HEX };
}
throw new Error("not an npub");
},
},
},
};
mock.method(storageService, "listConnections", async () => [
{ id: "backblaze_b2", provider: "backblaze_b2", meta: { defaultForUploads: true } },
]);
mock.method(storageService, "isUnlocked", () => true);
mock.method(storageService, "getConnection", async () => ({
provider: "backblaze_b2",
accessKeyId: "b2-key",
secretAccessKey: "b2-secret",
endpoint: "https://s3.us-west-004.backblazeb2.com",
region: "us-west-004",
forcePathStyle: false,
meta: {
bucket: "bitvid",
provider: "backblaze_b2",
publicBaseUrl: "https://bitvid.s3.us-west-004.backblazeb2.com",
defaultForUploads: true,
},
}));
});
afterEach(() => {
mock.restoreAll();
globalThis.window = originalWindow;
});
describe("r2Service.resolveConnection accepts hex or npub", () => {
it("resolves the configured connection when passed a HEX pubkey (the upload bug)", async () => {
const svc = new R2Service();
const conn = await svc.resolveConnection(HEX);
assert.ok(conn, "hex pubkey must resolve a connection, not null");
assert.equal(conn.provider, "backblaze_b2");
assert.equal(conn.bucket, "bitvid");
assert.equal(conn.baseDomain, "https://bitvid.s3.us-west-004.backblazeb2.com");
assert.equal(conn.accessKeyId, "b2-key");
});
it("still resolves when passed an npub", async () => {
const svc = new R2Service();
const conn = await svc.resolveConnection("npub1anything");
assert.ok(conn);
assert.equal(conn.provider, "backblaze_b2");
});
it("returns null for empty / non-key input", async () => {
const svc = new R2Service();
assert.equal(await svc.resolveConnection(""), null);
});
});
+19 -6
View File
@@ -903,12 +903,25 @@ Reported 2026-06-25. Relates to #17 (NIP-71 interop) / the bitvid→NIP-71 mirro
the export carries both). Storage + profile-modal suites 69/69; build + lint clean.
### 39. Image uploads should prefer uploading to configured storage (UX)
- [ ] Anywhere the user adds an image, prefer letting them **upload an image file to
their configured storage** (Cloudflare R2 / S3 / B2 / Blossom) and auto-grab the
resulting URL — rather than pasting a link. First target: the **profile image +
banner** in Edit Profile (upload → store → auto-fill the URL). Reuse the existing
upload pipeline (`s3UploadService` / thumbnail upload) and apply the same pattern
to other image inputs (thumbnails already do some of this).
- [x] **Profile image + banner upload — FIXED 2026-06-30.** The UI was already wired
(`ProfileEditController.handleUpload`: resolve connection → `uploadFile` → auto-fill
the URL field), but it was **broken**: it passed a **hex** pubkey to
`r2Service.resolveConnection()`, which only decoded `npub1…` strings
(`safeDecodeNpub`) → hex resolved to `null` → "Storage configuration missing" and the
Upload button stayed disabled even with storage configured. (Thumbnails worked
because `mediaUploader` uses `listConnections(pubkey)` directly.)
**Fix:** `resolveConnection` now accepts a hex pubkey OR an npub. The rest of the
chain is already provider-agnostic — `resolveConnection` returns the
`defaultForUploads` connection (any provider) with its `endpoint`/`baseDomain`/
`forcePathStyle`, `uploadFile` routes R2 vs generic-S3 by provider, and
`buildPublicUrl` builds the right URL — so this now works for R2 / B2 / Custom S3.
Test: `tests/r2-resolve-connection-hex.test.mjs` (3, with `nip19.decode` throwing on
hex to prove the hex path, not npub decoding). Build + lint clean.
- [ ] **VERIFY live:** unlock storage, Edit Profile → upload a picture/banner file →
uploads to the active provider's bucket and the URL auto-fills.
- [ ] **Extend the pattern** to any remaining image-URL inputs that still only accept a
pasted link (video thumbnail in upload/edit already uploads via `mediaUploader`).
Audit for others (e.g. future channel/branding image fields) and reuse the helper.
## Open — lower priority / infra