openapi: "3.1.0"

info:
  title: Lawenots Platform API
  version: "1.0.0"
  description: |
    Public API surface for the Lawenots platform — bookkeeping, allocation intelligence,
    and the Flywheel framework.

    **Authentication**

    Most workspace endpoints require a Supabase session cookie (established via `/login`).
    The MCP server uses short-lived Bearer tokens issued by the OAuth flow at
    `/api/mcp/oauth/authorize`. Public endpoints (pricing, contact) require no auth.

    **Rate limits**

    - `/api/public/*`: 120 requests / minute per IP
    - `/api/mcp`: 30 requests / minute per IP
    - `/api/contact`: 20 requests / minute per IP

  contact:
    url: https://www.lawenots.com/request-access
  license:
    name: Private — field-of-use license
    url: https://www.lawenots.com/terms

servers:
  - url: https://www.lawenots.com
    description: Production
  - url: http://localhost:3000
    description: Local development

tags:
  - name: Public
    description: Unauthenticated endpoints — no session or token required
  - name: MCP
    description: Model Context Protocol server for Claude connector integration
  - name: MCP OAuth
    description: OAuth 2.0 flow for the Claude connector marketplace
  - name: Stripe
    description: Subscription checkout (requires Supabase session)
  - name: Contact
    description: Lead capture and access requests

# ── Reusable components ────────────────────────────────────────────────────────

components:
  securitySchemes:
    supabaseSession:
      type: apiKey
      in: cookie
      name: sb-access-token
      description: Supabase session cookie established after login.

    mcpBearer:
      type: http
      scheme: bearer
      bearerFormat: LAWENOTS-MCP-TOKEN
      description: |
        Short-lived HMAC-SHA256 signed token issued by `/api/mcp/oauth/token`.
        Format: `base64url(payload).hmac_sha256_hex`. TTL: 1 hour.

  schemas:

    LawenotsService:
      type: object
      required: [service_key, display_name, category, pricing_model, price_usd, is_active]
      properties:
        service_key:     { type: string, example: intelligence_annual }
        display_name:    { type: string, example: "Lawenots Intelligence" }
        description:     { type: string, nullable: true }
        category:
          type: string
          enum: [core, subscription, addon, upgrade, overage]
        pricing_model:
          type: string
          enum: [one_time, annual, monthly, per_event]
        price_usd:       { type: number, format: float, example: 229.0 }
        is_active:       { type: boolean }

    ContactRequest:
      type: object
      required: [name, email]
      properties:
        name:     { type: string, maxLength: 120, example: "Alex Rivera" }
        email:    { type: string, format: email, example: "alex@company.com" }
        company:  { type: string, maxLength: 120, nullable: true }
        message:  { type: string, maxLength: 2000, nullable: true }
        interest:
          type: string
          nullable: true
          enum: [allocation, books, flywheel, crypto, governance, firm]

    CheckoutRequest:
      type: object
      required: [service_key]
      properties:
        service_key:
          type: string
          example: intelligence_annual
          description: Must be a purchasable service key from the catalog.
        renewal_mode:
          type: string
          enum: [auto, manual]
          default: auto
          description: |
            `auto` creates a Stripe Subscription (recurring). `manual` creates a
            one-time payment. Ignored for `one_time` pricing_model services.

    CheckoutResponse:
      type: object
      required: [url, sessionId, renewal_mode]
      properties:
        url:          { type: string, format: uri, description: "Stripe Checkout URL — redirect the user here." }
        sessionId:    { type: string }
        renewal_mode: { type: string, enum: [auto, manual, one_time] }

    McpTokenResponse:
      type: object
      required: [access_token, token_type, expires_in, scope]
      properties:
        access_token:
          type: string
          description: HMAC-SHA256 signed MCP token. Opaque to the client.
        token_type:
          type: string
          enum: [Bearer]
        expires_in:
          type: integer
          example: 3600
        scope:
          type: string
          example: "flywheel:read flywheel:write"

    OAuthServerMetadata:
      type: object
      properties:
        issuer:                               { type: string, format: uri }
        authorization_endpoint:               { type: string, format: uri }
        token_endpoint:                       { type: string, format: uri }
        token_endpoint_auth_methods_supported:
          type: array
          items: { type: string }
        response_types_supported:
          type: array
          items: { type: string }
        grant_types_supported:
          type: array
          items: { type: string }
        scopes_supported:
          type: array
          items: { type: string }
        code_challenge_methods_supported:
          type: array
          items: { type: string }

    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:   { type: string }
        message: { type: string }
        hint:    { type: string }

    RateLimitError:
      type: object
      properties:
        error: { type: string, example: "Too Many Requests" }

