diff --git a/CLAUDE.md b/CLAUDE.md index b455318..f4be011 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,6 +68,40 @@ pnpm db:push # Push Drizzle schema to local PostgreSQL pnpm db:studio # Open Drizzle Studio (DB browser) ``` +### Local HTTPS dev (rare — most contributors never need this) + +The default dev loop runs the journal on plain HTTP at +`http://localhost:3000`. WebAuthn passkeys, magic links, sessions, the +Terms gate, SSE — everything works over HTTP because the WebAuthn spec +treats `localhost` as a secure context regardless of scheme. CI's e2e +suite runs over plain HTTP too. + +There is exactly one feature that requires local HTTPS: **Wahoo OAuth**. +Wahoo (and most OAuth providers) reject `http://` redirect URIs, so the +`/api/sync/connect/wahoo` callback flow can only complete against an +HTTPS dev server. To run that flow: + +```bash +HTTPS=1 ORIGIN=https://localhost:3000 pnpm --filter @trails-cool/journal dev +``` + +`HTTPS=1` enables the `@vitejs/plugin-basic-ssl` cert + the ALPN +HTTP/1.1 workaround in `apps/journal/vite.config.ts`. `ORIGIN` makes the +WebAuthn server expect the HTTPS origin (set this together with HTTPS=1 +or you'll get origin-mismatch errors). Use `pnpm --filter` (not +`pnpm dev`) because turbo doesn't pass `HTTPS` through unless added to +its `globalPassThroughEnv` — `pnpm --filter` bypasses turbo entirely. + +**Don't set `ORIGIN=https://localhost:3000` in your `apps/journal/.env` +unless you intend to always run with `HTTPS=1`.** Mismatched values +break the e2e suite and generic dev. See +`apps/journal/.env.example` for what each var means. + +If you find yourself wanting `HTTPS=1` for any reason other than Wahoo +testing, write it down here so the assumption stays auditable — +"everything but Wahoo works over HTTP locally" is what keeps CI and +local config symmetric. + ## Testing Strategy - **Unit tests** (Vitest + jsdom): For packages, components, utilities, and app logic. diff --git a/apps/journal/.env.example b/apps/journal/.env.example new file mode 100644 index 0000000..ef8718a --- /dev/null +++ b/apps/journal/.env.example @@ -0,0 +1,55 @@ +# Journal local-dev environment. +# Copy to `.env` (gitignored) and edit. Most contributors don't need +# anything in this file — the journal app boots on plain HTTP at +# http://localhost:3000 with sensible defaults. + +# ──────────────────────────────────────────────────────────────────── +# DO NOT SET unless you know you need it +# ──────────────────────────────────────────────────────────────────── +# +# `ORIGIN` is what the WebAuthn ceremony uses as `expectedOrigin`. +# When unset it defaults to `http://localhost:3000`, which is what +# Playwright sends and what the browser sees in plain-HTTP dev. +# +# The ONLY reason to set this is if you're running the dev server +# over HTTPS (see HTTPS=1 below) and want passkey registration to +# succeed against the HTTPS origin. If you set it to `https://...` +# without also running the server over HTTPS, you'll get +# "Unexpected registration response origin" errors in dev and the +# local e2e suite (which always hits HTTP) will fail registration. +# +# ORIGIN=https://localhost:3000 + +# Flagship marker. Renders the project marketing block on the +# anonymous home and gates a few "this is the canonical instance" +# behaviors. Self-hosted instances leave this unset; flagship CI +# sets it to `true`. Either is fine for local dev — set it if you +# want to see the full marketing layout, leave it unset if you +# want the self-host home view. +# IS_FLAGSHIP=true + +# ──────────────────────────────────────────────────────────────────── +# Wahoo OAuth (only needed when actively testing the Wahoo import) +# ──────────────────────────────────────────────────────────────────── +# +# Wahoo's OAuth flow is the one feature that genuinely needs HTTPS +# locally: the provider rejects `http://` redirect URIs. To exercise +# the connect/disconnect/import flow end-to-end, you need: +# +# 1. Local HTTPS dev server: run `HTTPS=1 pnpm --filter +# @trails-cool/journal dev` (the basic-ssl plugin in +# vite.config.ts wires up the cert; see the comment there for +# the ALPN workaround). +# 2. ORIGIN=https://localhost:3000 (uncomment above). +# 3. Wahoo client credentials below, and a redirect URI of +# `https://localhost:3000/api/sync/callback/wahoo` registered +# in your Wahoo developer dashboard. +# +# WAHOO_CLIENT_ID= +# WAHOO_CLIENT_SECRET= +# WAHOO_WEBHOOK_TOKEN= + +# Integration test secret (only needed if running the integration +# test suite that drives the API directly). Generate with +# `openssl rand -hex 32`. +# INTEGRATION_SECRET= diff --git a/apps/journal/vite.config.ts b/apps/journal/vite.config.ts index 766e309..3875fcb 100644 --- a/apps/journal/vite.config.ts +++ b/apps/journal/vite.config.ts @@ -11,6 +11,26 @@ export default defineConfig({ plugins: [ tailwindcss(), reactRouter(), + // Local HTTPS dev is OPT-IN via `HTTPS=1`. Plain HTTP is the default + // and the right choice for ~everything: WebAuthn (passkeys) treats + // `localhost` as a secure context regardless of scheme, magic links + // work over HTTP, sessions / Terms gate / SSE all work over HTTP. + // CI doesn't set HTTPS=1 either — the e2e suite runs against the + // production `react-router-serve` build over plain HTTP and passes + // cleanly. So most contributors should never touch this flag. + // + // The one case where you DO need HTTPS=1 locally: + // - Wahoo OAuth callback testing — Wahoo (and most OAuth + // providers) reject `http://` redirect URIs, so the + // /api/sync/connect/wahoo flow can only complete against an + // HTTPS dev server. See docs/tooling.md for the full + // command (`HTTPS=1 ORIGIN=https://localhost:3000 pnpm + // --filter @trails-cool/journal dev`) and gotchas. + // + // If you find yourself wanting HTTPS=1 for any other reason, write + // it down in docs/tooling.md — the assumption that "everything but + // Wahoo works over HTTP locally" is what keeps the CI/local + // configurations symmetric. ...(process.env.HTTPS === "1" ? [import("@vitejs/plugin-basic-ssl").then((m) => m.default())] : []), sentryVitePlugin({ org: "trails-qq", @@ -36,8 +56,8 @@ export default defineConfig({ // `singleFetchAction` compares `Origin` against the `Host` header, // which HTTP/2 replaces with `:authority` and Node doesn't // synthesize back. Restricting ALPN to `http/1.1` keeps the Host - // header intact and lets fetcher form submissions through. - // Plain HTTP dev (the default) is unaffected. + // header intact and lets fetcher form submissions through. Only + // matters when `HTTPS=1` is set — see the plugins comment above. https: process.env.HTTPS === "1" ? { ALPNProtocols: ["http/1.1"] } : undefined, }, });