openapi: 3.0.0
info:
  title: Apikee Public API
  version: 2.0.0
  description: |
    Complete REST API for the Apikee platform.

    ## Authentication

    All requests must carry one of:

    | Header | Value |
    |--------|-------|
    | `x-api-key` | HMAC-SHA256 signed key issued by the apikee engine |
    | `Authorization` | `Bearer <session-token>` (web dashboard sessions) |

    ### API Key format
    Keys are compact, URL-safe strings produced by the `apikee` package's
    `ApikeeKeyEngine`. They encode a JSON claims payload (tenant ID, scopes,
    environment, expiry) signed with HMAC-SHA256.

    ```
    <base62-id>.<base62-claims>.<base62-hmac>
    ```

    The first 12 characters (`keyPrefix`) are safe to log for debugging.
    The full key is **shown exactly once** at creation and never stored in
    plain text (only a SHA-256 hash is persisted).

    ### Scopes
    Keys carry a `scopes` array. `["*"]` grants full access.
    Scope enforcement is project-defined.

    ## Base URL
    `https://apikee.dev/api/v1`

servers:
  - url: https://apikee.dev/api/v1
    description: Apikee API (production)
  - url: http://localhost:3000/api/v1
    description: Local development

security:
  - ApiKeyAuth: []
  - BearerAuth: []

