openapi: 3.1.0
info:
  title: corsproxy.dev API
  version: "0.1.0"
  description: |
    Managed CORS proxy API. Programmatic surface for the hosted runtime at
    `https://api.corsproxy.dev`. The same wire protocol is implemented by
    the MIT-licensed Go runtime at <https://github.com/melihbirim/corsproxy>
    if you want to self-host.

    Auth model:
      - **Public proxy** (`/proxy?url=…&key=sk_live_…` or `X-API-Key`) for
        browser / server callers forwarding requests to third-party APIs.
      - **Bearer JWT** (`Authorization: Bearer <jwt>`) for everything in
        `/v1/auth/*`, `/v1/api-keys/*`, `/v1/usage/*`, `/v1/billing/*`,
        and `/v1/compliance/*`. JWT is issued via the magic-link flow.

    Rate limits:
      - Free plan: 100 requests/day per account, enforced atomically by a
        per-user Durable Object. Over the limit returns `429`.
  contact:
    name: corsproxy.dev support
    email: contact@corsproxy.dev
    url: https://corsproxy.dev/
  license:
    name: MIT (runtime)
    url: https://github.com/melihbirim/corsproxy/blob/main/LICENSE
servers:
  - url: https://api.corsproxy.dev
    description: Production
tags:
  - name: health
  - name: proxy
    description: The CORS-forwarding endpoint. The reason the service exists.
  - name: auth
    description: Passwordless (magic-link) authentication.
  - name: keys
    description: Manage API keys used by the proxy endpoints.
  - name: usage
    description: Daily counters and bandwidth analytics (from D1).
  - name: billing
    description: Upgrade-request contact form.
  - name: marketing
    description: Newsletter signup (Resend audience).
  - name: compliance
    description: GDPR export + delete.

