{
  "name": "app-auth",
  "title": "App API - User Authentication",
  "description": "Sign in with Gipity: user authentication, consent, and session handling for apps",
  "guid": "sk_plat_auth",
  "category": "App services",
  "requiredTools": [],
  "content": "# App API - User Authentication\n\nWhen a function has `auth_level: \"user\"` or an LLM service uses `user_pays` billing, the app authenticates Gipity users via session cookies.\n\n## How It Works\nUsers logged into Gipity already have a session cookie on `.gipity.ai`. Your app needs two things:\n1. An **app token** (identifies your app to the API)\n2. `credentials: 'include'` on all fetch calls (sends the user's session cookie cross-origin)\n\n## Step 1: Get an App Token\n`POST https://a.gipity.ai/api/token` with your project GUID.\n\n```js\nconst res = await fetch('https://a.gipity.ai/api/token', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({ app: '<PROJECT_GUID>' })\n});\nconst json = await res.json();\nconst appToken = json.data.token;  // IMPORTANT: .data.token, NOT .token\n```\n\n**Response shape:**\n```json\n{ \"data\": { \"token\": \"eyJ...\", \"expiresIn\": 3600 } }\n```\n\nThe token is short-lived (1 hour). Cache it for the session and refresh when it expires.\n\n## Step 2: Check Auth State (Recommended)\n`GET /api/<PROJECT_GUID>/auth/status` - returns auth state + URLs in one call.\n\nAdd `X-App-Token` header and `credentials: 'include'`.\n\n**Response shapes:**\n```jsonc\n// Not logged in\n{ \"authenticated\": false, \"consented\": false, \"user\": null, \"loginUrl\": \"...\" }\n\n// Logged in, no consent\n{ \"authenticated\": true, \"consented\": false, \"user\": null, \"consentUrl\": \"...\" }\n\n// Fully authed\n{ \"authenticated\": true, \"consented\": true, \"user\": { \"guid\", \"displayName\", \"avatarUrl\", \"accountSlug\" } }\n```\n\nOptional query param: `?permissions=N` (default `1` = IDENTITY). Use `5` for IDENTITY + AI.\n\n## Lightweight Check\n`GET /api/auth/me` (with `credentials: 'include'`) returns `{ user: { guid, displayName, avatarUrl, accountSlug } }` or `{ user: null }`. No app context - cannot return login/consent URLs.\n\n## Error Codes (Function Calls)\nThe function API also returns auth-related error codes with redirect URLs:\n- **LOGIN_REQUIRED** (401): User not logged into Gipity. Redirect to `error.loginUrl`.\n- **CONSENT_REQUIRED** (403): User is logged in but hasn't granted permission. Redirect to `error.consentUrl`.\n\nAppend `&return=<app_url>` to redirect URLs so users return to the app after auth. The return URL must be on a Gipity-hosted domain (`app.gipity.ai`, `dev.gipity.ai`, `gipity.ai`).\n\n## Client Code - Popup Flow (Recommended)\nOpens login/consent in a popup. The app stays visible behind it.\n```js\nasync function checkAuth(appGuid, appToken) {\n  const res = await fetch(\\`https://a.gipity.ai/api/\\${appGuid}/auth/status\\`, {\n    credentials: 'include',\n    headers: { 'X-App-Token': appToken }\n  });\n  const data = await res.json();\n\n  if (data.authenticated && data.consented) {\n    return data.user; // Already authed\n  }\n\n  // Open login or consent popup\n  const authUrl = (data.loginUrl || data.consentUrl) + '&mode=popup';\n  const popup = window.open(authUrl, 'gipity_auth', 'width=450,height=600');\n  return new Promise(resolve => {\n    window.addEventListener('message', function handler(e) {\n      if (e.data?.type === 'gipity_auth') {\n        window.removeEventListener('message', handler);\n        if (e.data.status === 'success') {\n          // Re-check to get user profile\n          checkAuth(appGuid, appToken).then(resolve);\n        } else {\n          resolve(null); // User denied\n        }\n      }\n    });\n  });\n}\n```\n\n## Client Code - Redirect Flow\nNavigates away from the app. Use `&return=<app_url>` so users come back after auth.\n```js\nconst res = await fetch(\\`https://a.gipity.ai/api/\\${appGuid}/auth/status\\`, {\n  credentials: 'include',\n  headers: { 'X-App-Token': token }\n});\nconst data = await res.json();\nif (!data.authenticated) {\n  location.href = data.loginUrl + '&return=' + encodeURIComponent(location.href);\n} else if (!data.consented) {\n  location.href = data.consentUrl + '&return=' + encodeURIComponent(location.href);\n}\n```\n\n## Complete Example (Copy-Paste Ready)\nFull flow from token fetch to authenticated API call:\n```js\nconst API = 'https://a.gipity.ai';\nconst APP_GUID = '<PROJECT_GUID>';\n\n// Step 1: Get app token\nconst tokenRes = await fetch(\\`\\${API}/api/token\\`, {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({ app: APP_GUID })\n});\nconst tokenData = await tokenRes.json();\nconst appToken = tokenData.data.token;  // NOTE: .data.token, not .token\n\n// Step 2: Check auth status\nconst authRes = await fetch(\\`\\${API}/api/\\${APP_GUID}/auth/status\\`, {\n  credentials: 'include',\n  headers: { 'X-App-Token': appToken }\n});\nconst auth = await authRes.json();\n\nif (!auth.authenticated) {\n  // Open login popup\n  const popup = window.open(auth.loginUrl + '&mode=popup', 'gipity_auth', 'width=450,height=600');\n} else if (!auth.consented) {\n  // Open consent popup\n  const popup = window.open(auth.consentUrl + '&mode=popup', 'gipity_auth', 'width=450,height=600');\n} else {\n  // Step 3: Call an authenticated function\n  const res = await fetch(\\`\\${API}/api/\\${APP_GUID}/fn/my_function\\`, {\n    method: 'POST',\n    credentials: 'include',\n    headers: { 'Content-Type': 'application/json', 'X-App-Token': appToken },\n    body: JSON.stringify({ param1: 'value' })\n  });\n  const result = await res.json();\n}\n```\n\n## Common Mistakes\n- **Reading `response.token` instead of `response.data.token`** - the token endpoint wraps its response in a `data` object\n- **Forgetting `credentials: 'include'`** - without this, the session cookie won't be sent on cross-origin requests to `a.gipity.ai`\n- **Using the project GUID as the token** - the GUID identifies your app, but you still need to call `POST /api/token` to get an actual bearer token\n- **Using relative URLs** - app code runs on `app.gipity.ai` or `dev.gipity.ai`, so API calls must use the full URL `https://a.gipity.ai/...`"
}
