Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392# --- zero-downtime deploys ---# lith restarts leave :8888 dark for a few seconds (161 function imports# before listen). Instead of failing the first dial (502 at Cloudflare),# hold and retry for up to 30s so requests queue through the restart.(lith_proxy) { reverse_proxy localhost:8888 { lb_try_duration 30s lb_try_interval 250ms }}# lith production Caddyfile — full subdomain routing# Cloudflare handles TLS. Caddy serves HTTP on :80.# --- papers.aesthetic.computer ---# (touched 2026-04-29 to re-fire webhook after the install-Caddyfile-before-reload fix):443 { tls /etc/caddy/origin-cert.pem /etc/caddy/origin-key.pem log { output file /var/log/caddy/access.log { roll_size 50MiB roll_keep 3 roll_keep_for 72h } format json level INFO } # --- Global performance headers --- # Code modules change every deploy — short TTL so rollouts are visible fast. # (lib/disk .mjs are STATIC sub-imports without the ?v= cache-bust boot.mjs # applies to top-level modules, so a long TTL pins clients to stale runtime # code. The deploy script also purges the CDN; this bounds browser-side # staleness to ~1min.) @code path *.mjs *.js *.css *.lisp *.lua header @code Cache-Control "public, max-age=60, stale-while-revalidate=300" # `?` = set only if absent. The upstream Express app (server.mjs) already # sends Access-Control-Allow-Origin on proxied responses; adding a second # copy here produced "*, *", which browsers reject (the same bug the IPFS # block below hit). Set-if-absent yields exactly one header for both proxied # and Caddy-served responses. header @code ?Access-Control-Allow-Origin * # Content-hashed worker bundles are the exception to the short TTL: the # hash in the filename IS the version, and bios.mjs reads the current one # from disk-worker-manifest.json (fetched no-cache, so always revalidated). # A given filename never changes content, so browsers and the service # worker may keep it for a year — a new build means a new name, never a # stale hit. Must come after the @code rule so it wins for these paths. @immutable path_regexp ^/aesthetic\.computer/lib/disk\.worker\.[a-f0-9]+\.mjs$ header @immutable Cache-Control "public, max-age=31536000, immutable" # The two modules that must ship updates within seconds. `no-cache` means # store-but-always-revalidate: the browser (and the service worker's # network-first fetch) sends If-None-Match and gets a 304 when nothing # changed, instead of the 60s/300s stale window above or — what boot.mjs # used to do to get around that window — a `?v=<now>` bust that # re-downloaded bios.mjs on every boot. Must come after @code to win. @core path /aesthetic.computer/bios.mjs /aesthetic.computer/lib/disk.mjs header @core Cache-Control "no-cache" # Static assets: long cache (1h fresh, serve stale for 24h while revalidating) @cacheable path *.woff2 *.woff *.ttf *.png *.jpg *.jpeg *.svg *.gif *.webp *.ico *.mp3 *.wav *.mp4 *.json header @cacheable Cache-Control "public, max-age=3600, stale-while-revalidate=86400" header @cacheable ?Access-Control-Allow-Origin * # set-if-absent — see @code note # These are database-backed resolvers, not static object URLs. In # particular, do not cache the brief 404 window while a new #code and its # object-store upload converge. The redirect target remains cacheable. @mediaLookup path /media/paintings/* /media/tapes/* header @mediaLookup Cache-Control "no-cache, no-store, must-revalidate" # Service workers must always revalidate — cached sw.js delays rollout of # bumped CACHE_NAME, pinning clients to stale module caches. @serviceworker path /sw.js /firebase-messaging-sw.js header @serviceworker Cache-Control "no-cache, no-store, must-revalidate" # HTML: no-cache (always revalidate, but use ETag for 304) # `?` = set only if absent — proxied apps that send their own # Cache-Control (e.g. the sotce-net shell) were getting both headers. @html path / *.html header @html ?Cache-Control "no-cache" # Encode everything (Caddy does this by default, but be explicit) encode zstd gzip @nft host nft.aesthetic.computer handle @nft { root * /opt/ac/system/public/nft.aesthetic.computer file_server } @rdpmeta host rdp.aesthetic.computer handle @rdpmeta { root * /opt/ac/system/public/rdp.aesthetic.computer file_server } @papers host papers.aesthetic.computer papers.prompt.ac handle @papers { handle /api/mediascholar-status { import lith_proxy } handle /en { rewrite * /index.html?lang=en root * /opt/ac/system/public/papers.aesthetic.computer file_server } handle /da { rewrite * /index.html?lang=da root * /opt/ac/system/public/papers.aesthetic.computer file_server } handle /es { rewrite * /index.html?lang=es root * /opt/ac/system/public/papers.aesthetic.computer file_server } handle /cn { rewrite * /index.html?lang=zh root * /opt/ac/system/public/papers.aesthetic.computer file_server } root * /opt/ac/system/public/papers.aesthetic.computer # /platter is the existing white research-records page (platter.html). # We briefly created /platter/ as a directory with our own index — that # was the wrong call; restored below by deleting the dir's index and # reversing the prior 301 with a 302 to invalidate cached redirects. # /platter/jeffrey/ remains a real directory (the dashboard); its # explicit canonical redirect stays so the page's relative # `./manifest.json` fetch resolves correctly. # (See: 2026-04-29 platter restoration.) redir /platter/ /platter 302 redir /platter/jeffrey /platter/jeffrey/ permanent # SPA fallback for unknown paths: if none of {path}, {path}.html, # {path}/index.html, or {path}index.html exists on disk, serve # /index.html — but mark it no-cache. Without this, a request for a # not-yet-deployed PDF gets the SPA HTML, and Cloudflare pins that # HTML to the PDF URL for 4h via its default static-asset cache. # (See: 2026-04-26 latency-paper deploy gap.) The two index.html # terms cover both /platter (no slash → {path}/index.html) and # /platter/ (slash → {path}index.html). (See: 2026-04-29 # jeffrey-platter URL move.) @missing not file { try_files {path} {path}.html {path}/index.html {path}index.html } handle @missing { header Cache-Control "no-cache, must-revalidate, max-age=0" rewrite * /index.html file_server } # NOTE: {path} is intentionally absent from the top-level try_files. # Caddy's try_files matches directories as well as files, so including # {path} would silently match a request for /platter (the directory), # short-circuit before {path}/index.html, then file_server would serve # the wrong content rather than canonicalizing to /platter/. Without # {path} here, file_server handles existing regular files itself and # issues a 301 redirect for directory-without-slash to the canonical # /<dir>/ form. (See: 2026-04-29 platter URL move.) try_files {path}.html {path}/index.html {path}index.html file_server } # --- pop.aesthetic.computer --- @pop host pop.aesthetic.computer handle @pop { root * /opt/ac/system/public/pop.aesthetic.computer @release path_regexp release ^/releases/([a-z0-9-]+)$ handle @release { rewrite * /releases/{re.release.1}.json file_server } handle { try_files {path} {path}.html {path}/index.html /index.html file_server } } # --- bills.aesthetic.computer --- @bills host bills.aesthetic.computer handle @bills { root * /opt/ac/system/public/bills.aesthetic.computer try_files {path} {path}.html /index.html file_server } # --- nela.aesthetic.computer (NELA Computer Club donate page) --- @nela host nela.aesthetic.computer handle @nela { handle /api/* { import lith_proxy } # Static fallback wrapped in `handle {}` — bare try_files reorders # ahead of `handle` and eats /api/* (see the give block above). handle { root * /opt/ac/system/public/nela.aesthetic.computer try_files {path} {path}.html /index.html file_server } } # --- give.aesthetic.computer --- @give host give.aesthetic.computer handle @give { handle /api/* { import lith_proxy } handle /da { rewrite * /index.html?lang=da¤cy=dkk root * /opt/ac/system/public/give.aesthetic.computer file_server } handle /es { rewrite * /index.html?lang=es¤cy=usd root * /opt/ac/system/public/give.aesthetic.computer file_server } handle /de { rewrite * /index.html?lang=de¤cy=eur root * /opt/ac/system/public/give.aesthetic.computer file_server } handle /cn { rewrite * /index.html?lang=zh¤cy=usd root * /opt/ac/system/public/give.aesthetic.computer file_server } # Static fallback MUST be wrapped in `handle {}` so it's mutually # exclusive with `handle /api/*`. Bare `try_files`/`file_server` is # reordered ahead of `handle` by Caddy's default directive order, which # rewrites /api/* POSTs to /index.html → file_server 405s them (this # broke the give-portal cancel button). See news/feed blocks below. handle { root * /opt/ac/system/public/give.aesthetic.computer try_files {path} {path}.html /index.html file_server } } # --- whistlegraph.org (the index of the artform) --- # Live index of the artform; /<code> (e.g. /imab) falls back to index.html, # whose client router deep-links that piece. commands.json is the canonical # prompt feed consumed by AC, prompt.ac, and ac-native. @whistlegraph host whistlegraph.org www.whistlegraph.org handle @whistlegraph { root * /opt/ac/system/public/whistlegraph.org # Auth0-gated Whistlegraph Desk API. Keep the rest of aesthetic.com's API # namespace off this host; only this narrowly-scoped function crosses # from the static Whistlegraph site into lith. handle /api/whistlegraph-admin* { import lith_proxy } # Public, indexed read projection used by the Desk's paginated index. handle /api/whistlegraph-query* { import lith_proxy } # Free license verification. It comes first and keeps the original query # instead of rebuilding it, because the receipt being checked travels in # that query — the rewrite below would throw it away. @wgverify path /api/wg/verify /api/wg/verify/ handle @wgverify { rewrite * /api/whistlegraph-llm?resource=verify&{query} import lith_proxy } # Paid machine access, metered with x402. /api/wg/<resource>[/<code>] # flattens onto the one function; an unpaid GET returns 402 with the # terms, so asking always works. See whistlegraph-llm.mjs. @wgapi path_regexp wgapi ^/api/wg/([a-z]+)(?:/([A-Za-z0-9]+))?/?$ handle @wgapi { rewrite * /api/whistlegraph-llm?resource={re.wgapi.1}&code={re.wgapi.2} import lith_proxy } # The machine-readable index. llms.txt is the map an agent reads first; # index.md is the whole archive as Markdown. Both are generated by # toolchain/whistlegraph/gen-llms.mjs and are deliberately free — the # JSON they summarize is already public, so gating them would be theater. @wgllms path /llms.txt /robots.txt handle @wgllms { header Content-Type "text/plain; charset=utf-8" header Cache-Control "public, max-age=300, stale-while-revalidate=3600" header Access-Control-Allow-Origin "*" file_server } # /llms-full.txt is the same document under the name crawlers look for. @wgindexmd path /index.md /llms-full.txt handle @wgindexmd { rewrite * /index.md header Content-Type "text/markdown; charset=utf-8" header Cache-Control "public, max-age=300, stale-while-revalidate=3600" header Access-Control-Allow-Origin "*" file_server } # Auth0 returns to the bare origin. Only a root request carrying its # transaction state is served by the Desk; ordinary / remains the site. @wgcallback { path / query state=* } handle @wgcallback { rewrite * /admin.html header Cache-Control "no-store" file_server } # /desk is canonical. Keep the old /admin path as a quiet compatibility # alias for sessions begun before the callback moved to the bare origin. @wgdesk path /desk /desk/ /admin /admin/ /admin.html handle @wgdesk { rewrite * /admin.html header Cache-Control "no-store" file_server } @wgcommands path /api/commands /api/commands.json handle @wgcommands { rewrite * /commands.json header Cache-Control "public, max-age=60, stale-while-revalidate=300" header Access-Control-Allow-Origin "*" file_server } # Stable /post/<id> links get the same record-specific unfurl treatment. @wgpost path_regexp wgpost ^/post/([0-9]+)$ handle @wgpost { rewrite * /api/whistlegraph-og?id={re.wgpost.1} import lith_proxy } # /stars is the ORGSTARS sky. It has to claim its path here, above the # bare-code matcher, or that matcher reads "stars" as a whistlegraph code # and hands the request to the unfurl function instead of the page. @wgstars path /stars /stars/ handle @wgstars { rewrite * /stars.html header Cache-Control "public, max-age=60, stale-while-revalidate=300" file_server } # /comments is the back-of-house comment room — TikTok comment # screenshots getting sorted. Claimed here for the same reason as # /stars: the bare-code matcher would read "comments" as a code. @wgcomments path /comments /comments/ handle @wgcomments { rewrite * /comments.html header Cache-Control "public, max-age=60, stale-while-revalidate=300" file_server } # /wild is the field guide of third-party sightings — whistlegraph # content found loose on the open web. @wgwild path /wild /wild/ handle @wgwild { rewrite * /wild.html header Cache-Control "public, max-age=60, stale-while-revalidate=300" file_server } # Bare /<code> deep links (e.g. /imab, /clth) go to lith, which returns # index.html with THAT work's og:title / og:image / og:video injected — # so iMessage, Slack, Twitter, etc. unfurl the specific whistlegraph # instead of one generic card. Unknown codes fall back to the plain page. # Everything with a dot (index.html, *.json, *.jpg, favicon) skips this # matcher and is served straight off disk. @wgcode path_regexp wgcode ^/([A-Za-z0-9]+)$ handle @wgcode { rewrite * /api/whistlegraph-og?code={re.wgcode.1} import lith_proxy } handle { try_files {path} {path}.html /index.html file_server } } # --- tv.whistlegraph.org (the archive as broadcast) --- # Fullscreen back-to-back playback of the same curated index whistlegraph.org # serves: tv.html reads graphs.json / posts.json straight off this root and # the live curation overlay through the one narrowly-scoped function below. # Nothing else of the API namespace crosses onto this host. @wgtv host tv.whistlegraph.org handle @wgtv { root * /opt/ac/system/public/whistlegraph.org handle /api/whistlegraph-admin* { import lith_proxy } handle { try_files {path} /tv.html file_server } } # --- data.aesthetic.computer (Linked Open Data / CIDOC CRM) --- # Most paths go to lith, which rewrites them to the `crm` function (landing, # /@handle, /painting, /piece, /mood, /.well-known/void). /sparql is the one # exception: it goes straight to the Oxigraph SPARQL store (Stage 2). @data host data.aesthetic.computer handle @data { # Read-only SPARQL: rewrite to Oxigraph's /query endpoint. Its /update # and /store write endpoints are NEVER proxied — writes happen only # locally via the ETL (crm/build-graph.mjs). handle /sparql* { rewrite * /query reverse_proxy localhost:7878 } handle { import lith_proxy } } # --- news.aesthetic.computer --- @news host news.aesthetic.computer handle @news { handle /api/* { import lith_proxy } handle { rewrite * /api/news{uri} import lith_proxy } } # --- api.aesthetic.computer --- @apidomain host api.aesthetic.computer api.prompt.ac handle @apidomain { import lith_proxy } # --- feed.aesthetic.computer (DP-1 Feed V2 — Go + Postgres) --- @feed host feed.aesthetic.computer handle @feed { header Access-Control-Allow-Origin * handle /api/* { reverse_proxy localhost:8787 } handle /health { reverse_proxy localhost:8787 } handle { root * /opt/dp1-feed rewrite * /landing-page.html file_server } } # --- ipfs.aesthetic.computer (self-hosted IPFS gateway) --- # Kubo emits Access-Control-Allow-Origin: * on its own. An earlier Caddy # `header` directive was stacking a second copy, producing "*, *" that # browsers reject. Let Kubo own the header, Caddy just proxies. @ipfs host ipfs.aesthetic.computer handle @ipfs { reverse_proxy localhost:8090 } # --- justanothersystem.org --- @wwwjas2 host www.justanothersystem.org handle @wwwjas2 { redir https://justanothersystem.org{uri} 301 } @jas host justanothersystem.org handle @jas { handle /api/* { import lith_proxy } redir /bio /cv 301 redir /bio/ /cv 301 # Static fallback wrapped in handle{} so handle /api/* stays terminal — # bare try_files is reordered ahead of handle and 405s /api/* POSTs. handle { root * /opt/ac/system/public/justanothersystem.org try_files {path} {path}.html /index.html file_server } } # --- builds.false.work --- @builds_api { host builds.false.work path /api/* /.netlify/functions/* } handle @builds_api { import lith_proxy } @builds host builds.false.work handle @builds { root * /opt/ac/system/public/builds.false.work try_files {path} {path}.html /index.html file_server } # --- sotce.net --- # www → apex, 308 so a stray POST keeps its method. One canonical origin: # Auth0's callback/logout allowlists, the push service worker, and # localStorage all key on it. (Netlify used to do this; lith must.) @sotcewww host www.sotce.net handle @sotcewww { redir https://sotce.net{uri} 308 } @sotce host sotce.net handle @sotce { handle /api/* { import lith_proxy } handle /user { import lith_proxy } handle /handle { import lith_proxy } handle /authorized { import lith_proxy } handle /aesthetic.computer/* { uri strip_prefix /aesthetic.computer root * /opt/ac/system/public/aesthetic.computer file_server } # Everything else → sotce-net function handle { rewrite * /api/sotce-net{uri} import lith_proxy } } # --- kidlisp.com subdomains --- @keep host keep.kidlisp.com handle @keep { handle /api/* { import lith_proxy } handle /technology { root * /opt/ac/system/public/kidlisp.com rewrite * /keeps-tech.html file_server } handle /wallet { root * /opt/ac/system/public/kidlisp.com rewrite * /wallet/index.html file_server } handle { root * /opt/ac/system/public/kidlisp.com rewrite * /keeps.html file_server } } @buy host buy.kidlisp.com handle @buy { root * /opt/ac/system/public/kidlisp.com rewrite * /buy.html file_server } @pj host pj.kidlisp.com handle @pj { root * /opt/ac/system/public/kidlisp.com rewrite * /pj.html file_server } @device host device.kidlisp.com handle @device { handle /api/* { import lith_proxy } handle /js/* { root * /opt/ac/system/public/kidlisp.com file_server } handle /qr/* { root * /opt/ac/system/public/kidlisp.com rewrite * /qr.html file_server } handle { root * /opt/ac/system/public/kidlisp.com rewrite * /device.html file_server } } @topcalm host top.kidlisp.com calm.kidlisp.com handle @topcalm { handle /js/* { root * /opt/ac/system/public/kidlisp.com file_server } import lith_proxy } @learn host learn.kidlisp.com handle @learn { handle /api/* { import lith_proxy } handle /decree* { root * /opt/ac/system/public/kidlisp.com rewrite * /decree.html file_server } handle { root * /opt/ac/system/public/kidlisp.com rewrite * /learn.html file_server } } @keepsredirect host keeps.kidlisp.com handle @keepsredirect { redir https://keep.kidlisp.com{uri} 301 } @keepsac host keeps.aesthetic.computer handle @keepsac { import lith_proxy } # --- jas.life --- @jaslife host jas.life handle @jaslife { root * /opt/ac/system/public/jas.life try_files {path} {path}.html /index.html file_server } # --- www.jas.life redirect --- @wwwjas host www.jas.life handle @wwwjas { redir https://jas.life{uri} 301 } # --- pals.aesthetic.computer --- # pals.aesthetic.computer → /api/logo (dynamic logo generation) @pals host pals.aesthetic.computer handle @pals { rewrite * /api/logo{uri} import lith_proxy } @l5prompt host l5.prompt.ac handle @l5prompt { redir https://l5.aesthetic.computer{uri} 301 } @p5prompt host p5.prompt.ac handle @p5prompt { redir https://p5.aesthetic.computer{uri} 301 } @procprompt host processing.prompt.ac handle @procprompt { redir https://processing.aesthetic.computer{uri} 301 } @siteprompt host sitemap.prompt.ac handle @siteprompt { redir https://sitemap.aesthetic.computer{uri} 301 } @apiprompt host api.prompt.ac handle @apiprompt { redir https://api.aesthetic.computer{uri} 301 } # --- legacy duckweedtri hosts --- @duckweedaesthetic host duckweedtri.aesthetic.computer handle @duckweedaesthetic { redir https://aesthetic.computer{uri} 301 } @duckweedprompt host duckweedtri.prompt.ac handle @duckweedprompt { redir https://aesthetic.computer{uri} 301 } # --- www.prompt.ac redirect --- # (needs a proxied `www` A record in the prompt.ac zone — without it the # `*.prompt.ac → 100::` Worker wildcard swallows www and 522s.) @wwwpromptac host www.prompt.ac handle @wwwpromptac { redir https://prompt.ac{uri} 301 } # --- prompt.ac/menuband → menuband.app (Menu Band's own domain) --- # Menu Band now lives at menuband.app; this legacy path keeps working by # forwarding into it (subpath preserved: /menuband/privacy.html → # menuband.app/privacy.html). More specific than the host-only @promptac # below, so Caddy evaluates it first. @promptac_menuband { host prompt.ac path /menuband /menuband/* } handle @promptac_menuband { # route{} preserves written order so strip_prefix runs before redir # captures {uri} — otherwise the target keeps the /menuband prefix. route { uri strip_prefix /menuband redir https://menuband.app{uri} 301 } } # --- prompt.ac (apex root only) → HTML prompt shell (its own product) --- # Naked prompt.ac serves the fast HTML prompt that hosts the AC runtime in # an iframe. Every other prompt.ac/<path> still 301s to aesthetic.computer # (below), so deep links keep working. Evaluated before the redirect since # it appears first in source order. @promptac_root { host prompt.ac path / } handle @promptac_root { root * /opt/ac/system/public/prompt.ac rewrite * /index.html file_server } # --- prompt.ac/2018 → Jeffrey's prehistory-year media memorial --- @promptac_2018 { host prompt.ac path /2018 /2018/* } handle @promptac_2018 { root * /opt/ac/system/public/prompt.ac uri strip_prefix /2018 try_files {path} {path}/ /2018/index.html file_server } # --- prompt.ac/~<channel> → a live Aesthetic Code session --- # The address a phone gets when it scans the prompt rock. It exists to be # short: every byte of it is a module of QR, and the long form it expands # to (`/prompt~channel%20<id>~!autorun`) costs an extra version of the # symbol — smaller modules, on the surface hardest to scan. Twenty-seven # bytes with the scheme keeps it at version 2. # # A redirect rather than a rewrite: the shell reads the channel out of its # own location client-side, so it has to actually arrive at the long URL. # Must sit above @promptac, which otherwise claims every non-root path. @promptac_channel { host prompt.ac path_regexp promptacchannel ^/~([A-Za-z0-9_-]{4,16})$ } handle @promptac_channel { redir https://prompt.ac/prompt~channel%20{re.promptacchannel.1}~!autorun 302 } # The installer and its tarball, served as files from both hosts. Must sit # above @promptac, which otherwise hands every non-root path to the resolver # and would turn `curl prompt.ac/easel.sh` into a redirect to a piece. # # prompt.ac because it is nine bytes shorter than aesthetic.computer and an # install line is read aloud and typed by hand more often than it is copied. @easel_install { host prompt.ac aesthetic.computer path /easel.sh /easel.tar.gz /easel.json } handle @easel_install { root * /opt/ac/system/public header /easel.sh Content-Type "text/plain; charset=utf-8" header /easel.tar.gz Content-Type "application/gzip" header /easel.json Content-Type "application/json" # A stale installer is worse than a slow one: it points at a tarball # that may no longer be there. header Cache-Control "public, max-age=300" file_server } # --- prompt.ac routes --- # Non-root paths pass through lith's conflict-aware Whistlegraph resolver. # It sends live codes to whistlegraph.org and preserves the historical # aesthetic.computer redirect for pieces, commands, and unknown paths. @promptac { host prompt.ac not path / } handle @promptac { import lith_proxy } @tryshared { host l5.aesthetic.computer processing.aesthetic.computer path /aesthetic.computer/* } handle @tryshared { root * /opt/ac/system/public file_server } # --- l5.aesthetic.computer --- @l5 host l5.aesthetic.computer handle @l5 { handle /api/* { import lith_proxy } handle /docs* { import lith_proxy } # Static fallback wrapped in handle{} so handle /api/* stays terminal — # bare try_files is reordered ahead of handle and 405s /api/* POSTs. handle { root * /opt/ac/system/public/l5.aesthetic.computer try_files {path} {path}.html /index.html file_server } } # --- processing.aesthetic.computer --- @processing host processing.aesthetic.computer handle @processing { handle /api/* { import lith_proxy } handle /docs* { import lith_proxy } # Static fallback wrapped in handle{} so handle /api/* stays terminal — # bare try_files is reordered ahead of handle and 405s /api/* POSTs. handle { root * /opt/ac/system/public/processing.aesthetic.computer try_files {path} {path}.html /index.html file_server } } # --- rdp.jas.life --- @rdp host rdp.jas.life handle @rdp { root * /opt/ac/system/public/rdp.jas.life try_files {path} {path}.html /index.html file_server } # --- quiltnet.org --- @quiltnet host quiltnet.org www.quiltnet.org handle @quiltnet { root * /opt/ac/system/public/quiltnet.org try_files {path} {path}.html /index.html file_server } # --- kidlisp.com root domain --- @kidlisproot host kidlisp.com www.kidlisp.com handle @kidlisproot { handle /api/* { import lith_proxy } handle /.netlify/functions/* { import lith_proxy } handle /keeps { redir https://keep.kidlisp.com/ 301 } # Serve AC runtime assets for iframe embedding handle /aesthetic.computer/* { root * /opt/ac/system/public file_server } # kidlisp.com SPA handle { root * /opt/ac/system/public/kidlisp.com try_files {path} {path}.html /index.html file_server } } # --- notepat.com --- @notepatroot host notepat.com www.notepat.com handle @notepatroot { @notepatindex path / handle @notepatindex { rewrite * /notepat } # Permalink: notepat.com/amxd → current notepat.com.amxd as a # direct download. Clients get the Content-Disposition so it # lands in ~/Downloads instead of opening in-browser. handle /amxd { rewrite * /m4l/notepat.com.amxd header Content-Disposition "attachment; filename=\"notepat.com.amxd\"" root * /opt/ac/system/public file_server } import lith_proxy } # --- wipppps.world --- @wippppsroot host wipppps.world www.wipppps.world handle @wippppsroot { @wippppsindex path / handle @wippppsindex { rewrite * /wipppps } import lith_proxy } # --- danzballet.studio --- @danzballetroot host danzballet.studio www.danzballet.studio handle @danzballetroot { root * /opt/ac/system/public/danzballet.studio try_files {path} /index.html file_server } # --- sitemap.aesthetic.computer --- # The sitemap is a static network index, not the generic AC prompt shell. @sitemaproot { host sitemap.aesthetic.computer path / } handle @sitemaproot { root * /opt/ac/system/public rewrite * /sitemap.html file_server } @mainspa host aesthetic.computer www.aesthetic.computer lith.aesthetic.computer notepat.com www.notepat.com wipppps.world www.wipppps.world p5.aesthetic.computer sitemap.aesthetic.computer handle @mainspa { # Assets → DO Spaces CDN. The bucket root maps directly to # assets.aesthetic.computer (no /assets/ prefix exists in the # bucket), so we use handle_path to strip the matched /assets/ # prefix before substituting {path} into the redirect target. # The previous form (`handle /assets/*` + `{uri}`) doubled the # prefix and produced 403s on assets.aesthetic.computer/assets/... # (See: 2026-04-29 platter readings 403 fix.) handle_path /assets/* { redir https://assets.aesthetic.computer{path} 302 } # API → lith handle /api/* { import lith_proxy } handle /.netlify/functions/* { import lith_proxy } # Media → lith handle /media/* { import lith_proxy } # Static rewrite shortcuts handle /disks/* { uri strip_prefix /disks root * /opt/ac/system/public/aesthetic.computer/disks file_server } handle /lib/* { uri strip_prefix /lib root * /opt/ac/system/public/aesthetic.computer/lib file_server } # Static files, then SPA fallback handle { root * /opt/ac/system/public @static file handle @static { file_server } handle { import lith_proxy } } } # --- false.work --- @falseroot host false.work handle @falseroot { root * /opt/ac/system/public/false.work try_files {path} {path}.html /index.html file_server } @wwwfalse host www.false.work handle @wwwfalse { redir https://false.work{uri} 301 } # --- menuband.app (Menu Band — Mac App Store landing + privacy page) --- # The landing page uses absolute asset paths (/menuband/icon.png, # /aesthetic.computer/cursors/…), so those two prefixes are served from the # full public tree; everything else (apex → index.html, /privacy → privacy) # is served from the scoped menuband bundle. Each branch is wrapped in its # own handle{} — mixing bare try_files/file_server with nested handles gets # reordered by Caddy and breaks the reload (see the l5/kidlisp blocks). @menubandapp host menuband.app handle @menubandapp { # Backend API — the landing page fetches /api/version (recent-changes # feed + live-reload), and /advanced fetches /api/menuband-downloads # (the DMG counter, which moved off the landing page when the App # Store became the front door). Without this, those fall through to # the catch-all and get index.html instead of JSON, so "Recent # changes" never populates. Kept terminal (own handle{}) and ahead of # the static catch-all. handle /api/* { import lith_proxy } handle /menuband/* { root * /opt/ac/system/public file_server } handle /aesthetic.computer/* { root * /opt/ac/system/public file_server } # Apex → Mac App Store product page (the store listing is the # converting front door; @jeffrey, 2026-08-31). Only the bare root # forwards — /privacy.html, /support.html, /redeem.html, /advanced.html # must stay served here because App Review points at them. 302, not # 301, so the front door can move back without fighting browser caches. handle / { redir https://apps.apple.com/app/id6767311903 302 } handle { root * /opt/ac/system/public/menuband try_files {path} {path}.html /index.html file_server } } @wwwmenubandapp host www.menuband.app handle @wwwmenubandapp { redir https://menuband.app{uri} 301 } # --- Fallback for any unmatched host --- handle { import lith_proxy }}# --- oskiewar.com ---# Porkbun DNS points directly at lith, so this host needs a publicly trusted# certificate rather than the Cloudflare origin certificate on the :443 block.# midi.oskiewar.com is the same site: the shell reads the host name and turns# the MIDI bridge on, so scoring from a DAW needs no query string. It needs its# own A record at Porkbun — there is no wildcard on this zone.oskiewar.com, midi.oskiewar.com { tls { issuer acme { dir https://acme-v02.api.letsencrypt.org/directory } protocols tls1.2 tls1.3 } encode zstd gzip # Meet-style durable round URLs: live while a publisher is present, demo # playback after the round ends. Redirect old QR routes to the raw ID. @oskiewarwatchmatch path_regexp oskiewarwatchmatch ^/watch/([^/]+)/?$ redir @oskiewarwatchmatch https://oskiewar.com/{re.oskiewarwatchmatch.1} 308 @oskiewarwatchroot path /watch /watch/ redir @oskiewarwatchroot https://oskiewar.com/ 308 @oskiewarround path_regexp oskiewarround ^/(([a-z]{4,7}[0-9]{1,3})|([bdfgklmnprstvz][aeiou][bdfgklmnprstvz][aeiou][bdfgklmnprstvz][aeiou]-[bdfgklmnprstvz][aeiou][bdfgklmnprstvz][aeiou][bdfgklmnprstvz][aeiou]-[bdfgklmnprstvz][aeiou][bdfgklmnprstvz][aeiou][bdfgklmnprstvz][aeiou]))/?$ handle @oskiewarround { root * /opt/ac/xbox/live rewrite * /mac-test.html header Cache-Control no-cache file_server { precompressed br zstd } } # The game source and modules share the /oskiewar prefix with the live API # route below, so serve them before the proxy matcher claims them. # `precompressed` answers from the max-level .br/.zst sidecars that # xbox/tools/precompress-live.sh writes after every checkout — a fifth # smaller than encoding on the fly — and falls back to `encode` when a # sidecar is missing. # `no-cache` here means revalidate, not never-cache: an unchanged file # answers 304 against its ETag and loads from disk. Without the header the # browser's heuristic kept whole modules stale across deploys — a returning # visitor once ran a months-old round-room.mjs against the current relay. @oskiewarmodules path /oskiewar.js /oskiewar-sfx.mjs /oskiewar-voice.mjs /oskiewar-midi.mjs /oskiewar-wizard.mjs /oskiewar-fighter.mjs /oskiewar-map.mjs /oskiewar-workshop.mjs handle @oskiewarmodules { root * /opt/ac/xbox/live header Cache-Control no-cache file_server { precompressed br zstd } } handle /oskiewar* { import lith_proxy } # AC runtime services used by the spectator route. handle /api/* { import lith_proxy } handle /.netlify/functions/* { import lith_proxy } handle /media/* { import lith_proxy } handle /manifest.json { import lith_proxy } handle /aesthetic.computer/* { root * /opt/ac/system/public header Cache-Control no-cache file_server } handle /type/* { root * /opt/ac/system/public header Cache-Control no-cache file_server } # The one asset that genuinely never moves between deploys can skip the # revalidation round-trip for a week. woff2 is the same face at a third # of the bytes; the TTF stays for whatever cannot read it. handle /ComicRelief-Regular.woff2 { root * /opt/ac/system/public/papers.aesthetic.computer/foundry/fonts header Cache-Control "public, max-age=604800" file_server } handle /ComicRelief-Regular.ttf { root * /opt/ac/system/public/papers.aesthetic.computer/foundry/fonts header Cache-Control "public, max-age=604800" file_server } handle /sw.js { root * /opt/ac/system/public header Cache-Control no-cache file_server } # Add your coach: the page the title screen prints, and (through the # catch-all below) the one file it hands to a player's own agent. handle /coach { root * /opt/ac/xbox/live rewrite * /coach.html header Cache-Control no-cache file_server } @jevdemo path /jev-vs-jev /jev-vs-jev/ handle @jevdemo { root * /opt/ac/xbox/live rewrite * /jev-vs-jev/index.html header Cache-Control no-cache file_server } # Playable Canvas/Web Audio build shared with the desktop test harness. @oskiewargame path / /workshop /workshop/ handle @oskiewargame { root * /opt/ac/xbox/live rewrite * /mac-test.html header Cache-Control no-cache file_server { precompressed br zstd } } handle { root * /opt/ac/xbox/live header Cache-Control no-cache file_server { precompressed br zstd } }}www.oskiewar.com { tls { protocols tls1.2 tls1.3 } redir https://oskiewar.com{uri} 301}# --- nopaint.art ---# (touched 2026-07-19 x2 to force a reload after the DNS flip so ACME retries promptly)# This host's DNS points straight at lith (DigitalOcean zone, no Cloudflare# proxy), so it cannot ride the origin cert the :443 block uses — Caddy# manages a real ACME cert for it instead. The native remake landed, so the# root now serves the new No Paint on AC without leaving nopaint.art.# /classic stays the permanent home of the preserved 2021 Construct build,# and /gallery + the per-painting archive record pages keep serving here.nopaint.art { # Pin issuance to LE production (Caddy demoted this host to LE staging # after early post-DNS-flip failures; staging's resolvers held the stale # Vercel IP long after production had the new one). # # The `protocols` line is NOT a hardening tweak — it is what makes this # site's certificate get SERVED. The :443 catch-all above pins its # certificate_selection to the Cloudflare origin cert (any_tag cert0), # and without a distinct TLS setting here the Caddyfile adapter emits # only that catch-all connection policy, so every SNI — including this # one — gets the origin cert. A site-specific setting forces an # sni-matched policy with default (managed-cert) selection, ordered # before the catch-all. Verify with: # caddy adapt --config lith/Caddyfile | jq '.apps.http.servers.srv0.tls_connection_policies' tls { issuer acme { dir https://acme-v02.api.letsencrypt.org/directory } protocols tls1.2 tls1.3 } root * /opt/ac/system/public/nopaint.art encode gzip @nopaint_root path / handle @nopaint_root { rewrite * /nopaint import lith_proxy } # The conductor rewrites / to /nopaint:<seed>. Reloads and shared # sessions must boot AC again instead of matching an archive record. @nopaint_session path /nopaint /nopaint/ /nopaint:* /nopaint~* handle @nopaint_session { import lith_proxy } # AC runtime routes used by the native remake at the branded root. handle /presigned-upload-url/* { import lith_proxy } handle /api/* { import lith_proxy } handle /.netlify/functions/* { import lith_proxy } handle /media/* { import lith_proxy } handle /disks/* { uri strip_prefix /disks root * /opt/ac/system/public/aesthetic.computer/disks file_server } handle /lib/* { uri strip_prefix /lib root * /opt/ac/system/public/aesthetic.computer/lib file_server } handle /aesthetic.computer/* { root * /opt/ac/system/public file_server } handle /type/* { root * /opt/ac/system/public file_server } handle /sw.js { root * /opt/ac/system/public file_server } handle /manifest.json { root * /opt/ac/system/public file_server } handle_path /nopaint.art/* { root * /opt/ac/system/public/nopaint.art file_server } handle_path /classic* { file_server } handle /gallery* { file_server } # A single archive id gets a human-readable record page. Existing files # (runtime assets, icons, manifests) continue through the final handler. @nopaint_archive_record { not { file } path_regexp archiveRecord ^/[^/]+/?$ } handle @nopaint_archive_record { rewrite * /painting/index.html file_server } handle { file_server }}www.nopaint.art { # Same serving requirement as the apex block above: without a distinct # TLS setting, the catch-all's pinned origin cert answers this SNI too. tls { protocols tls1.2 tls1.3 } redir https://nopaint.art{uri} 301}# --- gym.anthonyzollo.com ---# Anthony's zone CNAMEs this host to lith.aesthetic.computer. Unlike AC's# Cloudflare-origin-cert catch-all, this external hostname needs its own public# ACME certificate and SNI policy. The HTML is updated through the authenticated# Express endpoint and stored outside the git checkout.gym.anthonyzollo.com { tls { issuer acme { dir https://acme-v02.api.letsencrypt.org/directory } protocols tls1.2 tls1.3 } encode zstd gzip @gymapi path /api/publish-gym /api/history-gym /api/rewind-gym handle @gymapi { import lith_proxy } handle { root * /var/lib/aesthetic-computer/gym.anthonyzollo.com try_files {path} /index.html header Cache-Control "no-cache" file_server }}# --- inbound.aesthetic.computer ---# Not a website. This is the SMTP door for Amail (lith/mail-inbound.mjs,# lith-mail.service on :25) — Google Workspace routes every unknown# @aesthetic.computer address here. The DNS record is DNS-only (grey cloud):# Cloudflare does not proxy port 25.## Its STARTTLS certificate can't come from Caddy's automation: the Cloudflare# origin cert loaded above is a self-issued *.aesthetic.computer wildcard, and# Caddy skips ACME for any name a loaded cert already covers. So certbot# fetches it by HTTP-01 through the webroot served below (:80), and the door# reads /etc/letsencrypt/live/inbound.aesthetic.computer/. There is no site# block for the host on purpose: one would answer the challenge itself.:80 { # certbot's HTTP-01 challenge files, before the redirect swallows them. # Both are `handle`s on purpose: a bare `redir` sorts ahead of `handle` # in Caddy's directive order and would answer first. handle /.well-known/acme-challenge/* { root * /var/lib/amail-acme file_server } handle { redir https://{host}{uri} 301 }}# --- mime.ac ---# DNS-only on Cloudflare; serve MIME at the apex with its own public TLS.mime.ac { tls { issuer acme { dir https://acme-v02.api.letsencrypt.org/directory } protocols tls1.2 tls1.3 } encode zstd gzip handle / { root * /opt/ac/system/public/mime header Cache-Control "no-cache" file_server } @mime_brand path /favicon.svg /favicon-32.png /apple-touch-icon.png /social/mime.jpg handle @mime_brand { root * /opt/ac/system/public/mime header Cache-Control "public, max-age=86400" file_server } handle /mime { redir / 302 } handle /mime/ { redir / 302 } handle /prompt { redir https://aesthetic.computer/prompt 302 } handle { import lith_proxy }}www.mime.ac { tls { issuer acme { dir https://acme-v02.api.letsencrypt.org/directory } } redir https://mime.ac{uri} 308}# Aesel uses Cloudflare DNS-only records and its own public certificate.aesel.app { tls { protocols tls1.2 tls1.3 issuer acme { dir https://acme-v02.api.letsencrypt.org/directory } } encode zstd gzip root * /opt/ac/system/public/aesel header X-Content-Type-Options nosniff header Referrer-Policy strict-origin-when-cross-origin file_server}www.aesel.app { tls { protocols tls1.2 tls1.3 issuer acme { dir https://acme-v02.api.letsencrypt.org/directory } } redir https://aesel.app{uri} 301}