openapi: 3.1.0
info:
  title: Activatist Phase 1 Foundation API
  version: 0.2.0
  license:
    name: Proprietary draft
    identifier: LicenseRef-Proprietary
  description: >
    Implemented foundation API for seller onboarding, Stripe Connect test-mode onboarding,
    admin review, product management, public product reads, optional CSV license inventory
    import, Stripe Checkout test-mode purchase flow with generated license issuance, and
    closed-beta buyer completion delivery. Real email delivery is implemented in staging
    for closed-beta verification. Refund handling is partially implemented through webhook
    status projections. Live payments and broader public-launch concerns remain outside
    this scope.
    New products default to generated_server_validated licenses; CSV import remains optional.
servers:
  - url: https://license.souko.work
security: []
paths:
  /healthz:
    get:
      operationId: getHealth
      summary: Liveness check
      responses:
        "200":
          description: Service is alive
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
        "429":
          $ref: "#/components/responses/Error"
        "503":
          $ref: "#/components/responses/Error"
  /readyz:
    get:
      operationId: getReadiness
      summary: Readiness check
      responses:
        "200":
          description: Service dependencies are ready
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
        "429":
          $ref: "#/components/responses/Error"
        "503":
          $ref: "#/components/responses/Error"
  /v1/sellers:
    post:
      operationId: createSeller
      summary: Create an authenticated seller application
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSellerRequest"
      responses:
        "201":
          description: Seller application created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Seller"
        "400":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"
  /v1/sellers/me:
    get:
      operationId: getCurrentSeller
      summary: Get the authenticated seller profile
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Seller profile
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Seller"
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
  /v1/sellers/stripe/account-link:
    post:
      operationId: createStripeAccountLink
      summary: Create a Stripe Connect hosted onboarding link
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Account link created
          content:
            application/json:
              schema:
                type: object
                required: [url]
                properties:
                  url:
                    type: string
                    format: uri
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
        "409":
          $ref: "#/components/responses/Error"
  /v1/sellers/stripe/refresh:
    post:
      operationId: refreshStripeOnboarding
      summary: Refresh seller Stripe onboarding state
      description: Authenticated seller endpoint that retrieves the connected account server-side, preferring Stripe Accounts v2, and updates only Stripe onboarding fields. It does not expose raw Stripe payloads or secrets.
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Refreshed seller profile
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Seller"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
  /v1/stripe/webhook:
    post:
      operationId: receiveStripeWebhook
      summary: Receive Stripe platform webhook events
      description: Verifies Stripe-Signature. checkout.session.completed marks the order paid and issues one generated_server_validated license idempotently. charge.refunded, charge.dispute.created, charge.dispute.closed, and payment_intent.payment_failed update only payment/order/license status transactionally. Accounts v2 thin account events are accepted and cause a server-side account retrieve before updating only stripe_onboarding_status fields.
      parameters:
        - name: Stripe-Signature
          in: header
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        "200":
          description: Event received
          content:
            application/json:
              schema:
                type: object
                required: [received, processed]
                properties:
                  received:
                    type: boolean
                  processed:
                    type: boolean
                  ignored:
                    type: boolean
        "400":
          $ref: "#/components/responses/Error"
  /v1/stripe/connect/webhook:
    post:
      operationId: receiveStripeConnectWebhook
      summary: Receive optional Stripe Connect webhook events
      description: Dedicated webhook endpoint for Stripe Connect / Accounts v2 seller-state events that use a separate STRIPE_CONNECT_WEBHOOK_SECRET. In staging this receives the account-scoped Accounts v2 thin Event Destination for requirements and merchant/recipient capability status changes.
      parameters:
        - name: Stripe-Signature
          in: header
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        "200":
          description: Event received
          content:
            application/json:
              schema:
                type: object
                required: [received, processed]
                properties:
                  received:
                    type: boolean
                  processed:
                    type: boolean
        "400":
          $ref: "#/components/responses/Error"
  /v1/admin/sellers:
    get:
      operationId: listAdminSellers
      summary: List sellers for admin review
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Seller list
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Seller"
        "403":
          $ref: "#/components/responses/Error"
  /v1/admin/sellers/{seller_id}/review:
    patch:
      operationId: reviewSeller
      summary: Review, approve, reject, or suspend a seller
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/SellerID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ReviewSellerRequest"
      responses:
        "200":
          description: Seller updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Seller"
        "400":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
  /v1/admin/products/{product_id}/review:
    patch:
      operationId: reviewProduct
      summary: Approve, reject, or suspend a product
      description: Product review is separate from seller review. Sellers cannot approve or publish their own products.
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ProductID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ReviewProductRequest"
      responses:
        "200":
          description: Product updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Product"
        "400":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
  /v1/admin/product-files:
    get:
      operationId: listAdminProductFiles
      summary: List product files for admin review
      description: Admin-only JSON review queue for private seller-file metadata. Responses intentionally omit storage bucket/key, presigned URLs, credentials, buyer email, completion tokens, and raw license keys.
      security:
        - bearerAuth: []
      parameters:
        - name: status
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/ProductFileStatus"
        - name: product_id
          in: query
          required: false
          schema:
            type: string
        - name: seller_id
          in: query
          required: false
          schema:
            type: string
        - name: hash_status
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/ProductFileHashStatus"
        - name: scan_status
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/ProductFileScanStatus"
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
      responses:
        "200":
          description: Product file review items
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/ProductFileAdmin"
        "400":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
  /v1/admin/products/{product_id}/files/{file_id}/review:
    patch:
      operationId: reviewProductFile
      summary: Review, manually mark clean, reject, or disable a product file
      description: Admin-only file review action. `manual_clean` records audited trusted-clean metadata but does not approve the file or make it buyer-visible. `approve` requires pending_review, hash_status=verified, and trusted clean from an allowed scanner or audited manual clean marker.
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ProductID"
        - $ref: "#/components/parameters/FileID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProductFileReviewRequest"
      responses:
        "200":
          description: Product file review state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductFileAdmin"
        "400":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
        "409":
          $ref: "#/components/responses/Error"
  /v1/admin/orders/{order_id}/completion-token:
    post:
      operationId: inspectPurchaseCompletionToken
      summary: Issue a staging/admin purchase completion token
      description: Admin-only staging/local inspection endpoint for STG-3 and delivery debugging. It returns a short-lived completion token for a paid order without exposing the raw license key. The runtime blocks this endpoint in production environments; real buyer delivery must use the email/outbox delivery path.
      security:
        - bearerAuth: []
      parameters:
        - name: order_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Completion token issued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminCompletionTokenResponse"
        "400":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
  /v1/admin/licenses/{license_id}/revoke:
    post:
      operationId: revokeIssuedLicense
      summary: Manually revoke an issued license
      description: Admin-only auditable revoke. The license record and activation history are preserved; validate and activate reject the license immediately after status changes to revoked.
      security:
        - bearerAuth: []
      parameters:
        - name: license_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RevokeLicenseRequest"
      responses:
        "200":
          description: License revoked
          content:
            application/json:
              schema:
                type: object
                required: [processed, license]
                properties:
                  processed:
                    type: boolean
                  license:
                    $ref: "#/components/schemas/IssuedLicense"
        "400":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
  /v1/products:
    post:
      operationId: createProduct
      summary: Create a product draft
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProductUpsertRequest"
      responses:
        "201":
          description: Product created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Product"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"
        "409":
          description: Product slug already reserved globally
          $ref: "#/components/responses/Error"
  /v1/products/{product_id}:
    get:
      operationId: getProduct
      summary: Get an authenticated seller product
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ProductID"
      responses:
        "200":
          description: Product
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Product"
        "403":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
    patch:
      operationId: updateProduct
      summary: Update an authenticated seller product
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ProductID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProductUpsertRequest"
      responses:
        "200":
          description: Product updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Product"
        "400":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
        "409":
          description: Product slug already reserved globally
          $ref: "#/components/responses/Error"
  /v1/products/{product_id}/publish:
    post:
      operationId: setProductPublication
      summary: Legacy publication toggle
      description: Sellers cannot publish directly. publish=true returns forbidden; use submit-review and admin product review instead. publish=false returns the product to draft unless suspended.
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ProductID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [publish]
              properties:
                publish:
                  type: boolean
      responses:
        "200":
          description: Product updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Product"
        "400":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
  /v1/products/{product_id}/submit-review:
    post:
      operationId: submitProductForReview
      summary: Submit an owned product draft for admin review
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ProductID"
      responses:
        "200":
          description: Product submitted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Product"
        "400":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
  /v1/products/{product_id}/code-batches:
    post:
      operationId: createCodeBatch
      summary: Import optional license-key CSV inventory for an imported_csv product
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ProductID"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [idempotency_key, file]
              properties:
                idempotency_key:
                  type: string
                file:
                  type: string
                  format: binary
      responses:
        "202":
          description: Code batch accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CodeBatch"
        "400":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
  /v1/products/{product_id}/files/upload-intent:
    post:
      operationId: createProductFileUploadIntent
      summary: Create a private seller-file upload intent
      description: Authenticated seller endpoint that creates private product_file metadata and a short-lived presigned PUT upload URL. The returned URL is sensitive, is not a public download URL, and must not be logged or committed. Closed beta requires seller files to remain private until hash verification, manual/static review, and separate admin approval pass.
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ProductID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProductFileUploadIntentRequest"
      responses:
        "201":
          description: Upload intent created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductFileUploadIntentResponse"
        "400":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
  /v1/products/{product_id}/files/{file_id}/finalize:
    post:
      operationId: finalizeProductFileUpload
      summary: Finalize a private seller-file upload
      description: Authenticated seller endpoint that verifies the private object with storage HEAD Object, records safe metadata, and moves the file to pending_review/private with hash_status=unverified. It does not approve the file, make it buyer-visible, or expose any download URL.
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ProductID"
        - $ref: "#/components/parameters/FileID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProductFileFinalizeRequest"
      responses:
        "200":
          description: Product file finalized for review
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductFile"
        "400":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
        "409":
          $ref: "#/components/responses/Error"
  /v1/public/products/{slug}:
    get:
      operationId: getPublicProduct
      summary: Get safe public product payload for hosted pages
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Public product payload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicProduct"
        "404":
          $ref: "#/components/responses/Error"
  /v1/public/products/{slug}/checkout:
    post:
      operationId: createCheckout
      summary: Create a Stripe Checkout Session for a public generated-license product
      description: Requires a product review status of approved or published, an internally approved or approved_limited seller, and Stripe onboarding state that allows charges. Seller approval alone is not enough. Uses Connect destination charges and an application fee.
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCheckoutRequest"
      responses:
        "201":
          description: Checkout Session created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CheckoutResponse"
        "400":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
        "429":
          $ref: "#/components/responses/Error"
  /v1/purchases/complete:
    post:
      operationId: retrievePurchaseCompletion
      summary: Retrieve a generated license after paid Checkout
      description: Authorizes controlled license redisplay with a short-lived signed completion token. Accountless buyer pages send token in the JSON body as `token`; legacy support flows may send buyer_email plus completion_token. Tokens must not be placed in URL query strings.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PurchaseCompletionRequest"
      responses:
        "200":
          description: Authorized license redisplay response
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/PurchaseCompletionResponse"
                  - $ref: "#/components/schemas/PurchaseCompletionPageResponse"
        "400":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
        "429":
          $ref: "#/components/responses/Error"
  /v1/purchases/files/{file_id}/download-intent:
    post:
      operationId: createPurchaseFileDownloadIntent
      summary: Create a buyer-authorized short-lived file download intent
      description: Accountless buyer endpoint. Requires a valid purchase completion token and a file that belongs to the purchased product and is approved, buyer-visible, hash-verified, and trusted clean. Returns a short-lived private GET URL only; the response intentionally omits storage bucket/key, R2 credentials, buyer email, raw license key, and permanent public URLs.
      parameters:
        - $ref: "#/components/parameters/FileID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/FileDownloadIntentRequest"
      responses:
        "200":
          description: Short-lived private download intent
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FileDownloadIntentResponse"
        "400":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
        "429":
          $ref: "#/components/responses/Error"
  /v1/licenses/validate:
    post:
      operationId: validateLicense
      summary: Validate a generated server-validated license key
      description: Uses normalized key plus HMAC lookup for a point-read. Does not create activations or increment activation_count.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LicenseRuntimeRequest"
      responses:
        "200":
          description: License validation result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LicenseRuntimeResponse"
        "400":
          $ref: "#/components/responses/Error"
  /v1/licenses/activate:
    post:
      operationId: activateLicense
      summary: Activate a generated server-validated license for one device
      description: Hashes device_fingerprint before storage and transactionally enforces activation_limit. Same-device activation is idempotent.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LicenseRuntimeRequest"
      responses:
        "200":
          description: License activation result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LicenseRuntimeResponse"
        "400":
          $ref: "#/components/responses/Error"
        "409":
          $ref: "#/components/responses/Error"
  /v1/licenses/deactivate:
    post:
      operationId: deactivateLicense
      summary: Deactivate one device activation for a generated license
      description: Marks the activation inactive and frees a seat. Repeated deactivation and unknown activation are idempotent.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LicenseRuntimeRequest"
      responses:
        "200":
          description: License deactivation result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LicenseRuntimeResponse"
        "400":
          $ref: "#/components/responses/Error"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Local foundation mode accepts X-User-ID and X-User-Email headers; production adapter is pluggable.
  parameters:
    ProductID:
      name: product_id
      in: path
      required: true
      schema:
        type: string
    SellerID:
      name: seller_id
      in: path
      required: true
      schema:
        type: string
    FileID:
      name: file_id
      in: path
      required: true
      schema:
        type: string
  responses:
    Error:
      description: Stable error response
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  schemas:
    HealthResponse:
      type: object
      required: [status]
      properties:
        status:
          type: string
          examples: [ok]
    Error:
      type: object
      required: [code, message]
      properties:
        request_id:
          type: string
        code:
          type: string
          examples: [invalid_request]
        message:
          type: string
    CreateSellerRequest:
      type: object
      required: [display_name, contact_email, support_email, site_url]
      properties:
        display_name:
          type: string
        contact_email:
          type: string
          format: email
        support_email:
          type: string
          format: email
        site_url:
          type: string
          format: uri
    ReviewSellerRequest:
      type: object
      required: [decision]
      properties:
        decision:
          type: string
          enum: [approve, approve_limited, needs_more_info, reject, suspend]
        note:
          type: string
    ReviewProductRequest:
      type: object
      required: [decision]
      properties:
        decision:
          type: string
          enum: [approve, reject, suspend]
        rejection_reason:
          type: string
        note:
          type: string
    Seller:
      type: object
      required: [id, owner_user_id, display_name, internal_review_status, stripe_onboarding_status]
      properties:
        id:
          type: string
        owner_user_id:
          type: string
        display_name:
          type: string
        contact_email:
          type: string
          format: email
        support_email:
          type: string
          format: email
        site_url:
          type: string
          format: uri
        internal_review_status:
          type: string
          enum: [draft, pending_review, approved_limited, approved, rejected, suspended]
        stripe_connected_account_id:
          type: string
        stripe_onboarding_status:
          type: string
          enum: [not_started, account_created, onboarding_started, requirements_due, charges_enabled, payouts_enabled, restricted, rejected]
        stripe_charges_enabled:
          type: boolean
        stripe_payouts_enabled:
          type: boolean
        stripe_requirements_due:
          type: array
          items:
            type: string
        stripe_disabled_reason:
          type: string
    ProductUpsertRequest:
      type: object
      required:
        - name
        - description
        - price_amount
        - currency
        - support_email
        - refund_policy
        - software_operating_environment
      properties:
        name:
          type: string
        description:
          type: string
        price_amount:
          type: integer
          minimum: 0
        currency:
          type: string
          minLength: 3
          maxLength: 3
        support_email:
          type: string
          format: email
        refund_policy:
          type: string
        software_operating_environment:
          type: string
        tokushoho_seller_name:
          type: string
        tokushoho_contact:
          type: string
        tokushoho_address:
          type: string
        forbidden_category_acknowledged:
          type: boolean
        license_issuance_mode:
          $ref: "#/components/schemas/LicenseIssuanceMode"
          default: generated_server_validated
        license_key_prefix:
          type: string
          default: LIC1
        activation_limit:
          type: integer
          default: 1
        features:
          type: array
          items:
            type: string
        deactivation_allowed:
          type: boolean
    Product:
      type: object
      required: [id, seller_id, slug, name, status, price_amount, currency, license_issuance_mode]
      properties:
        id:
          type: string
        seller_id:
          type: string
        slug:
          type: string
        name:
          type: string
        description:
          type: string
        status:
          type: string
          enum: [draft, pending_review, approved, published, unpublished, rejected, suspended]
        review_status:
          type: string
          enum: [draft, pending_review, approved, published, rejected, suspended]
        reviewed_by:
          type: string
        reviewed_at:
          type: string
          format: date-time
        rejection_reason:
          type: string
        price_amount:
          type: integer
        currency:
          type: string
        support_email:
          type: string
          format: email
        refund_policy:
          type: string
        software_operating_environment:
          type: string
        forbidden_category_acknowledged:
          type: boolean
        license_issuance_mode:
          $ref: "#/components/schemas/LicenseIssuanceMode"
        license_key_prefix:
          type: string
        activation_limit:
          type: integer
        features:
          type: array
          items:
            type: string
    PublicProduct:
      type: object
      required:
        - id
        - slug
        - name
        - description
        - price_amount
        - currency
        - seller_display_name
        - support_email
        - refund_policy
        - software_operating_environment
      properties:
        id:
          type: string
        slug:
          type: string
        name:
          type: string
        description:
          type: string
        price_amount:
          type: integer
        currency:
          type: string
        seller_display_name:
          type: string
        support_email:
          type: string
          format: email
        refund_policy:
          type: string
        software_operating_environment:
          type: string
        license_issuance_mode:
          $ref: "#/components/schemas/LicenseIssuanceMode"
        activation_limit:
          type: integer
        features:
          type: array
          items:
            type: string
    ProductFileUploadIntentRequest:
      type: object
      required: [filename, content_type, size_bytes, sha256_hex]
      properties:
        filename:
          type: string
          description: Original filename. The server stores a sanitized filename.
        content_type:
          type: string
        size_bytes:
          type: integer
          format: int64
          minimum: 1
        sha256_hex:
          type: string
          pattern: "^[a-fA-F0-9]{64}$"
          description: Declared SHA-256 for upload binding. The hash worker computes the authoritative verified hash after finalize.
    ProductFileUploadIntentResponse:
      type: object
      required: [product_file_id, upload_url, method, expires_at, max_size_bytes, storage_provider]
      properties:
        product_file_id:
          type: string
        upload_url:
          type: string
          format: uri
          description: Sensitive short-lived private upload URL. Do not log or persist in shared notes.
        method:
          type: string
          enum: [PUT]
        required_headers:
          type: object
          additionalProperties:
            type: string
          description: Headers the upload client must send with the PUT request, such as Content-Type, Content-Length, and checksum headers.
        expires_at:
          type: string
          format: date-time
        max_size_bytes:
          type: integer
          format: int64
        storage_provider:
          type: string
          enum: [r2, metadata_only]
    ProductFileFinalizeRequest:
      type: object
      required: [size_bytes, sha256_hex]
      properties:
        content_type:
          type: string
        size_bytes:
          type: integer
          format: int64
          minimum: 1
        sha256_hex:
          type: string
          pattern: "^[a-fA-F0-9]{64}$"
    ProductFileReviewRequest:
      type: object
      required: [decision]
      properties:
        decision:
          type: string
          enum: [approve, manual_clean, mark_manual_clean, reject, disable]
        visibility:
          $ref: "#/components/schemas/ProductFileVisibility"
        rejected_reason:
          type: string
          description: Required for reject.
        manual_scan_clean_reason:
          type: string
          maxLength: 500
          description: Required for manual_clean / mark_manual_clean. Do not include secrets, license keys, buyer data, or storage URLs.
    ProductFile:
      type: object
      required:
        - id
        - product_id
        - seller_id
        - file_name
        - content_type
        - size_bytes
        - hash_status
        - scan_status
        - status
        - visibility
      properties:
        id:
          type: string
        product_id:
          type: string
        seller_id:
          type: string
        file_name:
          type: string
        content_type:
          type: string
        size_bytes:
          type: integer
          format: int64
        sha256_hex:
          type: string
        verified_size_bytes:
          type: integer
          format: int64
        verified_content_type:
          type: string
        verified_sha256_hex:
          type: string
        hash_status:
          $ref: "#/components/schemas/ProductFileHashStatus"
        hash_error:
          type: string
        scan_status:
          $ref: "#/components/schemas/ProductFileScanStatus"
        scan_engine:
          type: string
        scan_signature_version:
          type: string
        scan_result_summary:
          type: string
        scan_error:
          type: string
        scan_trust_source:
          $ref: "#/components/schemas/ProductFileScanTrustSource"
        scan_trusted_clean:
          type: boolean
        storage_provider:
          type: string
          enum: [r2, metadata_only]
        storage_bucket:
          type: string
          description: Internal storage bucket from the seller finalize response. Do not expose to buyers.
        storage_key:
          type: string
          description: Internal storage object key from the seller finalize response. Do not expose to buyers.
        status:
          $ref: "#/components/schemas/ProductFileStatus"
        visibility:
          $ref: "#/components/schemas/ProductFileVisibility"
        created_at:
          type: string
          format: date-time
        uploaded_at:
          type: string
          format: date-time
        verified_at:
          type: string
          format: date-time
        scanned_at:
          type: string
          format: date-time
        approved_at:
          type: string
          format: date-time
        rejected_at:
          type: string
          format: date-time
        rejected_reason:
          type: string
    ProductFileAdmin:
      type: object
      required:
        - file_id
        - product_id
        - seller_id
        - filename
        - content_type
        - size_bytes
        - hash_status
        - scan_status
        - scan_trusted_clean
        - status
        - visibility
        - created_at
      properties:
        file_id:
          type: string
        product_id:
          type: string
        seller_id:
          type: string
        filename:
          type: string
        content_type:
          type: string
        size_bytes:
          type: integer
          format: int64
        verified_size_bytes:
          type: integer
          format: int64
        verified_content_type:
          type: string
        sha256_hex:
          type: string
        verified_sha256_hex:
          type: string
        hash_status:
          $ref: "#/components/schemas/ProductFileHashStatus"
        hash_error:
          type: string
        scan_status:
          $ref: "#/components/schemas/ProductFileScanStatus"
        scan_engine:
          type: string
        scan_signature_version:
          type: string
        scan_result_summary:
          type: string
        scan_error:
          type: string
        scan_trust_source:
          $ref: "#/components/schemas/ProductFileScanTrustSource"
        scan_trusted_clean:
          type: boolean
        manual_scan_clean_at:
          type: string
          format: date-time
        manual_scan_clean_by:
          type: string
        manual_scan_clean_reason:
          type: string
        manual_scan_previous_status:
          $ref: "#/components/schemas/ProductFileScanStatus"
        status:
          $ref: "#/components/schemas/ProductFileStatus"
        visibility:
          $ref: "#/components/schemas/ProductFileVisibility"
        created_at:
          type: string
          format: date-time
        uploaded_at:
          type: string
          format: date-time
        verified_at:
          type: string
          format: date-time
        scanned_at:
          type: string
          format: date-time
        approved_at:
          type: string
          format: date-time
        rejected_at:
          type: string
          format: date-time
        rejected_reason:
          type: string
    LicenseRuntimeRequest:
      type: object
      required: [product_id, license_key]
      properties:
        product_id:
          type: string
        license_key:
          type: string
          description: Raw license key supplied by client software. Never logged or returned.
        device_fingerprint:
          type: string
          description: Required for activate/deactivate. Hashed before storage.
    LicenseRuntimeResponse:
      type: object
      required: [valid, status, activation_limit, activation_count]
      properties:
        valid:
          type: boolean
        status:
          type: string
          enum: [unknown, active, revoked, refunded, disputed, suspended, expired]
        license_id:
          type: string
        product_id:
          type: string
        activation_limit:
          type: integer
        activation_count:
          type: integer
        features:
          type: array
          items:
            type: string
        expires_at:
          type: string
          format: date-time
    RevokeLicenseRequest:
      type: object
      required: [reason]
      properties:
        reason:
          type: string
          minLength: 1
          description: Audit reason for manual license revocation. Do not include raw license keys or buyer personal data.
    AdminCompletionTokenResponse:
      type: object
      required: [order_id, completion_token, expires_at]
      properties:
        order_id:
          type: string
        completion_token:
          type: string
          description: Short-lived HMAC-signed purchase completion token. Treat as sensitive and do not log or persist outside local staging state.
        expires_at:
          type: string
          format: date-time
        staging_only_note:
          type: string
    IssuedLicense:
      type: object
      required: [id, seller_id, product_id, status, license_issuance_mode, order_id]
      properties:
        id:
          type: string
        seller_id:
          type: string
        product_id:
          type: string
        status:
          type: string
          enum: [active, revoked, refunded, disputed, suspended, expired]
        license_issuance_mode:
          $ref: "#/components/schemas/LicenseIssuanceMode"
        order_id:
          type: string
        activation_count:
          type: integer
        expires_at:
          type: string
          format: date-time
    CreateCheckoutRequest:
      type: object
      properties:
        buyer_email:
          type: string
          format: email
          description: Optional email prefill for Stripe Checkout. Do not log buyer email.
    CheckoutResponse:
      type: object
      required: [order_id, checkout_session_id, checkout_url, platform_fee_jpy]
      properties:
        order_id:
          type: string
        checkout_session_id:
          type: string
        checkout_url:
          type: string
          format: uri
        platform_fee_jpy:
          type: integer
          description: Platform fee in JPY, calculated as 5.6% rounded up plus 45 JPY by default.
    PurchaseCompletionRequest:
      type: object
      properties:
        token:
          type: string
          description: Completion token sent by the accountless buyer page after reading the URL fragment. Do not send this in a URL query string.
        checkout_session_id:
          type: string
          description: Optional extra check; when supplied it must match the signed completion token.
        buyer_email:
          type: string
          format: email
          description: Used only for HMAC hash authorization against the signed completion token and stored order hash. Do not log plaintext buyer email.
        completion_token:
          type: string
          description: Legacy field for the server-generated short-lived HMAC-signed token containing order_id, checkout_session_id, buyer_email_hash, issued_license_id, expires_at, and jti. The server stores only the active token hash for the order; reissued tokens invalidate earlier tokens.
    PurchaseCompletionResponse:
      type: object
      required: [order_id, checkout_session_id, product_id, license_id, license_key, status]
      properties:
        order_id:
          type: string
        checkout_session_id:
          type: string
        product_id:
          type: string
        license_id:
          type: string
        license_key:
          type: string
          description: Raw generated license key. Returned only by the authorized completion retrieval flow.
        status:
          type: string
          enum: [active]
    PurchaseCompletionPageResponse:
      type: object
      required: [license_key, status]
      properties:
        product_name:
          type: string
        support_email:
          type: string
          format: email
        license_key:
          type: string
          description: Raw generated license key. Returned only after completion token authorization.
        status:
          type: string
          enum: [active]
        files:
          type: array
          items:
            $ref: "#/components/schemas/BuyerFile"
    BuyerFile:
      type: object
      required: [product_file_id, filename, content_type, size_bytes, scan_status]
      properties:
        product_file_id:
          type: string
        filename:
          type: string
        content_type:
          type: string
        size_bytes:
          type: integer
          format: int64
        verified_sha256_hex:
          type: string
        scan_status:
          type: string
        approved_at:
          type: string
          format: date-time
        uploaded_at:
          type: string
          format: date-time
    FileDownloadIntentRequest:
      type: object
      required: [completion_token]
      properties:
        completion_token:
          type: string
          description: Purchase completion token from the fragment-link completion flow. Do not send in URL query strings.
    FileDownloadIntentResponse:
      type: object
      required: [product_file_id, filename, content_type, size_bytes, method, download_url, expires_at]
      properties:
        product_file_id:
          type: string
        filename:
          type: string
        content_type:
          type: string
        size_bytes:
          type: integer
          format: int64
        verified_sha256_hex:
          type: string
        method:
          type: string
          enum: [GET]
        download_url:
          type: string
          format: uri
          description: Sensitive short-lived private download URL. Do not log or persist in shared notes.
        required_headers:
          type: object
          additionalProperties:
            type: string
        expires_at:
          type: string
          format: date-time
    Order:
      type: object
      required: [id, seller_id, product_id, status, amount_jpy, currency, platform_fee_jpy]
      properties:
        id:
          type: string
        seller_id:
          type: string
        product_id:
          type: string
        status:
          type: string
          enum: [pending_checkout, paid]
        amount_jpy:
          type: integer
        currency:
          type: string
          enum: [JPY]
        platform_fee_jpy:
          type: integer
        checkout_session_id:
          type: string
        payment_intent_id:
          type: string
        issued_license_id:
          type: string
    CodeBatch:
      type: object
      required: [id, seller_id, product_id, status, imported_count, duplicate_count, blank_count, error_count]
      properties:
        id:
          type: string
        seller_id:
          type: string
        product_id:
          type: string
        status:
          type: string
          enum: [processed, failed, needs_review]
        imported_count:
          type: integer
        duplicate_count:
          type: integer
        blank_count:
          type: integer
        error_count:
          type: integer
    LicenseIssuanceMode:
      type: string
      enum:
        - generated_server_validated
        - imported_csv
        - offline_signed_ed25519_future
      description: generated_server_validated is the default MVP path. imported_csv remains optional. offline_signed_ed25519_future is documented but not implemented.
    ProductFileStatus:
      type: string
      enum: [pending_upload, uploaded, pending_review, approved, rejected, disabled]
    ProductFileVisibility:
      type: string
      enum: [private, buyer_visible, public]
    ProductFileHashStatus:
      type: string
      enum: [unverified, verified, mismatch, failed]
    ProductFileScanStatus:
      type: string
      enum: [unscanned, pending, clean, suspicious, infected, failed, skipped]
    ProductFileScanTrustSource:
      type: string
      enum: [scanner, manual]
