
# AuthenticationIntent Integration

AuthenticationIntent lets 3rd-party apps authenticate users via Adom without handling credentials. The flow:

1. Your app calls `POST /auth/intents` → receives a `token` and `auth_url`
2. Direct the user to `auth_url` (browser, QR code, deep link, etc.)
3. The user logs in on Adom; your app receives a `session_token` via SSE or polling
4. Use `session_token` for authenticated API requests

Intents expire after **15 minutes**. Sessions default to **90 days** but accept a custom `max_age`.

If `$ARGUMENTS` is set, focus examples and explanations on: $ARGUMENTS

---

## API reference

Base prefix: `/auth`

### POST /auth/intents — Create an intent

**Request body** (JSON, all fields optional):

```json
{ "max_age": 2592000 }
```

`max_age` is the desired session lifetime in seconds. Defaults to `7,776,000` (90 days).

**Response** `201 Created`:

```json
{
    "token": "abc123xyz",
    "created_at": "2024-01-01T00:00:00Z",
    "expires_at": "2024-01-01T00:15:00Z",
    "requested_max_age": 2592000,
    "auth_url": "https://hydrogen.adom.inc/auth/intent?token=abc123xyz",
    "updates_url": "https://carbon.adom.inc/auth/intents/abc123xyz/status",
    "completed": false
}
```

### GET /auth/intents/{token} — Fetch intent state

Returns the same shape as the POST response. `completed` becomes `true` once the user has logged in.

### PATCH /auth/intents/{token} — Complete intent _(internal)_

Called by the Adom frontend when a logged-in user visits `auth_url`. Your application does **not** call this endpoint — the user's browser does.

### GET /auth/intents/{token}/status — Poll or stream for completion

**Short polling** — no special headers required:

```json
{ "state": "pending" }
// or, once the user has logged in (token is consumed on first retrieval):
{ "state": "authenticated", "session_token": "tok_..." }
```

**SSE streaming** — send `Accept: text/event-stream`:

Holds the connection open until the user logs in or the intent expires.

| Event           | Data                           |
| --------------- | ------------------------------ |
| `authenticated` | `{"session_token": "tok_..."}` |
| `timeout`       | _(no data — intent expired)_   |

The server sends SSE keep-alive pings, so normal connection timeouts won't fire prematurely.

---

## Integration examples

### TypeScript — SSE (recommended for long-lived CLI / desktop apps)

```typescript
async function authenticateWithAdom(
    apiBase: string,
    maxAge?: number,
): Promise<string> {
    const res = await fetch(`${apiBase}/auth/intents`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(maxAge ? { max_age: maxAge } : {}),
    });
    if (!res.ok) throw new Error(`Failed to create intent: ${res.status}`);
    const intent = await res.json();

    console.log("Authenticate here:", intent.auth_url);

    return new Promise((resolve, reject) => {
        const es = new EventSource(intent.updates_url);

        es.addEventListener("authenticated", (e) => {
            es.close();
            resolve(JSON.parse(e.data).session_token);
        });

        es.addEventListener("timeout", () => {
            es.close();
            reject(new Error("Authentication timed out — intent expired"));
        });

        es.onerror = () => {
            es.close();
            reject(new Error("SSE connection failed"));
        };
    });
}
```

### TypeScript — Short polling (simpler, works anywhere)

```typescript
async function createIntent(apiBase: string, maxAge?: number) {
    const res = await fetch(`${apiBase}/auth/intents`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(maxAge ? { max_age: maxAge } : {}),
    });
    if (!res.ok) throw new Error(`${res.status}`);
    return res.json() as Promise<{
        token: string;
        auth_url: string;
        updates_url: string;
        expires_at: string;
    }>;
}

async function pollForToken(
    statusUrl: string,
    intervalMs = 2000,
): Promise<string> {
    while (true) {
        const res = await fetch(statusUrl);
        if (!res.ok) throw new Error(`Poll failed: ${res.status}`);
        const { state, session_token } = await res.json();
        if (state === "authenticated") return session_token;
        if (state !== "pending") throw new Error(`Unexpected state: ${state}`);
        await new Promise((r) => setTimeout(r, intervalMs));
    }
}

// Usage:
const intent = await createIntent("https://carbon.adom.inc");
console.log("Open:", intent.auth_url);
const token = await pollForToken(intent.updates_url);
```

---

## Error reference

| HTTP  | Scenario                                        |
| ----- | ----------------------------------------------- |
| `404` | Token not found                                 |
| `410` | Intent expired (>15 min old)                    |
| `409` | Intent already linked to a session (PATCH only) |

---

## Key behaviours to be aware of

- **`session_token` is consumed on first retrieval.** Once returned by the status endpoint the `consumed` flag is set. Subsequent polls still return the token as long as the underlying session exists, but the intent can no longer be re-consumed by a different caller.
- **Intent TTL ≠ session TTL.** The intent always expires in 15 minutes. `max_age` only controls how long the resulting _session_ lives.
- **Sessions are `Application` kind**, separate from browser sessions. They carry different trust characteristics server-side.
- **SSE clients outside the browser must set `Accept: text/event-stream` explicitly.** Without it the server falls back to a one-shot JSON response.
- **The `auth_url` embeds the token as a query param.** Treat it as a short-lived, single-use URL — don't cache or log it.
