# Gateway Admin Token Recovery — Implementation Plan

**Mission:** `cli-unification-20260404`
**Task:** `CU-03-01` (planning only — no runtime code changes)
**Status:** Design locked (Session 1) — BetterAuth cookie-based recovery

---

## 1. Problem Statement

The gateway installer strands operators when the admin user exists but the admin
API token is missing. Concrete trigger:

- `~/.config/mosaic/gateway/meta.json` was deleted / regenerated.
- The installer was re-run after a previous successful bootstrap.

Flow today (`packages/mosaic/src/commands/gateway/install.ts:375-400`):

1. `bootstrapFirstUser` hits `GET /api/bootstrap/status`.
2. Server returns `needsSetup: false` because `users` count > 0.
3. Installer logs `Admin user already exists — skipping setup. (No admin token on file — sign in via the web UI to manage tokens.)` and returns.
4. The operator now has:
   - No token in `meta.json`.
   - No CLI path to mint a new one (`mosaic gateway <anything>` that needs the token fails).
   - `POST /api/bootstrap/setup` locked out — it only runs when `users` count is zero (`apps/gateway/src/admin/bootstrap.controller.ts:34-37`).
   - `POST /api/admin/tokens` gated by `AdminGuard` — requires either a bearer token (which they don't have) or a BetterAuth session (which they don't have in the CLI).

Dead end. The web UI is the only escape hatch today, and for headless installs even that may be inaccessible.

## 2. Design Summary

The BetterAuth session cookie is the authority. The operator runs
`mosaic gateway login` to sign in with email/password, which persists a session
cookie via `saveSession` (reusing `packages/mosaic/src/auth.ts`). With a valid
session, `mosaic gateway config recover-token` (stranded-operator entry point)
and `mosaic gateway config rotate-token` call the existing authenticated admin
endpoint `POST /api/admin/tokens` using the cookie, then persist the returned
plaintext to `meta.json` via `writeMeta`. **No new server endpoints are
required** — `AdminGuard` already accepts BetterAuth session cookies via its
`validateSession` path (`apps/gateway/src/admin/admin.guard.ts:90-120`).

## 3. Surface Contract

### 3.1 Server — no changes required

| Endpoint                       | Status          | Notes                                                                                                                    |
| ------------------------------ | --------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `POST /api/admin/tokens`       | **Reuse as-is** | `admin-tokens.controller.ts:46-72`. Returns `{ id, label, scope, expiresAt, lastUsedAt, createdAt, plaintext }`.         |
| `GET  /api/admin/tokens`       | **Reuse**       | Useful for `mosaic gateway config tokens list` follow-on (out of scope for CU-03-01, but trivial once auth path exists). |
| `DELETE /api/admin/tokens/:id` | **Reuse**       | Used by rotate flow for optional old-token revocation.                                                                   |
| `POST /api/bootstrap/setup`    | **Unchanged**   | Remains first-user-only; not part of recovery.                                                                           |

`AdminGuard.validateSession` takes BetterAuth cookies from `request.raw.headers`
via `fromNodeHeaders` and calls `auth.api.getSession({ headers })`. It also
enforces `role === 'admin'`. This is exactly the path the CLI will hit with
`Cookie: better-auth.session_token=...`.

**Confirmed feasible** during CU-03-01 investigation.

### 3.2 `mosaic gateway login`

Thin wrapper over the existing top-level `mosaic login`
(`packages/mosaic/src/cli.ts:42-76`) with gateway-specific defaults pulled from
`readMeta()`.

| Aspect              | Behavior                                                                                                                        |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Default gateway URL | `http://${meta.host}:${meta.port}` from `readMeta()`, fallback `http://localhost:14242`.                                        |
| Flow                | Prompt email + password -> `signIn()` -> `saveSession()`.                                                                       |
| Persistence         | `~/.mosaic/session.json` via existing `saveSession` (7-day expiry).                                                             |
| Decision            | **Thin wrapper**, not alias. Rationale: defaults differ (reads `meta.json`), and discoverability under `mosaic gateway --help`. |
| Implementation      | Share the sign-in logic by extracting a small `runLogin(gatewayUrl, email?, password?)` helper; both commands call it.          |

### 3.3 `mosaic gateway config rotate-token`

