openapi: 3.1.0
info:
  title: secureFlows Session API
  version: 1.3.3
  description: |
    **Session-only** surface: create an encrypted server-side session with a **Firebase ID token**,
    then read/write a JSON **payload** using a **`sessionToken`** JWT (`tokenType=SESSION`).

    ### Error responses
    Failures return JSON **`{"status": <code>, "error": "<message>"}`** (unless noted otherwise).

    ### Firebase email (workspace audit display)
    On **`POST /sessions`**, **`POST /sessions/get-or-create`**, and **`POST /sessions/renew/{sessionToken}`**,
    the server reads **`email`** from the verified Firebase ID token (when present) and **best-effort**
    stores it on the workspace end-user row. This does not change auth semantics; it lets workspace
    audit UIs show email instead of Firebase UID. No extra request fields — use a Firebase token that
    includes an email claim (typical email/password and many OAuth providers).

    ### Intended use
    - Tagged **`ai-safe`**: automation / agents should use **only** these paths, not console or integration flows.
    - Sessions are **scoped by workspace name** and Firebase UID at creation time.

    ### Critical integration rules (avoid common bugs)
    - Store `sessionToken` in **memory only** (never `localStorage` / durable browser storage).
    - Never send or trust client-provided identity (`userId`, `workspaceId`, `tenantId`) in request bodies.
    - **`410` means token expired, data intact**. Do not treat as data loss; obtain a fresh token via hosted login / renew.
    - `GET /api/v1/sessions` returns a **flat JSON object** (empty payload is `{}`).

    ### Token cheat sheet
    | Operation | Header |
    |-----------|--------|
    | `POST /api/v1/sessions` | `Authorization: Bearer <firebase-id-token>` |
    | `POST /api/v1/sessions/get-or-create` | `Authorization: Bearer <firebase-id-token>` |
    | `POST /api/v1/sessions/renew/{sessionToken}` | `Authorization: Bearer <firebase-id-token>` (path: previous session JWT, may be expired; URL-encode) |
    | `GET/POST/DELETE` on `/api/v1/sessions/...` (except renew) | `Authorization: Bearer <sessionToken>` |

    ### Example (curl)
    ```bash
    # 1) Create session
    curl -sS -X POST "http://<host>/api/v1/sessions" \\
      -H "Authorization: Bearer $FIREBASE_JWT" \\
      -H "Content-Type: application/json" \\
      -d '{"workspaceName":"demo-ws","payload":{"hello":"world"}}'

    # 2) Use sessionToken from response
    curl -sS "http://<host>/api/v1/sessions" \\
      -H "Authorization: Bearer $SESSION_JWT"
    ```

externalDocs:
  description: API documentation hub
  url: ../index.html

tags:
  - name: ai-safe
    description: Session lifecycle and payload keys (automation-safe subset)

