openapi: 3.1.0
info:
  title: Solid Server API
  description: |
    Multi-tenant Solid Protocol server with OAuth 2.0/OIDC authentication.
    
    This API implements the [Solid Protocol](https://solidproject.org/TR/protocol) 
    along with admin and management endpoints.
    
    ## Authentication
    
    Most endpoints require authentication via:
    - **Bearer tokens** - JWT from Clerk for admin/dashboard access
    - **DPoP tokens** - Solid-OIDC tokens for pod resource access
    
    ## Content Negotiation
    
    RDF resources support multiple formats via Accept header:
    - `text/turtle` (default)
    - `application/ld+json`
    - `application/n-triples`
    - `application/rdf+xml`
  version: 2.1.0
  contact:
    name: API Support
    url: https://github.com/your-org/solid-server
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT

servers:
  - url: http://localhost:3012
    description: Local development
  - url: https://api.example.com
    description: Production

tags:
  - name: Resources
    description: Solid Protocol resource operations (LDP)
  - name: Containers
    description: LDP container operations
  - name: Discovery
    description: Server discovery endpoints
  - name: OAuth
    description: OAuth 2.0 / OIDC endpoints
  - name: User
    description: User management
  - name: Admin
    description: Admin API endpoints
  - name: Health
    description: Health and monitoring
  - name: Notifications
    description: WebSocket notifications

paths:
  # ===========================================
  # Solid Protocol Resource Endpoints
  # ===========================================
  
  /{path}:
    get:
      tags: [Resources]
      summary: Get resource
      description: |
        Retrieve a resource or container. Supports content negotiation for RDF formats.
        Returns Link headers for ACL and type discovery.
      operationId: getResource
      parameters:
        - $ref: '#/components/parameters/ResourcePath'
        - $ref: '#/components/parameters/Accept'
        - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          description: Resource content
          headers:
            ETag:
              $ref: '#/components/headers/ETag'
            Link:
              $ref: '#/components/headers/Link'
            WAC-Allow:
              $ref: '#/components/headers/WACAllow'
          content:
            text/turtle:
              schema:
                type: string
              example: |
                @prefix foaf: <http://xmlns.com/foaf/0.1/> .
                <#me> a foaf:Person ;
                    foaf:name "Alice" .
            application/ld+json:
              schema:
                type: object
            application/n-triples:
              schema:
                type: string
            '*/*':
              schema:
                type: string
                format: binary
        '304':
          description: Not modified (ETag match)
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
        - bearerAuth: []
        - dpopAuth: []
        - {}
    
    head:
      tags: [Resources]
      summary: Get resource headers
      description: Same as GET but returns only headers, no body
      operationId: headResource
      parameters:
        - $ref: '#/components/parameters/ResourcePath'
      responses:
        '200':
          description: Resource exists
          headers:
            ETag:
              $ref: '#/components/headers/ETag'
            Link:
              $ref: '#/components/headers/Link'
            Content-Type:
              schema:
                type: string
            Content-Length:
              schema:
                type: integer
        '404':
          $ref: '#/components/responses/NotFound'
      security:
        - bearerAuth: []
        - dpopAuth: []
        - {}
    
    put:
      tags: [Resources]
      summary: Create or replace resource
      description: |
        Create a new resource or replace an existing one.
        Use `If-None-Match: *` to ensure creation only.
        Use `If-Match: "etag"` for conditional updates.
      operationId: putResource
      parameters:
        - $ref: '#/components/parameters/ResourcePath'
        - $ref: '#/components/parameters/IfMatch'
        - $ref: '#/components/parameters/IfNoneMatch'
        - $ref: '#/components/parameters/ContentType'
      requestBody:
        required: true
        content:
          text/turtle:
            schema:
              type: string
            example: |
              @prefix foaf: <http://xmlns.com/foaf/0.1/> .
              <#me> a foaf:Person ;
                  foaf:name "Alice" .
          application/ld+json:
            schema:
              type: object
          '*/*':
            schema:
              type: string
              format: binary
      responses:
        '201':
          description: Resource created
          headers:
            Location:
              schema:
                type: string
                format: uri
            ETag:
              $ref: '#/components/headers/ETag'
        '204':
          description: Resource replaced
          headers:
            ETag:
              $ref: '#/components/headers/ETag'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
      security:
        - bearerAuth: []
        - dpopAuth: []
    
    delete:
      tags: [Resources]
      summary: Delete resource
      description: |
        Delete a resource. Containers must be empty before deletion.
      operationId: deleteResource
      parameters:
        - $ref: '#/components/parameters/ResourcePath'
      responses:
        '204':
          description: Resource deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Conflict - container not empty
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - bearerAuth: []
        - dpopAuth: []
    
    patch:
      tags: [Resources]
      summary: Partial update (SPARQL Update)
      description: |
        Apply a SPARQL Update to modify an RDF resource.
        Supports INSERT, DELETE, DELETE/INSERT operations.
      operationId: patchResource
      parameters:
        - $ref: '#/components/parameters/ResourcePath'
        - $ref: '#/components/parameters/IfMatch'
      requestBody:
        required: true
        content:
          application/sparql-update:
            schema:
              type: string
            example: |
              PREFIX foaf: <http://xmlns.com/foaf/0.1/>
              DELETE { <#me> foaf:name ?old }
              INSERT { <#me> foaf:name "Alice Smith" }
              WHERE { <#me> foaf:name ?old }
      responses:
        '200':
          description: Resource updated
          headers:
            ETag:
              $ref: '#/components/headers/ETag'
        '400':
          description: Invalid SPARQL Update
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
      security:
        - bearerAuth: []
        - dpopAuth: []
    
    post:
      tags: [Containers]
      summary: Create resource in container
      description: |
        Create a new resource within a container.
        Use `Slug` header to suggest a name.
        Use `Link: <http://www.w3.org/ns/ldp#BasicContainer>; rel="type"` to create a sub-container.
      operationId: postToContainer
      parameters:
        - $ref: '#/components/parameters/ResourcePath'
        - name: Slug
          in: header
          description: Suggested resource name
          schema:
            type: string
          example: my-document.ttl
        - name: Link
          in: header
          description: Resource type (for creating containers)
          schema:
            type: string
          example: '<http://www.w3.org/ns/ldp#BasicContainer>; rel="type"'
      requestBody:
        required: true
        content:
          text/turtle:
            schema:
              type: string
          application/ld+json:
            schema:
              type: object
          text/plain:
            schema:
              type: string
          '*/*':
            schema:
              type: string
              format: binary
      responses:
        '201':
          description: Resource created
          headers:
            Location:
              description: URI of the created resource
              schema:
                type: string
                format: uri
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Container not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - bearerAuth: []
        - dpopAuth: []
    
    options:
      tags: [Resources]
      summary: CORS preflight / capability discovery
      description: Returns allowed methods and CORS headers
      operationId: optionsResource
      parameters:
        - $ref: '#/components/parameters/ResourcePath'
      responses:
        '204':
          description: Preflight response
          headers:
            Allow:
              schema:
                type: string
                example: GET, HEAD, PUT, POST, DELETE, PATCH, OPTIONS
            Accept-Patch:
              schema:
                type: string
                example: application/sparql-update
            Accept-Post:
              schema:
                type: string
                example: text/turtle, application/ld+json, */*

  # ===========================================
  # Discovery Endpoints
  # ===========================================
  
  /.well-known/openid-configuration:
    get:
      tags: [Discovery]
      summary: OpenID Connect discovery
      description: Returns OIDC provider configuration
      operationId: getOIDCConfig
      responses:
        '200':
          description: OIDC configuration
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OIDCConfiguration'

  /.well-known/jwks.json:
    get:
      tags: [Discovery]
      summary: JSON Web Key Set
      description: Returns public keys for token verification
      operationId: getJWKS
      responses:
        '200':
          description: JWKS
          content:
            application/json:
              schema:
                type: object
                properties:
                  keys:
                    type: array
                    items:
                      type: object

  /.well-known/solid:
    get:
      tags: [Discovery]
      summary: Solid server description
      description: Returns server capabilities and features
      operationId: getSolidDescription
      responses:
        '200':
          description: Server description
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ServerDescription'
            application/ld+json:
              schema:
                $ref: '#/components/schemas/ServerDescription'
            text/turtle:
              schema:
                type: string

  # ===========================================
  # OAuth / OIDC Endpoints
  # ===========================================
  
  /register:
    post:
      tags: [OAuth]
      summary: Register OAuth client
      description: Dynamic client registration (RFC 7591)
      operationId: registerClient
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClientRegistrationRequest'
      responses:
        '201':
          description: Client registered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientRegistrationResponse'
        '400':
          description: Invalid registration request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    
  /register/{client_id}:
    get:
      tags: [OAuth]
      summary: Get client information
      operationId: getClient
      parameters:
        - name: client_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Client information
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientRegistrationResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /authorize:
    get:
      tags: [OAuth]
      summary: Authorization endpoint
      description: |
        Initiates OAuth authorization flow.
        Redirects to login/consent UI.
      operationId: authorize
      parameters:
        - name: response_type
          in: query
          required: true
          schema:
            type: string
            enum: [code]
        - name: client_id
          in: query
          required: true
          schema:
            type: string
        - name: redirect_uri
          in: query
          required: true
          schema:
            type: string
            format: uri
        - name: scope
          in: query
          schema:
            type: string
            example: openid profile offline_access
        - name: state
          in: query
          schema:
            type: string
        - name: code_challenge
          in: query
          description: PKCE code challenge
          schema:
            type: string
        - name: code_challenge_method
          in: query
          schema:
            type: string
            enum: [S256, plain]
      responses:
        '302':
          description: Redirect to login/consent
        '400':
          description: Invalid request

  /token:
    post:
      tags: [OAuth]
      summary: Token endpoint
      description: Exchange authorization code for tokens or refresh tokens
      operationId: token
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                grant_type:
                  type: string
                  enum: [authorization_code, refresh_token]
                code:
                  type: string
                  description: Authorization code (for authorization_code grant)
                refresh_token:
                  type: string
                  description: Refresh token (for refresh_token grant)
                redirect_uri:
                  type: string
                  format: uri
                client_id:
                  type: string
                code_verifier:
                  type: string
                  description: PKCE code verifier
              required:
                - grant_type
                - client_id
      responses:
        '200':
          description: Token response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthError'

  /userinfo:
    get:
      tags: [OAuth]
      summary: UserInfo endpoint
      description: Returns claims about authenticated user
      operationId: userinfo
      responses:
        '200':
          description: User claims
          content:
            application/json:
              schema:
                type: object
                properties:
                  sub:
                    type: string
                  webid:
                    type: string
                    format: uri
                  name:
                    type: string
                  email:
                    type: string
      security:
        - bearerAuth: []
        - dpopAuth: []

  # ===========================================
  # User Management
  # ===========================================
  
  /api/me:
    get:
      tags: [User]
      summary: Get current user
      operationId: getCurrentUser
      responses:
        '200':
          description: Current user info
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '401':
          $ref: '#/components/responses/Unauthorized'
      security:
        - bearerAuth: []
        - dpopAuth: []

  /api/apps/authorized:
    get:
      tags: [User]
      summary: List authorized apps
      operationId: listAuthorizedApps
      responses:
        '200':
          description: List of authorized apps
          content:
            application/json:
              schema:
                type: object
                properties:
                  apps:
                    type: array
                    items:
                      $ref: '#/components/schemas/AuthorizedApp'
      security:
        - bearerAuth: []
        - dpopAuth: []

  /api/apps/authorized/{client_id}:
    delete:
      tags: [User]
      summary: Revoke app access
      operationId: revokeAppAccess
      parameters:
        - name: client_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Access revoked
        '404':
          $ref: '#/components/responses/NotFound'
      security:
        - bearerAuth: []
        - dpopAuth: []

  # ===========================================
  # Admin API
  # ===========================================
  
  /admin/tenants:
    get:
      tags: [Admin]
      summary: List tenants
      operationId: listTenants
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: List of tenants
          content:
            application/json:
              schema:
                type: object
                properties:
                  tenants:
                    type: array
                    items:
                      $ref: '#/components/schemas/Tenant'
                  total:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      security:
        - adminApiKey: []
        - bearerAuth: []
    
    post:
      tags: [Admin]
      summary: Create tenant
      operationId: createTenant
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTenantRequest'
      responses:
        '201':
          description: Tenant created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tenant'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - adminApiKey: []
        - bearerAuth: []

  /admin/tenants/{tenant_id}:
    get:
      tags: [Admin]
      summary: Get tenant
      operationId: getTenant
      parameters:
        - name: tenant_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Tenant details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tenant'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
        - adminApiKey: []
        - bearerAuth: []
    
    put:
      tags: [Admin]
      summary: Update tenant
      operationId: updateTenant
      parameters:
        - name: tenant_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTenantRequest'
      responses:
        '200':
          description: Tenant updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tenant'
      security:
        - adminApiKey: []
        - bearerAuth: []
    
    delete:
      tags: [Admin]
      summary: Delete tenant
      operationId: deleteTenant
      parameters:
        - name: tenant_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Tenant deleted
      security:
        - adminApiKey: []
        - bearerAuth: []

  /admin/users:
    get:
      tags: [Admin]
      summary: List users
      operationId: listUsers
      parameters:
        - name: tenant_id
          in: query
          schema:
            type: string
            format: uuid
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: List of users
          content:
            application/json:
              schema:
                type: object
                properties:
                  users:
                    type: array
                    items:
                      $ref: '#/components/schemas/AdminUser'
                  total:
                    type: integer
      security:
        - adminApiKey: []
        - bearerAuth: []

  /admin/stats:
    get:
      tags: [Admin]
      summary: Get system statistics
      operationId: getStats
      responses:
        '200':
          description: System statistics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SystemStats'
      security:
        - adminApiKey: []
        - bearerAuth: []

  # ===========================================
  # Health & Monitoring
  # ===========================================
  
  /health/live:
    get:
      tags: [Health]
      summary: Liveness probe
      description: Returns 200 if server is running
      operationId: liveness
      responses:
        '200':
          description: Server is alive
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok

  /health/ready:
    get:
      tags: [Health]
      summary: Readiness probe
      description: Returns 200 if server is ready to accept requests
      operationId: readiness
      responses:
        '200':
          description: Server is ready
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
                  checks:
                    type: object
                    properties:
                      database:
                        type: string
                      redis:
                        type: string
        '503':
          description: Server is not ready
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: degraded
                  checks:
                    type: object

  /.well-known/metrics:
    get:
      tags: [Health]
      summary: Prometheus metrics
      description: Returns metrics in Prometheus format
      operationId: metrics
      responses:
        '200':
          description: Metrics
          content:
            text/plain:
              schema:
                type: string

  # ===========================================
  # Webhooks
  # ===========================================
  
  /webhooks/clerk:
    post:
      tags: [Admin]
      summary: Clerk webhook
      description: Receives user lifecycle events from Clerk
      operationId: clerkWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                type:
                  type: string
                  enum: [user.created, user.updated, user.deleted]
                data:
                  type: object
      responses:
        '200':
          description: Webhook processed
        '401':
          description: Invalid signature

components:
  # ===========================================
  # Security Schemes
  # ===========================================
  
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Bearer token from Clerk
    
    dpopAuth:
      type: http
      scheme: dpop
      description: DPoP token from Solid-OIDC flow
    
    adminApiKey:
      type: apiKey
      in: header
      name: X-Admin-API-Key
      description: Admin API key for server-to-server calls

  # ===========================================
  # Parameters
  # ===========================================
  
  parameters:
    ResourcePath:
      name: path
      in: path
      required: true
      description: Resource path within the pod
      schema:
        type: string
      example: alice/profile/card
    
    Accept:
      name: Accept
      in: header
      description: Preferred content type
      schema:
        type: string
      example: text/turtle
    
    ContentType:
      name: Content-Type
      in: header
      description: Request body content type
      schema:
        type: string
      example: text/turtle
    
    IfMatch:
      name: If-Match
      in: header
      description: ETag for conditional update
      schema:
        type: string
      example: '"abc123"'
    
    IfNoneMatch:
      name: If-None-Match
      in: header
      description: ETag for conditional requests
      schema:
        type: string
      example: '"abc123"'
    
    Page:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1
    
    Limit:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20

  # ===========================================
  # Headers
  # ===========================================
  
  headers:
    ETag:
      description: Entity tag for caching
      schema:
        type: string
      example: '"abc123"'
    
    Link:
      description: Related resources (ACL, type)
      schema:
        type: string
      example: '<.acl>; rel="acl", <http://www.w3.org/ns/ldp#Resource>; rel="type"'
    
    WACAllow:
      description: Allowed access modes for current user
      schema:
        type: string
      example: 'user="read write", public="read"'

  # ===========================================
  # Response Templates
  # ===========================================
  
  responses:
    Unauthorized:
      description: Authentication required
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Unauthorized
            message: Authentication required
            statusCode: 401
    
    Forbidden:
      description: Access denied
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Forbidden
            message: You do not have permission to access this resource
            statusCode: 403
    
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Not Found
            message: Resource not found
            statusCode: 404
    
    PreconditionFailed:
      description: ETag mismatch
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Precondition Failed
            message: Resource has been modified
            statusCode: 412

  # ===========================================
  # Schemas
  # ===========================================
  
  schemas:
    Error:
      type: object
      required:
        - error
        - message
        - statusCode
      properties:
        error:
          type: string
        message:
          type: string
        statusCode:
          type: integer

    OAuthError:
      type: object
      properties:
        error:
          type: string
          enum:
            - invalid_request
            - invalid_client
            - invalid_grant
            - unauthorized_client
            - unsupported_grant_type
            - invalid_scope
        error_description:
          type: string

    OIDCConfiguration:
      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
        token_endpoint_auth_methods_supported:
          type: array
          items:
            type: string
        dpop_signing_alg_values_supported:
          type: array
          items:
            type: string

    ServerDescription:
      type: object
      properties:
        '@context':
          type: string
        name:
          type: string
        version:
          type: string
        features:
          type: array
          items:
            type: string
        api:
          type: object
          properties:
            registration:
              type: string
            authorization:
              type: string
            token:
              type: string
        authentication:
          type: object
          properties:
            oidc:
              type: object
              properties:
                issuer:
                  type: string

    ClientRegistrationRequest:
      type: object
      required:
        - redirect_uris
      properties:
        client_name:
          type: string
        redirect_uris:
          type: array
          items:
            type: string
            format: uri
        grant_types:
          type: array
          items:
            type: string
            enum: [authorization_code, refresh_token]
        response_types:
          type: array
          items:
            type: string
            enum: [code]
        scope:
          type: string
        token_endpoint_auth_method:
          type: string
          enum: [none, client_secret_basic, client_secret_post]
        logo_uri:
          type: string
          format: uri
        contacts:
          type: array
          items:
            type: string
            format: email

    ClientRegistrationResponse:
      type: object
      properties:
        client_id:
          type: string
        client_secret:
          type: string
        client_name:
          type: string
        redirect_uris:
          type: array
          items:
            type: string
        grant_types:
          type: array
          items:
            type: string
        response_types:
          type: array
          items:
            type: string
        token_endpoint_auth_method:
          type: string
        registration_client_uri:
          type: string
          format: uri
        client_id_issued_at:
          type: integer

    TokenResponse:
      type: object
      properties:
        access_token:
          type: string
        token_type:
          type: string
          enum: [Bearer, DPoP]
        expires_in:
          type: integer
        refresh_token:
          type: string
        id_token:
          type: string
        scope:
          type: string

    User:
      type: object
      properties:
        id:
          type: string
        webId:
          type: string
          format: uri
        email:
          type: string
          format: email
        name:
          type: string
        tenantId:
          type: string
          format: uuid

    AuthorizedApp:
      type: object
      properties:
        clientId:
          type: string
        clientName:
          type: string
        scopes:
          type: array
          items:
            type: string
        authorizedAt:
          type: string
          format: date-time

    Tenant:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        domain:
          type: string
        status:
          type: string
          enum: [active, suspended, deleted]
        storageQuota:
          type: integer
          description: Storage quota in bytes
        userLimit:
          type: integer
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    CreateTenantRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
        domain:
          type: string
        storageQuota:
          type: integer
        userLimit:
          type: integer

    UpdateTenantRequest:
      type: object
      properties:
        name:
          type: string
        status:
          type: string
          enum: [active, suspended]
        storageQuota:
          type: integer
        userLimit:
          type: integer

    AdminUser:
      type: object
      properties:
        id:
          type: string
          format: uuid
        externalId:
          type: string
        webId:
          type: string
          format: uri
        email:
          type: string
        name:
          type: string
        tenantId:
          type: string
          format: uuid
        storageUsed:
          type: integer
        resourceCount:
          type: integer
        createdAt:
          type: string
          format: date-time
        lastLoginAt:
          type: string
          format: date-time

    SystemStats:
      type: object
      properties:
        tenants:
          type: object
          properties:
            total:
              type: integer
            active:
              type: integer
        users:
          type: object
          properties:
            total:
              type: integer
            activeToday:
              type: integer
        resources:
          type: object
          properties:
            total:
              type: integer
            totalSize:
              type: integer
        requests:
          type: object
          properties:
            last24h:
              type: integer
            errorsLast24h:
              type: integer
