refactor(api): migrate /api/upload from Arweave to Supabase
Summary
Migrates POST /api/upload from Arweave to a Supabase public bucket. Arweave wallet has been broken in production (every real upload returns "Transaction verification failed") so this PR fully replaces the write path.
The endpoint now requires authentication (Authorization: Bearer <privy> or x-api-key) and stamps { uploaded_by: <accountId> } onto each storage object's metadata for audit/takedown. MIME validation is tightened — typeless / octet-stream files are rejected with 415. Error responses are generic; full details are logged server-side.
Paired with:
- recoupable/database#23 — creates the
public-uploadsbucket (already merged) - recoupable/chat#1740 — threads Privy
accessTokenthrough all upload callsites - recoupable/docs#196 — openapi spec updated to reflect auth + status codes
Sensitive uploads (signed URLs / private buckets) are already served by the existing user-files bucket — not changed by this PR.
What changes
Adds
lib/files/uploadFileHandler.ts— POST /api/upload handler (multipart + CORS + auth + metadata)lib/files/uploadPublicAsset.ts— provider-agnostic public upload primitivelib/supabase/storage/getPublicUrlByKey.ts— generates public URLs from object keyslib/networking/fetchRemoteImageBuffer.ts— SSRF-safe + size-capped + timeout-bounded image fetch (replaces the fetch half of the oldmirrorUrlToPublicBucket)lib/networking/isSafeHttpUrl.ts(moved fromlib/arweave/)- 2 new constants in
lib/const.ts:SUPABASE_PUBLIC_UPLOADS_BUCKET,SUPPORTED_UPLOAD_MIME
Rewires (Arweave → Supabase via uploadPublicAsset)
POST /api/upload— now auth-gated, MIME-validated, stampsuploaded_bymetadataGET /api/x402/image/generate— image + contract metadata both go to public buckethandleInstagramProfileScraperResults— apify webhook image mirrorcreateImageMoment— uploads contract metadata JSON viauploadPublicAssetgenerateAndProcessImage,createKnowledgeBase,registerGenerateTxtFileTool
Removes
- All
lib/arweave/upload*.tshelpers lib/arweave/client.tsarweavenpm dep
Read-side helpers (getFetchableUrl, arweaveGatewayUrl, IPFS utilities) stay — legacy ar:// URLs in DB/messages still need to resolve.
Test plan
-
pnpm test— 150/150 green -
pnpm lint:check+pnpm format:checkclean - End-to-end smoke via Privy token from chat preview: POST returns 200, supabase.co URL fetchable (byte-perfect round-trip),
uploaded_bymetadata stamped - 401 on unauth, 415 on octet-stream/disallowed MIME, 500 with generic message on storage failure
- Chat-side cutover (recoupable/chat#1740): both code paths (
lib/files/uploadFileand the dropzone inline fetch) verified hitting api with Bearer header against the public bucket
Summary by cubic
Phase 1 of the Arweave → Supabase migration: all writes now go to a public Supabase bucket while keeping /api/upload wire-compatible for chat. Adds auth to uploads, stamps uploader metadata, tightens MIME checks (allowlist now includes text/plain and application/json), unifies error responses, and hardens remote-image fetches.
-
Refactors
- Added
uploadPublicAsset,getPublicUrlByKey, SSRF-safefetchRemoteImageBuffer, and the/api/uploadroute viauploadFileHandler(CORS + multipart + auth). Handler attaches{ uploaded_by: accountId }, enforces an allowlist, returns generic errors, and re-emits auth failures as{ success: false, error }. - Rewired:
/api/upload,/api/x402/image/generate(returnsuploadError; typesmomentasCreateMomentResponse | null), Apify Instagram webhook (fetch → upload mirror), knowledge base, In Process moment creation; removed the Arweave orchestration by inlining at the two callers. createImageMomentnow acceptsimageUriand uploads contract metadata JSON viauploadPublicAsset.- Storage keys are flat UUIDs at the bucket root (no extensions) with
cache-control: 31536000; upload allowlist viaSUPPORTED_UPLOAD_MIME(now includestext/plainandapplication/jsonfor knowledge edits). - Inlined MCP
generate_txt_fileto upload directly via Supabase and returntxtUrl; the tool resolves the caller’s account and stamps{ uploaded_by }; updated the tool chain and tests. - Hardened
fetchRemoteImageBuffer: cancels unread bodies on early returns; preserves 10 MiB cap and timeout. Kept read-side helpers for legacyar:///IPFS URLs; removed all Arweave upload code and thearweavedependency.
- Added
-
Migration
- Requires bucket creation: recoupable/database#23.
- POST
/api/uploadnow requiresx-api-keyor Bearer auth; response shape unchanged, and 401s use{ success: false, error }. Chat: allow the new Supabase image hostname. - Out of scope for phase 1: signed URLs, private buckets, rate limiting.
Summary by CodeRabbit
Release Notes
-
New Features
- New file upload endpoint with multipart form support and CORS compatibility.
- Uploaded assets receive permanent public CDN URLs for persistent access.
-
Refactor
- Updated storage architecture for generated images, knowledge bases, text files, and profile pictures to provide direct public access.