paths:
  /api/v1/sessions:
    post:
      summary: Create a new session
      description: |
        Verifies **Firebase** ID token, creates a session for **`workspaceName`**, stores serialized
        **`payload`** (defaults to `{}` when omitted), and returns **`sessionToken`** (JWT; subject = internal session id).
        Default server-side TTL is **1 hour** (implementation detail).

        If the Firebase token includes **`email`**, the server best-effort persists it on the workspace
        end-user row (for audit display). Prefer **`POST /sessions/get-or-create`** for hosted-login-style
        flows that should reuse an active session.
      tags: [ai-safe]
      security:
        - firebaseBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SessionRequest'
            examples:
              emptyPayload:
                summary: No payload field (stored as {})
                value:
                  workspaceName: demo-workspace
                  app_id: acme-web
              minimal:
                summary: Simple payload
                value:
                  workspaceName: demo-workspace
                  app_id: acme-web
                  payload:
                    hello: world
                    from: safehook demo
      responses:
        "200":
          description: Session created successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateSessionResponse'
              examples:
                ok:
                  value:
                    sessionToken: eyJhbGciOi...SESSION
        "401":
          description: Missing or invalid Firebase Authorization header.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
              examples:
                missing:
                  value:
                    status: 401
                    error: Missing Authorization header
                invalid:
                  value:
                    status: 401
                    error: Invalid Firebase token
        "404":
          description: Workspace name unknown (if applicable)

    get:
      summary: Get the current session payload
      description: |
        Returns the decrypted session payload for the authenticated internal session token.

        Response shape is a **flat JSON object**. Empty payload returns `{}`.
      tags: [ai-safe]
      security:
        - sessionBearerAuth: []
      responses:
        "200":
          description: Session payload returned.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionPayloadObject'
              example:
                hello: world
                counter: 1
        "401":
          description: Missing or invalid internal session token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
              example:
                status: 401
                error: Missing Authorization header
        "410":
          description: |
            Session token expired (HTTP 410 ≠ data loss).

            The underlying session payload may still exist; obtain a fresh token (hosted login renewal / renew flow).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
              example:
                status: 410
                error: Session expired

  /api/v1/sessions/get-or-create:
    post:
      summary: Reuse active session or create one
      description: |
        Verifies **Firebase** ID token. If an **active** session already exists for
        **`(workspaceName, Firebase UID, app_id)`**, returns a new **`sessionToken`** JWT for the **most
        recently created** matching row (touches activity; does not create a duplicate session). The request
        **`payload` is ignored on reuse** — it is applied only when a new session row is created. Prefer a
        dedicated `app_id` per integration surface, or revoke old sessions, if you need a fresh payload.

        Otherwise behaves like **`POST /sessions`** (new row + default **1 hour** TTL).

        If the Firebase token includes **`email`**, the server best-effort persists or backfills it on the
        workspace end-user row (including when reusing an existing session). Intended for integrators that
        mirror hosted login session reuse.
      tags: [ai-safe]
      security:
        - firebaseBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SessionRequest'
      responses:
        "200":
          description: Session token for an existing or newly created session.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateSessionResponse'
        "401":
          description: Missing or invalid Firebase Authorization header.
        "404":
          description: Workspace name unknown (if applicable)

  /api/v1/sessions/renew/{sessionToken}:
    post:
      summary: Renew session JWT (replace expired token)
      description: |
        Verifies **Firebase** ID token. Parses **`sessionToken`** path segment as an internal SESSION JWT **without
        enforcing JWT expiry** (signature and `tokenType=SESSION` are still validated). Loads the
        session by id from the token subject; the Firebase UID must match the session owner.
        Increments **`tokenRevision`** on the server so **previous session JWTs** (same session id,
        older revision) are no longer accepted for `GET/POST/DELETE /sessions/...`.
        Returns a **new `sessionToken`** (with the new `tokenRevision` claim) and the current decrypted
        **payload**; extends server-side session expiry by **1 hour**.

        If the Firebase token includes **`email`**, the server best-effort backfills it on the session owner
        when the user row has no email yet (audit display only).

        Use the path form `POST /api/v1/sessions/renew/{sessionToken}` — **URL-encode** the JWT (e.g. `encodeURIComponent` in JS).
      tags: [ai-optional]
      security:
        - firebaseBearerAuth: []
      parameters:
        - name: sessionToken
          in: path
          required: true
          schema:
            type: string
          description: Previous SESSION JWT (may be expired); URL-encoded in the path
      responses:
        "200":
          description: New session token and payload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionRenewResponse'
        "400":
          description: Empty session token path segment (if applicable).
        "401":
          description: Missing Authorization, invalid Firebase token, or invalid session token (signature / shape).
        "403":
          description: Firebase user does not own this session.
        "404":
          description: Session id not found.
        "410":
          description: Session revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
              example:
                status: 410
                error: Session revoked

  /api/v1/sessions/set/{key}:
    post:
      summary: Set a value inside the session payload
      description: >
        Sets a key in the encrypted session payload and returns the updated decrypted payload.
        If the JSON body is `{ "value": <x> }`, the server unwraps it and stores `<x>` directly.
      tags: [ai-safe]
      security:
        - sessionBearerAuth: []
      parameters:
        - name: key
          in: path
          required: true
          schema:
            type: string
          description: Payload key to set
          example: hello
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SetSessionValueRequest'
            examples:
              wrapped:
                summary: Preferred wrapper format
                value:
                  value: new-value
              raw:
                summary: Raw value (server stores it as-is)
                value: true
      responses:
        "200":
          description: Updated session payload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionPayloadObject'
              example:
                hello: world
                newKey: new-value
        "401":
          description: Missing or invalid internal session token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
              example:
                status: 401
                error: Missing Authorization header

  /api/v1/sessions/get/{key}:
    get:
      summary: Get a single value from the session payload
      description: |
        Retrieves the decrypted payload value for `key`.

        **Important:** `404` means the key was never written (normal first-use case). Do not treat as an error.
      tags: [ai-safe]
      security:
        - sessionBearerAuth: []
      parameters:
        - name: key
          in: path
          required: true
          schema:
            type: string
          description: Payload key to read
          example: hello
      responses:
        "200":
          description: Key/value returned.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetSessionValueResponse'
              example:
                key: hello
                value: world
        "401":
          description: Missing or invalid internal session token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
              example:
                status: 401
                error: Missing Authorization header
        "404":
          description: Key not found (normal if never written yet).
        "410":
          description: |
            Session token expired (HTTP 410 ≠ data loss).

            The underlying session payload may still exist; obtain a fresh token (hosted login renewal / renew flow).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
              example:
                status: 410
                error: Session expired

  /api/v1/sessions/delete/{key}:
    delete:
      summary: Delete a key from the session payload
      description: >
        Removes `key` from the session payload and returns `true` if the key existed.
      tags: [ai-safe]
      security:
        - sessionBearerAuth: []
      parameters:
        - name: key
          in: path
          required: true
          schema:
            type: string
          description: Payload key to delete
          example: hello
      responses:
        "200":
          description: Deletion result.
          content:
            application/json:
              schema:
                type: boolean
              example: true
        "401":
          description: Missing or invalid internal session token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
              example:
                status: 401
                error: Missing Authorization header
        "410":
          description: |
            Session token expired (HTTP 410 ≠ data loss).

            The underlying session payload may still exist; obtain a fresh token (hosted login renewal / renew flow).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
              example:
                status: 410
                error: Session expired

  /api/v1/sessions/revoke:
    post:
      summary: Revoke the current session (by session token)
      description: >
        Revokes the session referenced by the internal SESSION Bearer token. This endpoint is useful
        for automation clients that only hold a session token and want to revoke it cleanly.
      tags: [ai-safe]
      security:
        - sessionBearerAuth: []
      responses:
        "200":
          description: Session revoked.
          content:
            application/json:
              schema:
                type: object
                properties:
                  revoked:
                    type: boolean
                    example: true
                  sessionId:
                    type: integer
                    format: int64
                    example: 123
        "401":
          description: Missing or invalid internal session token.
        "404":
          description: Session not found.
        "410":
          description: Session revoked or token superseded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
              example:
                status: 410
                error: Session revoked

