# YYDS Mail API — OpenAPI 3.1 specification.
#
# Single source of truth for the public HTTP surface. Hand-written; kept in
# sync with pkg/yydsapi/routes.go by tools/openapi_audit (make openapi-audit).
# Paths below intentionally omit the /v1 prefix — it lives in servers[].url.
openapi: 3.1.0
info:
  title: YYDS Mail API
  version: "1.0.0"
  description: |
    YYDS Mail is a receive-only email asset platform: temporary inboxes,
    custom domains, wildcard subdomains, API keys, webhooks and billing.

    All JSON endpoints share one envelope:
    `{ "success": bool, "data": ..., "error": "...", "errorCode": "..." }`.
    Prefer `errorCode` (stable, machine-readable) over the localized `error`
    text. The full errorCode catalog is served at `GET /v1/error-codes`.

    Rate limits: anonymous requests are IP rate-limited; authenticated
    requests are limited by plan tier. 429 responses include `Retry-After`.
    When the plan API call quota is exhausted, 429 carries
    `errorCode: quota_exhausted` plus `resetAt` and `upgrade` links —
    do not retry before `resetAt` (see QuotaExhaustedEnvelope).
  contact:
    name: YYDS Mail
    email: xiaolajiaoyyds@gmail.com
servers:
  - url: https://maliapi.215.im/v1
    description: Production

security: []

tags:
  - name: Public
    description: No-auth discovery endpoints (health, domains, plans, pricing, stats).
  - name: Inboxes
    description: >
      Canonical inbox resource (naming convergence step 1). Prefer
      GET /inboxes/{id} and GET /inboxes/{id}/messages over the legacy
      accounts read alias and the address-keyed message list.
  - name: Accounts
    description: Temporary inbox lifecycle (DuckMail/Mail.tm compatible surface).
  - name: Messages
    description: Read, mark, and delete mail for an inbox you control.
  - name: Mailbox Shares
    description: Public read-only mailbox share links.
  - name: Auth
    description: Registration, login, tokens, OAuth, passkeys.
  - name: Me
    description: Signed-in user resources (profile, inboxes, domains, API keys, webhooks...).
  - name: Payment
    description: Orders, checkout, balance top-up.
  - name: Content
    description: Public announcements and articles.
  - name: Admin
    x-internal: true
    description: Internal console endpoints. Not part of the public contract.

components:
  securitySchemes:
    cookieAuth:
      type: apiKey
      in: cookie
      name: yyds_access_token
      description: HttpOnly JWT cookie set by /auth/login (web console sessions).
    bearerAuth:
      type: http
      scheme: bearer
      description: "JWT access token via `Authorization: Bearer <access_token>`."
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: "API key (`AC-` prefix) created from the dashboard. Also accepted as `Authorization: Bearer AC-...`."
    tempToken:
      type: http
      scheme: bearer
      description: Short-lived temp-inbox token returned by POST /accounts and POST /token. Only valid for the temp-inbox surface (/accounts/me, /accounts/{id}, /messages*, /sources/{id}).

  parameters:
    limit:
      name: limit
      in: query
      description: Page size (handler-specific default and max apply).
      schema:
        type: integer
        minimum: 1
    offset:
      name: offset
      in: query
      description: Zero-based item offset for pagination.
      schema:
        type: integer
        minimum: 0
    address:
      name: address
      in: query
      description: >
        Inbox address to operate on. Required for JWT / API key callers;
        ignored for temp tokens (the token is bound to one inbox).
      schema:
        type: string
        format: email
    messageId:
      name: id
      in: path
      required: true
      description: Inbucket message ID.
      schema:
        type: string
    shareToken:
      name: token
      in: path
      required: true
      description: Public mailbox share token from the share link.
      schema:
        type: string

  schemas:
    Envelope:
      type: object
      description: Standard response envelope shared by every JSON endpoint.
      required: [success]
      properties:
        success:
          type: boolean
        data:
          description: Payload; omitted on most errors.
        error:
          type: string
          description: Localized human-readable error (zh/en via Accept-Language / X-Locale). Empty on success.
        errorCode:
          type: string
          description: Stable machine-readable error code (see GET /error-codes). Empty on success.
    ErrorEnvelope:
      type: object
      description: Error variant of the envelope.
      required: [success, error]
      properties:
        success:
          type: boolean
          const: false
        error:
          type: string
        errorCode:
          type: string
    QuotaExhaustedEnvelope:
      type: object
      description: >
        Quota circuit breaker 429 variant (errorCode `quota_exhausted`):
        returned when the account's plan API call quota (daily / weekly /
        monthly) is confirmed exhausted and no purchased call-pack credits
        remain. Further API-usage requests are rejected immediately until
        the quota period resets or a purchase restores capacity. Response
        headers: `Retry-After` (seconds until reset) and `X-RateLimit-Reset`
        (reset unix timestamp). Web console (JWT) requests are never
        circuit-broken, so users can always sign in to upgrade or buy packs.
      required: [success, error, errorCode, resetAt, upgrade]
      properties:
        success:
          type: boolean
          const: false
        error:
          type: string
          description: Localized message (zh/en via Accept-Language / X-Locale).
        errorCode:
          type: string
          const: quota_exhausted
        resetAt:
          type: string
          format: date-time
          description: When the quota resets, ISO 8601 in business timezone (Asia/Shanghai, +08:00).
        upgrade:
          type: object
          description: Absolute URLs to restore capacity immediately.
          properties:
            pricing:
              type: string
              format: uri
              description: Plan upgrade page.
            callPacks:
              type: string
              format: uri
              description: Call pack purchase page.
    EmailAddress:
      type: object
      properties:
        name:
          type: string
        address:
          type: string
    Attachment:
      type: object
      properties:
        id:
          type: string
        filename:
          type: string
        contentType:
          type: string
        size:
          type: integer
        downloadUrl:
          type: string
          description: Relative /serve/... URL. For temp tokens a ?token= query is appended.
        contentId:
          type: string
        disposition:
          type: string
    MessageListItem:
      type: object
      description: Header-level message summary (list view; attachments not parsed).
      properties:
        id:
          type: string
        inbox_id:
          type: string
        inboxId:
          type: string
          description: Compat duplicate of inbox_id.
        from:
          $ref: "#/components/schemas/EmailAddress"
        to:
          type: array
          items:
            $ref: "#/components/schemas/EmailAddress"
        subject:
          type: string
        seen:
          type: boolean
        size:
          type: integer
        hasAttachments:
          type: boolean
          description: Always false in list view (bodies are not parsed).
        createdAt:
          type: string
          format: date-time
    MessageDetail:
      type: object
      description: Full message content (GET /messages/{id}).
      properties:
        id:
          type: string
        inboxId:
          type: string
        messageId:
          type: string
          description: Raw RFC 5322 Message-ID header (with angle brackets).
        from:
          $ref: "#/components/schemas/EmailAddress"
        to:
          type: array
          items:
            $ref: "#/components/schemas/EmailAddress"
        subject:
          type: string
        intro:
          type: string
        text:
          type: string
        html:
          type: array
          items:
            type: string
        seen:
          type: boolean
        size:
          type: integer
        hasAttachments:
          type: boolean
        createdAt:
          type: string
          format: date-time
        cc:
          type: array
          items:
            type: string
        bcc:
          type: array
          items:
            type: string
        attachments:
          type: array
          items:
            $ref: "#/components/schemas/Attachment"
    TempAccount:
      type: object
      description: Temp inbox profile (GET /accounts/me, GET /accounts/{id}).
      properties:
        id:
          type: string
        address:
          type: string
        inboxType:
          type: string
          enum: [temp, persistent]
        source:
          type: string
          enum: [web, api]
        expiresAt:
          type: [string, "null"]
          format: date-time
        isActive:
          type: boolean
        messageCount:
          type: integer
        createdAt:
          type: string
          format: date-time
    CreateAccountRequest:
      type: object
      description: >
        All fields are optional; the server generates a prefix and chooses an
        allowed domain when omitted. API-key callers can opt into
        platform-aware selection with platformCode. Request-only exclusions
        are additive to persistent key exclusions and capped at 100 combined.
      properties:
        localPart:
          type: string
          description: Preferred prefix field (1-64 chars, alphanumeric/dot/underscore/hyphen).
        address:
          type: string
          description: Legacy prefix field; may include a domain (`user@domain`) which must match `domain`.
          deprecated: true
        domain:
          type: string
          description: Target domain. Mutually exclusive with wildcardRuleId.
        wildcardRuleId:
          type: string
          description: Create under a wildcard rule (owner/API-key scoped).
        subdomain:
          type: string
          description: Wildcard child-domain label; requires domain or wildcardRuleId (or the /accounts/wildcard endpoint).
        subdomainLabel:
          type: string
          description: Alias of subdomain.
        autoDomainStrategy:
          type: string
          description: Domain auto-pick strategy hint for API keys.
        platformCode:
          type: string
          description: >
            Registration platform code (for example github or discord). Uses
            the API key's default platform when omitted. Platform-aware
            requests apply the key's community-status avoidance policy.
        excludeDomainIds:
          type: array
          maxItems: 100
          description: Per-request domain ID exclusions, additive to the API key policy; 100 combined with excludeDomains and persistent exclusions.
          items:
            type: string
        excludeDomains:
          type: array
          maxItems: 100
          description: Per-request domain-name exclusions, additive to the API key policy; 100 combined with excludeDomainIds and persistent exclusions.
          items:
            type: string
        password:
          type: string
          description: Accepted for DuckMail compatibility; ignored (temp inboxes are passwordless).
    CreateAccountResponse:
      type: object
      properties:
        id:
          type: string
        address:
          type: string
        mode:
          type: string
          enum: [fixed, wildcard]
        domain:
          type: string
        subdomain:
          type: string
        token:
          type: string
          description: Temp-inbox bearer token for the temp-token auth scheme.
        inboxType:
          type: string
        source:
          type: string
        expiresAt:
          type: [string, "null"]
          format: date-time
        isActive:
          type: boolean
        createdAt:
          type: string
          format: date-time
        selection:
          type: object
          description: >
            Present for API-key domain selection. If an explicitly requested
            domain is excluded, creation returns HTTP 409 instead of silently
            replacing it; smart fallback applies only when domain is omitted.
          properties:
            domainId: { type: string }
            domain: { type: string }
            platformCode: { type: string }
            platformStatus: { type: string }
            fallbackUsed: { type: boolean }
            excludedCount: { type: integer }
        anonQuota:
          type: object
          description: Anonymous-creation quota hint (only for anonymous web-bridge callers).
    PlatformCatalogEntry:
      type: object
      required: [code, name, category, aliases, websiteDomain, iconSlug, brandColor, source, isActive, sortOrder]
      properties:
        code:
          type: string
          description: Stable value accepted as platformCode.
        name:
          type: string
        category:
          type: string
        aliases:
          type: array
          description: Reviewed sender or website domains used for platform detection.
          items: { type: string }
        websiteDomain: { type: string }
        iconSlug:
          type: string
          description: Optional Simple Icons slug rendered from the pinned local package.
        brandColor:
          type: string
          pattern: "^[A-Fa-f0-9]{0,6}$"
        source:
          type: string
          enum: [seed, admin, community]
        isActive:
          type: boolean
        sortOrder:
          type: integer
    PlatformSuggestion:
      type: object
      required: [id, name, proposedCode, websiteDomain, iconSlug, brandColor, source, status, supportCount, createdAt, updatedAt]
      properties:
        id: { type: string }
        name: { type: string }
        proposedCode: { type: string }
        websiteDomain: { type: string }
        iconSlug: { type: string }
        brandColor: { type: string }
        source:
          type: string
          enum: [manual, inbox_detection]
        status:
          type: string
          enum: [pending, approved, rejected, merged]
        supportCount:
          type: integer
          description: Distinct users supporting the grouped candidate.
        mergedPlatformCode: { type: [string, "null"] }
        reviewNote: { type: string }
        reviewedBy: { type: [string, "null"] }
        reviewedAt: { type: [string, "null"], format: date-time }
        submittedByMe: { type: boolean }
        senderDomain: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    PlatformDetectionResult:
      type: object
      required: [matched, confidence, reason, inboxId]
      properties:
        matched: { type: boolean }
        confidence:
          type: string
          enum: [none, high, suggestion]
        reason: { type: string }
        platform:
          oneOf:
            - $ref: "#/components/schemas/PlatformCatalogEntry"
            - type: "null"
        suggestedName: { type: string }
        suggestedCode: { type: string }
        websiteDomain: { type: string }
        senderName: { type: string }
        senderAddress: { type: string }
        senderDomain: { type: string }
        subject: { type: string }
        inboxId: { type: string }
        messageId: { type: string }
    APIKeyDomainExclusion:
      type: object
      required: [id, apiKeyId, domainId, domain, reasonCode, source, createdAt, updatedAt]
      properties:
        id: { type: string }
        apiKeyId: { type: string }
        domainId: { type: string }
        domain: { type: string }
        platformCode:
          type: [string, "null"]
          description: Null applies this exclusion to every platform.
        reasonCode: { type: string }
        source: { type: string }
        expiresAt: { type: [string, "null"], format: date-time }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    APIKeyDomainPolicy:
      type: object
      required: [apiKeyId, configured, avoidMode, fallbackMode, exclusions]
      properties:
        apiKeyId: { type: string }
        configured:
          type: boolean
          description: False for an existing key that has never saved a routing policy.
        defaultPlatformCode: { type: [string, "null"] }
        avoidMode:
          type: string
          enum: [off, confirmed, suspected]
        fallbackMode:
          type: string
          enum: [smart, strict]
        exclusions:
          type: array
          items:
            $ref: "#/components/schemas/APIKeyDomainExclusion"
        updatedAt: { type: [string, "null"], format: date-time }
    APIKeyDomainPolicyUpdate:
      type: object
      required: [avoidMode, fallbackMode, exclusions]
      properties:
        defaultPlatformCode:
          type: [string, "null"]
          description: Null clears the key's default platform.
        avoidMode:
          type: string
          enum: [off, confirmed, suspected]
        fallbackMode:
          type: string
          enum: [smart, strict]
        exclusions:
          type: array
          maxItems: 100
          items:
            type: object
            required: [domainId]
            properties:
              domainId: { type: string }
              platformCode:
                type: [string, "null"]
                description: Null applies this exclusion to every platform.
              reasonCode: { type: string, maxLength: 100 }
    DomainPlatformStatusPublic:
      type: object
      required:
        - domainId
        - domain
        - platformCode
        - platformName
        - platformIconSlug
        - platformBrandColor
        - status
        - effectiveStatus
        - reportCount
        - positiveCount
        - negativeCount
        - strongNegativeCount
        - confidence
        - platformIncident
        - updatedAt
      properties:
        domainId: { type: string }
        domain: { type: string }
        platformCode: { type: string }
        platformName: { type: string }
        platformIconSlug: { type: string }
        platformBrandColor: { type: string }
        status:
          type: string
          enum: [unknown, recently_available, watching, suspected_restricted, confirmed_restricted, recovering, platform_incident]
        effectiveStatus:
          type: string
          description: Final status after aggregation, platform incidents, and active admin overrides.
          enum: [unknown, recently_available, watching, suspected_restricted, confirmed_restricted, recovering, platform_incident]
        reportCount: { type: integer }
        positiveCount: { type: integer }
        negativeCount: { type: integer }
        strongNegativeCount: { type: integer }
        confidence: { type: number, format: double }
        platformIncident: { type: boolean }
        lastReportAt: { type: [string, "null"], format: date-time }
        statusChangedAt: { type: [string, "null"], format: date-time }
        updatedAt: { type: string, format: date-time }
    DomainPlatformReport:
      type: object
      required:
        - id
        - domainId
        - domain
        - platformCode
        - inboxId
        - observedDomain
        - outcome
        - reasonCode
        - evidenceStrength
        - reportScope
        - createdAt
        - updatedAt
      properties:
        id: { type: string }
        reporterUserId: { type: string }
        reporterApiKeyId: { type: [string, "null"] }
        domainId: { type: string }
        domain: { type: string }
        platformCode: { type: string }
        inboxId: { type: string }
        observedDomain: { type: string }
        outcome:
          type: string
          enum: [available, blocked, verification_received, verification_missing, other_failure]
        reasonCode: { type: string }
        evidenceStrength:
          type: string
          enum: [positive, strong, weak]
        reportScope:
          type: string
          enum: [exact, parent]
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    Domain:
      type: object
      properties:
        id:
          type: string
        domain:
          type: string
        ownerId:
          type: string
        isVerified:
          type: boolean
        isMxValid:
          type: boolean
        isCnameValid:
          type: boolean
        verificationToken:
          type: string
        isPublic:
          type: boolean
        dnsRecords:
          description: Raw DNS record JSON blob.
        sourceType:
          type: string
        dnsSyncMode:
          type: string
        dnsServiceConnectionId:
          type: string
        sourceZoneName:
          type: string
        dnsLastSyncStatus:
          type: string
        dnsLastSyncAt:
          type: string
          format: date-time
        verifiedAt:
          type: string
          format: date-time
        lastDnsCheckAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    DomainSelectionRequest:
      type: object
      required: [domainIds]
      properties:
        domainIds:
          type: array
          minItems: 1
          maxItems: 20
          uniqueItems: true
          items:
            type: string
          description: IDs of domains owned by the signed-in user.
    DomainDNSRecord:
      type: object
      required: [type, name, value]
      properties:
        type:
          type: string
          enum: [MX, TXT]
        name:
          type: string
          description: Complete DNS owner name. Clients may render it relative to sourceZoneName.
        value:
          type: string
          description: Raw value to submit to the DNS provider. TXT values are not wrapped in quotes.
        displayValue:
          type: string
        priority:
          type: integer
        ttl:
          type: integer
    DomainDNSConfigPreview:
      type: object
      required: [items, summary]
      properties:
        items:
          type: array
          items:
            type: object
            required: [domainId, domain, receivingReady, records]
            properties:
              domainId: { type: string }
              domain: { type: string }
              sourceZoneName:
                type: string
                description: Authoritative zone known by a connected DNS provider, when available.
              dnsSyncMode: { type: string }
              receivingReady: { type: boolean }
              records:
                type: array
                items:
                  $ref: "#/components/schemas/DomainDNSRecord"
        summary:
          type: object
          required: [domains, records]
          properties:
            domains: { type: integer }
            records: { type: integer }
    DomainDNSBatchVerify:
      type: object
      required: [items, summary]
      properties:
        items:
          type: array
          items:
            type: object
            required: [domainId, domain, result]
            properties:
              domainId: { type: string }
              domain: { type: string }
              result:
                type: string
                enum: [receiving_ready, dns_propagating, txt_missing, mx_missing, wildcard_mx_missing, record_conflict, check_failed]
              detail: { type: string }
              statusData:
                type: object
                additionalProperties: true
        summary:
          type: object
          required: [total]
          additionalProperties:
            type: integer
          properties:
            total: { type: integer }
    Plan:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        description: { type: string }
        priceMonthly: { type: number }
        maxDomains: { type: integer }
        maxInboxes: { type: integer }
        maxApiKeys: { type: integer }
        maxSubdomainDepth: { type: integer }
        maxMessagesPerDay: { type: integer }
        maxApiCallsDaily: { type: integer }
        maxApiCallsWeekly: { type: integer }
        maxApiCallsMonthly: { type: integer }
        maxWebhooks: { type: integer }
        maxWildcardRules: { type: integer }
        retentionDays: { type: integer }
        maxAttachmentBytes: { type: integer }
        storageBytes: { type: integer }
        maxRps: { type: integer }
        maxExternalSyncMessages:
          type: integer
          description: >
            Envelope retention cap PER linked external mailbox (封/账号).
            The sync engine keeps the newest N envelopes per account and
            evicts older index rows automatically.
        features:
          type: array
          items: { type: string }
        sortOrder: { type: integer }
        isActive: { type: boolean }

  responses:
    OK:
      description: Success envelope. See the endpoint summary/description for the data shape.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Envelope"
    Created:
      description: Resource created; data described in the endpoint summary.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Envelope"
    NoContent:
      description: Success with no body.
    BadRequest:
      description: Invalid input.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    Unauthorized:
      description: Missing or invalid credentials.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    Forbidden:
      description: Authenticated but not allowed.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    Conflict:
      description: Resource state conflict (e.g. address already in use).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    RateLimited:
      description: >
        Too many requests. `Retry-After` header included. Two variants share
        this status: (1) transient rate limiting (token bucket / burst guard)
        — back off for `Retry-After` seconds and retry; (2) quota exhaustion
        (`errorCode: quota_exhausted`, see QuotaExhaustedEnvelope) — the plan
        API call quota is used up; retrying before `resetAt` will keep
        returning 429. Purchase a call pack or upgrade the plan to resume
        immediately.
      content:
        application/json:
          schema:
            oneOf:
              - $ref: "#/components/schemas/ErrorEnvelope"
              - $ref: "#/components/schemas/QuotaExhaustedEnvelope"

