openapi: 3.1.0

info:
    title: Bigscreen Auth API
    version: "0"
    summary: Auth + identity surface of apps/api — login, account creation / profile, password reset, OAuth.
    description: |
        The auth surface of [`apps/api/api.ts`](../../apps/api/api.ts).
        Everything to do with identity: account creation, login (email
        + password, Steam, Oculus, magic-link), token verify / renew,
        profile retrieval and update, identity attach / detach, password
        reset, email verification, plus the OAuth 2.0 token endpoint and
        RFC 7517 / RFC 8414 well-known discovery.

        Sister spec: [`public-api`](./scalar/public.html) on the same
        service hosts everything non-auth — media, Beyond scans / orders,
        channels, topics, analytics, reports, eye-tracking, the Shopify
        webhook. Both bind to port `3009` and share the `api` bearer key —
        the split is documentation-only.

        > Distinct from [`apps/admin_api`](./scalar/admin.html) (admin
        > pipeline) and the cloud APIs ([cloud admin](./scalar/cloud.html),
        > [cloud public](./scalar/cloud-public.html)).

        ## Auth at a glance

        Most requests need two headers:

        | Header | Value |
        |--------|-------|
        | `Authorization` | `Bearer <api app key>` |
        | `x-access-token` | A JWT issued by `/auth/login` (or one of the
                             other login flows) — required on routes marked
                             `Auth: required` |

        Bypasses (no `Authorization` bearer needed):

        - `/oauth/token`, `/.well-known/jwks.json`,
          `/.well-known/oauth-authorization-server` — public per
          RFC 7517 / RFC 8414. Authenticate via `client_id` + `client_secret`
          in the request body. Gated on `OAUTH_TOKEN_ENABLED=true`;
          otherwise return `501`.

        Token renewal is via `GET /auth/renew` (or the legacy
        `POST /renew`) with the refresh token in `x-refresh-token` and a
        nonce in `x-nonce`. When the access token has expired, the server
        returns a fresh nonce in the `x-nonce` response header so the
        client can immediately retry against `/auth/renew`.

        ## Spec layout

        Sources are split per handler module under
        `docs/openapi/auth-modules/*.yaml`, joined by `$ref` from this
        root file. The bundled artifact at
        `docs/openapi/auth-api.bundled.yaml` is what the Scalar renderer
        reads — regenerate it with
        `npx -y @redocly/cli@latest bundle docs/openapi/auth-api.yaml -o docs/openapi/auth-api.bundled.yaml`
        after any edit.

servers:
    - url: http://localhost:3009
      description: Local dev (started by `yarn dev` — port `3009` is hard-coded in `apps/api/api.ts`).
    - url: https://api.bigscreenvr.com
      description: Production.

tags:
    - name: OAuth
      description: |
          OAuth 2.0 token endpoint plus RFC 7517 / RFC 8414 well-known
          discovery. Mounted before the standard auth middleware — clients
          authenticate via `client_id` + `client_secret`, not the Bigscreen
          bearer key. All three return `501` when
          `OAUTH_TOKEN_ENABLED` is not `true`.
    - name: Auth
      description: |
          Login, logout, access-token renew/verify, magic-link redemption,
          and the `whoami` debug endpoint. Username/password, Steam, and
          Oculus flows all live here.
    - name: Accounts
      description: |
          Account creation, profile retrieval and update, identity attach /
          detach (Steam, Oculus, email/password), email verification,
          per-device status, and feature flags.
    - name: Password
      description: Forgot-password / reset flow plus email-verification resend.

security:
    - apiKey: []
      accessToken: []

