{
  "schema_version": 1,
  "type": "skill",
  "slug": "auth-intent",
  "title": "AuthenticationIntent Integration",
  "brief": "Guide for integrating with the Adom AuthenticationIntent API — device auth flows, headless login, intent-based auth for 3rd-party apps.",
  "version": "1.0.0",
  "tags": [],
  "license": "MIT",
  "sample_prompts": [
    {
      "prompt": "How do I authenticate a CLI app with Adom?"
    },
    {
      "prompt": "Show me the auth intent polling flow in TypeScript"
    },
    {
      "prompt": "What happens when an auth intent expires?"
    }
  ],
  "source_path": "SKILL.md",
  "readme": "\n# AuthenticationIntent Integration\n\nAuthenticationIntent lets 3rd-party apps authenticate users via Adom without handling credentials. The flow:\n\n1. Your app calls `POST /auth/intents` → receives a `token` and `auth_url`\n2. Direct the user to `auth_url` (browser, QR code, deep link, etc.)\n3. The user logs in on Adom; your app receives a `session_token` via SSE or polling\n4. Use `session_token` for authenticated API requests\n\nIntents expire after **15 minutes**. Sessions default to **90 days** but accept a custom `max_age`.\n\nIf `$ARGUMENTS` is set, focus examples and explanations on: $ARGUMENTS\n\n---\n\n## API reference\n\nBase prefix: `/auth`\n\n### POST /auth/intents — Create an intent\n\n**Request body** (JSON, all fields optional):\n\n```json\n{ \"max_age\": 2592000 }\n```\n\n`max_age` is the desired session lifetime in seconds. Defaults to `7,776,000` (90 days).\n\n**Response** `201 Created`:\n\n```json\n{\n    \"token\": \"abc123xyz\",\n    \"created_at\": \"2024-01-01T00:00:00Z\",\n    \"expires_at\": \"2024-01-01T00:15:00Z\",\n    \"requested_max_age\": 2592000,\n    \"auth_url\": \"https://hydrogen.adom.inc/auth/intent?token=abc123xyz\",\n    \"updates_url\": \"https://carbon.adom.inc/auth/intents/abc123xyz/status\",\n    \"completed\": false\n}\n```\n\n### GET /auth/intents/{token} — Fetch intent state\n\nReturns the same shape as the POST response. `completed` becomes `true` once the user has logged in.\n\n### PATCH /auth/intents/{token} — Complete intent _(internal)_\n\nCalled by the Adom frontend when a logged-in user visits `auth_url`. Your application does **not** call this endpoint — the user's browser does.\n\n### GET /auth/intents/{token}/status — Poll or stream for completion\n\n**Short polling** — no special headers required:\n\n```json\n{ \"state\": \"pending\" }\n// or, once the user has logged in (token is consumed on first retrieval):\n{ \"state\": \"authenticated\", \"session_token\": \"tok_...\" }\n```\n\n**SSE streaming** — send `Accept: text/event-stream`:\n\nHolds the connection open until the user logs in or the intent expires.\n\n| Event           | Data                           |\n| --------------- | ------------------------------ |\n| `authenticated` | `{\"session_token\": \"tok_...\"}` |\n| `timeout`       | _(no data — intent expired)_   |\n\nThe server sends SSE keep-alive pings, so normal connection timeouts won't fire prematurely.\n\n---\n\n## Integration examples\n\n### TypeScript — SSE (recommended for long-lived CLI / desktop apps)\n\n```typescript\nasync function authenticateWithAdom(\n    apiBase: string,\n    maxAge?: number,\n): Promise<string> {\n    const res = await fetch(`${apiBase}/auth/intents`, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify(maxAge ? { max_age: maxAge } : {}),\n    });\n    if (!res.ok) throw new Error(`Failed to create intent: ${res.status}`);\n    const intent = await res.json();\n\n    console.log(\"Authenticate here:\", intent.auth_url);\n\n    return new Promise((resolve, reject) => {\n        const es = new EventSource(intent.updates_url);\n\n        es.addEventListener(\"authenticated\", (e) => {\n            es.close();\n            resolve(JSON.parse(e.data).session_token);\n        });\n\n        es.addEventListener(\"timeout\", () => {\n            es.close();\n            reject(new Error(\"Authentication timed out — intent expired\"));\n        });\n\n        es.onerror = () => {\n            es.close();\n            reject(new Error(\"SSE connection failed\"));\n        };\n    });\n}\n```\n\n### TypeScript — Short polling (simpler, works anywhere)\n\n```typescript\nasync function createIntent(apiBase: string, maxAge?: number) {\n    const res = await fetch(`${apiBase}/auth/intents`, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify(maxAge ? { max_age: maxAge } : {}),\n    });\n    if (!res.ok) throw new Error(`${res.status}`);\n    return res.json() as Promise<{\n        token: string;\n        auth_url: string;\n        updates_url: string;\n        expires_at: string;\n    }>;\n}\n\nasync function pollForToken(\n    statusUrl: string,\n    intervalMs = 2000,\n): Promise<string> {\n    while (true) {\n        const res = await fetch(statusUrl);\n        if (!res.ok) throw new Error(`Poll failed: ${res.status}`);\n        const { state, session_token } = await res.json();\n        if (state === \"authenticated\") return session_token;\n        if (state !== \"pending\") throw new Error(`Unexpected state: ${state}`);\n        await new Promise((r) => setTimeout(r, intervalMs));\n    }\n}\n\n// Usage:\nconst intent = await createIntent(\"https://carbon.adom.inc\");\nconsole.log(\"Open:\", intent.auth_url);\nconst token = await pollForToken(intent.updates_url);\n```\n\n---\n\n## Error reference\n\n| HTTP  | Scenario                                        |\n| ----- | ----------------------------------------------- |\n| `404` | Token not found                                 |\n| `410` | Intent expired (>15 min old)                    |\n| `409` | Intent already linked to a session (PATCH only) |\n\n---\n\n## Key behaviours to be aware of\n\n- **`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.\n- **Intent TTL ≠ session TTL.** The intent always expires in 15 minutes. `max_age` only controls how long the resulting _session_ lives.\n- **Sessions are `Application` kind**, separate from browser sessions. They carry different trust characteristics server-side.\n- **SSE clients outside the browser must set `Accept: text/event-stream` explicitly.** Without it the server falls back to a one-shot JSON response.\n- **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.\n",
  "author": {
    "id": "695820315b5f1e4db2fcf602",
    "name": "Kyle Bergstedt",
    "email": "[email protected]"
  },
  "visibility": {
    "public": true
  },
  "hero": null,
  "discovery_triggers": [],
  "discovery_pitch": null,
  "metadata": {},
  "created_at": "2026-05-28T05:29:31.981Z",
  "updated_at": "2026-05-28T05:29:31.981Z",
  "sub_skills": [],
  "parent_app": null
}