paths:
  # ── Public discovery ──
  /health:
    get:
      tags: [Public]
      summary: Health check
      description: Returns ok/degraded based on PG, Redis (if configured), and the SMTP listener. Degraded responses use HTTP 503 with success=false.
      operationId: getHealth
      responses:
        "200":
          description: All subsystems healthy. data = { status, version, buildDate }.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "503":
          description: One or more subsystems degraded (same data shape, success=false).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"

  /llms.txt:
    get:
      tags: [Public]
      summary: LLM-friendly API summary (plain text)
      operationId: getLlmsTxt
      responses:
        "200":
          description: Plain-text public API summary for AI assistants.
          content:
            text/plain:
              schema:
                type: string

  /openapi.yaml:
    get:
      tags: [Public]
      summary: This OpenAPI specification (YAML)
      description: "Read-only, CORS-open. Importable into Postman / Swagger UI: https://maliapi.215.im/v1/openapi.yaml"
      operationId: getOpenAPIYaml
      responses:
        "200":
          description: OpenAPI 3.1 document.
          content:
            application/yaml:
              schema:
                type: string

  /openapi.json:
    get:
      tags: [Public]
      summary: This OpenAPI specification (JSON)
      operationId: getOpenAPIJson
      responses:
        "200":
          description: OpenAPI 3.1 document converted from the YAML source.
          content:
            application/json:
              schema:
                type: object

  /error-codes:
    get:
      tags: [Public]
      summary: Public errorCode catalog
      description: >
        Machine-readable registry of every stable errorCode the API can emit,
        with English and Chinese messages. Generated from the server's error
        localization table; prefer these codes over localized error text.
      operationId: getErrorCodes
      responses:
        "200":
          description: "{ source, count, errorCodes: [{ code, en, zh?, source }] }."
          content:
            application/json:
              schema:
                type: object
                properties:
                  source:
                    type: string
                  count:
                    type: integer
                  errorCodes:
                    type: array
                    items:
                      type: object
                      properties:
                        code:
                          type: string
                        en:
                          type: string
                          description: English message, or the regexp pattern for source=pattern entries.
                        zh:
                          type: string
                        source:
                          type: string
                          enum: [exact, pattern]

  /domains:
    get:
      tags: [Public]
      summary: List available domains
      description: >
        Anonymous callers see public domains only. Authenticated (JWT / API key)
        callers additionally see their own receiving-ready private domains.
      operationId: listDomains
      security:
        - {}
        - cookieAuth: []
        - bearerAuth: []
        - apiKey: []
      responses:
        "200":
          description: data = array of Domain.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Domain"
        "429":
          $ref: "#/components/responses/RateLimited"

  /platforms:
    get:
      tags: [Public]
      summary: List supported registration platforms
      description: Returns the stable platformCode values accepted by platform-aware account creation, API-key routing policy, and domain-platform reports.
      operationId: listPlatforms
      responses:
        "200":
          description: data.platforms = active platform catalog.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        required: [platforms]
                        properties:
                          platforms:
                            type: array
                            items:
                              $ref: "#/components/schemas/PlatformCatalogEntry"
        "429": { $ref: "#/components/responses/RateLimited" }

  /plans:
    get:
      tags: [Public]
      summary: List active plans
      operationId: listPlans
      responses:
        "200":
          description: data = array of Plan.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Plan"
        "429":
          $ref: "#/components/responses/RateLimited"

  /pricing:
    get:
      tags: [Public]
      summary: Public pricing (API-call packages + capacity add-ons + rate-limit tiers)
      operationId: getPricing
      responses:
        "200":
          description: >
            data = { packages, addonPackages, rateLimits, currency }.
            addonPackages entries are { type: storage|inboxes, quantity,
            priceCents } — storage quantity is GB, inboxes quantity is
            persistent inbox slots (A3 purchased pool).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "429":
          $ref: "#/components/responses/RateLimited"

  /stats:
    get:
      tags: [Public]
      summary: Public platform statistics
      operationId: getPublicStats
      responses:
        "200":
          description: data = public stats snapshot (cached ~30s).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "429":
          $ref: "#/components/responses/RateLimited"

  # ── Inboxes (canonical resource, C1 naming convergence step 1) ──
  /inboxes/{id}:
    get:
      tags: [Inboxes]
      summary: Get inbox by ID (canonical)
      description: >
        Canonical successor of GET /accounts/{id} — same handler, identical
        behavior and response shape. Temp tokens may only read their own
        inbox; JWT / API key callers must own the inbox.
      operationId: getInboxById
      security:
        - tempToken: []
        - cookieAuth: []
        - bearerAuth: []
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: data = TempAccount.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/TempAccount"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /inboxes/{id}/messages:
    get:
      tags: [Inboxes]
      summary: List messages in an inbox (canonical)
      description: >
        Canonical successor of GET /messages?address=… keyed by inbox ID
        instead of address. Delegates to the same implementation: identical
        filters, pagination, and response shape. Temp tokens may only read
        their own inbox; JWT / API key callers must own the inbox.
      operationId: listInboxMessages
      security:
        - tempToken: []
        - cookieAuth: []
        - bearerAuth: []
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          description: Max items (default 50, max 200).
          schema:
            type: integer
            minimum: 1
            maximum: 200
        - $ref: "#/components/parameters/offset"
        - name: seen
          in: query
          description: Read-state filter (seen=false = only unread).
          schema:
            type: boolean
        - name: since
          in: query
          description: Only messages at/after this RFC 3339 timestamp (inclusive).
          schema:
            type: string
            format: date-time
        - name: q
          in: query
          description: Case-insensitive substring match on subject and from.
          schema:
            type: string
        - name: after_id
          in: query
          description: Cursor — return messages strictly after this id (supersedes offset).
          schema:
            type: string
      responses:
        "200":
          description: data = { messages, total, unreadCount, nextCursor? }.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          messages:
                            type: array
                            items:
                              $ref: "#/components/schemas/MessageListItem"
                          total:
                            type: integer
                          unreadCount:
                            type: integer
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Temp inbox (accounts) ──
  /accounts:
    post:
      tags: [Accounts]
      summary: Create a temporary inbox
      description: >
        DuckMail-compatible creation endpoint. Anonymous browser creation is
        restricted to the official web bridge; API callers authenticate with
        an API key or JWT. Response includes a temp token bound to the inbox.
      operationId: createAccount
      security:
        - {}
        - cookieAuth: []
        - bearerAuth: []
        - apiKey: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateAccountRequest"
      responses:
        "201":
          description: data = created temp inbox + token.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/CreateAccountResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"

  /accounts/me:
    get:
      tags: [Accounts]
      summary: Current temp inbox profile
      description: Temp-token only; returns the inbox bound to the presented token.
      operationId: getAccountMe
      security:
        - tempToken: []
      responses:
        "200":
          description: data = TempAccount.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/TempAccount"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /accounts/{id}:
    get:
      tags: [Accounts]
      summary: "Get temp inbox by ID (legacy alias of GET /inboxes/{id})"
      description: >
        Deprecated read alias — use the canonical GET /inboxes/{id} instead
        (same handler, identical response). Responses carry Deprecation and
        Link successor-version headers. The alias remains available for a
        12-month transition window.
      operationId: getAccountById
      deprecated: true
      security:
        - tempToken: []
        - cookieAuth: []
        - bearerAuth: []
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: data = TempAccount.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/TempAccount"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags: [Accounts]
      summary: Deactivate a temp inbox
      description: Temp tokens may only delete their own inbox; JWT / API key (write) callers may delete temp inboxes they own. Persistent inboxes use DELETE /me/inboxes/{id}.
      operationId: deleteAccount
      security:
        - tempToken: []
        - cookieAuth: []
        - bearerAuth: []
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Inbox deactivated (no body).
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"

  /token:
    post:
      tags: [Accounts]
      summary: Refresh the temp token for an inbox
      description: DuckMail-compatible. Requires the current (still valid) temp token for the same address; issues a fresh one.
      operationId: refreshTempToken
      security:
        - tempToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [address]
              properties:
                address:
                  type: string
                password:
                  type: string
                  description: Accepted for compatibility; ignored.
      responses:
        "200":
          description: data = { id, address, token }.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ── Messages ──
  /messages:
    get:
      tags: [Messages]
      summary: List messages in an inbox
      operationId: listMessages
      security:
        - tempToken: []
        - cookieAuth: []
        - bearerAuth: []
        - apiKey: []
      parameters:
        - $ref: "#/components/parameters/address"
        - name: limit
          in: query
          description: Max items (default 50, max 200).
          schema:
            type: integer
            minimum: 1
            maximum: 200
        - $ref: "#/components/parameters/offset"
      responses:
        "200":
          description: data = { messages, total, unreadCount }.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          messages:
                            type: array
                            items:
                              $ref: "#/components/schemas/MessageListItem"
                          total:
                            type: integer
                          unreadCount:
                            type: integer
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /messages/next:
    get:
      tags: [Messages]
      summary: Take the next unread message (atomically marks it read)
      description: >
        Returns the oldest unread message in scope and marks it read in the
        same operation. Designed for one-call verification-code retrieval:
        the response detail includes `verificationCode` when the server
        detects an OTP-style code. Concurrent callers never receive the
        same message. Returns 204 when no unread message exists (after the
        optional wait window).
      operationId: takeNextMessage
      security:
        - tempToken: []
        - cookieAuth: []
        - bearerAuth: []
        - apiKey: []
      parameters:
        - name: address
          in: query
          description: Restrict to one owned inbox; omit to take from all inboxes.
          schema:
            type: string
        - name: wait
          in: query
          description: Long-poll seconds (0-30) to hold the request until mail arrives.
          schema:
            type: integer
            minimum: 0
            maximum: 30
      responses:
        "200":
          description: data = { message (MessageDetail incl. verificationCode), inboxAddress }.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          message:
                            $ref: "#/components/schemas/MessageDetail"
                          inboxAddress:
                            type: string
        "204":
          description: No unread message in scope.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /messages/{id}:
    get:
      tags: [Messages]
      summary: Get full message detail
      operationId: getMessage
      security:
        - tempToken: []
        - cookieAuth: []
        - bearerAuth: []
        - apiKey: []
      parameters:
        - $ref: "#/components/parameters/messageId"
        - $ref: "#/components/parameters/address"
      responses:
        "200":
          description: data = MessageDetail.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/MessageDetail"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags: [Messages]
      summary: Update message state (seen / starred)
      description: >
        Back-compat semantics: a PATCH without a `starred` field marks the
        message read; a starred-only PATCH leaves `seen` untouched. Stars are
        per-user and require a JWT or API key identity. API keys need write
        permission.
      operationId: updateMessage
      security:
        - cookieAuth: []
        - bearerAuth: []
        - apiKey: []
        - tempToken: []
      parameters:
        - $ref: "#/components/parameters/messageId"
        - $ref: "#/components/parameters/address"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                seen:
                  type: boolean
                starred:
                  type: boolean
      responses:
        "200":
          description: data = { id, seen, starred? }.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags: [Messages]
      summary: Delete a message
      description: API keys need write permission. Returns 204 on success.
      operationId: deleteMessage
      security:
        - cookieAuth: []
        - bearerAuth: []
        - apiKey: []
        - tempToken: []
      parameters:
        - $ref: "#/components/parameters/messageId"
        - $ref: "#/components/parameters/address"
      responses:
        "204":
          description: Message deleted (no body).
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /messages/mark-read:
    post:
      tags: [Messages]
      summary: Mark all messages in an inbox as read
      description: API keys need write permission.
      operationId: markMailboxRead
      security:
        - cookieAuth: []
        - bearerAuth: []
        - apiKey: []
        - tempToken: []
      parameters:
        - $ref: "#/components/parameters/address"
      responses:
        "200":
          description: data = { mailbox, updated, alreadySeen, total }.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /sources/{id}:
    get:
      tags: [Messages]
      summary: Get raw RFC 822 message source
      operationId: getMessageSource
      security:
        - tempToken: []
        - cookieAuth: []
        - bearerAuth: []
        - apiKey: []
      parameters:
        - $ref: "#/components/parameters/messageId"
        - $ref: "#/components/parameters/address"
      responses:
        "200":
          description: data = { id, data } with the raw source as a string.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Legacy creation aliases (deprecated) ──
  /inboxes:
    post:
      tags: [Accounts]
      summary: "Legacy alias of POST /accounts"
      description: Same behavior as POST /accounts. Responds with Deprecation and Link successor-version headers.
      operationId: legacyCreateInbox
      deprecated: true
      security:
        - {}
        - cookieAuth: []
        - bearerAuth: []
        - apiKey: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateAccountRequest"
      responses:
        "201":
          description: Same payload as POST /accounts.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"

  /mailboxes:
    post:
      tags: [Accounts]
      summary: "Legacy alias of POST /accounts"
      operationId: legacyCreateMailbox
      deprecated: true
      security:
        - {}
        - cookieAuth: []
        - bearerAuth: []
        - apiKey: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateAccountRequest"
      responses:
        "201":
          description: Same payload as POST /accounts.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"

  /emails:
    post:
      tags: [Accounts]
      summary: "Legacy alias of POST /accounts"
      operationId: legacyCreateEmail
      deprecated: true
      security:
        - {}
        - cookieAuth: []
        - bearerAuth: []
        - apiKey: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateAccountRequest"
      responses:
        "201":
          description: Same payload as POST /accounts.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"

  /accounts/wildcard:
    post:
      tags: [Accounts]
      summary: Create a temp inbox under a wildcard child domain
      description: >
        Forces wildcard mode. Requires authentication (JWT / API key). When
        subdomain is omitted, the server uses the key default or allocates a
        random child domain.
      operationId: createWildcardAccount
      security:
        - cookieAuth: []
        - bearerAuth: []
        - apiKey: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateAccountRequest"
      responses:
        "201":
          description: data = created inbox (mode = wildcard) + token.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/CreateAccountResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"

  # ── Mailbox shares (public, token-based) ──
  /mailbox-shares/{token}:
    get:
      tags: [Mailbox Shares]
      summary: Get share metadata
      description: Public. Inactive shares (revoked/expired/view-limit) return the same meta with success=false and HTTP 410/403.
      operationId: getMailboxShareMeta
      parameters:
        - $ref: "#/components/parameters/shareToken"
      responses:
        "200":
          description: data = share meta { id, accessMode, passwordRequired, expiresAt, maxViews, viewCount, allowMessageDetail, status, accessGranted?, accessExpiresAt? }.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "404":
          $ref: "#/components/responses/NotFound"
        "410":
          description: Share revoked / expired / inbox gone (meta still included in data).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"

  /mailbox-shares/{token}/access:
    post:
      tags: [Mailbox Shares]
      summary: Unlock a share (password if required) and get a session token
      operationId: accessMailboxShare
      parameters:
        - $ref: "#/components/parameters/shareToken"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                password:
                  type: string
      responses:
        "200":
          description: data = { accessToken, expiresAt, inboxAddress, inboxExpiresAt?, share }. Also sets a share session cookie.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "410":
          description: Share no longer active or view limit consumed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"

  /mailbox-shares/{token}/ws-ticket:
    get:
      tags: [Mailbox Shares]
      summary: Get a WebSocket ticket for a share session
      operationId: getMailboxShareWsTicket
      parameters:
        - $ref: "#/components/parameters/shareToken"
      responses:
        "200":
          description: data = { ticket } (30s TTL).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "410":
          description: Share no longer active.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /mailbox-shares/{token}/messages:
    get:
      tags: [Mailbox Shares]
      summary: List messages in a shared mailbox
      operationId: listMailboxShareMessages
      parameters:
        - $ref: "#/components/parameters/shareToken"
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
        - $ref: "#/components/parameters/offset"
      responses:
        "200":
          description: data = { messages, total, unreadCount } (same item shape as GET /messages).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "410":
          description: Share no longer active.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /mailbox-shares/{token}/messages/{id}:
    get:
      tags: [Mailbox Shares]
      summary: Get message detail in a shared mailbox
      description: Requires allowMessageDetail on the share.
      operationId: getMailboxShareMessage
      parameters:
        - $ref: "#/components/parameters/shareToken"
        - $ref: "#/components/parameters/messageId"
      responses:
        "200":
          description: data = MessageDetail (attachment URLs point to the share attachment endpoint).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/MessageDetail"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /mailbox-shares/{token}/messages/{id}/attachments/{num}/{file}:
    get:
      tags: [Mailbox Shares]
      summary: Download an attachment from a shared message
      operationId: getMailboxShareAttachment
      parameters:
        - $ref: "#/components/parameters/shareToken"
        - $ref: "#/components/parameters/messageId"
        - name: num
          in: path
          required: true
          description: Zero-based attachment index.
          schema:
            type: integer
        - name: file
          in: path
          required: true
          description: Attachment filename (display only).
          schema:
            type: string
      responses:
        "200":
          description: Raw attachment bytes with original content type.
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Auth core ──
  /auth/status:
    get:
      tags: [Auth]
      summary: Auth feature flags
      operationId: getAuthStatus
      responses:
        "200":
          description: data = { registerEnabled, passwordLoginEnabled, passwordRegisterEnabled, emailVerificationRequired, inviteCodeRequired, turnstileEnabled, turnstileSiteKey, passkeyEnabled, twofaEnabled, minPasswordLength, maxDisplayNameLength, oauthProviders }.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"

  /auth/register/check:
    post:
      tags: [Auth]
      summary: Check username/email availability
      operationId: checkRegisterAvailability
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                username:
                  type: string
                email:
                  type: string
      responses:
        "200":
          description: data = { username?, usernameAvailable, email?, emailAvailable }.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "400":
          $ref: "#/components/responses/BadRequest"

  /auth/register:
    post:
      tags: [Auth]
      summary: Register a new account
      operationId: register
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [displayName, username, email, password]
              properties:
                displayName:
                  type: string
                username:
                  type: string
                email:
                  type: string
                password:
                  type: string
                  description: 8-128 characters.
                inviteCode:
                  type: string
                turnstileToken:
                  type: string
      responses:
        "201":
          description: >
            When email verification is required, data = { emailVerificationRequired: true, emailVerified: false }.
            Otherwise data = { id, access_token, expires_in, emailVerified } and auth cookies are set.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /auth/login:
    post:
      tags: [Auth]
      summary: Login with username/email + password
      operationId: login
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, password]
              properties:
                username:
                  type: string
                  description: Username or email.
                password:
                  type: string
                turnstileToken:
                  type: string
                twofaCode:
                  type: string
                  description: Required when the account has 2FA enabled.
      responses:
        "200":
          description: data = { id, access_token, expires_in, emailVerified }. Sets yyds_access_token / yyds_refresh_token cookies.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /auth/verify-email:
    post:
      tags: [Auth]
      summary: Verify the signed-in user's email with a code
      operationId: verifyEmail
      security:
        - cookieAuth: []
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code:
                  type: string
      responses:
        "200":
          description: data = { verified, verifiedAt }.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /auth/verify-email/register:
    post:
      tags: [Auth]
      summary: Verify email during registration (no session yet)
      description: On success issues tokens and sets auth cookies (registration login).
      operationId: verifyEmailRegistration
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, code]
              properties:
                email:
                  type: string
                code:
                  type: string
      responses:
        "200":
          description: data = { id, access_token, expires_in, emailVerified }.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"

  /auth/resend-verify:
    post:
      tags: [Auth]
      summary: Resend verification email (signed-in)
      operationId: resendVerify
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200":
          description: data = { sent, expiresInSeconds }.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /auth/resend-verify/register:
    post:
      tags: [Auth]
      summary: Resend verification email during registration
      description: Always responds sent=true (does not reveal whether the email exists).
      operationId: resendVerifyRegistration
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
      responses:
        "200":
          description: data = { sent, expiresInSeconds }.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "400":
          $ref: "#/components/responses/BadRequest"

  /auth/forgot-password:
    post:
      tags: [Auth]
      summary: Request a password reset email
      description: Always responds sent=true (does not reveal whether the email exists).
      operationId: forgotPassword
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
      responses:
        "200":
          description: data = { sent, expiresInSeconds }.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "400":
          $ref: "#/components/responses/BadRequest"

  /auth/reset-password/validate:
    get:
      tags: [Auth]
      summary: Validate a password reset token
      operationId: validatePasswordReset
      parameters:
        - name: token
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: data = { valid, email, expiresAt }.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "400":
          $ref: "#/components/responses/BadRequest"

  /auth/reset-password:
    post:
      tags: [Auth]
      summary: Reset password with a token
      description: Revokes all refresh tokens for the user on success.
      operationId: resetPassword
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, password]
              properties:
                token:
                  type: string
                password:
                  type: string
      responses:
        "200":
          description: "data = { reset: true }."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"

  /auth/token/refresh:
    post:
      tags: [Auth]
      summary: Rotate refresh token + reissue access token
      description: >
        Reads the yyds_refresh_token cookie. Rotates on every call; presenting
        a rotated token again revokes the whole token family (reuse detection).
      operationId: refreshToken
      responses:
        "200":
          description: data = { access_token, expires_in }. Sets rotated cookies.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /auth/token/revoke:
    post:
      tags: [Auth]
      summary: Revoke the current refresh token (logout)
      operationId: revokeToken
      responses:
        "204":
          description: Token revoked and auth cookies cleared (no body).

  /auth/oauth/config:
    get:
      tags: [Auth]
      summary: Available OAuth providers + CSRF state
      operationId: getOAuthConfig
      responses:
        "200":
          description: data = { providers, state }. Sets the yyds_oauth_state cookie (5 min).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"

  /auth/oauth/github:
    post:
      tags: [Auth]
      summary: Exchange a GitHub authorization code for local tokens
      operationId: oauthGitHub
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code:
                  type: string
                state:
                  type: string
                redirectUri:
                  type: string
      responses:
        "200":
          description: data = { id, access_token, expires_in, isNewUser }.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"

  /auth/oauth/linuxdo:
    post:
      tags: [Auth]
      summary: Exchange a LinuxDo authorization code for local tokens
      operationId: oauthLinuxDo
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code:
                  type: string
                state:
                  type: string
                redirectUri:
                  type: string
      responses:
        "200":
          description: data = { id, access_token, expires_in, isNewUser }.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"

  /auth/passkey/login/begin:
    post:
      tags: [Auth]
      summary: Begin passkey (WebAuthn) discoverable login
      operationId: passkeyLoginBegin
      responses:
        "200":
          description: data = WebAuthn credential assertion options. Sets a passkey session cookie.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "400":
          $ref: "#/components/responses/BadRequest"

  /auth/passkey/login/finish:
    post:
      tags: [Auth]
      summary: Finish passkey login
      operationId: passkeyLoginFinish
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: WebAuthn assertion response from navigator.credentials.get().
      responses:
        "200":
          description: data = { id, access_token, expires_in }.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /auth/ws-ticket:
    get:
      tags: [Auth]
      summary: Get a short-lived WebSocket ticket
      operationId: getWsTicket
      security:
        - cookieAuth: []
        - bearerAuth: []
        - apiKey: []
        - tempToken: []
      responses:
        "200":
          description: data = { ticket } (30s TTL, pass as ?ticket= to /ws).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ── Public content ──
  /announcements:
    get:
      tags: [Content]
      summary: List active announcements
      operationId: listAnnouncements
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
        - $ref: "#/components/parameters/limit"
      responses:
        "200":
          description: >
            Without page/limit, data = array of announcements. With pagination,
            data = { announcements, page, limit, total }.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "429":
          $ref: "#/components/responses/RateLimited"

  /articles:
    get:
      tags: [Content]
      summary: List published articles
      operationId: listArticles
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
        - $ref: "#/components/parameters/limit"
      responses:
        "200":
          description: data = { articles, total, page, limit }.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "429":
          $ref: "#/components/responses/RateLimited"

  /articles/{slug}:
    get:
      tags: [Content]
      summary: Get a published article by slug
      operationId: getArticleBySlug
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: data = Article.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "404":
          $ref: "#/components/responses/NotFound"

  /article-assets/{path}:
    get:
      tags: [Content]
      summary: Serve an uploaded article image asset
      operationId: getArticleAsset
      parameters:
        - name: path
          in: path
          required: true
          description: Asset path (greedy match).
          schema:
            type: string
      responses:
        "200":
          description: Raw image bytes.
          content:
            image/*:
              schema:
                type: string
                format: binary
        "404":
          $ref: "#/components/responses/NotFound"

  /domain-reward/config:
    get:
      tags: [Public]
      summary: Domain reward rule configuration
      operationId: getDomainRewardConfig
      responses:
        "200":
          description: data = { usagePerCredit, creditsPerCycle, runHour, creditExpireDays }.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"

  # ── Payment (public endpoints) ──
  /payment/methods:
    get:
      tags: [Payment]
      summary: List enabled payment methods
      operationId: getPaymentMethods
      responses:
        "200":
          description: data = { methods }.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"

  /payment/notify:
    get:
      tags: [Payment]
      summary: Payment gateway async callback
      description: Called by the payment gateway, not by API clients. Signature-verified.
      operationId: paymentNotify
      responses:
        "200":
          description: Plain-text gateway acknowledgement.
          content:
            text/plain:
              schema:
                type: string

  /payment/return:
    get:
      tags: [Payment]
      summary: Post-payment browser redirect
      operationId: paymentReturn
      responses:
        "302":
          description: Redirects to the configured frontend return URL.

  /payment/balance-notify:
    get:
      tags: [Payment]
      summary: Balance top-up gateway async callback
      description: Called by the payment gateway, not by API clients. Signature-verified.
      operationId: balanceNotify
      responses:
        "200":
          description: Plain-text gateway acknowledgement.
          content:
            text/plain:
              schema:
                type: string

  # ── Me: profile & quota ──
  /me:
    get:
      tags: [Me]
      summary: Get my profile + effective quota
      description: data = { user, isAdmin, quota }.
      operationId: getMe
      security: &userAuth
        - cookieAuth: []
        - bearerAuth: []
        - apiKey: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    patch:
      tags: [Me]
      summary: Update my profile
      description: Currently supports displayName (changeable once). data = { user, isAdmin, quota }.
      operationId: updateMe
      security: *userAuth
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                displayName:
                  type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /me/quota:
    get:
      tags: [Me]
      summary: My quota usage (dashboard dimensions)
      operationId: getMyQuota
      security: &jwtOnly
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/growth-tasks:
    get:
      tags: [Me]
      summary: Growth task progress and rewards
      description: data = { tasks (fixed set with completed/rewarded state), allCompleted }.
      operationId: getMyGrowthTasks
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/pig/feed:
    post:
      tags: [Me]
      summary: Report a pig-feeding interaction (server-adjudicated reward)
      description: >
        data = { state (rewarded|cooldown|daily_cap|disabled|too_fast),
        rewarded, rewardCalls, todayRewarded, dailyCap, cooldownRemaining }.
        Rewards are granted only when pig.feed_reward_enabled is on; grants
        are capped per business day and rate-limited server-side.
      operationId: pigFeed
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/pig/feed-status:
    get:
      tags: [Me]
      summary: Pig feeding reward config and today's progress
      description: data = { rewardEnabled, rewardCalls, dailyCap, todayRewarded }.
      operationId: getPigFeedStatus
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/banana/wallet:
    get:
      tags: [Me]
      summary: Banana wallet snapshot (balance, today's feeds, expiring soon)
      description: >
        data = { enabled, wallet: { bananas, walletCap, todayFed, feedDailyCap,
        rewardEnabled, expiringSoon, nextAccrualAt } }. Reading the wallet also
        lazily accrues daily-login and time-based banana grants.
      operationId: getBananaWallet
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/banana/history:
    get:
      tags: [Me]
      summary: Get banana feeding history (last N entries)
      description: >
        Query param ?limit=10 (default 10, max 50). Aggregates per-second feed
        batches. data = array of { fedAt, count, eaten, rotten, refused, rewardCalls }.
      operationId: getBananaHistory
      security: *jwtOnly
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 50
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/banana/claim:
    post:
      tags: [Me]
      summary: Claim bananas from the pending pool (manual pickup)
      description: >
        Body { source: "daily_login" | "accrual" | "all" }. Moves pending
        bananas into the wallet inventory (banana_grants). Respects wallet cap;
        returns actual claimed count. data = { claimed, wallet }.
      operationId: claimBananas
      security: *jwtOnly
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                source:
                  type: string
                  enum: [daily_login, accrual, all]
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/banana/feed:
    post:
      tags: [Me]
      summary: Feed bananas to the pig (server-adjudicated, per-banana outcomes)
      description: >
        Body { count } (1-10, default 1). data = { state
        (ok|no_bananas|daily_cap|disabled|too_fast), results[] with outcome
        (eaten|rotten|refused) and rewardCalls, totalReward, wallet }.
        Refused bananas are returned and do not count toward the daily cap.
      operationId: feedBananas
      security: *jwtOnly
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                count:
                  type: integer
                  minimum: 1
                  maximum: 10
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/credits:
    get:
      tags: [Me]
      summary: My active credit balances grouped by source
      operationId: getMyCredits
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/redeem:
    post:
      tags: [Me]
      summary: Redeem a code (credits or plan)
      operationId: redeemCode
      security: *jwtOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code:
                  type: string
                confirmDowngrade:
                  type: boolean
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "410":
          description: Code expired / deactivated / fully used.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /me/redeem/history:
    get:
      tags: [Me]
      summary: My redemption history
      operationId: getRedeemHistory
      security: *userAuth
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── Me: inboxes ──
  /me/inboxes:
    get:
      tags: [Me]
      summary: List my inboxes
      description: "data = { inboxes, total, limit, offset, type, state, q, counts }. Query: type=persistent|temp, state, q (address search, max 128 chars), limit (default 100, max 500), offset."
      operationId: listMyInboxes
      security: *userAuth
      parameters:
        - name: type
          in: query
          schema:
            type: string
            enum: [persistent, temp]
        - name: state
          in: query
          schema:
            type: string
        - name: q
          in: query
          schema:
            type: string
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Me]
      summary: Create a persistent inbox
      operationId: createMyInbox
      security: *userAuth
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                address:
                  type: string
                domain:
                  type: string
                displayName:
                  type: string
      responses:
        "201": { $ref: "#/components/responses/Created" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }

  /me/inboxes/inactive:
    delete:
      tags: [Me]
      summary: Bulk-delete inactive inboxes
      operationId: cleanupInactiveInboxes
      security: *userAuth
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/inboxes/batch-delete:
    post:
      tags: [Me]
      summary: Batch-delete or batch-deactivate inboxes by IDs
      description: "mode=delete (default) hard-deletes with async mail purge; mode=deactivate soft-disables (temp API inboxes only). Max 500 ids. data = { deleted, errors }."
      operationId: batchDeleteInboxes
      security: *jwtOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ids]
              properties:
                ids:
                  type: array
                  items:
                    type: string
                mode:
                  type: string
                  enum: [delete, deactivate]
                  default: delete
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/inboxes/cleanup:
    post:
      tags: [Me]
      summary: Age-based temp inbox cleanup (dry-run preview then execute)
      description: "Deletes the caller's TEMP inboxes created more than olderThanDays days ago (persistent inboxes are never eligible). dryRun=true returns { affected }; execute returns { deleted, remaining } — loop while remaining > 0. onlyEmpty restricts to inboxes with no messages."
      operationId: cleanupMyInboxes
      security: *jwtOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [olderThanDays]
              properties:
                olderThanDays:
                  type: integer
                  minimum: 1
                  maximum: 3650
                onlyEmpty:
                  type: boolean
                  default: false
                dryRun:
                  type: boolean
                  default: false
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/inboxes/{id}:
    delete:
      tags: [Me]
      summary: Delete an inbox (persistent = hard delete + mail purge)
      operationId: deleteMyInbox
      security: *userAuth
      parameters: &idParam
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/inboxes/{id}/restore:
    post:
      tags: [Me]
      summary: Restore a deactivated API temp inbox
      operationId: restoreMyInbox
      security: *userAuth
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  /me/inboxes/{id}/claim:
    post:
      tags: [Me]
      summary: Claim an anonymous temp inbox into my account
      operationId: claimInbox
      security: *userAuth
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  /me/unread-summary:
    get:
      tags: [Me]
      summary: Active inbox count + aggregated unread total
      description: data = { activeInboxes, totalUnread, sampled }.
      operationId: getMyUnreadSummary
      security: *userAuth
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/messages/unified:
    get:
      tags: [Me]
      summary: Unified message feed across my inboxes (+ external accounts)
      description: "data = { messages, total, unreadCount }. total and unreadCount for external accounts are both derived from the synced external_messages index (unreadCount <= total always holds). Query: limit (max 200), offset, filter, domain, q (subject/from search), source=local|external|all, accountId."
      operationId: getUnifiedMessages
      security: *userAuth
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
        - $ref: "#/components/parameters/offset"
        - name: filter
          in: query
          schema:
            type: string
        - name: domain
          in: query
          schema:
            type: string
        - name: q
          in: query
          schema:
            type: string
        - name: source
          in: query
          schema:
            type: string
            enum: [local, external, all]
        - name: accountId
          in: query
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/messages/cleanup:
    post:
      tags: [Me]
      summary: Age-based local message cleanup (dry-run preview then execute)
      description: "Deletes LOCAL messages older than olderThanDays days across the caller's own inboxes. Optional domain / mailbox narrow the scope. External (IMAP-aggregated) messages are read-only and never touched. dryRun=true returns { affected }; execute returns { deleted, remaining } — loop while remaining > 0."
      operationId: cleanupMyMessages
      security: *jwtOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [olderThanDays]
              properties:
                olderThanDays:
                  type: integer
                  minimum: 1
                  maximum: 3650
                domain:
                  type: string
                mailbox:
                  type: string
                dryRun:
                  type: boolean
                  default: false
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── Me: mailbox shares (owner side) ──
  /me/inboxes/{id}/shares:
    get:
      tags: [Me]
      summary: List shares for my inbox
      operationId: listMyMailboxShares
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      tags: [Me]
      summary: Create a share link for my inbox
      operationId: createMailboxShare
      security: *jwtOnly
      parameters: *idParam
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                password:
                  type: string
                expiresAt:
                  type: string
                  format: date-time
                maxViews:
                  type: integer
                allowMessageDetail:
                  type: boolean
      responses:
        "201": { $ref: "#/components/responses/Created" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/inboxes/{id}/shares/{shareId}:
    delete:
      tags: [Me]
      summary: Revoke a mailbox share
      operationId: revokeMailboxShare
      security: *jwtOnly
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: shareId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Me: wildcard rules ──
  /me/wildcard-rules:
    get:
      tags: [Me]
      summary: List my wildcard rules
      operationId: listMyWildcardRules
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/wildcard-rules/{id}/auto-allocate:
    post:
      tags: [Me]
      summary: Auto-allocate the next sequential child subdomain
      operationId: autoAllocateWildcard
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/wildcard-rules/{id}/assignments:
    get:
      tags: [Me]
      summary: List child-subdomain assignments of a wildcard rule
      operationId: listWildcardAssignments
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/wildcard-rules/{id}/assignments/{assignmentId}:
    delete:
      tags: [Me]
      summary: Delete a wildcard assignment
      operationId: deleteWildcardAssignment
      security: *jwtOnly
      parameters: &assignmentParams
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: assignmentId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  /me/wildcard-rules/{id}/assignments/{assignmentId}/inboxes:
    get:
      tags: [Me]
      summary: List inboxes under a wildcard assignment
      operationId: listWildcardAssignmentInboxes
      security: *jwtOnly
      parameters: *assignmentParams
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/wildcard-rules/{id}/assignments/{assignmentId}/release:
    post:
      tags: [Me]
      summary: Release a wildcard assignment (unbind inbox)
      operationId: releaseWildcardAssignment
      security: *jwtOnly
      parameters: *assignmentParams
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Me: domains ──
  /me/domains:
    get:
      tags: [Me]
      summary: List my domains
      operationId: listMyDomains
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Me]
      summary: Add a custom domain
      description: A leading `*.` enables a wildcard rule for the domain.
      operationId: createMyDomain
      security: *jwtOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain:
                  type: string
                enableWildcardRule:
                  type: boolean
      responses:
        "201": { $ref: "#/components/responses/Created" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { $ref: "#/components/responses/Conflict" }

  /me/domains/batch:
    post:
      tags: [Me]
      summary: Batch-add custom domains
      operationId: batchCreateMyDomains
      security: *jwtOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domains]
              properties:
                domains:
                  type: array
                  items:
                    type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/domains/batch-delete:
    post:
      tags: [Me]
      summary: Batch-delete my domains by IDs
      description: "Deletes up to 50 owned domains; each goes through the full cascade (inbox cleanup in-transaction, mail files purged asynchronously). data = { results: [{id, domain, success, error?}], deleted, failed }."
      operationId: batchDeleteMyDomains
      security: *jwtOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ids]
              properties:
                ids:
                  type: array
                  items:
                    type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/domains/dns-config-preview:
    post:
      tags: [Me]
      summary: Build copy-ready DNS configuration for selected domains
      description: Returns authoritative MX/TXT instructions without exposing DNS credentials. Selection authorization is validated before any item is returned.
      operationId: previewDomainDnsConfiguration
      security: *jwtOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DomainSelectionRequest"
      responses:
        "200":
          description: DNS configuration preview for every selected domain.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    required: [data]
                    properties:
                      data:
                        $ref: "#/components/schemas/DomainDNSConfigPreview"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/domains/batch-verify:
    post:
      tags: [Me]
      summary: Verify DNS configuration for selected domains
      description: Verifies up to 20 owned domains with bounded concurrency and returns grouped, retryable outcomes. Concurrent batches for the same user are rejected.
      operationId: batchVerifyDomainDns
      security: *jwtOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DomainSelectionRequest"
      responses:
        "200":
          description: Per-domain DNS verification outcomes.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    required: [data]
                    properties:
                      data:
                        $ref: "#/components/schemas/DomainDNSBatchVerify"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  /me/domains/{id}:
    patch:
      tags: [Me]
      summary: Update my domain (e.g. visibility)
      operationId: updateMyDomain
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Me]
      summary: Delete my domain
      operationId: deleteMyDomain
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  /me/domains/{id}/dns-guide:
    get:
      tags: [Me]
      summary: DNS setup guide (records to add)
      operationId: getDomainDnsGuide
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/domains/{id}/dns-status:
    get:
      tags: [Me]
      summary: Live DNS verification status
      operationId: getDomainDnsStatus
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/domains/{id}/verify:
    post:
      tags: [Me]
      summary: Trigger domain DNS verification
      operationId: verifyMyDomain
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/domains/{id}/dns-sync:
    post:
      tags: [Me]
      summary: Sync DNS records via a connected DNS provider
      operationId: syncDomainDns
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/domains/{id}/reserved-prefixes:
    get:
      tags: [Me]
      summary: List owner protected reserved prefixes for a shared domain
      operationId: listOwnerReservedPrefixes
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    put:
      tags: [Me]
      summary: Replace the owner protected reserved prefix set for a domain
      operationId: updateOwnerReservedPrefixes
      security: *jwtOnly
      parameters: *idParam
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [prefixes]
              properties:
                prefixes:
                  type: array
                  items:
                    type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/domains/{id}/catchall:
    get:
      tags: [Me]
      summary: Get catch-all safe mode settings and eligibility for a domain
      operationId: getMyCatchall
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    put:
      tags: [Me]
      summary: Enable or disable catch-all safe mode (owner proof required)
      operationId: updateMyCatchall
      security: *jwtOnly
      parameters: *idParam
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [enabled]
              properties:
                enabled:
                  type: boolean
                confirm:
                  type: boolean
                  description: Explicit owner acknowledgment, required when enabling
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  /me/catchall/claims:
    get:
      tags: [Me]
      summary: List catch-all pending claims across owned domains
      operationId: listMyCatchallClaims
      security: *jwtOnly
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [open, all, claimed_private, expired_unclaimed, blocked_prefix]
        - name: domainId
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/catchall/claims/{id}/claim:
    post:
      tags: [Me]
      summary: Claim a catch-all pending address as a private inbox
      operationId: claimCatchallAddress
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /me/catchall/claims/{id}/dismiss:
    post:
      tags: [Me]
      summary: Dismiss (expire or block) a catch-all pending claim
      operationId: dismissCatchallClaim
      security: *jwtOnly
      parameters: *idParam
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                block:
                  type: boolean
                  description: true marks the local part blocked_prefix permanently
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  /me/domains/{id}/wildcard-rules:
    get:
      tags: [Me]
      summary: List wildcard rules for a domain
      operationId: listDomainWildcardRules
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Me]
      summary: Create a wildcard rule for a domain
      operationId: createDomainWildcardRule
      security: *jwtOnly
      parameters: *idParam
      responses:
        "201": { $ref: "#/components/responses/Created" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { $ref: "#/components/responses/Conflict" }

  /me/domains/{id}/wildcard-rules/{ruleId}:
    patch:
      tags: [Me]
      summary: Update a wildcard rule (pause/resume etc.)
      operationId: updateDomainWildcardRule
      security: *jwtOnly
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: ruleId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/domain-stats:
    get:
      tags: [Me]
      summary: Usage stats for my shared domains
      operationId: getMyDomainStats
      security: *userAuth
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── Me: DNS service connections ──
  /me/dns-service-connections:
    get:
      tags: [Me]
      summary: List DNS provider connections
      operationId: listDnsConnections
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Me]
      summary: Create a DNS provider connection (e.g. Cloudflare token)
      operationId: createDnsConnection
      security: *jwtOnly
      responses:
        "201": { $ref: "#/components/responses/Created" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/dns-service-connections/test-draft:
    post:
      tags: [Me]
      summary: Test a DNS connection draft before saving
      operationId: testDnsConnectionDraft
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/dns-service-connections/{id}:
    patch:
      tags: [Me]
      summary: Update a DNS connection
      operationId: updateDnsConnection
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Me]
      summary: Delete a DNS connection
      operationId: deleteDnsConnection
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/dns-service-connections/{id}/test:
    post:
      tags: [Me]
      summary: Test a saved DNS connection
      operationId: testDnsConnection
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/dns-service-connections/{id}/zones:
    get:
      tags: [Me]
      summary: List zones visible to a DNS connection
      operationId: listDnsConnectionZones
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Me: API keys ──
  /me/api-keys:
    get:
      tags: [Me]
      summary: List my API keys
      description: data = { apiKeys, total }. API-key callers only see their own key metadata.
      operationId: listMyApiKeys
      security: *userAuth
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Me]
      summary: Create an API key
      description: The plaintext key is returned in this response and can later be re-revealed via /me/api-keys/{id}/reveal.
      operationId: createApiKey
      security: *jwtOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  maxLength: 100
                permissions:
                  type: array
                  items:
                    type: string
                    enum: [read, write, all]
                domainScope:
                  type: string
                  enum: [all, public, own, specific]
                allowedDomainIds:
                  type: array
                  items:
                    type: string
                defaultDomainId:
                  type: string
                wildcardRuleScope:
                  type: string
                  enum: [none, own, specific]
                allowedWildcardRuleIds:
                  type: array
                  items:
                    type: string
                defaultWildcardRuleId:
                  type: string
                defaultWildcardSubdomainLabel:
                  type: string
                expiresAt:
                  type: string
                  format: date-time
      responses:
        "201": { $ref: "#/components/responses/Created" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /me/api-keys/{id}:
    patch:
      tags: [Me]
      summary: Update an API key (name / scope / expiry / enabled)
      description: >
        Fields are optional and independent: name, domainScope (+
        allowedDomainIds), expiresAt (RFC3339 to set, null to clear,
        absent to keep), enabled (reversible disable/enable).
      operationId: updateApiKey
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Me]
      summary: Delete an API key
      operationId: deleteApiKey
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/api-keys/{id}/rotate:
    post:
      tags: [Me]
      summary: Rotate an API key secret
      description: >
        Generates a new secret (returned once in this response). The old
        secret keeps authenticating until graceUntil (default 24h,
        system-configurable), then stops permanently. Key id, scope and
        usage history are preserved.
      operationId: rotateApiKey
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/api-keys/{id}/reveal:
    get:
      tags: [Me]
      summary: Reveal the full API key value
      operationId: revealApiKey
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/api-keys/{id}/usage:
    get:
      tags: [Me]
      summary: API key usage stats
      operationId: getApiKeyUsage
      security: *userAuth
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/api-keys/{id}/domain-policy:
    get:
      tags: [Me]
      summary: Get an API key's platform-aware domain policy
      operationId: getApiKeyDomainPolicy
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200":
          description: data = the API key's effective routing policy.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/APIKeyDomainPolicy"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    put:
      tags: [Me]
      summary: Replace an API key's platform-aware domain policy
      description: >
        Replaces the target platform, avoidance mode, fallback mode, and up
        to 100 global or platform-specific domain exclusions atomically.
      operationId: updateApiKeyDomainPolicy
      security: *jwtOnly
      parameters: *idParam
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/APIKeyDomainPolicyUpdate"
      responses:
        "200":
          description: data = the fully replaced API-key routing policy.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/APIKeyDomainPolicy"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/domains/{id}/platform-status:
    get:
      tags: [Me]
      summary: List platform-specific availability for a domain
      description: >
        Returns distinct-user report counts and effective status for every
        supported platform. This status is separate from DNS/MX health.
      operationId: getDomainPlatformStatus
      security: *userAuth
      parameters: *idParam
      responses:
        "200":
          description: data = domain identity, the current proof window, and one public status per active platform.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        required: [domainId, domain, statuses, reportMaxAgeHours]
                        properties:
                          domainId: { type: string }
                          domain: { type: string }
                          statuses:
                            type: array
                            items:
                              $ref: "#/components/schemas/DomainPlatformStatusPublic"
                          reportMaxAgeHours:
                            type: integer
                            description: Maximum age of an inbox accepted as report proof; runtime-configurable.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/domain-platform-summary:
    get:
      tags: [Me]
      summary: Batch platform-status summaries for domain cards
      description: Returns at most three relevant platform signals per accessible domain. Intended for asset-list summaries and avoids one request per domain.
      operationId: getDomainPlatformSummary
      security: *userAuth
      parameters:
        - name: domainIds
          in: query
          required: true
          description: Comma-separated domain IDs, with 1 to 50 unique values.
          schema: { type: string }
        - name: platformCode
          in: query
          description: Optional reviewed platform. When present, returns that platform's marked status for each requested domain.
          schema: { type: string }
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/domain-platform-reports:
    post:
      tags: [Me]
      summary: Submit a verified domain/platform result
      description: >
        The inbox must belong to the current user, match the reported domain
        (exactly or as a child domain), and have been created within the
        configured proof window (72 hours by default; read the current value
        from reportMaxAgeHours). One user contributes
        at most one current report per domain and platform. When
        excludeForApiKey is true, the report, aggregate update, and API-key
        exclusion are committed atomically. Only domain_not_allowed,
        disposable_email_blocked, and unsupported_email_domain are strong
        blocked reasons; unknown, CAPTCHA, IP/rate-limit, and missing-mail
        reasons remain weak signals.
      operationId: createDomainPlatformReport
      security: *userAuth
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domainId, platformCode, inboxId, outcome]
              properties:
                domainId: { type: string }
                platformCode: { type: string }
                inboxId: { type: string }
                outcome:
                  type: string
                  enum: [available, blocked, verification_received, verification_missing, other_failure]
                reasonCode: { type: string, maxLength: 100 }
                excludeForApiKey: { type: boolean }
                apiKeyId: { type: string }
      responses:
        "201":
          description: data = the caller's upserted report and current public aggregate status.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        required: [report]
                        properties:
                          report:
                            $ref: "#/components/schemas/DomainPlatformReport"
                          status:
                            oneOf:
                              - $ref: "#/components/schemas/DomainPlatformStatusPublic"
                              - type: "null"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /me/platform-suggestions:
    get:
      tags: [Me]
      summary: List the current user's platform suggestions
      operationId: listMyPlatformSuggestions
      security: *jwtOnly
      responses:
        "200":
          description: data.suggestions contains grouped candidates supported by the current user.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          suggestions:
                            type: array
                            items: { $ref: "#/components/schemas/PlatformSuggestion" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Me]
      summary: Submit or support a missing registration platform
      description: Duplicate official domains are grouped and supportCount counts distinct users. Providing inboxId attaches reviewed metadata from an owned inbox; message bodies are never persisted in suggestion evidence.
      operationId: createPlatformSuggestion
      security: *jwtOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, websiteDomain]
              properties:
                name: { type: string, minLength: 2, maxLength: 80 }
                websiteDomain: { type: string }
                inboxId: { type: string }
                messageId: { type: string }
      responses:
        "201": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /me/platform-suggestions/detect:
    post:
      tags: [Me]
      summary: Detect a registration platform from an owned inbox
      description: Inspects only the latest message metadata (sender, subject, timestamp). A known reviewed sender domain returns a catalog match; an unknown domain returns a candidate that still requires user confirmation and admin review.
      operationId: detectPlatformFromInbox
      security: *jwtOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [inboxId]
              properties:
                inboxId: { type: string }
      responses:
        "200":
          description: data = detection result.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/PlatformDetectionResult" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/BadRequest" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /admin/domain-platform-status:
    get:
      tags: [Admin]
      summary: List aggregated domain/platform statuses
      operationId: adminListDomainPlatformStatuses
      security: *jwtOnly
      parameters:
        - name: platformCode
          in: query
          schema: { type: string }
        - name: status
          in: query
          schema:
            type: string
            enum: [unknown, recently_available, watching, suspected_restricted, confirmed_restricted, recovering, platform_incident]
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 500, default: 200 }
      responses:
        "200": { $ref: "#/components/responses/OK" }

  /admin/domain-platform-events:
    get:
      tags: [Admin]
      summary: List domain/platform status history
      operationId: adminListDomainPlatformEvents
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }

  /admin/domains/{id}/platform-status/{platformCode}:
    put:
      tags: [Admin]
      summary: Set or clear an admin platform-status override
      operationId: adminSetDomainPlatformOverride
      security: *jwtOnly
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: platformCode
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                status:
                  type: [string, "null"]
                  enum: [unknown, recently_available, watching, suspected_restricted, confirmed_restricted, recovering, platform_incident, null]
                note: { type: string, maxLength: 1000 }
                expiresAt: { type: [string, "null"], format: date-time }
      responses:
        "200": { $ref: "#/components/responses/OK" }

  /admin/platform-incidents:
    get:
      tags: [Admin]
      summary: List active platform-wide incidents
      operationId: adminListPlatformIncidents
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }

  /admin/platform-incidents/{platformCode}:
    put:
      tags: [Admin]
      summary: Observe, confirm, or resolve a platform-wide incident
      operationId: adminUpdatePlatformIncident
      security: *jwtOnly
      parameters:
        - name: platformCode
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              properties:
                status:
                  type: string
                  enum: [observing, confirmed, resolved]
                reasonCode: { type: string, maxLength: 100 }
                note: { type: string, maxLength: 1000 }
      responses:
        "200": { $ref: "#/components/responses/OK" }

  /admin/platforms:
    get:
      tags: [Admin]
      summary: List the complete platform catalog, including inactive entries
      operationId: adminListPlatforms
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }

  /admin/platforms/{code}:
    put:
      tags: [Admin]
      summary: Create or update a platform catalog entry
      operationId: adminUpsertPlatform
      security: *jwtOnly
      parameters:
        - name: code
          in: path
          required: true
          schema: { type: string, pattern: "^[a-z0-9][a-z0-9_-]{0,63}$" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, minLength: 2, maxLength: 80 }
                category: { type: string }
                aliases:
                  type: array
                  items: { type: string }
                websiteDomain: { type: string }
                iconSlug: { type: string }
                brandColor: { type: string }
                isActive: { type: boolean }
                sortOrder: { type: integer }
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }

  /admin/platform-suggestions:
    get:
      tags: [Admin]
      summary: List grouped platform candidates for review
      operationId: adminListPlatformSuggestions
      security: *jwtOnly
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, approved, rejected, merged]
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 200 }
      responses:
        "200": { $ref: "#/components/responses/OK" }

  /admin/platform-suggestions/{id}:
    put:
      tags: [Admin]
      summary: Approve, merge, or reject a platform candidate
      operationId: adminReviewPlatformSuggestion
      security: *jwtOnly
      parameters: *idParam
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [decision]
              properties:
                decision:
                  type: string
                  enum: [approve, merge, reject]
                platformCode: { type: string }
                name: { type: string }
                websiteDomain: { type: string }
                iconSlug: { type: string }
                brandColor: { type: string }
                mergePlatformCode: { type: string }
                reviewNote: { type: string, maxLength: 1000 }
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  # ── Me: webhooks ──
  /me/webhooks:
    get:
      tags: [Me]
      summary: List my webhooks
      description: data = { webhooks, total }. Secrets are stripped (hasSecret + hint only).
      operationId: listMyWebhooks
      security: *userAuth
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Me]
      summary: Create a webhook
      description: Server generates a signing secret unless one is provided; the plaintext secret is shown once in this response.
      operationId: createWebhook
      security: *jwtOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  maxLength: 2048
                events:
                  type: array
                  items:
                    type: string
                    enum: [message.received, message.deleted]
                secret:
                  type: string
                  maxLength: 256
      responses:
        "201": { $ref: "#/components/responses/Created" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /me/webhooks/{id}:
    patch:
      tags: [Me]
      summary: Update a webhook
      operationId: updateWebhook
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Me]
      summary: Delete a webhook
      operationId: deleteWebhook
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/webhooks/{id}/test:
    post:
      tags: [Me]
      summary: Send a signed test event
      operationId: testWebhook
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/webhooks/{id}/rotate-secret:
    post:
      tags: [Me]
      summary: Rotate the webhook signing secret
      description: Old secret is invalidated; the new one is shown once.
      operationId: rotateWebhookSecret
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/webhooks/{id}/regenerate-secret:
    post:
      tags: [Me]
      summary: "Legacy alias of rotate-secret"
      operationId: regenerateWebhookSecret
      deprecated: true
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/webhooks/{id}/deliveries:
    get:
      tags: [Me]
      summary: List recent webhook deliveries
      operationId: listWebhookDeliveries
      security: *userAuth
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Me: external accounts (IMAP aggregation) ──
  /me/external-accounts:
    get:
      tags: [Me]
      summary: List external mail accounts
      description: >
        data = { accounts, total, maxPerUser, planSyncLimit, minSyncLimit }.
        Each account carries syncMessageLimit (user override, absent = follow
        plan), effectiveSyncLimit (enforced cap = min(plan, override)) and
        syncedMessageCount (live envelope count — the same COUNT the rolling
        eviction enforces).
      operationId: listExternalAccounts
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Me]
      summary: Add an external mail account (IMAP verified before save)
      operationId: createExternalAccount
      security: *jwtOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [provider, email, credential]
              properties:
                provider:
                  type: string
                email:
                  type: string
                credential:
                  type: string
                imapHost:
                  type: string
                imapPort:
                  type: integer
                useTls:
                  type: boolean
      responses:
        "201": { $ref: "#/components/responses/Created" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { $ref: "#/components/responses/Conflict" }

  /me/external-accounts/providers:
    get:
      tags: [Me]
      summary: List supported external providers
      operationId: listExternalProviders
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/external-accounts/test-draft:
    post:
      tags: [Me]
      summary: Test external account credentials before saving
      operationId: testExternalAccountDraft
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /me/external-accounts/oauth/{provider}/authorize:
    get:
      tags: [Me]
      summary: Start the OAuth2 (XOAUTH2) binding flow for Gmail/Outlook
      description: >
        Mints a one-shot CSRF state (10 min TTL, bound to the current user and
        provider) and redirects (302) to the provider's consent page.
        provider = gmail | outlook.
      operationId: externalOAuthAuthorize
      security: *jwtOnly
      parameters:
        - name: provider
          in: path
          required: true
          schema:
            type: string
            enum: [gmail, outlook]
      responses:
        "302":
          description: Redirect to the provider authorization page.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503":
          description: OAuth client credentials or redirect base URL not configured.

  /external-accounts/oauth/{provider}/callback:
    get:
      tags: [Me]
      summary: OAuth2 provider callback (browser redirect target)
      description: >
        Public endpoint the provider redirects back to. Validates and burns
        the state token, exchanges the code, resolves the mailbox address via
        userinfo, verifies XOAUTH2 IMAP login, then persists the account and
        redirects (302) to the frontend /external-accounts page with
        ?oauth=success or ?oauth=error&reason=<code>. Never returns JSON.
      operationId: externalOAuthCallback
      security: []
      parameters:
        - name: provider
          in: path
          required: true
          schema:
            type: string
            enum: [gmail, outlook]
        - name: state
          in: query
          required: true
          schema:
            type: string
        - name: code
          in: query
          required: false
          schema:
            type: string
        - name: error
          in: query
          required: false
          schema:
            type: string
      responses:
        "302":
          description: Redirect to the frontend with the binding outcome.

  /me/external-accounts/{id}:
    patch:
      tags: [Me]
      summary: Update an external account
      description: >
        Body fields (all optional): displayName, enabled, credential
        (rotates the stored password after server-side re-verification),
        syncMessageLimit (per-account envelope retention cap — clamped to
        [10, plan cap]; <= 0 clears the override so the account follows the
        plan entitlement; changing it queues an immediate resync that
        trims or backfills to the new cap).
      operationId: updateExternalAccount
      security: *jwtOnly
      parameters: *idParam
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                displayName:
                  type: string
                enabled:
                  type: boolean
                credential:
                  type: string
                syncMessageLimit:
                  type: integer
                  description: >
                    Envelope retention cap for this account. Clamped to
                    [10, plan cap]; <= 0 clears the override (follow plan).
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Me]
      summary: Remove an external account
      operationId: deleteExternalAccount
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/external-accounts/{id}/sync:
    post:
      tags: [Me]
      summary: Request an IMAP sync
      operationId: syncExternalAccount
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /me/external-accounts/{id}/test:
    post:
      tags: [Me]
      summary: Test a saved external account connection
      operationId: testExternalAccount
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /me/external-messages/{id}:
    get:
      tags: [Me]
      summary: Get an aggregated external message
      operationId: getExternalMessage
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/external-messages/{id}/flags:
    post:
      tags: [Me]
      summary: Update external message flags (seen / starred)
      operationId: updateExternalMessageFlags
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Me: account security ──
  /me/email/send-code:
    post:
      tags: [Me]
      summary: Send an email-bind verification code
      operationId: sendEmailBindCode
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/email/bind:
    post:
      tags: [Me]
      summary: Bind an email address with a code
      operationId: bindEmail
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/password/change:
    post:
      tags: [Me]
      summary: Change my password
      operationId: changePassword
      security: *jwtOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [currentPassword, newPassword]
              properties:
                currentPassword:
                  type: string
                newPassword:
                  type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/bind/github:
    post:
      tags: [Me]
      summary: Bind a GitHub account
      operationId: bindGitHub
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { $ref: "#/components/responses/Conflict" }

  /me/bind/linuxdo:
    post:
      tags: [Me]
      summary: Bind a LinuxDo account
      operationId: bindLinuxDo
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { $ref: "#/components/responses/Conflict" }

  /me/oauth-bindings:
    get:
      tags: [Me]
      summary: List my OAuth bindings
      operationId: getOAuthBindings
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/passkey/register/begin:
    post:
      tags: [Me]
      summary: Begin passkey registration
      operationId: passkeyRegisterBegin
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/passkey/register/finish:
    post:
      tags: [Me]
      summary: Finish passkey registration
      operationId: passkeyRegisterFinish
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/passkeys:
    get:
      tags: [Me]
      summary: List my passkeys
      operationId: listPasskeys
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    delete:
      tags: [Me]
      summary: Delete a passkey
      operationId: deletePasskey
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/twofa:
    get:
      tags: [Me]
      summary: 2FA status
      description: data = { enabled, confirmedAt, recoveryCodesRemaining }.
      operationId: getTwoFAStatus
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/twofa/setup:
    post:
      tags: [Me]
      summary: Begin 2FA setup (TOTP secret + QR)
      operationId: setupTwoFA
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/twofa/enable:
    post:
      tags: [Me]
      summary: Confirm and enable 2FA
      operationId: enableTwoFA
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/twofa/disable:
    post:
      tags: [Me]
      summary: Disable 2FA
      operationId: disableTwoFA
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/twofa/recovery-codes:
    post:
      tags: [Me]
      summary: Regenerate 2FA recovery codes
      operationId: regenerateRecoveryCodes
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/security/sessions:
    get:
      tags: [Me]
      summary: List active sessions
      operationId: listSessions
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/security/sessions/{id}:
    delete:
      tags: [Me]
      summary: Revoke a session
      operationId: revokeSession
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/security/sessions/revoke-all:
    post:
      tags: [Me]
      summary: Revoke all sessions except the current one
      operationId: revokeAllSessions
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/security/login-activities:
    get:
      tags: [Me]
      summary: Recent login activity
      operationId: listLoginActivities
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/deactivate:
    post:
      tags: [Me]
      summary: Deactivate my account (cooldown applies)
      operationId: deactivateAccount
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/preferences:
    get:
      tags: [Me]
      summary: Get my notification / credit preferences
      operationId: getMyPreferences
      security: *userAuth
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    patch:
      tags: [Me]
      summary: Update my preferences
      operationId: updateMyPreferences
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── Me: Telegram ──
  /me/telegram/bind-token:
    post:
      tags: [Me]
      summary: Create a Telegram bind token + deep link
      operationId: createTelegramBindToken
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "503":
          description: Telegram bot not configured.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /me/telegram/status:
    get:
      tags: [Me]
      summary: Telegram binding status
      description: data = { botConfigured, botUsername, bound, username, enabled, mailPush, announcementPush }.
      operationId: getTelegramStatus
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/telegram/test:
    post:
      tags: [Me]
      summary: Send a Telegram test notification
      operationId: sendTelegramTest
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/telegram/binding:
    patch:
      tags: [Me]
      summary: Toggle Telegram notifications
      description: "At least one of enabled / mailPush / announcementPush is required. enabled is the channel master switch (requires an active binding); mailPush and announcementPush are per-user sub-switches."
      operationId: updateTelegramEnabled
      security: *jwtOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                enabled:
                  type: boolean
                mailPush:
                  type: boolean
                announcementPush:
                  type: boolean
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    delete:
      tags: [Me]
      summary: Unbind Telegram
      operationId: deleteTelegramBinding
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── Me: feedback & notifications ──
  /me/messages:
    get:
      tags: [Me]
      summary: List my feedback messages
      operationId: listMyFeedback
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Me]
      summary: Send a feedback message
      operationId: createUserMessage
      security: *jwtOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [content]
              properties:
                content:
                  type: string
                  maxLength: 2000
      responses:
        "201": { $ref: "#/components/responses/Created" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /me/messages/unread-count:
    get:
      tags: [Me]
      summary: Unread admin-reply count
      operationId: getUnreadRepliesCount
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/messages/mark-read:
    post:
      tags: [Me]
      summary: Mark feedback replies as read
      operationId: markMessagesRead
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/feedback:
    get:
      tags: [Me]
      summary: List my feedback threads
      operationId: listMyFeedbackThreads
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/feedback/{id}/reply:
    post:
      tags: [Me]
      summary: Reply in a feedback thread
      operationId: replyFeedbackThread
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/feedback/unread-count:
    get:
      tags: [Me]
      summary: Unread feedback reply count
      operationId: getUnreadFeedbackCount
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/notifications:
    get:
      tags: [Me]
      summary: Notification feed (cursor-paginated)
      description: "data = { notifications, hasMore, nextCursor }. Query: cursor, limit (max 100), unread=true, category=security|billing|domain|announcement."
      operationId: listMyNotifications
      security: *jwtOnly
      parameters:
        - name: cursor
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
        - name: unread
          in: query
          schema:
            type: boolean
        - name: category
          in: query
          schema:
            type: string
            enum: [security, billing, domain, announcement]
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/notifications/unread-count:
    get:
      tags: [Me]
      summary: Unread notification count
      operationId: getNotificationUnreadCount
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/notifications/read-all:
    post:
      tags: [Me]
      summary: Mark all notifications read
      operationId: markAllNotificationsRead
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/notifications/clear:
    post:
      tags: [Me]
      summary: Clear all notifications
      description: Deletes every user_notifications row for the caller; virtual announcement entries cannot be deleted, so active announcements are marked read instead. data = { status, deleted }.
      operationId: clearMyNotifications
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/notifications/{id}/read:
    post:
      tags: [Me]
      summary: Mark one notification read
      operationId: markNotificationRead
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/announcements/{id}/read:
    post:
      tags: [Me]
      summary: Mark an announcement read
      operationId: markAnnouncementRead
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Me: balance ──
  /me/balance:
    get:
      tags: [Me]
      summary: My balance + recent transactions
      description: data = { balance, transactions }. Amounts in cents.
      operationId: getMyBalance
      security: *jwtOnly
      parameters:
        - $ref: "#/components/parameters/limit"
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/balance/topup:
    post:
      tags: [Me]
      summary: Create a balance top-up order
      operationId: balanceTopUp
      security: *jwtOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                amount:
                  type: integer
                  description: Cents. Min 100 (1 yuan), max 1000000 (10000 yuan).
                payType:
                  type: string
                  enum: [alipay, wxpay]
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/balance/purchase:
    post:
      tags: [Me]
      summary: Purchase API-call credits or capacity add-on packs with balance
      description: >
        Deducts balance and grants a purchased-pool credit. type "api_calls"
        buys API-call credits (quantity = calls); A3 capacity add-ons:
        type "storage" buys storage packs (quantity = GB), type "inboxes"
        buys persistent inbox slots (quantity = slots). Purchased packs
        stack onto the plan baseline at read/enforcement time and never
        rewrite it.
      operationId: balancePurchase
      security: *jwtOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type, quantity]
              properties:
                type:
                  type: string
                  enum: [api_calls, storage, inboxes]
                quantity:
                  type: integer
                  description: >
                    Pack size — must match a catalog entry from GET /pricing
                    (packages for api_calls, addonPackages for
                    storage/inboxes).
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402":
          description: Insufficient balance.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  /me/balance/purchase-plan:
    post:
      tags: [Me]
      summary: Purchase a plan with balance
      operationId: balancePurchasePlan
      security: *jwtOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [planId]
              properties:
                planId:
                  type: string
                planDays:
                  type: integer
                  enum: [30, 90, 180, 365]
                confirmDowngrade:
                  type: boolean
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402":
          description: Insufficient balance.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

  # ── Payment (signed-in) ──
  /payment/checkout-options:
    post:
      tags: [Payment]
      summary: Get checkout quote + payment options for a plan
      operationId: getCheckoutOptions
      security: *jwtOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [planId]
              properties:
                planId:
                  type: string
                planDays:
                  type: integer
                confirmDowngrade:
                  type: boolean
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { $ref: "#/components/responses/Conflict" }

  /payment/checkout-preview:
    post:
      tags: [Payment]
      summary: "Alias of checkout-options"
      operationId: getCheckoutPreview
      deprecated: true
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /payment/upgrade-preview:
    post:
      tags: [Payment]
      summary: Preview a prorated plan upgrade
      operationId: getUpgradePreview
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /payment/upgrade-order:
    post:
      tags: [Payment]
      summary: Create a prorated upgrade order
      operationId: createUpgradeOrder
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { $ref: "#/components/responses/Conflict" }

  /payment/create-order:
    post:
      tags: [Payment]
      summary: Create a plan purchase order (external gateway)
      operationId: createPaymentOrder
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /payment/orders:
    get:
      tags: [Payment]
      summary: List my payment orders
      operationId: listMyPaymentOrders
      security: *jwtOnly
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /payment/orders/{id}/cancel:
    post:
      tags: [Payment]
      summary: Cancel my pending order
      operationId: cancelMyPaymentOrder
      security: *jwtOnly
      parameters: *idParam
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── Admin (internal console; grouped by tag only, not part of the public contract) ──
  /metrics:
    get:
      tags: [Admin]
      x-internal: true
      summary: Metrics (internal)
      operationId: metrics
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/stats:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin Stats (internal)
      operationId: adminStats
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/monitor:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin System Monitor (internal)
      operationId: adminSystemMonitor
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/quota-alerts:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin Quota Alerts (internal)
      description: >
        F1 — users at/above the quota alert threshold (default 90%,
        system_config quota.alert_threshold_percent) across
        inboxes / apiCallsDaily / storage. data = { alerts, total, page,
        limit, thresholdPercent, counts }.
      operationId: adminQuotaAlerts
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: dimension
          in: query
          schema:
            type: string
            enum: [all, inboxes, apiCallsDaily, storage]
        - name: page
          in: query
          schema: { type: integer, minimum: 1 }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100 }
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/access-blocks:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Upsert Access Block (internal)
      operationId: adminUpsertAccessBlock
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/access-blocks/{id}:
    patch:
      tags: [Admin]
      x-internal: true
      summary: Admin Update Access Block (internal)
      operationId: adminUpdateAccessBlock
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/access-blocks/{id}/release:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Release Access Block (internal)
      operationId: adminReleaseAccessBlock
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/users:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin List Users (internal)
      operationId: adminListUsers
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/users/{id}:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin Get User (internal)
      operationId: adminGetUser
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    patch:
      tags: [Admin]
      x-internal: true
      summary: Admin Update User (internal)
      operationId: adminUpdateUser
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/users/{id}/active:
    patch:
      tags: [Admin]
      x-internal: true
      summary: Admin Toggle User Active (internal)
      operationId: adminToggleUserActive
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/domains:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin List Domains (internal)
      operationId: adminListDomains
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Create Public Domain (internal)
      operationId: adminCreatePublicDomain
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/domains/stats:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin Domain Stats (internal)
      operationId: adminDomainStats
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/wildcard-rules:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin List Wildcard Rules (internal)
      operationId: adminListWildcardRules
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/domains/{id}/catchall:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin Get Catchall (internal)
      operationId: adminGetCatchall
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    put:
      tags: [Admin]
      x-internal: true
      summary: Admin Upsert Catchall (internal)
      operationId: adminUpsertCatchall
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/domains/{id}/governance-tier:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin Get Governance Tier (internal)
      operationId: adminGetGovernanceTier
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    put:
      tags: [Admin]
      x-internal: true
      summary: Admin Upsert Governance Tier (internal)
      operationId: adminUpsertGovernanceTier
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/domains/dedicated-usage:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin Dedicated Domain Cap Usage (internal)
      description: >
        D3: dedicated-tier cap configuration and live sliding-window usage per
        domain, aggregated per isolation group (label + statistics only), plus
        process-level cap check/reject counters.
      operationId: adminDedicatedUsage
      security:
        - cookieAuth: []
        - bearerAuth: []
  /admin/domains/governance/events:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin List Governance Events (internal)
      operationId: adminListGovernanceEvents
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/domains/governance/runs:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin List Governance Runs (internal)
      operationId: adminListGovernanceRuns
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/domains/{id}/governance:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin Get Domain Governance (internal)
      operationId: adminGetDomainGovernance
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/domains/{id}/governance/pause:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Pause Domain Governance (internal)
      operationId: adminPauseDomainGovernance
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reasonCode]
              properties:
                reasonCode:
                  type: string
                  enum: [risk_manual_review, ops_manual_pause]
                note:
                  type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
  /admin/domains/{id}/governance/resume:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Resume Domain Governance (internal)
      operationId: adminResumeDomainGovernance
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
  /admin/wildcard-rules/{id}:
    patch:
      tags: [Admin]
      x-internal: true
      summary: Admin Update Wildcard Rule (internal)
      operationId: adminUpdateWildcardRule
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/wildcard-rules/{id}/assignments:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin List Wildcard Assignments (internal)
      operationId: adminListWildcardAssignments
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/wildcard-rules/{id}/assignments/{assignmentId}/inboxes:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin List Wildcard Assignment Inboxes (internal)
      operationId: adminListWildcardAssignmentInboxes
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: assignmentId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/wildcard-rules/{id}/assignments/{assignmentId}:
    delete:
      tags: [Admin]
      x-internal: true
      summary: Admin Delete Wildcard Assignment (internal)
      operationId: adminDeleteWildcardAssignment
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: assignmentId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/wildcard-rules/{id}/assignments/{assignmentId}/release:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Release Wildcard Assignment (internal)
      operationId: adminReleaseWildcardAssignment
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: assignmentId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/domains/duplicates:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin Domain Duplicates (internal)
      operationId: adminDomainDuplicates
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/domains/{id}:
    patch:
      tags: [Admin]
      x-internal: true
      summary: Admin Update Domain (internal)
      operationId: adminUpdateDomain
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Admin]
      x-internal: true
      summary: Admin Delete Domain (internal)
      operationId: adminDeleteDomain
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/domains/batch-delete:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Batch Delete Domains (internal)
      operationId: adminBatchDeleteDomains
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/domains/cleanup-unverified:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Cleanup Unverified Domains (internal)
      operationId: adminCleanupUnverifiedDomains
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/config:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin List Config (internal)
      operationId: adminListConfig
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/config/{key}:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin Get Config (internal)
      operationId: adminGetConfig
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: key
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    put:
      tags: [Admin]
      x-internal: true
      summary: Admin Set Config (internal)
      operationId: adminSetConfig
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: key
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/config/notification/bark-test:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Test Bark (internal)
      operationId: adminTestBark
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/config/notification/bark-status:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin Bark Status (internal)
      operationId: adminBarkStatus
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/config/telegram/verify-bot:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Verify Telegram Bot (internal)
      operationId: adminVerifyTelegramBot
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/mail-sender-profiles:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin List Mail Sender Profiles (internal)
      operationId: adminListMailSenderProfiles
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/mail-sender-profiles/{code}:
    put:
      tags: [Admin]
      x-internal: true
      summary: Admin Upsert Mail Sender Profile (internal)
      operationId: adminUpsertMailSenderProfile
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/mail-sender-profiles/{code}/activate:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Activate Mail Sender Profile (internal)
      operationId: adminActivateMailSenderProfile
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/mail-sender-profiles/{code}/test:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Test Mail Sender Profile (internal)
      operationId: adminTestMailSenderProfile
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/plans:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin List Plans (internal)
      operationId: adminListPlans
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/plans/{id}:
    put:
      tags: [Admin]
      x-internal: true
      summary: Admin Update Plan (internal)
      operationId: adminUpdatePlan
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/redemption-codes:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Create Redemption Codes (internal)
      operationId: adminCreateRedemptionCodes
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin List Redemption Codes (internal)
      operationId: adminListRedemptionCodes
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/redemption-codes/{id}:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin Get Redemption Code (internal)
      operationId: adminGetRedemptionCode
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Admin]
      x-internal: true
      summary: Admin Delete Redemption Code (internal)
      operationId: adminDeleteRedemptionCode
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/users/{id}/redeem-history:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin User Redeem History (internal)
      operationId: adminUserRedeemHistory
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/attention-counts:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin Attention Counts (internal)
      operationId: adminAttentionCounts
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/messages:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin List Feedback (internal)
      operationId: adminListFeedback
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/messages/{id}:
    delete:
      tags: [Admin]
      x-internal: true
      summary: Admin Delete Message (internal)
      operationId: adminDeleteMessage
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/messages/{id}/reply:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Reply Message (internal)
      operationId: adminReplyMessage
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/messages/{id}/close:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Close Message (internal)
      operationId: adminCloseMessage
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/announcements:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin List Announcements (internal)
      operationId: adminListAnnouncements
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Create Announcement (internal)
      description: Publishes an in-app announcement. Email fan-out occurs only when pushEmail is explicitly true; omitted and false both skip email.
      operationId: adminCreateAnnouncement
      security:
        - cookieAuth: []
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, content]
              properties:
                title:
                  type: string
                  maxLength: 200
                content:
                  type: string
                  maxLength: 5000
                type:
                  type: string
                  enum: [info, warning, update]
                  default: info
                isPinned:
                  type: boolean
                  default: false
                pushEmail:
                  type: boolean
                  default: false
                  description: Explicit opt-in for this publish. Queues email only for eligible users who enabled announcement email.
                pushTelegram:
                  type: boolean
                  default: true
                  description: Whether to fan out this publish to eligible Telegram subscribers.
      responses:
        "201": { $ref: "#/components/responses/Created" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/announcements/{id}:
    patch:
      tags: [Admin]
      x-internal: true
      summary: Admin Update Announcement (internal)
      operationId: adminUpdateAnnouncement
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Admin]
      x-internal: true
      summary: Admin Delete Announcement (internal)
      operationId: adminDeleteAnnouncement
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/email-templates:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin List Email Templates (internal)
      operationId: adminListEmailTemplates
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/email-templates/{code}:
    patch:
      tags: [Admin]
      x-internal: true
      summary: Admin Update Email Template (internal)
      operationId: adminUpdateEmailTemplate
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/email-templates/{code}/preview:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Preview Email Template (internal)
      operationId: adminPreviewEmailTemplate
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/email-templates/{code}/reset:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Reset Email Template (internal)
      operationId: adminResetEmailTemplate
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/email-dispatch:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin List Email Dispatch Jobs (internal)
      operationId: adminListEmailDispatchJobs
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/email-dispatch/{id}:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin Get Email Dispatch Job (internal)
      operationId: adminGetEmailDispatchJob
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/email-dispatch/{id}/retry:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Retry Email Dispatch Job (internal)
      operationId: adminRetryEmailDispatchJob
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/articles:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin List Articles (internal)
      operationId: adminListArticles
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Create Article (internal)
      operationId: adminCreateArticle
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/articles/upload-image:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Upload Article Image (internal)
      operationId: adminUploadArticleImage
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/articles/{id}:
    patch:
      tags: [Admin]
      x-internal: true
      summary: Admin Update Article (internal)
      operationId: adminUpdateArticle
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Admin]
      x-internal: true
      summary: Admin Delete Article (internal)
      operationId: adminDeleteArticle
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/orders:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin List Orders (internal)
      operationId: adminListOrders
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/orders/stats:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin Order Stats (internal)
      operationId: adminOrderStats
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/orders/{id}/refund:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Refund Order (internal)
      operationId: adminRefundOrder
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/orders/{id}/cancel:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Cancel Order (internal)
      operationId: adminCancelOrder
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/revenue/summary:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin Revenue Summary (internal)
      operationId: adminRevenueSummary
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/balance/{userId}:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin Get User Balance (internal)
      operationId: adminGetUserBalance
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/balance/{userId}/grant:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Grant Balance (internal)
      operationId: adminGrantBalance
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/balance/{userId}/deduct:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Deduct Balance (internal)
      operationId: adminDeductBalance
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/cleanup/status:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin Cleanup Status (internal)
      operationId: adminCleanupStatus
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/cleanup/trigger:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Cleanup Trigger (internal)
      operationId: adminCleanupTrigger
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/cleanup/preview:
    get:
      tags: [Admin]
      x-internal: true
      summary: Admin Cleanup Preview (internal)
      operationId: adminCleanupPreview
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/cleanup/execute:
    post:
      tags: [Admin]
      x-internal: true
      summary: Admin Cleanup Execute (internal)
      operationId: adminCleanupExecute
      security:
        - cookieAuth: []
        - bearerAuth: []
      responses:
        "200": { $ref: "#/components/responses/OK" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  # ── Realtime ──
  /ws:
    get:
      tags: [Public]
      summary: WebSocket endpoint for realtime new-message events
      description: >
        HTTP GET upgrade to WebSocket. Authenticate with a short-lived ticket
        from GET /auth/ws-ticket (?ticket=...) or an auth cookie.
      operationId: websocket
      security:
        - {}
        - cookieAuth: []
      responses:
        "101":
          description: Switching protocols (WebSocket upgrade).
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          description: >
            Too many concurrent WebSocket connections for this account
            (default cap 10, env YYDS_WS_MAX_CONNS_PER_USER). Close idle
            connections and retry.
