From 2cbecb25f165d72486a3cd13078bad86190280ae Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 15 Sep 2026 16:34:02 -0700 Subject: [PATCH] Serve mime.ac with AC sign-in and preserved comment drafts --- lith/Caddyfile | 38 ++++++++++++++++ system/backend/MIME.md | 19 +++++++- system/public/mime/index.html | 82 +++++++++++++++++++++++++++++++---- 3 files changed, 129 insertions(+), 10 deletions(-) diff --git a/lith/Caddyfile b/lith/Caddyfile index 036b3dc388..43c602f67f 100644 --- a/lith/Caddyfile +++ b/lith/Caddyfile @@ -1313,3 +1313,41 @@ gym.anthonyzollo.com { 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 + } + 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 +} diff --git a/system/backend/MIME.md b/system/backend/MIME.md index 1384bbc0d2..6fe56dc820 100644 --- a/system/backend/MIME.md +++ b/system/backend/MIME.md @@ -1,6 +1,7 @@ # Mime -Discussion on AC media. The frontend is `/mime/`; the API is `/api/mime`. +Discussion on AC media. The frontend is `/mime/` and the `https://mime.ac/` +apex; the API is `/api/mime`. `www.mime.ac` redirects to the apex. The main feed snaps to one viewport per media item without cropping, following the tape viewer (`disks/tv.mjs`). It loads 12 posts at a time and preserves the current item across thread visits and orientation changes. Videos play muted @@ -70,3 +71,19 @@ MIME_TEST_MONGO_URI=mongodb://127.0.0.1:27017 node --test system/backend/tests/m Tests cover automatic discovery without writes, attribution, concurrent first replies, attachment rejection, verified authorship, source visibility, storage URLs, legacy uploads, pagination, and identity across tape conversion. + +## Auth0 domains + +MIME uses the existing AC SPA client (`LVdZaMbyXctkGfZDnpzDATB5nR0ZhmMt`) +through `hi.aesthetic.computer`. Keep all existing application URL entries; +append these exact values in the Auth0 application settings: + +- Allowed Callback URLs: `https://mime.ac/`, `https://aesthetic.computer/mime/` +- Allowed Logout URLs: `https://mime.ac/`, `https://aesthetic.computer/mime/` +- Allowed Web Origins and Allowed Origins (CORS): `https://mime.ac` + +The SDK handles authorization-code callbacks and validates OAuth state. The +return hash and an in-progress comment survive the login redirect. Account +handles are still resolved by the API from the verified access token. Browser +storage is origin-scoped, so MIME offers sign-in on the new domain; it does +not copy tokens between domains. The wordmark returns to AC's prompt. diff --git a/system/public/mime/index.html b/system/public/mime/index.html index 00037301e8..791d35b048 100644 --- a/system/public/mime/index.html +++ b/system/public/mime/index.html @@ -46,7 +46,11 @@ a:hover { color: var(--ink); } :focus-visible { outline: 2px solid var(--link); outline-offset: 3px; } ::selection { background: var(--ink); color: var(--paper); } - header { text-align: left; padding: 1em 1em 0.4em; } + header { display: flex; align-items: center; justify-content: space-between; gap: 1em; text-align: left; padding: 1em 1em 0.4em; } + .account { display: flex; align-items: center; justify-content: flex-end; gap: 0.5em; flex-wrap: wrap; font-size: 0.85em; } + .account button { font: inherit; color: var(--link); background: var(--paper); border: 1px solid var(--card-edge); border-radius: 4px; min-height: 36px; padding: 0.3em 0.6em; cursor: pointer; } + .account [role="status"] { max-width: 24ch; color: var(--accent); } + .account [hidden] { display: none; } header h1 { margin: 0; color: var(--ink); line-height: 1; } .wordmark { display: inline-flex; align-items: center; min-height: 44px; color: inherit; text-decoration: none; } .wordmark svg { display: block; width: 108px; height: 33px; overflow: visible; } @@ -121,7 +125,8 @@ border: 1px dashed var(--card-edge); padding: 0.8em 1.2em; } body.feed { padding: 0; overflow: hidden; } - body.feed header { position: fixed; top: 0; left: 0; z-index: 2; padding-top: max(0.6em, env(safe-area-inset-top)); } + body.feed header { width: 100%; pointer-events: none; position: fixed; top: 0; left: 0; z-index: 2; padding-top: max(0.6em, env(safe-area-inset-top)); } + body.feed header a, body.feed .account { pointer-events: auto; } body.feed header a { background: var(--paper); } body.feed main { max-width: none; padding: 0; height: 100vh; height: 100dvh; @@ -148,7 +153,7 @@ .feed-post > .meta { margin: 0 0.75rem; } .feed-post > .body { margin: 0 0.75rem; max-height: 15vh; overflow: auto; } .feed-post > .discussion { align-self: center; flex-shrink: 0; margin: 0; } - .feed-more { position: absolute; top: max(0.6em, env(safe-area-inset-top)); right: 0.75rem; display: flex; align-items: center; gap: 0.5em; max-width: calc(100% - 9rem); } + .feed-more { position: absolute; top: max(3.75rem, calc(env(safe-area-inset-top) + 3rem)); right: 0.75rem; display: flex; align-items: center; gap: 0.5em; max-width: calc(100% - 9rem); } .feed-more button { padding: 0.3em 0.6em; } .feed-more [role="status"] { color: var(--dim); font-size: 0.85em; } body.feed main > p { margin: 4rem 0.75rem; } @@ -166,7 +171,7 @@

