# Shell: Auth and session

Source: `apps/oshun/admin/src/lib/session-cookie.ts`,
`apps/oshun/admin/src/lib/server-session.ts`,
`apps/oshun/admin/src/lib/customer-session.ts`,
`apps/oshun/admin/src/app/handoff/page.tsx`,
`apps/oshun/admin/src/app/handoff/HandoffLauncher.tsx`,
`apps/oshun/admin/src/app/unauthorized/page.tsx`,
`libs/oshun/navigation/src/admin-ia.ts`
(`OSHUN_ADMIN_WORKSPACE_MODEL.requiredScopes`, `canEnterAdminWorkspace`)

How an operator becomes an operator — the privileged handoff, the session
cookie, the scopes, and the unauthorized fallback.

## Three cookies you need to know

| Cookie                                          | Owner  | Purpose                                                                                            |
| ----------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------- |
| `OSHUN_ADMIN_SESSION_COOKIE_NAME` (admin)       | Admin  | Gates every non-public admin path via middleware                                                   |
| `OSHUN_CONSUMER_SESSION_COOKIE_NAME` (consumer) | V1 PWA | Customer session; admin's `/handoff` reads it to know whether to offer a privileged-handoff prompt |

The admin and consumer cookies are scoped separately. A customer session alone
does NOT confer admin access — explicit privileged handoff is required.

## Auth states

- [ ] **anonymous** — no cookies; gated paths bounce to `/unauthorized`
- [ ] **customer-only** — only the consumer cookie; visiting an admin path
      bounces to `/unauthorized?reason=missing-session`; visiting `/handoff`
      offers the privileged-handoff flow
- [ ] **admin (valid)** — admin cookie parses cleanly; can access any path their
      `scopes` allow
- [ ] **admin (invalid)** — admin cookie present but `parseAdminSessionToken`
      returns null; middleware deletes the cookie and bounces to
      `/unauthorized?reason=invalid-session`
- [ ] **admin + insufficient scope for workspace** — middleware lets the request
      through (it only checks for _any_ valid admin session), but the page's
      `WorkspaceEntryPoint` renders an "Access not granted" panel and the BFF
      returns `accessible: false`

## Sign-in flow: privileged handoff

`apps/oshun/admin/src/app/handoff/page.tsx`

- [ ] **Visit `/handoff`** — public path (no admin session required)
- [ ] **Has admin session already** — `parseAdminSessionToken(adminToken)`
      returns valid; redirect to `returnTo ?? '/'`
- [ ] **No admin session** — render `<HandoffLauncher>` with:
  - **Eyebrow**: "Oshun · Operations"
  - **Heading**: "Privileged handoff"
  - **Body**: explanation that admin cockpit is entered through explicit
    privileged handoff from consumer
  - `hasCustomerSession` flag (from `OSHUN_CONSUMER_SESSION_COOKIE_NAME`
    presence)
  - `requestId` (from `x-request-id` header) for support reference
- [ ] **`returnTo` query param** — sanitized via `sanitizeReturnTo` (verify the
      rules — likely path-only, no protocol/host)
- [ ] **`reason` query param** — surfaced for the operator to see why they hit
      handoff

The handoff itself is an out-of-band action (likely a back-channel admin login
or a one-time link). The page describes the flow but the admin cookie is set by
a separate endpoint (verify which).

## Sign-out

The admin app does not have a dedicated sign-out route in
`apps/oshun/admin/src/app`. Sign-out is presumably handled by:

- Deleting `OSHUN_ADMIN_SESSION_COOKIE_NAME` via an API route (verify
  `apps/oshun/admin/src/app/api/admin/*`)
- OR a header action wired through the studio governance flow

Open question: where does the operator sign out from? Flag in matrix.

## Session refresh

The admin cookie is parsed on every request via `parseAdminSessionToken`. No
refresh path is visible in `session-cookie.ts` — token rotation likely happens
out-of-band on each privileged handoff. Open question: does the token expire?

## Scopes and workspace access

Each workspace declares `requiredScopes` in `OSHUN_ADMIN_WORKSPACE_MODEL`.
Examples:

