skill
AuthenticationIntent Integration
UnreviewedGuide for integrating with the Adom AuthenticationIntent API — device auth flows, headless login, intent-based auth for 3rd-party apps.
{
"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
}