# App API - User Authentication

When a function has `auth_level: "user"` or an LLM service uses `user_pays` billing, the app authenticates Gipity users via session cookies.

## How It Works
Users logged into Gipity already have a session cookie on `.gipity.ai`. Your app needs two things:
1. An **app token** (identifies your app to the API)
2. `credentials: 'include'` on all fetch calls (sends the user's session cookie cross-origin)

## Step 1: Get an App Token
`POST https://a.gipity.ai/api/token` with your project GUID.

```js
const res = await fetch('https://a.gipity.ai/api/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ app: '<PROJECT_GUID>' })
});
const json = await res.json();
const appToken = json.data.token;  // IMPORTANT: .data.token, NOT .token
```

**Response shape:**
```json
{ "data": { "token": "eyJ...", "expiresIn": 3600 } }
```

The token is short-lived (1 hour). Cache it for the session and refresh when it expires.

## Step 2: Check Auth State (Recommended)
`GET /api/<PROJECT_GUID>/auth/status` - returns auth state + URLs in one call.

Add `X-App-Token` header and `credentials: 'include'`.

**Response shapes:**
```jsonc
// Not logged in
{ "authenticated": false, "consented": false, "user": null, "loginUrl": "..." }

// Logged in, no consent
{ "authenticated": true, "consented": false, "user": null, "consentUrl": "..." }

// Fully authed
{ "authenticated": true, "consented": true, "user": { "guid", "displayName", "avatarUrl", "accountSlug" } }
```

Optional query param: `?permissions=N` (default `1` = IDENTITY). Use `5` for IDENTITY + AI.

## Lightweight Check
`GET /api/auth/me` (with `credentials: 'include'`) returns `{ user: { guid, displayName, avatarUrl, accountSlug } }` or `{ user: null }`. No app context - cannot return login/consent URLs.

## Error Codes (Function Calls)
The function API also returns auth-related error codes with redirect URLs:
- **LOGIN_REQUIRED** (401): User not logged into Gipity. Redirect to `error.loginUrl`.
- **CONSENT_REQUIRED** (403): User is logged in but hasn't granted permission. Redirect to `error.consentUrl`.

Append `&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`).

## Client Code - Popup Flow (Recommended)
Opens login/consent in a popup. The app stays visible behind it.
```js
async function checkAuth(appGuid, appToken) {
  const res = await fetch(\`https://a.gipity.ai/api/\${appGuid}/auth/status\`, {
    credentials: 'include',
    headers: { 'X-App-Token': appToken }
  });
  const data = await res.json();

  if (data.authenticated && data.consented) {
    return data.user; // Already authed
  }

  // Open login or consent popup
  const authUrl = (data.loginUrl || data.consentUrl) + '&mode=popup';
  const popup = window.open(authUrl, 'gipity_auth', 'width=450,height=600');
  return new Promise(resolve => {
    window.addEventListener('message', function handler(e) {
      if (e.data?.type === 'gipity_auth') {
        window.removeEventListener('message', handler);
        if (e.data.status === 'success') {
          // Re-check to get user profile
          checkAuth(appGuid, appToken).then(resolve);
        } else {
          resolve(null); // User denied
        }
      }
    });
  });
}
```

## Client Code - Redirect Flow
Navigates away from the app. Use `&return=<app_url>` so users come back after auth.
```js
const res = await fetch(\`https://a.gipity.ai/api/\${appGuid}/auth/status\`, {
  credentials: 'include',
  headers: { 'X-App-Token': token }
});
const data = await res.json();
if (!data.authenticated) {
  location.href = data.loginUrl + '&return=' + encodeURIComponent(location.href);
} else if (!data.consented) {
  location.href = data.consentUrl + '&return=' + encodeURIComponent(location.href);
}
```

## Complete Example (Copy-Paste Ready)
Full flow from token fetch to authenticated API call:
```js
const API = 'https://a.gipity.ai';
const APP_GUID = '<PROJECT_GUID>';

// Step 1: Get app token
const tokenRes = await fetch(\`\${API}/api/token\`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ app: APP_GUID })
});
const tokenData = await tokenRes.json();
const appToken = tokenData.data.token;  // NOTE: .data.token, not .token

// Step 2: Check auth status
const authRes = await fetch(\`\${API}/api/\${APP_GUID}/auth/status\`, {
  credentials: 'include',
  headers: { 'X-App-Token': appToken }
});
const auth = await authRes.json();

if (!auth.authenticated) {
  // Open login popup
  const popup = window.open(auth.loginUrl + '&mode=popup', 'gipity_auth', 'width=450,height=600');
} else if (!auth.consented) {
  // Open consent popup
  const popup = window.open(auth.consentUrl + '&mode=popup', 'gipity_auth', 'width=450,height=600');
} else {
  // Step 3: Call an authenticated function
  const res = await fetch(\`\${API}/api/\${APP_GUID}/fn/my_function\`, {
    method: 'POST',
    credentials: 'include',
    headers: { 'Content-Type': 'application/json', 'X-App-Token': appToken },
    body: JSON.stringify({ param1: 'value' })
  });
  const result = await res.json();
}
```

## Common Mistakes
- **Reading `response.token` instead of `response.data.token`** - the token endpoint wraps its response in a `data` object
- **Forgetting `credentials: 'include'`** - without this, the session cookie won't be sent on cross-origin requests to `a.gipity.ai`
- **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
- **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/...`
