💬 Sample prompts Paste any of these into Claude Code to use this skill
How do I authenticate a CLI app with Adom?
Show me the auth intent polling flow in TypeScript
What happens when an auth intent expires?
Install this skill

Paste this into Claude Code (VS Code panel, Adom editor, or terminal) to install:

Search the Adom Wiki for the skill "AuthenticationIntent Integration" (slug: auth-intent) at https://wiki-ufypy5dpx93o.adom.cloud/wiki/skills/auth-intent and install it into my local ~/.claude/skills/auth-intent/ directory. Fetch the skill_source content from the wiki page and save it as SKILL.md. Then confirm it's installed by showing the first 5 lines.
?
What is a skill? Skills are instructions that teach AI assistants like Claude Code how to perform specific tasks. The description below is loaded into the AI as context when you invoke this skill. Well-written skills make the AI significantly more effective. Like Wikipedia, anyone can improve a skill by clicking Edit AI Skill — or have your AI submit an edit on your behalf.

Description

Edit AI Skill

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):

{ "max_age": 2592000 }

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

Response 201 Created:

{
    "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:

{ "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.

EventData
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)

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. => {
            es.close();
            reject(new Error("SSE connection failed"));
        };
    });
}

TypeScript — Short polling (simpler, works anywhere)

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

HTTPScenario
404Token not found
410Intent expired (>15 min old)
409Intent 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.

Skill Source

Edit AI Skill
---
name: auth-intent
description: Guide for integrating with the Adom AuthenticationIntent API. Use when building 3rd-party application login flows, generating client code to authenticate users via Adom, or answering questions about the intent-based auth flow.
argument-hint: [language]
when_to_use: Trigger when the user asks about device auth flows, headless login, application sessions, polling for a session token, the /auth/intents endpoint, or directing a user to an auth URL from a non-browser client.
---

# 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.

Sub-Skills
?
What are Sub-Skills?

Sub-skills are community-contributed AI skill extensions for this component. They teach AI assistants about specific tools, configurators, or workflows.

Examples:

  • A manufacturer’s configuration tool for a motor controller
  • A community-written design guide for an amplifier circuit
  • An automated test/validation script for a sensor module

How to add one: Click Add Sub-Skill, provide the URL to your skill and a brief description. Submissions are reviewed by the Adom team before going live.

No sub-skills yet. Be the first to contribute one!

Recent activity

1 commit
  • 🏷
    Release v1.0.0 John Lauer 11 days ago
    Initial publish of auth-intent skill — API reference, SSE and polling examples, error reference
0 revisions · Updated 2026-05-17 00:33:43