# RegardingWork Hub — LLM-Friendly API Reference

This file is the canonical, agent-feedable reference for integrating with
RegardingWork Hub (`https://hub.regardingwork.com`). Everything you need to
build a mini-app or third-party integration is in this single file.

- Hosted: <https://hub.regardingwork.com/docs/llms.txt>
- OpenAPI spec: <https://hub.regardingwork.com/docs/openapi.json>
- Source-of-truth status page: <https://hub.regardingwork.com/api/status>

---

## Decision tree — pick your integration type

| Building...                                                       | Use     | Why                                                                       |
|-------------------------------------------------------------------|---------|---------------------------------------------------------------------------|
| Internal RegardingWork app on a `*.regardingwork.com` subdomain   | **SSO** | One redirect, JWT in callback URL, done in <1 hour. No client secret.     |
| External third-party app on your own domain                       | **OAuth 2.0** | Authorization-code flow with PKCE. Requires registered client + secret. |

If unsure: you're almost certainly building a mini-app → use SSO.

---

## Core concepts

- **Base URL**: `https://hub.regardingwork.com/api`
- **Auth scheme**: JWT bearer tokens (`Authorization: Bearer <jwt>`), HS256.
- **Token lifetimes**: access tokens **30 days**, refresh tokens **30 days**.
  (Extended for Mac-app stability; if you cache the token, plan for ≤30d.)
- **CORS**: enabled for `*.regardingwork.com` and a small allowlist of partner
  origins on `/api/*` routes only.
- **Login identifier**: `username`, **not** email. Email is optional metadata.
- **CSRF**: web routes only. API routes are exempt — JWT is the auth.

---

## Standard user object

Every endpoint that returns a user uses the same shape. Treat this as the
single source of truth:

```json
{
  "id": 12345,
  "username": "janechen",
  "email": "jane@example.com",
  "email_verified": true,
  "bio": "Hi, I'm Jane.",
  "website_url": "https://example.com",
  "profile_photo_url": "https://hub.regardingwork.com/api/public/user/12345/profile-photo/file?v=a1b2c3d4",
  "has_profile_photo": true,
  "created_at": "2025-01-15T10:30:00",
  "updated_at": "2026-04-22T14:11:09",
  "is_active": true,
  "role": "USER",
  "premium_tier": "FREE"
}
```

### `profile_photo_url` semantics — important

- **Always absolute** (or `null`). Render directly in `<img src="...">`. Do
  not prepend a base URL.
- **The unauthenticated public endpoint** (`/api/public/user/<id>/profile-photo/file`)
  is the actual binary source. The URL value points there.
- **Cache-buster `?v=...`** changes when the user uploads a new photo. Use
  string-equality on `profile_photo_url` to detect updates and invalidate any
  CDN cache you keep.
- `email` and `email_verified` are present only when the response is
  authenticated as that user (e.g. `/api/auth/me`, `/api/auth/validate`).

---

## Mini-app integration (SSO) — full flow

### 1. Redirect the user to Hub

```
https://hub.regardingwork.com/api/auth/sso/authorize
  ?redirect_uri=https://yourapp.regardingwork.com/auth/callback
  &service=yourapp
```

- `redirect_uri` host **must** be in Hub's SSO allowlist. Add it via the SSO
  request form: <https://hub.regardingwork.com/docs/request-sso>.
- If the user is not logged in, Hub redirects to `/login?next=<sso-url>` and
  bounces back here after login.

### 2. Receive the callback

Hub redirects to your `redirect_uri` with these query params:

| Param                | Meaning                                               |
|----------------------|-------------------------------------------------------|
| `token`              | JWT access token (preferred name)                     |
| `access_token`       | Same JWT, duplicated for legacy consumers             |
| `user_id`            | Hub user ID (integer)                                 |
| `username`           | Hub username                                          |
| `profile_photo_url`  | Absolute photo URL (only present if user has a photo) |
| `has_profile_photo`  | `1` if photo present, omitted otherwise               |