- `dashboard` — `['admin:*', 'admin:studio']`
- `inbox` — `['admin:*', 'admin:studio']`
- `review` — `['admin:*', 'admin:studio', 'admin:workspace:review']`
- `trust-safety` — `['admin:*', 'admin:studio', 'admin:workspace:moderation']`
- `incidents` — `['admin:*', 'admin:studio', 'admin:workspace:incident']`
- `personas` — `['admin:*', 'admin:studio', 'admin:workspace:persona']`
- `models` — `['admin:*', 'admin:studio', 'admin:workspace:model']`
- `editorial` — `['admin:*', 'admin:studio', 'admin:workspace:editorial']`
- `research-integrity` —
  `['admin:*', 'admin:studio', 'admin:workspace:research-integrity']`
- `rights` — `['admin:*', 'admin:studio', 'admin:workspace:rights']`
- `support` — `['admin:*', 'admin:studio', 'admin:workspace:support']`
- `privacy` — `['admin:*', 'admin:studio', 'admin:workspace:privacy']`
- `policy` — `['admin:*', 'admin:studio', 'admin:workspace:policy']`
- `lilith` — `['admin:*', 'admin:studio', 'admin:workspace:lilith']`

`admin:*` is the superuser scope; `admin:studio` is the broad studio-operator
scope; workspace-specific scopes (`admin:workspace:*`) allow finer-grained
delegation. `canEnterAdminWorkspace(scopes, workspaceId)` returns true if any of
the workspace's `requiredScopes` is present.

### Scopes test matrix

| Operator scopes                                            | Can enter dashboard | Can enter review | Can enter trust-safety |
| ---------------------------------------------------------- | ------------------- | ---------------- | ---------------------- |
| `['admin:*']`                                              | ✓                   | ✓                | ✓                      |
| `['admin:studio']`                                         | ✓                   | ✓                | ✓                      |
| `['admin:workspace:review']`                               | ✗                   | ✓                | ✗                      |
| `['admin:workspace:moderation']`                           | ✗                   | ✗                | ✓                      |
| `['admin:workspace:review', 'admin:workspace:moderation']` | ✗                   | ✓                | ✓                      |
| `[]`                                                       | ✗                   | ✗                | ✗                      |

The above is derived from how the `requiredScopes` arrays are written — verify
with the canonical `canEnterAdminWorkspace` test suite at
`libs/oshun/navigation/src/admin-ia.test.ts`.

## Unauthorized page (`/unauthorized`)

`apps/oshun/admin/src/app/unauthorized/page.tsx`

Three known reasons (from `REASON_COPY`):

- [ ] **`missing-session`** — "Admin session required" heading; body tells
      operator to use privileged handoff and that customer sessions cannot enter
      admin routes
- [ ] **`invalid-session`** — "Admin session expired or invalid" heading; body
      tells operator to request a new privileged handoff
- [ ] **`forbidden-workspace`** — "Workspace access denied" heading; body tells
      operator their role doesn't grant the workspace scope

`returnTo` query param is preserved so the operator can resume after handoff.

## Cross-references

- [02-routing-layouts.md](./02-routing-layouts.md) — middleware that enforces
  the session
- [04-workspace-pattern.md](./04-workspace-pattern.md) — how individual pages
  enforce scope after middleware passes
- V1 PWA equivalent:
  [`../../WALKTHROUGH/shell/04-auth-session.md`](../../WALKTHROUGH/shell/04-auth-session.md)
- Session cookie module: `apps/oshun/admin/src/lib/session-cookie.ts`
- Server session module: `apps/oshun/admin/src/lib/server-session.ts`

## Open questions / known gaps

- [ ] Document the upstream service that issues the admin session token after
      privileged handoff
- [ ] Confirm token expiry / refresh policy
- [ ] Locate the sign-out path (likely an `/api/admin/*` route)
- [ ] Verify `sanitizeReturnTo` rules in `handoff/page.tsx`
- [ ] Document how `forbidden-workspace` is triggered — middleware doesn't check
      scope, so this reason originates from a page or BFF
