openapi: 3.0.3
info:
  title: itjeom Partner API
  version: 0.4.0
  description: |
    **Live** Partner API for third-party apps building against itjeom central shop/board/member data.

    - Base: `https://api.itjeom.com/partner/v1`
    - Auth: OAuth2-style token endpoint — `client_credentials` and `refresh_token` only (no authorize redirect).
    - Credentials: hub member applies via developers console → operator approves → `client_id` / `client_secret` (server-side only).
    - Scopes: see `GET /meta/scopes` (`live` = requestable now; `planned` = roadmap only).
    - **Members:** D1 non-PII only (`user_level`, schema, referral, `local_member_id` link). No email/password/name/status. Not hub `/itjeom-hub` users.

    **Do not use** internal `/shop/*` engine routes, storefront session cookies, or **admin JWT** / `site_hash` flows from third-party apps. Use this surface only.
servers:
  - url: https://api.itjeom.com/partner/v1
    description: Production Partner API
tags:
  - name: auth
  - name: meta
  - name: shop
  - name: board
  - name: members
paths:
  /meta/health:
    get:
      tags: [meta]
      summary: Partner surface health
      operationId: partnerMetaHealth
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  version: { type: string, example: '0.3.0' }
  /meta/scopes:
    get:
      tags: [meta]
      summary: OAuth scope definition list
      operationId: partnerMetaScopes
      description: |
        Permission names for Partner API (not a product catalog).
        `status=live` can be requested on app apply; `planned` is documented but not grantable yet.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  version: { type: string }
                  scopes:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, example: shop:read }
                        group: { type: string, example: shop }
                        label_ko: { type: string }
                        status: { type: string, enum: [live, planned] }
                        requestable: { type: boolean }
  /oauth/token:
    post:
      tags: [auth]
      summary: Issue or refresh access token
      operationId: partnerOAuthToken
      description: |
        Supports `grant_type=client_credentials` or `grant_type=refresh_token`.
        Body may be JSON or `application/x-www-form-urlencoded`.
        Optional HTTP Basic auth with `client_id:client_secret` instead of body fields.
      security:
        - basicAuth: []
        - {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TokenRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/TokenRequest'
      responses:
        '200':
          description: Token issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /shop/products:
    get:
      tags: [shop]
      summary: List products
      operationId: partnerShopProductList
      security:
        - bearerAuth: []
      parameters:
        - name: page
          in: query
          schema: { type: integer, minimum: 1, default: 1 }
        - name: per_page
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
        - name: q
          in: query
          schema: { type: string }
          description: Optional search query
      responses:
        '200':
          description: Product list page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /shop/products/{uniq}:
    get:
      tags: [shop]
      summary: Get product by uniq
      operationId: partnerShopProductGet
      security:
        - bearerAuth: []
      parameters:
        - name: uniq
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Product
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Product'
        '404':
          description: Not found
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /shop/orders:
    post:
      tags: [shop]
      summary: Create order
      description: Requires `shop:order:write`. Payment completion may be webhook-driven.
      operationId: partnerShopOrderCreate
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderCreateRequest'
      responses:
        '201':
          description: Order created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /board/posts:
    get:
      tags: [board]
      summary: List board posts
      operationId: partnerBoardPostList
      security:
        - bearerAuth: []
      parameters:
        - name: board
          in: query
          required: true
          schema: { type: string }
          description: Board slug/name
        - name: page
          in: query
          schema: { type: integer, minimum: 1, default: 1 }
        - name: per_page
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
      responses:
        '200':
          description: Post list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /board/posts/{uniq}:
    get:
      tags: [board]
      summary: Get board post by uniq
      operationId: partnerBoardPostGet
      security:
        - bearerAuth: []
      parameters:
        - name: uniq
          in: path
          required: true
          schema: { type: string }
        - name: board
          in: query
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Post
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Post'
        '404':
          description: Not found
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /members:
    get:
      tags: [members]
      summary: List tenant members (D1 non-PII)
      operationId: partnerMemberList
      security:
        - bearerAuth: []
      description: Requires member:read. No email/password/name/status.
      parameters:
        - name: page
          in: query
          schema: { type: integer, minimum: 1, default: 1 }
        - name: per_page
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
      responses:
        '200':
          description: Member list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MemberListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags: [members]
      summary: Create D1 member shadow row
      operationId: partnerMemberCreate
      security:
        - bearerAuth: []
      description: Requires member:write. Creates non-PII shadow only.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                user_level: { type: string, example: '0' }
                signup_schema_slug: { type: string, example: default }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Member'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /members/meta:
    get:
      tags: [members]
      summary: Batch member meta by ids
      operationId: partnerMemberMeta
      security:
        - bearerAuth: []
      parameters:
        - name: ids
          in: query
          required: true
          schema: { type: string }
          description: Comma-separated D1 tenant_member ids (max 200)
      responses:
        '200':
          description: Meta items
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /members/auth/token:
    post:
      tags: [members]
      summary: Mint hybrid member JWT (delegated auth)
      operationId: partnerMemberAuthToken
      security:
        - bearerAuth: []
      description: |
        Requires member:auth. Partner must have already verified the end-user.
        Needs linked local_member_id. Emits tenant JWT (member_store=hybrid), not hub users.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                local_member_id: { type: integer }
                id: { type: integer, description: D1 tenant_member_id (alt) }
      responses:
        '200':
          description: Token minted
        '403':
          description: hybrid_link_missing or scope denied
        '401':
          $ref: '#/components/responses/Unauthorized'
  /members/{id}:
    get:
      tags: [members]
      summary: Get member by D1 id
      operationId: partnerMemberGet
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Member'
        '404':
          description: Not found
    patch:
      tags: [members]
      summary: Update member meta (user_level, signup_schema_slug)
      operationId: partnerMemberPatch
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                user_level: { type: string }
                signup_schema_slug: { type: string }
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Member'
  /members/{id}/link:
    post:
      tags: [members]
      summary: Link local_member_id to D1 shadow
      operationId: partnerMemberLink
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [local_member_id]
              properties:
                local_member_id: { type: integer }
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Member'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Partner access_token from /oauth/token
    basicAuth:
      type: http
      scheme: basic
      description: client_id as username, client_secret as password (token endpoint only)
  responses:
    Unauthorized:
      description: Missing or invalid token / credentials
    Forbidden:
      description: Token valid but scope or site denied
  schemas:
    TokenRequest:
      type: object
      properties:
        grant_type:
          type: string
          enum: [client_credentials, refresh_token]
        client_id:
          type: string
        client_secret:
          type: string
        refresh_token:
          type: string
        scope:
          type: string
          description: >
            Space-separated live scopes for client_credentials
            (shop:read, board:read, shop:order:write, member:read, member:write, member:auth).
            Full list: GET /meta/scopes.
    TokenResponse:
      type: object
      required: [access_token, token_type, expires_in]
      properties:
        access_token: { type: string }
        token_type: { type: string, example: Bearer }
        expires_in: { type: integer }
        refresh_token: { type: string }
        scope: { type: string }
    Product:
      type: object
      properties:
        uniq: { type: string }
        title: { type: string }
        price: { type: number }
        currency: { type: string, example: KRW }
        status: { type: string }
        thumbnail_url: { type: string, format: uri }
    ProductListResponse:
      type: object
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Product' }
        page: { type: integer }
        per_page: { type: integer }
        total: { type: integer }
    Post:
      type: object
      properties:
        uniq: { type: string }
        title: { type: string }
        status: { type: string }
        board: { type: string }
        created_at: { type: string, format: date-time }
        thumbnail_url: { type: string, format: uri }
    PostListResponse:
      type: object
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Post' }
        page: { type: integer }
        per_page: { type: integer }
        total: { type: integer }
    OrderCreateRequest:
      type: object
      required: [lines]
      properties:
        lines:
          type: array
          items:
            type: object
            required: [product_uniq, qty]
            properties:
              product_uniq: { type: string }
              qty: { type: integer, minimum: 1 }
        buyer:
          type: object
          properties:
            email: { type: string, format: email }
            name: { type: string }
    Order:
      type: object
      properties:
        uniq: { type: string }
        status: { type: string }
        total: { type: number }
    Member:
      type: object
      description: D1 non-PII tenant member (no email/password/name/status)
      properties:
        id: { type: integer, description: D1 itjeom_tenant_member_id }
        local_member_id: { type: integer, nullable: true }
        user_level: { type: string }
        signup_schema_slug: { type: string }
        referral_code: { type: string, nullable: true }
        referrer_type: { type: string, nullable: true }
        referrer_ref: { type: integer, nullable: true }
        created_at: { type: string, nullable: true }
        updated_at: { type: string, nullable: true }
    MemberListResponse:
      type: object
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Member' }
        page: { type: integer }
        per_page: { type: integer }
        total: { type: integer }
