{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "AppConfig",
  "type": "object",
  "properties": {
    "audit_enabled": {
      "description": "Record management and OAuth audit events. Read at startup; existing rows remain queryable.",
      "type": "boolean",
      "default": true
    },
    "cache": {
      "$ref": "#/$defs/CacheBackendConfig",
      "default": {
        "kind": "memory"
      }
    },
    "console": {
      "$ref": "#/$defs/ConsoleConfig",
      "default": {
        "enabled": true,
        "path": null
      }
    },
    "cors_origins": {
      "description": "Browser origins allowed to call the APIs. Empty means same-origin only.",
      "type": "array",
      "default": [],
      "items": {
        "type": "string"
      }
    },
    "data_dir": {
      "description": "Root for instance state a backend resolves relative paths against: the\nSQLite file, the local file-storage root, downloaded vocabularies.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "file_storage": {
      "description": "None keeps file-backed resources (publications, vocabularies) disabled;\ncore refuses those operations rather than inventing a location.",
      "anyOf": [
        {
          "$ref": "#/$defs/FileStorageConfig"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "host": {
      "description": "Listen address of a native host. Ignored by the edge host, which is\ngiven its socket by the runtime.",
      "type": "string",
      "default": "127.0.0.1"
    },
    "master_key": {
      "$ref": "#/$defs/MasterKeyConfig",
      "default": {
        "key": {
          "kind": "none"
        },
        "next": {
          "kind": "none"
        },
        "rotate": false
      }
    },
    "oauth": {
      "$ref": "#/$defs/OAuthIssuerConfig",
      "default": {
        "access_ttl_secs": 3600,
        "cli_client_ids": [],
        "code_ttl_secs": 300,
        "device_ttl_secs": 900,
        "refresh_ttl_secs": 2592000
      }
    },
    "port": {
      "type": "integer",
      "format": "uint16",
      "default": 8787,
      "maximum": 65535,
      "minimum": 0
    },
    "public_base_url": {
      "description": "Absolute external origin, e.g. `https://gproxy.example.com`. Used to\nmint publication URLs and the OAuth issuer identifier when the request\nitself cannot be trusted to name the instance.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "session_ttl_secs": {
      "type": "integer",
      "format": "uint64",
      "default": 2592000,
      "minimum": 0
    },
    "store": {
      "$ref": "#/$defs/StoreBackendConfig",
      "default": {
        "kind": "sqlite",
        "path": "gproxy.db"
      }
    },
    "trusted_proxies": {
      "description": "Peers whose `x-forwarded-for` / `x-forwarded-proto` are believed. Empty\nmeans the socket address is the client address, which is the safe\ndefault: a spoofed client IP is a spoofed rate-limit bucket.",
      "type": "array",
      "default": [],
      "items": {
        "type": "string"
      }
    }
  },
  "additionalProperties": false,
  "$defs": {
    "CacheBackendConfig": {
      "description": "Shared TTL state, counters and invalidation notices. `Memory` is per\nprocess: correct for a single instance, and silently wrong for several,\nwhich is why a multi-instance deployment must name `Redis` or `Store`.",
      "oneOf": [
        {
          "type": "object",
          "properties": {
            "kind": {
              "type": "string",
              "const": "memory"
            }
          },
          "required": [
            "kind"
          ]
        },
        {
          "type": "object",
          "properties": {
            "kind": {
              "type": "string",
              "const": "redis"
            },
            "namespace": {
              "type": [
                "string",
                "null"
              ]
            },
            "url": {
              "type": "string"
            }
          },
          "required": [
            "kind",
            "url"
          ]
        },
        {
          "description": "The database itself as the cache, for edge deployments with no Redis.",
          "type": "object",
          "properties": {
            "kind": {
              "type": "string",
              "const": "store"
            }
          },
          "required": [
            "kind"
          ]
        }
      ]
    },
    "ConsoleConfig": {
      "type": "object",
      "properties": {
        "enabled": {
          "type": "boolean",
          "default": true
        },
        "path": {
          "description": "Serve the console from a directory instead of the embedded bundle.",
          "type": [
            "string",
            "null"
          ],
          "default": null
        }
      },
      "additionalProperties": false
    },
    "FileStorageConfig": {
      "description": "Where publication and vocabulary bytes live. Unset means neither is served.",
      "oneOf": [
        {
          "description": "A local directory, resolved against `data_dir` when relative.",
          "type": "object",
          "properties": {
            "kind": {
              "type": "string",
              "const": "fs"
            },
            "root": {
              "type": "string"
            }
          },
          "required": [
            "kind",
            "root"
          ]
        },
        {
          "type": "object",
          "properties": {
            "access_key_id": {
              "type": [
                "string",
                "null"
              ]
            },
            "bucket": {
              "type": "string"
            },
            "endpoint": {
              "description": "Set for S3-compatible services that are not AWS.",
              "type": [
                "string",
                "null"
              ]
            },
            "kind": {
              "type": "string",
              "const": "s3"
            },
            "region": {
              "type": [
                "string",
                "null"
              ]
            },
            "root": {
              "description": "Key prefix inside the bucket; empty uses the bucket root.",
              "type": [
                "string",
                "null"
              ]
            },
            "secret_access_key": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          "required": [
            "kind",
            "bucket"
          ]
        }
      ]
    },
    "MasterKey": {
      "description": "A 32-byte key, encoded. Two encodings because operators paste whichever\ntheir secret manager emits, and guessing between them silently is worse\nthan naming it.",
      "oneOf": [
        {
          "type": "object",
          "properties": {
            "kind": {
              "type": "string",
              "const": "none"
            }
          },
          "required": [
            "kind"
          ]
        },
        {
          "type": "object",
          "properties": {
            "kind": {
              "type": "string",
              "const": "hex"
            },
            "value": {
              "type": "string"
            }
          },
          "required": [
            "kind",
            "value"
          ]
        },
        {
          "description": "Standard or URL-safe alphabet, with or without padding.",
          "type": "object",
          "properties": {
            "kind": {
              "type": "string",
              "const": "base64"
            },
            "value": {
              "type": "string"
            }
          },
          "required": [
            "kind",
            "value"
          ]
        }
      ]
    },
    "MasterKeyConfig": {
      "description": "The key that seals upstream credential secrets at rest, and the rotation\nflow around it.\n\nRotation is two configured keys and a switch: `key` is what the instance\ndecrypts with today, `next` is what it re-seals to. With `rotate` set, the\nbinary performs the rotation at startup as one management operation and the\noperator then promotes `next` to `key`. Leaving `key` unset stores secrets\nin plaintext; that is a supported deployment, not an accident, and the\nbinary warns about it once at startup.",
      "type": "object",
      "properties": {
        "key": {
          "$ref": "#/$defs/MasterKey",
          "default": {
            "kind": "none"
          }
        },
        "next": {
          "description": "The key to re-seal to. Configured on its own it does nothing; the\ninstance keeps using `key`.",
          "$ref": "#/$defs/MasterKey",
          "default": {
            "kind": "none"
          }
        },
        "rotate": {
          "description": "Perform the rotation at startup. Ignored without `next`.",
          "type": "boolean",
          "default": false
        }
      },
      "additionalProperties": false
    },
    "OAuthIssuerConfig": {
      "description": "Lifetimes of what the OAuth issuer mints. Access tokens are short because\nthey are bearer credentials with no revocation check on the hot path;\ncodes and device codes are short because they are one-shot handoffs.",
      "type": "object",
      "properties": {
        "access_ttl_secs": {
          "type": "integer",
          "format": "uint64",
          "default": 3600,
          "minimum": 0
        },
        "cli_client_ids": {
          "description": "Clients whose access tokens may perform *any* operation their\npermissions and granted scopes allow, rather than only the coding-agent\nbaseline admission holds every other OAuth client to.\n\nEmpty by default, which is the safe end: an access token is a\ncredential a user handed to somebody else's program, so it starts able\nto list models, count tokens, generate, stream and compact only when\ntheir scopes were granted. An operator names a first-party CLI here once they accept that it\nspeaks for the user across the whole API. See\n[`admission::permission`](crate::admission::permission).",
          "type": "array",
          "default": [],
          "items": {
            "type": "string"
          }
        },
        "code_ttl_secs": {
          "type": "integer",
          "format": "uint64",
          "default": 300,
          "minimum": 0
        },
        "device_ttl_secs": {
          "type": "integer",
          "format": "uint64",
          "default": 900,
          "minimum": 0
        },
        "refresh_ttl_secs": {
          "type": "integer",
          "format": "uint64",
          "default": 2592000,
          "minimum": 0
        }
      },
      "additionalProperties": false
    },
    "StoreBackendConfig": {
      "description": "Where the durable configuration and usage live. Which variants a build can\nactually open depends on the backend features the binary was built with;\nan unsupported one is refused when the connection is made, not here.",
      "oneOf": [
        {
          "description": "A local file, resolved against `data_dir` when relative.",
          "type": "object",
          "properties": {
            "kind": {
              "type": "string",
              "const": "sqlite"
            },
            "path": {
              "type": "string"
            }
          },
          "required": [
            "kind",
            "path"
          ]
        },
        {
          "description": "Any SeaORM connection string (`postgres://`, `mysql://`, `sqlite://`).",
          "type": "object",
          "properties": {
            "dsn": {
              "type": "string"
            },
            "kind": {
              "type": "string",
              "const": "url"
            }
          },
          "required": [
            "kind",
            "dsn"
          ]
        },
        {
          "description": "A Cloudflare D1 binding name, resolved from the Worker environment.",
          "type": "object",
          "properties": {
            "binding": {
              "type": "string"
            },
            "kind": {
              "type": "string",
              "const": "d1"
            }
          },
          "required": [
            "kind",
            "binding"
          ]
        },
        {
          "type": "object",
          "properties": {
            "kind": {
              "type": "string",
              "const": "libsql"
            },
            "token": {
              "type": [
                "string",
                "null"
              ]
            },
            "url": {
              "type": "string"
            }
          },
          "required": [
            "kind",
            "url"
          ]
        }
      ]
    }
  }
}