# ── Paths ──────────────────────────────────────────────────────────────────────

paths:

  # ── Public ──────────────────────────────────────────────────────────────────

  /api/public/pricing:
    get:
      tags: [Public]
      operationId: getPublicPricing
      summary: Service catalog
      description: |
        Returns all active services from the Lawenots pricing catalog.
        Cached for 600 seconds (10 minutes) at the CDN layer.
        No authentication required.
      responses:
        "200":
          description: Active services
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/LawenotsService" }
        "429":
          description: Rate limit exceeded
          headers:
            Retry-After:       { schema: { type: integer } }
            X-RateLimit-Limit: { schema: { type: integer } }
          content:
            text/plain:
              schema: { type: string }

  /api/public/platform-capabilities:
    get:
      tags: [Public]
      operationId: getPlatformCapabilities
      summary: Platform execution mode and capabilities
      description: |
        Returns the current execution mode (monitoring vs live), paper trading
        availability, and autonomous tier claims mode. Useful for connector
        integrations that need to understand the platform's current posture.
      responses:
        "200":
          description: Platform capability snapshot
          content:
            application/json:
              schema:
                type: object
                properties:
                  executionLiveMode:       { type: boolean }
                  executionPaused:         { type: boolean }
                  paperTradingOptIn:       { type: boolean }
                  autonomousClaimsMode:
                    type: string
                    enum: [monitoring, paper_opt_in, live]
                  platformModeBanner:      { type: string, nullable: true }

  # ── Contact ─────────────────────────────────────────────────────────────────

  /api/contact:
    post:
      tags: [Contact]
      operationId: submitContactForm
      summary: Request access / contact form
      description: |
        Processes request-access and contact form submissions from `/request-access`.
        Sends an operator alert via Resend and a confirmation email to the user.
        Rate limited to 20 requests per minute per IP.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContactRequest" }
      responses:
        "200":
          description: Form received and notifications dispatched
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean, example: true }
        "422":
          description: Validation failed
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:  { type: string }
                  issues: { type: array }
        "429":
          description: Rate limit exceeded

  # ── Stripe ──────────────────────────────────────────────────────────────────

  /api/stripe/create-subscription-checkout:
    post:
      tags: [Stripe]
      operationId: createSubscriptionCheckout
      summary: Create a Stripe Checkout session
      description: |
        Creates a Stripe Checkout session for a purchasable Lawenots service.
        Requires an active Supabase session. Returns a Stripe URL to redirect
        the user to.

        **Purchasable services** (as of this version):
        - `core_v1` — $199 one-time
        - `intelligence_annual` — $229/yr
        - `autonomous_annual` — $899/yr
        - `intelligence_lifetime` — $799 one-time
        - `autonomous_lifetime` — $2,999 one-time
        - `addon_personal_finance` — $49/yr
        - `addon_defi_onchain` — $49/yr
        - (and others — see `/api/public/pricing`)

        Services that cannot be purchased via this endpoint:
        - `scenario_sim_overage` (per-event billing)
        - Individual intelligence add-ons (use `intelligence_addon_bundle` instead)
      security:
        - supabaseSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CheckoutRequest" }
      responses:
        "200":
          description: Checkout session created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CheckoutResponse" }
        "400":
          description: Invalid service_key or non-purchasable service
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "401":
          description: No active session — redirect user to /login
        "403":
          description: Prerequisite subscription missing (e.g. v1_to_v2_upgrade without core_v1)
        "409":
          description: User already has an active subscription for this service
        "503":
          description: Stripe not configured on this deployment

  # ── MCP Server ──────────────────────────────────────────────────────────────

  /api/mcp:
    post:
      tags: [MCP]
      operationId: mcpRequest
      summary: MCP protocol endpoint
      description: |
        Stateless Streamable HTTP transport for the LAWENOTS MCP server.
        Claude sends JSON-RPC 2.0 MCP messages here after completing the OAuth flow.

        **Available tools:**
        - `get_flywheel_state` — portfolio summary (requires `flywheel:read`)
        - `get_credit_capacity` — credit facility status (requires `flywheel:read`)
        - `get_open_containers` — active swing and income containers (requires `flywheel:read`)
        - `check_rr_gate` — validates R/R ratio for a proposed trade (requires `flywheel:read`)
        - `validate_swing_covenants` — full covenant checklist (requires `flywheel:read`)
        - `create_swing_parameters` — HITL trade parameter builder (requires `flywheel:write`)
        - `get_morning_brief` — plain-English daily summary (requires `flywheel:read`)

        Also requires `flywheel_access` service subscription.
      security:
        - mcpBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: JSON-RPC 2.0 MCP message (initialize, tools/list, tools/call, etc.)
      responses:
        "200":
          description: MCP protocol response
          content:
            application/json:
              schema:
                type: object
                description: JSON-RPC 2.0 response
        "401":
          description: Missing or invalid Bearer token
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "403":
          description: flywheel_access subscription required
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "429":
          description: Rate limit exceeded (30 req/min per IP)
          headers:
            Retry-After:            { schema: { type: integer } }
            X-RateLimit-Limit:      { schema: { type: integer } }
            X-RateLimit-Remaining:  { schema: { type: integer } }

  # ── MCP OAuth ────────────────────────────────────────────────────────────────

  /api/mcp/oauth/authorize:
    get:
      tags: [MCP OAuth]
      operationId: mcpOAuthAuthorize
      summary: OAuth 2.0 authorization endpoint
      description: |
        Entry point for the OAuth Authorization Code flow. Claude redirects here
        when a user adds the LAWENOTS connector. Redirects to `/login?mcp_auth=...`
        which completes Supabase authentication and returns with an auth code.
      parameters:
        - name: response_type
          in: query
          required: true
          schema: { type: string, enum: [code] }
        - name: client_id
          in: query
          required: true
          schema: { type: string, example: claude-connector }
        - name: redirect_uri
          in: query
          required: true
          schema: { type: string, format: uri }
        - name: state
          in: query
          required: true
          schema: { type: string }
        - name: scope
          in: query
          schema: { type: string, default: "flywheel:read flywheel:write" }
      responses:
        "302":
          description: Redirect to LAWENOTS login with encoded OAuth context
        "400":
          description: Invalid response_type or missing redirect_uri
        "401":
          description: Unknown client_id

  /api/mcp/oauth/token:
    post:
      tags: [MCP OAuth]
      operationId: mcpOAuthToken
      summary: OAuth 2.0 token endpoint
      description: |
        Exchanges an authorization code for a LAWENOTS MCP access token.
        The token is HMAC-SHA256 signed with `MCP_TOKEN_SECRET` and valid for 1 hour.
        Accepts both `application/json` and `application/x-www-form-urlencoded`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [grant_type, code]
              properties:
                grant_type: { type: string, enum: [authorization_code] }
                code:       { type: string, description: "Authorization code from /authorize redirect" }
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [grant_type, code]
              properties:
                grant_type: { type: string, enum: [authorization_code] }
                code:       { type: string }
      responses:
        "200":
          description: MCP access token issued
          content:
            application/json:
              schema: { $ref: "#/components/schemas/McpTokenResponse" }
        "400":
          description: Invalid or expired authorization code
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "500":
          description: MCP_TOKEN_SECRET not configured on this deployment

  /api/mcp/.well-known/oauth-authorization-server:
    get:
      tags: [MCP OAuth]
      operationId: mcpOAuthMetadata
      summary: OAuth 2.0 server metadata (RFC 8414)
      description: |
        Auto-discovery endpoint used by Claude's connector marketplace to locate
        the authorization and token endpoints. No authentication required.
      responses:
        "200":
          description: OAuth 2.0 authorization server metadata
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OAuthServerMetadata" }
