Skip to content

Fix OAuth callback redirect behind reverse proxy#15

Open
jonasRoX wants to merge 58 commits into
moleculeprotocol:mainfrom
roxhealth:main
Open

Fix OAuth callback redirect behind reverse proxy#15
jonasRoX wants to merge 58 commits into
moleculeprotocol:mainfrom
roxhealth:main

Conversation

@jonasRoX

Copy link
Copy Markdown

Summary

When the app runs behind a reverse proxy (e.g. AWS ALB), request.url in the OAuth callback handler resolves to the internal container address (http://localhost:3000) rather than the public domain. This causes users to be redirected to localhost:3000 after completing Google OAuth sign-in.

The fix uses NEXT_PUBLIC_SITE_URL (if set) or the X-Forwarded-Host header as the redirect origin, falling back to the Host header.

Changes

  • src/app/auth/callback/route.ts: Construct redirect URL from NEXT_PUBLIC_SITE_URL or forwarded headers instead of request.url

Testing

  • Tested on AWS ECS Fargate behind an ALB with Google OAuth — users are now correctly redirected to the public domain after sign-in
  • No impact when running locally (NEXT_PUBLIC_SITE_URL is typically not set in dev, so it falls back to Host header which is localhost:3000 as expected)

Behind the ALB, request.url resolves to localhost:3000. Use
NEXT_PUBLIC_SITE_URL (or X-Forwarded-Host) as the origin for
the post-OAuth redirect so users return to beach.roxhealth.net.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented Apr 11, 2026

Copy link
Copy Markdown

@jonasRoX is attempting to deploy a commit to the Molecule Team on Vercel.

A member of the Team first needs to authorize it.

jonasRoX and others added 28 commits April 11, 2026 23:28
Same localhost:3000 issue as the OAuth callback — use
NEXT_PUBLIC_SITE_URL or X-Forwarded-Host for the redirect origin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The updateSession utility existed but was never wired into
Next.js middleware, so server components couldn't read the
auth session from cookies. This caused the navbar to always
show the logged-out state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…resh

Next.js 16 uses proxy.ts instead of middleware.ts, and it already
calls updateSession. The middleware.ts was causing a build error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without force-dynamic, Next.js can cache the server-rendered layout
(including the Navbar auth check), causing the navbar to always show
the logged-out state regardless of the user's session cookies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The navbar issue was caused by a missing profile record, not
static rendering. Reverting to allow Next.js default caching.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Components now use the env var for skill command URLs and registry
base URLs, falling back to beach.science only if unset.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added three heartbeat setup options:
- Option A: Built-in heartbeat file (OpenClaw)
- Option B: Cron job (PicoClaw and similar)
- Option C: Manual check-in
Plus duplicate engagement prevention guidance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Skill files (skill.md, heartbeat.md, skill.json, skills.json) are now
served via API route handlers instead of as static files. The handlers
read the source from public/ and replace the canonical base URL
(https://beach.science) with NEXT_PUBLIC_SITE_URL at request time.

This allows self-hosted deployments to serve skill files that point
to their own domain without any build-time hacks or source modifications.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Next.js serves static files from public/ before checking route
handlers. Moving skill files to data/ lets the API routes at
/skill.md, /heartbeat.md etc. serve them with URL replacement.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Merges moleculeprotocol/science.beach main (53 commits) into our fork.

Breaking changes handled:
- DB migrations added to scripts/supabase-init.sql (idempotent)
- Kept custom skill-files.ts and route handlers (upstream deleted them)

Notable upstream additions:
- Upvote/downvote reactions system (value column on reactions)
- V2 design system reskin (WaveHeader, pixel art, MobileNavDrawer)
- ResearchersSidebar — top researchers ranked by score
- /docs and /docs/skills/* skill registry browser
- Cove required on new posts
- Feed sort and refresh improvements
- New @fontsource/quicksand dependency

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add cove_name to post examples in skill.md and heartbeat.md (V2 requires cove on all posts)
- Update heartbeat reaction example to include {"value": 1} body
- Bump skill version to 1.13.0 in skill.json and skills.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds an email + password auth form below the Google button, with a
toggle between Sign in and Sign up modes. Both use GoTrue server
actions; sign-up redirects to an email confirmation message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Server-side fetch to /api/internal/generate-infographic was routing
through NEXT_PUBLIC_SITE_URL (beach.roxhealth.net), hitting the
ALB Cognito rule and getting 302-redirected. Use localhost:3000
directly so internal calls never leave the container.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
skill.md was 21KB (~5,500 tokens), hitting Tier 1 Sonnet rate limits
(10K TPM) during agent registration. Reduced to 7.7KB (~1,900 tokens)
by cutting verbose sections while keeping all API endpoints intact.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Old limit: 5 per IP per hour — blocked all students on the same
university NAT when one agent retried multiple times.

New limits:
- 30 per IP per hour (shared network headroom)
- 10 per handle per hour (per-handle abuse protection)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Email/password auth never called /auth/callback so profiles were never
created, causing createPost to silently redirect to /login. ensureProfile
now runs after signUpWithEmail and signInWithEmail, mirroring the Google
OAuth callback behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ion guard, pending queue, and skill verify fixes

- Replace $BEACH_API_KEY env var with inline MEMORY.md extraction in all curl examples
  (env vars don't persist across PicoClaw exec calls — this was silencing 3 agents)
- Wrap registration in guard block to prevent re-registration from destroying session state
- Add Step 0 queue drain at heartbeat start to retry failed posts from previous sessions
- Add Draft and Queue Pattern section with pending_posts.json discipline
- Add explicit unknown/outdated/verified handling for skill verify with one-attempt-max rule
- Bump skill version to 1.15.0

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Registers all skills needed by the 10-agent pipeline in app/data/skills.json
so users can install them via a single prompt in any PicoClaw chat:
- 4 custom skills served from beach.science (gdpr-health-data, us-market-access,
  eu-health-economics, market-intelligence-reports)
- 10 OpenClaw Medical Skills installed from GitHub raw URLs

Adds market-intelligence-reports skill file to public/skills/ — discovers market
reports, assesses gating status, surfaces REPORT REQUEST blocks to users for
paywalled sources, and ingests user-provided content on next heartbeat.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add gdpr-health-data, us-market-access, and eu-health-economics skill
files so beach.science can serve them to privacy, payer-us, and payer-eu
agents via direct install curl.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Postgres trigger parses @handle from every comment body and inserts
  a notification row for each mentioned profile (skips self-mentions,
  unknown handles). Works for both human and agent comments.
- Markdown component pre-processes @handle into profile links before
  rendering — mentions appear as bold blue clickable links.
- NotificationBell component polls every 30s, shows unread count badge,
  opens dropdown with mention list, marks all as read on open.
- Navbar wires in NotificationBell for authenticated users.
- GET/PATCH /api/v1/notifications for fetching and marking read.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes TypeScript build error caused by missing type definition for the
notifications table added in the Supabase migration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New GET /api/v1/profiles/search?q= endpoint (prefix search, user-auth)
- New MentionTextArea component: detects @word at cursor, debounces
  fetch, shows dropdown with avatar + handle + name, keyboard nav
  (arrows/Enter/Tab/Escape), splices handle on select
- Replace TextArea with MentionTextArea in CommentSection (top-level
  comment + reply forms); use key-based remount to clear after submit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Agents must not post "I'm online / ready / monitoring / waiting" messages.
Added explicit prohibition to both skill.md content guidelines and
heartbeat.md posting section. Bump skill version to 1.17.0 so agents
re-fetch on next heartbeat.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- MentionTextArea: flip dropdown to bottom-full so it appears above
  the textarea, right where the @ is typed
- NotificationBell: switch from direct Supabase client query to
  GET /api/v1/notifications — server-side auth is more reliable and
  fixes 0-count bug caused by browser session not being picked up

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- NotificationBell now accepts userId prop (passed from server Navbar)
- Subscribes to postgres_changes INSERT on notifications table filtered
  by recipient_id — bell updates instantly when a new mention arrives
- Keeps API route fetch for initial load and after each new event
  (to get fully joined actor/post/comment data)
- Removes 30s polling interval
- Migration: adds notifications table to supabase_realtime publication

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
FloRox96 and others added 29 commits May 12, 2026 16:57
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Without middleware, access tokens expire after ~1 hour and server-side
getUser() calls in API routes return null, silently breaking notification
fetches and any other authenticated API routes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds id="comment-{id}" anchor to each comment node and updates the
notification bell link to include #comment-{id} fragment so clicking
a notification scrolls directly to the mention.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Session refresh was already handled by proxy.ts -> lib/supabase/middleware.ts.
The new middleware.ts caused a Next.js 16 build error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When navigating via #comment-{id} anchor:
- Force-uncollapses the target comment and all its ancestors
- Highlights the target with a subtle blue tint
- Smooth-scrolls to it after React finishes rendering

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- All comments mentioning the current user are always highlighted in blue
- hashchange listener handles clicking multiple notifications without reload
- useEffect in CommentNode reactively uncollapses on highlightedId change
  (fixes useState-only approach which only ran once on mount)
- currentUserHandle passed from page.tsx for mention detection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Next.js Link uses history.pushState for hash changes which does not
fire the browser hashchange event, so same-page notification clicks
were silently changing the URL without CommentSection responding.

NotificationBell now dispatches a 'comment-focus' CustomEvent on click.
CommentSection listens for it alongside the initial hash check on mount.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Agents embedding multi-line analysis in -d '{"body":"..."}' produce
invalid JSON (literal newlines are not allowed in JSON strings), causing
400 errors. The analysis was silently lost and agents fell back to
posting just a newline character, which passes min(1) validation.

Replace all comment curl examples in skill.md and heartbeat.md with the
python3 heredoc pattern that properly encodes any content via json.dumps.
Add explicit warning above the examples explaining why.

Bumps skill version to 1.17.0 so agents re-fetch on next heartbeat.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Raises the API comment body limit from 5,000 to 10,000 characters.
Adds a visible notice to the Comments section of skill.md so agents
know the limit exists and can split long outputs before hitting a
silent 400 rejection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- skill.md: add reply-vs-new-comment rule at top of Comments section
- heartbeat.md: add engagement guide row for direct-reply threading

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…bmc-canvas skill

- Add canvas_blocks JSONB migration (20260515120000)
- Extend CreatePostSchema with discriminated union for canvas type (9 required blocks)
- insertPost() writes canvas_blocks and sets image_status: pending for canvas posts
- triggerBmcGeneration() wires canvas posts to POST /api/internal/generate-bmc
- BMC system prompt (bmc.ts) + generateBmcImage() in gemini.ts using gemini-3-pro-image-preview
- generate-bmc route: validates canvas type, runs Gemini inside after(), uploads full + thumb WebP
- bmc-canvas skill.md: full OpenClaw SKILL.md with self-contained curl, 9-block table, rate limits
- FeedCard: Canvas badge for canvas posts, "Generating canvas..." loading state
- skill.md: canvas post type documented with complete curl example
- skill.json: bumped to 1.20.0; skills.json: bmc-canvas entry added

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…PI key absent

Module-level construction of GoogleGenAI throws immediately if GOOGLE_GEMINI_API
is unset, causing Next.js page-data collection to fail for both internal routes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Canvas posts now land in the same cove as the hypothesis thread, not
a separate "Business Model Canvas" cove. Agent fetches cove_id from
the hypothesis post and passes it to the canvas payload.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds img handler to Markdown component — images render full-width with
rounded corners and a cursor-zoom-in pointer; clicking opens the full
resolution in a new tab. Used by canvas agent to embed BMC images
directly in hypothesis thread comments.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ubmission

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add 'canvas' alongside 'hypothesis' wherever image display is gated
on post type, so canvas posts show their generated BMC image, loading
state, and error state on the detail page. Also update OG metadata to
include the canvas image URL.

Add migration 20260518000000_add_canvas_type.sql documenting the
constraint fix already applied to the live DB.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…mission

Before writing the payload, the canvas agent must now reconcile any
metric that appears with different values across blocks (CAC, conversion
rates, timelines, market size). Canonical value priority: most recent
agent update wins; if agents genuinely disagree, use the widest
defensible range and flag it in the body summary.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… only on new activity

Rewrites heartbeat.md (362 → 89 lines) into a cheap-by-default protocol. Root
cause of LLM cost blowout: every heartbeat drained a queue, re-fetched and
hash-verified all skills, loaded the full feed, and read full comment threads —
all re-sent across up to 50 iterations, producing 50-138k-token calls (~$4 per
heartbeat on Sonnet/Opus).

New flow:
- Phase 1 triage: one python block fetches the feed and diffs each thread's
  comment_count against ~/.picoclaw/workspace/heartbeat_seen.json. The raw feed
  is processed inside the script so it never enters the LLM context — the agent
  only sees a short HEARTBEAT_OK / ACTIONABLE verdict. No new activity → exit in
  ~2 steps and a few hundred tokens.
- Phase 2: read and act on ONLY the threads that changed, per AGENT.md rules.
- Removed per-heartbeat skill verification entirely (skills are reinstalled on
  every container boot — runtime verification was pure waste, Bug 5).

comment_count is the right signal: it bumps on new comments, unlike post
updated_at (which doesn't). Expect ~10x+ cost cut on the ~530 daily no-op
heartbeats while preserving full work on real activity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eed post) (#2)

The only way to produce a BMC image was to create a `canvas`-type post, so
agents ended up with two artifacts per hypothesis: the in-thread synthesis
comment AND a separate standalone canvas post. This adds an endpoint that
renders the same nine-block image WITHOUT creating any post and returns the
public image URL synchronously, so an agent can embed it directly in its
single hypothesis-thread comment (![BMC](url)).

- POST /api/v1/bmc-image { canvas_blocks } -> 200 { image_url } | 502 { error }
- Auth via authenticateAgent (same beach_ key as posting); per-agent rate limit.
- Reuses generateBmcImage (gemini-3-pro-image-preview) + sharp webp; uploads to
  the infographics bucket under bmc-standalone/<uuid>.webp; returns getPublicUrl.
- Export CanvasBlocksSchema for reuse by the endpoint's request schema.
- Document the endpoint in data/skill.md; bump skill.json 1.21.0 -> 1.22.0.

The existing canvas-post flow (POST /api/v1/posts type=canvas + generate-bmc)
is left intact for backward compatibility.

Co-authored-by: JesusJonesDE <6793481+JesusJonesDE@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…et (#3)

Adds a public credit-balance widget to the navbar. A cached Next.js route
handler (GET /api/openrouter/credits) reads the OpenRouter management key
server-side, calls GET /api/v1/credits, and returns only remaining credits
(total_credits − total_usage) — totals never leave the server. Results are
cached ~60s so anonymous traffic can't hammer OpenRouter, with a 5s upstream
timeout and graceful degradation ({ available: false }) when the key is unset
or the upstream fails. The client badge polls every ~60s and hides while
loading or unavailable.

OPENROUTER_MANAGEMENT_KEY is optional in the server env schema so a missing
key never breaks app boot. Documented in a new .env.example.

Co-authored-by: JesusJonesDE <6793481+JesusJonesDE@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the faint outlined badge with a filled smoke-7 chip, bold uppercase
"Credits" label and bold mono value for readability next to the nav buttons.

Co-authored-by: JesusJonesDE <6793481+JesusJonesDE@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er, unsubscribe) (#5)

* feat(email): SES sender + branded HTML notification template

Adds the email layer for notifications:
- template.ts: email-client-safe (table layout, inline styles, hidden
  preheader) branded HTML template + plain-text fallback.
- ses.ts: SESv2 sender using SES_* env creds; no-op + graceful when unset.
- notifications.ts: maps notification type → email content (mention,
  bmc_created, generic fallback) and sends.
- env: optional SES_REGION/SES_ACCESS_KEY_ID/SES_SECRET_ACCESS_KEY/EMAIL_FROM.

Wiring (poller, DB columns, BMC hook, unsubscribe) lands in follow-up commits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(email): notification email pipeline (poller, BMC trigger, unsubscribe)

- DB migration: notifications.email_sent_at, profiles.email_notifications
  (default on) + unsubscribe_token (+ indexes); database.types updated.
- In-process poller (instrumentation.ts → email/poller.ts): every ~30s claims
  unsent notifications atomically, resolves recipient email + opt-in, sends via
  SES. No-op when SES unconfigured; releases claim to retry on send failure.
- BMC trigger: POST /api/v1/bmc-image accepts optional post_id; on success
  creates a 'bmc_created' notification for the hypothesis's initiating human
  (post author agent's claimer). Documented in skill.md; skill.json → 1.23.0.
- /unsubscribe?token= page flips email_notifications off via the stable token.

Verified end-to-end: real bmc_created email rendered + sent via SES to a test
inbox using the app's own send path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: JesusJonesDE <6793481+JesusJonesDE@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- BMC notifications now fire reliably regardless of agent adoption: when an
  agent comment embeds a BMC image (bmc-standalone URL) on a hypothesis, the
  server creates a deduped 'bmc_created' notification for the initiating human.
  Shared helper createBmcCreatedNotification() (deduped per post) is used by
  both the comment-detection path and the explicit /api/v1/bmc-image post_id path.
- Template: Gmail-hardening — bgcolor attributes (Gmail drops CSS background,
  which showed a grey gutter), margin:0 auto + Outlook/Gmail ghost-table wrapper.

Co-authored-by: JesusJonesDE <6793481+JesusJonesDE@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…xel art) (#7)

Rewrites the infographic generation prompt: drops the 16-bit retro pixel-art /
beach game aesthetic in favor of a bold, high-energy flat-vector digital-health
poster matching the ROX Health brand — electric cobalt (#0040FF), coral-red
(#FF3860) accent, charcoal/white, heavy uppercase geometric headlines, diagonal
color-blocking, and oversized hero stat callouts. Keeps the proven split-panel
problem/solution structure and big-validation-stat emphasis from the original.

Co-authored-by: JesusJonesDE <6793481+JesusJonesDE@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Composite the ROX wordmark onto the bottom-right of every generated post
  infographic, on a small white rounded plate so it stays legible on any
  background (addRoxLogo via sharp; best-effort, falls back to unbranded).
- Prompt: reserve a clean empty bottom-right area and forbid model-drawn
  logos/footer ribbons, so the overlaid logo never collides with content.
- Drop the obsolete pixel-art 'nearest' thumbnail kernel.

Verified across multiple generated samples: small logo, no overlap.

Co-authored-by: JesusJonesDE <6793481+JesusJonesDE@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ew (#9)

Collapsed comment previews showed raw markdown (e.g. "![Business Model
Canvas](https://…)", "## …", "**…**"). Now the preview shows a small image
thumbnail when the comment embeds an image, plus a markdown-stripped one-line
snippet (image → alt text, links → text, symbols removed).

Co-authored-by: JesusJonesDE <6793481+JesusJonesDE@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restyles the BMC image prompt from the old Beach Science teal/sand palette to
the modern ROX look: white canvas, cobalt (#0040FF) block header bars with white
labels, charcoal content, coral (#FF3860) accent on Revenue Streams + the title
rule, cobalt outer frame, bold geometric sans-serif, flat (no shadows). Keeps the
standard Osterwalder 9-block grid. Matches the restyled post infographics.

Co-authored-by: JesusJonesDE <6793481+JesusJonesDE@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

3 participants