paths:
  /v1/health:
    get:
      tags: [health]
      summary: Health probe
      description: Plain liveness check. Always 200 when the worker is up.
      responses:
        "200":
          description: Service is healthy.
          content:
            application/json:
              schema:
                type: object
                required: [status, timestamp, version]
                properties:
                  status: { type: string, example: "ok" }
                  timestamp: { type: string, format: date-time }
                  version: { type: string, example: "0.1.0" }

  /proxy:
    get: { $ref: "#/paths/~1v1~1proxy/get" }
    post: { $ref: "#/paths/~1v1~1proxy/post" }
  /v1/proxy:
    get:
      tags: [proxy]
      summary: Forward a GET request to an upstream URL
      description: |
        The core proxy. Sends `GET <url>` upstream and rewrites the response
        with permissive CORS headers so a browser can read it.
      parameters:
        - in: query
          name: url
          required: true
          description: Absolute URL of the upstream API to call.
          schema:
            type: string
            format: uri
            example: https://api.github.com/users/octocat
        - in: query
          name: key
          required: false
          description: API key for browser callers. Server callers should use the `X-API-Key` header instead.
          schema:
            type: string
            example: sk_live_abc123
        - in: header
          name: X-API-Key
          required: false
          description: API key for server-side callers.
          schema:
            type: string
      responses:
        "200":
          description: Upstream returned a successful response. Body and most headers passed through.
        "401":
          description: Missing / invalid API key.
        "429":
          description: Daily rate limit exceeded.
        "502":
          description: Upstream unreachable or rejected the request.
    post:
      tags: [proxy]
      summary: Forward a POST request to an upstream URL
      parameters:
        - $ref: "#/paths/~1v1~1proxy/get/parameters/0"
        - $ref: "#/paths/~1v1~1proxy/get/parameters/1"
        - $ref: "#/paths/~1v1~1proxy/get/parameters/2"
      requestBody:
        required: false
        description: Pass-through to the upstream URL.
        content:
          application/json:
            schema: { type: object }
      responses:
        "200": { description: Upstream response, with CORS headers added. }
        "401": { description: Missing or invalid API key. }
        "429": { description: Daily rate limit exceeded. }

  /v1/auth/captcha:
    get:
      tags: [auth]
      summary: Issue a math CAPTCHA challenge
      description: Required before /v1/auth/passwordless and /v1/marketing/subscribe.
      responses:
        "200":
          description: CAPTCHA challenge issued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      captcha_id: { type: string }
                      question: { type: string, example: "What is 3 + 4?" }
                      expires_at: { type: integer }

  /v1/auth/passwordless:
    post:
      tags: [auth]
      summary: Request a magic-link email
      description: |
        Creates the user on first request (verified=false, onboarded=false)
        and emails a one-time sign-in link valid for 15 minutes. The link
        target is `https://app.corsproxy.dev/auth/callback?token=…`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, captcha_id, captcha_answer]
              properties:
                email: { type: string, format: email }
                captcha_id: { type: string }
                captcha_answer: { type: integer }
      responses:
        "200": { description: Link sent (or would have been — response intentionally identical for unknown emails). }
        "400": { $ref: "#/components/responses/BadRequest" }

  /v1/auth/passwordless/callback:
    post:
      tags: [auth]
      summary: Exchange a magic-link token for a JWT
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token: { type: string }
      responses:
        "200":
          description: JWT issued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      token: { type: string, description: Bearer JWT (15-day TTL). }
                      requires_onboarding: { type: boolean }

  /v1/auth/onboarding:
    post:
      tags: [auth]
      summary: Complete first-time onboarding (terms / privacy / optional name)
      security: [ { bearerAuth: [] } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [terms_accepted, privacy_accepted]
              properties:
                terms_accepted: { type: boolean }
                privacy_accepted: { type: boolean }
                name: { type: string, maxLength: 80 }
      responses:
        "200": { description: Onboarding complete. }

  /v1/auth/me:
    get:
      tags: [auth]
      summary: Current user profile (excluding password hash)
      security: [ { bearerAuth: [] } ]
      responses:
        "200":
          description: User object.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/User" }

  /v1/auth/logout:
    post:
      tags: [auth]
      summary: Invalidate the current session
      security: [ { bearerAuth: [] } ]
      responses:
        "200": { description: Logged out. }

  /v1/api-keys:
    get:
      tags: [keys]
      summary: List the user's API keys
      security: [ { bearerAuth: [] } ]
      responses:
        "200":
          description: Array of API keys (no secret values).
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      keys:
                        type: array
                        items: { $ref: "#/components/schemas/ApiKey" }
    post:
      tags: [keys]
      summary: Create a new API key (shown in full only on the response)
      security: [ { bearerAuth: [] } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, maxLength: 80 }
                allowed_origins:
                  type: array
                  items: { type: string, format: uri }
                upstream_rules:
                  type: array
                  description: Inject headers when the proxy target matches this rule.
                  items: { $ref: "#/components/schemas/UpstreamRule" }
      responses:
        "200": { description: Key created. The `key` field is the only time the secret is returned. }

  /v1/api-keys/{id}:
    get:
      tags: [keys]
      summary: Fetch an API key (metadata only, never the secret)
      security: [ { bearerAuth: [] } ]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200": { description: Key metadata. }
        "404": { description: Key not found or owned by another user. }
    delete:
      tags: [keys]
      summary: Revoke an API key
      security: [ { bearerAuth: [] } ]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200": { description: Key revoked. }

  /v1/usage/account:
    get:
      tags: [usage]
      summary: Today's usage for the account
      security: [ { bearerAuth: [] } ]
      responses:
        "200":
          description: Daily counters from D1 (1 read, no log scan).
          content:
            application/json:
              schema:
                type: object
                properties:
                  total_usage: { type: integer }
                  limit: { type: integer }
                  remaining: { type: integer }
                  percentage: { type: integer }
                  bandwidth_today_bytes: { type: integer }
                  bandwidth_today_mb: { type: number }
                  reset_time: { type: string, format: date-time }

  /v1/usage/account/history:
    get:
      tags: [usage]
      summary: Last 7 days of account usage
      security: [ { bearerAuth: [] } ]
      responses:
        "200":
          description: Array of `{date, usage}` plus summary stats.

  /v1/usage/keys/{key_id}:
    get:
      tags: [usage]
      summary: Today's usage for a single API key
      security: [ { bearerAuth: [] } ]
      parameters:
        - in: path
          name: key_id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Usage count for the given key, today only.

  /v1/usage/overall:
    get:
      tags: [usage]
      summary: 30- and 7-day rollups with bandwidth
      security: [ { bearerAuth: [] } ]
      responses:
        "200":
          description: Aggregates (30 D1 reads, no log scan).

  /v1/billing/options:
    get:
      tags: [billing]
      summary: List paid plans (for the upgrade form)
      security: [ { bearerAuth: [] } ]
      responses:
        "200": { description: Pro + Enterprise descriptors. }

  /v1/billing/upgrade-request:
    post:
      tags: [billing]
      summary: Submit an upgrade-request contact form
      description: |
        Emails sales (contact@corsproxy.dev) with the user's plan, last-7-day
        usage, requested plan, and free-text reason. No charge — Stripe checkout
        isn't wired yet.
      security: [ { bearerAuth: [] } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [target_plan, reason]
              properties:
                target_plan: { type: string, enum: [pro, enterprise] }
                reason: { type: string, maxLength: 4000 }
                expected_daily_requests: { type: integer, minimum: 0 }
      responses:
        "200": { description: Request sent. }

  /v1/marketing/subscribe:
    post:
      tags: [marketing]
      summary: Newsletter signup
      description: Adds the email to our Resend audience.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, captcha_id, captcha_answer]
              properties:
                email: { type: string, format: email }
                captcha_id: { type: string }
                captcha_answer: { type: integer }
                source: { type: string, enum: [landing, blog, onboarding, other] }
      responses:
        "200": { description: Subscribed. Response is identical for already-subscribed addresses (no probing). }

  /v1/compliance/export:
    get:
      tags: [compliance]
      summary: Export everything we know about the authenticated user
      security: [ { bearerAuth: [] } ]
      responses:
        "200":
          description: JSON with profile, API keys, usage history, and request logs.

  /v1/compliance/account:
    delete:
      tags: [compliance]
      summary: Hard-delete the authenticated user's account and all data
      description: Article 17 of GDPR. Irreversible.
      security: [ { bearerAuth: [] } ]
      responses:
        "200": { description: Account deleted. }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: "Issued via /v1/auth/passwordless/callback. Pass as `Authorization: Bearer <jwt>`."
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: For server-side proxy calls. Browser callers should use the `?key=` query param instead.

  responses:
    BadRequest:
      description: Bad request — invalid parameters or CAPTCHA.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:
    User:
      type: object
      properties:
        id: { type: string }
        email: { type: string, format: email }
        verified: { type: boolean }
        onboarded: { type: boolean }
        plan: { type: string, enum: [free, pro, enterprise] }
        daily_limit: { type: integer }
        status: { type: string, enum: [active, suspended, deleted] }
        created_at: { type: string, format: date-time }

    ApiKey:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        created_at: { type: string, format: date-time }
        last_used: { type: string, format: date-time, nullable: true }
        status: { type: string, enum: [active, revoked] }
        allowed_origins:
          type: array
          items: { type: string }

    UpstreamRule:
      type: object
      description: |
        Attach this rule to a key to inject upstream credentials at the
        proxy. The browser never sees the third-party key.
      required: [target_host]
      properties:
        target_host: { type: string, example: api.openai.com }
        path_prefix: { type: string, example: "/v1/" }
        headers:
          type: object
          additionalProperties: { type: string }

    Error:
      type: object
      properties:
        success: { type: boolean, example: false }
        error: { type: string }
        code: { type: string }
