Enable managed auth in production (validated end-to-end, plus two fixes) - #39
Conversation
Flips VITE_MANAGED_AUTH to "true". Users sign in with Clerk and the server provisions a per-user OpenRouter key, so nobody pastes their own. Rollback is the same line back to "false" — one deploy, no data migration, since projects live in IndexedDB and are untouched either way. Also records the Phase 2 COEP spike result, open since June. Serving a real managed build behind the same COEP: require-corp / COOP: same-origin headers render.yaml sets and driving it in Chrome: crossOriginIsolated is true, Clerk's chunks load and initialize, ZERO external origins are requested, and nothing is blocked. The no-rhc + bundled @clerk/ui approach does what it was meant to. Two things must be true in the dashboards or this deploy is not what it looks like, and both are noted in render.yaml: VITE_CLERK_PUBLISHABLE_KEY must be set (unset silently falls back to BYOK, because isManagedAuthEnabled() requires BOTH), and Google must be enabled as a Clerk social connection — which on a production instance needs your own Google OAuth credentials. Still unvalidated: the OAuth redirect round-trip, and the managed agent loop itself. /api/agent/step and the three web tools have only ever seen fixtures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019gS8yjMedGgujEad6zRD7i
hyperio-mc
left a comment
There was a problem hiding this comment.
Review
This is the managed-auth production flip — one config value from false to true, plus updated documentation. Exactly the separation PR #37 recommended.
What's good
Minimal and correct. 2 files, +28/-5. The actual code change is a single value in render.yaml ("false" -> "true"). The rest is documentation updates that record the COEP spike results, what's validated vs not, and the rollback procedure.
COEP spike is thorough. Served a real VITE_MANAGED_AUTH=true production build behind the same COEP/COOP headers, verified crossOriginIsolated=true (WebContainers still work), Clerk chunks load same-origin, zero external origins requested, nothing blocked. The no-rhc + bundled @clerk/ui approach is validated.
Rollback is trivial. Flip the value back to "false" — one deploy, no data migration. Projects live in IndexedDB and are untouched. This is the right risk profile for a production flip.
The comment update is excellent. It documents what must be true in the dashboards (VITE_CLERK_PUBLISHABLE_KEY set, Google enabled as Clerk social connection with your own OAuth credentials), and notes that unset publishable key silently falls back to BYOK because isManagedAuthEnabled() requires both. Future operators won't be confused by a silent fallback.
Honest about gaps. The PR body explicitly lists what this does NOT validate: the OAuth redirect round-trip (can't be tested with placeholder keys) and the managed agent loop (only seen fixtures). The first-run checklist is concrete and actionable.
oauthFlow: 'redirect' already handled. COOP: same-origin severs window.opener, so popup OAuth can't report back. This was handled before — the redirect flow is already forced. Good that it was checked.
CI: all green
3 checks pass — gleam+vitest+build (32s), server (41s), smoke browser (1m31s).
Verdict
This is the cleanest possible production flip — one config value, thorough validation behind it, trivial rollback, honest documentation of remaining gaps. The first-run checklist is the right post-merge plan.
Looks good to merge.
Reviewed by MC Agent
The local smoke test found it: clicking "Sign out" closed the account panel and reloaded the page, but the session stayed alive. Clerk's own API still reported it `active`, with an expire_at a week out. A user signing out on a shared machine stayed signed in. Three correct-looking pieces raced. `managedSignOut()` fires `void signOut()` unawaited; Clerk clears `user` locally and synchronously *before* the revoke request lands; the boot gate's listener saw `!user` and reloaded immediately, and the navigation killed the request in flight. On reload Clerk restored the session from the still-valid cookie. Nothing looked wrong from the UI, which is why unit tests missed it. Only checking against Clerk's API rather than the screen could catch it. signOut() now guards with `signingOut`, awaits the revoke to completion, and owns the reload itself. The gate's listener still reloads for a session that ends elsewhere (expiry, another tab) — that path is unchanged. The new test fails against the pre-fix code, so it locks the bug out rather than merely passing beside the fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CwNJQ4sAbsG9ZgoioFaSLr
projects.ts has always claimed the schema in a comment: "build-db must be opened at ONE version, and two modules opening it at different versions throws on whichever is second." workspace-store.ts obeys it. env-store.ts did not — it opened the same database at version 1 while projects.ts owned it at 2, so once anything upgraded the database, env-store threw VersionError. Latent rather than live: nothing imports env-store today. Bumping env-store to 2 would have been the wrong fix. A database already at 2 never fires onupgradeneeded again, so the store would silently never be created — a quiet NotFoundError instead of a loud VersionError. The version has to advance, so it goes to 3 and the owner creates the store. env-store.ts now delegates to openBuildDb, the same one-liner workspace-store.ts already used. There is exactly one indexedDB.open for build-db in src/. A version bump also needs onblocked and onversionchange, which were missing: a connection open at an older version blocks the upgrade silently and forever. The pre-fix test run demonstrated it — one case failed by timing out at 5001ms, not by assertion. The old env-store test carried ~50 lines of fake-DB scaffolding no test used, and its stubbed open() ignored the version argument, so the four storage functions had zero coverage while appearing tested. Rewritten on fake-indexeddb (as projects.test.ts already did): 3 tests to 14, covering both module orderings and every historical database shape. Five fail against the pre-fix code. Verified against a real v2 database: upgrades to v3 with all projects, their files and messages, and current-project-id intact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CwNJQ4sAbsG9ZgoioFaSLr
Two holes the 922717f smoke failure exposed. Neither is the crash itself — that was a renderer death on the runner, not reproducible locally (37/37, three runs, same tree). The artifact meant to explain a smoke failure explains nothing. upload-artifact skips anything under a dot-directory unless include-hidden-files is set, and scripts/.smoke is exactly that. The artifact from the failing run was 247 bytes: vite.log, and none of the screenshots or video, despite the job log printing each path as it wrote them. So the one step that exists to diagnose a smoke failure silently discarded 100% of its evidence. Safe to enable: the path stays scoped to scripts/.smoke, the smokes run with VITE_MANAGED_AUTH=false, and the only credentials they inject are apiKey: 'sk-test' and the empty string. Nothing real renders on screen. Render auto-deployed main regardless of CI. The merge gate added in 3b3ecab guards the PR path; nothing guarded the merge itself, so PR #39 shipped managed auth to production while main's smoke job was still red — CI and the deploy racing rather than ordered. Both services now use autoDeployTrigger: checksPass, with the manual-deploy escape hatch noted so a red main cannot strand a hotfix. Deliberately not touched: the renderer crash. Its cause is unproven and tuning deviceScaleFactor or dropping the video would be guessing at it — and might mask it. Fix the evidence pipeline first; let the next occurrence say what actually happened. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CwNJQ4sAbsG9ZgoioFaSLr
Flips
VITE_MANAGED_AUTHto"true". Users sign in with Clerk (Google) and the server provisions a per-user OpenRouter key, so nobody pastes their own.Rollback is the same line back to
"false"— one deploy, no data migration. Projects live in IndexedDB and are untouched either way.The Phase 2 COEP spike passes
Open since June, and the thing gating this whole rollout. I served a real
VITE_MANAGED_AUTH=trueproduction build behind the sameCOEP: require-corp/COOP: same-originheadersrender.yamlsets, and drove it in Chrome:crossOriginIsolatedtrue— WebContainers still workThe
no-rhc+ bundled@clerk/uiapproach does exactly what it was meant to. Managed mode also builds cleanly: ~240 code-split chunks, 4.4 MB total, none of it in the entry chunk.Worth noting the sign-in gate already forces
oauthFlow: 'redirect'—COOP: same-originseverswindow.opener, so popup OAuth could never report back and Google sign-in would hang. That was handled before I got here.Update: the two open items are now validated, and cost two bug fixes
This PR previously listed the OAuth redirect round-trip and the managed agent loop as unvalidatable without real credentials. Both have now been run end-to-end against a real Clerk dev instance and a real OpenRouter provisioning key, driving Chrome through the whole funnel.
/api/agent/step)limit_reset: monthly, encrypted at rest (101-byteUint8Array, nosk-or-anywhere)openrouter.ai— only/api/*and ClerkcrossOriginIsolated === true, zero CDN requests, zero COEP errorsFull evidence:
docs/managed-auth-local-smoke-progress.html.Fix 1 — sign-out never revoked the session (security-relevant)
Clicking "Sign out" looked like it worked. It didn't: Clerk's API still reported the session
active, expiring a week out. A user signing out on a shared machine stayed signed in.managedSignOut()fires unawaited → Clerk clearsusersynchronously before the revoke request lands → the boot gate reloads on that event → the navigation kills the request in flight → the session returns from the still-valid cookie.Every piece looked correct in isolation, which is why unit tests missed it; only checking Clerk's API rather than the screen could catch it.
signOut()now guards, awaits the revoke, and owns the reload. Verified: session goesactive→removed.Fix 2 —
build-dbhad two schema ownersenv-store.tsopenedbuild-dbat version 1 whileprojects.tsowned it at 2 — the exact hazardprojects.ts's own comment warns about. Latent rather than live, since nothing importsenv-storetoday.Bumping the consumer to 2 would have been the wrong fix: a database already at 2 never fires
onupgradeneeded, so the store would silently never be created. The version advances to 3 and the owner creates it. Also adds theonblocked/onversionchangehandling a version bump requires — without it a stale tab deadlocks the upgrade forever, which the pre-fix test run demonstrated by timing out at 5001 ms.Verified against a real v2 database: upgrades to v3 with all 3 projects, their files, messages, and
current-project-idintact.Tests
810 green — 197 gleam + 304 vitest + 309 server (baseline 796). Both new test files were run against the pre-fix code and fail there, so they lock the bugs out rather than passing beside the fixes. The
env-storesuite went from 3 tests to 14: it previously carried ~50 lines of unused fake-DB scaffolding and a stub whoseopen()ignored the version argument, so all four storage functions had zero coverage while appearing tested.What this still does NOT validate
/webhooks/clerkwas disabled and only the lazy fallback ran.user.deletedcleanup is unexercised./api/*rewrite — stood in for by the Vite proxy, equivalent in shape, not the same infrastructure.budget_exhausted— would have meant burning $5 or minting a throwaway low-limit key.Known, not fixed
Provisioning is not idempotent across an empty/lost
userstable: the server decides from its own DB and never asks OpenRouter whether abuild-user-<clerk id>key already exists, so it mints a duplicate and orphans the old one. Demonstrated during this run — a local run created a second live key for a user who already had one.render.yamlalready flags the risk; this makes it concrete. Needs a product decision (reuse-by-name on provision, or a cleanup pass).First-run checklist after deploy
VITE_CLERK_PUBLISHABLE_KEYmust be set on the Render service — unset silently falls back to BYOK, and everything above would be testing the wrong pathlazy-provisioning user …)web_searchneedsBRAVE_SEARCH_API_KEYor it reports itself unavailable (web_fetch/web_postare unaffected)web_postapproval — the one gated tool🤖 Generated with Claude Code
https://claude.ai/code/session_01CwNJQ4sAbsG9ZgoioFaSLr