paths:
    /oauth/token:
        $ref: "./auth-modules/oauth.yaml#/~1oauth~1token"
    /.well-known/jwks.json:
        $ref: "./auth-modules/oauth.yaml#/~1.well-known~1jwks.json"
    /.well-known/oauth-authorization-server:
        $ref: "./auth-modules/oauth.yaml#/~1.well-known~1oauth-authorization-server"

    /auth/whoami:
        $ref: "./auth-modules/auth.yaml#/~1auth~1whoami"
    /login:
        $ref: "./auth-modules/auth.yaml#/~1login"
    /renew:
        $ref: "./auth-modules/auth.yaml#/~1renew"
    /verify:
        $ref: "./auth-modules/auth.yaml#/~1verify"
    /auth/login:
        $ref: "./auth-modules/auth.yaml#/~1auth~1login"
    /auth/logout:
        $ref: "./auth-modules/auth.yaml#/~1auth~1logout"
    /auth/oculus/login:
        $ref: "./auth-modules/auth.yaml#/~1auth~1oculus~1login"
    /auth/steam/login:
        $ref: "./auth-modules/auth.yaml#/~1auth~1steam~1login"
    /auth/verify:
        $ref: "./auth-modules/auth.yaml#/~1auth~1verify"
    /auth/renew:
        $ref: "./auth-modules/auth.yaml#/~1auth~1renew"
    /auth/oculus/verify:
        $ref: "./auth-modules/auth.yaml#/~1auth~1oculus~1verify"
    /auth/token/login/{singleUseLoginToken}:
        $ref: "./auth-modules/auth.yaml#/~1auth~1token~1login~1{singleUseLoginToken}"
    /auth/token2/login/{publicToken}:
        $ref: "./auth-modules/auth.yaml#/~1auth~1token2~1login~1{publicToken}"

    /auth/account:
        $ref: "./auth-modules/accounts.yaml#/~1auth~1account"
    /auth/profile:
        $ref: "./auth-modules/accounts.yaml#/~1auth~1profile"
    /auth/account/attach:
        $ref: "./auth-modules/accounts.yaml#/~1auth~1account~1attach"
    /auth/steam/account:
        $ref: "./auth-modules/accounts.yaml#/~1auth~1steam~1account"
    /auth/oculus/account:
        $ref: "./auth-modules/accounts.yaml#/~1auth~1oculus~1account"
    /auth/steam/attach:
        $ref: "./auth-modules/accounts.yaml#/~1auth~1steam~1attach"
    /auth/steam/detach:
        $ref: "./auth-modules/accounts.yaml#/~1auth~1steam~1detach"
    /auth/email/verify/{token}:
        $ref: "./auth-modules/accounts.yaml#/~1auth~1email~1verify~1{token}"
    /auth/email/update/{emailUpdateToken}:
        $ref: "./auth-modules/accounts.yaml#/~1auth~1email~1update~1{emailUpdateToken}"
    /auth/device/{deviceUniqueIdentifier}:
        $ref: "./auth-modules/accounts.yaml#/~1auth~1device~1{deviceUniqueIdentifier}"
    /auth/account/flags:
        $ref: "./auth-modules/accounts.yaml#/~1auth~1account~1flags"

    /auth/forgot:
        $ref: "./auth-modules/password.yaml#/~1auth~1forgot"
    /auth/reset:
        $ref: "./auth-modules/password.yaml#/~1auth~1reset"
    /auth/reset/{resetPasswordToken}:
        $ref: "./auth-modules/password.yaml#/~1auth~1reset~1{resetPasswordToken}"
    /auth/resend:
        $ref: "./auth-modules/password.yaml#/~1auth~1resend"

components:
    securitySchemes:
        apiKey:
            type: http
            scheme: bearer
            bearerFormat: ApiKey
            description: |
                Bearer key registered for the `api` application. May be a
                64-character app key or, when OAuth is enabled, an OAuth 2.0
                JWT issued by `/oauth/token` — admission shape determines
                which path runs.
        accessToken:
            type: apiKey
            in: header
            name: x-access-token
            description: |
                JWT for the end-user account the request is acting as.
                Required on routes documented as `Auth: required`. Some
                routes are documented as `Auth: optional` — the JWT, if
                present, scopes / personalises the response, but its
                absence is not an error.
        refreshToken:
            type: apiKey
            in: header
            name: x-refresh-token
            description: |
                Refresh token issued alongside the access token. Required
                on `/auth/renew` and the legacy `/renew` to mint a new
                access token.
        nonce:
            type: apiKey
            in: header
            name: x-nonce
            description: |
                One-time nonce returned by the server in the `x-nonce`
                response header when an access token has expired. Required
                on the next `/auth/renew` call.

    responses:
        Unauthorized:
            description: Missing or invalid `Authorization` / `x-access-token`.
            content:
                application/json:
                    schema: { $ref: "#/components/schemas/Error" }
        NotFound:
            description: Resource not found.
            content:
                application/json:
                    schema: { $ref: "#/components/schemas/Error" }

    headers:
        AccessToken:
            description: Newly-minted access-token JWT. Clients should replace any prior `x-access-token` value with this one.
            schema: { type: string }
        RefreshToken:
            description: Newly-minted refresh token. Required on subsequent `/auth/renew` calls.
            schema: { type: string }
        Timestamp:
            description: Server-side issue timestamp (Unix milliseconds).
            schema: { type: string }
        Nonce:
            description: One-time nonce required on the next `/auth/renew` call.
            schema: { type: string }

    schemas:
        Error:
            type: object
            required: [error]
            properties:
                error: { type: string }
                code:
                    type: string
                    description: Optional machine-readable error code.
                details:
                    description: Optional structured details.
            example:
                error: "Invalid credentials"
                code: "InvalidCredentials"