tags:
  - name: projects
    description: Projects, environments, templates and logs
  - name: endpoints
    description: API endpoint definitions
  - name: clients
    description: Clients and API keys (including validation & creation)

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: |
        HMAC-SHA256 signed key produced by the apikee engine.
        Format: `<base62-id>.<base62-claims>.<base62-hmac>`
        The first 12 characters are the `keyPrefix` (safe for logging).
    BearerAuth:
      type: http
      scheme: bearer
      description: Session token from a browser login (dashboard use).

  parameters:
    project_env:
      name: project_env
      in: query
      schema: { type: string }
      description: e.g. my-app-production
    page:
      name: page
      in: query
      schema: { type: integer, default: 1 }
    limit:
      name: limit
      in: query
      schema: { type: integer, default: 10, maximum: 100 }

  schemas:
    Pagination:
      type: object
      properties:
        total: { type: integer }
        page: { type: integer }
        limit: { type: integer }
        pages: { type: integer }

    ErrorBase:
      type: object
      properties:
        code: { type: string }
        status: { type: integer }
        error: { type: string }

    ClientKey:
      type: object
      description: |
        Represents one API key issued by the apikee engine.
        `keyHash` is never returned — only `keyPrefix` (first 12 chars) is exposed
        for display/debugging. The full raw key is returned **once** at creation
        inside `rawKey` and must be copied by the caller immediately.
      properties:
        id:
          type: string
          description: "Stable key ID embedded in the signed payload claims."
        name: { type: string, nullable: true }
        keyPrefix:
          type: string
          description: "First 12 characters of the raw key — safe to log."
        rawKey:
          type: string
          nullable: true
          description: "Full raw key — returned ONLY on creation, never stored in plain text."
        scopes:
          type: array
          items: { type: string }
          description: "Permission scopes encoded in the key. ['*'] = full access."
        environment:
          type: string
          description: "Environment this key was issued for (e.g. 'prod', 'dev')."
        enabled: { type: boolean }
        requestCount: { type: integer }
        lastUsedAt: { type: string, format: date-time, nullable: true }
        expiresAt: { type: string, format: date-time, nullable: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        metadata: { type: object, nullable: true, additionalProperties: true }

    Client:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        type:
          type: string
          enum: [person, company, service, device, internal]
        status:
          type: string
          enum: [active, suspended, inactive]
        source: { type: object, nullable: true, additionalProperties: true }
        ipWhitelist:
          type: array
          items: { type: string }
          description: "Allowed IP addresses or CIDR ranges. Empty = all IPs allowed."
        metadata: { type: object, nullable: true, additionalProperties: true }
        templateId: { type: string, nullable: true }
        envId: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        keys:
          type: array
          description: "API keys for this client. `rawKey` is only present immediately after creation."
          items: { $ref: '#/components/schemas/ClientKey' }
        stats:
          type: object
          nullable: true
          properties:
            total: { type: integer }
            success: { type: integer }
            error: { type: integer }
            avgDurationMs: { type: integer }

    CreateClientItem:
      type: object
      required: [name, type]
      properties:
        name: { type: string, description: "Display name for the client" }
        type:
          type: string
          enum: [person, company, service, device, internal]
          description: "Client type — determines how the client is categorized"
        templateId:
          type: string
          nullable: true
          description: |
            ID of the Template to apply to this client.
            Templates define rate limits (per hour/day/week/month) and the maximum number of
            allowed clients. When a client has a template, all its API keys are subject to
            the template's rate limits. Use GET /client/templates?project_env=... to list
            available templates.
        source: { type: object, nullable: true, additionalProperties: true }
        ipWhitelist: { type: array, items: { type: string }, description: "Allowed IPs or CIDR ranges. Empty = unrestricted." }
        metadata: { type: object, nullable: true, additionalProperties: true, description: "Arbitrary key-value metadata to attach to the client" }
        keys:
          type: array
          description: |
            Optional initial API keys to generate for the client.
            Each key is signed by the apikee HMAC engine. The full `rawKey` is
            returned only in the creation response — copy it immediately.
          items:
            type: object
            required: [name]
            properties:
              name: { type: string }
              expiresAt: { type: string, format: date-time, nullable: true, description: "ISO-8601 expiry or null for no expiry." }
              metadata: { type: object, nullable: true, additionalProperties: true }

    UpdateClientBody:
      type: object
      properties:
        name: { type: string }
        type: { type: string, enum: [person, company, service, device, internal] }
        source: { type: object, nullable: true, additionalProperties: true }
        ipWhitelist: { type: array, items: { type: string }, description: "Allowed IPs or CIDR ranges. Empty = unrestricted." }
        metadata: { type: object, nullable: true, additionalProperties: true }
        templateId:
          type: string
          nullable: true
          description: "Set or change the Template for this client. Pass null to remove the template."
        keys:
          type: array
          description: |
            Upsert API keys. Provide `id` to update an existing key; omit `id` to create a new one.
            New keys are signed by the apikee engine — the full raw key is returned once.
          items:
            type: object
            properties:
              id: { type: string, description: "Key ID to update. Omit to create a new key." }
              name: { type: string }
              expiresAt: { type: string, format: date-time, nullable: true }
              metadata: { type: object, nullable: true, additionalProperties: true }

    ValidateKeyBody:
      type: object
      required: [name, timestamp, headers]
      description: |
        Body for the key-validation + log endpoint (`POST /client/{client_uuid}`).
        Supply the incoming request headers so Apikee can verify the HMAC signature
        and log the call against the correct endpoint.
      properties:
        name:
          type: string
          description: "Display name of the API key being validated (from the key's `name` field)."
        timestamp:
          type: string
          format: date-time
          description: "ISO-8601 timestamp of the incoming request. Used for replay-attack protection."
        createdAt: { type: string, format: date-time, nullable: true }
        expiresAt: { type: string, format: date-time, nullable: true }
        headers:
          type: object
          additionalProperties: true
          description: "Full request headers forwarded by your API server (used for HMAC verification)."
        payload:
          type: object
          nullable: true
          additionalProperties: true
          description: "Optional request body / query params to attach to the log entry."

    Endpoint:
      type: object
      properties:
        method: { type: string }
        path: { type: string }
        name: { type: string }
        description: { type: string, nullable: true }
        request: { type: object, nullable: true, additionalProperties: true }
        response: { type: object, nullable: true, additionalProperties: true }
        metadata: { type: object, nullable: true, additionalProperties: true }
        method_path: { type: string }
        project_env: { type: string }
        stats:
          type: object
          properties:
            total: { type: integer }
            success: { type: integer }
            error: { type: integer }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    EndpointCreateItem:
      type: object
      required: [method, path, name]
      properties:
        method: { type: string }
        path: { type: string }
        name: { type: string }
        description: { type: string, nullable: true }
        request: { type: object, nullable: true, additionalProperties: true }
        response: { type: object, nullable: true, additionalProperties: true }
        metadata: { type: object, nullable: true, additionalProperties: true }

    ProjectSummary:
      type: object
      properties:
        id: { type: string }
        slug: { type: string }
        name: { type: string }
        description: { type: string, nullable: true }
        metadata: { type: object, nullable: true, additionalProperties: true }
        team:
          type: object
          properties:
            id: { type: string }
            name: { type: string }
            members: { type: object, properties: { count: {type:integer}, max: {type:integer}, list: {type:array, items:{type:object}} } }
            credentials: { type: object, properties: { count: {type:integer}, list: {type:array, items:{type:object}} } }
        createdAt: { type: string, format: date-time }
        envs:
          type: object
          properties:
            count: { type: integer }
            max: { type: integer }
            list: { type: array, items: { $ref: '#/components/schemas/EnvSummary' } }
        env:
          type: object
          nullable: true
          $ref: '#/components/schemas/EnvSummary'

    EnvSummary:
      type: object
      properties:
        project_env: { type: string }
        type: { type: string }
        url: { type: string, nullable: true, format: uri }
        version: { type: string, nullable: true }
        status: { type: string, nullable: true }
        metadata: { type: object, nullable: true, additionalProperties: true }
        templates: { type: object, properties: { count: {type:integer}, list: {type:array, items:{$ref:'#/components/schemas/Template'}} } }
        clients: { type: object, properties: { count: {type:integer}, max: {type:integer} } }
        endpoints: { type: object, properties: { count: {type:integer}, max: {type:integer} } }
        rateLimits:
          type: object
          properties:
            hour: { type: object, properties: { count: {type:integer}, max: {type:integer} } }
            day: { type: object, properties: { count: {type:integer}, max: {type:integer} } }
            week: { type: object, properties: { count: {type:integer}, max: {type:integer} } }
            month: { type: object, properties: { count: {type:integer}, max: {type:integer} } }

    Template:
      type: object
      description: |
        A Template defines the rate-limiting plan and access policy applied to clients.
        Clients assigned a template have their API key requests counted against the
        template's rate limits (per hour, day, week, and month). `maxClients` caps how
        many clients can use the same template within an environment.
      properties:
        id: { type: string }
        name: { type: string }
        description: { type: string, nullable: true }
        maxClients:
          type: integer
          nullable: true
          description: "Maximum number of clients allowed to use this template. null = unlimited."
        rateLimitHour:
          type: integer
          nullable: true
          description: "Maximum API requests per hour per client. null = unlimited."
        rateLimitDay:
          type: integer
          nullable: true
          description: "Maximum API requests per day per client. null = unlimited."
        rateLimitWeek:
          type: integer
          nullable: true
          description: "Maximum API requests per week per client. null = unlimited."
        rateLimitMonth:
          type: integer
          nullable: true
          description: "Maximum API requests per month per client. null = unlimited."
        metadata: { type: object, nullable: true, additionalProperties: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    LogEntry:
      type: object
      properties:
        id: { type: string }
        clientId: { type: string }
        endpointId: { type: string }
        status: { type: string, enum: [success, error] }
        durationMs: { type: integer }
        timestamp: { type: string, format: date-time }
        method_path: { type: string }
        name: { type: string }

paths:
  /project:
    get:
      tags: [projects]
      summary: List projects
      parameters:
        - $ref: '#/components/parameters/page'
        - $ref: '#/components/parameters/limit'
        - name: name
          in: query
          schema: { type: string }
        - name: status
          in: query
          schema: { type: string }
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/ProjectSummary' } }
                  pagination: { $ref: '#/components/schemas/Pagination' }
              example:
                data:
                  - slug: "my-app"
                    name: "My Application"
                    envs:
                      count: 2
                      list:
                        - project_env: "my-app-production"
                          type: "production"
                          clients: { count: 4, max: 50 }
                pagination:
                  total: 5
                  page: 1
                  limit: 10
                  pages: 1

    post:
      tags: [projects]
      summary: Create project
      description: |
        Creates a new project with optional environments and templates.
        No credentials (API keys) are auto-generated — use the credential management
        endpoints to create keys for the team after the project is created.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [slug, name]
              properties:
                slug: { type: string, pattern: '^[a-z0-9-]+$' }
                name: { type: string }
                description: { type: string, nullable: true }
                metadata: { type: object, nullable: true, additionalProperties: true }
                envs:
                  type: array
                  items:
                    type: object
                    required: [type]
                    properties:
                      type: { type: string }
                      url: { type: string, nullable: true }
                      templates:
                        type: array
                        items: { $ref: '#/components/schemas/Template' }
            example:
              slug: "my-app"
              name: "My Application"
              description: "Production backend"
              envs:
                - type: "production"
                  templates:
                    - name: "Mobile App"
                      maxClients: 50
                      rateLimitDay: 10000
      responses:
        '201':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProjectSummary' }

  /project/{project_env}:
    get:
      tags: [projects]
      summary: Get project detail
      parameters:
        - name: project_env
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProjectSummary' }

    put:
      tags: [projects]
      summary: Update project (or single env)
      description: |
        Updates project-level fields and/or environments.
        - Use the top-level `name`, `description`, `metadata` fields to update the project.
        - Use `singleEnv` to create or update the environment identified by `project_env`.
        - Use `envs` array to create, update, or delete multiple environments in one call
          (set `delete: true` on individual env items to remove them, `forceDelete: true` to bypass the active-clients check).
      parameters:
        - name: project_env
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                description: { type: string, nullable: true }
                metadata: { type: object, nullable: true, additionalProperties: true }
                singleEnv:
                  type: object
                  description: "Create or update the specific env identified by the `project_env` path param."
                  properties:
                    type: { type: string }
                    url: { type: string, nullable: true }
                    version: { type: string, nullable: true }
                    status: { type: string, nullable: true }
                    metadata: { type: object, nullable: true, additionalProperties: true }
                    delete: { type: boolean, description: "Set to true to delete this env." }
                    forceDelete: { type: boolean, description: "Force-delete even if active clients exist." }
                    templates:
                      type: array
                      items: { $ref: '#/components/schemas/Template' }
                envs:
                  type: array
                  description: "Batch create/update/delete environments."
                  items:
                    type: object
                    properties:
                      id: { type: string, description: "Env ID for update/delete, omit to create." }
                      type: { type: string }
                      url: { type: string, nullable: true }
                      version: { type: string, nullable: true }
                      status: { type: string, nullable: true }
                      metadata: { type: object, nullable: true, additionalProperties: true }
                      delete: { type: boolean }
                      forceDelete: { type: boolean }
                      templates:
                        type: array
                        items: { $ref: '#/components/schemas/Template' }
            example:
              name: "Updated Application"
              singleEnv:
                type: "production"
                url: "https://api.myapp.com"
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProjectSummary' }

    delete:
      tags: [projects]
      summary: Delete project / env / template
      description: |
        Deletes a project, environment, or template depending on the parameters provided.
        - No query params → deletes the env named in `project_env` (e.g. `my-app-production`), or the whole project if `project_env` is just a slug.
        - `envId` → deletes that specific environment.
        - `templateId` → deletes that specific template.
        Set `forceDelete=true` to remove resources that still have active clients.
      parameters:
        - name: project_env
          in: path
          required: true
          schema: { type: string }
        - name: envId
          in: query
          schema: { type: string }
          description: "ID of the environment to delete. Takes precedence over the slug-inferred env."
        - name: templateId
          in: query
          schema: { type: string }
          description: "ID of the template to delete."
        - name: forceDelete
          in: query
          schema: { type: boolean }
          description: "Set to true to force-delete even when active clients exist."
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  deleted: { type: string, enum: [project, env, template] }
                  forced: { type: boolean }

  /project/{project_env}/logs:
    get:
      tags: [projects]
      summary: Query logs
      parameters:
        - name: project_env
          in: path
          required: true
          schema: { type: string }
        - $ref: '#/components/parameters/page'
        - $ref: '#/components/parameters/limit'
        - name: methodPath
          in: query
          schema: { type: string }
        - name: clientId
          in: query
          schema: { type: string }
        - name: status
          in: query
          schema: { type: string, enum: ["success", "error"] }
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/LogEntry' } }
                  pagination: { $ref: '#/components/schemas/Pagination' }

  /client:
    get:
      tags: [clients]
      summary: List clients
      parameters:
        - $ref: '#/components/parameters/project_env'
        - $ref: '#/components/parameters/page'
        - $ref: '#/components/parameters/limit'
        - name: name
          in: query
          schema: { type: string }
        - name: type
          in: query
          schema: { type: string }
        - name: templateId
          in: query
          schema: { type: string }
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Client' } }
                  pagination: { $ref: '#/components/schemas/Pagination' }

    post:
      tags: [clients]
      summary: Create client(s)
      description: |
        Creates one or more clients. If `keys` are specified, each key is signed
        by the apikee HMAC engine and the full `rawKey` is returned **once** in
        the response inside the key object. Store it immediately — it cannot be
        retrieved again (only the SHA-256 hash is persisted).
      parameters:
        - $ref: '#/components/parameters/project_env'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/CreateClientItem'
                - type: array
                  items: { $ref: '#/components/schemas/CreateClientItem' }
            example:
              name: "Mobile Backend"
              type: "service"
              templateId: "clt_abc123"
              metadata:
                owner: "dev-team"
              keys:
                - name: "Prod Key"
                  expiresAt: "2027-12-31T23:59:59Z"
      responses:
        '201':
          description: |
            Client created. If keys were requested, `keys[].rawKey` contains the
            full signed key — shown once only.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/Client'
                  - type: array
                    items: { $ref: '#/components/schemas/Client' }

  /client/{client_uuid}:
    get:
      tags: [clients]
      summary: Get client (includes stats)
      parameters:
        - name: client_uuid
          in: path
          required: true
          schema: { type: string, format: uuid }
        - $ref: '#/components/parameters/project_env'
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Client' }

    put:
      tags: [clients]
      summary: Update client + upsert keys
      parameters:
        - name: client_uuid
          in: path
          required: true
          schema: { type: string, format: uuid }
        - $ref: '#/components/parameters/project_env'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateClientBody' }
            example:
              name: "Updated Mobile Backend"
              keys:
                - id: "key-uuid-123"
                  name: "Renamed Key"
                  expiresAt: "2028-01-01T00:00:00Z"
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Client' }

    delete:
      tags: [clients]
      summary: Delete client or single key
      parameters:
        - name: client_uuid
          in: path
          required: true
          schema: { type: string, format: uuid }
        - name: keyId
          in: query
          schema: { type: string }
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }

    post:
      tags: [clients]
      summary: Validate key usage & log to Mongo
      parameters:
        - name: client_uuid
          in: path
          required: true
          schema: { type: string, format: uuid }
        - $ref: '#/components/parameters/project_env'
        - name: method_path
          in: query
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ValidateKeyBody' }
            example:
              name: "Prod Key"
              timestamp: "2026-03-14T20:45:00Z"
              headers:
                user-agent: "curl/8.5.0"
                x-forwarded-for: "203.0.113.1"
              payload:
                userId: 12345
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  clientId: { type: string }
                  project_env: { type: string }
                  method_path: { type: string }
                  durationMs: { type: integer, nullable: true }

  /client/templates:
    get:
      tags: [clients]
      summary: List templates for project_env
      description: |
        Returns all Templates defined for the given environment.
        Use the returned `id` as `templateId` when creating or updating clients.
        Templates define rate limits (hour/day/week/month) and max client counts.
      parameters:
        - $ref: '#/components/parameters/project_env'
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Template' } }

  /endpoint:
    get:
      tags: [endpoints]
      summary: List endpoints
      parameters:
        - $ref: '#/components/parameters/project_env'
        - $ref: '#/components/parameters/page'
        - $ref: '#/components/parameters/limit'
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Endpoint' } }
                  pagination: { $ref: '#/components/schemas/Pagination' }

    post:
      tags: [endpoints]
      summary: Create endpoint(s)
      description: |
        Pass `project_env` as a query parameter (e.g. `?project_env=my-app-production`).
        The request body is the endpoint object or an array of endpoint objects directly.
      parameters:
        - $ref: '#/components/parameters/project_env'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/EndpointCreateItem'
                - type: array
                  items: { $ref: '#/components/schemas/EndpointCreateItem' }
            example:
              method: "POST"
              path: "/webhooks"
              name: "Stripe Webhook"
              description: "Handle payment events"
              request:
                type: "object"
                properties:
                  event: { type: "string" }
              response:
                type: "object"
                properties:
                  success: { type: "boolean" }
      responses:
        '201':
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/Endpoint'
                  - type: array
                    items: { $ref: '#/components/schemas/Endpoint' }

  /endpoint/{method_path}:
    get:
      tags: [endpoints]
      summary: Get endpoint
      parameters:
        - name: method_path
          in: path
          required: true
          schema: { type: string }
        - $ref: '#/components/parameters/project_env'
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Endpoint' }

    put:
      tags: [endpoints]
      summary: Update endpoint
      parameters:
        - name: method_path
          in: path
          required: true
          schema: { type: string }
        - $ref: '#/components/parameters/project_env'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                description: { type: string, nullable: true }
                request: { type: object, additionalProperties: true }
                response: { type: object, additionalProperties: true }
                metadata: { type: object, additionalProperties: true }
            example:
              name: "Updated Stripe Webhook"
              description: "Handles all payment events"
              metadata:
                version: "v2"
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Endpoint' }

    delete:
      tags: [endpoints]
      summary: Delete endpoint
      parameters:
        - name: method_path
          in: path
          required: true
          schema: { type: string }
        - $ref: '#/components/parameters/project_env'
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  method_path: { type: string }
                  project_env: { type: string }