Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

keep.kidlisp.com: fix rekeep flow end-to-end

- lith/Caddyfile: dedupe Access-Control-Allow-Origin on ipfs.aesthetic.computer
so browsers stop rejecting the gateway with "*, *". Kubo already emits the
header; strip upstream copies and let Caddy own exactly one.
- keep-update.mjs: drop Pinata (403s) in favor of self-hosted Kubo with oven
seeder mirroring, matching keep-prepare-background.mjs.
- keeps.html: await initAuth0 in startMintFlow before deciding on the Bearer
header, fixing the first-click race that produced a spurious 401 on the
validate-ownership step.

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

+50 -37
+6 -2
lith/Caddyfile
··· 133 133 } 134 134 135 135 # --- ipfs.aesthetic.computer (self-hosted IPFS gateway) --- 136 + # Kubo's gateway already emits Access-Control-Allow-Origin; strip upstream 137 + # copies so Caddy owns exactly one, avoiding "*, *" dupes that browsers reject. 136 138 @ipfs host ipfs.aesthetic.computer 137 139 handle @ipfs { 138 - header Access-Control-Allow-Origin * 139 - reverse_proxy localhost:8090 140 + reverse_proxy localhost:8090 { 141 + header_down -Access-Control-Allow-Origin 142 + header_down Access-Control-Allow-Origin * 143 + } 140 144 } 141 145 142 146 # --- justanothersystem.org ---
+37 -34
system/netlify/functions/keep-update.mjs
··· 36 36 return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; 37 37 } 38 38 39 - // Get Pinata credentials from database 40 - async function getPinataCredentials() { 41 - const { db } = await connect(); 42 - const secrets = await db.collection("secrets").findOne({ _id: "pinata" }); 43 - 44 - if (!secrets) { 45 - throw new Error("Pinata credentials not found in database"); 46 - } 47 - 48 - return { 49 - apiKey: secrets.apiKey, 50 - apiSecret: secrets.apiSecret, 51 - }; 39 + // ─── IPFS Upload (self-hosted Kubo node on lith + oven seeder) ─────────────── 40 + // Matches keep-prepare-background.mjs so the on-chain sync path has the same 41 + // storage+mirroring guarantees as the prepare pipeline (no Pinata dependency). 42 + const IPFS_API = process.env.IPFS_API_URL || "http://localhost:5001"; 43 + const IPFS_SEEDER_URL = process.env.IPFS_SEEDER_URL || "http://137.184.237.166:5001"; 44 + const USE_GATEWAY_URLS = process.env.USE_IPFS_GATEWAY_URLS === "true"; 45 + const IPFS_GATEWAY = process.env.IPFS_GATEWAY || "https://ipfs.aesthetic.computer"; 46 + 47 + function formatIpfsUri(hash) { 48 + return USE_GATEWAY_URLS ? `${IPFS_GATEWAY}/ipfs/${hash}` : `ipfs://${hash}`; 52 49 } 53 50 54 - // Upload JSON metadata to IPFS via Pinata 55 - async function uploadJsonToIPFS(data, name) { 56 - const { apiKey, apiSecret } = await getPinataCredentials(); 57 - 58 - const response = await fetch("https://api.pinata.cloud/pinning/pinJSONToIPFS", { 59 - method: "POST", 60 - headers: { 61 - "Content-Type": "application/json", 62 - pinata_api_key: apiKey, 63 - pinata_secret_api_key: apiSecret, 64 - }, 65 - body: JSON.stringify({ 66 - pinataContent: data, 67 - pinataMetadata: { name }, 68 - }), 69 - }); 51 + // Seed content to the oven IPFS node (fire-and-forget for faster gateway propagation) 52 + function seedToSecondaryNode(hash) { 53 + fetch(`${IPFS_SEEDER_URL}/api/v0/pin/add?arg=${hash}`, { method: "POST", signal: AbortSignal.timeout(120000) }) 54 + .then(r => r.ok ? console.log(`🌱 KEEP-UPDATE: seeded ${hash.slice(0, 12)}... to oven`) : null) 55 + .catch(() => {}); // Best-effort, don't block pipeline 56 + } 70 57 71 - if (!response.ok) { 72 - throw new Error(`Metadata upload failed: ${response.status}`); 58 + async function uploadJsonToIPFS(data, name, timeoutMs = 30000) { 59 + const content = JSON.stringify(data); 60 + const formData = new FormData(); 61 + formData.append("file", new Blob([content], { type: "application/json" }), name); 62 + const controller = new AbortController(); 63 + const timeout = setTimeout(() => controller.abort(), Math.max(3000, timeoutMs)); 64 + try { 65 + const res = await fetch(`${IPFS_API}/api/v0/add?pin=true`, { 66 + method: "POST", 67 + body: formData, 68 + signal: controller.signal, 69 + }); 70 + clearTimeout(timeout); 71 + if (!res.ok) throw new Error(`Metadata upload failed: ${res.status}`); 72 + const result = await res.json(); 73 + seedToSecondaryNode(result.Hash); 74 + return formatIpfsUri(result.Hash); 75 + } catch (err) { 76 + clearTimeout(timeout); 77 + if (err.name === "AbortError") throw new Error(`Metadata upload timed out after ${Math.round(timeoutMs / 1000)}s`); 78 + throw err; 73 79 } 74 - 75 - const result = await response.json(); 76 - return `ipfs://${result.IpfsHash}`; 77 80 } 78 81 79 82 async function getTezosCredentials() {
+7 -1
system/public/kidlisp.com/keeps.html
··· 6038 6038 setMintStep('wallet', 'done', address.slice(0, 8) + '...' + address.slice(-4)); 6039 6039 addTrackEntry('Wallet connected: ' + address.slice(0, 8) + '...' + address.slice(-4)); 6040 6040 6041 - // Refresh token 6041 + // Refresh token — ensure Auth0 has finished initializing before we 6042 + // decide whether to send the Authorization header. Without this guard, 6043 + // a fast first click can race initAuth0() and POST keep-prepare without 6044 + // auth, producing a spurious 401 "Please log in first" on first open. 6045 + if (!acAuth0Client) { 6046 + try { await initAuth0(); } catch {} 6047 + } 6042 6048 if (acAuth0Client) { 6043 6049 try { acToken = await acAuth0Client.getTokenSilently(); } catch {} 6044 6050 }