openapi: 3.0.0
info:
  title: Fatoora Partner Signer
  version: "1.0"
  description: >
    Two-host API for Fatoora Partner Signer integrators (partner-signer / partner-signer-pro plans).
    Domain-whitelist and client-pre-registration management run on the Fatoora cloud
    (https://business.fatoora.tn). Session opening, local PKCS#11 signing, and optional direct TTN
    filing run on the FatooraSigner agent installed on the end client's own machine
    (http://127.0.0.1:38443 by default) — see the per-operation `servers` override on each path.
    Not extracted from a single generated source file; hand-authored from
    fatooraBusiness/src/routes/{signer-bridge,partner-signer}.routes.ts and
    fatooraSigner's tn.fatoora.signer.webbridge package. Cloud-side paths (domains/clients CRUD,
    session validation) were live-verified against a dev instance on 2026-07-11, which found and fixed
    a critical bug (domain whitelist writes crashed with 500 on every call) and flagged an unresolved
    CORS architecture gap — see the trust-signer-session path description. Local-agent paths
    (127.0.0.1:38443) remain code-derived; no agent instance was available to test against live.
  contact:
    name: Fatoora Support
    email: support@fatoora.tn
servers:
  - url: https://business.fatoora.tn
    description: Fatoora Cloud (production)

paths:
  /api/auth/login:
    post:
      summary: Authenticate the end client and obtain a short-lived JWT
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string }
                password: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  accessToken: { type: string }

  /api/partner/signer/domains:
    get:
      summary: List whitelisted domains for this partner app
      security: [{ bearerAuth: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  domains:
                    type: array
                    items:
                      type: object
                      properties: { id: { type: string }, domain: { type: string } }
                  total: { type: integer }
                  partnerId: { type: string }
    post:
      summary: Whitelist a domain
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain:
                  type: string
                  description: Bare hostname, e.g. "yourapp.example.com" (no scheme)
      responses:
        "201":
          description: Created
        "403":
          description: DOMAIN_LIMIT_REACHED
        "409":
          description: DOMAIN_ALREADY_EXISTS

  /api/partner/signer/domains/{domain}:
    delete:
      summary: Remove a whitelisted domain
      security: [{ bearerAuth: [] }]
      parameters:
        - name: domain
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: OK
        "404":
          description: DOMAIN_NOT_FOUND

  /api/partner/signer/clients:
    get:
      summary: List pre-registered client tax IDs
      security: [{ bearerAuth: [] }]
      parameters:
        - name: search
          in: query
          schema: { type: string }
        - name: active
          in: query
          schema: { type: string, enum: ["true", "false"] }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  clients:
                    type: array
                    items: { $ref: "#/components/schemas/SignerClient" }
                  total: { type: integer }
    post:
      summary: Pre-register (or reactivate) a client tax ID
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [taxId]
              properties:
                taxId: { type: string }
                clientName: { type: string }
      responses:
        "201":
          description: Created / reactivated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SignerClient" }
        "403":
          description: CLIENT_LIMIT_REACHED

  /api/partner/signer/clients/bulk:
    post:
      summary: Bulk pre-register client tax IDs
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [clients]
              properties:
                clients:
                  type: array
                  minItems: 1
                  maxItems: 200
                  items:
                    type: object
                    required: [taxId]
                    properties:
                      taxId: { type: string }
                      clientName: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  inserted: { type: integer }
                  updated: { type: integer }
                  errors:
                    type: array
                    items:
                      type: object
                      properties: { taxId: { type: string }, error: { type: string } }
        "403":
          description: CLIENT_LIMIT_REACHED

  /api/partner/signer/clients/{id}:
    patch:
      summary: Activate or deactivate a pre-registered client
      security: [{ bearerAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [isActive]
              properties: { isActive: { type: boolean } }
      responses:
        "200":
          description: OK
        "404":
          description: CLIENT_NOT_FOUND
    delete:
      summary: Remove a pre-registered client
      security: [{ bearerAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: OK
        "404":
          description: CLIENT_NOT_FOUND

  /api/auth/trust-signer-session:
    get:
      summary: Validate a signing session (called by the FatooraSigner agent, not directly by partners)
      description: >
        Not called directly by partner apps — the FatooraSigner local agent calls this on the
        partner's/client's behalf when opening a session. Documented here for completeness/troubleshooting.
        WARNING (confirmed live, 2026-07-11): this route sits behind fatooraBusiness's global Express
        cors() middleware, driven by the server's CORS_ORIGINS env var — a separate allowlist, unaware
        of the partner's self-service allowed_domains. An Origin not in CORS_ORIGINS is rejected with
        500 {"message":"Origin <url> not allowed by CORS"} BEFORE the allowed_domains check (which would
        otherwise return the documented 403 ORIGIN_NOT_WHITELISTED) ever runs. A partner's own domain
        whitelist entry is necessary but not sufficient in production; Fatoora staff must also add the
        domain to CORS_ORIGINS server-side.
        Validation order confirmed live: PARTNER_NOT_FOUND -> NOT_SIGNER_PARTNER -> PLAN_NOT_ELIGIBLE ->
        ORIGIN_NOT_WHITELISTED -> CLIENT_NOT_PRE_REGISTERED (a paused subscription surfaces
        PLAN_NOT_ELIGIBLE even with no domain/client configured).
      security: [{ bearerAuth: [] }]
      parameters:
        - name: X-Partner-App-ID
          in: header
          required: false
          schema: { type: string }
        - name: Origin
          in: header
          required: false
          schema: { type: string }
      responses:
        "500":
          description: >
            "Origin <url> not allowed by CORS" — global CORS rejection, confirmed live, occurs before
            any of the 403 checks below. Not represented in the response body schema (plain-text-ish
            JSON {message} from the Express cors() error handler, not the app's own ErrorResponse shape).
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SessionDescriptor" }
        "403":
          description: PARTNER_NOT_FOUND / NOT_SIGNER_PARTNER / PLAN_NOT_ELIGIBLE / ORIGIN_NOT_WHITELISTED / CLIENT_NOT_PRE_REGISTERED

  /api/web-bridge/v1/session:
    post:
      summary: Open a local signing session
      servers:
        - url: http://127.0.0.1:38443
          description: FatooraSigner local agent (default port; user-configurable)
      security: [{ bearerAuth: [] }]
      parameters:
        - name: X-Partner-App-ID
          in: header
          required: false
          schema: { type: string }
      requestBody:
        required: false
      responses:
        "200":
          description: Session opened
          content:
            application/json:
              schema:
                type: object
                properties:
                  sessionId: { type: string }
                  idleExpiresAt: { type: string, format: date-time }
                  absoluteExpiresAt: { type: string, format: date-time, nullable: true }
                  partnerMode: { type: boolean }
                  partnerAppId: { type: string }
        "401":
          description: Invalid or expired token
        "403":
          description: Forwarded cloud error code (ORIGIN_NOT_WHITELISTED, CLIENT_NOT_PRE_REGISTERED, PLAN_NOT_ELIGIBLE, ...)
        "502":
          description: Cloud unreachable
    delete:
      summary: Close a signing session
      servers:
        - url: http://127.0.0.1:38443
          description: FatooraSigner local agent
      security: [{ sessionAuth: [] }]
      responses:
        "204":
          description: Session closed

  /api/web-bridge/v1/tokens/certificates:
    get:
      summary: List public certificates on the connected PKCS#11 token (no PIN required)
      servers:
        - url: http://127.0.0.1:38443
          description: FatooraSigner local agent
      security: [{ sessionAuth: [] }]
      parameters:
        - name: driverId
          in: query
          required: false
          schema: { type: string }
      responses:
        "200":
          description: OK

  /api/web-bridge/v1/tokens/detect-with-certificates:
    post:
      summary: Detect PKCS#11 tokens and read certificate details (PIN required)
      servers:
        - url: http://127.0.0.1:38443
          description: FatooraSigner local agent
      security: [{ sessionAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                pin: { type: string }
                encryptedPin: { type: string }
                driverId: { type: string, nullable: true }
      responses:
        "200":
          description: OK

  /api/web-bridge/v1/xml/sign:
    post:
      summary: Sign a TEIF XML document with the client's local PKCS#11 token
      servers:
        - url: http://127.0.0.1:38443
          description: FatooraSigner local agent
      security: [{ sessionAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SignRequest" }
      responses:
        "200":
          description: >
            Always 200 unless the native PKCS#11 library fails to load — check the `ok` and
            `errorCode` fields for business-level success/failure.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SignResponse" }
        "500":
          description: Native library load failure (LinkageError / ClassNotFoundException)

  /api/web-bridge/v1/xml/inspect:
    post:
      summary: Inspect / verify a signed TEIF XML document
      servers:
        - url: http://127.0.0.1:38443
          description: FatooraSigner local agent
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [xml]
              properties:
                xml: { type: string }
                revocationOnline: { type: boolean, default: true }
                normalizeTtnReturnedXmlForDss: { type: boolean, default: false }
      responses:
        "200":
          description: DSS validation details

  /api/web-bridge/v1/credentials/encrypt-pin:
    post:
      summary: Encrypt a PKCS#11 PIN, session-bound, in-memory only
      servers:
        - url: http://127.0.0.1:38443
          description: FatooraSigner local agent
      security: [{ sessionAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [pin]
              properties: { pin: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties: { encryptedPin: { type: string } }

  /api/web-bridge/v1/credentials/encrypt-ttn:
    post:
      summary: Encrypt TTN (TradeNet) credentials, session-bound, in-memory only
      servers:
        - url: http://127.0.0.1:38443
          description: FatooraSigner local agent
      security: [{ sessionAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [login, password, matricule]
              properties:
                login: { type: string }
                password: { type: string }
                matricule: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties: { encryptedCredential: { type: string } }

  /api/web-bridge/v1/ttn/check-availability:
    get:
      summary: Check TTN availability and latency
      servers:
        - url: http://127.0.0.1:38443
          description: FatooraSigner local agent
      security: [{ sessionAuth: [] }]
      parameters:
        - name: ttnMode
          in: query
          schema: { type: string, enum: [TEST, PROD] }
      responses:
        "200":
          description: OK

  /api/web-bridge/v1/ttn/save-efact:
    post:
      summary: Submit a signed invoice directly to TTN
      servers:
        - url: http://127.0.0.1:38443
          description: FatooraSigner local agent
      security: [{ sessionAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [encryptedCredential, documentEfact]
              properties:
                encryptedCredential: { type: string }
                documentEfact: { type: string, description: "Base64-encoded signed TEIF XML" }
                ttnMode: { type: string, enum: [TEST, PROD], default: PROD }
                ttnUrl: { type: string, nullable: true }
      responses:
        "200":
          description: TTN submission result

  /api/web-bridge/v1/ttn/consult-efact:
    post:
      summary: Pull received invoices from TTN
      servers:
        - url: http://127.0.0.1:38443
          description: FatooraSigner local agent
      security: [{ sessionAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [encryptedCredential, efactCriteria]
              properties:
                encryptedCredential: { type: string }
                efactCriteria: { type: object }
                ttnMode: { type: string, enum: [TEST, PROD], default: PROD }
      responses:
        "200":
          description: Received invoices list

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: End-client JWT from POST /api/auth/login
    sessionAuth:
      type: apiKey
      in: header
      name: X-Fatoora-Signer-Session
      description: Session id returned by POST /api/web-bridge/v1/session

  schemas:
    SessionDescriptor:
      type: object
      properties:
        valid: { type: boolean }
        sub: { type: string }
        userId: { type: string }
        orgId: { type: string }
        roles: { type: array, items: { type: string } }
        scopes: { type: array, items: { type: string } }
        partnerMode: { type: boolean }
        partnerAppId: { type: string }

    SignerClient:
      type: object
      properties:
        id: { type: string }
        taxId: { type: string }
        clientName: { type: string, nullable: true }
        isActive: { type: boolean }
        createdAt: { type: string, format: date-time }

    SignRequest:
      type: object
      required: [xml]
      properties:
        xml: { type: string }
        pin: { type: string }
        encryptedPin: { type: string }
        pkcs11Path: { type: string }
        slotId: { type: integer }
        keyAlias: { type: string }
        sha1: { type: string }
        subjectContains: { type: string }
        includeChain: { type: boolean }
        signerRole: { type: string }

    SignResponse:
      type: object
      properties:
        mode: { type: string, example: sign }
        ok: { type: boolean }
        signedXml: { type: string, nullable: true }
        signatureLevel: { type: string, nullable: true }
        error: { type: string, nullable: true }
        errorCode: { type: string, nullable: true, example: SIGN_FAILED }