| Aspect       | Behavior                                                                                                                                                                                                                             |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Precondition | Valid session (via `loadSession` + `validateSession`). On failure, print: "Not signed in — run `mosaic gateway login`" and exit non-zero.                                                                                            |
| Request      | `POST ${gatewayUrl}/api/admin/tokens` with header `Cookie: <session>`, body `{ label: "CLI token (rotated YYYY-MM-DD)" }`.                                                                                                           |
| On success   | Read meta via `readMeta()`, set `meta.adminToken = plaintext`, `writeMeta(meta)`. Print the token banner (reuse `printAdminTokenBanner` shape).                                                                                      |
| Old token    | **Optional `--revoke-old`** flag. When set and a previous `meta.adminToken` existed, call `DELETE /api/admin/tokens/:id` after rotation. Requires listing first to find the id; punt to CU-03-02 decision. Document as nice-to-have. |
| Exit codes   | `0` success; `1` network error; `2` auth error; `3` server rejection.                                                                                                                                                                |

### 3.4 `mosaic gateway config recover-token`

Superset of `rotate-token` with an inline login nudge — the "stranded operator"
entry point.

| Step | Action                                                                                                                           |
| ---- | -------------------------------------------------------------------------------------------------------------------------------- |
| 1    | `readMeta()` — derive gateway URL. If meta is missing entirely, fall back to `--gateway` flag or default.                        |
| 2    | `loadSession(gatewayUrl)` then `validateSession`. If either fails, prompt inline: email + password -> `signIn` -> `saveSession`. |
| 3    | `POST /api/admin/tokens` with cookie, label `"Recovered via CLI YYYY-MM-DDTHH:mm"`.                                              |
| 4    | Persist plaintext to `meta.json` via `writeMeta`.                                                                                |
| 5    | Print the token banner and next-steps hints (e.g. `mosaic gateway status`).                                                      |
| 6    | Exit `0`.                                                                                                                        |

Key property: this command is **runnable with nothing but email+password in hand**.
It assumes the gateway is up but assumes no prior CLI session state.

### 3.5 File touch list (for CU-03-02..05 execution)

| File                                                  | Change                                                                                     |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `packages/mosaic/src/commands/gateway.ts`             | Register `login`, `config recover-token`, `config rotate-token` subcommands under `gw`.    |
| `packages/mosaic/src/commands/gateway/config.ts`      | Add `runRecoverToken`, `runRotateToken` handlers; export from module.                      |
| `packages/mosaic/src/commands/gateway/login.ts` (new) | Thin wrapper calling shared `runLogin` helper with meta-derived default URL.               |
| `packages/mosaic/src/auth.ts`                         | No change expected. Possibly export a `requireSession(gatewayUrl)` helper (reuse pattern). |
| `packages/mosaic/src/commands/gateway/install.ts`     | `bootstrapFirstUser` branch: "user exists, no token" -> offer recovery (see Section 4).    |

## 4. Installer Fix (CU-03-06 preview)

Current stranding point is `install.ts:388-395`. The fix:

```
if (!status.needsSetup) {
  if (meta.adminToken) {
    // unchanged — happy path
  } else {
    // NEW: prompt "Admin exists but no token on file. Recover now? [Y/n]"
    // If yes -> call runRecoverToken(gatewayUrl) inline (interactive):
    //   - prompt email + password
    //   - signIn -> saveSession
    //   - POST /api/admin/tokens
    //   - writeMeta(meta) with returned plaintext
    //   - print banner
    // If no -> print the current stranded message but include:
    //   "Run `mosaic gateway config recover-token` when ready."
  }
}
```

Shape notes (actual code lands in CU-03-06):

- Extract the recovery body so it can be called **both** from the standalone
  command and from `bootstrapFirstUser` without duplicating prompts.
- Reuse the same `rl` readline interface already open in `bootstrapFirstUser`
  for the inline prompts.
- Preserve non-interactive behavior: if `process.stdin.isTTY` is false, skip the
  prompt and emit the "run recover-token" hint only.

## 5. Test Strategy (CU-03-07 scope)

### 5.1 Happy paths

