From e7064a7c6ab00d6c3d9343e970d9befe92c9fb49 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 21 Aug 2026 10:17:28 -0400 Subject: [PATCH] bliss-cli: detect failed logins instead of caching anon sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server issues a connect.sid cookie on every response, including a rejected login (which re-renders the form as 200). login() treated any returned cookie as success, so bad creds silently cached an anonymous session and the CLI stayed logged out with no warning — and since it only re-authenticates on a 401 (which the server never sends), the stale cookie was never refreshed. Trust the redirect status instead (302 = success, 2xx = rejected) and throw a clear error with the HTTP code on failure. Add a `bliss login` command that forces a fresh login past any stale cookie. Co-Authored-By: Claude Opus 4.8 --- bliss-cli/bliss | 8 ++++++++ bliss-cli/client.js | 18 +++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/bliss-cli/bliss b/bliss-cli/bliss index 2fe9bc5..17c6b18 100755 --- a/bliss-cli/bliss +++ b/bliss-cli/bliss @@ -68,6 +68,14 @@ const commands = { out(c.BASE); } }, + // Force a fresh login with BLISS_USER / BLISS_PASS and report the result. + // Unlike normal commands (which reuse a cached cookie and only re-auth on a + // 401 the server never sends), this always hits /login, so it's the way to + // (re)authenticate after changing creds or clearing a stale anonymous cookie. + async login() { + await c.login(); // throws with a clear message if the server rejects creds + out("logged in as '" + (process.env.BLISS_USER || "?") + "' -> " + c.BASE); + }, // ---- reads (JSON via /plumbing) ---- async structures() { out(await c.getJSON("/plumbing/structures")); diff --git a/bliss-cli/client.js b/bliss-cli/client.js index a82c14f..c99b47a 100644 --- a/bliss-cli/client.js +++ b/bliss-cli/client.js @@ -79,10 +79,21 @@ async function login() { headers: { "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ username: USER, password: PASS }).toString(), }); + // The server issues an (anonymous) connect.sid on EVERY response — including a + // failed login, which re-renders the form as a 200. So the presence of a + // cookie says nothing about success. The real signal is the redirect: a good + // login 302s to `next`/"/", a rejected one stays 2xx. Trust the status, not + // the cookie, or we'd cache an anonymous session and silently stay logged out. const cookie = extractCookie(res); - if (!cookie) { - // A 200 back from /login means the login form re-rendered => bad creds. - throw new Error("login failed for user '" + USER + "' (check BLISS_PASS)"); + const redirected = res.status >= 300 && res.status < 400; + if (!redirected || !cookie) { + throw new Error( + "login failed for user '" + + USER + + "' — server rejected the credentials (HTTP " + + res.status + + "). Check BLISS_USER / BLISS_PASS.", + ); } saveCookie(cookie); return cookie; @@ -168,6 +179,7 @@ async function getText(urlPath) { module.exports = { BASE, setTarget, + login, request, getJSON, getText,