Integrate / OAuth

# OAuth apps (PKCE)

Act on a member's behalf with per-scope consent that the member or the organization can revoke at any time — never beyond the member's own permissions.

## Register an app

`POST /api/v1/oauth-apps` (`oauth_app:write`) registers a `public` client — CLIs, desktop and mobile apps, PKCE and no secret — or a `confidential` one, whose `client_secret` (`ycs_…`) is returned once and rotated with `POST /api/v1/oauth-apps/{app_id}/rotate-secret`. Redirect URIs are absolute, `https` or loopback `http`, and matched exactly; the app's `scopes` bound what any consent may ask for.

```
# POST /api/v1/oauth-apps                                     (oauth_app:write)
{ "name": "Acme CLI", "app_type": "public",
  "redirect_uris": ["http://127.0.0.1:8765/callback"],
  "scopes": ["openid", "usage:read"], "homepage": "https://acme.dev" }

# 201 — client_secret: null for public apps, shown once for confidential
{ "app": { "id": "oapp_01J…", "client_id": "ycl_9f2k…a71", "state": "active", "has_secret": false, … },
  "client_secret": null }
```

## Authorization code with PKCE

Consent is per scope and per user, given on the console's consent screen and revocable at any time — by the user (`GET /api/v1/oauth/grants`, `POST …/grants/{grant_id}/revoke`) or by the organization (`/api/v1/oauth-apps/grants`). Revoking a grant or an app revokes every token issued under it in the same transaction. PKCE with `S256` is required for every client, public or confidential.

```
# 1. send the user to the consent screen with a PKCE challenge
GET https://yelno.ai/auth/oauth/authorize
  ?client_id=ycl_9f2k…a71
  &response_type=code
  &redirect_uri=https://app.example.com/callback
  &scope=openid%20usage:read
  &code_challenge=<base64url(sha256(verifier))>&code_challenge_method=S256
  &state=…
# allow → https://app.example.com/callback?code=…&state=…
# deny  → https://app.example.com/callback?error=access_denied&state=…

# 2. exchange the code (form-encoded or JSON;
#    confidential clients add client_secret in the body or HTTP Basic)
POST /api/v1/oauth/token
  grant_type=authorization_code&code=…&redirect_uri=https://app.example.com/callback
  &client_id=ycl_9f2k…a71&code_verifier=…

{ "access_token": "<JWT>", "token_type": "Bearer", "expires_in": 3600,
  "refresh_token": "yrt_…", "scope": "openid usage:read", "organization_id": "org_01J8…" }

# 3. call the management API as the user
Authorization: Bearer <access_token>      # 1 h; grant_type=refresh_token renews (30 d, rotating)
GET /api/v1/oauth/userinfo                # openid → sub, organization_id, azp (+ email scope)
GET /api/v1/usage                         # usage:read
```

## Scopes

A token can never exceed the granting member's own permissions: each scope maps onto management permissions that are AND-ed with the member's live role, so a scope only ever narrows. No scope covers API keys' secret material, provider credentials or content — and none covers inference: user tokens drive the management API, not `/v1/chat/completions`. `GET /api/v1/oauth/scopes` serves this table for the consent screen.

| Scope | Description at consent | Permissions |
| --- | --- | --- |
| openid | Sign you in and identify your account | — (required for /oauth/userinfo) |
| email | Read your email address | — (adds email to userinfo) |
| usage:read | Read usage, cost and request summaries | usage:read · org:read |
| requests:read | Read individual request records | usage:read |
| wallet:read | Read wallet balances and credit grants | wallet:read |
| keys:read | List API keys (never their secret material) | key:read |
| catalog:read | Read the model catalog as your organization sees it | catalog:read |
| projects:write | Create and update projects | project:read · project:write |

## Tokens and revocation

Access tokens are JWTs that live one hour and carry `scp`, `azp` (the client id) and the member's live `role`. Refresh tokens live 30 days and rotate on every use: presenting an already-rotated refresh token revokes every token of the grant, and presenting an authorization code twice revokes the tokens it produced. `POST /api/v1/oauth/revoke` (RFC 7009) revokes a token and always answers `200`. A revoked token gets `401 token_revoked`; a call outside the granted scopes gets `403 insufficient_scope`. Token-endpoint errors use the house envelope with the RFC 6749 code in `error.code` — `invalid_request`, `invalid_client` (401), `invalid_grant`, `unsupported_grant_type`, `invalid_scope`.

Source: https://yelno.ai/docs/oauth
