Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/references/GLOSSARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ Hand-written TypeScript interface/type shaping data between layers (e.g. `LabelD
MP OAuth endpoint at `/oauth/connect/endsession` that ends the SSO session and redirects to a pre-registered `post_logout_redirect_uri`. The app always sends `client_id` (the OIDC sign-in client) and adds `id_token_hint` when the process that handled sign-in still holds the id_token.

**Not to be confused with:** Better Auth's local `signOut` (only clears the BA cookie).
**Defined in:** `src/components/user-menu/actions.ts` (`handleSignOut`)
**See:** `components/user-menu.md`
**Defined in:** `src/components/shared-actions/sign-out.ts` (`handleSignOut`)
**See:** `auth/oauth-flow.md` § Sign-out flow

## escapeFilterString (aliases: MP filter quote escape)

Expand Down
4 changes: 2 additions & 2 deletions .claude/references/GOTCHAS.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,15 @@ Known pitfalls for agents working on MPNext-Tools. Each entry has a symptom-firs
**Symptom:** Clicking sign-out in prod lands the user at `http://localhost:3000` (or some other wrong base URL).
**Root cause:** `handleSignOut` uses `process.env.BETTER_AUTH_URL || process.env.NEXTAUTH_URL || 'http://localhost:3000'`. If both env vars are unset in prod, the literal fallback fires silently.
**Fix:** Fixed — `handleSignOut` reads the origin through `getAuthBaseUrl()`, which throws when unset; there is no localhost fallback.
**Enforced where:** `src/components/user-menu/actions.ts` (`getAuthBaseUrl()` / `getMpBaseUrl()`), `src/lib/env.ts`.
**Enforced where:** `src/components/shared-actions/sign-out.ts` (`getAuthBaseUrl()` / `getMpBaseUrl()`), `src/lib/env.ts`.
**Related:** [auth/](auth/)

### GOTCHA-006: User appears to sign out but is silently re-authenticated (logout loop)

**Symptom:** Clicking sign-out clears the cookie, but the OAuth IDP immediately re-signs-in the user because the redirect back bounces into `/signin` which rehydrates the session.
**Root cause:** The `post_logout_redirect_uri` value sent by `handleSignOut` is not registered on the MP OAuth client's allow-list, so MP ignores it and issues a fresh session.
**Fix:** Register the exact value of `BETTER_AUTH_URL` as a post-logout redirect URI on the MP OAuth client. The value must match byte-for-byte (no trailing slash drift).
**Enforced where:** `src/components/user-menu/actions.ts:18-23`.
**Enforced where:** `src/components/shared-actions/sign-out.ts` (`handleSignOut`).
**Related:** [auth/oauth-flow.md](auth/oauth-flow.md)

### GOTCHA-007: MP audit columns show wrong user (or Better Auth nanoid) in created_by/modified_by
Expand Down
4 changes: 2 additions & 2 deletions .claude/references/auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Better Auth (`1.7.x`) wired to Ministry Platform OIDC via the `genericOAuth` plu
| `src/app/signin/page.tsx` | Sign-in page — auto-redirects via `authClient.signIn.social({ provider: "ministryplatform" })` |
| `src/proxy.ts` | Route protection (session-cookie presence check + `x-pathname` forwarding) |
| `src/components/layout/auth-wrapper.tsx` | Server-component guard — redirects to `/signin` with `callbackUrl` |
| `src/components/user-menu/actions.ts` | `handleSignOut()` — clears the session, then `endsession` with `client_id` (+ `id_token_hint` when this process has it) |
| `src/components/shared-actions/sign-out.ts` | `handleSignOut()` — clears the session, then `endsession` with `client_id` (+ `id_token_hint` when this process has it) |
| `src/contexts/user-context.tsx` | `UserProvider` — loads MP profile client-side from `userGuid` |
| `src/contexts/session-context.tsx` | `useAppSession()` — thin wrapper around `authClient.useSession()` |
| `src/auth.test.ts` | Config pins, `customSession`, `mapProfileToUser` |
Expand All @@ -35,7 +35,7 @@ Better Auth (`1.7.x`) wired to Ministry Platform OIDC via the `genericOAuth` plu
| `src/test-utils/mock-oidc.ts` | Mock MP OIDC provider (per-endpoint failure modes, `signIdToken`) |
| `src/proxy.test.ts` | Proxy route-protection tests |
| `src/components/layout/auth-wrapper.test.tsx` | `AuthWrapper` redirect tests |
| `src/components/user-menu/actions.test.ts` | `handleSignOut` tests |
| `src/components/shared-actions/sign-out.test.ts` | `handleSignOut` tests |

## Related domains
- `../contexts/README.md` — `UserProvider` consumes `userGuid` from the session
Expand Down
15 changes: 8 additions & 7 deletions .claude/references/auth/oauth-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: OAuth Flow (genericOAuth, lazy id_token verification, OIDC endsession)
domain: auth
type: reference
applies_to: [src/lib/auth.ts, src/lib/env.ts, src/app/signin/page.tsx, src/app/api/auth/[...all]/route.ts, src/components/user-menu/actions.ts, src/components/user-menu/user-menu.tsx]
applies_to: [src/lib/auth.ts, src/lib/env.ts, src/app/signin/page.tsx, src/app/api/auth/[...all]/route.ts, src/components/shared-actions/sign-out.ts, src/app/session-error/page.tsx]
symbols: [auth, ministryPlatformProviderConfig, lazyIdTokenVerifier, sharedInstance, getMpClient, handleSignOut]
related: [sessions.md, user-identity.md, route-protection.md, ../security/README.md]
last_verified: 2026-10-10
Expand All @@ -17,8 +17,8 @@ id_token verification, profile mapping, and RP-initiated logout via `endsession`
- `src/lib/env.ts` — `getMpBaseUrl()`, `getAuthBaseUrl()`, `getMpClient()` (validated env readers)
- `src/app/signin/sign-in-content.tsx` — calls `authClient.signIn.social({ provider: "ministryplatform", callbackURL })`
- `src/app/api/auth/[...all]/route.ts` — deny-by-default allowlist in front of `toNextJsHandler(auth)`
- `src/components/user-menu/actions.ts` — `handleSignOut()` server action
- Tests: `src/auth.oidc-discovery.test.ts` (outages, lazy discovery), `src/auth.user-info.test.ts` (`getUserInfo` with signed tokens), `src/auth.id-token-sign-in.test.ts` (F12), `src/auth.shared-instance.test.ts`, `src/auth.test.ts` (config pins), `src/components/user-menu/actions.test.ts`; mock MP in `src/test-utils/mock-oidc.ts`
- `src/components/shared-actions/sign-out.ts` — `handleSignOut()` server action (the only caller is the `/session-error` form)
- Tests: `src/auth.oidc-discovery.test.ts` (outages, lazy discovery), `src/auth.user-info.test.ts` (`getUserInfo` with signed tokens), `src/auth.id-token-sign-in.test.ts` (F12), `src/auth.shared-instance.test.ts`, `src/auth.test.ts` (config pins), `src/components/shared-actions/sign-out.test.ts`; mock MP in `src/test-utils/mock-oidc.ts`

## Key concepts
- **No boot-time discovery (security Step 4, upstream MPNext issue #101).** The provider has explicit `authorizationUrl`, `tokenUrl` and `endSessionEndpoint` and **no `discoveryUrl`**, so building the auth instance — `next build`, cold starts, every page's session check — makes **no MP call** and the provider always registers. With `discoveryUrl`, better-auth 1.7 fetched discovery once at boot with no timeout or retry: one MP blip left `/sign-in/social` answering `404 PROVIDER_NOT_FOUND` until a restart, and a hung MP stalled every request ~300 s.
Expand Down Expand Up @@ -65,7 +65,7 @@ the outage lands on `/auth-error?error=unable_to_get_user_info` (retry copy),
and the next sign-in recovers on the same process. `PROVIDER_NOT_FOUND` /
`oauth_provider_not_found` now means misconfiguration only.

## Sign-out flow (`src/components/user-menu/actions.ts`)
## Sign-out flow (`src/components/shared-actions/sign-out.ts`)

1. `auth.api.signOut({ headers, body: { disableRedirect: true } })` — clears the
app session **first**, so a broken env below still signs the user out, and
Expand All @@ -76,9 +76,10 @@ and the next sign-in recovers on the same process. `PROVIDER_NOT_FOUND` /
`${MP}/oauth/connect/endsession?post_logout_redirect_uri=<BETTER_AUTH_URL>&client_id=<OIDC client>[&id_token_hint=…]`
— `post_logout_redirect_uri` exactly as registered (better-auth would add a
trailing `/` and drop `client_id` when it has a hint).
4. `redirect()` — a `NEXT_REDIRECT` throw. `user-menu.tsx` wraps the call in
try/catch with `unstable_rethrow(err)` **first**, then alerts on a genuine
failure (it used to fail silently).
4. `redirect()` — a `NEXT_REDIRECT` throw. Invoke it as a `<form action>` (as
`/session-error` does) so Next handles the redirect. A future client caller
that `await`s it in try/catch must call `unstable_rethrow(err)` **first**, or
it swallows the navigation as an "error".

**Limitation:** the id_token lives only in the in-memory account row of the
process that handled the callback. A sign-out on another serverless instance
Expand Down
6 changes: 3 additions & 3 deletions .claude/references/auth/user-identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ user: {
> `node_modules/better-auth/dist/db/schema.mjs`) no longer lets an
> `input: false` additionalField through when a value is supplied — in 1.6.11 it
> throws `"userGuid is not allowed to be set"`, so the OAuth-created user never
> gets a `userGuid`. Result: `session.user.userGuid` is `undefined` → blank
> avatar, dead user menu, broken MP profile lookups. There is no user-facing
> gets a `userGuid`. Result: `session.user.userGuid` is `undefined` → every
> tool redirects to `/session-error`, and MP profile lookups break. There is no user-facing
> form that sets this field (genericOAuth only; no email/password signup or
> update-user endpoint), so `input: true` carries no practical risk here.
> Guarded by `src/auth.test.ts` (runs the real better-auth parser against the
Expand All @@ -76,7 +76,7 @@ After **any** better-auth version change:
3. **Manual smoke test (required — nothing else catches this):** `npm run dev`,
**sign out and sign in fresh** (stale sessions predate any user-record change),
open `/api/auth/get-session`, and confirm `user.userGuid` is present. Confirm
the header avatar renders and the user menu opens with a working sign-out.
a tool loads (not `/session-error`).
4. If `userGuid` is missing, inspect `parseAdditionalUserInput` (formerly
`parseAdditionalUserInputFromProviderProfile`) in
`node_modules/better-auth/dist/db/schema.mjs`.
Expand Down
4 changes: 1 addition & 3 deletions .claude/references/components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,18 @@ Reference docs for `src/components/` — the tool layout framework, layout wrapp
| `group-wizard.md` | Multi-step group wizard (shard 13) | Editing the 6-step create/edit group flow |
| `dev-panel.md` | Localhost-only developer overlay (shard 14) | Understanding `ToolContainer` auto-rendering behavior |
| `field-management.md` | Drag-and-drop page field ordering (shard 16) | Working on MP page field layout UX |
| `user-menu.md` | User dropdown + OIDC sign-out (shard 17) | Working on sign-out or RP-initiated logout |

## Code surfaces
| Path | Role |
|------|------|
| `src/components/tool/` | `ToolContainer`, `ToolHeader`, `ToolFooter` — 4 files |
| `src/components/layout/` | `AuthWrapper` server component + test — 3 files |
| `src/components/ui/` | 22 shadcn/ui primitives |
| `src/components/shared-actions/` | Cross-feature server actions — 2 files (+ local `README.md`) |
| `src/components/shared-actions/` | Cross-feature server actions — `user.ts`, `domain.ts`, `sign-out.ts` + tests (+ local `README.md`) |
| `src/components/address-labels/` | Address-label feature — 13 files |
| `src/components/dev-panel/` | Dev overlay — 3 files + `panels/` subdir |
| `src/components/field-management/` | Field-order editor — 10 files |
| `src/components/group-wizard/` | Group wizard — 14 files |
| `src/components/user-menu/` | User menu — 4 files |

## Conventions (domain-wide)
- File names: `kebab-case.tsx` (e.g., `tool-container.tsx`)
Expand Down
4 changes: 3 additions & 1 deletion .claude/references/components/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ applies_to:
- src/components/layout/index.ts
- src/components/shared-actions/user.ts
- src/components/shared-actions/user.test.ts
- src/components/shared-actions/sign-out.ts
- src/components/shared-actions/README.md
symbols: [AuthWrapper, getCurrentUserProfile]
related:
Expand All @@ -19,14 +20,15 @@ last_verified: 2026-09-13
---

## Purpose
`AuthWrapper` is the server-side session gate used at the app-shell level; it redirects unauthenticated requests to `/signin` while preserving the original path+query as `callbackUrl`. `shared-actions/` holds server actions used across multiple features (currently just `getCurrentUserProfile`).
`AuthWrapper` is the server-side session gate used at the app-shell level; it redirects unauthenticated requests to `/signin` while preserving the original path+query as `callbackUrl`. `shared-actions/` holds server actions used across multiple features (`getCurrentUserProfile`, `getMpTimezone` in `domain.ts`, and `handleSignOut`).

## Files
- `src/components/layout/auth-wrapper.tsx` — server component, 31 lines
- `src/components/layout/auth-wrapper.test.tsx` — redirect + callback preservation tests
- `src/components/layout/index.ts` — barrel: `AuthWrapper`
- `src/components/shared-actions/user.ts` — `getCurrentUserProfile` server action
- `src/components/shared-actions/user.test.ts` — unit tests for the action
- `src/components/shared-actions/sign-out.ts` — `handleSignOut` server action (see `auth/oauth-flow.md` § Sign-out flow)
- `src/components/shared-actions/README.md` — when-to-shard-vs-colocate guidelines

## Key concepts
Expand Down
Loading
Loading