- +

+
@@ -311,6 +321,10 @@ function renderPost(post, isOp, inThread = false, contextBoard, inFeed = false) } const AC_CLIENT = "LVdZaMbyXctkGfZDnpzDATB5nR0ZhmMt"; +const AUTH_REDIRECT = location.origin + (location.hostname === "mime.ac" ? "/" : "/mime/"); +const accountAction = document.getElementById("account-action"); +const accountHandle = document.getElementById("account-handle"); +const accountStatus = document.getElementById("account-status"); const AC_CACHE_PREFIX = "@@auth0spajs@@::" + AC_CLIENT + "::"; let identity = { token: null, handle: null }; let authClientPromise, identityPromise; @@ -342,7 +356,7 @@ function authClient() { return new window.auth0.Auth0Client({ domain: "https://hi.aesthetic.computer", clientId: AC_CLIENT, cacheLocation: "localstorage", useRefreshTokens: true, - authorizationParams: { redirect_uri: location.origin }, + authorizationParams: { redirect_uri: AUTH_REDIRECT }, httpTimeoutInSeconds: 30, }); })().catch((error) => { authClientPromise = null; throw error; }); @@ -378,11 +392,39 @@ function showIdentity(input) { input.setAttribute("aria-label", identity.token ? "Commenting as" : "Name (optional)"); } +function showAccount() { + accountHandle.textContent = identity.handle || ""; + accountAction.textContent = identity.token ? "sign out" : "sign in"; +} + +accountAction.onclick = async () => { + accountAction.disabled = true; + accountStatus.textContent = ""; + try { + const client = await authClient(); + if (identity.token) { + localStorage.removeItem("session-aesthetic"); + await client.logout({ logoutParams: { returnTo: AUTH_REDIRECT } }); + } else { + const form = view.querySelector("#post"); + if (form) sessionStorage.setItem("mime-comment-draft", JSON.stringify({ + hash: location.hash, text: form.querySelector("textarea").value, + })); + await client.loginWithRedirect({ appState: { hash: location.hash || "#/" }, + authorizationParams: { redirect_uri: AUTH_REDIRECT } }); + } + } catch (error) { + accountStatus.textContent = error.message || "could not sign in"; + accountAction.disabled = false; + } +}; + function updateIdentity() { - refreshIdentity().then(() => { + return refreshIdentity().then(() => { + showAccount(); const input = view.querySelector('[name="commenter"]'); if (input) showIdentity(input); - }).catch(() => {}); + }).catch(() => { accountStatus.textContent = "sign in again to use your handle"; }); } window.addEventListener("focus", updateIdentity); window.addEventListener("storage", (event) => { @@ -407,6 +449,10 @@ function postForm(parent) { status.id = "status"; status.setAttribute("role", "status"); form.append(text, name, button, status); + try { + const draft = JSON.parse(sessionStorage.getItem("mime-comment-draft")); + if (draft?.hash === location.hash && typeof draft.text === "string") text.value = draft.text; + } catch {} refreshIdentity().then(() => showIdentity(name)).catch(() => { if (form.isConnected) status.textContent = "sign in again to use your handle"; }); @@ -429,6 +475,7 @@ function postForm(parent) { }); const out = await res.json(); if (!res.ok) { status.textContent = out.error; return; } + sessionStorage.removeItem("mime-comment-draft"); history.replaceState(null, "", "#/t/" + (parent || out.code)); await route(); } catch (error) { @@ -644,8 +691,25 @@ window.addEventListener("keydown", (event) => { view.scrollTo({ top: index * view.clientHeight, behavior: matchMedia("(prefers-reduced-motion: reduce)").matches ? "instant" : "smooth" }); }); -window.onhashchange = route; -route(); +async function start() { + const params = new URLSearchParams(location.search); + if (params.has("state") && (params.has("code") || params.has("error"))) { + accountAction.disabled = true; + try { + const client = await authClient(); + const { appState } = await client.handleRedirectCallback(); + const hash = typeof appState?.hash === "string" && appState.hash.startsWith("#/") ? appState.hash : "#/"; + history.replaceState(null, "", location.pathname + hash); + } catch (error) { + accountStatus.textContent = error.message || "could not finish sign-in"; + history.replaceState(null, "", location.pathname + "#/"); + } finally { accountAction.disabled = false; } + } + window.onhashchange = route; + await route(); + await updateIdentity(); +} +start(); -- 2.51.2