Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

admin-rebake: endpoint + kidlisp-cli/rebake.mjs to refresh oven media

POST /api/admin-rebake fires keep-prepare-background in rebake mode for
any KidLisp piece (minted or not), bypassing the wallet/Tezos checks the
keep-mint flow imposes. Resulting artifactUri/thumbnailUri land in
kidlisp.<code>.pendingRebake. Companion CLI is kidlisp-cli/rebake.mjs;
README documents both it and the existing regenerate-media.mjs.

Use this to recover from broken oven captures (e.g. all-black 528-byte
WebPs from a Puppeteer flake) without walking the keep-mint UI.

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

+367 -5
+67 -5
kidlisp-cli/README.md
··· 2 2 3 3 Home for the public `kidlisp` CLI implementation and distribution specs. 4 4 5 - Initial docs: 6 - - `INSTALLER-SPEC.md` - Website one-liner installer contract (`curl | sh`, PowerShell). 7 - - `RELEASE-SPEC.md` - Release asset names, checksums, channels, and update model. 5 + ## Specs (planning) 6 + 7 + - `INSTALLER-SPEC.md` — Website one-liner installer contract (`curl | sh`, PowerShell). 8 + - `RELEASE-SPEC.md` — Release asset names, checksums, channels, and update model. 8 9 9 - Status: 10 - - Spec-first planning directory (implementation to follow). 10 + ## Tools 11 + 12 + ### `rebake.mjs` — Refresh a piece's oven artifact + thumbnail 13 + 14 + Admin-only. Re-runs the oven bake (HTML bundle + WebP thumbnail) for any 15 + KidLisp piece — minted or not — and writes the new IPFS URIs to 16 + `kidlisp.<code>.pendingRebake` (or `ipfsMedia` on first bake). Use this 17 + when an oven thumbnail came back broken (e.g. an all-black 528-byte WebP 18 + from a Puppeteer flake) and you want a fresh pin without walking the 19 + keep-mint UI. 20 + 21 + ```bash 22 + node kidlisp-cli/rebake.mjs $2un 23 + node kidlisp-cli/rebake.mjs $2un --api https://localhost:8888 24 + ``` 25 + 26 + Auth: requires an admin AC login. The CLI reads `~/.ac-token` and 27 + auto-launches `tezos/ac-login.mjs` if the token is missing or expired. 28 + 29 + Backed by `POST /api/admin-rebake` (`system/netlify/functions/admin-rebake.mjs`). 30 + Direct API call: 31 + 32 + ```bash 33 + curl -X POST "https://aesthetic.computer/api/admin-rebake" \ 34 + -H "Authorization: Bearer $(jq -r .access_token < ~/.ac-token)" \ 35 + -H "Content-Type: application/json" \ 36 + -d '{"piece":"$2un"}' 37 + # → { jobId, status: "preparing", statusUrl: "/api/keep-status?jobId=…" } 38 + 39 + curl "https://aesthetic.computer/api/keep-status?jobId=<jobId>" 40 + ``` 41 + 42 + For minted-token regen with on-chain `edit_metadata` (full chain update), 43 + use `regenerate-media.mjs` instead. 44 + 45 + ### `regenerate-media.mjs` — Rebake + on-chain update for minted Keeps 46 + 47 + Looks up minted tokens on TzKT, rebakes media via `/api/keep-prepare`, 48 + then signs and broadcasts `edit_metadata` locally (Taquito). Requires both 49 + an AC login and a Tezos signing key. 50 + 51 + ```bash 52 + # Single piece (looks up tokenId/minter on TzKT first) 53 + node kidlisp-cli/regenerate-media.mjs $cow 54 + 55 + # All tokens on the active keeps contract 56 + node kidlisp-cli/regenerate-media.mjs --all 57 + 58 + # Dry run, or rebake-only (no chain tx) 59 + node kidlisp-cli/regenerate-media.mjs $cow --dry-run 60 + node kidlisp-cli/regenerate-media.mjs $cow --skip-update 61 + 62 + # Filter --all to one minter 63 + node kidlisp-cli/regenerate-media.mjs --all --creator tz1... 64 + ``` 65 + 66 + Flags: `--dry-run`, `--skip-update`, `--api URL`, `--contract KT1...`, 67 + `--creator tz1...`, `--delay MS`, `--tezos-key <sk...>`. Tezos key sources 68 + checked in order: `--tezos-key`, `$TEZOS_PRIVATE_KEY`, `~/.ac-tezos-key`. 69 + 70 + ## Status 71 + 72 + Spec-first planning directory; the tools above are working today.
+169
kidlisp-cli/rebake.mjs
··· 1 + #!/usr/bin/env node 2 + // rebake.mjs — Admin-driven piece media rebake (no Tezos required). 3 + // 4 + // Usage: node kidlisp-cli/rebake.mjs $2un 5 + // node kidlisp-cli/rebake.mjs $2un --api https://localhost:8888 6 + // 7 + // Calls /api/admin-rebake (admin only) to refresh the oven-baked 8 + // artifact + thumbnail for a piece — works for both minted tokens 9 + // and unminted pieces. Polls /api/keep-status until the job ends 10 + // and prints the resulting IPFS URIs. 11 + // 12 + // Use this when an oven thumbnail or bundle came back broken 13 + // (e.g. the all-black 528-byte WebP from a Puppeteer flake) and 14 + // you want a fresh pin without walking the keep-mint UI. 15 + // 16 + // For minted-token full regen (rebake + on-chain edit_metadata), 17 + // use regenerate-media.mjs instead. 18 + 19 + import fs from "fs"; 20 + import path from "path"; 21 + import os from "os"; 22 + import { execFileSync } from "child_process"; 23 + import { fileURLToPath } from "url"; 24 + 25 + const __dirname = path.dirname(fileURLToPath(import.meta.url)); 26 + const AC_LOGIN = path.join(__dirname, "..", "tezos", "ac-login.mjs"); 27 + 28 + const args = process.argv.slice(2); 29 + function opt(name, fallback) { 30 + const i = args.indexOf(`--${name}`); 31 + return i >= 0 && args[i + 1] ? args[i + 1] : fallback; 32 + } 33 + const API_BASE = opt("api", "https://aesthetic.computer").replace(/\/$/, ""); 34 + 35 + const flagsWithValues = new Set(["api"]); 36 + const pieces = []; 37 + for (let i = 0; i < args.length; i++) { 38 + const a = args[i]; 39 + if (a.startsWith("--")) { 40 + if (flagsWithValues.has(a.slice(2))) i++; 41 + continue; 42 + } 43 + pieces.push(a); 44 + } 45 + 46 + if (pieces.length === 0) { 47 + console.log(` 48 + rebake — Admin rebake of a KidLisp piece's oven media (artifact + thumbnail) 49 + 50 + Usage: 51 + node kidlisp-cli/rebake.mjs \\$2un 52 + node kidlisp-cli/rebake.mjs \\$2un --api https://localhost:8888 53 + 54 + Auth: requires an admin AC login (~/.ac-token, refreshed by ac-login). 55 + `); 56 + process.exit(0); 57 + } 58 + 59 + if (API_BASE.includes("localhost")) { 60 + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; 61 + } 62 + 63 + // ── Auth ── 64 + const authPath = path.join(os.homedir(), ".ac-token"); 65 + function readToken() { 66 + if (!fs.existsSync(authPath)) return null; 67 + try { 68 + const data = JSON.parse(fs.readFileSync(authPath, "utf8")); 69 + if (data.expires_at && Date.now() > data.expires_at) return null; 70 + return data; 71 + } catch { 72 + return null; 73 + } 74 + } 75 + 76 + let auth = readToken(); 77 + if (!auth) { 78 + console.log("\n🔐 Not logged in — launching ac-login...\n"); 79 + try { 80 + execFileSync("node", [AC_LOGIN], { stdio: "inherit" }); 81 + } catch { 82 + console.error("❌ Login failed."); 83 + process.exit(1); 84 + } 85 + auth = readToken(); 86 + if (!auth) { 87 + console.error("❌ Still no valid token."); 88 + process.exit(1); 89 + } 90 + } 91 + 92 + const TOKEN = auth.access_token; 93 + const WHO = auth.user?.handle ? `@${auth.user.handle}` : auth.user?.email || "unknown"; 94 + 95 + const POLL_INTERVAL_MS = 2500; 96 + const POLL_TIMEOUT_MS = 5 * 60 * 1000; 97 + 98 + async function rebakeOne(pieceArg) { 99 + const piece = pieceArg.replace(/^\$/, ""); 100 + console.log(`\n${"─".repeat(60)}\n🎨 $${piece} (admin: ${WHO})`); 101 + 102 + const startRes = await fetch(`${API_BASE}/api/admin-rebake`, { 103 + method: "POST", 104 + headers: { 105 + "Content-Type": "application/json", 106 + Authorization: `Bearer ${TOKEN}`, 107 + }, 108 + body: JSON.stringify({ piece: `$${piece}` }), 109 + }); 110 + 111 + const text = await startRes.text(); 112 + let started; 113 + try { started = JSON.parse(text); } catch { started = { error: text.slice(0, 300) }; } 114 + 115 + if (!startRes.ok) { 116 + console.error(`❌ admin-rebake HTTP ${startRes.status}: ${started.error || text}`); 117 + return { piece, success: false }; 118 + } 119 + 120 + const jobId = started.jobId; 121 + if (!jobId) { 122 + console.error(`❌ no jobId in response: ${JSON.stringify(started).slice(0, 200)}`); 123 + return { piece, success: false }; 124 + } 125 + console.log(` job ${jobId} → polling ${API_BASE}/api/keep-status`); 126 + 127 + const start = Date.now(); 128 + let lastLine = ""; 129 + while (Date.now() - start < POLL_TIMEOUT_MS) { 130 + await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS)); 131 + let job; 132 + try { 133 + const r = await fetch(`${API_BASE}/api/keep-status?jobId=${encodeURIComponent(jobId)}`); 134 + if (!r.ok) continue; 135 + job = await r.json(); 136 + } catch { continue; } 137 + 138 + const elapsed = ((Date.now() - start) / 1000).toFixed(0); 139 + const line = `[${elapsed}s] ${job.status}: ${job.stage || "?"} — ${job.message || job.stageMessage || ""}`; 140 + if (line !== lastLine) { 141 + process.stdout.write(`\r ${line.padEnd(78)}`); 142 + lastLine = line; 143 + } 144 + 145 + if (job.status === "ready") { 146 + process.stdout.write("\n"); 147 + console.log(` ✅ ready`); 148 + if (job.artifactUri) console.log(` artifact: ${job.artifactUri}`); 149 + if (job.thumbnailUri) console.log(` thumbnail: ${job.thumbnailUri}`); 150 + return { piece, success: true, ...job }; 151 + } 152 + if (job.status === "failed") { 153 + process.stdout.write("\n"); 154 + console.error(` ❌ failed at ${job.errorStage || job.stage}: ${job.error || "unknown"}`); 155 + return { piece, success: false }; 156 + } 157 + } 158 + 159 + process.stdout.write("\n"); 160 + console.error(` ⏱ timed out after ${POLL_TIMEOUT_MS / 1000}s (job may still be running)`); 161 + return { piece, success: false }; 162 + } 163 + 164 + let failed = 0; 165 + for (const p of pieces) { 166 + const r = await rebakeOne(p); 167 + if (!r.success) failed++; 168 + } 169 + process.exit(failed > 0 ? 1 : 0);
+131
system/netlify/functions/admin-rebake.mjs
··· 1 + // admin-rebake.mjs — Admin-only piece media rebake endpoint. 2 + // 3 + // POST /api/admin-rebake 4 + // Body: { piece } 5 + // Auth: AC Bearer token (must be admin) 6 + // Returns: { jobId, status, statusUrl } 7 + // 8 + // Triggers the keep-prepare-background pipeline in rebake mode for any 9 + // piece — minted or not. Skips wallet/Tezos checks since admin is the 10 + // caller. Use this to refresh broken oven artifacts (bundle/thumbnail) 11 + // without walking the keep-mint UI. Poll /api/keep-status?jobId=… for 12 + // progress; the resulting artifactUri/thumbnailUri land in 13 + // kidlisp.<code>.pendingRebake (and ipfsMedia for first-bake pieces). 14 + // 15 + // Companion CLI: kidlisp-cli/rebake.mjs 16 + 17 + import { authorize, hasAdmin, handleFor } from "../../backend/authorization.mjs"; 18 + import { connect } from "../../backend/database.mjs"; 19 + import { loadKidlispPiece } from "../../backend/kidlisp-read.mjs"; 20 + import { upsertJob, formatJobForClient, getJob } from "../../backend/keep-job.mjs"; 21 + 22 + const dev = process.env.CONTEXT === "dev"; 23 + if (dev) process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; 24 + 25 + const CORS_HEADERS = { 26 + "Access-Control-Allow-Origin": "*", 27 + "Access-Control-Allow-Headers": "Content-Type, Authorization", 28 + "Content-Type": "application/json", 29 + }; 30 + 31 + function jsonResponse(statusCode, body) { 32 + return { statusCode, headers: CORS_HEADERS, body: JSON.stringify(body) }; 33 + } 34 + 35 + export const handler = async (event) => { 36 + if (event.httpMethod === "OPTIONS") { 37 + return { statusCode: 200, headers: CORS_HEADERS, body: "" }; 38 + } 39 + if (event.httpMethod !== "POST") { 40 + return jsonResponse(405, { error: "Method not allowed" }); 41 + } 42 + 43 + let body; 44 + try { 45 + body = JSON.parse(event.body || "{}"); 46 + } catch { 47 + return jsonResponse(400, { error: "Invalid JSON body" }); 48 + } 49 + 50 + const pieceName = body.piece?.replace(/^\$/, "").trim(); 51 + if (!pieceName) return jsonResponse(400, { error: "Missing 'piece'" }); 52 + 53 + const user = await authorize(event.headers); 54 + if (!user) return jsonResponse(401, { error: "Unauthorized" }); 55 + if (!(await hasAdmin(user))) return jsonResponse(403, { error: "Admin only" }); 56 + 57 + const database = await connect(); 58 + const piece = await loadKidlispPiece(database, pieceName); 59 + if (!piece) return jsonResponse(404, { error: `Piece '$${pieceName}' not found` }); 60 + 61 + const usersCol = database.db.collection("users"); 62 + const userDoc = await usersCol.findOne({ _id: user.sub }); 63 + // keep-job uniqueness key is (piece, wallet); use the admin's linked 64 + // wallet when available so jobs are scoped per-admin, else fall back 65 + // to a sentinel that's clearly an admin rebake. 66 + const wallet = userDoc?.tezos?.address || `admin:${user.sub}`; 67 + const userHandle = await handleFor(user.sub); 68 + 69 + // Clear any prior in-flight job so the rebake always runs fresh. 70 + const existingJob = await getJob(pieceName, wallet); 71 + if (existingJob) { 72 + try { 73 + await database.db 74 + .collection("keep-jobs") 75 + .deleteOne({ _id: existingJob._id }); 76 + } catch (err) { 77 + console.warn("admin-rebake: failed to clear stale job:", err.message); 78 + } 79 + } 80 + 81 + const job = await upsertJob({ 82 + piece: pieceName, 83 + wallet, 84 + user: user.sub, 85 + handle: userHandle, 86 + isRebake: true, 87 + regenerate: true, 88 + }); 89 + 90 + const siteUrl = 91 + process.env.URL || 92 + process.env.DEPLOY_URL || 93 + (dev ? "http://localhost:8888" : "https://aesthetic.computer"); 94 + 95 + const bgPayload = { 96 + jobId: job._id.toString(), 97 + pieceName, 98 + isRebake: true, 99 + regenerate: true, 100 + creatorWalletAddress: wallet, 101 + userHandle, 102 + walletAddress: wallet, 103 + }; 104 + 105 + try { 106 + const bgRes = await fetch(`${siteUrl}/.netlify/functions/keep-prepare-background`, { 107 + method: "POST", 108 + headers: { "Content-Type": "application/json" }, 109 + body: JSON.stringify(bgPayload), 110 + }); 111 + const bgOk = bgRes.status === 202 || bgRes.ok; 112 + if (!bgOk) { 113 + console.error(`admin-rebake: background HTTP ${bgRes.status}`); 114 + return jsonResponse(502, { 115 + error: `Background pipeline failed to launch (HTTP ${bgRes.status})`, 116 + jobId: job._id.toString(), 117 + }); 118 + } 119 + } catch (err) { 120 + console.error("admin-rebake: background unreachable:", err.message); 121 + return jsonResponse(502, { 122 + error: `Background pipeline unreachable: ${err.message}`, 123 + jobId: job._id.toString(), 124 + }); 125 + } 126 + 127 + return jsonResponse(200, { 128 + ...formatJobForClient(job), 129 + statusUrl: `/api/keep-status?jobId=${job._id.toString()}`, 130 + }); 131 + };