Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

keeps: pin CIDs to a public pinning service after every upload

Adds a vendor-neutral pinToPublicService() helper that speaks the standard
IPFS Pinning Service API spec (Filebase, web3.storage, Estuary all work).
Pins the CID we already own — no re-upload — so objkt's indexer and other
public gateways have a well-peered secondary source when our self-hosted
Kubo node is slow or briefly unreachable. Fire-and-forget, skips cleanly
when IPFS_PINNING_SERVICE_URL/TOKEN env vars are not configured.

Wired into both keep-prepare-background.mjs (new mints + rebakes) and
keep-update.mjs (on-chain metadata sync), alongside the existing oven
seeder so we retain our own mirror.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+51 -2
+27 -1
system/netlify/functions/keep-prepare-background.mjs
··· 371 371 && piece?.ipfsMedia?.sourceHash === hashSource(piece.source); 372 372 } 373 373 374 - // ─── IPFS Upload (self-hosted Kubo node on lith + oven seeder) ─────────────── 374 + // ─── IPFS Upload (self-hosted Kubo node on lith + oven seeder + public pin) ── 375 375 const IPFS_API = process.env.IPFS_API_URL || "http://localhost:5001"; 376 376 const IPFS_SEEDER_URL = process.env.IPFS_SEEDER_URL || "http://137.184.237.166:5001"; 377 377 378 + // Public pinning service (IPFS Pinning Service API spec — Filebase, web3.storage, 379 + // Estuary, etc.). Vendor-neutral so provider can be swapped by env var alone. 380 + // Pinning the CID we already own (no re-upload) gives objkt's indexer / public 381 + // gateways a well-peered place to fetch from while our node stays primary. 382 + const IPFS_PINNING_SERVICE_URL = process.env.IPFS_PINNING_SERVICE_URL || ""; 383 + const IPFS_PINNING_SERVICE_TOKEN = process.env.IPFS_PINNING_SERVICE_TOKEN || ""; 384 + 378 385 // Seed content to the oven IPFS node (fire-and-forget for faster gateway propagation) 379 386 function seedToSecondaryNode(hash) { 380 387 fetch(`${IPFS_SEEDER_URL}/api/v0/pin/add?arg=${hash}`, { method: "POST", signal: AbortSignal.timeout(120000) }) ··· 382 389 .catch(() => {}); // Best-effort, don't block pipeline 383 390 } 384 391 392 + // Pin existing CID on a public IPFS pinning service (fire-and-forget). 393 + // Skips silently when credentials aren't configured. 394 + function pinToPublicService(hash, name) { 395 + if (!IPFS_PINNING_SERVICE_URL || !IPFS_PINNING_SERVICE_TOKEN) return; 396 + fetch(`${IPFS_PINNING_SERVICE_URL}/pins`, { 397 + method: "POST", 398 + headers: { 399 + "Content-Type": "application/json", 400 + Authorization: `Bearer ${IPFS_PINNING_SERVICE_TOKEN}`, 401 + }, 402 + body: JSON.stringify({ cid: hash, ...(name ? { name } : {}) }), 403 + signal: AbortSignal.timeout(30000), 404 + }) 405 + .then(r => r.ok ? console.log(`📌 Pinned ${hash.slice(0, 12)}... to public service`) : r.text().then(t => console.warn(`📌 Public pin ${r.status}: ${t.slice(0, 200)}`))) 406 + .catch(() => {}); // Best-effort, don't block pipeline 407 + } 408 + 385 409 async function uploadToIPFS(content, filename, mimeType, timeoutMs = 90000) { 386 410 const formData = new FormData(); 387 411 formData.append("file", new Blob([content], { type: mimeType }), filename); ··· 397 421 if (!res.ok) throw new Error(`IPFS upload failed: ${res.status}`); 398 422 const result = await res.json(); 399 423 seedToSecondaryNode(result.Hash); 424 + pinToPublicService(result.Hash, filename); 400 425 return formatIpfsUri(result.Hash); 401 426 } catch (err) { 402 427 clearTimeout(timeout); ··· 421 446 if (!res.ok) throw new Error(`Metadata upload failed: ${res.status}`); 422 447 const result = await res.json(); 423 448 seedToSecondaryNode(result.Hash); 449 + pinToPublicService(result.Hash, name); 424 450 return formatIpfsUri(result.Hash); 425 451 } catch (err) { 426 452 clearTimeout(timeout);
+24 -1
system/netlify/functions/keep-update.mjs
··· 36 36 return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; 37 37 } 38 38 39 - // ─── IPFS Upload (self-hosted Kubo node on lith + oven seeder) ─────────────── 39 + // ─── IPFS Upload (self-hosted Kubo node on lith + oven seeder + public pin) ── 40 40 // Matches keep-prepare-background.mjs so the on-chain sync path has the same 41 41 // storage+mirroring guarantees as the prepare pipeline (no Pinata dependency). 42 42 const IPFS_API = process.env.IPFS_API_URL || "http://localhost:5001"; ··· 44 44 const USE_GATEWAY_URLS = process.env.USE_IPFS_GATEWAY_URLS === "true"; 45 45 const IPFS_GATEWAY = process.env.IPFS_GATEWAY || "https://ipfs.aesthetic.computer"; 46 46 47 + // Public pinning service (IPFS Pinning Service API spec — Filebase, etc.). 48 + // Pins the CID we already own, no re-upload, so objkt's indexer and other 49 + // gateways have a well-peered secondary to fetch from. 50 + const IPFS_PINNING_SERVICE_URL = process.env.IPFS_PINNING_SERVICE_URL || ""; 51 + const IPFS_PINNING_SERVICE_TOKEN = process.env.IPFS_PINNING_SERVICE_TOKEN || ""; 52 + 47 53 function formatIpfsUri(hash) { 48 54 return USE_GATEWAY_URLS ? `${IPFS_GATEWAY}/ipfs/${hash}` : `ipfs://${hash}`; 49 55 } ··· 55 61 .catch(() => {}); // Best-effort, don't block pipeline 56 62 } 57 63 64 + // Pin existing CID on a public pinning service (fire-and-forget). 65 + function pinToPublicService(hash, name) { 66 + if (!IPFS_PINNING_SERVICE_URL || !IPFS_PINNING_SERVICE_TOKEN) return; 67 + fetch(`${IPFS_PINNING_SERVICE_URL}/pins`, { 68 + method: "POST", 69 + headers: { 70 + "Content-Type": "application/json", 71 + Authorization: `Bearer ${IPFS_PINNING_SERVICE_TOKEN}`, 72 + }, 73 + body: JSON.stringify({ cid: hash, ...(name ? { name } : {}) }), 74 + signal: AbortSignal.timeout(30000), 75 + }) 76 + .then(r => r.ok ? console.log(`📌 KEEP-UPDATE: pinned ${hash.slice(0, 12)}... to public service`) : r.text().then(t => console.warn(`📌 KEEP-UPDATE: public pin ${r.status}: ${t.slice(0, 200)}`))) 77 + .catch(() => {}); // Best-effort, don't block pipeline 78 + } 79 + 58 80 async function uploadJsonToIPFS(data, name, timeoutMs = 30000) { 59 81 const content = JSON.stringify(data); 60 82 const formData = new FormData(); ··· 71 93 if (!res.ok) throw new Error(`Metadata upload failed: ${res.status}`); 72 94 const result = await res.json(); 73 95 seedToSecondaryNode(result.Hash); 96 + pinToPublicService(result.Hash, name); 74 97 return formatIpfsUri(result.Hash); 75 98 } catch (err) { 76 99 clearTimeout(timeout);