openapi: 3.1.0
info:
  title: Nucleo OAuth & OpenID Connect API
  version: "2026-10"
  summary: Authorization server for apps and AI assistants that act on behalf of a Nucleo user.
  description: |
    Nucleo Core is the identity provider of the platform: every person signs in once at
    `auth.nucleoplatform.com` and every module (Brain, Catalog, Commerce, Intelligence) trusts the
    access tokens it issues.

    External clients — MCP connectors in Claude or ChatGPT, partner tools, local scripts — use the
    standard **OAuth 2.0 authorization code flow with PKCE (S256)**. They can discover the endpoints
    from the metadata documents (RFC 8414 / OpenID Connect Discovery) and register themselves with
    **Dynamic Client Registration** (RFC 7591), without asking Nucleo for credentials. With the scope
    `openid` the code exchange also returns a signed **ID token** (OpenID Connect Core).

    Every token belongs to a person: there is no server-to-server (`client_credentials`) grant. A
    partner integration acts on behalf of the user who connected it.

    Tokens issued to self-registered clients always carry the scope `nucleo.ai_tools`: they are valid
    for the Nucleo AI tools (the MCP connector and the modules' `/api/v1/ai-tools`), for `userinfo`
    and for revoking themselves — every other Nucleo API answers `403 insufficient_scope`.
    The user always sees a consent screen before such a client receives a token.
x-nucleo:
  product: oauth
  module: platform
  audience: [ai-assistant, partner]
  stability: stable
  format: json
  order: 1
servers:
  - url: https://auth.nucleoplatform.com
    description: Production issuer (`iss`). All endpoints below are relative to it.
tags:
  - name: Discovery
    description: |
      Machine-readable metadata. Start here: a compliant client needs nothing else than the issuer URL
      `https://auth.nucleoplatform.com`.
  - name: Client registration
    description: |
      Dynamic Client Registration (RFC 7591). Anonymous by design (as required by the MCP
      authorization spec): the guard rails are on the metadata — `https` (or loopback) redirect URIs,
      authorization code grant only, a fixed set of scopes. Self-registered clients are always
      treated as third-party: the user approves them on a consent screen.
  - name: Authorization
    description: |
      Browser-facing part of the authorization code flow: sign-in, consent, redirect back with a code.
  - name: Tokens
    description: |
      Token endpoint and token revocation. Access tokens and ID tokens are RS256 JWTs signed with the key
      published in the JWKS; access tokens last **8 hours**, ID tokens **1 hour**. Refresh tokens last
      **30 days**.
  - name: Identity
    description: The signed-in person, their companies and module access (OpenID Connect UserInfo).
  - name: Organization branding
    description: |
      Public, unauthenticated branding of a company (name, logos, favicons), used by Nucleo's public
      pages (for example booking pages) that have no signed-in user but must carry the company's brand.
paths:
  /.well-known/openid-configuration:
    get:
      operationId: getOpenIdConfiguration
      summary: Get OpenID Connect discovery document
      tags: [Discovery]
      security: []
      description: |
        OpenID Connect Discovery 1.0 document. Public, cacheable, served with
        `Access-Control-Allow-Origin: *` so browser-based clients can read it.

        Notes on the current implementation:
        - `response_types_supported` is only `code`; implicit and hybrid flows are not available.
        - `grant_types_supported` is `authorization_code` and `refresh_token`: there is no
          `client_credentials` grant.
        - `code_challenge_methods_supported` is only `S256`.
        - ID tokens are signed with `RS256` and returned by the code exchange when the scope includes
          `openid`; `claims_supported` lists every claim they can carry. The `request`, `request_uri`
          and `claims` authorization parameters are not supported.
        - For everything beyond the ID token claims (companies, module access) call
          [`userinfo`](#operation/getUserInfo) with the access token.
      responses:
        "200":
          description: Discovery document.
          headers:
            Access-Control-Allow-Origin:
              schema: { type: string, example: "*" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OpenIdConfiguration" }
              examples:
                production:
                  value:
                    issuer: https://auth.nucleoplatform.com
                    authorization_endpoint: https://auth.nucleoplatform.com/oauth/authorize
                    token_endpoint: https://auth.nucleoplatform.com/oauth/token
                    userinfo_endpoint: https://auth.nucleoplatform.com/api/oauth/userinfo
                    jwks_uri: https://auth.nucleoplatform.com/.well-known/jwks.json
                    registration_endpoint: https://auth.nucleoplatform.com/oauth/register
                    scopes_supported: [openid, profile, email, offline_access, nucleo.ai_tools]
                    response_types_supported: [code]
                    response_modes_supported: [query]
                    grant_types_supported: [authorization_code, refresh_token]
                    code_challenge_methods_supported: [S256]
                    token_endpoint_auth_methods_supported: [client_secret_post, client_secret_basic, none]
                    service_documentation: https://nucleoplatform.com/developers
                    subject_types_supported: [public]
                    id_token_signing_alg_values_supported: [RS256]
                    claims_supported: [iss, sub, aud, azp, exp, iat, jti, auth_time, nonce, email, email_verified, name, picture, locale]
                    request_parameter_supported: false
                    request_uri_parameter_supported: false
                    claims_parameter_supported: false
  /.well-known/oauth-authorization-server:
    get:
      operationId: getAuthorizationServerMetadata
      summary: Get OAuth authorization server metadata
      tags: [Discovery]
      security: []
      description: |
        OAuth 2.0 Authorization Server Metadata (RFC 8414). This is the document MCP clients reach after
        reading the protected-resource metadata of the Nucleo MCP server
        (`https://mcp.nucleoplatform.com/.well-known/oauth-protected-resource`); from here they take
        `registration_endpoint` to register themselves. Public, with `Access-Control-Allow-Origin: *`.
      responses:
        "200":
          description: Authorization server metadata.
          headers:
            Access-Control-Allow-Origin:
              schema: { type: string, example: "*" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AuthorizationServerMetadata" }
              examples:
                production:
                  value:
                    issuer: https://auth.nucleoplatform.com
                    authorization_endpoint: https://auth.nucleoplatform.com/oauth/authorize
                    token_endpoint: https://auth.nucleoplatform.com/oauth/token
                    registration_endpoint: https://auth.nucleoplatform.com/oauth/register
                    jwks_uri: https://auth.nucleoplatform.com/.well-known/jwks.json
                    userinfo_endpoint: https://auth.nucleoplatform.com/api/oauth/userinfo
                    scopes_supported: [openid, profile, email, offline_access, nucleo.ai_tools]
                    response_types_supported: [code]
                    response_modes_supported: [query]
                    grant_types_supported: [authorization_code, refresh_token]
                    code_challenge_methods_supported: [S256]
                    token_endpoint_auth_methods_supported: [client_secret_post, client_secret_basic, none]
                    service_documentation: https://nucleoplatform.com/developers
  /.well-known/jwks.json:
    get:
      operationId: getJwks
      summary: Get the token signing keys
      tags: [Discovery]
      security: []
      description: |
        JSON Web Key Set with the RSA public key that signs Nucleo access tokens and ID tokens (`RS256`).
        The `kid` is the base64url SHA-256 of the public key, so it changes only when the key is rotated;
        ID tokens carry it in their header. Use it to verify a token locally (signature, `exp`, `aud` =
        your `client_id`). Public, with `Access-Control-Allow-Origin: *`. Returns `{"keys": []}` only if
        the server has no key configured.
      responses:
        "200":
          description: Key set.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Jwks" }
              examples:
                production:
                  value:
                    keys:
                      - kty: RSA
                        use: sig
                        alg: RS256
                        kid: 3rL1x0dXq6m0yBqH2l7xZr9n0bq8f4k1s2vJt8o5pWc
                        n: 0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw
                        e: AQAB
  /oauth/register:
    options:
      operationId: preflightClientRegistration
      summary: CORS preflight for client registration
      tags: [Client registration]
      security: []
      description: Lets browser-based clients register themselves. Always answers `204` with permissive CORS headers.
      responses:
        "204":
          description: Preflight accepted.
          headers:
            Access-Control-Allow-Origin:
              schema: { type: string, example: "*" }
            Access-Control-Allow-Methods:
              schema: { type: string, example: "POST, OPTIONS" }
            Access-Control-Allow-Headers:
              schema: { type: string, example: "Content-Type, Authorization" }
    post:
      operationId: registerClient
      summary: Register an OAuth client dynamically
      tags: [Client registration]
      security: []
      x-rate-limit: { limit: 10, window: "1m", scope: "per IP" }
      description: |
        RFC 7591 Dynamic Client Registration. No authentication: any client can register, the user's
        consent is what protects the account.

        Rules applied to the metadata:
        - `redirect_uris` is **required**, 1 to 5 absolute URIs, `https` only — `http` is accepted only
          for loopback hosts (`localhost`, `127.0.0.1`, `[::1]`, RFC 8252). No fragment, no `*`.
          Duplicates are removed.
        - `grant_types` defaults to `["authorization_code"]`; only `authorization_code` and
          `refresh_token` are accepted and `authorization_code` must be present. `refresh_token` is
          always added.
        - `response_types` may only contain `code`.
        - `token_endpoint_auth_method` defaults to `none` (public client + PKCE, as MCP connectors
          work). `client_secret_post` or `client_secret_basic` make it a confidential client and the
          response includes a `client_secret` that never expires — it is shown **only once**.
        - `scope` is filtered (not rejected) to the supported set; when nothing valid is requested you
          get all of them. `nucleo.ai_tools` is always added.
        - `client_name` is stripped of HTML and cut to 120 characters (default: the host of the first
          redirect URI); `client_uri` is cut to 250 characters. Other RFC 7591 fields are ignored.

        Every call creates a **new** client (not idempotent). Clients registered here are third-party:
        the user always sees the consent screen, and their tokens are limited to the AI tools
        (`nucleo.ai_tools`).

        Rate limit: 10 requests per minute per IP address.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ClientRegistrationRequest" }
            examples:
              mcpConnector:
                summary: Public client (MCP connector)
                value:
                  client_name: Acme Assistant
                  redirect_uris: [https://assistant.acme.example/oauth/callback]
                  grant_types: [authorization_code, refresh_token]
                  response_types: [code]
                  token_endpoint_auth_method: none
                  scope: openid profile email offline_access
              confidential:
                summary: Confidential client (server-side app)
                value:
                  client_name: Acme Reporting
                  client_uri: https://reports.acme.example
                  redirect_uris: [https://reports.acme.example/auth/nucleo/callback]
                  token_endpoint_auth_method: client_secret_basic
                  scope: openid email
              localTool:
                summary: Local tool with loopback redirect
                value:
                  client_name: acme-cli
                  redirect_uris: [http://127.0.0.1:8765/callback]
      responses:
        "201":
          description: Client created.
          headers:
            Access-Control-Allow-Origin:
              schema: { type: string, example: "*" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ClientRegistrationResponse" }
              examples:
                public:
                  summary: Public client
                  value:
                    client_id: 9d3f2b1c-6a4e-4f7b-9c2d-1e8a7b6c5d4f
                    client_id_issued_at: 1791100800
                    client_name: Acme Assistant
                    redirect_uris: [https://assistant.acme.example/oauth/callback]
                    grant_types: [authorization_code, refresh_token]
                    response_types: [code]
                    token_endpoint_auth_method: none
                    scope: openid profile email offline_access nucleo.ai_tools
                confidential:
                  summary: Confidential client
                  value:
                    client_id: 9d3f2b1c-7b5f-4a8c-8d3e-2f9b8c7d6e5a
                    client_id_issued_at: 1791100800
                    client_secret: s3cr3tExampleValueOnlyShownOnce000000000
                    client_secret_expires_at: 0
                    client_name: Acme Reporting
                    redirect_uris: [https://reports.acme.example/auth/nucleo/callback]
                    grant_types: [authorization_code, refresh_token]
                    response_types: [code]
                    token_endpoint_auth_method: client_secret_basic
                    scope: openid email nucleo.ai_tools
        "400":
          description: Invalid client metadata (RFC 7591 §3.2.2 error response).
          headers:
            Access-Control-Allow-Origin:
              schema: { type: string, example: "*" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OAuthError" }
              examples:
                missingRedirect:
                  value: { error: invalid_redirect_uri, error_description: redirect_uris is required and must be a non-empty array. }
                tooMany:
                  value: { error: invalid_redirect_uri, error_description: Too many redirect_uris (max 5). }
                insecure:
                  value: { error: invalid_redirect_uri, error_description: redirect_uris must use https (http is allowed only for loopback addresses). }
                fragment:
                  value: { error: invalid_redirect_uri, error_description: redirect_uris must not contain a fragment. }
                wildcard:
                  value: { error: invalid_redirect_uri, error_description: Wildcards are not allowed in redirect_uris. }
                grant:
                  value: { error: invalid_client_metadata, error_description: "Unsupported grant_types: client_credentials. Only authorization_code and refresh_token are supported." }
                responseType:
                  value: { error: invalid_client_metadata, error_description: Only the "code" response_type is supported. }
                authMethod:
                  value: { error: invalid_client_metadata, error_description: Unsupported token_endpoint_auth_method. }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /oauth/authorize:
    get:
      operationId: authorize
      summary: Start the authorization code flow
      tags: [Authorization]
      security: []
      description: |
        Open this URL in the user's browser (not in an embedded web view). Nucleo:
        1. shows the Nucleo sign-in page (`/login`) if the user has no session;
        2. shows a **consent screen** naming your client and the requested scopes (self-registered
           clients always see it; Nucleo's own apps skip it);
        3. redirects to `redirect_uri` with `code` and `state` — or with `error=access_denied` if the
           user clicks *Cancel*.

        PKCE is required for public clients (`token_endpoint_auth_method: none`) and only `S256` is
        advertised. Authorization codes are single-use and short-lived (10 minutes): exchange them
        immediately at `/oauth/token`.

        `redirect_uri` must match exactly one of the URIs registered for the client. Errors that make
        the redirect unsafe (unknown client, unregistered `redirect_uri`) are shown to the user instead
        of being redirected.
      parameters:
        - { name: response_type, in: query, required: true, schema: { type: string, enum: [code] } }
        - name: client_id
          in: query
          required: true
          schema: { type: string }
          example: 9d3f2b1c-6a4e-4f7b-9c2d-1e8a7b6c5d4f
        - name: redirect_uri
          in: query
          required: true
          schema: { type: string, format: uri }
          example: https://assistant.acme.example/oauth/callback
        - name: scope
          in: query
          required: false
          description: Space-separated. Unknown scopes are rejected with `invalid_scope`.
          schema: { type: string }
          example: openid profile email offline_access
        - name: state
          in: query
          required: false
          description: Opaque value returned unchanged; strongly recommended against CSRF.
          schema: { type: string }
          example: af0ifjsldkj
        - name: code_challenge
          in: query
          required: false
          description: "BASE64URL(SHA256(code_verifier)). Required for public clients."
          schema: { type: string, minLength: 43, maxLength: 128 }
          example: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
        - name: code_challenge_method
          in: query
          required: false
          schema: { type: string, enum: [S256] }
        - name: nonce
          in: query
          required: false
          description: |
            OpenID Connect nonce (up to 255 characters). Returned unchanged in the `nonce` claim of the
            ID token; check it there to bind the ID token to your sign-in request.
          schema: { type: string, maxLength: 255 }
          example: n-0S6_WzA2Mj
        - name: prompt
          in: query
          required: false
          description: "`login` forces a new sign-in, `consent` forces the consent screen, `none` fails with `login_required` / `consent_required` instead of showing UI."
          schema: { type: string, enum: [none, login, consent] }
      responses:
        "200":
          description: Sign-in page or consent screen (HTML).
          content:
            text/html:
              schema: { type: string }
        "302":
          description: |
            Redirect. Either to `/login` (no session), or back to the client:
            `https://assistant.acme.example/oauth/callback?code=def502…&state=af0ifjsldkj` on approval,
            `…?error=access_denied&error_description=…&state=af0ifjsldkj` on denial or invalid request.
          headers:
            Location:
              schema: { type: string, format: uri }
              example: https://assistant.acme.example/oauth/callback?code=def50200a1b2c3&state=af0ifjsldkj
  /oauth/token:
    post:
      operationId: issueToken
      summary: Exchange a code or refresh a token
      tags: [Tokens]
      security: []
      x-rate-limit: { limit: 60, window: "1m", scope: "per IP" }
      description: |
        Standard OAuth 2.0 token endpoint (`application/x-www-form-urlencoded` or JSON).

        - **authorization_code**: send `code`, `redirect_uri` (same as in the authorize request),
          `client_id`, and `code_verifier` (PKCE). Confidential clients also authenticate with
          `client_secret` (body) or HTTP Basic.
        - **refresh_token**: send `refresh_token` and `client_id` (plus secret for confidential
          clients). A **new refresh token** is returned and the previous one is revoked — always store
          the latest one.
        - There is no `client_credentials` grant: every token acts for a person.

        **ID token.** When the granted scope includes `openid`, the `authorization_code` exchange also
        returns `id_token`: an RS256 JWT signed with the JWKS key (`kid` in the header), valid for 1 hour.
        Claims: `iss` (`https://auth.nucleoplatform.com`), `sub` (same as `userinfo`), `aud` and `azp`
        (your `client_id`), `iat`, `exp`, `jti`, `auth_time` (when the person signed in to Nucleo), and
        `nonce` if you sent one to `/oauth/authorize`. With `email`: `email`, `email_verified`. With
        `profile`: `name`, `locale`, and `picture` when the person has an avatar. The refresh exchange
        does not return a new ID token: keep the one from sign-in. An ID token is not an access token —
        sending it as `Bearer` gets `401`.

        Lifetimes: access token 8 hours (`expires_in: 28800`), refresh token 30 days. For
        self-registered clients the granted scope always includes `nucleo.ai_tools`.

        Rate limit: 60 requests per minute per IP address.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: "#/components/schemas/TokenRequest" }
            examples:
              authorizationCode:
                value:
                  grant_type: authorization_code
                  client_id: 9d3f2b1c-6a4e-4f7b-9c2d-1e8a7b6c5d4f
                  redirect_uri: https://assistant.acme.example/oauth/callback
                  code: def50200a1b2c3
                  code_verifier: dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
              refresh:
                value:
                  grant_type: refresh_token
                  client_id: 9d3f2b1c-6a4e-4f7b-9c2d-1e8a7b6c5d4f
                  refresh_token: def50200f9e8d7c6
          application/json:
            schema: { $ref: "#/components/schemas/TokenRequest" }
      responses:
        "200":
          description: Tokens issued.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TokenResponse" }
              examples:
                success:
                  value:
                    token_type: Bearer
                    expires_in: 28800
                    access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiI5ZDNmMmIxYy02YTRlIiwic3ViIjoiNDIiLCJzY29wZXMiOlsib3BlbmlkIiwibnVjbGVvLmFpX3Rvb2xzIl19.signature
                    refresh_token: def50200f9e8d7c6b5a4
                    id_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjNyTDF4MGRYcTZtMHlCcUgybDd4WnI5bjBicThmNGsxczJ2SnQ4bzVwV2MifQ.eyJpc3MiOiJodHRwczovL2F1dGgubnVjbGVvcGxhdGZvcm0uY29tIiwic3ViIjoiNDIiLCJhdWQiOiI5ZDNmMmIxYy02YTRlLTRmN2ItOWMyZC0xZThhN2I2YzVkNGYiLCJhenAiOiI5ZDNmMmIxYy02YTRlLTRmN2ItOWMyZC0xZThhN2I2YzVkNGYiLCJpYXQiOjE3OTExMDA4MDAsImV4cCI6MTc5MTEwNDQwMCwianRpIjoiMGI2ZjFkMmUtNGMzYS00ZThiLTlmN2QtMmExYzNlNWI3ZDlmIiwiYXV0aF90aW1lIjoxNzkxMTAwNzUwLCJub25jZSI6Im4tMFM2X1d6QTJNaiIsImVtYWlsIjoiZ2l1bGlhLnJvc3NpQGFjbWUuZXhhbXBsZSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlfQ.signature
        "400":
          description: Invalid request, grant or scope.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OAuthServerError" }
              examples:
                invalidGrant:
                  value:
                    error: invalid_grant
                    error_description: The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.
                    hint: Failed to verify `code_verifier`.
                unsupportedGrant:
                  value:
                    error: unsupported_grant_type
                    error_description: The authorization grant type is not supported by the authorization server.
                    hint: Check that all required parameters have been provided
        "401":
          description: Client authentication failed.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OAuthServerError" }
              examples:
                invalidClient:
                  value:
                    error: invalid_client
                    error_description: Client authentication failed
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/oauth/userinfo:
    get:
      operationId: getUserInfo
      summary: Get the signed-in user
      tags: [Identity]
      security:
        - nucleoOAuth: [openid]
        - bearerToken: []
      description: |
        OpenID Connect UserInfo. Returns who the token belongs to, the companies (organizations) they
        belong to and the Nucleo modules they can use, with their role.

        - `sub` is the stable user id: key your local records on it, never on `email`.
        - `orgs[].id` is the company id to pass as `X-Nucleo-Company` to the AI tools.
        - `apps[]` lists module access per company (`org_id: null` = global access for Nucleo staff).
          During the module migration legacy slugs may appear next to `brain`, `catalog`, `commerce`,
          `intelligence`: ignore slugs you do not know.
        - `disabled_sections` lists, per company id, the sub-modules switched off for that company.
        - `scope` is the effective scope of the token: `nucleo.ai_tools` marks a token of a
          self-registered (AI) client.
        - `impersonator` is non-null only when Nucleo staff is acting as the user.

        Available to self-registered clients' tokens.
      responses:
        "200":
          description: The user.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UserInfo" }
              examples:
                customer:
                  value:
                    sub: "42"
                    name: Giulia Rossi
                    email: giulia.rossi@acme.example
                    email_verified: true
                    avatar_url: https://cdn.acme.example/avatars/42.png
                    language: it
                    theme: light
                    is_internal: false
                    is_owner: false
                    org_ids: [7]
                    orgs:
                      - { id: 7, slug: acme, name: Acme Apparel, logo_url: https://cdn.acme.example/logo.png, role: admin }
                    app_access: [catalog, commerce]
                    apps:
                      - { slug: catalog, role: editor, org_id: 7 }
                      - { slug: commerce, role: viewer, org_id: 7 }
                    disabled_sections:
                      "7": [pos]
                    client_id: 9d3f2b1c-6a4e-4f7b-9c2d-1e8a7b6c5d4f
                    scope: openid profile email offline_access nucleo.ai_tools
                    impersonator: null
        "401": { $ref: "#/components/responses/Unauthenticated" }
  /api/auth/revoke:
    post:
      operationId: revokeCurrentToken
      summary: Revoke the current access token
      tags: [Tokens]
      security:
        - nucleoOAuth: []
        - bearerToken: []
      description: |
        Revokes the access token sent in `Authorization` and every refresh token issued with it
        (sign-out / disconnect). Idempotent from the client's point of view: a revoked token then
        answers `401`, on the MCP connector immediately as well. Available to self-registered clients'
        tokens. The confirmation message is currently returned in Italian.
      responses:
        "200":
          description: Token revoked.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
              examples:
                revoked:
                  value: { message: Token revocato. }
        "401": { $ref: "#/components/responses/Unauthenticated" }
  /api/public/organizations/{slug}/branding:
    get:
      operationId: getOrganizationBranding
      summary: Get a company's public branding
      tags: [Organization branding]
      security: []
      x-rate-limit: { limit: 120, window: "1m", scope: "per IP" }
      description: |
        Name, logos (light and dark) and favicons of a company, by slug. Only data that is public by
        nature (logo URLs point to public storage) — never members, domains or billing. Cached for
        5 minutes on the server and sent with `Cache-Control: public, max-age=300`.

        Rate limit: 120 requests per minute per IP address.
      parameters:
        - name: slug
          in: path
          required: true
          description: Company slug, lowercase. Must match `^[a-z0-9][a-z0-9-]{0,63}$`, otherwise `404`.
          schema: { type: string, pattern: "^[a-z0-9][a-z0-9-]{0,63}$", maxLength: 64 }
          example: acme
      responses:
        "200":
          description: Branding.
          headers:
            Cache-Control:
              schema: { type: string, example: "public, max-age=300" }
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: "#/components/schemas/OrganizationBranding" }
              examples:
                acme:
                  value:
                    data:
                      slug: acme
                      name: Acme Apparel
                      logo_url: https://cdn.acme.example/brand/logo.svg
                      logo_dark_url: https://cdn.acme.example/brand/logo-dark.svg
                      favicon_url: https://cdn.acme.example/brand/favicon.png
                      favicon_light_url: null
        "404":
          description: Unknown or invalid slug.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
              examples:
                notFound:
                  value: { message: Not found }
        "429": { $ref: "#/components/responses/TooManyRequests" }
components:
  securitySchemes:
    nucleoOAuth:
      type: oauth2
      description: |
        Authorization code with PKCE (S256). Register your client at `/oauth/register` (or ask Nucleo for a
        pre-registered one). Self-registered clients' tokens always carry `nucleo.ai_tools`.
      flows:
        authorizationCode:
          authorizationUrl: https://auth.nucleoplatform.com/oauth/authorize
          tokenUrl: https://auth.nucleoplatform.com/oauth/token
          refreshUrl: https://auth.nucleoplatform.com/oauth/token
          scopes:
            openid: OpenID Connect sign-in
            profile: Name and avatar
            email: Email address
            offline_access: Long-lived access through a refresh token
            nucleo.ai_tools: Nucleo tools for AI assistants (added automatically to self-registered clients)
    bearerToken:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: "`Authorization: Bearer <access_token>` obtained from `/oauth/token`."
  responses:
    Unauthenticated:
      description: Missing, expired or revoked access token.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Message" }
          examples:
            unauthenticated:
              value: { message: Unauthenticated. }
    TooManyRequests:
      description: Rate limit exceeded.
      headers:
        Retry-After:
          schema: { type: integer, example: 42 }
        X-RateLimit-Limit:
          schema: { type: integer, example: 10 }
        X-RateLimit-Remaining:
          schema: { type: integer, example: 0 }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Message" }
          examples:
            throttled:
              value: { message: Too Many Attempts. }
  schemas:
    Message:
      type: object
      properties:
        message: { type: string }
    OAuthError:
      type: object
      required: [error]
      properties:
        error:
          type: string
          enum: [invalid_redirect_uri, invalid_client_metadata]
        error_description: { type: string }
    OAuthServerError:
      type: object
      description: Error body of the token endpoint.
      required: [error]
      properties:
        error:
          type: string
          enum: [invalid_request, invalid_client, invalid_grant, unauthorized_client, unsupported_grant_type, invalid_scope, access_denied]
        error_description: { type: string }
        hint: { type: string }
    OpenIdConfiguration:
      type: object
      properties:
        issuer: { type: string, format: uri }
        authorization_endpoint: { type: string, format: uri }
        token_endpoint: { type: string, format: uri }
        userinfo_endpoint: { type: string, format: uri }
        jwks_uri: { type: string, format: uri }
        registration_endpoint: { type: string, format: uri }
        scopes_supported: { type: array, items: { type: string } }
        response_types_supported: { type: array, items: { type: string } }
        grant_types_supported: { type: array, items: { type: string } }
        code_challenge_methods_supported: { type: array, items: { type: string } }
        token_endpoint_auth_methods_supported: { type: array, items: { type: string } }
        response_modes_supported: { type: array, items: { type: string } }
        service_documentation: { type: string, format: uri }
        subject_types_supported: { type: array, items: { type: string } }
        id_token_signing_alg_values_supported: { type: array, items: { type: string, enum: [RS256] } }
        claims_supported: { type: array, items: { type: string } }
        request_parameter_supported: { type: boolean, enum: [false] }
        request_uri_parameter_supported: { type: boolean, enum: [false] }
        claims_parameter_supported: { type: boolean, enum: [false] }
    AuthorizationServerMetadata:
      type: object
      properties:
        issuer: { type: string, format: uri }
        authorization_endpoint: { type: string, format: uri }
        token_endpoint: { type: string, format: uri }
        registration_endpoint: { type: string, format: uri }
        jwks_uri: { type: string, format: uri }
        userinfo_endpoint: { type: string, format: uri }
        scopes_supported: { type: array, items: { type: string } }
        response_types_supported: { type: array, items: { type: string } }
        response_modes_supported: { type: array, items: { type: string } }
        grant_types_supported: { type: array, items: { type: string } }
        code_challenge_methods_supported: { type: array, items: { type: string } }
        token_endpoint_auth_methods_supported: { type: array, items: { type: string } }
        service_documentation: { type: string, format: uri }
    Jwks:
      type: object
      required: [keys]
      properties:
        keys:
          type: array
          items:
            type: object
            properties:
              kty: { type: string, enum: [RSA] }
              use: { type: string, enum: [sig] }
              alg: { type: string, enum: [RS256] }
              kid: { type: string }
              n: { type: string, description: Modulus, base64url. }
              e: { type: string, description: Exponent, base64url. }
    ClientRegistrationRequest:
      type: object
      required: [redirect_uris]
      properties:
        redirect_uris:
          type: array
          minItems: 1
          maxItems: 5
          items: { type: string, format: uri }
          description: "`https`, or `http` on loopback only. No fragment, no wildcard."
        client_name:
          type: string
          maxLength: 120
          description: Shown on the consent screen. HTML is stripped.
        client_uri:
          type: string
          format: uri
          maxLength: 250
        grant_types:
          type: array
          items: { type: string, enum: [authorization_code, refresh_token] }
          default: [authorization_code]
        response_types:
          type: array
          items: { type: string, enum: [code] }
          default: [code]
        token_endpoint_auth_method:
          type: string
          enum: [none, client_secret_post, client_secret_basic]
          default: none
        scope:
          type: string
          description: Space-separated; filtered to the supported scopes.
          default: openid profile email
    ClientRegistrationResponse:
      type: object
      required: [client_id, client_id_issued_at, redirect_uris, grant_types, response_types, token_endpoint_auth_method, scope]
      properties:
        client_id: { type: string }
        client_id_issued_at: { type: integer, description: Unix timestamp. }
        client_secret: { type: string, description: Confidential clients only. Shown once. }
        client_secret_expires_at: { type: integer, enum: [0], description: "0 = never expires." }
        client_name: { type: string }
        redirect_uris: { type: array, items: { type: string, format: uri } }
        grant_types: { type: array, items: { type: string } }
        response_types: { type: array, items: { type: string } }
        token_endpoint_auth_method: { type: string }
        scope: { type: string }
    TokenRequest:
      type: object
      required: [grant_type, client_id]
      properties:
        grant_type: { type: string, enum: [authorization_code, refresh_token] }
        client_id: { type: string }
        client_secret: { type: string, description: Confidential clients using `client_secret_post`. }
        code: { type: string, description: authorization_code only. }
        redirect_uri: { type: string, format: uri, description: authorization_code only; must equal the one used at `/oauth/authorize`. }
        code_verifier: { type: string, minLength: 43, maxLength: 128, description: PKCE verifier; required when a `code_challenge` was sent. }
        refresh_token: { type: string, description: refresh_token only. }
        scope: { type: string, description: Optional; on refresh it may only narrow the original scope. }
    TokenResponse:
      type: object
      required: [token_type, expires_in, access_token]
      properties:
        token_type: { type: string, enum: [Bearer] }
        expires_in: { type: integer, example: 28800 }
        access_token: { type: string, description: RS256 JWT. }
        refresh_token: { type: string }
        id_token:
          type: string
          description: |
            RS256 JWT (OpenID Connect ID token). Only on the `authorization_code` exchange when the scope
            includes `openid`. Claims are described in `IdTokenClaims`.
    IdTokenClaims:
      type: object
      description: Payload of the ID token. Verify the signature with the JWKS key named by `kid`.
      required: [iss, sub, aud, azp, exp, iat, jti, auth_time]
      properties:
        iss: { type: string, format: uri, example: https://auth.nucleoplatform.com }
        sub: { type: string, description: Stable user id, same as `userinfo.sub`., example: "42" }
        aud: { type: string, description: Your `client_id`. }
        azp: { type: string, description: Your `client_id`. }
        exp: { type: integer, description: Unix time; 1 hour after `iat`. }
        iat: { type: integer, description: Unix time of issue. }
        jti: { type: string, description: Unique id of this ID token. }
        auth_time: { type: integer, description: Unix time when the person signed in to Nucleo. }
        nonce: { type: string, description: The `nonce` sent to `/oauth/authorize`; absent if none was sent. }
        email: { type: string, format: email, description: Scope `email`. }
        email_verified: { type: boolean, description: Scope `email`. }
        name: { type: string, description: Scope `profile`. }
        locale: { type: string, example: it, description: Scope `profile`. }
        picture: { type: string, format: uri, description: Scope `profile`, only when the person has an avatar. }
    UserInfo:
      type: object
      required: [sub, email]
      properties:
        sub: { type: string, description: Stable user id. }
        name: { type: string }
        email: { type: string, format: email }
        email_verified: { type: boolean }
        avatar_url: { type: [string, "null"], format: uri }
        language: { type: string, example: en }
        theme: { type: string, enum: [light, dark] }
        is_internal: { type: boolean, description: True only for Nucleo staff with access to every company. }
        is_owner: { type: boolean }
        org_ids: { type: array, items: { type: integer } }
        orgs:
          type: array
          items:
            type: object
            properties:
              id: { type: integer }
              slug: { type: string }
              name: { type: string }
              logo_url: { type: [string, "null"], format: uri }
              role: { type: string, example: admin }
        app_access: { type: array, items: { type: string }, description: Module slugs the user can open. }
        apps:
          type: array
          items:
            type: object
            properties:
              slug: { type: string, example: catalog }
              role: { type: string, example: editor }
              org_id: { type: [integer, "null"] }
        disabled_sections:
          type: object
          description: Company id → list of disabled sub-module slugs.
          additionalProperties: { type: array, items: { type: string } }
        client_id: { type: [string, "null"] }
        scope: { type: string, description: Effective scopes of the token, space-separated. }
        impersonator:
          type: [object, "null"]
          properties:
            id: { type: integer }
            name: { type: string }
            email: { type: string, format: email }
            expires_at: { type: string, format: date-time }
    OrganizationBranding:
      type: object
      properties:
        slug: { type: string }
        name: { type: string }
        logo_url: { type: [string, "null"], format: uri }
        logo_dark_url: { type: [string, "null"], format: uri }
        favicon_url: { type: [string, "null"], format: uri }
        favicon_light_url: { type: [string, "null"], format: uri }
