From c046719cd49d6b8bb40b5b06006f4360df7acd34 Mon Sep 17 00:00:00 2001 From: pip-install-python Date: Thu, 20 Aug 2026 15:03:44 -0500 Subject: [PATCH 1/2] Clerk: the return trip needs an env nobody ever set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sign-in on this host reached 2plot.ai, succeeded, and left the user there. No error on either side, client or server — which is why it survived a deploy. CLERK_SATELLITE_SIGN_IN_REDIRECT was unset, so dash-clerk-auth's buildSatelliteRedirect() returned null and both click handlers (lib/auth.py's #clerk-login-button delegation and assets/auth_gate.js's #auth-gate-*) fell through to Clerk.redirectToSignIn(). That hops to CLERK_SIGN_IN_URL — accounts.2plot.ai, Clerk's HOSTED Account Portal. The hub's Dash app is never in the loop, so none of its returnTo machinery runs: not the whitelist check against allowed_redirect_origins(), not the force-redirect, not the /onboarding auto-open. Whether the user comes back is then decided entirely by the Clerk dashboard's own allowed-redirect list, and when that does not name this host ClerkJS drops signInForceRedirectUrl silently and uses the portal's default. Successful sign-in, wrong destination, nothing logged. Set to https://2plot.ai/onboarding, the button navigates to a page on the hub's own app, which validates ?returnTo= against a list that already contains https://leaflet.2plot.dev and sends the user home. auth_gate.js appends &mode=signup, which that page reads to open the sign-UP modal, so one value serves both buttons. The variable is read by dash-clerk-auth from the environment directly, so lib/auth.py does not pass it through — declaring it in render.yaml is the whole fix. This file's own note is why it was missing: it said to set it "only once 2plot.ai has a page that honours ?returnTo=". That page shipped; nobody came back. So register() now WARNS at boot whenever satellite mode is on and this is unset, because a misconfiguration with no error anywhere needs something to say it out loud. DEPLOYMENT.md gains the two-path table, and a blueprint-vs-dashboard drift section: Render applies envVars on a blueprint SYNC, not on autoDeploy, so the live service is also missing PAGE_VISIBILITY_FILE (control-board toggles reset every deploy), AD_SERVER_URL (no ads), ANALYTICS_GEO_LOOKUP, and carries two Gen-1 variables nothing reads. PYTHONUNBUFFERED is correctly absent — it is a Dockerfile ENV here, as in the boilerplate. 182 passed, flake8 clean, render.yaml parses. Co-Authored-By: Claude Opus 5 (1M context) --- .env.example | 10 ++++++++ DEPLOYMENT.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++-- lib/auth.py | 42 +++++++++++++++++++++++++------ render.yaml | 33 +++++++++++++++++++++++++ 4 files changed, 143 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index 9b07a80..935f268 100644 --- a/.env.example +++ b/.env.example @@ -94,6 +94,16 @@ DASH_LEAFLET2_BASE_URL=http://localhost:8050 # Production primary (2plot.ai on its custom Clerk domains): # CLERK_SIGN_IN_URL=https://accounts.2plot.ai/sign-in # CLERK_SIGN_UP_URL=https://accounts.2plot.ai/sign-up +# +# THE RETURN TRIP — required in production, and the one Clerk setting that +# fails with no error anywhere. Set, the Sign In button navigates to +# https://2plot.ai/onboarding?returnTo=, a page on the hub's own +# app that validates returnTo and sends the user home. Unset, sign-in falls +# back to the Clerk-hosted Account Portal, the hub's app never sees the +# request, and the user authenticates successfully and stays on 2plot.ai. +# Leave it unset LOCALLY (localhost is not a satellite); render.yaml sets it +# in production. lib/auth.py warns at boot when satellite mode lacks it. +# CLERK_SATELLITE_SIGN_IN_REDIRECT=https://2plot.ai/onboarding # CLERK_FRONTEND_API=https://clerk.2plot.ai # CLERK_IS_SATELLITE=false # SESSION_SECRET= diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 446740a..512c7fa 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -147,6 +147,7 @@ is logged. | `CLERK_SATELLITE_SIGN_IN_REDIRECT` | (unset) | Optional, dash-clerk-auth ≥ 0.9.2. Absolute URL on the **primary** that Sign In navigates to, with this page in `?returnTo=`. Read by the package itself. Unset, sign-in falls back to `Clerk.redirectToSignIn()` forcing this page as the return — what this site ships today. Only set it once `2plot.ai` honours `?returnTo=`. | | `SESSION_SECRET` | (generated) | Signs the session + `__dca_identity` cookies. Without it dash-clerk-auth uses a **public dev default**. | | `ADMIN_EMAILS` | `a@b.com,c@d.com` | Allowlist for `/admin/control-board`. `OWNER_EMAIL` always counts. | +| `CLERK_SATELLITE_SIGN_IN_REDIRECT` | — | **Required in production.** `https://2plot.ai/onboarding`. Unset, sign-in reaches the primary and never returns — see the next section. | | `DISABLE_CLERK` | `1` | Dev kill switch — reads as "intentionally off" without touching the keys. Never set in production. | | `ALLOW_UNGATED_ADMIN` | `1` | Lets `/admin/control-board` render without Clerk. **Never set in production.** | @@ -183,6 +184,67 @@ instance and cannot host satellites): All five are already literals in `render.yaml`; only `CLERK_SECRET_KEY` and `CLERK_PUBLISHABLE_KEY` need entering in the dashboard. +### The sign-in return trip — the failure with no error + +**Symptom:** the visitor clicks Sign In, authenticates successfully on the +primary, and is left there. Nothing is logged on either host, no console error, +no failed request. Observed live on this service 2026-08-20. + +**Cause:** `CLERK_SATELLITE_SIGN_IN_REDIRECT` unset. There are two paths, and +only one of them belongs to this network: + +| | Set to `https://2plot.ai/onboarding` | Unset (the fallback) | +|---|---|---| +| Where the button goes | `2plot.ai/onboarding?returnTo=` | `CLERK_SIGN_IN_URL` → `accounts.2plot.ai`, Clerk's **hosted** Account Portal | +| Who validates the return | 2plot.ai's own Dash app, against `lib.auth.allowed_redirect_origins()` | Clerk's dashboard allowed-redirect list | +| Sign-up button | `&mode=signup` opens the sign-**up** modal | no equivalent | +| When it breaks | the hub's whitelist rejects the origin — visible in that repo | ClerkJS silently drops `signInForceRedirectUrl` and uses the portal default | + +The fallback is why this is so quiet: the hub's Dash app is **never in the +loop**, so none of its `returnTo` machinery — the whitelist check, the +force-redirect, the auto-open — ever executes. `accounts.2plot.ai` is Clerk's +property, not ours. + +`dash-clerk-auth` reads the variable from the environment itself, so +`lib/auth.py` does not pass it through; setting it on the service is the whole +fix. `lib/auth.py` now **warns at boot** whenever satellite mode is on and this +is unset, because the original note here said to set it "once 2plot.ai has a +page that honours `?returnTo=`" — 2plot.ai gained that page, and nobody came +back. + +**The other half to check when this misbehaves** lives in the *2plotai* repo, +not here: `allowed_redirect_origins()` is env-first, so if +`CLERK_ALLOWED_REDIRECT_ORIGINS` is set on the hub's service it **replaces** the +built-in list rather than extending it. `https://leaflet.2plot.dev` is in the +built-in default; confirm it survived any override. + +### Blueprint vs dashboard drift + +`render.yaml` declares the contract; Render applies `envVars` on a **blueprint +sync**, not on an autoDeploy from a git push. So the two drift, silently, and +the code cannot tell. As measured on 2026-08-20 the live service was missing: + +| Missing on the service | Consequence | +|---|---| +| `CLERK_SATELLITE_SIGN_IN_REDIRECT` | The sign-in return trip above. **Fix this one first.** | +| `PAGE_VISIBILITY_FILE` | Control-board overrides land on the container filesystem instead of `/var/data`, so every toggle resets on the next deploy. | +| `AD_SERVER_URL` | The ad slot never renders (the client's circuit breaker just keeps it hidden). | +| `ANALYTICS_GEO_LOOKUP` | Re-enables the per-visit ip-api.com lookup that Cloudflare's country header already answers. | +| `PAGE_DEFAULT_TIER` | Harmless today — `PAGE_DEFAULT_VISIBILITY` is set and is an accepted alias — but the gate's flip lever should be the canonical name. | +| `LLMS_SMALL_TIER` / `LLMS_FULL_TIER` | Harmless (`run.py` defaults both to `public` explicitly), but the knob is invisible on the dashboard. | +| `WEB_CONCURRENCY` | gunicorn falls back to its own worker default. | + +And carrying two variables **nothing reads any more** — Gen-1 leftovers retired +with the single-module tracker; delete them so the dashboard stops implying +they do something: + +- `SATELLITE_ANALYTICS_DRY_RUN` +- `SATELLITE_APP_ID` (the trio reads `SATELLITE_APP_KEY`) + +`PYTHONUNBUFFERED` belongs in the **Dockerfile**, not the dashboard — it is set +there already (`ENV PYTHONUNBUFFERED=1`), which is why it is correctly absent +from the service's variable list. + ### Registering the satellite on the primary — two separate lists Easy to conflate, and each fails differently. @@ -429,8 +491,10 @@ offline. The hub verifies the Clerk token against Clerk's JWKS and pins the environment table. 2. `GET /llms.txt`, `/robots.txt`, `/sitemap.xml` all 200, and the sitemap URLs use `leaflet.2plot.dev` (i.e. `APP_BASE_URL` is set correctly). -3. Sign in from the site — you should bounce to 2plot.ai and land **back here**, - not on the primary's home page. +3. Sign in from the site — you should bounce to **`2plot.ai/onboarding`** (not + `accounts.2plot.ai`) and land **back here**, not on the primary's home page. + Landing on the primary means `CLERK_SATELLITE_SIGN_IN_REDIRECT` is unset; + the boot log warns about it, and "The sign-in return trip" above is the fix. 4. `/admin/control-board` shows the page table with **no** dev-mode banner. 4b. The gate's boot line reads `access wiring ON` (see "Shipping dark, then flipping"). With `PAGE_DEFAULT_TIER=public` that is the dark launch: diff --git a/lib/auth.py b/lib/auth.py index b28e140..8237923 100644 --- a/lib/auth.py +++ b/lib/auth.py @@ -30,14 +30,29 @@ CLERK_SATELLITE_DOMAIN "leaflet.2plot.dev" (host only, no scheme) CLERK_SIGN_UP_URL https://2plot.ai/sign-up CLERK_SATELLITE_SIGN_IN_REDIRECT - OPTIONAL (dash-clerk-auth >= 0.9.2). An absolute URL - on the PRIMARY that the Sign In button navigates to, - with this page carried in ?returnTo=. Read by the - package itself, so we do not pass it through. Unset, - sign-in falls back to Clerk.redirectToSignIn() with - this page forced as the return — the behaviour this - site already ships. Only set it once 2plot.ai has a - page that honours ?returnTo=. + REQUIRED in production, despite reading as optional. + An absolute URL on the PRIMARY that the Sign In + button navigates to, with this page carried in + ?returnTo=. Read by dash-clerk-auth from the + environment itself, so we do not pass it through. + Set to https://2plot.ai/onboarding (render.yaml). + + It was left unset here while the note below said + "only set it once 2plot.ai has a page that honours + ?returnTo=". 2plot.ai gained exactly that page — + /onboarding, which validates returnTo against its + own satellite whitelist and force-redirects home — + and nobody came back to set it. The result, live on + 2026-08-20: sign-in succeeded on the primary and + stranded the user there. + + Unset, the fallback is Clerk.redirectToSignIn(), + which hops to CLERK_SIGN_IN_URL — the Clerk-HOSTED + Account Portal. The hub's Dash app never sees that + request, so none of its returnTo handling runs and + the return depends solely on the Clerk dashboard's + own allowed-redirect list. That failure is silent + on both sides. See the boot warning in register(). Admin allowlist (drives /admin/control-board): ADMIN_EMAILS comma-separated, case-insensitive @@ -263,6 +278,17 @@ def register() -> bool: f"CLERK_SATELLITE_DOMAIN={SATELLITE_HOST} (and CLERK_IS_SATELLITE=true) " "or ClerkJS will fail with 'satellite needs a domain'." ) + if is_satellite and not (os.getenv("CLERK_SATELLITE_SIGN_IN_REDIRECT") or "").strip(): + # The one Clerk misconfiguration with NO error on either side: the + # user signs in successfully and is simply left on the primary. It + # shipped that way here for weeks because nothing said so. Now + # something does. + print( + "[auth] ⚠️ Satellite mode with CLERK_SATELLITE_SIGN_IN_REDIRECT " + "unset — sign-in will hop to the Clerk Account Portal instead of " + "the hub, and users may not be returned to this site at all. " + "Set it to https://2plot.ai/onboarding (see render.yaml)." + ) if is_satellite and sat_domain: _install_satellite_signin_delegation() diff --git a/render.yaml b/render.yaml index 80f2085..a3c76b4 100644 --- a/render.yaml +++ b/render.yaml @@ -142,6 +142,39 @@ services: value: https://accounts.2plot.ai/sign-in - key: CLERK_SIGN_UP_URL value: https://accounts.2plot.ai/sign-up + # THE RETURN TRIP. Without this, sign-in reaches the primary and the + # user never comes back — the exact failure observed on this host + # 2026-08-20. + # + # Two different paths exist, and only one of them is ours: + # + # SET (this): the Sign In button NAVIGATES to + # https://2plot.ai/onboarding?returnTo=. + # That is a page on the hub's own Dash app, which + # injects a script that validates returnTo against + # lib.auth.allowed_redirect_origins() (leaflet.2plot.dev + # is in that list) and then force-redirects home. The + # network owns and can debug every hop. + # + # UNSET: dash-clerk-auth falls back to + # Clerk.redirectToSignIn(), which goes to + # CLERK_SIGN_IN_URL — accounts.2plot.ai, Clerk's HOSTED + # Account Portal. The hub's Dash app is never in the + # loop, so none of its returnTo machinery runs, and the + # return depends entirely on the Clerk dashboard's own + # allowed-redirect list. When that does not name this + # host, ClerkJS silently drops signInForceRedirectUrl + # and uses the portal's default after-sign-in URL: the + # user authenticates successfully and lands on 2plot.ai. + # No error, client-side or server-side. + # + # dash-clerk-auth reads this name from the environment itself + # (satellite_sign_in_redirect's env fallback), so lib/auth.py does not + # pass it through. `assets/auth_gate.js` appends &mode=signup for the + # "Create free account" button, which /onboarding reads to open the + # sign-UP modal instead — so this one value serves both buttons. + - key: CLERK_SATELLITE_SIGN_IN_REDIRECT + value: https://2plot.ai/onboarding # REQUIRED in satellite mode. A production custom-domain instance cannot # derive this from CLERK_SIGN_IN_URL — it must be set explicitly. - key: CLERK_FRONTEND_API From b8e658c8f6996bbd0da63d6a9f31022bd342e5bb Mon Sep 17 00:00:00 2001 From: pip-install-python Date: Thu, 20 Aug 2026 15:07:40 -0500 Subject: [PATCH 2/2] auth: catch the plausible wrong value for the satellite redirect, too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLERK_SATELLITE_SIGN_IN_REDIRECT reads like a boolean and is not — it is a destination. buildSatelliteRedirect() builds `?returnTo=` by plain concatenation, so a truthy non-URL resolves against THIS host: CLERK_SATELLITE_SIGN_IN_REDIRECT=true sends the Sign In button to https://leaflet.2plot.dev/true?returnTo=..., which 404s. That is worse than leaving it unset, where at least the Clerk Account Portal fallback signs the user in before stranding them. dash-clerk-auth notices and does not act on it: one logger.warning, then it uses the value anyway. That warning lands several screens up in the boot output, nowhere near the other [auth] lines a deploy check reads. So the guard added in c046719 now covers both mistakes — unset, and set to something that is not an absolute http(s) URL — and says which one happened. Verified against '', 'true', a bare host, and the correct URL. Co-Authored-By: Claude Opus 5 (1M context) --- .env.example | 9 +++++++-- DEPLOYMENT.md | 14 ++++++++++++++ lib/auth.py | 18 +++++++++++++++++- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 935f268..e616c79 100644 --- a/.env.example +++ b/.env.example @@ -101,8 +101,13 @@ DASH_LEAFLET2_BASE_URL=http://localhost:8050 # app that validates returnTo and sends the user home. Unset, sign-in falls # back to the Clerk-hosted Account Portal, the hub's app never sees the # request, and the user authenticates successfully and stays on 2plot.ai. -# Leave it unset LOCALLY (localhost is not a satellite); render.yaml sets it -# in production. lib/auth.py warns at boot when satellite mode lacks it. +# A DESTINATION, not a flag. It must be an absolute http(s) URL — the value +# is concatenated as `?returnTo=`, so `true` or a bare host +# resolves against THIS site and the button 404s, which is worse than +# leaving it unset. dash-clerk-auth only logs a warning and uses a bad +# value anyway; lib/auth.py warns at boot for both mistakes. +# Leave it unset LOCALLY (localhost is not a satellite); render.yaml sets +# it in production. # CLERK_SATELLITE_SIGN_IN_REDIRECT=https://2plot.ai/onboarding # CLERK_FRONTEND_API=https://clerk.2plot.ai # CLERK_IS_SATELLITE=false diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 512c7fa..f0b1fb2 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -205,6 +205,20 @@ loop**, so none of its `returnTo` machinery — the whitelist check, the force-redirect, the auto-open — ever executes. `accounts.2plot.ai` is Clerk's property, not ours. +**It is a destination, not a flag.** The value must be an absolute `http(s)` +URL, because `buildSatelliteRedirect()` builds `?returnTo=` by +concatenation. A truthy non-URL is the worst of the three states: + +| Value | Where the Sign In button goes | +|---|---| +| `https://2plot.ai/onboarding` | `https://2plot.ai/onboarding?returnTo=…` — correct | +| *(unset)* | Clerk Account Portal — the stranding above | +| `true`, or a bare `2plot.ai/onboarding` | `https://leaflet.2plot.dev/true?returnTo=…` — resolved against **this** host, 404, sign-in impossible | + +`dash-clerk-auth` does not reject a bad value: it emits one `logger.warning` +and uses it regardless. `lib/auth.py` therefore prints its own `[auth] ⚠️` +line for both mistakes, alongside the rest of the boot diagnostics. + `dash-clerk-auth` reads the variable from the environment itself, so `lib/auth.py` does not pass it through; setting it on the service is the whole fix. `lib/auth.py` now **warns at boot** whenever satellite mode is on and this diff --git a/lib/auth.py b/lib/auth.py index 8237923..fbfa9ae 100644 --- a/lib/auth.py +++ b/lib/auth.py @@ -278,7 +278,8 @@ def register() -> bool: f"CLERK_SATELLITE_DOMAIN={SATELLITE_HOST} (and CLERK_IS_SATELLITE=true) " "or ClerkJS will fail with 'satellite needs a domain'." ) - if is_satellite and not (os.getenv("CLERK_SATELLITE_SIGN_IN_REDIRECT") or "").strip(): + sat_redirect = (os.getenv("CLERK_SATELLITE_SIGN_IN_REDIRECT") or "").strip() + if is_satellite and not sat_redirect: # The one Clerk misconfiguration with NO error on either side: the # user signs in successfully and is simply left on the primary. It # shipped that way here for weeks because nothing said so. Now @@ -289,6 +290,21 @@ def register() -> bool: "the hub, and users may not be returned to this site at all. " "Set it to https://2plot.ai/onboarding (see render.yaml)." ) + elif is_satellite and not sat_redirect.startswith(("http://", "https://")): + # It is a URL, not a flag, and the package does not reject a bad one: + # it logs a warning and uses the value anyway. `buildSatelliteRedirect` + # concatenates `?returnTo=`, so a non-absolute value is + # resolved against THIS host — CLERK_SATELLITE_SIGN_IN_REDIRECT=true + # sends the Sign In button to https:///true?returnTo=..., + # which is strictly worse than leaving it unset. The package's own + # warning goes to `logger`, several screens up in the boot output; + # this one prints where the rest of the [auth] diagnostics are. + print( + f"[auth] ⚠️ CLERK_SATELLITE_SIGN_IN_REDIRECT={sat_redirect!r} is not " + "an absolute http(s) URL. It is a DESTINATION, not a flag — the " + "Sign In button will navigate to a path on THIS host and 404. " + "Set it to https://2plot.ai/onboarding." + ) if is_satellite and sat_domain: _install_satellite_signin_delegation()