You can render the avatar immediately from `profile_photo_url`. No further
call required for that.

### 3. Validate the token (server-side, every request)

```http
GET /api/auth/validate
Authorization: Bearer <token>
```

Returns the standard user object plus a `valid: true` flag and a `premium`
compatibility block. Use this on every authenticated request — do not trust
the JWT without validating against Hub (revocation is centralized).

### 4. Logout

```http
POST /api/auth/logout
Authorization: Bearer <token>
```

Or for global SSO logout across all RegardingWork services, redirect the user to:

```
https://hub.regardingwork.com/api/auth/global-logout?redirect_uri=https://yourapp.regardingwork.com
```

---

## Third-party integration (OAuth 2.0) — full flow

Use when your app is **not** on `*.regardingwork.com`. Standard
authorization-code-with-PKCE flow.

### 1. Register your app

Email <support@regardingwork.com> with: app name, redirect URIs, requested
scopes. You receive `client_id` + `client_secret`.

### 2. Authorization request

```
https://hub.regardingwork.com/api/oauth/authorize
  ?response_type=code
  &client_id=<your-client-id>
  &redirect_uri=<your-callback>
  &scope=profile
  &state=<csrf-token>
  &code_challenge=<base64url-sha256-of-verifier>
  &code_challenge_method=S256
```

### 3. Token exchange

```http
POST /api/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=<from-callback>
&redirect_uri=<same-as-step-2>
&client_id=<your-client-id>
&client_secret=<your-client-secret>
&code_verifier=<original-verifier>
```

Response: `{ access_token, refresh_token, token_type: "Bearer", expires_in }`

Validate the access token via `/api/auth/validate` exactly as for SSO.

---

## Endpoint reference

All paths below are relative to `https://hub.regardingwork.com`.

### Authentication

| Method | Path                       | Auth     | Purpose                                          |
|--------|----------------------------|----------|--------------------------------------------------|
| POST   | `/api/auth/register`       | none     | Create user. Body: `username`, `email`, `password`. |
| POST   | `/api/auth/login`          | none     | Body: `username`, `password`. Returns tokens + user. |
| POST   | `/api/auth/logout`         | bearer   | Revokes the access token (and optionally a refresh token in body). |
| GET    | `/api/auth/global-logout`  | bearer   | Revokes + redirects to `redirect_uri` (cross-service logout). |
| POST   | `/api/auth/refresh`        | none     | Body: `refresh_token`. Returns new access token.  |
| GET    | `/api/auth/validate`       | bearer   | **Critical for mini-apps.** Returns user + `valid:true`. |
| GET    | `/api/auth/me`             | bearer   | Returns the current user object (alias of validate sans premium block). |
| GET    | `/api/auth/user/<id>`      | none     | Public user profile. |
| PUT    | `/api/auth/user/<id>`      | bearer   | Update own profile (bio, photo URL, website). |

### Profile photos

| Method | Path                                                | Auth   | Returns                       |
|--------|-----------------------------------------------------|--------|-------------------------------|
| GET    | `/api/auth/user/<id>/profile-photo`                 | bearer | JSON `{user_id, username, profile_photo_url, has_photo}` |
| GET    | `/api/auth/user/<username>/profile-photo`           | bearer | Same as above, by username   |
| POST   | `/api/auth/users/profile-photos`                    | bearer | Batch lookup: body `{user_ids:[…], usernames:[…]}` → array |
| GET    | `/api/auth/user/<id>/profile-photo/file`            | bearer | Raw image bytes              |
| GET    | `/api/public/user/<id>/profile-photo`               | none   | JSON metadata, no auth       |
| GET    | `/api/public/user/<username>/profile-photo`         | none   | Same, by username            |
| GET    | `/api/public/user/<id>/profile-photo/file`          | none   | **Raw image bytes, no auth.** This is the URL embedded in `profile_photo_url`. |

