diff --git a/lith/Caddyfile b/lith/Caddyfile index f34537d4ca..6f0e8c1f83 100644 --- a/lith/Caddyfile +++ b/lith/Caddyfile @@ -110,6 +110,13 @@ reverse_proxy localhost:8888 } + # --- ipfs.aesthetic.computer (self-hosted IPFS gateway) --- + @ipfs host ipfs.aesthetic.computer + handle @ipfs { + header Access-Control-Allow-Origin * + reverse_proxy localhost:8090 + } + # --- justanothersystem.org --- @wwwjas2 host www.justanothersystem.org handle @wwwjas2 { diff --git a/system/netlify/functions/keep-prepare-background.mjs b/system/netlify/functions/keep-prepare-background.mjs index 67e16d9140..63468a6ee5 100644 --- a/system/netlify/functions/keep-prepare-background.mjs +++ b/system/netlify/functions/keep-prepare-background.mjs @@ -367,25 +367,24 @@ function isCachedMediaValid(piece) { && piece?.ipfsMedia?.sourceHash === hashSource(piece.source); } -// ─── IPFS Upload ───────────────────────────────────────────────────────────── +// ─── IPFS Upload (self-hosted Kubo node on lith) ───────────────────────────── +const IPFS_API = process.env.IPFS_API_URL || "http://localhost:5001"; + async function uploadToIPFS(content, filename, mimeType, timeoutMs = 90000) { - const pinata = await getPinataCredentials(); const formData = new FormData(); formData.append("file", new Blob([content], { type: mimeType }), filename); - formData.append("pinataMetadata", JSON.stringify({ name: filename })); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), Math.max(3000, timeoutMs)); try { - const res = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", { + const res = await fetch(`${IPFS_API}/api/v0/add?pin=true`, { method: "POST", - headers: { pinata_api_key: pinata.apiKey, pinata_secret_api_key: pinata.apiSecret }, body: formData, signal: controller.signal, }); clearTimeout(timeout); if (!res.ok) throw new Error(`IPFS upload failed: ${res.status}`); const result = await res.json(); - return formatIpfsUri(result.IpfsHash); + return formatIpfsUri(result.Hash); } catch (err) { clearTimeout(timeout); if (err.name === "AbortError") throw new Error(`IPFS upload timed out after ${Math.round(timeoutMs / 1000)}s`); @@ -394,24 +393,21 @@ async function uploadToIPFS(content, filename, mimeType, timeoutMs = 90000) { } async function uploadJsonToIPFS(json, name, timeoutMs = 30000) { - const pinata = await getPinataCredentials(); + const content = JSON.stringify(json); + const formData = new FormData(); + formData.append("file", new Blob([content], { type: "application/json" }), name); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), Math.max(3000, timeoutMs)); try { - const res = await fetch("https://api.pinata.cloud/pinning/pinJSONToIPFS", { + const res = await fetch(`${IPFS_API}/api/v0/add?pin=true`, { method: "POST", - headers: { - "Content-Type": "application/json", - pinata_api_key: pinata.apiKey, - pinata_secret_api_key: pinata.apiSecret, - }, - body: JSON.stringify({ pinataContent: json, pinataMetadata: { name } }), + body: formData, signal: controller.signal, }); clearTimeout(timeout); if (!res.ok) throw new Error(`Metadata upload failed: ${res.status}`); const result = await res.json(); - return formatIpfsUri(result.IpfsHash); + return formatIpfsUri(result.Hash); } catch (err) { clearTimeout(timeout); if (err.name === "AbortError") throw new Error(`Metadata upload timed out after ${Math.round(timeoutMs / 1000)}s`); @@ -495,8 +491,6 @@ async function runPipeline({ jobId, pieceName, isRebake, regenerate, creatorWall log("cache", "Reusing cached"); } - const pinataCredentials = await getPinataCredentials(); - // ── Thumbnail (async) ────────────────────────────────────────────── let thumbnailPromise; if (!useCachedMedia) { @@ -509,25 +503,26 @@ async function runPipeline({ jobId, pieceName, isRebake, regenerate, creatorWall const controller = new AbortController(); const tid = setTimeout(() => controller.abort(), timeoutMs); try { - const res = await fetch(`${ovenUrl}/grab-ipfs`, { + // Use /grab to get raw image, then pin to local IPFS + const res = await fetch(`${ovenUrl}/grab`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ piece: `$${pieceName}`, format: "webp", width: 256, height: 256, density: 2, - duration: forceFresh ? 4000 : 5000, fps: 8, playbackFps: 16, quality: 70, - source: "keep", - author: userHandle ? `@${userHandle}` : null, + duration: forceFresh ? 4000 : 5000, fps: 8, + quality: 70, cacheKey: forceFresh ? `rebake-${pieceSourceHash}-${Date.now()}` : `src-${pieceSourceHash}`, skipCache: forceFresh, - pinataKey: pinataCredentials.apiKey, - pinataSecret: pinataCredentials.apiSecret, }), signal: controller.signal, }); clearTimeout(tid); if (!res.ok) throw new Error(`Oven ${res.status}`); - return res.json(); + const buffer = Buffer.from(await res.arrayBuffer()); + const thumbFilename = `${pieceName}-thumbnail.webp-${pieceSourceHash.slice(0, 16)}`; + const ipfsUri = await uploadToIPFS(buffer, thumbFilename, "image/webp"); + return { ipfsUri, size: buffer.length }; } catch (err) { clearTimeout(tid); if (err.name === "AbortError") throw new Error(`Oven timed out after ${timeoutMs / 1000}s`); @@ -536,18 +531,12 @@ async function runPipeline({ jobId, pieceName, isRebake, regenerate, creatorWall }; try { - const result = await tryOven(OVEN_URL, KEEP_MINT_THUMBNAIL_TIMEOUT_MS); - if (result?.ipfsUri) return result; - if (result?.error) throw new Error(result.error); - return result; + return await tryOven(OVEN_URL, KEEP_MINT_THUMBNAIL_TIMEOUT_MS); } catch (err) { log("thumbnail", `Primary oven failed: ${err.message}`); if (OVEN_URL !== OVEN_FALLBACK_URL) { try { - const fb = await tryOven(OVEN_FALLBACK_URL, KEEP_MINT_THUMBNAIL_TIMEOUT_MS); - if (fb?.ipfsUri) return fb; - if (fb?.error) throw new Error(fb.error); - return fb; + return await tryOven(OVEN_FALLBACK_URL, KEEP_MINT_THUMBNAIL_TIMEOUT_MS); } catch (fbErr) { return { error: fbErr.message }; }