{
  "openapi": "3.0.3",
  "info": {
    "title": "RegardingWork Hub API",
    "version": "1.0.0",
    "description": "Centralized authentication and SSO service for the RegardingWork ecosystem. See https://hub.regardingwork.com/docs/llms.txt for a narrative integration guide.",
    "contact": {
      "name": "RegardingWork Hub support",
      "url": "https://hub.regardingwork.com/docs"
    }
  },
  "servers": [
    { "url": "https://hub.regardingwork.com", "description": "Production" }
  ],
  "tags": [
    { "name": "auth", "description": "User authentication & session management" },
    { "name": "users", "description": "User profile read/update" },
    { "name": "photos", "description": "Profile photo endpoints" },
    { "name": "sso", "description": "Single Sign-On for RegardingWork mini-apps" },
    { "name": "oauth", "description": "OAuth 2.0 for third-party integrations" },
    { "name": "teams", "description": "Team management" },
    { "name": "utility", "description": "Status & health" }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      }
    },
    "schemas": {
      "User": {
        "type": "object",
        "properties": {
          "id": { "type": "integer", "example": 12345 },
          "username": { "type": "string", "example": "janechen" },
          "email": { "type": "string", "nullable": true, "example": "jane@example.com" },
          "email_verified": { "type": "boolean" },
          "bio": { "type": "string", "nullable": true },
          "website_url": { "type": "string", "nullable": true },
          "profile_photo_url": {
            "type": "string",
            "nullable": true,
            "description": "Absolute URL. Render directly. Includes a ?v= cache-buster that changes when the photo changes.",
            "example": "https://hub.regardingwork.com/api/public/user/12345/profile-photo/file?v=a1b2c3d4"
          },
          "has_profile_photo": { "type": "boolean" },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" },
          "is_active": { "type": "boolean" },
          "role": { "type": "string", "enum": ["USER", "SUPERADMIN"] },
          "premium_tier": { "type": "string", "enum": ["FREE", "PREMIUM"] }
        },
        "required": ["id", "username", "is_active", "role", "premium_tier", "has_profile_photo"]
      },
      "TokenPair": {
        "type": "object",
        "properties": {
          "access_token": { "type": "string" },
          "refresh_token": { "type": "string" },
          "expires_in": { "type": "integer", "example": 2592000, "description": "Seconds. Currently 30 days." },
          "user": { "$ref": "#/components/schemas/User" }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string" },
          "valid": { "type": "boolean" },
          "error_description": { "type": "string" }
        },
        "required": ["error"]
      },
      "ProfilePhotoMeta": {
        "type": "object",
        "properties": {
          "user_id": { "type": "integer" },
          "username": { "type": "string" },
          "profile_photo_url": { "type": "string", "nullable": true },
          "has_photo": { "type": "boolean" }
        }
      },
      "ValidateResponse": {
        "allOf": [
          { "$ref": "#/components/schemas/User" },
          {
            "type": "object",
            "properties": {
              "valid": { "type": "boolean", "example": true },
              "user": { "$ref": "#/components/schemas/User" },
              "premium": {
                "type": "object",
                "properties": {
                  "tier": { "type": "string" },
                  "tier_name": { "type": "string" },
                  "is_active": { "type": "boolean" },
                  "expires_at": { "type": "string", "nullable": true },
                  "bandwidth_limit_mb": { "type": "integer" }
                }
              },
              "token_type": { "type": "string", "example": "access" },
              "expires_at": { "type": "integer", "description": "Unix timestamp" }
            }
          }
        ]
      },
      "Team": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "name": { "type": "string" },
          "slug": { "type": "string" },
          "description": { "type": "string", "nullable": true },
          "owner_id": { "type": "integer" },
          "member_count": { "type": "integer" },
          "created_at": { "type": "string", "format": "date-time" }
        }
      }
    }
  },
  "paths": {
    "/api/auth/register": {
      "post": {
        "tags": ["auth"],
        "summary": "Register a new user",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["username", "password"],
                "properties": {
                  "username": { "type": "string", "minLength": 3, "maxLength": 64, "pattern": "^[a-zA-Z0-9_-]+$" },
                  "email": { "type": "string", "format": "email" },
                  "password": { "type": "string", "minLength": 8 }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TokenPair" } } } },
          "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/auth/login": {
      "post": {
        "tags": ["auth"],
        "summary": "Authenticate with username and password",
        "description": "Hub uses USERNAME, not email, for login.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["username", "password"],
                "properties": {
                  "username": { "type": "string" },
                  "password": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TokenPair" } } } },
          "401": { "description": "Invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/auth/logout": {
      "post": {
        "tags": ["auth"],
        "summary": "Revoke the current access token",
        "security": [{ "bearerAuth": [] }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": { "refresh_token": { "type": "string" } }
              }
            }
          }
        },
        "responses": { "200": { "description": "Logged out" }, "401": { "description": "Unauthorized" } }
      }
    },
    "/api/auth/global-logout": {
      "get": {
        "tags": ["auth"],
        "summary": "Cross-service logout — revokes tokens then redirects",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          { "name": "redirect_uri", "in": "query", "required": true, "schema": { "type": "string", "format": "uri" } }
        ],
        "responses": { "302": { "description": "Redirect to redirect_uri" } }
      }
    },
    "/api/auth/refresh": {
      "post": {
        "tags": ["auth"],
        "summary": "Exchange a refresh token for a new access token",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["refresh_token"],
                "properties": { "refresh_token": { "type": "string" } }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TokenPair" } } } },
          "401": { "description": "Invalid or expired refresh token" }
        }
      }
    },
    "/api/auth/validate": {
      "get": {
        "tags": ["auth"],
        "summary": "Validate a JWT and return the user object",
        "description": "Critical endpoint for mini-apps. Returns user data in both flat (legacy) and nested formats.",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": { "description": "Valid", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidateResponse" } } } },
          "401": { "description": "Invalid, expired, or revoked", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "post": {
        "tags": ["auth"],
        "summary": "Same as GET (POST accepted for legacy clients)",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": { "description": "Valid", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidateResponse" } } } },
          "401": { "description": "Invalid", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/auth/me": {
      "get": {
        "tags": ["auth"],
        "summary": "Get the authenticated user",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "user": { "$ref": "#/components/schemas/User" } } } } } },
          "401": { "description": "Unauthorized" }
        }
      }
    },
    "/api/auth/user/{user_id}": {
      "parameters": [{ "name": "user_id", "in": "path", "required": true, "schema": { "type": "integer" } }],
      "get": {
        "tags": ["users"],
        "summary": "Public user profile",
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "user": { "$ref": "#/components/schemas/User" } } } } } },
          "404": { "description": "Not found" }
        }
      },
      "put": {
        "tags": ["users"],
        "summary": "Update own profile",
        "security": [{ "bearerAuth": [] }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "bio": { "type": "string", "nullable": true },
                  "profile_photo_url": { "type": "string", "nullable": true },
                  "website_url": { "type": "string", "nullable": true },
                  "email": { "type": "string", "description": "Only updatable for accounts with placeholder emails." }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Updated" },
          "401": { "description": "Unauthorized" },
          "403": { "description": "Forbidden — can only update own profile" }
        }
      }
    },
    "/api/auth/user/{user_id}/profile-photo": {
      "get": {
        "tags": ["photos"],
        "summary": "Profile photo metadata (authenticated)",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "name": "user_id", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProfilePhotoMeta" } } } }
        }
      }
    },
    "/api/auth/user/{username}/profile-photo": {
      "get": {
        "tags": ["photos"],
        "summary": "Profile photo metadata by username (authenticated)",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "name": "username", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProfilePhotoMeta" } } } }
        }
      }
    },
    "/api/auth/users/profile-photos": {
      "post": {
        "tags": ["photos"],
        "summary": "Batch profile photo lookup",
        "security": [{ "bearerAuth": [] }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "user_ids": { "type": "array", "items": { "type": "integer" } },
                  "usernames": { "type": "array", "items": { "type": "string" } }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "users": { "type": "array", "items": { "$ref": "#/components/schemas/ProfilePhotoMeta" } },
                    "total": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/auth/user/{user_id}/profile-photo/file": {
      "get": {
        "tags": ["photos"],
        "summary": "Raw profile photo bytes (authenticated)",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "name": "user_id", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "200": { "description": "Image", "content": { "image/jpeg": {}, "image/png": {}, "image/gif": {} } },
          "404": { "description": "No photo" }
        }
      }
    },
    "/api/public/user/{user_id}/profile-photo": {
      "get": {
        "tags": ["photos"],
        "summary": "Profile photo metadata (public, no auth)",
        "parameters": [{ "name": "user_id", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProfilePhotoMeta" } } } }
        }
      }
    },
    "/api/public/user/{username}/profile-photo": {
      "get": {
        "tags": ["photos"],
        "summary": "Profile photo metadata by username (public, no auth)",
        "parameters": [{ "name": "username", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProfilePhotoMeta" } } } }
        }
      }
    },
    "/api/public/user/{user_id}/profile-photo/file": {
      "get": {
        "tags": ["photos"],
        "summary": "Raw profile photo bytes (public, no auth)",
        "description": "This is the URL embedded in profile_photo_url. Render directly in <img>.",
        "parameters": [{ "name": "user_id", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "200": { "description": "Image", "content": { "image/jpeg": {}, "image/png": {}, "image/gif": {} } },
          "404": { "description": "No photo" }
        }
      }
    },
    "/api/auth/sso/authorize": {
      "get": {
        "tags": ["sso"],
        "summary": "SSO authorization redirect",
        "description": "Redirects authenticated user to redirect_uri with token params. Redirects unauthenticated users to login.",
        "parameters": [
          { "name": "redirect_uri", "in": "query", "required": true, "schema": { "type": "string", "format": "uri" }, "description": "Must be in Hub's SSO domain allowlist." },
          { "name": "service", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": {
          "302": {
            "description": "Redirect to redirect_uri with query params: token, access_token, user_id, username, profile_photo_url (if photo present), has_profile_photo (if photo present)."
          },
          "400": { "description": "Invalid redirect_uri domain" },
          "401": { "description": "Authentication required (when Accept: application/json)" }
        }
      }
    },
    "/api/auth/sso/token": {
      "post": {
        "tags": ["sso"],
        "summary": "Re-mint SSO tokens",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TokenPair" } } } }
        }
      }
    },
    "/api/auth/sso/callback": {
      "get": {
        "tags": ["sso"],
        "summary": "SSO callback handler (legacy mini-apps)",
        "parameters": [
          { "name": "token", "in": "query", "schema": { "type": "string" } },
          { "name": "access_token", "in": "query", "schema": { "type": "string" } },
          { "name": "user_id", "in": "query", "schema": { "type": "integer" } },
          { "name": "username", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "token": { "type": "string" }, "user_id": { "type": "integer" }, "username": { "type": "string" } } } } } }
        }
      }
    },
    "/api/oauth/authorize": {
      "get": {
        "tags": ["oauth"],
        "summary": "OAuth 2.0 authorization endpoint",
        "parameters": [
          { "name": "client_id", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "response_type", "in": "query", "required": true, "schema": { "type": "string", "enum": ["code"] } },
          { "name": "redirect_uri", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "scope", "in": "query", "schema": { "type": "string" } },
          { "name": "state", "in": "query", "schema": { "type": "string" } },
          { "name": "code_challenge", "in": "query", "schema": { "type": "string" } },
          { "name": "code_challenge_method", "in": "query", "schema": { "type": "string", "enum": ["S256", "plain"] } }
        ],
        "responses": { "302": { "description": "Redirect to redirect_uri with code + state" } }
      }
    },
    "/api/oauth/token": {
      "post": {
        "tags": ["oauth"],
        "summary": "OAuth 2.0 token exchange",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": ["grant_type", "code", "redirect_uri", "client_id"],
                "properties": {
                  "grant_type": { "type": "string", "enum": ["authorization_code", "refresh_token"] },
                  "code": { "type": "string" },
                  "redirect_uri": { "type": "string" },
                  "client_id": { "type": "string" },
                  "client_secret": { "type": "string" },
                  "code_verifier": { "type": "string" },
                  "refresh_token": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TokenPair" } } } }
        }
      }
    },
    "/api/teams": {
      "get": {
        "tags": ["teams"],
        "summary": "List teams the authenticated user belongs to",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "teams": { "type": "array", "items": { "$ref": "#/components/schemas/Team" } } } } } } }
        }
      },
      "post": {
        "tags": ["teams"],
        "summary": "Create a team",
        "security": [{ "bearerAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name", "slug"],
                "properties": {
                  "name": { "type": "string" },
                  "slug": { "type": "string" },
                  "description": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "team": { "$ref": "#/components/schemas/Team" } } } } } }
        }
      }
    },
    "/api/teams/{slug}": {
      "get": {
        "tags": ["teams"],
        "summary": "Team detail",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "OK" }, "404": { "description": "Not found" } }
      }
    },
    "/api/teams/{slug}/members": {
      "parameters": [{ "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } }],
      "get": {
        "tags": ["teams"],
        "summary": "List team members",
        "security": [{ "bearerAuth": [] }],
        "responses": { "200": { "description": "OK" } }
      },
      "post": {
        "tags": ["teams"],
        "summary": "Add member",
        "security": [{ "bearerAuth": [] }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "required": ["username"], "properties": { "username": { "type": "string" }, "role": { "type": "string" } } } } }
        },
        "responses": { "201": { "description": "Added" } }
      }
    },
    "/api/teams/{slug}/members/{username}": {
      "parameters": [
        { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } },
        { "name": "username", "in": "path", "required": true, "schema": { "type": "string" } }
      ],
      "put": {
        "tags": ["teams"],
        "summary": "Update member role",
        "security": [{ "bearerAuth": [] }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "properties": { "role": { "type": "string" } } } } }
        },
        "responses": { "200": { "description": "Updated" } }
      },
      "delete": {
        "tags": ["teams"],
        "summary": "Remove member",
        "security": [{ "bearerAuth": [] }],
        "responses": { "204": { "description": "Removed" } }
      }
    },
    "/api/user/teams": {
      "get": {
        "tags": ["teams"],
        "summary": "List teams (alias of /api/teams)",
        "security": [{ "bearerAuth": [] }],
        "responses": { "200": { "description": "OK" } }
      }
    },
    "/api/teams/{slug}/invite": {
      "post": {
        "tags": ["teams"],
        "summary": "Send team invitation by email",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "required": ["email"], "properties": { "email": { "type": "string", "format": "email" } } } } }
        },
        "responses": { "200": { "description": "Invitation sent" } }
      }
    },
    "/api/invitations/{token}": {
      "post": {
        "tags": ["teams"],
        "summary": "Accept team invitation",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "name": "token", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "Joined" } }
      }
    },
    "/api/user/invitations": {
      "get": {
        "tags": ["teams"],
        "summary": "List pending invitations for the user",
        "security": [{ "bearerAuth": [] }],
        "responses": { "200": { "description": "OK" } }
      }
    },
    "/api/status": {
      "get": {
        "tags": ["utility"],
        "summary": "Liveness + sanity check",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "example": "ok" },
                    "total_users": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
