---
name: hd-adom-auth
description: >
  Adom auth in HD — how login/logout works (auth intent flow), where the
  session token lives, the profile menu, and how the Adom session feeds the
  setup steps. For Claude Code auth specifically, see hd-claude-auth.
  READ when troubleshooting Adom login failures, session token expiry, the
  profile dropdown, or virgin-reset Adom-auth options. Trigger words —
  adom login, login, logout, auth, session token, auth intent, confirmation
  code, profile menu, avatar, sign in, sign out, hydrogen-session.txt, auth
  proxy, clear session, HD adom login, Your profile, Your repositories.
---

# Hydrogen Desktop -- Authentication Reference

## Auth Flow Overview

HD uses an **auth intent** flow (no email/password entry in the app).
The Tauri backend creates a short-lived intent token on the Carbon API,
opens the user's default browser to the Adom login page, and polls until
the user completes sign-in in the browser.

### Sequence

1. HD calls `invoke('auth_intent_start')` -> Rust POSTs to
   `https://carbon.adom.inc/auth/intents` with `{"max_age": 7776000}`
   (90-day session).
2. Carbon returns `{token, confirmation_code, expires_at, auth_url}`.
3. HD opens the browser to the **`auth_url` Carbon returned**, falling back to
   `https://hydrogen.adom.inc/auth/intent?token={token}` (singular `intent`,
   token as a query param — NOT `/auth/intents/{token}`).
4. HD displays the confirmation code on screen and shows a spinner.
5. Frontend polls `invoke('auth_intent_poll', {token})` every 2 seconds.
   Rust GETs `https://carbon.adom.inc/auth/intents/{token}/status`.
6. When the user confirms in the browser, the response includes
   `{state: "authenticated", session_token: "..."}`.
7. Rust saves the session token to `%APPDATA%\hydrogen-desktop\hydrogen-session.txt`
   and a replay copy to `%APPDATA%\hydrogen-desktop\replay-session.txt`.
8. Frontend redirects to the dashboard.

### Source files

- **Login page**: `src/routes/auth/login/+page.svelte`
- **Auth Rust backend**: `src-tauri/crates/hd-app/src/lib.rs` (functions
  `auth_intent_start`, `auth_intent_poll`, `auth_proxy`,
  `check_replay_credentials`, `replay_adom_login`). NOTE: the old
  `src-tauri/src/lib.rs` is a pre-crate-split vestige — the live code is in the
  `hd-app` crate.
- **Auth components**: `src/lib/components/auth/` (Header, Form, Footer,
  SubmitButton, Error, Success)
- **User API**: `src/lib/api/authenticated_user.ts`

---

## Session Token

**Location**: `%APPDATA%\hydrogen-desktop\hydrogen-session.txt`

A plain-text file containing the Adom session cookie value. Created by either:
- The auth intent flow (on successful poll)
- The auth proxy (when Carbon returns a `set-cookie: session_token=...` header)

The auth proxy (`invoke('auth_proxy', ...)`) reads this file and attaches it
as a `Cookie: session_token=...` header on every request to `carbon.adom.inc`.

### Token lifecycle

- **Created**: on login (auth intent or browser set-cookie capture)
- **Refreshed**: automatically when the auth proxy receives a new set-cookie
- **Cleared**: on logout (`auth_proxy({path: "/auth/clear-local"})`) or on
  401 response (Rust auto-deletes the file)
- **Max age**: 90 days (set at intent creation)

### Reading the token programmatically

```bash
# From the control API (default 47084; the port is dynamic — read ~/.adom/hd-control-url)
curl "$(cat ~/.adom/hd-control-url)/auth-token"

# From inside the workspace (injected by setup step inject-api-key, mode 644)
# run inside the workspace — WSL2: `wsl -d Adom-Workspace -u adom -- cat /var/run/adom/api-key`; Docker: `docker exec <container> cat /var/run/adom/api-key`
cat /var/run/adom/api-key
```

---

## Auth Proxy

The Tauri command `auth_proxy` is the gateway for all authenticated requests
to the Adom Carbon API. It:

