Skip to content

Enable managed auth in production (validated end-to-end, plus two fixes) - #39

Merged
twilson63 merged 3 commits into
mainfrom
enable-managed-auth
Jul 30, 2026
Merged

Enable managed auth in production (validated end-to-end, plus two fixes)#39
twilson63 merged 3 commits into
mainfrom
enable-managed-auth

Conversation

@twilson63

@twilson63 twilson63 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Flips VITE_MANAGED_AUTH to "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=true production build behind the same COEP: require-corp / COOP: same-origin headers render.yaml sets, and drove it in Chrome:

Check Result
crossOriginIsolated true — WebContainers still work
Clerk chunks load yes, same-origin
External origins requested none
Blocked by COEP none

The no-rhc + bundled @clerk/ui approach 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-origin severs window.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.

Previously open Result
OAuth redirect round-trip ✅ Google sign-in leaves the origin and returns in the same tab, session intact
Managed agent loop (/api/agent/step) ✅ one prompt → 9 tool steps → 2 files → self-verified build → app rendered in a live WebContainer
Lazy provisioning ✅ key minted with limit_reset: monthly, encrypted at rest (101-byte Uint8Array, no sk-or- anywhere)
Spend attribution ✅ $0.0122 landed on that user's key, not the provisioning key
Direct provider traffic zero browser requests to openrouter.ai — only /api/* and Clerk
Isolation under real Clerk crossOriginIsolated === true, zero CDN requests, zero COEP errors

Full 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 clears user synchronously 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 goes activeremoved.

Fix 2 — build-db had two schema owners

env-store.ts opened build-db at version 1 while projects.ts owned it at 2 — the exact hazard projects.ts's own comment warns about. Latent rather than live, since nothing imports env-store today.

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 the onblocked / onversionchange handling 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-id intact.

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-store suite went from 3 tests to 14: it previously carried ~50 lines of unused fake-DB scaffolding and a stub whose open() ignored the version argument, so all four storage functions had zero coverage while appearing tested.

What this still does NOT validate

  • Webhook-delivered provisioning. Clerk can't reach localhost, so /webhooks/clerk was disabled and only the lazy fallback ran. user.deleted cleanup is unexercised.
  • The production Clerk instance. Testing used a dev instance, whose Google connection uses Clerk's shared credentials. Production needs its own — see the checklist below.
  • The real Render /api/* rewrite — stood in for by the Vite proxy, equivalent in shape, not the same infrastructure.
  • 402 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 users table: the server decides from its own DB and never asks OpenRouter whether a build-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.yaml already 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

  1. VITE_CLERK_PUBLISHABLE_KEY must be set on the Render service — unset silently falls back to BYOK, and everything above would be testing the wrong path
  2. Google must be enabled on the production Clerk instance, with your own Google OAuth credentials rather than Clerk's shared dev ones
  3. Sign in with Google → lands back in the app, not on the Account Portal
  4. First turn provisions an OpenRouter key (server log: lazy-provisioning user …)
  5. A multi-step turn completes — exercises the step-token HMAC round-trip for real
  6. web_search needs BRAVE_SEARCH_API_KEY or it reports itself unavailable (web_fetch/web_post are unaffected)
  7. web_post approval — the one gated tool

🤖 Generated with Claude Code

https://claude.ai/code/session_01CwNJQ4sAbsG9ZgoioFaSLr

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 hyperio-mc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

twilson63 and others added 2 commits July 30, 2026 16:50
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
@twilson63
twilson63 temporarily deployed to enable-managed-auth - build PR #39 July 30, 2026 20:51 — with Render Destroyed
@twilson63 twilson63 changed the title Enable managed auth in production Enable managed auth in production (validated end-to-end, plus two fixes) Jul 30, 2026
@twilson63
twilson63 merged commit 922717f into main Jul 30, 2026
3 checks passed
twilson63 added a commit that referenced this pull request Jul 31, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants