openapi: 3.0.3
info:
  title: Kenshiki Pulse Bonded Sessions API
  version: 0.1.0
  description: >
    Pulse bonds a high-risk browser workflow to the applicant's carried phone.
    The browser is a presentation surface, not the root of trust. The server
    must verify a bonded session before accepting a protected action.
servers:
  - url: https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/api/v1
    description: Kenshiki Pulse Worker production API
security:
  - bearerAuth: []
tags:
  - name: Sessions
  - name: Attestations
paths:
  /sessions:
    post:
      tags: [Sessions]
      summary: Create a pending bonded session
      description: >
        Creates a short-lived pending session. Production integrations should call
        this from the customer's backend with a server credential, store
        completion_secret server-side, and return only the public session object
        to the browser. The hosted embed renders the QR or mobile handoff from
        that public session, opens the WebSocket, and waits for the carried phone
        to bond the session.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSessionRequest"
      responses:
        "201":
          description: Pending session created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BondedSession"
        "401":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
        "429":
          $ref: "#/components/responses/Error"
  /sessions/{session_id}:
    get:
      tags: [Sessions]
      summary: Retrieve a bonded session
      description: >
        Server-side integrations must call this endpoint before accepting a gated
        submit. Trust only a fresh bonded session scoped to the expected workflow
        and action.
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: Session state and bounded verification summary
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BondedSession"
        "404":
          $ref: "#/components/responses/Error"
    delete:
      tags: [Sessions]
      summary: Kill a session
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "204":
          description: Session killed
        "404":
          $ref: "#/components/responses/Error"
  /sessions/{session_id}/form-completed:
    post:
      tags: [Sessions]
      summary: Complete the action and close the bond (bonded → completed)
      description: >
        Call once the protected action has durably succeeded. Moves the session
        from bonded to completed and flips the carried phone's Pulse drawer to
        success. A bond is for one action; if you never complete it, it simply
        expires on its TTL. Requires the per-session completion secret returned
        by POST /sessions (X-Pulse-Completion-Secret) — a party holding only the
        session_id cannot complete. In production, the customer's backend creates
        the session and keeps this secret server-side.
      parameters:
        - $ref: "#/components/parameters/SessionId"
        - name: X-Pulse-Completion-Secret
          in: header
          required: true
          description: The completion_secret from the session create response.
          schema:
            type: string
      responses:
        "200":
          description: Action completed; session moved to completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  session_id:
                    type: string
                    example: sess_9f2c
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
  /attestations:
    post:
      tags: [Attestations]
      summary: Submit a bonded pair assertion
      description: >
        Called by the Pulse mobile app after it evaluates continuity of life and
        signs the session assertion. Customer websites should not call this from
        the browser.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BondedPairAssertion"
      responses:
        "200":
          description: Session bonded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BondedSession"
        "400":
          $ref: "#/components/responses/Error"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: server beta credential
  parameters:
    SessionId:
      name: session_id
      in: path
      required: true
      schema:
        type: string
        example: sess_9f2c
  responses:
    Error:
      description: Typed error response
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
  schemas:
    CreateSessionRequest:
      type: object
      properties:
        workflow:
          type: string
          example: credit_application
        action:
          type: string
          example: submit_application
    BondedSession:
      type: object
      required:
        - id
        - state
        - nonce
        - api_base_url
        - qr_payload
        - universal_link
        - challenge_expires_at
        - expires_at
        - verification
      properties:
        id:
          type: string
          example: sess_9f2c
        state:
          type: string
          enum: [pending, bonded, step_up_required, completed, killed, expired]
        nonce:
          type: string
          example: nonce_abc
        qr_payload:
          type: string
          example: pulse://bond?session_id=sess_9f2c&nonce=nonce_abc
        qr_svg_url:
          type: string
          example: /v1/sessions/sess_9f2c/qr.svg?nonce=nonce_abc
        api_base_url:
          type: string
          description: Worker origin that minted this session. The phone app and browser polling must use this same origin.
          example: https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev
        universal_link:
          type: string
          example: https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/bond?session_id=sess_9f2c&nonce=nonce_abc
        websocket_url:
          type: string
          example: wss://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/sessions/sess_9f2c
        completion_secret:
          type: string
          nullable: true
          description: >
            Per-session capability to complete the action (close the bond),
            returned only on create. In a server-authoritative flow keep it
            server-side; never expose it to untrusted parties or the browser.
          example: cs_secret_abc
        challenge_expires_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
        workflow:
          type: string
          nullable: true
        action:
          type: string
          nullable: true
        verification:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/PulseVerification"
    PulseVerification:
      type: object
      description: >
        Bounded proof summary. It is not raw sensor history, location history,
        or identity-provider internals.
      properties:
        device_possession:
          type: boolean
        local_auth:
          type: string
          enum: [verified, failed]
        carrier:
          type: object
          properties:
            number_verified:
              type: boolean
            sim_swap_recent:
              type: boolean
            device_status:
              type: string
              enum: [active, inactive, unknown]
        sensor_continuity:
          type: object
          properties:
            motion_present:
              type: boolean
            continuity_score:
              type: number
              minimum: 0
              maximum: 1
        passport:
          type: object
          description: Optional workflow-scoped passport evidence. Passport is not globally required for every Pulse user.
          properties:
            tier:
              type: string
              enum: [none, document, chip]
    BondedPairAssertion:
      type: object
      required: [session_id, nonce, device_id, subject_id, issued_at, app_attest, verified_evidence]
      properties:
        session_id:
          type: string
        nonce:
          type: string
        device_id:
          type: string
        subject_id:
          type: string
        issued_at:
          type: string
          format: date-time
        app_attest:
          type: object
          required: [key_id, client_data_hash, authenticator_data, signature]
          properties:
            key_id:
              type: string
            client_data_hash:
              type: string
              description: Base64url SHA-256 hash over the Pulse bond challenge payload.
            authenticator_data:
              type: string
              description: Base64url App Attest authenticator data.
            signature:
              type: string
              description: Base64url ECDSA P-256 signature from the registered App Attest key.
        verified_evidence:
          type: object
          required: [issued_at, expires_at, local_auth, carrier, sensor_continuity, signature]
          description: Server-signed evidence summary. Client self-asserted evidence is not accepted in production.
          properties:
            issued_at:
              type: string
              format: date-time
            expires_at:
              type: string
              format: date-time
            local_auth:
              type: object
            carrier:
              type: object
            sensor_continuity:
              type: object
            signature:
              type: string
              description: HMAC over the canonical evidence payload.
    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              example: invalid_request
            message:
              type: string
            requestId:
              type: string