1. Reads the session token from disk
2. Attaches it as a cookie to the outgoing request
3. Captures any `set-cookie` response and updates the saved token
4. On 401, deletes the token and returns `{expired: true}`
5. Supports a special `path: "/auth/clear-local"` to delete the token file

**Base URL**: `https://carbon.adom.inc`

```javascript
// Example: get the current user
const result = await invoke('auth_proxy', {
  path: '/api/me',
  method: 'GET'
});
// result = { status: 200, data: { id: "...", name: "jlauer12", ... }, ok: true }
```

---

## Credential Replay

On login page mount, HD checks for saved credentials before starting the
full auth intent flow:

1. `invoke('check_replay_credentials')` checks if replay files exist on disk.
2. If `replay-session.txt` exists (has_adom = true), HD shows a "REPLAY"
   badge and a 5-second countdown.
3. `invoke('replay_adom_login')` attempts to authenticate using the saved
   session token.
4. If replay succeeds, the user is logged in without opening a browser.
5. If replay fails, HD falls back to the normal auth intent flow.

This makes re-login after a virgin reset instant when the user chose to
preserve the Adom session token.

---

## Claude Credentials

**Location**: `%APPDATA%\hydrogen-desktop\replay-claude-credentials.json`

Stores Claude Code OAuth credentials so they can be replayed into the
workspace during bootstrap. The setup step `inject-api-key` reads the
session token to authenticate with the Adom API, and Claude credentials
are injected separately.

### Virgin reset options for auth

The virgin reset panel (Setup Panel -> Virgin Reset) has per-item toggles:

| Toggle | File | What it clears |
|--------|------|----------------|
| Adom session token | `hydrogen-session.txt` | Adom login. Next launch requires re-auth. |
| Claude credentials | `replay-claude-credentials.json` | Claude Code OAuth. Next bootstrap re-authenticates Claude. |

These are independent -- you can wipe Claude creds without losing Adom login,
or vice versa.

### CSS selectors for virgin reset auth options

```
label.virgin-opt  -- each checkbox row
input[type="checkbox"]  -- the toggle (bind:checked to virginOpts.adom_token / virginOpts.claude_token)
.virgin-hint  -- shows filename (hydrogen-session.txt / replay-claude-credentials.json)
.vp-badge  -- status badge ("exists" / "clean")
.vp-exists  -- green badge (file exists)
.vp-clean  -- gray badge (file gone)
```

---

## Profile Menu

**Location**: Top-right corner of the editor nav bar.
**Source**: `src/lib/components/editor/EditorNav.svelte` (lines ~2403-2443)

Click the circular user avatar to open a dropdown with two sections.

### Menu items

| Item | Icon | Action |
|------|------|--------|
| Your profile | `mdi:account-circle-outline` | Navigates to `/users/{username}` |
| Your repositories | `mdi:folder-outline` | Navigates to `/users/{username}/repositories` |
| Your molecules | `mdi:atom` | Navigates to `/users/{username}/molecules` |
| Log out | `mdi:logout` | Confirms, then clears session + redirects to login |

**CRITICAL**: Logout is ONLY in the profile menu (top-right avatar), NOT in
the Adom menu (top-left logo). Do not look for logout in the wrong menu.

### CSS selectors

| Element | Selector |
|---------|----------|
| Profile dropdown container | `.profile-dropdown-container` |
| Avatar button | `.profile-dropdown-button` |
| Avatar image | `.profile-dropdown-button img` |
| Dropdown (open) | `.profile-dropdown-content.open` |
| Menu items | `.profile-dropdown-content .dropdown-link` |
| Menu item icons | `.dropdown-link span` |

### CDP eval examples

```javascript
// Open the profile menu
document.querySelector('.profile-dropdown-button')?.click();

// Click "Your profile"
var links = document.querySelectorAll('.profile-dropdown-content .dropdown-link');
if (links[0]) links[0].click();

// Click "Your repositories"
if (links[1]) links[1].click();

// Click "Your molecules"
if (links[2]) links[2].click();

// Click "Log out"
var logoutBtn = Array.from(document.querySelectorAll('.profile-dropdown-content .dropdown-link'))
  .find(function(e) { return e.textContent.includes('Log out'); });
if (logoutBtn) logoutBtn.click();
// Note: this triggers a confirm dialog. To auto-confirm, you need to handle the dialog.

// Read the current user's avatar URL
var avatar = document.querySelector('.profile-dropdown-button img');
avatar ? avatar.src : 'no avatar';

// Read the current username from the avatar alt text
avatar ? avatar.alt : 'unknown';
```

---

## Logout Flow

1. User clicks "Log out" in profile menu.
2. `showConfirm('Are you sure you want to log out?', 'Log Out')` -- confirmation dialog.
3. If confirmed (Tauri mode):
   - `invoke('auth_proxy', {path: '/auth/logout', method: 'DELETE'})` -- server-side logout
   - `invoke('auth_proxy', {path: '/auth/clear-local', method: 'POST'})` -- deletes `hydrogen-session.txt`
   - Clears auth-related domain preferences from `localStorage['hd-url-routing-prefs']`
4. Redirects to `/auth/login`.

---

## Login Page UI

**Route**: `/auth/login`

### Tauri mode (auth intent)
- Adom logo + "Log in to Hydrogen" header
- Confirmation code in a monospace box (`.confirmation-code`)
- Spinner + "Waiting for you to sign in..."
- "Open browser again" button (`.retry-button`)
- "Continue without login" link at the bottom (`.skip-login a`)

### Browser mode (email/password)
- Standard form with email + password fields
- "Log In" submit button
- Footer with registration link

### Auth intent CSS selectors

| Element | Selector |
|---------|----------|
| Container | `.intent-container` |
| Confirmation code | `.confirmation-code` |
| Spinner | `.spinner` |
| Status message | `.intent-message` |
| Hint text | `.intent-hint` |
| Success message | `.intent-success` |
| Error message | `.intent-error` |
| Retry button | `.retry-button` |
| Replay badge | `.replay-badge` |
| Skip login link | `.skip-login a` |

### CDP eval on the login page

```javascript
// Check auth intent state
document.querySelector('.intent-message')?.textContent;
// -> "Waiting for you to sign in..." or "Authenticated! Redirecting..."

// Read the confirmation code
document.querySelector('.confirmation-code')?.textContent;

// Click retry
document.querySelector('.retry-button')?.click();

// Skip login (continue without auth)
document.querySelector('.skip-login a')?.click();
```

---

## How Auth Affects Setup Steps

The setup/bootstrap process depends on a valid session token:

1. **inject-api-key** -- reads the HD-side `hydrogen-session.txt`, writes the
   token into the workspace at `/var/run/adom/api-key` (mode 644) as the Adom
   API key. If the token is missing or expired, this step fails and downstream
   steps (adom-cli, gallia, etc.) cannot authenticate.

2. **Claude auth** -- uses `replay-claude-credentials.json` if available;
   otherwise requires the user to complete the Claude OAuth flow inside the
   VS Code iframe.

If bootstrap fails on auth, the fix is usually: log in again via the profile
menu login page, then re-run bootstrap from the Adom menu.

---

## Control API Auth Endpoints

| Method | Path | Description |
|--------|------|-------------|
| GET | `/auth-token` | Read the saved Adom session token |
| POST | `/claude-auth` | Start Claude Code OAuth (PKCE) |
| GET | `/oauth/callback` | Receives OAuth redirect (auto, do not call) |
| POST | `/start-oauth-proxy` | Spin up temp TCP listener for OAuth callback |
| POST | `/auth/proxy` | Proxy request to Carbon API with session token |

See the **hd-settings** skill (API Explorer section) for the
full endpoint reference.

---

## Cross-references

- **hd-ui** -- menu layout, where profile menu lives
- **hd-setup** -- bootstrap steps that depend on auth
- **hd-settings** -- API Explorer (auth endpoints tab)
- **hd-networking** -- control API port (default 47084; dynamic — read ~/.adom/hd-control-url)
- **carbon-preferences** -- user preferences that require auth
