openapi: 3.1.0
info:
  title: 1ll.me API
  version: 1.0.0-alpha.2
  summary: Product-owned API for the independent 1ll.me Go control plane
  description: |
    This document is the source of truth for the clean-room Go implementation.
    Browser clients use an opaque HttpOnly session and a session-bound CSRF
    token. Programmatic clients use scoped API keys. Kutt field names, tokens,
    and native endpoints are not part of this contract.
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
servers:
  - url: https://1ll.me/api/v1
    description: Production after an explicitly approved cutover
  - url: http://localhost:8080/api/v1
    description: Local development
tags:
  - name: Status
    description: Sanitized public availability information.
  - name: Session
    description: Account registration, authentication, recovery, and browser sessions.
  - name: Public links
    description: Anonymous creation and advisory code checks protected by abuse policy.
  - name: Links
    description: Workspace-owned link lifecycle and export resources.
  - name: API keys
    description: Scoped, revocable credentials for programmatic clients.
  - name: Analytics
    description: Privacy-preserving reports for authorized workspace resources.
  - name: Admin
    description: MFA-gated platform administration and risk response.
security:
  - cookieAuth: []
  - bearerAuth: []
paths:
  /status:
    get:
      operationId: getPublicStatus
      tags: [Status]
      summary: Read a sanitized service status
      security: []
      responses:
        '200':
          description: Sanitized control-plane status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusResponse'

  /session/registration:
    get:
      operationId: getRegistrationPolicy
      tags: [Session]
      summary: Read sanitized public registration availability
      security: []
      responses:
        '200':
          description: Public registration policy
          content:
            application/json:
              schema: {$ref: '#/components/schemas/RegistrationPolicyResponse'}
        '503': {$ref: '#/components/responses/Unavailable'}

  /session/register:
    post:
      operationId: register
      tags: [Session]
      summary: Register an account and personal workspace
      security: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
      responses:
        '201':
          description: Account created
          headers:
            X-Request-ID: {$ref: '#/components/headers/RequestID'}
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationResponse'
        '400': {$ref: '#/components/responses/ValidationFailed'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '409': {$ref: '#/components/responses/Conflict'}
        '429': {$ref: '#/components/responses/RateLimited'}
        '503': {$ref: '#/components/responses/Unavailable'}

  /session/login:
    post:
      operationId: login
      tags: [Session]
      summary: Create an opaque browser session
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '200':
          description: Session created
          headers:
            Set-Cookie:
              description: Opaque HttpOnly session cookie and non-secret CSRF bootstrap cookie.
              schema: {type: string}
            X-Request-ID: {$ref: '#/components/headers/RequestID'}
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionResponse'
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/AccountUnavailable'}
        '429': {$ref: '#/components/responses/RateLimited'}

  /session/logout:
    post:
      operationId: logout
      tags: [Session]
      summary: Revoke the current browser session
      security:
        - cookieAuth: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '204': {description: Session revoked}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/CSRFInvalid'}

  /session:
    get:
      operationId: getCurrentSession
      tags: [Session]
      summary: Read the current actor and CSRF bootstrap state
      security:
        - cookieAuth: []
      responses:
        '200':
          description: Current browser session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionResponse'
        '401': {$ref: '#/components/responses/Unauthorized'}

  /sessions:
    get:
      operationId: listSessions
      tags: [Session]
      summary: List active browser sessions
      security:
        - cookieAuth: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Active sessions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionListResponse'
        '401': {$ref: '#/components/responses/Unauthorized'}

  /sessions/{sessionId}:
    delete:
      operationId: revokeSession
      tags: [Session]
      summary: Revoke one owned browser session
      security:
        - cookieAuth: []
      parameters:
        - $ref: '#/components/parameters/SessionID'
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '204': {description: Session revoked}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}

  /session/verify-email:
    post:
      operationId: verifyEmail
      tags: [Session]
      summary: Consume a single-use email verification link token or code
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/VerifyEmailRequest'}
      responses:
        '200':
          description: Email verified
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AcceptedResponse'}
        '400': {$ref: '#/components/responses/ValidationFailed'}
        '429': {$ref: '#/components/responses/RateLimited'}

  /session/resend-verification:
    post:
      operationId: resendVerification
      tags: [Session]
      summary: Queue an enumeration-safe verification message
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/EmailVerificationRequest'}
      responses:
        '202':
          description: Request accepted
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AcceptedResponse'}
        '429': {$ref: '#/components/responses/RateLimited'}
        '503': {$ref: '#/components/responses/Unavailable'}

  /session/password-reset-requests:
    post:
      operationId: requestPasswordReset
      tags: [Session]
      summary: Queue an enumeration-safe password reset message
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/EmailRequest'}
      responses:
        '202':
          description: Request accepted
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AcceptedResponse'}
        '429': {$ref: '#/components/responses/RateLimited'}
        '503': {$ref: '#/components/responses/Unavailable'}

  /session/password-resets:
    post:
      operationId: resetPassword
      tags: [Session]
      summary: Consume a reset token and replace the account password
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasswordResetRequest'
      responses:
        '200':
          description: Password replaced and other sessions revoked
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AcceptedResponse'}
        '400': {$ref: '#/components/responses/ValidationFailed'}
        '429': {$ref: '#/components/responses/RateLimited'}

  /session/mfa:
    get:
      operationId: getMFAStatus
      tags: [Session]
      summary: Read the current browser session MFA state
      security:
        - cookieAuth: []
      responses:
        '200':
          description: MFA state
          content:
            application/json:
              schema: {$ref: '#/components/schemas/MFAStatusResponse'}
    delete:
      operationId: disableMFA
      tags: [Session]
      summary: Disable MFA after password re-authentication and revoke other sessions
      security:
        - cookieAuth: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [currentPassword]
              properties:
                currentPassword: {type: string, minLength: 1, maxLength: 128, format: password}
      responses:
        '200':
          description: MFA disabled and other sessions revoked
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AcceptedResponse'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/RateLimited'}

  /session/mfa/enrollments:
    post:
      operationId: beginMFAEnrollment
      tags: [Session]
      summary: Begin TOTP enrollment and return the secret once
      security:
        - cookieAuth: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '201':
          description: Pending TOTP enrollment
          content:
            application/json:
              schema: {$ref: '#/components/schemas/MFAEnrollmentResponse'}

  /session/mfa/enrollments/confirm:
    post:
      operationId: confirmMFAEnrollment
      tags: [Session]
      summary: Confirm TOTP enrollment and return recovery codes once
      security:
        - cookieAuth: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [factorId, code]
              properties:
                factorId: {type: string, format: uuid}
                code: {type: string, minLength: 6, maxLength: 64}
      responses:
        '200':
          description: MFA enabled
          content:
            application/json:
              schema: {$ref: '#/components/schemas/MFAConfirmationResponse'}

  /session/mfa/verify:
    post:
      operationId: verifyMFA
      tags: [Session]
      summary: Complete an MFA challenge with TOTP or a recovery code
      security:
        - cookieAuth: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [code]
              properties:
                code: {type: string, minLength: 6, maxLength: 64}
      responses:
        '200':
          description: MFA challenge completed
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AcceptedResponse'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '429': {$ref: '#/components/responses/RateLimited'}

  /account/password:
    patch:
      operationId: changePassword
      tags: [Session]
      summary: Change the current account password and revoke other sessions
      security:
        - cookieAuth: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [currentPassword, newPassword]
              properties:
                currentPassword: {type: string, format: password, minLength: 1, maxLength: 128}
                newPassword: {type: string, format: password, minLength: 12, maxLength: 128}
      responses:
        '200':
          description: Password changed
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AcceptedResponse'}

  /public/links:
    post:
      operationId: createAnonymousLink
      tags: [Public links]
      summary: Create an expiring anonymous link
      description: Permanent anonymous links are rejected. The management secret is returned once and only its digest is stored.
      security: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/PublicLinkCreateRequest'}
      responses:
        '201':
          description: Anonymous link created
          headers:
            ETag: {$ref: '#/components/headers/ETag'}
          content:
            application/json:
              schema: {$ref: '#/components/schemas/PublicLinkCreateResponse'}
        '400': {$ref: '#/components/responses/ValidationFailed'}
        '409': {$ref: '#/components/responses/Conflict'}
        '429': {$ref: '#/components/responses/RateLimited'}

  /public/links/{code}:
    get:
      operationId: getAnonymousLink
      tags: [Public links]
      summary: Read an anonymous link with its one-time management secret
      security:
        - anonymousManagement: []
      parameters:
        - $ref: '#/components/parameters/ShortCodePath'
      responses:
        '200':
          description: Anonymous link metadata
          headers:
            ETag: {$ref: '#/components/headers/ETag'}
          content:
            application/json:
              schema: {$ref: '#/components/schemas/LinkResponse'}
        '404': {$ref: '#/components/responses/NotFound'}
        '429': {$ref: '#/components/responses/RateLimited'}
    patch:
      operationId: updateAnonymousLink
      tags: [Public links]
      summary: Update an anonymous link with its one-time management secret
      description: Anonymous links cannot be made permanent or extended beyond one year.
      security:
        - anonymousManagement: []
      parameters:
        - $ref: '#/components/parameters/ShortCodePath'
        - $ref: '#/components/parameters/IfMatch'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/AnonymousLinkUpdateRequest'}
      responses:
        '200':
          description: Anonymous link updated
          headers:
            ETag: {$ref: '#/components/headers/ETag'}
          content:
            application/json:
              schema: {$ref: '#/components/schemas/LinkResponse'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/RevisionConflict'}
        '429': {$ref: '#/components/responses/RateLimited'}
    delete:
      operationId: deleteAnonymousLink
      tags: [Public links]
      summary: Soft-delete an anonymous link with its one-time management secret
      security:
        - anonymousManagement: []
      parameters:
        - $ref: '#/components/parameters/ShortCodePath'
        - $ref: '#/components/parameters/IfMatch'
      responses:
        '204': {description: Anonymous link soft-deleted and its code tombstoned}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/RevisionConflict'}
        '429': {$ref: '#/components/responses/RateLimited'}

  /public/codes/check:
    post:
      operationId: checkPublicCode
      tags: [Public links]
      summary: Check whether a custom code appears available
      description: This check never reserves the code. Creation is decided by a database unique constraint.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CodeCheckRequest'}
      responses:
        '200':
          description: Advisory availability result
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CodeCheckResponse'}
        '429': {$ref: '#/components/responses/RateLimited'}

  /public/links/{code}/unlock:
    post:
      operationId: unlockPublicLink
      tags: [Public links]
      summary: Verify a link password and issue a short-lived host-bound grant
      security: []
      parameters:
        - name: code
          in: path
          required: true
          schema: {$ref: '#/components/schemas/ShortCode'}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [password]
              properties:
                password: {type: string, minLength: 1, maxLength: 128}
      responses:
        '200':
          description: Password grant issued
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [redirect]
                    properties:
                      redirect: {type: string, format: uri}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/RateLimited'}

  /links:
    get:
      operationId: listLinks
      tags: [Links]
      summary: List links in the active workspace
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Search'
        - $ref: '#/components/parameters/StateFilter'
        - $ref: '#/components/parameters/TagFilter'
        - $ref: '#/components/parameters/LinkSort'
        - $ref: '#/components/parameters/SortOrder'
        - $ref: '#/components/parameters/CreatedFrom'
        - $ref: '#/components/parameters/CreatedTo'
      responses:
        '200':
          description: Cursor-paginated links
          content:
            application/json:
              schema: {$ref: '#/components/schemas/LinkListResponse'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
    post:
      operationId: createLink
      tags: [Links]
      summary: Create a workspace-owned link
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/LinkCreateRequest'}
      responses:
        '201':
          description: Link created
          headers:
            ETag: {$ref: '#/components/headers/ETag'}
          content:
            application/json:
              schema: {$ref: '#/components/schemas/LinkResponse'}
        '400': {$ref: '#/components/responses/ValidationFailed'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '409': {$ref: '#/components/responses/Conflict'}
        '413': {$ref: '#/components/responses/BodyTooLarge'}
        '429': {$ref: '#/components/responses/RateLimited'}

  /links/{linkId}:
    parameters:
      - $ref: '#/components/parameters/LinkID'
    get:
      operationId: getLink
      tags: [Links]
      summary: Read one workspace-owned link
      responses:
        '200':
          description: Link
          headers:
            ETag: {$ref: '#/components/headers/ETag'}
          content:
            application/json:
              schema: {$ref: '#/components/schemas/LinkResponse'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
    patch:
      operationId: updateLink
      tags: [Links]
      summary: Update one workspace-owned link using optimistic concurrency
      parameters:
        - $ref: '#/components/parameters/IfMatch'
        - $ref: '#/components/parameters/CSRFToken'
        - name: revision
          in: query
          deprecated: true
          schema: {type: integer, minimum: 1}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/LinkUpdateRequest'}
      responses:
        '200':
          description: Link updated
          headers:
            ETag: {$ref: '#/components/headers/ETag'}
          content:
            application/json:
              schema: {$ref: '#/components/schemas/LinkResponse'}
        '400': {$ref: '#/components/responses/ValidationFailed'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/RevisionConflict'}
    delete:
      operationId: deleteLink
      tags: [Links]
      summary: Soft-delete a link and permanently tombstone its code
      parameters:
        - $ref: '#/components/parameters/IfMatch'
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '204': {description: Link soft-deleted}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/RevisionConflict'}

  /links/{linkId}/restore:
    post:
      operationId: restoreLink
      tags: [Links]
      summary: Restore an owned soft-deleted link during retention
      parameters:
        - $ref: '#/components/parameters/LinkID'
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '200':
          description: Link restored
          content:
            application/json:
              schema: {$ref: '#/components/schemas/LinkResponse'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}

  /links/{linkId}/qr:
    get:
      operationId: getLinkQRCode
      tags: [Links]
      summary: Generate a QR code for an owned link
      parameters:
        - $ref: '#/components/parameters/LinkID'
      responses:
        '200':
          description: QR image generated on demand
          content:
            image/png:
              schema: {type: string, contentEncoding: binary}
        '404': {$ref: '#/components/responses/NotFound'}

  /codes/check:
    get:
      operationId: checkCode
      tags: [Links]
      summary: Check code availability for the active workspace and domain
      description: This check is advisory and does not reserve a code.
      parameters:
        - name: code
          in: query
          required: true
          schema: {$ref: '#/components/schemas/ShortCode'}
      responses:
        '200':
          description: Advisory availability result
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CodeCheckResponse'}

  /tags:
    get:
      operationId: listTags
      tags: [Links]
      summary: List tags in the active workspace
      responses:
        '200':
          description: Tags and link counts
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/Tag'}

  /links/imports:
    post:
      operationId: createLinkImport
      tags: [Links]
      summary: Import up to 500 links from a bounded CSV document
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          text/csv:
            schema: {type: string, maxLength: 10485760}
      responses:
        '200':
          description: Per-row import result
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ImportResponse'}
        '413': {$ref: '#/components/responses/BodyTooLarge'}

  /links/exports:
    post:
      operationId: createLinkExport
      tags: [Links]
      summary: Export up to 10,000 active links as CSV
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '200':
          description: CSV export
          content:
            text/csv:
              schema: {type: string}

  /api-keys:
    get:
      operationId: listAPIKeys
      tags: [API keys]
      summary: List API key metadata without secrets
      security:
        - cookieAuth: []
      responses:
        '200':
          description: API key metadata
          content:
            application/json:
              schema: {$ref: '#/components/schemas/APIKeyListResponse'}
    post:
      operationId: createAPIKey
      tags: [API keys]
      summary: Create a scoped API key and return its secret once
      security:
        - cookieAuth: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/APIKeyCreateRequest'}
      responses:
        '201':
          description: Key created; the secret is shown only in this response
          content:
            application/json:
              schema: {$ref: '#/components/schemas/APIKeySecretResponse'}
        '400': {$ref: '#/components/responses/ValidationFailed'}

  /api-keys/{keyId}:
    delete:
      operationId: revokeAPIKey
      tags: [API keys]
      summary: Revoke an owned API key immediately
      security:
        - cookieAuth: []
      parameters:
        - $ref: '#/components/parameters/APIKeyID'
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '204': {description: Key revoked}
        '404': {$ref: '#/components/responses/NotFound'}

  /api-keys/{keyId}/rotate:
    post:
      operationId: rotateAPIKey
      tags: [API keys]
      summary: Replace an owned key secret and return the new secret once
      security:
        - cookieAuth: []
      parameters:
        - $ref: '#/components/parameters/APIKeyID'
        - $ref: '#/components/parameters/CSRFToken'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201':
          description: Key rotated
          content:
            application/json:
              schema: {$ref: '#/components/schemas/APIKeySecretResponse'}
        '404': {$ref: '#/components/responses/NotFound'}

  /links/{linkId}/analytics/summary:
    get:
      operationId: getLinkAnalyticsSummary
      tags: [Analytics]
      summary: Read a bounded analytics summary for an owned link
      parameters:
        - $ref: '#/components/parameters/LinkID'
        - $ref: '#/components/parameters/RangeStart'
        - $ref: '#/components/parameters/RangeEnd'
      responses:
        '200':
          description: Analytics summary with bounded timeline, geographic, environment, and attribution dimensions
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AnalyticsSummaryResponse'}
        '404': {$ref: '#/components/responses/NotFound'}

  /admin/overview:
    get:
      operationId: getAdminOverview
      tags: [Admin]
      summary: Read platform user and link totals
      security:
        - cookieAuth: []
      responses:
        '200':
          description: Platform totals
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AdminOverviewResponse'}
        '403': {$ref: '#/components/responses/Forbidden'}

  /admin/users:
    get:
      operationId: listAdminUsers
      tags: [Admin]
      summary: List platform users
      security:
        - cookieAuth: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Search'
        - $ref: '#/components/parameters/UserStateFilter'
        - $ref: '#/components/parameters/UserRoleFilter'
        - $ref: '#/components/parameters/UserSort'
        - $ref: '#/components/parameters/SortOrder'
        - $ref: '#/components/parameters/CreatedFrom'
        - $ref: '#/components/parameters/CreatedTo'
      responses:
        '200':
          description: Platform users
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AdminUserListResponse'}

  /admin/users/{userId}/suspend:
    post:
      operationId: suspendAdminUser
      tags: [Admin]
      summary: Suspend a user and revoke active browser sessions
      security:
        - cookieAuth: []
      parameters:
        - {name: userId, in: path, required: true, schema: {type: string, format: uuid}}
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '204': {description: User suspended}

  /admin/users/{userId}/restore:
    post:
      operationId: restoreAdminUser
      tags: [Admin]
      summary: Restore a suspended user
      security:
        - cookieAuth: []
      parameters:
        - {name: userId, in: path, required: true, schema: {type: string, format: uuid}}
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '204': {description: User restored}

  /admin/users/{userId}/role:
    patch:
      operationId: updateAdminUserRole
      tags: [Admin]
      summary: Change a platform role and revoke the target user's active sessions
      security:
        - cookieAuth: []
      parameters:
        - {name: userId, in: path, required: true, schema: {type: string, format: uuid}}
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/AdminUserRoleRequest'}
      responses:
        '204': {description: Platform role updated}
        '409': {$ref: '#/components/responses/Conflict'}

  /admin/users/{userId}/sessions/revoke:
    post:
      operationId: revokeAdminUserSessions
      tags: [Admin]
      summary: Revoke every active browser session for a user
      security:
        - cookieAuth: []
      parameters:
        - {name: userId, in: path, required: true, schema: {type: string, format: uuid}}
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '204': {description: Active sessions revoked}

  /admin/links:
    get:
      operationId: listAdminLinks
      tags: [Admin]
      summary: List platform links across workspaces
      security:
        - cookieAuth: []
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Search'
        - $ref: '#/components/parameters/StateFilter'
        - $ref: '#/components/parameters/LinkSort'
        - $ref: '#/components/parameters/SortOrder'
        - $ref: '#/components/parameters/CreatedFrom'
        - $ref: '#/components/parameters/CreatedTo'
      responses:
        '200':
          description: Platform links
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AdminLinkListResponse'}

  /admin/links/{linkId}:
    patch:
      operationId: updateAdminLink
      tags: [Admin]
      summary: Update a link across workspace boundaries
      security:
        - cookieAuth: []
      parameters:
        - $ref: '#/components/parameters/LinkID'
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/LinkUpdateRequest'}
      responses:
        '200':
          description: Link updated
          content:
            application/json:
              schema: {$ref: '#/components/schemas/LinkResponse'}
        '409': {$ref: '#/components/responses/Conflict'}
        '428': {$ref: '#/components/responses/PreconditionRequired'}
    delete:
      operationId: deleteAdminLink
      tags: [Admin]
      summary: Soft-delete a link across workspace boundaries
      security:
        - cookieAuth: []
      parameters:
        - $ref: '#/components/parameters/LinkID'
        - $ref: '#/components/parameters/CSRFToken'
        - name: revision
          in: query
          required: true
          schema: {type: integer, minimum: 1}
      responses:
        '204': {description: Link deleted and tombstoned}
        '409': {$ref: '#/components/responses/Conflict'}
        '428': {$ref: '#/components/responses/PreconditionRequired'}

  /admin/links/{linkId}/quarantine:
    post:
      operationId: quarantineAdminLink
      tags: [Admin]
      summary: Quarantine a risk link and invalidate redirect caches
      security:
        - cookieAuth: []
      parameters:
        - $ref: '#/components/parameters/LinkID'
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '204': {description: Link quarantined}

  /admin/links/{linkId}/restore:
    post:
      operationId: restoreAdminLink
      tags: [Admin]
      summary: Clear quarantine and restore a risk link
      security:
        - cookieAuth: []
      parameters:
        - $ref: '#/components/parameters/LinkID'
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '204': {description: Link restored}

  /admin/settings/registration:
    get:
      operationId: getAdminRegistrationSettings
      tags: [Admin]
      summary: Read runtime account registration settings
      security:
        - cookieAuth: []
      responses:
        '200':
          description: Registration settings
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AdminRegistrationSettingsResponse'}
    patch:
      operationId: updateAdminRegistrationSettings
      tags: [Admin]
      summary: Enable or disable public account registration
      security:
        - cookieAuth: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/AdminRegistrationSettingsRequest'}
      responses:
        '200':
          description: Updated registration settings
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AdminRegistrationSettingsResponse'}
        '409': {$ref: '#/components/responses/Conflict'}

  /admin/settings/smtp:
    get:
      operationId: getAdminSMTPSettings
      tags: [Admin]
      summary: Read redacted runtime SMTP settings
      security:
        - cookieAuth: []
      responses:
        '200':
          description: Redacted SMTP settings
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AdminSMTPSettingsResponse'}
    patch:
      operationId: updateAdminSMTPSettings
      tags: [Admin]
      summary: Update runtime SMTP settings and a write-only encrypted password
      security:
        - cookieAuth: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/AdminSMTPSettingsRequest'}
      responses:
        '200':
          description: Updated redacted SMTP settings
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AdminSMTPSettingsResponse'}
        '400': {$ref: '#/components/responses/ValidationFailed'}

  /admin/settings/smtp/test:
    post:
      operationId: testAdminSMTPSettings
      tags: [Admin]
      summary: Send one audited SMTP delivery test
      security:
        - cookieAuth: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [recipient]
              properties:
                recipient: {type: string, format: email, maxLength: 320}
      responses:
        '204': {description: Test message delivered}
        '502':
          description: SMTP delivery failed without exposing transport details
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ErrorResponse'}

components:
  securitySchemes:
    cookieAuth:
      type: apiKey
      in: cookie
      name: __Host-1ll_session
      description: Opaque production browser session. The development cookie uses a non-Host prefix over HTTP.
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: 1ll_live_<identifier>_<secret>
      description: Scoped, revocable programmatic API key.
    anonymousManagement:
      type: http
      scheme: bearer
      bearerFormat: 1ll_manage_<secret>
      description: One-time anonymous-link management secret. Only its digest is stored.

  headers:
    RequestID:
      description: Correlation identifier generated or accepted by the API.
      schema: {type: string, maxLength: 128}
    ETag:
      description: Quoted resource revision used with If-Match.
      schema: {type: string, examples: ['"revision-7"']}

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: {type: string, minLength: 8, maxLength: 128}
      description: Required for retry-safe client mutations. Completed results are retained for 24 hours by default; unfinished requests use a short fenced lease.
    CSRFToken:
      name: X-CSRF-Token
      in: header
      required: false
      schema: {type: string, minLength: 32, maxLength: 512}
      description: Required when a request authenticates with the browser session cookie; ignored for unambiguous API-key authentication.
    IfMatch:
      name: If-Match
      in: header
      required: false
      description: Current quoted revision. Required unless the temporary request-body revision compatibility field is supplied.
      schema: {type: string, examples: ['"revision-7"']}
    Limit:
      name: limit
      in: query
      schema: {type: integer, minimum: 1, maximum: 100, default: 20}
    Cursor:
      name: cursor
      in: query
      schema: {type: string, maxLength: 512}
    Search:
      name: search
      in: query
      schema: {type: string, maxLength: 160}
    StateFilter:
      name: state
      in: query
      schema: {$ref: '#/components/schemas/LinkState'}
    TagFilter:
      name: tag
      in: query
      schema: {type: array, maxItems: 10, items: {type: string, maxLength: 32}}
    LinkSort:
      name: sort
      in: query
      schema: {type: string, enum: [createdAt, updatedAt, visits, code], default: createdAt}
    UserSort:
      name: sort
      in: query
      schema: {type: string, enum: [createdAt, email, links], default: createdAt}
    SortOrder:
      name: order
      in: query
      schema: {type: string, enum: [asc, desc], default: desc}
    CreatedFrom:
      name: createdFrom
      in: query
      schema: {type: string, format: date-time}
    CreatedTo:
      name: createdTo
      in: query
      schema: {type: string, format: date-time}
    UserStateFilter:
      name: state
      in: query
      schema: {type: string, enum: [pending, active, suspended, deletion_pending]}
    UserRoleFilter:
      name: role
      in: query
      schema: {type: string, enum: [member, admin]}
    LinkID:
      name: linkId
      in: path
      required: true
      schema: {type: string, format: uuid}
    ShortCodePath:
      name: code
      in: path
      required: true
      schema: {$ref: '#/components/schemas/ShortCode'}
    SessionID:
      name: sessionId
      in: path
      required: true
      schema: {type: string, format: uuid}
    APIKeyID:
      name: keyId
      in: path
      required: true
      schema: {type: string, format: uuid}
    RangeStart:
      name: from
      in: query
      schema: {type: string, format: date-time}
    RangeEnd:
      name: to
      in: query
      schema: {type: string, format: date-time}

  schemas:
    RegistrationPolicy:
      type: object
      additionalProperties: false
      required: [enabled, verificationMethod, unavailableReason]
      properties:
        enabled: {type: boolean}
        verificationMethod: {type: string, enum: [email_code, none]}
        unavailableReason:
          type: [string, 'null']
          enum: [registration_disabled, email_delivery_unavailable, null]
    RegistrationPolicyResponse:
      type: object
      required: [data]
      properties:
        data: {$ref: '#/components/schemas/RegistrationPolicy'}
    RegisterRequest:
      type: object
      additionalProperties: false
      required: [email, password, turnstileToken]
      properties:
        email: {type: string, format: email, maxLength: 254}
        password: {type: string, format: password, minLength: 12, maxLength: 128}
        locale: {type: string, enum: [zh-CN, en-US], default: zh-CN}
        turnstileToken: {type: string, minLength: 1, maxLength: 4096}
    LoginRequest:
      type: object
      additionalProperties: false
      required: [email, password]
      properties:
        email: {type: string, format: email, maxLength: 254}
        password: {type: string, format: password, minLength: 1, maxLength: 128}
        turnstileToken: {type: string, maxLength: 4096}
    EmailRequest:
      type: object
      additionalProperties: false
      required: [email]
      properties:
        email: {type: string, format: email, maxLength: 254}
    EmailVerificationRequest:
      type: object
      additionalProperties: false
      required: [email, turnstileToken]
      properties:
        email: {type: string, format: email, maxLength: 254}
        turnstileToken: {type: string, minLength: 1, maxLength: 4096}
    VerifyEmailRequest:
      oneOf:
        - type: object
          additionalProperties: false
          required: [token]
          properties:
            token: {type: string, minLength: 32, maxLength: 512}
        - type: object
          additionalProperties: false
          required: [email, code]
          properties:
            email: {type: string, format: email, maxLength: 254}
            code: {type: string, pattern: '^[0-9]{6}$'}
    PasswordResetRequest:
      type: object
      additionalProperties: false
      required: [token, password]
      properties:
        token: {type: string, minLength: 32, maxLength: 512}
        password: {type: string, format: password, minLength: 12, maxLength: 128}
    RegistrationResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [authenticated, email, verificationRequired]
          properties:
            authenticated: {type: boolean}
            email: {type: string, format: email}
            verificationRequired: {type: boolean}
    SessionResponse:
      type: object
      required: [data]
      properties:
        data: {$ref: '#/components/schemas/Session'}
    Session:
      type: object
      required: [authenticated, email, isAdmin, mfaRequired]
      properties:
        authenticated: {type: boolean, const: true}
        email: {type: string, format: email}
        isAdmin: {type: boolean}
        mfaRequired: {type: boolean, description: True while the current session must complete MFA before business API access.}
    BrowserSession:
      type: object
      required: [id, current, userAgent, lastSeenAt, expiresAt, createdAt]
      properties:
        id: {type: string, format: uuid}
        current: {type: boolean}
        userAgent: {type: string, maxLength: 200}
        lastSeenAt: {type: string, format: date-time}
        expiresAt: {type: string, format: date-time}
        createdAt: {type: string, format: date-time}
    SessionListResponse:
      type: object
      required: [data, meta]
      properties:
        data: {type: array, items: {$ref: '#/components/schemas/BrowserSession'}}
        meta: {$ref: '#/components/schemas/CursorMeta'}
    ShortCode:
      type: string
      minLength: 6
      maxLength: 64
      pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
    LinkState:
      type: string
      enum: [active, disabled, expired, quarantined, deleted]
    CapMode:
      type: string
      enum: [standard, strict]
      description: Strict caps disable edge failover for exact origin enforcement.
    LinkCreateRequest:
      type: object
      additionalProperties: false
      required: [target]
      properties:
        target: {type: string, format: uri, maxLength: 2048}
        customCode: {oneOf: [{$ref: '#/components/schemas/ShortCode'}, {type: 'null'}]}
        title: {type: [string, 'null'], maxLength: 160}
        description: {type: [string, 'null'], maxLength: 2000, deprecated: true, description: Temporary client alias for privateNote.}
        privateNote: {type: [string, 'null'], maxLength: 2000}
        expiresIn: {type: [string, 'null'], pattern: '^[1-9][0-9]*(?:s|h|d|w)$', examples: [1h, 7d, 30d]}
        expiresAt: {type: string, format: date-time, description: Use expiresIn with null for a permanent authenticated link.}
        password: {type: [string, 'null'], minLength: 3, maxLength: 128}
        maxRedirects: {type: [integer, 'null'], minimum: 1}
        capMode: {$ref: '#/components/schemas/CapMode'}
        forwardQuery: {type: boolean, default: false}
        tags: {type: array, maxItems: 10, items: {type: string, minLength: 1, maxLength: 32}}
        utm: {$ref: '#/components/schemas/UTMFields'}
    PublicLinkCreateRequest:
      type: object
      additionalProperties: false
      required: [target, turnstileToken]
      properties:
        target: {type: string, format: uri, maxLength: 2048}
        customCode: {oneOf: [{$ref: '#/components/schemas/ShortCode'}, {type: 'null'}]}
        title: {type: [string, 'null'], maxLength: 160}
        description: {type: [string, 'null'], maxLength: 2000, deprecated: true, description: Temporary client alias for privateNote.}
        privateNote: {type: [string, 'null'], maxLength: 2000}
        expiresIn: {type: string, pattern: '^[1-9][0-9]*(?:s|h|d|w)$', examples: [1h, 7d, 30d]}
        expiresAt: {type: string, format: date-time}
        password: {type: [string, 'null'], minLength: 3, maxLength: 128}
        maxRedirects: {type: [integer, 'null'], minimum: 1}
        capMode: {$ref: '#/components/schemas/CapMode'}
        forwardQuery: {type: boolean, default: false}
        utm: {$ref: '#/components/schemas/UTMFields'}
        turnstileToken: {type: string, minLength: 1, maxLength: 4096}
        website: {type: string, maxLength: 0, description: Honeypot field; non-empty requests are rejected.}
    LinkUpdateRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        target: {type: string, format: uri, maxLength: 2048}
        customCode: {$ref: '#/components/schemas/ShortCode'}
        title: {type: [string, 'null'], maxLength: 160}
        description: {type: [string, 'null'], maxLength: 2000, deprecated: true}
        privateNote: {type: [string, 'null'], maxLength: 2000}
        state: {type: string, enum: [active, disabled]}
        expiresIn: {type: [string, 'null'], pattern: '^[1-9][0-9]*(?:s|h|d|w)$'}
        expiresAt: {type: string, format: date-time, description: Use expiresIn with null to remove an existing expiry.}
        password: {type: [string, 'null'], minLength: 3, maxLength: 128}
        maxRedirects: {type: [integer, 'null'], minimum: 1}
        capMode: {$ref: '#/components/schemas/CapMode'}
        forwardQuery: {type: boolean}
        tags: {type: array, maxItems: 10, items: {type: string, minLength: 1, maxLength: 32}}
        utm: {$ref: '#/components/schemas/UTMFields'}
        revision: {type: integer, minimum: 1, deprecated: true, description: Approved Nuxt compatibility alias for If-Match.}
    AnonymousLinkUpdateRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        target: {type: string, format: uri, maxLength: 2048}
        customCode: {$ref: '#/components/schemas/ShortCode'}
        title: {type: [string, 'null'], maxLength: 160}
        description: {type: [string, 'null'], maxLength: 2000, deprecated: true}
        privateNote: {type: [string, 'null'], maxLength: 2000}
        state: {type: string, enum: [active, disabled]}
        expiresIn: {type: string, pattern: '^[1-9][0-9]*(?:s|h|d|w)$'}
        expiresAt: {type: string, format: date-time}
        password: {type: [string, 'null'], minLength: 3, maxLength: 128}
        maxRedirects: {type: [integer, 'null'], minimum: 1}
        capMode: {$ref: '#/components/schemas/CapMode'}
        forwardQuery: {type: boolean}
        utm: {$ref: '#/components/schemas/UTMFields'}
        revision: {type: integer, minimum: 1, deprecated: true, description: Approved Nuxt compatibility alias for If-Match.}
    UTMFields:
      type: object
      additionalProperties: false
      properties:
        utm_source: {type: string, maxLength: 256}
        utm_medium: {type: string, maxLength: 256}
        utm_campaign: {type: string, maxLength: 256}
        utm_term: {type: string, maxLength: 256}
        utm_content: {type: string, maxLength: 256}
    Link:
      type: object
      required: [id, code, shortUrl, target, state, createdAt, updatedAt, expiresAt, visitCount, passwordProtected, revision, tags, forwardQuery, capMode, utm]
      properties:
        id: {type: string, format: uuid}
        workspaceId: {type: [string, 'null'], format: uuid}
        domainId: {type: string, format: uuid}
        code: {$ref: '#/components/schemas/ShortCode'}
        shortUrl: {type: string, format: uri}
        target: {type: string, format: uri}
        title: {type: [string, 'null']}
        privateNote: {type: [string, 'null']}
        description: {type: [string, 'null'], deprecated: true, description: Temporary client alias for privateNote.}
        state: {$ref: '#/components/schemas/LinkState'}
        banned: {type: boolean, deprecated: true, description: Temporary derived client compatibility; true for quarantined links.}
        createdAt: {type: string, format: date-time}
        updatedAt: {type: string, format: date-time}
        expiresAt: {type: [string, 'null'], format: date-time}
        visitCount: {type: integer, minimum: 0}
        passwordProtected: {type: boolean}
        maxRedirects: {type: [integer, 'null'], minimum: 1}
        capMode: {$ref: '#/components/schemas/CapMode'}
        forwardQuery: {type: boolean}
        revision: {type: integer, minimum: 1}
        tags: {type: array, maxItems: 10, items: {$ref: '#/components/schemas/Tag'}}
        utm: {$ref: '#/components/schemas/UTMFields'}
    LinkResponse:
      type: object
      required: [data]
      properties:
        data: {$ref: '#/components/schemas/Link'}
    PublicLinkCreateResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [link, managementSecret]
          properties:
            link: {$ref: '#/components/schemas/Link'}
            managementSecret: {type: string, pattern: '^1ll_manage_[A-Za-z0-9_-]{43}$', description: Returned once; the service stores only a digest.}
    LinkListResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [items, meta]
          properties:
            items: {type: array, items: {$ref: '#/components/schemas/Link'}}
            meta: {$ref: '#/components/schemas/CollectionMeta'}
    AdminLink:
      allOf:
        - $ref: '#/components/schemas/Link'
        - type: object
          required: [ownerEmail]
          properties:
            ownerEmail: {type: [string, 'null'], format: email}
    AdminLinkListResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [items, meta]
          properties:
            items: {type: array, items: {$ref: '#/components/schemas/AdminLink'}}
            meta: {$ref: '#/components/schemas/CollectionMeta'}
    CursorMeta:
      type: object
      required: [nextCursor, hasMore]
      properties:
        nextCursor: {type: [string, 'null']}
        hasMore: {type: boolean}
    CollectionMeta:
      type: object
      required: [nextCursor, hasMore, limit, total]
      properties:
        nextCursor: {type: [string, 'null']}
        hasMore: {type: boolean}
        limit: {type: integer, minimum: 1}
        total: {type: integer, minimum: 0}
    Tag:
      type: object
      required: [id, name]
      properties:
        id: {type: string, format: uuid}
        name: {type: string, minLength: 1, maxLength: 32}
        linkCount: {type: integer, minimum: 0}
    CodeCheckRequest:
      type: object
      additionalProperties: false
      required: [code]
      properties:
        code: {$ref: '#/components/schemas/ShortCode'}
    CodeCheckResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [available, advisory]
          properties:
            available: {type: boolean}
            advisory: {type: boolean, const: true}
            reason: {type: [string, 'null'], enum: [reserved, taken, tombstoned, invalid, null]}
    APIKeyScope:
      type: string
      enum: [links:read, links:write, links:delete, analytics:read, domains:read, domains:write, account:read]
    APIKeyCreateRequest:
      type: object
      additionalProperties: false
      required: [name, scopes]
      properties:
        name: {type: string, minLength: 1, maxLength: 80}
        scopes: {type: array, minItems: 1, uniqueItems: true, items: {$ref: '#/components/schemas/APIKeyScope'}}
        expiresAt: {type: [string, 'null'], format: date-time}
        cidrAllowlist: {type: array, maxItems: 20, items: {type: string}}
    APIKey:
      type: object
      required: [id, name, prefix, lastFour, scopes, createdAt, expiresAt, lastUsedAt, revokedAt]
      properties:
        id: {type: string, format: uuid}
        name: {type: string}
        prefix: {type: string}
        lastFour: {type: string, minLength: 4, maxLength: 4}
        scopes: {type: array, items: {$ref: '#/components/schemas/APIKeyScope'}}
        createdAt: {type: string, format: date-time}
        expiresAt: {type: [string, 'null'], format: date-time}
        lastUsedAt: {type: [string, 'null'], format: date-time}
        revokedAt: {type: [string, 'null'], format: date-time}
    APIKeyListResponse:
      type: object
      required: [data]
      properties:
        data: {type: array, items: {$ref: '#/components/schemas/APIKey'}}
    APIKeySecretResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [key, secret]
          properties:
            key: {$ref: '#/components/schemas/APIKey'}
            secret: {type: string, description: Full secret returned exactly once.}
    ImportResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [created, failed, failures]
          properties:
            created: {type: integer, minimum: 0, maximum: 500}
            failed: {type: integer, minimum: 0, maximum: 500}
            failures:
              type: array
              maxItems: 500
              items:
                type: object
                required: [row, code]
                properties:
                  row: {type: integer, minimum: 2}
                  code: {type: string}
    MFAStatusResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [required, enrolled, verified]
          properties:
            required: {type: boolean}
            enrolled: {type: boolean}
            verified: {type: boolean}
    MFAEnrollmentResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [factorId, secret, uri]
          properties:
            factorId: {type: string, format: uuid}
            secret: {type: string, description: TOTP secret returned exactly once.}
            uri: {type: string, description: otpauth URI returned exactly once.}
    MFAConfirmationResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [recoveryCodes]
          properties:
            recoveryCodes:
              type: array
              minItems: 10
              maxItems: 10
              items: {type: string, description: Single-use recovery code returned exactly once.}
    AnalyticsSummaryResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [clicks, humanClicks, botClicks, uniqueEstimate, byDeliverySource, timeline, countries, devices, browsers, operatingSystems, referrers, utmSources, coverage, complete]
          properties:
            clicks: {type: integer, minimum: 0}
            humanClicks: {type: integer, minimum: 0}
            botClicks: {type: integer, minimum: 0}
            uniqueEstimate: {type: integer, minimum: 0}
            byDeliverySource:
              type: object
              required: [origin, edgeFailover, reconciled]
              properties:
                origin: {type: integer, minimum: 0}
                edgeFailover: {type: integer, minimum: 0}
                reconciled: {type: integer, minimum: 0}
            timeline:
              type: array
              maxItems: 366
              items:
                type: object
                required: [bucket, clicks]
                properties:
                  bucket: {type: string, format: date-time}
                  clicks: {type: integer, minimum: 0}
            countries:
              type: array
              maxItems: 249
              items: {$ref: '#/components/schemas/AnalyticsDimensionCount'}
            devices:
              type: array
              maxItems: 10
              items: {$ref: '#/components/schemas/AnalyticsDimensionCount'}
            browsers:
              type: array
              maxItems: 10
              items: {$ref: '#/components/schemas/AnalyticsDimensionCount'}
            operatingSystems:
              type: array
              maxItems: 10
              items: {$ref: '#/components/schemas/AnalyticsDimensionCount'}
            referrers:
              type: array
              maxItems: 10
              items: {$ref: '#/components/schemas/AnalyticsDimensionCount'}
            utmSources:
              type: array
              maxItems: 10
              items: {$ref: '#/components/schemas/AnalyticsDimensionCount'}
            coverage:
              type: object
              required: [legacyClicks, partialDimensions]
              properties:
                legacyClicks: {type: integer, minimum: 0}
                partialDimensions:
                  type: array
                  uniqueItems: true
                  items: {type: string, enum: [human_bot, unique, device, referrer, utm]}
            complete: {type: boolean}
            gapReason: {type: [string, 'null']}
    AnalyticsDimensionCount:
      type: object
      required: [key, clicks]
      properties:
        key: {type: string, minLength: 1, maxLength: 255}
        clicks: {type: integer, minimum: 0}
    AdminOverviewResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [users, links, activeUsers, suspendedUsers, activeLinks, quarantinedLinks, notificationQueued, notificationFailed]
          properties:
            users: {type: integer, minimum: 0}
            links: {type: integer, minimum: 0}
            activeUsers: {type: integer, minimum: 0}
            suspendedUsers: {type: integer, minimum: 0}
            activeLinks: {type: integer, minimum: 0}
            quarantinedLinks: {type: integer, minimum: 0}
            notificationQueued: {type: integer, minimum: 0}
            notificationFailed: {type: integer, minimum: 0}
    AdminUserListResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [items, meta]
          properties:
            items:
              type: array
              items:
                type: object
                required: [id, email, role, verified, banned, status, linksCount, activeSessions, lastLoginAt, createdAt]
                properties:
                  id: {type: string, format: uuid}
                  email: {type: string, format: email}
                  role: {type: string, enum: [member, admin]}
                  verified: {type: boolean}
                  banned: {type: boolean}
                  status: {type: string, enum: [pending, active, suspended, deletion_pending]}
                  linksCount: {type: integer, minimum: 0}
                  activeSessions: {type: integer, minimum: 0}
                  lastLoginAt: {type: [string, 'null'], format: date-time}
                  createdAt: {type: string, format: date-time}
            meta: {$ref: '#/components/schemas/CollectionMeta'}
    AdminUserRoleRequest:
      type: object
      additionalProperties: false
      required: [role]
      properties:
        role: {type: string, enum: [member, admin]}
    AdminRegistrationSettings:
      type: object
      additionalProperties: false
      required: [enabled, emailDeliveryReady, verificationMethod, source, updatedAt]
      properties:
        enabled: {type: boolean}
        emailDeliveryReady: {type: boolean}
        verificationMethod: {type: string, enum: [email_code, none]}
        source: {type: string, enum: [environment, database]}
        updatedAt: {type: [string, 'null'], format: date-time}
    AdminRegistrationSettingsRequest:
      type: object
      additionalProperties: false
      required: [enabled]
      properties:
        enabled: {type: boolean}
    AdminRegistrationSettingsResponse:
      type: object
      required: [data]
      properties:
        data: {$ref: '#/components/schemas/AdminRegistrationSettings'}
    AdminSMTPSettings:
      type: object
      required: [enabled, address, username, from, mode, serverName, timeoutSeconds, source, passwordConfigured, updatedAt]
      properties:
        enabled: {type: boolean}
        address: {type: string, maxLength: 255, examples: [smtp.example.com:587]}
        username: {type: string, maxLength: 255}
        from: {type: string, format: email, maxLength: 320}
        mode: {type: string, enum: [starttls, tls, none]}
        serverName: {type: string, maxLength: 255}
        timeoutSeconds: {type: integer, minimum: 1, maximum: 10}
        source: {type: string, enum: [environment, database]}
        passwordConfigured: {type: boolean}
        updatedAt: {type: [string, 'null'], format: date-time}
    AdminSMTPSettingsRequest:
      type: object
      additionalProperties: false
      required: [enabled, address, username, from, mode, serverName, timeoutSeconds]
      properties:
        enabled: {type: boolean}
        address: {type: string, maxLength: 255}
        username: {type: string, maxLength: 255}
        password: {type: string, format: password, maxLength: 512, writeOnly: true, description: Omit to preserve the current encrypted password; send an empty string to clear it.}
        from: {type: string, format: email, maxLength: 320}
        mode: {type: string, enum: [starttls, tls, none]}
        serverName: {type: string, maxLength: 255}
        timeoutSeconds: {type: integer, minimum: 1, maximum: 10}
    AdminSMTPSettingsResponse:
      type: object
      required: [data]
      properties:
        data: {$ref: '#/components/schemas/AdminSMTPSettings'}
    StatusResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [status, time]
          properties:
            status: {type: string, enum: [operational, degraded, unavailable]}
            time: {type: string, format: date-time}
    AcceptedResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [accepted]
          properties:
            accepted: {type: boolean, const: true}
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, requestId]
          properties:
            code:
              type: string
              enum:
                - AUTH_REQUIRED
                - AUTH_INVALID
                - AUTH_MFA_REQUIRED
                - AUTH_AMBIGUOUS
                - MFA_CODE_INVALID
                - MFA_FACTOR_NOT_FOUND
                - MFA_NOT_REQUIRED
                - ACCOUNT_PENDING
                - ACCOUNT_SUSPENDED
                - REGISTRATION_DISABLED
                - ACCOUNT_RECOVERY_DISABLED
                - EMAIL_DELIVERY_DISABLED
                - CSRF_INVALID
                - TURNSTILE_REQUIRED
                - TURNSTILE_INVALID
                - LINK_NOT_FOUND
                - LINK_CODE_TAKEN
                - SLUG_TAKEN
                - SLUG_RESERVED
                - LINK_EXPIRED
                - LINK_DISABLED
                - LINK_QUARANTINED
                - LINK_PASSWORD_REQUIRED
                - LINK_PASSWORD_INVALID
                - LINK_VISIT_LIMIT_REACHED
                - DESTINATION_INVALID
                - DESTINATION_BLOCKED
                - DOMAIN_NOT_VERIFIED
                - DOMAIN_NOT_ALLOWED
                - FORBIDDEN
                - REVISION_CONFLICT
                - REVISION_REQUIRED
                - EMAIL_UNAVAILABLE
                - EMAIL_NOT_VERIFIED
                - INVALID_CREDENTIALS
                - VERIFICATION_TOKEN_INVALID
                - VERIFICATION_CODE_INVALID
                - RESET_TOKEN_INVALID
                - SESSION_NOT_FOUND
                - API_KEY_NOT_FOUND
                - CURRENT_PASSWORD_INVALID
                - CURSOR_INVALID
                - HOST_INVALID
                - ORIGIN_INVALID
                - EXPIRY_TOO_LONG
                - IMPORT_TOO_LARGE
                - QUERY_INVALID
                - CODE_GENERATION_EXHAUSTED
                - IDEMPOTENCY_KEY_INVALID
                - IDEMPOTENCY_KEY_REUSED
                - IDEMPOTENCY_IN_PROGRESS
                - USER_NOT_FOUND
                - ADMIN_SELF_SUSPEND
                - ADMIN_SELF_DEMOTE
                - ADMIN_LAST_ADMIN
                - ADMIN_SELF_SESSION_REVOKE
                - SMTP_DISABLED
                - SMTP_TEST_FAILED
                - RATE_LIMITED
                - QUOTA_EXCEEDED
                - VALIDATION_FAILED
                - SERVICE_DEGRADED
                - SERVICE_UNAVAILABLE
                - INTERNAL_ERROR
                - CONTENT_TYPE_INVALID
                - BODY_TOO_LARGE
                - METHOD_NOT_ALLOWED
                - ROUTE_NOT_FOUND
            message: {type: string}
            requestId: {type: string}
            retryAfterSeconds: {type: integer, minimum: 1}
            details:
              type: array
              maxItems: 32
              items:
                type: object
                required: [field, reason]
                properties:
                  field: {type: string}
                  reason: {type: string}

  responses:
    ValidationFailed:
      description: Request validation failed
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ErrorResponse'}
    Unauthorized:
      description: Authentication is missing or invalid
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ErrorResponse'}
    Forbidden:
      description: The actor cannot access the requested workspace resource
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ErrorResponse'}
    CSRFInvalid:
      description: Browser CSRF validation failed
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ErrorResponse'}
    AccountUnavailable:
      description: The account is pending, suspended, or requires MFA
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ErrorResponse'}
    NotFound:
      description: Resource is absent or hidden by tenant isolation
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ErrorResponse'}
    Conflict:
      description: A unique, tombstone, or idempotency conflict occurred
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ErrorResponse'}
    RevisionConflict:
      description: If-Match does not match the current resource revision
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ErrorResponse'}
    PreconditionRequired:
      description: A current resource revision is required for this mutation
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ErrorResponse'}
    RateLimited:
      description: A bounded rate or concurrency limit rejected the request
      headers:
        Retry-After:
          schema: {type: integer, minimum: 1}
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ErrorResponse'}
    BodyTooLarge:
      description: The request body exceeds the endpoint limit
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ErrorResponse'}
    Unavailable:
      description: The control plane is temporarily unavailable
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ErrorResponse'}