components:
  securitySchemes:
    firebaseBearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Firebase ID token (`POST /sessions`, `POST /sessions/get-or-create`, `POST /sessions/renew/{sessionToken}`).
        When the token includes `email`, the server best-effort stores it on the workspace end-user row.
    sessionBearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Session JWT from `POST /sessions` or `POST /sessions/renew/{sessionToken}` (`tokenType=SESSION`; subject = session id;
        `tokenRevision` claim must match the server; superseded after renew)

  schemas:
    ApiError:
      type: object
      required: [status, error]
      properties:
        status:
          type: integer
          description: HTTP status code
        error:
          type: string
          description: Human-readable error message

    SessionRequest:
      type: object
      required: [workspaceName]
      properties:
        workspaceName:
          type: string
          description: Workspace name to scope the session
          example: demo-workspace
        app_id:
          type: string
          nullable: true
          description: Optional app id (must belong to the workspace). Used for per-app session policy and auditing.
          example: acme-web
        payload:
          type: object
          additionalProperties: true
          default: {}
          description: Arbitrary payload object stored in the session. Optional; omitted or null is stored as `{}`.
          example:
            hello: world
            from: safehook demo

    CreateSessionResponse:
      type: object
      required: [sessionToken]
      properties:
        sessionToken:
          type: string
          description: Internal JWT session token (tokenType=SESSION)
          example: eyJhbGciOi...SESSION

    SessionRenewResponse:
      type: object
      required: [sessionToken, payload]
      properties:
        sessionToken:
          type: string
          description: New internal JWT session token (tokenType=SESSION)
        payload:
          $ref: '#/components/schemas/SessionPayloadObject'

    SessionPayloadObject:
      type: object
      additionalProperties: true
      description: Decrypted key/value payload object
      example:
        hello: world
        counter: 1

    SetSessionValueRequest:
      description: >
        JSON body can be either `{ "value": <x> }` (preferred) or a raw JSON value.
      oneOf:
        - type: object
          required: [value]
          additionalProperties: false
          properties:
            value: {}
        - type: string
        - type: number
        - type: integer
        - type: boolean
        - type: array
        - type: object
          additionalProperties: true
        - type: 'null'

    GetSessionValueResponse:
      type: object
      required: [key, value]
      properties:
        key:
          type: string
          example: hello
        value:
          description: Stored payload value for the requested key
          example: world

