diff --git a/lith/Caddyfile b/lith/Caddyfile index 2f0e910ce2..aa776408e5 100644 --- a/lith/Caddyfile +++ b/lith/Caddyfile @@ -133,10 +133,14 @@ } # --- ipfs.aesthetic.computer (self-hosted IPFS gateway) --- + # Kubo's gateway already emits Access-Control-Allow-Origin; strip upstream + # copies so Caddy owns exactly one, avoiding "*, *" dupes that browsers reject. @ipfs host ipfs.aesthetic.computer handle @ipfs { - header Access-Control-Allow-Origin * - reverse_proxy localhost:8090 + reverse_proxy localhost:8090 { + header_down -Access-Control-Allow-Origin + header_down Access-Control-Allow-Origin * + } } # --- justanothersystem.org --- diff --git a/system/netlify/functions/keep-update.mjs b/system/netlify/functions/keep-update.mjs index 0e2bbe709b..ec344b6db2 100644 --- a/system/netlify/functions/keep-update.mjs +++ b/system/netlify/functions/keep-update.mjs @@ -36,44 +36,47 @@ function sse(event, data) { return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; } -// Get Pinata credentials from database -async function getPinataCredentials() { - const { db } = await connect(); - const secrets = await db.collection("secrets").findOne({ _id: "pinata" }); - - if (!secrets) { - throw new Error("Pinata credentials not found in database"); - } - - return { - apiKey: secrets.apiKey, - apiSecret: secrets.apiSecret, - }; +// ─── IPFS Upload (self-hosted Kubo node on lith + oven seeder) ─────────────── +// Matches keep-prepare-background.mjs so the on-chain sync path has the same +// storage+mirroring guarantees as the prepare pipeline (no Pinata dependency). +const IPFS_API = process.env.IPFS_API_URL || "http://localhost:5001"; +const IPFS_SEEDER_URL = process.env.IPFS_SEEDER_URL || "http://137.184.237.166:5001"; +const USE_GATEWAY_URLS = process.env.USE_IPFS_GATEWAY_URLS === "true"; +const IPFS_GATEWAY = process.env.IPFS_GATEWAY || "https://ipfs.aesthetic.computer"; + +function formatIpfsUri(hash) { + return USE_GATEWAY_URLS ? `${IPFS_GATEWAY}/ipfs/${hash}` : `ipfs://${hash}`; } -// Upload JSON metadata to IPFS via Pinata -async function uploadJsonToIPFS(data, name) { - const { apiKey, apiSecret } = await getPinataCredentials(); - - const response = await fetch("https://api.pinata.cloud/pinning/pinJSONToIPFS", { - method: "POST", - headers: { - "Content-Type": "application/json", - pinata_api_key: apiKey, - pinata_secret_api_key: apiSecret, - }, - body: JSON.stringify({ - pinataContent: data, - pinataMetadata: { name }, - }), - }); - - if (!response.ok) { - throw new Error(`Metadata upload failed: ${response.status}`); - } +// Seed content to the oven IPFS node (fire-and-forget for faster gateway propagation) +function seedToSecondaryNode(hash) { + fetch(`${IPFS_SEEDER_URL}/api/v0/pin/add?arg=${hash}`, { method: "POST", signal: AbortSignal.timeout(120000) }) + .then(r => r.ok ? console.log(`🌱 KEEP-UPDATE: seeded ${hash.slice(0, 12)}... to oven`) : null) + .catch(() => {}); // Best-effort, don't block pipeline +} - const result = await response.json(); - return `ipfs://${result.IpfsHash}`; +async function uploadJsonToIPFS(data, name, timeoutMs = 30000) { + const content = JSON.stringify(data); + 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(`${IPFS_API}/api/v0/add?pin=true`, { + method: "POST", + body: formData, + signal: controller.signal, + }); + clearTimeout(timeout); + if (!res.ok) throw new Error(`Metadata upload failed: ${res.status}`); + const result = await res.json(); + seedToSecondaryNode(result.Hash); + 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`); + throw err; + } } async function getTezosCredentials() { diff --git a/system/public/kidlisp.com/keeps.html b/system/public/kidlisp.com/keeps.html index 177686f7e5..327343431e 100644 --- a/system/public/kidlisp.com/keeps.html +++ b/system/public/kidlisp.com/keeps.html @@ -6038,7 +6038,13 @@ setMintStep('wallet', 'done', address.slice(0, 8) + '...' + address.slice(-4)); addTrackEntry('Wallet connected: ' + address.slice(0, 8) + '...' + address.slice(-4)); - // Refresh token + // Refresh token — ensure Auth0 has finished initializing before we + // decide whether to send the Authorization header. Without this guard, + // a fast first click can race initAuth0() and POST keep-prepare without + // auth, producing a spurious 401 "Please log in first" on first open. + if (!acAuth0Client) { + try { await initAuth0(); } catch {} + } if (acAuth0Client) { try { acToken = await acAuth0Client.getTokenSilently(); } catch {} }