Clerk frontend integration: one image, runtime-switched identity

- src/lib/auth/bearer.ts is the single identity dispatch for both
  server-side token consumers (RSC apiFetch and the /api proxy route):
  local -> httpOnly tc_session cookie; clerk -> Clerk getToken() session
  JWT. The Clerk SDK is imported lazily, so the air-gapped/local path
  never loads it
- Runtime env (AUTH_MODE / CLERK_PUBLISHABLE_KEY / CLERK_SECRET_KEY),
  deliberately NOT build-time NEXT_PUBLIC_*: the same standalone image
  serves both deployment targets
- Conditional <ClerkProvider> in the root layout (publishableKey passed
  at render from runtime env); /login renders Clerk's <SignIn /> in
  clerk mode and the local form otherwise; proxy.ts middleware delegates
  to clerkMiddleware() only when active
- Helm: frontend deployment injects the Clerk keys from a Secret when
  auth.mode=clerk
- mode.ts unit-tested (default local, exact-match clerk, loud failure
  without the publishable key); the local path stays proven by all 29
  journeys; the Clerk branch is thin delegation to the SDK, exercised in
  deployment smoke per docs/clerk.md

157 Rust + 68 frontend tests + 29 Playwright journeys.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-10 10:36:51 -05:00
co-authored by Claude Fable 5
parent cbc8d35a2e
commit ceca21ca79
14 changed files with 302 additions and 22 deletions
@@ -25,6 +25,20 @@ spec:
env:
- name: API_ORIGIN
value: http://teamclaw-server:8080
{{- if eq .Values.auth.mode "clerk" }}
- name: AUTH_MODE
value: clerk
- name: CLERK_PUBLISHABLE_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.auth.clerkSecretName }}
key: publishable-key
- name: CLERK_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.auth.clerkSecretName }}
key: secret-key
{{- end }}
resources: {{- toYaml .Values.frontend.resources | nindent 12 }}
securityContext:
allowPrivilegeEscalation: false