### SSO

| Method | Path                          | Auth          | Purpose                                       |
|--------|-------------------------------|---------------|-----------------------------------------------|
| GET    | `/api/auth/sso/authorize`     | session/none  | Redirect endpoint. See SSO flow above.        |
| POST   | `/api/auth/sso/token`         | bearer        | Re-mint tokens (refresh-equivalent for SSO).  |
| GET    | `/api/auth/sso/callback`      | none          | API callback handler for legacy mini-apps.    |

### OAuth 2.0

| Method | Path                  | Auth         | Purpose                                  |
|--------|-----------------------|--------------|------------------------------------------|
| GET    | `/api/oauth/authorize`| session      | OAuth authorization endpoint with PKCE.  |
| POST   | `/api/oauth/token`    | client creds | Exchange code for tokens.                |

### Teams

| Method | Path                                        | Auth   | Purpose                              |
|--------|---------------------------------------------|--------|--------------------------------------|
| GET    | `/api/teams`                                | bearer | List teams the user belongs to.      |
| POST   | `/api/teams`                                | bearer | Create a team. Body: `name`, `slug`. |
| GET    | `/api/teams/<slug>`                         | bearer | Team detail.                         |
| GET    | `/api/teams/<slug>/members`                 | bearer | List members.                        |
| POST   | `/api/teams/<slug>/members`                 | bearer | Add member by username.              |
| PUT    | `/api/teams/<slug>/members/<username>`      | bearer | Update member role.                  |
| DELETE | `/api/teams/<slug>/members/<username>`      | bearer | Remove member.                       |
| GET    | `/api/user/teams`                           | bearer | Same as `/api/teams` (alias).        |
| POST   | `/api/teams/<slug>/invite`                  | bearer | Send invite email. Body: `email`.    |
| POST   | `/api/invitations/<token>`                  | bearer | Accept an invitation by token.       |
| GET    | `/api/user/invitations`                     | bearer | Pending invitations for the user.    |

### Utility

| Method | Path           | Auth | Purpose                                    |
|--------|----------------|------|--------------------------------------------|
| GET    | `/api/status`  | none | Liveness + sanity check (user count etc.). |

---

## Common gotchas

1. **Login uses `username`, not `email`.** Hub stores email but
   `/api/auth/login` only accepts username. If you have email, look up the
   username via your local DB or surface a username field in the form.
2. **`profile_photo_url` is now absolute.** If your code prepends
   `https://hub.regardingwork.com` to it, **stop** — you'll double-prepend.
   Render the field directly.
3. **The cache-buster (`?v=...`) is intentional.** Don't strip it. It's how
   you detect photo updates: when the URL value changes, the photo changed.
4. **CORS rejections** on `/api/*` mean your origin isn't in Hub's allowlist
   (DB-backed `cors_domains` table + hardcoded fallback in `app.py`). Email
   support to add a domain.
5. **SSO domain rejections** (HTTP 400 `Invalid redirect_uri domain`) mean
   your callback host isn't in `sso_domains`. Submit
   <https://hub.regardingwork.com/docs/request-sso>.
6. **Token revocation is centralized.** A logout on Hub invalidates the JWT
   for every service. Always validate via `/api/auth/validate` rather than
   trusting JWT signature alone.
7. **Test credentials** for staging/local: `janechen` / `jane123`.

---

## Error response shape

All API errors use this shape (HTTP status set appropriately):

```json
{ "error": "human-readable message", "valid": false }
```

`valid: false` is present only on auth-related endpoints. Some endpoints add
an `error_description` field (OAuth-style) or `details` for stack traces in
debug mode.

---

## Versioning

This document tracks the current production deployment. The OpenAPI spec
(`/docs/openapi.json`) is the formal machine-readable contract. If they
disagree, the OpenAPI spec wins; please report the inconsistency.