| Command                               | Scenario                                         | Expected                                                 |
| ------------------------------------- | ------------------------------------------------ | -------------------------------------------------------- |
| `mosaic gateway login`                | Valid creds                                      | `session.json` written, 7-day expiry, exit 0             |
| `mosaic gateway config rotate-token`  | Valid session, server reachable                  | `meta.json` updated, banner printed, new token usable    |
| `mosaic gateway config recover-token` | No session, valid creds, server reachable        | Prompts for creds, writes session + meta, exit 0         |
| Installer inline recovery             | Re-run after `meta.json` wipe, operator says yes | Meta restored, banner printed, no manual CLI step needed |

### 5.2 Error paths (must all produce actionable messages and non-zero exit)

| Failure                           | Expected handling                                                                 |
| --------------------------------- | --------------------------------------------------------------------------------- |
| Invalid email/password            | BetterAuth 401 surfaced as "Sign-in failed: <server message>", exit 2             |
| Expired stored session            | Recover command silently re-prompts; rotate command exits 2 with "run login" hint |
| Gateway down / connection refused | "Could not reach gateway at <url>" exit 1                                         |
| Server rejects token creation     | Print status + body excerpt, exit 3                                               |
| Meta file missing (recover)       | Fall back to `--gateway` flag or default; warn that meta will be created          |
| Non-admin user                    | `AdminGuard` 403 surfaced as "User is not an admin", exit 2                       |

### 5.3 Integration test (recommended)

Spin up gateway in test harness, create admin user via `/api/bootstrap/setup`,
wipe `meta.json`, invoke `mosaic gateway config recover-token` programmatically,
assert new `meta.adminToken` works against `GET /api/admin/tokens`.

## 6. Risks & Open Questions

| #   | Item                                                                                                                                                                                    | Severity | Mitigation                                                                                                     |
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------- |
| 1   | `AdminGuard.validateSession` calls `getSession` with `fromNodeHeaders(request.raw.headers)`. CLI sends `Cookie:` header only. Confirm BetterAuth reads from `Cookie`, not `Set-Cookie`. | Low      | Confirmed — `mosaic login` + `mosaic tui` already use this flow successfully (`cli.ts:137-181`).               |
| 2   | Session cookie local expiry (7d) vs BetterAuth server-side expiry may drift.                                                                                                            | Low      | `validateSession` hits `get-session`; handle 401 by re-prompting.                                              |
| 3   | Label collision / unbounded token growth if operators run `recover-token` repeatedly.                                                                                                   | Low      | Include ISO timestamp in label. Optional `--revoke-old` in CU-03-02. Add `tokens list/prune` later.            |
| 4   | `mosaic login` exists at top level and `mosaic gateway login` is a wrapper — risk of confusion.                                                                                         | Low      | Document that `gateway login` is the preferred entry for gateway operators; top-level stays for compatibility. |
| 5   | `meta.json` write is not atomic. Crash between token creation and `writeMeta` leaves an orphan token server-side with no plaintext on disk.                                             | Medium   | Accept for now — re-running `recover-token` mints a fresh token. Document as known limitation.                 |
| 6   | Non-TTY installer runs (CI, headless provisioners) cannot prompt for creds interactively.                                                                                               | Medium   | Installer inline recovery must skip prompt when `!process.stdin.isTTY`; emit the recover-token hint.           |
| 7   | If `BETTER_AUTH_SECRET` rotates between login and recover, the session cookie is invalid — user must re-login. Acceptable but surface a clear error.                                    | Low      | Error handler maps 401 on recover -> "Session invalid; re-run `mosaic gateway login`".                         |
| 8   | No MFA today. When MFA lands, BetterAuth sign-in will return a challenge, not a cookie — recovery UX will need a second prompt step.                                                    | Future   | Out of scope for this mission. Flag for future CLI work.                                                       |

## 7. Downstream Task Hooks

| Task     | Scope                                                                      |
| -------- | -------------------------------------------------------------------------- |
| CU-03-02 | Implement `mosaic gateway login` wrapper + shared `runLogin` extraction.   |
| CU-03-03 | Implement `mosaic gateway config rotate-token`.                            |
| CU-03-04 | Implement `mosaic gateway config recover-token`.                           |
| CU-03-05 | Wire commands into `gateway.ts` registration, update `--help` copy.        |
| CU-03-06 | Installer inline recovery hook in `bootstrapFirstUser`.                    |
| CU-03-07 | Tests per Section 5.                                                       |
| CU-03-08 | Docs: update gateway install README + operator runbook with recovery flow. |
