# Generated reference - see docs/api/README.md for how this is produced,
# validated (go test ./internal/server) and served (GET /api/openapi.yaml).
# Do not hand-edit paths/schemas without also updating internal/api/api.go's
# routes or internal/model's structs to match.
openapi: 3.1.0
info:
  title: go-proxy-manager admin API
  version: 0.1.0
  summary: 'REST API for the go-proxy-manager admin panel: config CRUD, settings, backup/restore, DNS
    sync, Ingress discovery, and admin auth.'
  description: 'Covers the admin API surface only (port 8081 by default), not the public proxy
    listeners that serve proxied traffic. Every non-2xx response uses the uniform `{"error": "..."}` shape
    (see #/components/schemas/Error), except the plain-text/HTML auth endpoints noted individually below.


    Authentication: an admin session cookie (`sessionCookie`) or a scoped API bearer token (`bearerAuth`)
    - either satisfies every operation below unless noted. A session''s access is governed by its role
    (`user` is read-only: every GET under /api/ plus GET /api/me, and 403 on every write and on
    anything under /api/api-tokens - GET /api/config omits `apiTokens` for it; `admin` is unconstrained);
    a token''s access is additionally
    governed by its granted scopes, shown per-operation as `x-scope` (`admin` satisfies every scope check;
    a concrete `<plural>:write` implies `<plural>:read`; `*:read`/`*:write` must be granted literally
    - a resource-scoped grant does not satisfy a wildcard requirement). Mutating session requests additionally
    require the `X-CSRF-Token` header (double-submit against the value GET /api/me returns); bearer requests
    never need it.


    HA: on an instance running GPM_HA_ROLE=follower, every write (any POST/PUT/DELETE under /api/) answers
    503 rather than being applied; reads are unaffected. See docs/ha.md.'
  license:
    name: See repository LICENSE
servers:
- url: http://localhost:8081
  description: Admin panel (default). Bind to loopback or front with your own ingress - do not
    expose to the internet.
tags:
- name: Auth
  description: Login/logout and the current principal.
- name: Meta
  description: Health, version, capabilities, whole-config reads, logs, this spec.
- name: Settings
  description: The singleton application settings object.
- name: Backup & restore
  description: Whole-config export/import as a portable archive.
- name: DNS sync
  description: Pi-hole / Cloudflare CNAME reconciliation for opted-in hosts.
- name: Ingress discovery
  description: Kubernetes Ingress -> managed proxy host reconciliation.
- name: Docker discovery
  description: Labelled Docker container -> managed proxy host reconciliation.
- name: API tokens
  description: Scoped bearer credentials for automation.
- name: Profiling (opt-in)
  description: net/http/pprof, only mounted with GPM_PPROF=1.
- name: Metrics (opt-in)
  description: Prometheus text exposition, only mounted with GPM_METRICS=1.
- name: Proxy hosts
- name: Redirect hosts
- name: Stream hosts
- name: Parked hosts
- name: Certificates
- name: Client CAs
- name: DNS providers
- name: Identity providers
- name: Upstream groups
- name: Access lists
- name: Middlewares
- name: Webhooks
  description: Outbound lifecycle notification targets (settings.webhooks) - delivery state and test sends.
- name: Notifications
  description: Outbound alert targets (settings.notifications, ntfy/Discord/generic) - delivery state
    and test sends.
security:
- sessionCookie: []
- bearerAuth: []
components:
  securitySchemes:
    sessionCookie:
      type: apiKey
      in: cookie
      name: gpm_session
      description: 'Admin browser session, issued by /auth/login (OIDC) or /auth/local (bcrypt).
        Cookie is HttpOnly and SameSite=Lax. It is named __Host-gpm_session when the cookie is
        issued Secure, and gpm_session otherwise; both names are accepted on the way in, so a
        session survives the deployment moving between the two. GPM_COOKIE_SECURE=auto (the
        default) decides per request: Secure when the request reached gpm over TLS, when a trusted
        proxy asserted X-Forwarded-Proto: https, or when settings.externalBaseURL is an https URL.
        GPM_COOKIE_SECURE=1 always sets Secure and =0 never does. A session principal has whatever
        access its role (admin/user) grants and is unaffected by API-token scopes. Mutating requests
        also need the X-CSRF-Token header.'
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: gpm_<random>
      description: A scoped API token (config kind api-tokens), created via PUT /api/api-tokens/{name}.
        Every token principal is admin-role by construction; real authorization is per-route scope checking
        (see each operation's x-scope). No CSRF header is used or required.
  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
          description: Human-readable error message.
      required:
      - error
      description: Uniform error shape returned by every endpoint on a non-2xx response.
    Secret:
      type: string
      description: 'A secret-valued field. On write, must be a ${ENV:NAME} or ${FILE:/path} placeholder
        (a literal value is accepted for local dev but never committed as-is - see docs/configuration.md#secrets).
        On read: a placeholder returns verbatim, an empty secret returns "", and a literal non-empty value
        is always redacted to "***".'
    ObjectMeta:
      type: object
      properties:
        name: &id001
          type: string
          description: Identity and YAML filename. Lowercase alphanumeric plus -_. , must start/end alphanumeric.
        displayName: &id002
          type: string
          description: Optional human label.
        labels: &id003
          type: object
          properties: {}
          description: Arbitrary key/value metadata. `gpm.rake.pro/managed-by` is reserved for automated
            reconcilers.
          additionalProperties:
            type: string
        tags: &id004
          type: array
          items:
            type: string
          description: Free-form grouping/filter labels.
        disabled: &id005
          type: boolean
          description: Keep the object in config but exclude it from the running proxy listeners. Default false.
        createdAt: &id006
          type: string
          description: 'Creation timestamp, resolved by the store. On CREATE (no object with this
            kind/name exists yet) a client-supplied createdAt is honoured if present, so an importer
            or a migration can restore historical timestamps; omitted, it is set to server time. On
            UPDATE the stored value is preserved and any createdAt in the request body is ignored -
            the server''s stored value always wins, so a save can never move an existing object''s
            creation time.'
          format: date-time
        updatedAt: &id007
          type: string
          description: Set by the store on every write. Read-only.
          format: date-time
    Upstream:
      type: object
      properties:
        scheme:
          type: string
          description: http or https.
          enum:
          - http
          - https
        host:
          type: string
        port:
          type: integer
          minimum: 1
          maximum: 65535
        path:
          type: string
          description: Base path prefixed to every forwarded request (/api + /v1/x -> /api/v1/x). Absolute,
            no dot-segments, no query string. Applied after any location prefix strip and any rewrite
            middleware. Empty forwards the path unchanged.
        hostHeader:
          type: string
          description: 'Host header sent upstream: empty keeps the client Host (default), "upstream" sends
            the upstream''s own host:port, anything else is sent literally (a hostname, optionally host:port).'
      required:
      - scheme
      - host
      - port
    HSTS:
      type: object
      properties:
        enabled:
          type: boolean
        maxAge:
          type: integer
          description: Seconds.
        includeSubdomains:
          type: boolean
        preload:
          type: boolean
    ClientCertHeaders:
      type: object
      properties:
        subjectHeader:
          type: string
          description: Overrides the header carrying the certificate subject (RFC 2253). Empty selects
            X-Client-Cert-Subject.
        san:
          type: boolean
          description: Send X-Client-Cert-SAN.
        serial:
          type: boolean
          description: Send X-Client-Cert-Serial (lower-case hex).
        fingerprint:
          type: boolean
          description: Send X-Client-Cert-Fingerprint (SHA-256, lower-case hex).
    ClientAuth:
      type: object
      properties:
        caRef:
          type: string
          description: Name of the ClientCA trust anchor.
        mode:
          type: string
          description: require (reject unverified) or optional (verify if presented). Default require.
          enum:
          - require
          - optional
        identityHeaders:
          $ref: '#/components/schemas/ClientCertHeaders'
      required:
      - caRef
    TLSSettings:
      type: object
      properties:
        certificateRef:
          type: string
          description: Name of a Certificate object.
        forceSSL:
          type: boolean
          description: Redirect http to https.
        hsts:
          $ref: '#/components/schemas/HSTS'
        minTLSVersion:
          type: string
          description: 1.2 (default) or 1.3.
          enum:
          - ''
          - '1.2'
          - '1.3'
        clientAuth:
          $ref: '#/components/schemas/ClientAuth'
      description: 'Requires forceSSL: true when clientAuth is set (mTLS must never be servable in the
        clear).'
    HostTimeouts:
      type: object
      properties:
        connectSeconds:
          type: integer
          description: 0-3600. 0 uses the shared transport default.
          minimum: 0
          maximum: 3600
        readSeconds:
          type: integer
          description: 0-3600. Time-to-first-byte only, not a streaming-body cap.
          minimum: 0
          maximum: 3600
    Location:
      type: object
      properties:
        path:
          type: string
        upstream:
          $ref: '#/components/schemas/Upstream'
        upstreamGroupRef:
          type: string
          description: Mutually exclusive with upstream.
        stripPrefix:
          type: boolean
          description: Remove this location's matched prefix before forwarding (/app/foo -> /foo, /app ->
            /). Applied inside the whole security chain, which still evaluates the original client path.
            Default false.
        middlewares:
          type: array
          items:
            type: string
        accessLists:
          type: array
          items:
            type: string
        auth:
          allOf:
          - $ref: '#/components/schemas/AuthMiddleware'
          description: >-
            Inline auth gate for this path, with no Middleware object. Same shape,
            validation and handler as a type=auth middleware, stacked on top of
            the host's own inline block and evaluated before any auth middleware
            this location or its host references.
        rateLimit:
          allOf:
          - $ref: '#/components/schemas/RateLimitMiddleware'
          description: >-
            Inline rate limit for this path, with no Middleware object. Same
            shape, validation and limiter as a type=rate-limit middleware,
            stacked on top of the host's own inline block.
      required:
      - path
    DNSSyncPolicy:
      type: object
      properties:
        lanDirect:
          type: boolean
          description: Publish a local (Pi-hole) CNAME.
        publicCname:
          type: boolean
          description: Publish a public (Cloudflare) CNAME.
    ProxyHost:
      type: object
      properties:
        name: *id001
        displayName: *id002
        labels: *id003
        tags: *id004
        disabled: *id005
        createdAt: *id006
        updatedAt: *id007
        domains:
          type: array
          items:
            type: string
          description: At least one required.
        upstream:
          $ref: '#/components/schemas/Upstream'
        upstreamGroupRef:
          type: string
          description: Name of an UpstreamGroup. Exactly one of upstream/upstreamGroupRef is set.
        robotsNoIndex:
          type: boolean
          description: 'Emit X-Robots-Tag: noindex, nofollow.'
        maintenance:
          type: boolean
          description: >-
            Take this host out of service without removing or disabling it: gpm
            answers every request itself with a 503 maintenance response (with a
            Retry-After from settings.maintenance) and never dials the upstream.
            The host keeps its domains, certificate and DNS records, so clearing
            the flag restores service. settings.maintenance.enabled turns
            maintenance on fleet-wide regardless of this flag - the global switch
            wins over a per-host false. Operator-owned: Kubernetes Ingress
            discovery carries this value forward across a reconcile instead of
            deriving it, exactly as it does for disabled.
        timeouts:
          $ref: '#/components/schemas/HostTimeouts'
        tls:
          $ref: '#/components/schemas/TLSSettings'
        dns:
          $ref: '#/components/schemas/DNSSyncPolicy'
        middlewares:
          type: array
          items:
            type: string
          description: Host-wide, applied before any location-scoped ones.
        accessLists:
          type: array
          items:
            type: string
        auth:
          allOf:
          - $ref: '#/components/schemas/AuthMiddleware'
          description: >-
            Gate this host through an identity provider with NO Middleware
            object. Identical shape, validation and proxy-listener handler to a
            type=auth middleware; Middleware objects remain the reuse path for a
            gate shared across a fleet. Not mutually exclusive with middlewares:
            the inline block runs at the auth chain position, before any auth
            middleware this host references, and every gate must pass.
        rateLimit:
          allOf:
          - $ref: '#/components/schemas/RateLimitMiddleware'
          description: >-
            Throttle this host with a per-client-IP token bucket with NO
            Middleware object. Identical shape, validation and limiter to a
            type=rate-limit middleware, applied at the rate-limit chain position
            (outermost) before any rate-limit middleware this host references.
        locations:
          type: array
          items:
            $ref: '#/components/schemas/Location'
        securityHeaders:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/SecurityHeaderValue'
          description: >-
            Per-host response headers, merged per key over
            settings.securityHeaders (the host value - and its scope - wins for a
            header it names; omitted headers fall through to the settings
            default). Each header carries a scope (all / generated-only /
            proxied-only) selecting which responses it lands on; the value is a
            bare string (scope all) or a {value, scope} object. Applied
            set-if-absent, so a proxied upstream's own header is never clobbered.
            Strict-Transport-Security is not settable here (the per-host tls.hsts
            setting owns it); hop-by-hop headers, CR/LF in names/values and an
            unknown scope are rejected.
        stripResponseHeaders:
          type: array
          items:
            type: string
          description: >-
            Response headers removed from what this host's upstream sends, in
            addition to settings.stripResponseHeaders (the effective set is the
            UNION of the two - a host can only strip more, never re-expose a
            header the fleet strips). Matching is case-insensitive. Invalid
            header names, hop-by-hop headers, the response-semantic headers
            (Content-Type/-Length/-Encoding, Vary, Location, Sec-WebSocket-*)
            and case-insensitive duplicates are rejected.
        trustedProxies:
          type: array
          nullable: true
          items:
            type: string
          description: >-
            CIDRs or bare IPs of the L7 proxies whose X-Forwarded-For is believed
            when deriving this host's client IP. REPLACES settings.trustedProxies
            for this host rather than extending it. Absent (or null) inherits the
            fleet-wide list; PRESENT AND EMPTY ([]) trusts nobody for this host
            whatever the fleet default is - the connection peer is the client and
            X-Forwarded-For is never read. The derived address is
            what access lists, geo, rate limits, every allowFrom exemption, the
            access log and the upstream X-Forwarded-For/X-Real-IP all compare.
            Entries must parse as a CIDR or an IP address; 0.0.0.0/0 and ::/0 are
            accepted but warned about at load.
      required:
      - name
      - domains
      description: Terminates TLS for one or more domains and reverse-proxies to an upstream.
    RedirectHost:
      type: object
      properties:
        name: *id001
        displayName: *id002
        labels: *id003
        tags: *id004
        disabled: *id005
        createdAt: *id006
        updatedAt: *id007
        domains:
          type: array
          items:
            type: string
          description: At least one required.
        targetScheme:
          type: string
          description: http | https | auto.
          enum:
          - ''
          - http
          - https
          - auto
        targetDomain:
          type: string
        statusCode:
          type: integer
          description: 301|302|307|308. Default 301.
          enum:
          - 0
          - 301
          - 302
          - 307
          - 308
        preservePath:
          type: boolean
        tls:
          $ref: '#/components/schemas/TLSSettings'
      required:
      - name
      - domains
      - targetDomain
      description: Issues HTTP redirects for its domains.
    StreamHost:
      type: object
      properties:
        name: *id001
        displayName: *id002
        labels: *id003
        tags: *id004
        disabled: *id005
        createdAt: *id006
        updatedAt: *id007
        listenPort:
          type: integer
          minimum: 1
          maximum: 65535
        protocol:
          type: string
          description: tcp | udp | both.
          enum:
          - tcp
          - udp
          - both
        target:
          $ref: '#/components/schemas/StreamTarget'
        tls:
          $ref: '#/components/schemas/StreamTLS'
        accessLists:
          type: array
          description: 'AccessList names gating the connection on the client IP, evaluated
            before any backend dial. Only the ip rules and geo apply at L4; a list with
            basicAuth users is rejected.'
          items:
            type: string
      required:
      - name
      - listenPort
      - protocol
      - target
      description: Forwards raw TCP/UDP from a listen port to a backend.
    StreamTarget:
      type: object
      description: 'The backend a stream host forwards to. Mirrors Upstream''s host/port
        vocabulary minus the scheme: a raw TCP/UDP stream carries an arbitrary protocol,
        so an http/https scheme has no meaning for it.'
      properties:
        host:
          type: string
        port:
          type: integer
          minimum: 1
          maximum: 65535
      required:
      - host
      - port
    StreamTLS:
      type: object
      description: 'TLS awareness for a TCP stream host: SNI routing on a shared listen
        port, with the handshake either forwarded untouched or terminated at gpm. Requires
        protocol tcp.'
      properties:
        mode:
          type: string
          description: passthrough (route on SNI, never decrypt) | terminate (handshake
            at gpm, plaintext upstream).
          enum:
          - passthrough
          - terminate
        sniMatch:
          type: array
          description: 'Server names this host claims: exact, or a single-label *.suffix
            wildcard. Required for every host that shares its listen port.'
          items:
            type: string
        certificateRef:
          type: string
          description: Certificate to present. Required in terminate mode, forbidden in
            passthrough.
      required:
      - mode
    ParkedHost:
      type: object
      properties:
        name: *id001
        displayName: *id002
        labels: *id003
        tags: *id004
        disabled: *id005
        createdAt: *id006
        updatedAt: *id007
        domains:
          type: array
          items:
            type: string
          description: At least one required.
        statusCode:
          type: integer
          description: Default 404.
        tls:
          $ref: '#/components/schemas/TLSSettings'
      required:
      - name
      - domains
      description: 'Serves a 404 (or custom status) for claimed domains: reserves a name
        without serving anything, absorbing unmatched vhosts.'
    ACMESpec:
      type: object
      properties:
        email:
          type: string
        directoryURL:
          type: string
          description: Default Let's Encrypt production.
        keyType:
          type: string
          description: ecdsa only (default when empty); rsa is not implemented and is rejected.
          enum:
          - ''
          - ecdsa
        challenge:
          type: string
          description: dns-01 is the only supported challenge.
          enum:
          - ''
          - dns-01
        dnsProvider:
          type: string
          description: Name of a DNSProvider object.
      required:
      - email
      - dnsProvider
    CustomCertSpec:
      type: object
      properties:
        certFile:
          type: string
          description: PEM chain, relative to the cert store.
        keyFile:
          type: string
          description: PEM private key, relative to the cert store.
      required:
      - certFile
      - keyFile
    Certificate:
      type: object
      properties:
        name: *id001
        displayName: *id002
        labels: *id003
        tags: *id004
        disabled: *id005
        createdAt: *id006
        updatedAt: *id007
        type:
          type: string
          description: custom | acme.
          enum:
          - custom
          - acme
        domains:
          type: array
          items:
            type: string
          description: At least one required.
        acme:
          $ref: '#/components/schemas/ACMESpec'
        custom:
          $ref: '#/components/schemas/CustomCertSpec'
        notBefore:
          type: string
          format: date-time
          readOnly: true
          description: Leaf certificate's validity start. Absent for an acme certificate with no
            completed order yet (state pending).
        notAfter:
          type: string
          format: date-time
          readOnly: true
          description: Leaf certificate's expiry. Absent for an acme certificate with no completed
            order yet (state pending).
        daysRemaining:
          type: integer
          readOnly: true
          description: Whole days until notAfter, negative once expired. Absent alongside notAfter.
        issuer:
          type: string
          readOnly: true
          description: Leaf certificate's issuer common name. Absent alongside notAfter.
        state:
          type: string
          readOnly: true
          enum:
          - valid
          - expiring
          - expired
          - pending
          - error
          description: 'valid: notAfter is more than 30 days out. expiring: at or inside 30 days.
            expired: past notAfter. pending: acme certificate, no order has completed yet. error: the
            last attempt (ACME order, or reading a custom certificate file) failed - see lastError.'
        lastError:
          type: string
          readOnly: true
          description: 'Most recent renewal/read failure. Empty or absent means the last attempt (if
            any) succeeded. The text depends on the caller: an **admin** gets the raw ACME or
            file/parse message, truncated to 300 characters; any **non-admin** caller (the read-only
            viewer role, or a certificates:read token) gets the same short classified reason
            GET /health uses, e.g. ''dns-01 challenge or DNS provider failure''. The raw text can
            embed a third party''s HTTP response body, and certificates:read is held by the viewer
            role, so it is not disclosed any more widely than /health does.'
        lastAttempt:
          type: string
          format: date-time
          readOnly: true
          description: When the ACME manager last attempted to issue/renew this certificate. Acme
            certificates only; absent for custom and for an acme certificate the manager has not
            looked at yet (e.g. an HA follower, which does not run it).
        sans:
          type: array
          items:
            type: string
          readOnly: true
          description: Leaf certificate's subject alternative names (DNS names and IPs). Absent
            alongside notAfter.
      required:
      - name
      - type
      - domains
      description: Desired state for a TLS certificate, plus read-only status fields (notBefore
        through sans) the server derives from the certificate store and the ACME manager's in-memory
        state on every read. None of the status fields are accepted on write.
    DNSProvider:
      type: object
      properties:
        name: *id001
        displayName: *id002
        labels: *id003
        tags: *id004
        disabled: *id005
        createdAt: *id006
        updatedAt: *id007
        provider:
          type: string
          enum:
          - cloudflare
          - digitalocean
          - hetzner
          - desec
          - rfc2136
          - acme-dns
          description: >-
            DNS-01 solver to use. cloudflare, digitalocean, hetzner and desec are
            token-authenticated REST providers; rfc2136 sends TSIG-signed dynamic
            updates to any nameserver; acme-dns writes to a joohoi/acme-dns
            account reached through a CNAME.
        config:
          type: object
          properties: {}
          description: >-
            Provider-specific credential map. The REST providers take apiToken,
            e.g. {"apiToken": "${FILE:/run/secrets/cf_token}"}. rfc2136 takes
            server, zone, tsigKeyName, tsigSecret and optional tsigAlgorithm,
            ttl, transport, timeout. acme-dns takes baseURL, username, password
            and subdomain, plus optional allowInsecureLocal ("true") to permit a
            loopback baseURL - a link-local baseURL (the cloud metadata range) is
            always refused, at write time and again at connect time.
          additionalProperties:
            $ref: '#/components/schemas/Secret'
      required:
      - name
      - provider
      description: Reusable credentials for an ACME DNS-01 provider.
    ClientCA:
      type: object
      properties:
        name: *id001
        displayName: *id002
        labels: *id003
        tags: *id004
        disabled: *id005
        createdAt: *id006
        updatedAt: *id007
        caPEM:
          type: string
          description: PEM-encoded CA certificate bundle. Public; may be inline or a placeholder.
        crlFile:
          type: string
          description: Optional CRL path, relative to the cert store. Mutually exclusive with crlPEM.
            Re-read on reload and mtime watch.
        crlPEM:
          type: string
          description: Optional inline PEM-encoded CRL. Mutually exclusive with crlFile.
        crlPolicy:
          type: string
          description: fail-closed (default, reject on an unusable CRL) | fail-open.
          enum:
          - ''
          - fail-closed
          - fail-open
        caKeyFile:
          type: string
          description: Optional signing key path, relative to the cert store (absolute and .. paths are
            rejected). Set it to let this CA issue client certificates. Mutually exclusive with caKeyPEM.
        caKeyPEM:
          allOf:
          - $ref: '#/components/schemas/Secret'
          description: Optional inline signing key, as a ${FILE:...} / ${ENV:...} placeholder (a literal
            key is refused at commit). Mutually exclusive with caKeyFile.
        expiryWarningDays:
          type: integer
          minimum: 0
          maximum: 3650
          default: 30
          description: How far ahead a certificate issued by this CA is reported as expiring. 0 or unset
            uses the 30-day default. Advisory only - nothing renews on its own. Note PUT replaces the
            whole object, so omitting it on a write resets it to the default.
      required:
      - name
      - caPEM
      description: Trust anchor for mTLS client-certificate verification. With a signing key it can also
        issue client certificates (POST /api/client-cas/{name}/issue). Either bring your own CA (paste
        caPEM, optionally point caKeyFile at a key you placed in the certificate store) or have gpm
        generate one (POST /api/client-cas/{name}/generate), which fills both fields in for you.
    ClientCertRequest:
      type: object
      properties:
        commonName:
          type: string
          maxLength: 64
          description: Subject common name of the issued certificate. Also the download filename stem,
            sanitized.
        validityDays:
          type: integer
          minimum: 0
          maximum: 3650
          default: 365
          description: Certificate lifetime in days. Omit it, or send 0, for the 365-day default;
            any other value must be 1-3650. A negative value is rejected.
        password:
          type: string
          minLength: 12
          description: Encrypts the returned PKCS#12 bundle. Required, at least 12 characters, never
            stored or logged. The floor exists because the bundle uses the legacy PKCS#12 encoder for
            device compatibility, which derives its integrity MAC with a single KDF iteration - the
            password is effectively the only protection once the file leaves gpm.
        sans:
          type: array
          maxItems: 32
          items:
            type: string
          description: Optional subject alternative names, printable ASCII only (a certificate stores
            them as IA5String, so an internationalised domain must be supplied punycoded as "xn--...").
            An entry that parses as an IP becomes an IP SAN, one containing "@" an email SAN, anything
            else a DNS SAN.
      required:
      - commonName
      - password
      description: One client-certificate issuance.
    ClientCAGenerateRequest:
      type: object
      properties:
        commonName:
          type: string
          maxLength: 64
          description: Subject common name of the generated CA. Defaults to the ClientCA object name.
        validityDays:
          type: integer
          minimum: 0
          maximum: 7300
          default: 3650
          description: CA lifetime in days. Omit it, or send 0, for the ten-year default; any other
            value must be 1-7300. A CA is a trust anchor pinned into device configuration, so rotating
            it means re-provisioning every device - hence the long default.
        organization:
          type: string
          maxLength: 64
          description: Optional subject organization (O).
      description: One CA generation. Every field is optional; an empty body is valid.
    ClientCertRenewRequest:
      type: object
      properties:
        password:
          type: string
          minLength: 12
          description: Encrypts the renewed PKCS#12 bundle. Required, at least 12 characters, never
            stored or logged. Same floor and reasoning as issuance.
        validityDays:
          type: integer
          minimum: 0
          maximum: 3650
          default: 365
          description: Lifetime of the renewed certificate in days. Omit it, or send 0, for the 365-day
            default; any other value must be 1-3650. A negative value is rejected.
      required:
      - password
      description: One renewal. The common name and SANs come from the record, not the request.
    IssuedClientCert:
      type: object
      properties:
        ca:
          type: string
        commonName:
          type: string
        sans:
          type: array
          items:
            type: string
        serial:
          type: string
          description: Lower-case hex.
        notBefore:
          type: string
          format: date-time
        notAfter:
          type: string
          format: date-time
        issuedAt:
          type: string
          format: date-time
        supersededBy:
          type: string
          description: Serial of the renewal that replaced this certificate, if any. Superseding does
            not revoke - the old certificate stays valid until it expires.
        supersededAt:
          type: string
          format: date-time
        status:
          type: string
          description: Derived at request time from notAfter and the CA's expiryWarningDays.
          enum:
          - ok
          - expiring
          - expired
        daysRemaining:
          type: integer
          description: Whole days until expiry, floored; negative once expired.
      description: One client certificate this CA issued. Carries no key material.
    IssuedClientCertList:
      type: object
      properties:
        expiryWarningDays:
          type: integer
          description: The CA's effective warning window, so a client need not re-derive the default.
        certificates:
          type: array
          items:
            $ref: '#/components/schemas/IssuedClientCert'
      required:
      - expiryWarningDays
      - certificates
      description: Every client certificate this CA has issued, newest first.
    OIDCSpec:
      type: object
      properties:
        issuerURL:
          type: string
        clientID:
          type: string
        clientSecret:
          $ref: '#/components/schemas/Secret'
        scopes:
          type: array
          items:
            type: string
          description: Default [openid, profile, email, groups].
        usePKCE:
          type: boolean
          description: Default true.
        requireVerifiedEmail:
          type: boolean
      required:
      - issuerURL
      - clientID
    ForwardAuthSpec:
      type: object
      properties:
        trustedProxies:
          type: array
          items:
            type: string
          description: CIDRs allowed to assert identity headers. Required, non-empty.
        userHeader:
          type: string
          description: e.g. X-authentik-username.
        emailHeader:
          type: string
        nameHeader:
          type: string
        groupsHeader:
          type: string
        groupsDelimiter:
          type: string
          description: Default ",".
        amrHeader:
          type: string
          description: Authentication-methods header, drives MFA delegation. Empty asserts no MFA.
      required:
      - trustedProxies
      - userHeader
    AuthRequestSpec:
      type: object
      properties:
        outpostURL:
          type: string
          description: Base URL of the auth server, e.g. http://auth-outpost:9000.
        pathPrefix:
          type: string
          description: Default /outpost.goauthentik.io.
        authPath:
          type: string
          description: Default pathPrefix + /auth/nginx.
        copyHeaders:
          type: array
          items:
            type: string
          description: Response headers copied from the auth subrequest onto the upstream request. Default
            the Authentik X-authentik-* set.
      required:
      - outpostURL
    RoleMapping:
      type: object
      properties:
        groupsClaim:
          type: string
          description: Default "groups".
        adminGroups:
          type: array
          items:
            type: string
        userGroups:
          type: array
          items:
            type: string
        defaultRole:
          type: string
          description: Applied when no group matches. "" (deny) | user | admin.
          enum:
          - ''
          - user
          - admin
        allowDefaultAdmin:
          type: boolean
          description: 'Required true to set defaultRole: admin (otherwise every authenticated user would
            get admin).'
    IdentityProvider:
      type: object
      properties:
        name: *id001
        displayName: *id002
        labels: *id003
        tags: *id004
        disabled: *id005
        createdAt: *id006
        updatedAt: *id007
        type:
          type: string
          description: oidc | forward-auth | auth-request.
          enum:
          - oidc
          - forward-auth
          - auth-request
        oidc:
          $ref: '#/components/schemas/OIDCSpec'
        forwardAuth:
          $ref: '#/components/schemas/ForwardAuthSpec'
        authRequest:
          $ref: '#/components/schemas/AuthRequestSpec'
        roleMapping:
          $ref: '#/components/schemas/RoleMapping'
      required:
      - name
      - type
      description: An auth source. Can drive admin-panel login and/or gate proxy hosts via an auth middleware.
    Stickiness:
      type: object
      properties:
        cookie:
          type: string
          description: Default gpm-sticky-<group>.
        ttl:
          type: string
          description: Go duration ("30m", "12h"), or a whole-day "3d" suffix. Required, > 0.
      required:
      - ttl
    HealthCheck:
      type: object
      properties:
        path:
          type: string
          description: HTTP GET path; empty keeps a plain TCP-connect probe.
        intervalSeconds:
          type: integer
          description: Default 5.
        timeoutSeconds:
          type: integer
          description: Default 3.
        rise:
          type: integer
          description: Consecutive successes to mark healthy again. Default 2.
        fall:
          type: integer
          description: Consecutive failures to mark unhealthy. Default 2.
    GroupUpstream:
      type: object
      properties:
        scheme:
          type: string
          enum:
          - http
          - https
        host:
          type: string
        port:
          type: integer
          minimum: 1
          maximum: 65535
        path:
          type: string
          description: Base path prefixed to every request sent to THIS member, applied by the failover
            transport. Same rules as Upstream.path. healthCheck.path is used verbatim and is not prefixed.
        hostHeader:
          type: string
          description: Host header sent to THIS member. Same values as Upstream.hostHeader.
        weight:
          type: integer
          description: Relative share for round-robin/least-connections, 1-256. 0 defaults to 1. Ignored
            by failover/ip-hash.
      required:
      - scheme
      - host
      - port
    UpstreamGroup:
      type: object
      properties:
        name: *id001
        displayName: *id002
        labels: *id003
        tags: *id004
        disabled: *id005
        createdAt: *id006
        updatedAt: *id007
        upstreams:
          type: array
          items:
            $ref: '#/components/schemas/GroupUpstream'
          description: At least one required.
        policy:
          type: string
          description: failover (default) | round-robin | least-connections | ip-hash.
          enum:
          - ''
          - failover
          - round-robin
          - least-connections
          - ip-hash
        stickiness:
          $ref: '#/components/schemas/Stickiness'
        healthCheck:
          $ref: '#/components/schemas/HealthCheck'
      required:
      - name
      - upstreams
      description: An ordered set of interchangeable backends a proxy host can forward to instead of a
        single upstream.
    BasicAuthUser:
      type: object
      properties:
        username:
          type: string
        passwordHash:
          type: string
          description: bcrypt hash ($2a$/$2b$/$2y$, 60 characters). Required unless password is sent
            instead. Anything that is not a bcrypt hash is refused at write time.
        password:
          type: string
          format: password
          writeOnly: true
          description: Plaintext password, accepted on a write of an auth spec - a Middleware of type
            auth, or the inline auth block on a ProxyHost or one of its locations. The API hashes it
            with bcrypt and stores passwordHash; it is never persisted and never echoed back. Omit it
            to keep the stored hash unchanged. Merged by position, so send users in the order they are
            displayed.
      required:
      - username
    IPRule:
      type: object
      properties:
        action:
          type: string
          enum:
          - allow
          - deny
        cidr:
          type: string
          description: e.g. 10.0.0.0/8 or 1.2.3.4/32. Exactly one of cidr or source must be set.
        source:
          type: string
          description: Names an entry in the list's sources; the rule matches every network in that
            source's last fetched set. Exactly one of cidr or source must be set. A source that has
            never been fetched resolves to the empty set, so the rule matches nothing.
        paths:
          type: array
          items:
            type: string
          description: Exact request paths the rule is limited to (clean, absolute, no query). When
            empty the rule applies to every request. Allow-only - action deny alongside paths is
            refused, since an exact match cannot be relied on to deny. Matching is case-sensitive
            with no trailing-slash folding. A list carrying a path-scoped rule cannot be attached to
            a stream host.
        methods:
          type: array
          items:
            type: string
          description: Upper-case HTTP methods the path-scoped rule covers. Only valid with paths;
            empty means GET and HEAD.
      required:
      - action
    AccessListGeo:
      type: object
      properties:
        countryAllow:
          type: array
          items:
            type: string
          description: ISO-3166-1 alpha-2 whitelist. Takes priority over countryDeny.
        countryDeny:
          type: array
          items:
            type: string
          description: ISO-3166-1 alpha-2 denylist. Only consulted when countryAllow is empty.
        onUnknown:
          type: string
          description: allow | deny, for an IP with no country in the database.
          enum:
          - ''
          - allow
          - deny
      description: Requires an operator-supplied GeoIP database (GPM_GEOIP_DB); none is bundled.
    AccessList:
      type: object
      properties:
        name: *id001
        displayName: *id002
        labels: *id003
        tags: *id004
        disabled: *id005
        createdAt: *id006
        updatedAt: *id007
        satisfyAny:
          type: boolean
          deprecated: true
          description: 'Deprecated, removed in v2: OR- vs AND-evaluation across basicAuth and IP/geo.
            Migrate with POST /api/access-lists/{name}/migrate-basic-auth; it becomes allowFrom on the
            auth middleware. Still honoured.'
        basicAuth:
          type: array
          deprecated: true
          items:
            $ref: '#/components/schemas/BasicAuthUser'
          description: 'Deprecated, removed in v2: use an auth middleware with mode basic. Migrate with
            POST /api/access-lists/{name}/migrate-basic-auth. Still honoured.'
        rules:
          type: array
          items:
            $ref: '#/components/schemas/IPRule'
          description: Ordered, evaluated top-down.
        defaultAction:
          type: string
          description: Applied when no IP rule matches. Default deny.
          enum:
          - ''
          - allow
          - deny
        sources:
          type: array
          items:
            $ref: '#/components/schemas/AccessListSource'
          description: Remote IP feeds a rule may reference by name. The fetched sets live in the
            committed ledger config/access-list-sources.yaml, never inline here.
        geo:
          $ref: '#/components/schemas/AccessListGeo'
      required:
      - name
      description: Ordered IP allow/deny rules, optional remote IP sources, and optional GeoIP country
        rules over the resolved client IP. The basicAuth/satisfyAny pair is deprecated - put
        username/password gating in an auth middleware with mode basic.
    BasicAuthMigrationAttachment:
      type: object
      properties:
        kind:
          type: string
          description: Always ProxyHost today; a stream host may not reference a basicAuth list.
        name:
          type: string
        path:
          type: string
          description: Location path when the reference is on a location; absent for a host-wide one.
    BasicAuthMigration:
      type: object
      properties:
        accessList:
          type: string
        middleware:
          type: string
          description: Name of the auth middleware created, always "<accessList>-basic".
        users:
          type: array
          items:
            type: string
          description: Usernames carried over. No credential material is echoed.
        allowFrom:
          type: array
          items:
            type: string
          description: Literal allow CIDRs copied onto the middleware. Populated only when the list set
            satisfyAny.
        satisfyAny:
          type: boolean
          description: What the list had, so a reader can see why allowFrom is or is not populated.
        attachTo:
          type: array
          items:
            $ref: '#/components/schemas/BasicAuthMigrationAttachment'
        detachAccessList:
          type: boolean
          description: Whether the access list is REMOVED from those hosts and locations. True only
            when the list set satisfyAny and every rule it carries moved into the middleware's
            allowFrom - the list evaluated as "IP match OR password", so leaving its now-mandatory
            rules attached would lock out password users outside them. False when the list still
            carries a deny rule, a source-backed rule or geo rules, in which case it stays attached
            and warnings says so.
        warnings:
          type: array
          items:
            type: string
          description: Allow rules that could not be represented as allowFrom (source-backed or
            path-scoped) and were therefore not copied, plus a warning when the list stays attached
            and its remaining rules become mandatory.
        plan:
          type: boolean
          description: True for a dry run (?plan=1), which changed nothing.
        commit:
          type: string
          description: Commit the apply produced; absent for a plan.
      description: What the basic-auth migration did, or (with ?plan=1) would do.
    AccessListSource:
      type: object
      properties:
        name:
          type: string
        url:
          type: string
          description: Absolute https URL of a plain-text feed - one IP or CIDR per line, "#" comments
            and blank lines ignored. http is not accepted, and no proxy is honoured. An entry that is
            a default route, broader than /8 (IPv4) or /32 (IPv6), or in a non-public range refuses
            the whole fetch.
        interval:
          type: string
          description: Go duration between re-fetches. Empty means 24h; below 1h is refused.
        maxEntries:
          type: integer
          description: Cap on how many networks the feed may carry. 0 means 10000; a body with more is
            refused whole.
      required:
      - name
      - url
      description: A remote published IP feed referenced by an IPRule's source field.
    AccessListSourceStatus:
      type: object
      properties:
        enabled:
          type: boolean
        lastRun:
          type: string
          format: date-time
        lastSuccess:
          type: string
          format: date-time
        error:
          type: string
        refused:
          type: integer
          description: Sources whose most recent fetch was refused; their previously fetched set is
            still what the proxy listeners serve.
        sources:
          type: array
          items:
            type: object
            properties:
              list:
                type: string
              name:
                type: string
              url:
                type: string
              fetchedAt:
                type: string
                format: date-time
              entryCount:
                type: integer
              lastError:
                type: string
              lastAttempt:
                type: string
                format: date-time
      description: Last access-list source fetch result. Shape mirrored from internal/accesssync; see
        docs/deployment.md#access-list-remote-sources.
    BasicAuthSpec:
      type: object
      properties:
        users:
          type: array
          items:
            $ref: '#/components/schemas/BasicAuthUser'
          description: Accepted credentials. At least one is required; at most 64.
        realm:
          type: string
          maxLength: 128
          description: Realm advertised in the WWW-Authenticate challenge. Empty means the gated host's
            name. Printable ASCII only, with no quote or backslash - it is sent verbatim in a quoted
            header parameter.
      required:
      - users
      description: Credential set for an auth middleware in basic mode. This is where username/password
        gating belongs; the identical users on an AccessList (basicAuth) are deprecated.
    AuthMiddleware:
      type: object
      properties:
        identityProvider:
          type: string
          description: Required except in client-cert mode.
        mode:
          type: string
          description: oidc | forward-auth | auth-request | client-cert | basic. Defaults from the IdP
            type. client-cert and basic name no identityProvider - the TLS handshake and the local
            credential set are their identity sources.
          enum:
          - ''
          - oidc
          - forward-auth
          - auth-request
          - client-cert
          - basic
        requiredRoles:
          type: array
          items:
            type: string
        allowFrom:
          type: array
          items:
            type: string
          description: CIDRs that bypass authentication entirely - no auth subrequest, no client-certificate
            requirement, no password prompt, no identity headers. Supported in auth-request, client-cert
            and basic modes; refused
            in oidc and forward-auth mode, and refused when mode is unset and the referenced provider's
            type resolves to one of those (set mode explicitly). Matched against the client IP the HOST
            resolves, which honours X-Forwarded-For only from that host's trusted proxies - and a
            client-cert middleware contributes none, so on a pure client-cert host this compares the raw
            TCP peer. See docs/configuration.md before placing a proxy address inside one of these
            networks.
        clientCertRoles:
          type: object
          properties: {}
          description: Certificate subject (or bare CN) -> role, for client-cert mode.
          additionalProperties:
            type: string
        basic:
          allOf:
          - $ref: '#/components/schemas/BasicAuthSpec'
          description: Credential set for basic mode. Required there and refused in every other mode.
            requiredRoles is not supported alongside it (a username carries no roles).
    HeadersMiddleware:
      type: object
      properties:
        setRequest:
          type: object
          properties: {}
          additionalProperties:
            type: string
        setResponse:
          type: object
          properties: {}
          additionalProperties:
            type: string
        removeRequest:
          type: array
          items:
            type: string
        removeResponse:
          type: array
          items:
            type: string
    GuardTrigger:
      type: object
      properties:
        paths:
          type: array
          items:
            type: string
        methods:
          type: array
          items:
            type: string
        queryEquals:
          type: object
          properties: {}
          additionalProperties:
            type: string
    GuardMiddleware:
      type: object
      properties:
        triggers:
          type: array
          items:
            $ref: '#/components/schemas/GuardTrigger'
          description: At least one required. Fires when ANY trigger matches.
        allowFrom:
          type: array
          items:
            type: string
          description: CIDRs exempt from the deny.
        denyStatus:
          type: integer
          description: 4xx/5xx. Default 403.
      required:
      - triggers
    RateLimitMiddleware:
      type: object
      properties:
        requestsPerSecond:
          type: number
          description: Legacy shorthand. Mutually exclusive with requests+window.
        requests:
          type: number
          description: Paired with window, e.g. requests:100, window:"1m".
        window:
          type: string
          description: Go duration string.
        burst:
          type: integer
          description: Default ceil(requests).
        allowFrom:
          type: array
          items:
            type: string
          description: CIDRs that bypass rate limiting entirely.
        blockFor:
          type: string
          description: Go duration; once exceeded, further requests are rejected for this long regardless
            of refill.
      description: Per-client-IP token bucket. Exactly one of requestsPerSecond or requests+window must
        be set.
    RewriteRule:
      type: object
      description: One ordered rewrite rule. For prefixRules, from is a path prefix; for regexRules it is
        a Go regular expression implicitly anchored with ^, and to may reference capture groups as $1.
      properties:
        from:
          type: string
        to:
          type: string
      required:
      - from
      - to
    RewriteMiddleware:
      type: object
      description: Upstream-facing request-path rewrite. Rule kinds are evaluated exact, then prefix (longest
        first), then regex (in order); the first match wins and no rule sees another rule's output. Never
        a redirect - method and body are preserved. At least one rule of any kind is required.
      properties:
        replacePath:
          type: object
          properties: {}
          description: Exact request path -> replacement path. No prefix or pattern matching.
          additionalProperties:
            type: string
        prefixRules:
          type: array
          maxItems: 32
          description: Prefix replacements, boundary matched like a location (/reports never captures /reports-evil).
            The longest matching from wins.
          items:
            $ref: '#/components/schemas/RewriteRule'
        regexRules:
          type: array
          maxItems: 32
          description: Regex replacements. Each from is compiled at config load (max 256 characters); a pattern
            that does not compile is a validation error naming the rule index. The matched leading span is
            replaced and the remainder appended unchanged.
          items:
            $ref: '#/components/schemas/RewriteRule'
    BouncerMiddleware:
      type: object
      description: >-
        Deny hook: ask an operator-run bouncer whether the client IP is banned.
        A hook, not a bundled WAF - gpm ships no rules or detection engine.
      properties:
        provider:
          type: string
          description: crowdsec (LAPI bouncer flow) | http (generic deny hook). Default crowdsec.
          enum:
          - crowdsec
          - http
        url:
          type: string
          description: crowdsec - the LAPI base URL. http - the full endpoint to GET.
        apiKey:
          type: string
          description: >-
            Sent as X-Api-Key. Required for crowdsec (cscli bouncers add gpm).
            Store as a ${ENV:...} / ${FILE:...} placeholder; a literal value
            reads back as ***.
        timeout:
          type: string
          description: Go duration bounding one bouncer call. Default 2s.
        cacheTTL:
          type: string
          description: >-
            Go duration a verdict is reused for the same client IP, and the
            stream-mode delta poll interval. Default 60s. An error-derived
            verdict is capped at 5s regardless.
        cacheMaxEntries:
          type: integer
          description: Bound on the per-middleware verdict cache. Default 10000.
        onError:
          type: string
          description: Verdict when the bouncer gives no usable answer. Default fail-open.
          enum:
          - fail-open
          - fail-closed
        denyStatus:
          type: integer
          description: Status for a denied request. Default 403.
        denyWith:
          type: string
          description: Denial body - the custom error page, or a bare status body. Default error-page.
          enum:
          - error-page
          - plain
        allowFrom:
          type: array
          description: Client CIDRs that bypass the bouncer entirely.
          items:
            type: string
        stream:
          type: boolean
          description: >-
            crowdsec only. Pull the whole decision set once and then deltas
            every cacheTTL, so the request path is a local IP/CIDR lookup.
      required:
      - url
    Middleware:
      type: object
      properties:
        name: *id001
        displayName: *id002
        labels: *id003
        tags: *id004
        disabled: *id005
        createdAt: *id006
        updatedAt: *id007
        type:
          type: string
          description: auth | headers | guard | rate-limit | rewrite | bouncer.
          enum:
          - auth
          - headers
          - guard
          - rate-limit
          - rewrite
          - bouncer
        auth:
          $ref: '#/components/schemas/AuthMiddleware'
        headers:
          $ref: '#/components/schemas/HeadersMiddleware'
        guard:
          $ref: '#/components/schemas/GuardMiddleware'
        rateLimit:
          $ref: '#/components/schemas/RateLimitMiddleware'
        rewrite:
          $ref: '#/components/schemas/RewriteMiddleware'
        bouncer:
          $ref: '#/components/schemas/BouncerMiddleware'
      required:
      - name
      - type
      description: A reusable, named processing step referenced by hosts/locations. Exactly one of auth/headers/guard/rateLimit/rewrite
        is set, matching type.
    APIToken:
      type: object
      properties:
        name: *id001
        displayName: *id002
        labels: *id003
        tags: *id004
        disabled: *id005
        createdAt: *id006
        updatedAt: *id007
        scopes: &id008
          type: array
          items:
            type: string
          description: e.g. "proxy-hosts:read", "*:read", "admin". At least one required.
        expiresAt: &id009
          type: string
          description: 'Unset: never expires.'
          format: date-time
        lastUsed: &id010
          type: string
          description: In-memory last-use timestamp, decorated onto GET responses. Resets on restart.
            Never persisted.
          format: date-time
      required:
      - name
      - scopes
      description: A non-interactive bearer credential for the REST API. TokenHash is server-owned and
        never returned.
    APITokenCreateResponse:
      type: object
      properties:
        name: *id001
        displayName: *id002
        labels: *id003
        tags: *id004
        disabled: *id005
        createdAt: *id006
        updatedAt: *id007
        scopes: *id008
        expiresAt: *id009
        lastUsed: *id010
        token:
          type: string
          description: The plaintext secret ("gpm_..."). Shown exactly once, on create or ?rotate=1.
        tokenNote:
          type: string
          description: Human-readable reminder that the token cannot be retrieved again.
      required:
      - name
      - scopes
      description: Response to PUT /api-tokens/{name} the first time a token is created, or with ?rotate=1.
    AdminAuthSettings:
      type: object
      properties:
        providers:
          type: array
          items:
            type: string
          description: IdentityProvider names allowed to log into the admin panel.
        localLoginEnabled:
          type: boolean
          description: Default true (anti-lockout).
        ssoOnly:
          type: boolean
          description: Disables local login. Requires at least one providers entry.
    WebhookConfig:
      type: object
      properties:
        name:
          type: string
        url:
          type: string
          description: Absolute http(s) endpoint.
        secret:
          $ref: '#/components/schemas/Secret'
        disabled:
          type: boolean
      required:
      - name
      - url
      description: Outbound lifecycle notification POSTed after every successful config change.
    NotificationTarget:
      type: object
      properties:
        name:
          type: string
        type:
          type: string
          enum:
          - ntfy
          - discord
          - generic
        url:
          type: string
          description: Absolute http(s) endpoint. A discord url must contain "/api/webhooks/".
        secret:
          $ref: '#/components/schemas/Secret'
          description: ntfy access token or generic bearer token, sent as "Authorization Bearer".
            Unused for discord.
        disabled:
          type: boolean
        events:
          type: array
          items:
            type: string
            enum:
            - cert.renewal_failed
            - cert.expiring
            - cert.expired
            - upstream.unhealthy
            - upstream.recovered
            - acme.account_error
            - discovery.frozen
            - config.changed
          description: Empty selects the default set (every kind above except config.changed,
            which is opt-in).
      required:
      - name
      - type
      - url
      description: Outbound alert receiver - see settings.notifications and internal/notify.
    NotificationsSettings:
      type: object
      properties:
        targets:
          type: array
          items:
            $ref: '#/components/schemas/NotificationTarget'
        expiringThresholdDays:
          type: integer
          description: Days before ACME expiry a certificate joins the daily cert.expiring digest.
            0 selects the default (14).
      description: Empty (default) sends nothing, matching every deployment before this feature.
    PiholeDNSSync:
      type: object
      properties:
        enabled:
          type: boolean
        url:
          type: string
          description: Pi-hole base URL, no /api suffix.
        appPassword:
          $ref: '#/components/schemas/Secret'
        apexTarget:
          type: string
          description: CNAME target every managed record points at.
    CloudflareDNSSync:
      type: object
      properties:
        enabled:
          type: boolean
        dnsProviderRef:
          type: string
          description: Name of a DNSProvider whose config.apiToken is reused.
        zoneName:
          type: string
        apexTarget:
          type: string
        proxied:
          type: boolean
          description: Cloudflare orange-cloud flag. Default false.
    DNSSyncSettings:
      type: object
      properties:
        pihole:
          $ref: '#/components/schemas/PiholeDNSSync'
        cloudflare:
          $ref: '#/components/schemas/CloudflareDNSSync'
      description: Both backends independently enabled; both disabled (default) is inert.
    IngressHostTemplate:
      type: object
      properties:
        upstream:
          $ref: '#/components/schemas/Upstream'
        upstreamGroupRef:
          type: string
          description: Mutually exclusive with upstream.
        tls:
          $ref: '#/components/schemas/TLSSettings'
        robotsNoIndex:
          type: boolean
        timeouts:
          $ref: '#/components/schemas/HostTimeouts'
        middlewares:
          type: array
          items:
            type: string
        accessLists:
          type: array
          items:
            type: string
        tags:
          type: array
          items:
            type: string
        stripResponseHeaders:
          type: array
          items:
            type: string
          description: >-
            Applied verbatim to every derived host (see ProxyHost's own field of
            the same name). Validated by the same rules; without it a hand-set
            list on a managed host is reverted on the next reconcile.
        securityHeaders:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/SecurityHeaderValue'
          description: >-
            Applied verbatim to every derived host (see ProxyHost's own field of
            the same name), merging over settings.securityHeaders per key on that
            host. Validated by the same rules; without it a hand-set override on
            a managed host is reverted on the next reconcile.
        defaultDNS:
          $ref: '#/components/schemas/DNSSyncPolicy'
        allowedDomainSuffixes:
          type: array
          items:
            type: string
          description: Narrows the global list for hosts derived from this template/profile. Must be a
            subset.
      description: The shape every proxy host derived from discovery takes. certificateRef is required.
    IngressProfileRule:
      type: object
      properties:
        namespace:
          type: string
          description: Empty matches any namespace.
        matchLabels:
          type: object
          properties: {}
          additionalProperties:
            type: string
        profile:
          type: string
          description: '"template" or a profiles key. Required.'
      required:
      - profile
    IngressDiscoverySettings:
      type: object
      properties:
        enabled:
          type: boolean
        apiURL:
          type: string
          description: Absolute https URL. Empty uses the in-cluster endpoint.
        tokenFile:
          type: string
          description: Absolute path. Empty uses the projected in-cluster path.
        caFile:
          type: string
          description: Absolute path. Empty uses the projected in-cluster CA.
        namespace:
          type: string
          description: Empty lists cluster-wide (still annotation-gated).
        labelSelector:
          type: string
        pollInterval:
          type: string
          description: Go duration. Default 1m, minimum 15s.
        allowedDomainSuffixes:
          type: array
          items:
            type: string
          description: Required when enabled.
        template:
          $ref: '#/components/schemas/IngressHostTemplate'
        profiles:
          type: object
          properties: {}
          description: Named chains an Ingress may select by annotation. "template" is reserved.
          additionalProperties:
            $ref: '#/components/schemas/IngressHostTemplate'
        profileRules:
          type: array
          items:
            $ref: '#/components/schemas/IngressProfileRule'
          description: Operator-side profile routing by namespace/labels, evaluated in order.
        profileSelection:
          type: string
          description: annotation-or-rules (default) | rules-only.
          enum:
          - ''
          - annotation-or-rules
          - rules-only
      description: Disabled (default) is inert. Discovers annotated Kubernetes Ingress objects into managed
        proxy hosts.
    TLSFleetSettings:
      type: object
      properties:
        minVersion:
          type: string
          description: >-
            Fleet-wide minimum TLS version for every HTTPS and stream-terminate
            listener: 1.2 (default, and what an empty value means) or 1.3. A
            ProxyHost's own tls.minTLSVersion overrides it in either direction,
            so a legacy host can stay on 1.2 under a 1.3 fleet floor.
          enum:
          - ''
          - '1.2'
          - '1.3'
    Settings:
      type: object
      properties:
        schemaVersion:
          type: integer
        appName:
          type: string
          description: Default "Go Proxy Manager".
        externalBaseURL:
          type: string
          description: Canonical public URL of the admin panel. Must be absolute.
        adminAuth:
          $ref: '#/components/schemas/AdminAuthSettings'
        tls:
          $ref: '#/components/schemas/TLSFleetSettings'
        webhooks:
          type: array
          items:
            $ref: '#/components/schemas/WebhookConfig'
        notifications:
          $ref: '#/components/schemas/NotificationsSettings'
        dnsSync:
          $ref: '#/components/schemas/DNSSyncSettings'
        proxyProtocol:
          $ref: '#/components/schemas/ProxyProtocolSettings'
        trustedProxies:
          type: array
          items:
            type: string
          description: >-
            Fleet-wide CIDRs or bare IPs of the L7 proxies whose X-Forwarded-For
            is believed. The single source of truth for the client IP: when the
            connection peer is inside this set the client IP is the rightmost
            X-Forwarded-For entry that is not itself a trusted proxy, otherwise
            it is the peer. That one value is compared by access lists, geo, rate
            limits, guard/auth-request/client-cert/bouncer allowFrom, the
            basic-auth lockout, the access log and the X-Forwarded-For/X-Real-IP
            sent upstream. Empty (default) trusts nobody. A ProxyHost's own
            trustedProxies REPLACES this list for that host. Distinct from
            proxyProtocol.trustedCIDRs (L4, who may rewrite the connection
            address) and identityProvider.forwardAuth.trustedProxies (who may
            assert identity headers). Entries must parse as a CIDR or an IP
            address; 0.0.0.0/0 and ::/0 are accepted but warned about at load.
        ingressDiscovery:
          $ref: '#/components/schemas/IngressDiscoverySettings'
        securityHeaders:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/SecurityHeaderValue'
          description: >-
            Fleet-default response headers. Each header carries a scope (all /
            generated-only / proxied-only) selecting whether it lands on
            gpm-generated responses (auth-gate denials, sign-in redirects, error
            pages, path-rejection 400s, the no-such-host 404, parked/redirect
            hosts), on proxied upstream responses, or both; the default (and the
            legacy bare-string form) is all. A ProxyHost's own securityHeaders
            merges over this per key. Applied set-if-absent, so an app's own
            headers are never clobbered. Empty (default) ships nothing.
            Strict-Transport-Security (owned by tls.hsts), hop-by-hop headers,
            CR/LF in a name or value, and an unknown scope are rejected.
        maintenance:
          $ref: '#/components/schemas/MaintenanceSettings'
        stripResponseHeaders:
          type: array
          items:
            type: string
          description: >-
            Fleet-default list of response headers removed from what an upstream
            sends - the backend-identifying headers an app leaks (Server,
            X-Powered-By, X-AspNet-Version). Matching is case-insensitive; a
            ProxyHost's own stripResponseHeaders is the UNION with this list.
            The removal happens on the upstream's own response (including a 101
            Upgrade handshake), so it never reaches a header gpm adds -
            securityHeaders, HSTS, X-Robots-Tag, a forward-auth Set-Cookie
            refresh, gzip's Content-Encoding, a headers middleware's setResponse.
            gpm-generated responses (denials, redirects, error pages, the 404,
            the upstream-unreachable 502/504) have no upstream response and are
            untouched. Empty (default) strips nothing. Invalid header names,
            hop-by-hop headers, the response-semantic headers
            (Content-Type/-Length/-Encoding, Vary, Location, Sec-WebSocket-*)
            and case-insensitive duplicates are rejected.
      description: Singleton application configuration (config/settings.yaml).
    MaintenanceSettings:
      type: object
      description: >-
        Fleet-wide downtime switch. While a proxy host is in maintenance gpm
        answers every request to it itself - HTTP 503 with a Retry-After - and
        never dials the upstream. The body is the errorPages template configured
        for 503 (the host's own override first, then the settings-level pages),
        or gpm's built-in maintenance page when none is configured, negotiated on
        Accept (JSON / HTML / plain text) and never empty. Only proxy hosts are
        affected; redirect, parked and stream hosts keep serving, and ACME
        HTTP-01 is still answered so certificates renew during a window.
      properties:
        enabled:
          type: boolean
          description: >-
            Put EVERY proxy host into maintenance, whatever its own maintenance
            flag says. The global switch wins over a per-host false; turning it
            off returns each host to its own flag. Takes effect on the next
            request, with no restart and no router rebuild.
        retryAfterSeconds:
          type: integer
          minimum: 0
          maximum: 86400
          description: >-
            Retry-After header value on every maintenance response, per-host ones
            included. 0 (the default) selects 300 seconds.
    SecurityHeaderValue:
      description: >-
        One configured security response header value. Two accepted forms: a bare
        string (the legacy form; scope defaults to "all"), or a {value, scope}
        object carrying an explicit per-header scope. The API emits the bare
        string for an all-scope header and the object only for a non-default
        scope, so an unchanged config round-trips as plain strings.
      oneOf:
      - type: string
        description: Header value at the default scope "all".
      - type: object
        required:
        - value
        properties:
          value:
            type: string
          scope:
            type: string
            enum:
            - all
            - generated-only
            - proxied-only
            default: all
            description: >-
              Which responses the header applies to. "all" (default) both
              gpm-generated and proxied upstream responses; "generated-only" only
              responses gpm writes itself (never a proxied app); "proxied-only"
              only proxied upstream responses.
    ProxyProtocolSettings:
      type: object
      description: 'Inbound HAProxy PROXY protocol (v1 and v2) on the HTTP/HTTPS proxy
        listeners and every TCP stream listener. The parsed source replaces the connection
        RemoteAddr, so access lists, geo, rate limits, X-Forwarded-For, the access log
        and metrics all see the real client. The header is an unauthenticated claim and
        is honoured ONLY from a peer inside trustedCIDRs.'
      properties:
        enabled:
          type: boolean
        trustedCIDRs:
          type: array
          description: CIDRs or bare IPs of the load balancers whose header is believed.
            Required when enabled; there is no trust-everyone mode.
          items:
            type: string
        timeout:
          type: string
          description: 'Go duration deadline for reading a complete header from a trusted
            peer. Default 5s, maximum 1m.'
          example: 5s
    Config:
      type: object
      properties:
        schemaVersion:
          type: integer
        proxyHosts:
          type: array
          items:
            $ref: '#/components/schemas/ProxyHost'
        redirectHosts:
          type: array
          items:
            $ref: '#/components/schemas/RedirectHost'
        streamHosts:
          type: array
          items:
            $ref: '#/components/schemas/StreamHost'
        parkedHosts:
          type: array
          items:
            $ref: '#/components/schemas/ParkedHost'
        certificates:
          type: array
          items:
            $ref: '#/components/schemas/Certificate'
        clientCAs:
          type: array
          items:
            $ref: '#/components/schemas/ClientCA'
        dnsProviders:
          type: array
          items:
            $ref: '#/components/schemas/DNSProvider'
        identityProviders:
          type: array
          items:
            $ref: '#/components/schemas/IdentityProvider'
        upstreamGroups:
          type: array
          items:
            $ref: '#/components/schemas/UpstreamGroup'
        accessLists:
          type: array
          items:
            $ref: '#/components/schemas/AccessList'
        middlewares:
          type: array
          items:
            $ref: '#/components/schemas/Middleware'
        apiTokens:
          type: array
          items:
            $ref: '#/components/schemas/APIToken'
      description: The full assembled config graph, as returned by GET /config.
    ConfigSummary:
      type: object
      description: Object counts per kind, as returned by GET /config/summary.
      properties:
        counts:
          type: object
          description: Keyed by the plural resource name used in API paths (e.g.
            "proxy-hosts"), one entry per first-class config kind. apiTokens is 0
            for a caller without api-tokens:read, same restriction GET /config
            applies to the rows themselves.
          additionalProperties:
            type: integer
        disabled:
          type: object
          description: Count of objects with meta.disabled=true, by kind. Today
            only "proxy-hosts" is reported.
          additionalProperties:
            type: integer
        maintenance:
          type: object
          description: Count of proxy hosts with maintenance=true. Maintenance is
            a ProxyHost-only field, so only "proxy-hosts" is reported.
          additionalProperties:
            type: integer
        head:
          type: string
          description: The config repo's current commit, same value as
            GET /health's configHead.
    Commit:
      type: object
      properties:
        hash:
          type: string
        author:
          type: string
        email:
          type: string
        message:
          type: string
        when:
          type: string
          format: date-time
      description: One git commit in the config repo's history.
    CommitRef:
      type: object
      properties:
        hash:
          type: string
          description: A commit hash from GET /history or GET /{plural}/{name}/history.
      required:
      - hash
      description: Request body for POST /revert and POST /{plural}/{name}/revert.
    CommitResponse:
      type: object
      properties:
        commit:
          type: string
          description: The new commit hash created by this write (the revert/restore is itself a new commit).
      required:
      - commit
    VersionInfo:
      type: object
      properties:
        version:
          type: string
        commit:
          type: string
        date:
          type: string
        go:
          type: string
          description: Go toolchain version used to build.
        os:
          type: string
        arch:
          type: string
      description: Build identity, from -ldflags at build time.
    Principal:
      type: object
      properties:
        Subject:
          type: string
          description: Stable IdP/local subject identifier. No json tag on this Go field, so the key is
            capitalized as shown.
        Email:
          type: string
        Name:
          type: string
        Role:
          type: string
          description: admin | user.
          enum:
          - admin
          - user
        IdP:
          type: string
          description: Name of the IdentityProvider that authenticated this session; empty for local login.
        csrfToken:
          type: string
          description: Required back as the X-CSRF-Token header on mutating session requests. Empty for
            a token principal.
        isToken:
          type: boolean
          description: True when authenticated by an API token rather than a session cookie.
        scopes:
          type: array
          items:
            type: string
          description: The token's granted scopes. Empty for a session principal (governed by role alone).
        readOnly:
          type: boolean
          description: True for the `user` role, which may read everything except API tokens and
            write nothing. The SPA renders a read-only banner and disables every Save control when
            it is set.
      description: The authenticated caller, as returned by GET /api/me.
    Capabilities:
      type: object
      properties:
        geoip:
          type: object
          properties:
            dbLoaded:
              type: boolean
        apiTokens:
          type: object
          properties:
            enabled:
              type: boolean
        dnsSync:
          type: object
          properties:
            piholeEnabled:
              type: boolean
            cloudflareEnabled:
              type: boolean
        ingressDiscovery:
          type: object
          properties:
            enabled:
              type: boolean
        ha:
          type: object
          properties:
            role:
              type: string
              enum:
              - leader
              - follower
            readOnly:
              type: boolean
        metrics:
          type: object
          properties:
            enabled:
              type: boolean
              description: True when GET /metrics is mounted (GPM_METRICS=1). The SPA greys the link
                out when false rather than linking at a route that answers 404.
        maintenance:
          type: object
          properties:
            globalEnabled:
              type: boolean
              description: True when settings.maintenance.enabled is on, i.e. every proxy host is
                currently serving the maintenance page whatever its own maintenance flag says. The
                SPA says so on the host editor rather than showing a per-host toggle that reads off
                while the host is in fact down.
        adminLogin:
          type: object
          properties:
            configured:
              type: boolean
              description: False in the bootstrap failure state - no local admin credential and no
                admin SSO provider that renders a sign-in button, so the login page cannot succeed
                for anyone. The SPA and the server-rendered login page both show the remedy.
            totp:
              type: boolean
              description: True when the local admin must present a TOTP code after their password
                (GPM_LOCAL_ADMIN_TOTP_SECRET is set). Says nothing about SSO logins, whose MFA
                belongs to the IdP.
            cookieSecure:
              type: string
              enum:
              - secure
              - insecure-private
              - insecure-public
              description: How the admin session cookie was issued for this request; insecure-public
                means plain HTTP from a routable address and the SPA shows a banner; absent when
                unwired.
        scopeSubjects:
          type: array
          items:
            type: string
          description: The authoritative list of scope subjects (resource plurals + settings/dns-sync/ingress-discovery/docker-discovery/metrics),
            for building a token-scope form.
      description: 'Read-only runtime feature-availability probe. Not scope-gated: any authenticated caller
        may ask.'
    RuntimeInfo:
      type: object
      description: Read-only startup facts for the admin UI's Runtime card.
      properties:
        version:
          type: string
        haRole:
          type: string
          enum:
          - leader
          - follower
        listeners:
          type: object
          properties:
            http:
              type: string
              description: Proxy listener HTTP listen address (-http-addr / GPM_HTTP_ADDR).
            https:
              type: string
              description: Proxy listener HTTPS listen address (-https-addr / GPM_HTTPS_ADDR).
            admin:
              type: string
              description: Admin listen address (-admin-addr / GPM_ADMIN_ADDR).
        paths:
          type: object
          description: Deployment filesystem layout. OMITTED for a caller without the admin scope
            (the read-only user role and any narrower token).
          properties:
            configDir:
              type: string
            certDir:
              type: string
            sessionDB:
              type: string
        metricsEnabled:
          type: boolean
        accessLogEnabled:
          type: boolean
          description: Live value, not the -access-log flag (PUT /api/logs toggles it at runtime).
        geoipLoaded:
          type: boolean
        secretFileRoots:
          type: array
          items:
            type: string
          description: Effective ${FILE:...} allowlist (GPM_SECRET_FILE_ROOTS). Empty for a caller
            without the admin scope.
        localAdminConfigured:
          type: boolean
          description: True when both GPM_LOCAL_ADMIN_USER and a bcrypt hash are set. The username
            and the hash are never reported.
        localAdminTOTP:
          type: boolean
          description: True when a TOTP secret is set for the local admin (GPM_LOCAL_ADMIN_TOTP_SECRET
            or its _FILE form). The secret itself is never reported.
        pprofEnabled:
          type: boolean
    Health:
      type: object
      description: Aggregate runtime health for GET /api/health. Everything comes from in-memory state,
        no ACME CA or DNS calls.
      properties:
        dataPlane:
          type: object
          properties:
            https:
              $ref: '#/components/schemas/HealthListener'
            http:
              $ref: '#/components/schemas/HealthListener'
        certificates:
          type: object
          description: Counted from the same status fields GET /certificates reports; total includes
            every state.
          properties:
            total:
              type: integer
            expiring:
              type: integer
            expired:
              type: integer
            error:
              type: integer
        upstreamGroups:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              healthy:
                type: integer
              unhealthy:
                type: integer
        acme:
          type: object
          properties:
            lastRenewalRun:
              type: string
              format: date-time
              description: When the ACME renewal loop last ran. Absent if it has never run (an HA
                follower, which does not run it, or before the first tick).
            lastError:
              type: string
              description: The most recent renewal failure across every certificate, truncated to
                300 characters. Absent if none has failed since startup.
        haRole:
          type: string
          enum:
          - leader
          - follower
        configHead:
          type: string
          description: Current config repo HEAD commit SHA.
        configWarnings:
          type: array
          items:
            type: string
          description: Non-fatal config smells (mirrors Config.Warnings()), including one entry per
            config file with an unknown YAML key an older struct silently ignored - see
            docs/operations/upgrading.md#rollback. Absent when there is nothing to warn about.
    HealthListener:
      type: object
      properties:
        listening:
          type: boolean
        addr:
          type: string
    WebhookDelivery:
      type: object
      properties:
        name:
          type: string
        url:
          type: string
          description: Target URL. For a caller without the admin scope the path and query are
            replaced with "/(redacted)" - for a Discord, Slack or ntfy receiver the URL path is the
            credential, and these status routes ride settings:read.
        disabled:
          type: boolean
        lastAttempt:
          type: string
          format: date-time
          description: RFC3339. Absent when the target has never fired in this process.
        status:
          type: integer
          description: HTTP status the receiver returned; absent when the request got no response.
        durationMs:
          type: integer
        ok:
          type: boolean
        error:
          type: string
    WebhookTestResult:
      type: object
      properties:
        name:
          type: string
        url:
          type: string
        status:
          type: integer
        durationMs:
          type: integer
        ok:
          type: boolean
        error:
          type: string
    DNSSyncStatus:
      type: object
      properties:
        lastRun:
          type: string
          format: date-time
        lastSuccess:
          type: string
          format: date-time
          description: The last reconcile in which nothing failed. Separate from lastRun because a
            backend failing since six hours ago is exactly what "last run 2 minutes ago" hides.
        pihole:
          type: object
          properties:
            enabled:
              type: boolean
            ok:
              type: boolean
            desired:
              type: integer
            managed:
              type: integer
            created:
              type: integer
            adopted:
              type: integer
            retargeted:
              type: integer
            deleted:
              type: integer
            skipped:
              type: integer
            untouched:
              type: integer
            error:
              type: string
        cloudflare:
          type: object
          properties:
            enabled:
              type: boolean
            ok:
              type: boolean
            desired:
              type: integer
            managed:
              type: integer
            created:
              type: integer
            adopted:
              type: integer
            retargeted:
              type: integer
            deleted:
              type: integer
            skipped:
              type: integer
            untouched:
              type: integer
            error:
              type: string
      description: Last-reconcile result, per backend. Shape mirrored from internal/dnssync; see docs/configuration.md#dnssyncsettings
        for field meanings.
      additionalProperties: true
    DNSSyncPlan:
      type: object
      properties:
        generatedAt:
          type: string
          format: date-time
        pihole:
          type: object
          properties:
            enabled:
              type: boolean
            ok:
              type: boolean
            create:
              type: array
              items:
                type: string
            adopt:
              type: array
              items:
                type: string
            retarget:
              type: array
              items:
                type: string
            delete:
              type: array
              items:
                type: string
            skip:
              type: array
              items:
                type: string
            untouched:
              type: integer
            error:
              type: string
        cloudflare:
          type: object
          properties:
            enabled:
              type: boolean
            ok:
              type: boolean
            create:
              type: array
              items:
                type: string
            adopt:
              type: array
              items:
                type: string
            retarget:
              type: array
              items:
                type: string
            delete:
              type: array
              items:
                type: string
            skip:
              type: array
              items:
                type: string
            untouched:
              type: integer
            error:
              type: string
      description: Dry-run preview of what a reconcile would do. Never writes anything.
      additionalProperties: true
    DockerDiscoveryStatus:
      type: object
      properties:
        enabled:
          type: boolean
        lastRun:
          type: string
          format: date-time
        lastSuccess:
          type: string
          description: 'Watch this, not lastRun, for staleness: a failed run freezes the managed set instead
            of changing it.'
          format: date-time
        reachable:
          type: boolean
          description: Whether the last run could talk to the Docker Engine endpoint at all.
        endpoint:
          type: string
          description: The socket path or host URL the last run used.
        discovered:
          type: integer
        managed:
          type: integer
        created:
          type: integer
        updated:
          type: integer
        deleted:
          type: integer
        skipped:
          type: integer
        error:
          type: string
        commit:
          type: string
        hosts:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              container:
                type: string
              profile:
                type: string
              domains:
                type: array
                items:
                  type: string
              action:
                type: string
                enum:
                - created
                - updated
                - unchanged
                - deleted
                - skipped
              reason:
                type: string
                description: Present on a skip, and on the two updates that are not a plain derive.
      description: Last-reconcile result. Shape mirrored from internal/docker; see design/docker-discovery.md.
      additionalProperties: true
    DockerDiscoveryPlan:
      type: object
      properties:
        generatedAt:
          type: string
          format: date-time
      description: Dry-run preview mirroring the reconcile decisions, without writing anything.
      additionalProperties: true
    IngressDiscoveryStatus:
      type: object
      properties:
        enabled:
          type: boolean
        lastRun:
          type: string
          format: date-time
        lastSuccess:
          type: string
          description: 'Watch this, not lastRun, for staleness: a failed run freezes the managed set instead
            of changing it.'
          format: date-time
        discovered:
          type: integer
        managed:
          type: integer
        created:
          type: integer
        updated:
          type: integer
        deleted:
          type: integer
        skipped:
          type: integer
        error:
          type: string
        hosts:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              namespace:
                type: string
              profile:
                type: string
              action:
                type: string
                enum:
                - created
                - updated
                - unchanged
                - deleted
                - skipped
              reason:
                type: string
                description: Present when action is skipped.
      description: Last-reconcile result. Shape mirrored from internal/k8s; see docs/deployment.md#kubernetes-ingress-discovery.
      additionalProperties: true
    IngressDiscoveryPlan:
      type: object
      properties:
        generatedAt:
          type: string
          format: date-time
      description: Dry-run preview mirroring the reconcile decisions, without writing anything.
      additionalProperties: true
    UpstreamHealth:
      type: object
      properties: {}
      description: Map of UpstreamGroup name -> per-upstream live health, e.g. {"edge-nodes":[{"upstream":"http://192.0.2.11:80","healthy":true,"weight":1,"active":3}]}.
      additionalProperties:
        type: object
        properties:
          upstream:
            type: string
            description: scheme://host:port
          healthy:
            type: boolean
          weight:
            type: integer
          active:
            type: integer
            description: In-flight request count.
    Logs:
      type: object
      properties:
        enabled:
          type: boolean
          description: False, with an empty entries array, when no access-log viewer is wired.
        entries:
          type: array
          items:
            type: object
            properties: {}
            additionalProperties: true
          description: Newest first. Shape depends on internal/dataplane's access-log entry.
      description: Recent proxied-request access-log entries for the in-UI log viewer.
    ReconcileAck:
      type: object
      properties:
        status:
          type: string
          enum:
          - reconciled
      description: Fallback ack when no status payload is wired.
    RevokeAck:
      type: object
      properties:
        status:
          type: string
          enum:
          - revoked
  responses:
    BadRequest:
      description: Malformed request body, invalid object, or a validation error (e.g. a dangling reference,
        a literal secret, an invalid name).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: No valid session cookie or bearer token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: Authenticated, but the caller's role or API-token scope does not permit this action.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: No object of this kind with this name exists.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Conflict:
      description: Delete refused because another object still references this one, or a reconcile is
        already in flight.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    FollowerReadOnly:
      description: This instance is an HA follower (GPM_HA_ROLE=follower) and refuses every write; make
        the change on the leader.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotImplemented:
      description: The backing feature is not wired on this instance (e.g. no DNSSync/IngressDiscovery/SSO-revocation
        dependency was configured).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ServerError:
      description: The store or an underlying dependency failed, or a write committed but could not be
        applied to the running configuration.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NoContent:
      description: Deleted.
  parameters:
    NamePath:
      name: name
      in: path
      required: true
      description: Object name (the identity/filename).
      schema:
        type: string
    CSRFTokenHeader:
      name: X-CSRF-Token
      in: header
      required: false
      description: Required on this mutating request when authenticated by session cookie (the value from
        GET /api/me's csrfToken). Not required, and ignored, for bearer-token authentication - a token
        carries no ambient browser authority to defend against.
      schema:
        type: string
paths:
  /api/access-list-sources/reconcile:
    post:
      summary: Fetch every due access-list source now
      description: 'Synchronous. Never queues: a run already in flight answers 409 instead of blocking.
        A source whose fetch is refused (non-200, empty body, over maxEntries, any unparseable line)
        keeps its previously fetched set.'
      tags:
      - Access lists
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessListSourceStatus'
        '409':
          $ref: '#/components/responses/Conflict'
        '501':
          $ref: '#/components/responses/NotImplemented'
        '502':
          description: The ledger could not be read or written.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: access-lists:write
  /api/access-list-sources/status:
    get:
      summary: Report what each access-list source last resolved to
      tags:
      - Access lists
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessListSourceStatus'
        '501':
          $ref: '#/components/responses/NotImplemented'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: access-lists:read
  /api/access-lists:
    get:
      summary: List access lists
      tags:
      - Access lists
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AccessList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: access-lists:read
  /api/access-lists/{name}:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Get one AccessList
      tags:
      - Access lists
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessList'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: access-lists:read
    put:
      summary: Create or update an AccessList
      tags:
      - Access lists
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AccessList'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: access-lists:write
    delete:
      summary: Delete an AccessList
      tags:
      - Access lists
      responses:
        '204':
          $ref: '#/components/responses/NoContent'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: access-lists:write
  /api/access-lists/{name}/history:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Commit history for one AccessList
      description: Up to the 50 most recent commits touching this object's file.
      tags:
      - Access lists
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Commit'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: access-lists:read
  /api/access-lists/{name}/revert:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    post:
      summary: Revert one AccessList to a past commit
      description: Restores only this object's file to its state at the given commit; every other object
        is left as-is. api-tokens is exempt from write scope by construction elsewhere, but reverting
        an APIToken specifically is refused (see docs/configuration.md) so a rotation always means revocation.
      tags:
      - Access lists
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommitRef'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommitResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: access-lists:write
  /api/access-lists/{name}/migrate-basic-auth:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    post:
      summary: Migrate an AccessList's deprecated basicAuth to an auth middleware
      description: 'Creates an auth middleware named "<name>-basic" with mode basic from the list''s
        basicAuth users, attaches it to every proxy host and location that references the list, copies
        the list''s literal allow CIDRs to the middleware''s allowFrom ONLY when satisfyAny was set (the
        sole configuration in which an IP match alone admitted a request), and clears basicAuth and
        satisfyAny on the list - all in a single commit. When every rule moved, the list is also
        DETACHED from those hosts and locations (see detachAccessList): with satisfyAny it admitted on
        "IP match OR password", so leaving its rules attached would make them mandatory. Source-backed
        and path-scoped allow rules are not copied and are reported in warnings. Admin-scoped because one call rewrites access lists,
        middlewares and proxy hosts together. Add ?plan=1 for a dry run that returns the same payload
        and changes nothing.'
      tags:
      - Access lists
      parameters:
      - name: plan
        in: query
        required: false
        description: Set to 1 for a dry run. Nothing is written and the response carries no commit.
        schema:
          type: string
          enum:
          - '1'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BasicAuthMigration'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: admin
  /api/api-tokens:
    get:
      summary: List api tokens
      tags:
      - API tokens
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/APIToken'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: admin
  /api/api-tokens/{name}:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Get one APIToken
      tags:
      - API tokens
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/APIToken'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: admin
    put:
      summary: Create or update an APIToken
      description: Creates or updates the named token. A new token (or an existing one re-PUT with ?rotate=1)
        mints a fresh secret server-side, returned exactly once as `token`/`tokenNote`. An ordinary edit
        (scopes, expiry, disabled) with no ?rotate carries the existing digest forward unchanged. TokenHash
        is never accepted from the client.
      tags:
      - API tokens
      parameters:
      - name: rotate
        in: query
        required: false
        description: 'api-tokens only: 1 mints a replacement secret, invalidating the old one immediately.'
        schema:
          type: string
          enum:
          - '1'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/APIToken'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/APITokenCreateResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: admin
    delete:
      summary: Delete an APIToken
      tags:
      - API tokens
      responses:
        '204':
          $ref: '#/components/responses/NoContent'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: admin
  /api/api-tokens/{name}/history:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Commit history for one APIToken
      description: Up to the 50 most recent commits touching this object's file.
      tags:
      - API tokens
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Commit'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: admin
  /api/api-tokens/{name}/revert:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    post:
      summary: Revert one APIToken to a past commit
      description: Restores only this object's file to its state at the given commit; every other object
        is left as-is. api-tokens is exempt from write scope by construction elsewhere, but reverting
        an APIToken specifically is refused (see docs/configuration.md) so a rotation always means revocation.
      tags:
      - API tokens
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommitRef'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommitResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: admin
  /api/backup:
    get:
      summary: Download a portable config backup archive
      description: 'Gzip-compressed tar of every config object YAML plus settings.yaml (excludes .git
        history, certs, and session.db). Admin scope, not *:read: unlike the JSON reads, the raw YAML
        carries api-tokens'' stored digests, which are offline-crackable.'
      tags:
      - Backup & restore
      responses:
        '200':
          description: gpm-config-backup.tar.gz
          content:
            application/gzip:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: admin
  /api/capabilities:
    get:
      summary: Runtime feature-availability probe
      description: 'Not scope-gated: any authenticated caller (session or token, any scope) may ask what
        this instance can do.'
      tags:
      - Meta
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Capabilities'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/certificates:
    get:
      summary: List certificates
      tags:
      - Certificates
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Certificate'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: certificates:read
  /api/certificates/{name}:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Get one Certificate
      tags:
      - Certificates
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Certificate'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: certificates:read
    put:
      summary: Create or update a Certificate
      tags:
      - Certificates
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Certificate'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Certificate'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: certificates:write
    delete:
      summary: Delete a Certificate
      tags:
      - Certificates
      responses:
        '204':
          $ref: '#/components/responses/NoContent'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: certificates:write
  /api/certificates/{name}/history:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Commit history for one Certificate
      description: Up to the 50 most recent commits touching this object's file.
      tags:
      - Certificates
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Commit'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: certificates:read
  /api/certificates/{name}/revert:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    post:
      summary: Revert one Certificate to a past commit
      description: Restores only this object's file to its state at the given commit; every other object
        is left as-is. api-tokens is exempt from write scope by construction elsewhere, but reverting
        an APIToken specifically is refused (see docs/configuration.md) so a rotation always means revocation.
      tags:
      - Certificates
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommitRef'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommitResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: certificates:write
  /api/certificates/{name}/renew:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    post:
      summary: Force an immediate certificate renewal
      description: Starts an ACME order for this certificate now, ignoring the normal 30-day renewal
        window. Responds as soon as the order has STARTED, not once it completes - DNS-01 propagation
        alone can take minutes. Poll GET /certificates/{name}'s status fields (state, lastAttempt,
        lastError) to see the outcome. Only one order runs at a time across the whole instance; a
        second renew request while one is already in flight gets 409, not queued. A 1-hour cooldown
        also applies per certificate, counted from its last attempt - failed attempts included - so
        a retry loop cannot hammer the CA into a rate limit.
      tags:
      - Certificates
      responses:
        '200':
          description: The order was accepted and is now running in the background.
          content:
            application/json:
              schema:
                type: object
                properties:
                  started:
                    type: boolean
                required:
                - started
        '400':
          description: This certificate is type custom, not acme. gpm does not renew custom certificates
            - replace the file and PUT the object.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: 'Refused for one of two reasons, both named in the error message: an ACME order
            is already in flight for this instance (this certificate or another one), or this
            certificate is within its 1-hour renew cooldown, counted from its last attempt whether
            that attempt succeeded or failed. The cooldown message states the remaining wait
            ("retry in 42m10s"). Wait and retry; nothing is queued.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '501':
          description: This instance is not the ACME issuer (an HA follower, or ACME is not configured).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '502':
          description: Unexpected error starting the order (rare - the request itself, not the order,
            failed). An order failure once running (CA rejection, DNS-01 propagation timeout) is NOT
            reported here; check the certificate's lastError status field.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: certificates:write
  /api/client-cas:
    get:
      summary: List client cas
      tags:
      - Client CAs
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ClientCA'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: client-cas:read
  /api/client-cas/{name}:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Get one ClientCA
      tags:
      - Client CAs
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientCA'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: client-cas:read
    put:
      summary: Create or update a ClientCA
      tags:
      - Client CAs
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClientCA'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientCA'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: client-cas:write
    delete:
      summary: Delete a ClientCA
      tags:
      - Client CAs
      responses:
        '204':
          $ref: '#/components/responses/NoContent'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: client-cas:write
  /api/client-cas/{name}/history:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Commit history for one ClientCA
      description: Up to the 50 most recent commits touching this object's file.
      tags:
      - Client CAs
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Commit'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: client-cas:read
  /api/client-cas/{name}/revert:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    post:
      summary: Revert one ClientCA to a past commit
      description: Restores only this object's file to its state at the given commit; every other object
        is left as-is. api-tokens is exempt from write scope by construction elsewhere, but reverting
        an APIToken specifically is refused (see docs/configuration.md) so a rotation always means revocation.
      tags:
      - Client CAs
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommitRef'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommitResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: client-cas:write
  /api/client-cas/{name}/issue:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    post:
      summary: Issue a client certificate from this ClientCA
      description: Mints an RSA-2048 client certificate signed by the CA's configured signing key
        (caKeyFile / caKeyPEM) and returns it as a password-protected PKCS#12 bundle. RSA and the
        legacy PKCS#12 encoder are deliberate - iOS and some Android/Wear releases reject ECDSA client
        certificates and PBES2 bundles. The generated private key is never persisted or logged; it
        exists only in the response body, so a lost bundle means re-issuing. Nothing in the config
        changes, so unlike every other write this route creates no revision and no history entry.
      tags:
      - Client CAs
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClientCertRequest'
      responses:
        '200':
          description: The PKCS#12 bundle. Content-Disposition names it after the sanitized common name.
          headers:
            Content-Disposition:
              description: attachment; filename="<sanitized-cn>.p12"
              schema:
                type: string
            X-Client-Cert-Serial:
              description: Serial number of the issued certificate, lower-case hex.
              schema:
                type: string
          content:
            application/x-pkcs12:
              schema:
                type: string
                format: binary
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: This ClientCA is verify-only - it has no caKeyFile / caKeyPEM, so it cannot issue.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: The certificate was signed but could not be recorded (no certificate store is
            configured, or it is unwritable), so it was NOT issued and no bundle is returned. Also
            covers a signing key that is configured but unusable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: client-cas:write
  /api/client-cas/{name}/generate:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    post:
      summary: Generate a new self-signed ClientCA
      description: Creates a complete, issuance-capable client CA from nothing, so an mTLS setup needs
        no openssl and no hand-placed key file. Generates an RSA-4096 self-signed CA (CA:TRUE with
        pathlen:0, keyUsage certSign+cRLSign, random serial, small backdate), writes the private key
        into the managed certificate store at "client-cas/{name}.key" with mode 0600, and saves the
        ClientCA object with that path as caKeyFile and the certificate inline as caPEM. Unlike
        POST /issue this DOES mutate config - it goes through the normal store save, so it commits and
        appears in object history exactly like a PUT, and the response is the created object. The
        private key is never returned and never logged. A key file another ClientCA still references is
        never overwritten (409); an unreferenced one is reclaimed. A failed config save removes the key
        it just wrote so the name can be retried. Deleting the ClientCA later does NOT delete the key
        file - see docs/configuration.md.
      tags:
      - Client CAs
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClientCAGenerateRequest'
      responses:
        '200':
          description: The created ClientCA, exactly as PUT would return it. Carries the certificate and
            the key path, never the key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientCA'
        '400':
          $ref: '#/components/responses/BadRequest'
        '409':
          description: A ClientCA of this name already exists, or a key file for it is already present in
            the certificate store AND another ClientCA still names it as its caKeyFile (the error names
            that CA). Nothing on disk changes. An UNREFERENCED key file is not a conflict - it is
            reclaimed and replaced, since it can only be residue from a crash or from a deleted CA.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Key generation or the certificate-store write failed, or no certificate store is
            configured. No ClientCA is created. Note a failure of the config-store save afterwards
            surfaces as 400, per this API's convention for a rejected write, and the key just written is
            removed so the name can be retried.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: client-cas:write
  /api/client-cas/{name}/certificates:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: List the client certificates this ClientCA has issued
      description: Runtime state, not config - it records what gpm issued, never what the operator
        declared, so it lives in the certificate store rather than the git-backed config repo and
        appears in no revision. Each record carries the subject, SANs, serial and validity window plus
        a status derived from the CA's expiryWarningDays; it never carries key material, and gpm keeps
        no copy of the certificate or its PKCS#12 bundle. A superseded record (renewed) stays listed
        until it expires, because the certificate it names is still valid on every device holding it.
      tags:
      - Client CAs
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IssuedClientCertList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: client-cas:read
  /api/client-cas/{name}/certificates/{serial}/renew:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    - name: serial
      in: path
      required: true
      description: Serial of the issued certificate to renew, lower-case hex, as reported by the list.
      schema:
        type: string
    post:
      summary: Renew one issued client certificate
      description: Reissues the identity already on record - the same common name and SANs, which are
        deliberately NOT accepted from the client - with a brand-new private key and serial, and marks
        the old record superseded by the new one. The response is a PKCS#12 bundle exactly like
        POST /issue. Renewing does NOT revoke the old certificate - revocation stays CRL-based
        (ClientCA.crlFile / crlPEM) - so every device still holding the old bundle keeps working until
        it expires. There is no client-side renewal - each device must import the new .p12 by hand.
      tags:
      - Client CAs
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClientCertRenewRequest'
      responses:
        '200':
          description: The renewed PKCS#12 bundle.
          headers:
            Content-Disposition:
              description: attachment; filename="<sanitized-cn>.p12"
              schema:
                type: string
            X-Client-Cert-Serial:
              description: Serial number of the renewed certificate, lower-case hex.
              schema:
                type: string
          content:
            application/x-pkcs12:
              schema:
                type: string
                format: binary
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: No such ClientCA, or no issued certificate with that serial.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: This certificate was already renewed. The error names the serial that superseded
            it - renew that certificate instead. Renewing a superseded record again would mint a second
            live certificate for one identity and rewrite the supersede link.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: This ClientCA is verify-only - its signing key was removed since the certificate
            was issued, so it cannot renew.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: The certificate was signed but could not be recorded (no certificate store is
            configured, or it is unwritable), so it was NOT issued and no bundle is returned.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: client-cas:write
  /api/config:
    get:
      summary: Dump the whole assembled config
      description: 'Needs the wildcard read scope: no single resource scope is enough.'
      tags:
      - Meta
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Config'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: '*:read'
  /api/config/summary:
    get:
      summary: Object counts per config kind
      description: The same store read GET /api/config does, reduced to counts so
        a caller that only needs "how many" (the UI sidebar, to decide whether the
        advanced nav group starts open) does not have to fetch every object.
      tags:
      - Meta
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConfigSummary'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: '*:read'
  /api/parked-hosts:
    get:
      summary: List parked hosts
      tags:
      - Parked hosts
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ParkedHost'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: parked-hosts:read
  /api/parked-hosts/{name}:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Get one ParkedHost
      tags:
      - Parked hosts
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParkedHost'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: parked-hosts:read
    put:
      summary: Create or update a ParkedHost
      tags:
      - Parked hosts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ParkedHost'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParkedHost'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: parked-hosts:write
    delete:
      summary: Delete a ParkedHost
      tags:
      - Parked hosts
      responses:
        '204':
          $ref: '#/components/responses/NoContent'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: parked-hosts:write
  /api/parked-hosts/{name}/history:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Commit history for one ParkedHost
      description: Up to the 50 most recent commits touching this object's file.
      tags:
      - Parked hosts
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Commit'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: parked-hosts:read
  /api/parked-hosts/{name}/revert:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    post:
      summary: Revert one ParkedHost to a past commit
      description: Restores only this object's file to its state at the given commit; every other object
        is left as-is. api-tokens is exempt from write scope by construction elsewhere, but reverting
        an APIToken specifically is refused (see docs/configuration.md) so a rotation always means revocation.
      tags:
      - Parked hosts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommitRef'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommitResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: parked-hosts:write
  /api/dns-providers:
    get:
      summary: List dns providers
      tags:
      - DNS providers
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/DNSProvider'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: dns-providers:read
  /api/dns-providers/{name}:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Get one DNSProvider
      tags:
      - DNS providers
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DNSProvider'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: dns-providers:read
    put:
      summary: Create or update a DNSProvider
      tags:
      - DNS providers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DNSProvider'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DNSProvider'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: dns-providers:write
    delete:
      summary: Delete a DNSProvider
      tags:
      - DNS providers
      responses:
        '204':
          $ref: '#/components/responses/NoContent'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: dns-providers:write
  /api/dns-providers/{name}/history:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Commit history for one DNSProvider
      description: Up to the 50 most recent commits touching this object's file.
      tags:
      - DNS providers
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Commit'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: dns-providers:read
  /api/dns-providers/{name}/revert:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    post:
      summary: Revert one DNSProvider to a past commit
      description: Restores only this object's file to its state at the given commit; every other object
        is left as-is. api-tokens is exempt from write scope by construction elsewhere, but reverting
        an APIToken specifically is refused (see docs/configuration.md) so a rotation always means revocation.
      tags:
      - DNS providers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommitRef'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommitResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: dns-providers:write
  /api/dns-sync/plan:
    get:
      summary: Preview a DNS sync reconcile
      description: 'Read-only dry run: reports what a reconcile would create/adopt/retarget/delete, without
        changing anything.'
      tags:
      - DNS sync
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DNSSyncPlan'
        '409':
          $ref: '#/components/responses/Conflict'
        '501':
          $ref: '#/components/responses/NotImplemented'
        '502':
          description: The backend call failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: dns-sync:read
  /api/dns-sync/reconcile:
    post:
      summary: Run a DNS sync reconcile now
      description: 'Synchronous. Never queues: a run already in flight answers 409 instead of blocking.'
      tags:
      - DNS sync
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DNSSyncStatus'
        '409':
          $ref: '#/components/responses/Conflict'
        '501':
          $ref: '#/components/responses/NotImplemented'
        '502':
          description: The backend (Pi-hole/Cloudflare) call failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: dns-sync:write
  /api/dns-sync/status:
    get:
      summary: Last DNS sync reconcile result
      tags:
      - DNS sync
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DNSSyncStatus'
        '501':
          $ref: '#/components/responses/NotImplemented'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: dns-sync:read
  /api/health:
    get:
      summary: Aggregate runtime health
      description: A single-request snapshot of whether this instance is actually working - proxy
        listener state, certificate expiry counts, upstream-group health, the ACME renewal loop's last
        run, HA role and the current config HEAD. Everything comes from in-memory state (plus the cached
        certificate files GET /certificates already reads); it never calls out to an ACME CA or a DNS
        provider.
      tags:
      - Meta
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Health'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: '*:read'
  /api/history:
    get:
      summary: Whole-config commit history
      description: Up to the 100 most recent commits across the entire config repo.
      tags:
      - Meta
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Commit'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: '*:read'
  /api/identity-providers:
    get:
      summary: List identity providers
      tags:
      - Identity providers
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/IdentityProvider'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: identity-providers:read
  /api/identity-providers/{name}:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Get one IdentityProvider
      tags:
      - Identity providers
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentityProvider'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: identity-providers:read
    put:
      summary: Create or update an IdentityProvider
      tags:
      - Identity providers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentityProvider'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentityProvider'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: identity-providers:write
    delete:
      summary: Delete an IdentityProvider
      tags:
      - Identity providers
      responses:
        '204':
          $ref: '#/components/responses/NoContent'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: identity-providers:write
  /api/identity-providers/{name}/history:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Commit history for one IdentityProvider
      description: Up to the 50 most recent commits touching this object's file.
      tags:
      - Identity providers
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Commit'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: identity-providers:read
  /api/identity-providers/{name}/revert:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    post:
      summary: Revert one IdentityProvider to a past commit
      description: Restores only this object's file to its state at the given commit; every other object
        is left as-is. api-tokens is exempt from write scope by construction elsewhere, but reverting
        an APIToken specifically is refused (see docs/configuration.md) so a rotation always means revocation.
      tags:
      - Identity providers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommitRef'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommitResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: identity-providers:write
  /api/docker-discovery/plan:
    get:
      summary: Preview a Docker discovery reconcile
      description: Read-only dry run, without changing anything.
      tags:
      - Docker discovery
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DockerDiscoveryPlan'
        '409':
          $ref: '#/components/responses/Conflict'
        '501':
          $ref: '#/components/responses/NotImplemented'
        '502':
          description: The Docker Engine API call failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: docker-discovery:read
  /api/docker-discovery/reconcile:
    post:
      summary: Run a Docker discovery reconcile now
      description: 'Synchronous. Never queues: a run already in flight answers 409 instead of blocking.'
      tags:
      - Docker discovery
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DockerDiscoveryStatus'
        '409':
          $ref: '#/components/responses/Conflict'
        '501':
          $ref: '#/components/responses/NotImplemented'
        '502':
          description: The Docker Engine API call failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: docker-discovery:write
  /api/docker-discovery/status:
    get:
      summary: Last Docker discovery reconcile result
      tags:
      - Docker discovery
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DockerDiscoveryStatus'
        '501':
          $ref: '#/components/responses/NotImplemented'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: docker-discovery:read
  /api/ingress-discovery/plan:
    get:
      summary: Preview an Ingress discovery reconcile
      description: Read-only dry run, without changing anything.
      tags:
      - Ingress discovery
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngressDiscoveryPlan'
        '409':
          $ref: '#/components/responses/Conflict'
        '501':
          $ref: '#/components/responses/NotImplemented'
        '502':
          description: The Kubernetes API call failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: ingress-discovery:read
  /api/ingress-discovery/reconcile:
    post:
      summary: Run an Ingress discovery reconcile now
      description: 'Synchronous. Never queues: a run already in flight answers 409 instead of blocking.'
      tags:
      - Ingress discovery
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngressDiscoveryStatus'
        '409':
          $ref: '#/components/responses/Conflict'
        '501':
          $ref: '#/components/responses/NotImplemented'
        '502':
          description: The Kubernetes API call failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: ingress-discovery:write
  /api/ingress-discovery/status:
    get:
      summary: Last Ingress discovery reconcile result
      tags:
      - Ingress discovery
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngressDiscoveryStatus'
        '501':
          $ref: '#/components/responses/NotImplemented'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: ingress-discovery:read
  /api/logs:
    get:
      summary: Recent proxied-request access-log entries
      description: Newest first, for the in-UI log viewer. Empty/disabled when no access-log viewer is
        wired.
      tags:
      - Meta
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Logs'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: '*:read'
    put:
      summary: Toggle proxied-request capture live
      description: Runtime-only - never persisted, so a restart reverts to the `-access-log` flag /
        `GPM_ACCESS_LOG`. Admin scope because it changes global logging output, not one resource.
      tags:
      - Meta
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - enabled
              properties:
                enabled:
                  type: boolean
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  enabled:
                    type: boolean
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: admin
  /api/me:
    get:
      summary: Current authenticated principal
      description: Proves the session/role gate and hands the SPA its CSRF token. Works for a session
        OR a bearer token.
      tags:
      - Auth
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Principal'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/middlewares:
    get:
      summary: List middlewares
      tags:
      - Middlewares
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Middleware'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: middlewares:read
  /api/middlewares/{name}:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Get one Middleware
      tags:
      - Middlewares
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Middleware'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: middlewares:read
    put:
      summary: Create or update a Middleware
      tags:
      - Middlewares
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Middleware'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Middleware'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: middlewares:write
    delete:
      summary: Delete a Middleware
      tags:
      - Middlewares
      responses:
        '204':
          $ref: '#/components/responses/NoContent'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: middlewares:write
  /api/middlewares/{name}/history:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Commit history for one Middleware
      description: Up to the 50 most recent commits touching this object's file.
      tags:
      - Middlewares
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Commit'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: middlewares:read
  /api/middlewares/{name}/revert:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    post:
      summary: Revert one Middleware to a past commit
      description: Restores only this object's file to its state at the given commit; every other object
        is left as-is. api-tokens is exempt from write scope by construction elsewhere, but reverting
        an APIToken specifically is refused (see docs/configuration.md) so a rotation always means revocation.
      tags:
      - Middlewares
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommitRef'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommitResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: middlewares:write
  /api/openapi.yaml:
    get:
      summary: This OpenAPI 3.1 specification
      description: Same auth as GET /api/me (any authenticated session or token; no particular role or
        scope beyond authentication).
      tags:
      - Meta
      responses:
        '200':
          description: OK
          content:
            application/yaml:
              schema:
                type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/proxy-hosts:
    get:
      summary: List proxy hosts
      tags:
      - Proxy hosts
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ProxyHost'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: proxy-hosts:read
  /api/proxy-hosts/{name}:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Get one ProxyHost
      tags:
      - Proxy hosts
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProxyHost'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: proxy-hosts:read
    put:
      summary: Create or update a ProxyHost
      tags:
      - Proxy hosts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProxyHost'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProxyHost'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: proxy-hosts:write
    delete:
      summary: Delete a ProxyHost
      tags:
      - Proxy hosts
      responses:
        '204':
          $ref: '#/components/responses/NoContent'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: proxy-hosts:write
  /api/proxy-hosts/{name}/history:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Commit history for one ProxyHost
      description: Up to the 50 most recent commits touching this object's file.
      tags:
      - Proxy hosts
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Commit'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: proxy-hosts:read
  /api/proxy-hosts/{name}/revert:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    post:
      summary: Revert one ProxyHost to a past commit
      description: Restores only this object's file to its state at the given commit; every other object
        is left as-is. api-tokens is exempt from write scope by construction elsewhere, but reverting
        an APIToken specifically is refused (see docs/configuration.md) so a rotation always means revocation.
      tags:
      - Proxy hosts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommitRef'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommitResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: proxy-hosts:write
  /api/redirect-hosts:
    get:
      summary: List redirect hosts
      tags:
      - Redirect hosts
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/RedirectHost'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: redirect-hosts:read
  /api/redirect-hosts/{name}:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Get one RedirectHost
      tags:
      - Redirect hosts
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedirectHost'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: redirect-hosts:read
    put:
      summary: Create or update a RedirectHost
      tags:
      - Redirect hosts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RedirectHost'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedirectHost'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: redirect-hosts:write
    delete:
      summary: Delete a RedirectHost
      tags:
      - Redirect hosts
      responses:
        '204':
          $ref: '#/components/responses/NoContent'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: redirect-hosts:write
  /api/redirect-hosts/{name}/history:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Commit history for one RedirectHost
      description: Up to the 50 most recent commits touching this object's file.
      tags:
      - Redirect hosts
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Commit'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: redirect-hosts:read
  /api/redirect-hosts/{name}/revert:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    post:
      summary: Revert one RedirectHost to a past commit
      description: Restores only this object's file to its state at the given commit; every other object
        is left as-is. api-tokens is exempt from write scope by construction elsewhere, but reverting
        an APIToken specifically is refused (see docs/configuration.md) so a rotation always means revocation.
      tags:
      - Redirect hosts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommitRef'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommitResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: redirect-hosts:write
  /api/restore:
    post:
      summary: Restore the whole config from a backup archive
      description: Replaces the current config, validates the result, and commits it as one revision.
        If the archive yields an invalid config (including a literal, uncommittable secret) the working
        tree is rolled back to HEAD and nothing is committed. Max 8MB gzipped body.
      tags:
      - Backup & restore
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommitResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: admin
      requestBody:
        required: true
        content:
          application/gzip:
            schema:
              type: string
              format: binary
  /api/revert:
    post:
      summary: Revert the whole config to a past commit
      description: Recorded as a new commit, so the revert is itself revertible. Rewrites every object,
        so it needs admin scope for a token principal.
      tags:
      - Meta
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommitRef'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommitResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: admin
  /api/runtime:
    get:
      summary: Read-only runtime facts (flags, env, listeners, paths)
      description: How THIS process was started. Reports no username, hash or secret value - only
        addresses, paths and booleans. Sourced from the flags/env parsed at startup, never re-read
        per request.
      tags:
      - Meta
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RuntimeInfo'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: settings:read
  /api/settings:
    get:
      summary: Get the singleton application settings
      tags:
      - Settings
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Settings'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: settings:read
    put:
      summary: Replace the application settings
      description: 'Admin scope, not settings:write: a settings write can point webhooks/dnsSync at an
        attacker-controlled URL with a ${ENV:...} placeholder, and can rewrite adminAuth.'
      tags:
      - Settings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Settings'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Settings'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: admin
  /api/sso/revoke:
    post:
      summary: Revoke every outstanding per-host SSO session
      description: Users re-authenticate at the IdP on their next request.
      tags:
      - Meta
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RevokeAck'
        '501':
          $ref: '#/components/responses/NotImplemented'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: admin
  /api/stream-hosts:
    get:
      summary: List stream hosts
      tags:
      - Stream hosts
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/StreamHost'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: stream-hosts:read
  /api/stream-hosts/{name}:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Get one StreamHost
      tags:
      - Stream hosts
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StreamHost'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: stream-hosts:read
    put:
      summary: Create or update a StreamHost
      tags:
      - Stream hosts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StreamHost'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StreamHost'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: stream-hosts:write
    delete:
      summary: Delete a StreamHost
      tags:
      - Stream hosts
      responses:
        '204':
          $ref: '#/components/responses/NoContent'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: stream-hosts:write
  /api/stream-hosts/{name}/history:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Commit history for one StreamHost
      description: Up to the 50 most recent commits touching this object's file.
      tags:
      - Stream hosts
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Commit'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: stream-hosts:read
  /api/stream-hosts/{name}/revert:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    post:
      summary: Revert one StreamHost to a past commit
      description: Restores only this object's file to its state at the given commit; every other object
        is left as-is. api-tokens is exempt from write scope by construction elsewhere, but reverting
        an APIToken specifically is refused (see docs/configuration.md) so a rotation always means revocation.
      tags:
      - Stream hosts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommitRef'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommitResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: stream-hosts:write
  /api/upstream-groups:
    get:
      summary: List upstream groups
      tags:
      - Upstream groups
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/UpstreamGroup'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: upstream-groups:read
  /api/upstream-groups/{name}:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Get one UpstreamGroup
      tags:
      - Upstream groups
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpstreamGroup'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: upstream-groups:read
    put:
      summary: Create or update an UpstreamGroup
      tags:
      - Upstream groups
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpstreamGroup'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpstreamGroup'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: upstream-groups:write
    delete:
      summary: Delete an UpstreamGroup
      tags:
      - Upstream groups
      responses:
        '204':
          $ref: '#/components/responses/NoContent'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: upstream-groups:write
  /api/upstream-groups/{name}/history:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    get:
      summary: Commit history for one UpstreamGroup
      description: Up to the 50 most recent commits touching this object's file.
      tags:
      - Upstream groups
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Commit'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: upstream-groups:read
  /api/upstream-groups/{name}/revert:
    parameters:
    - $ref: '#/components/parameters/NamePath'
    post:
      summary: Revert one UpstreamGroup to a past commit
      description: Restores only this object's file to its state at the given commit; every other object
        is left as-is. api-tokens is exempt from write scope by construction elsewhere, but reverting
        an APIToken specifically is refused (see docs/configuration.md) so a rotation always means revocation.
      tags:
      - Upstream groups
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommitRef'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommitResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '503':
          $ref: '#/components/responses/FollowerReadOnly'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: upstream-groups:write
  /api/upstream-health:
    get:
      summary: Live per-upstream-group health
      tags:
      - Meta
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpstreamHealth'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: '*:read'
  /api/webhooks/status:
    get:
      summary: Last-delivery state per webhook target
      description: In-memory and per-process - an operational hint, never config. Every configured
        target is listed; one that has never fired has an empty lastAttempt.
      tags:
      - Webhooks
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/WebhookDelivery'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: settings:read
  /api/webhooks/{name}/test:
    post:
      summary: Send a synthetic test event to one webhook target
      description: 'POSTs {"action":"test","kind":"Webhook","name":"<target>","time":"<RFC3339>"} and
        waits for the answer (5s timeout). A disabled target is still tested, so a receiver can be
        proved before it is turned on. A refused or timed-out delivery is a 200 with ok:false - only
        an unknown target name is a 404. Admin scope: it makes gpm POST to an admin-configured URL on
        demand and resolves that target''s secret.'
      tags:
      - Webhooks
      parameters:
      - name: name
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: The test ran; see ok/error for what the receiver did.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookTestResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No webhook target with that name is configured.
        '501':
          description: Webhook delivery is not wired in this deployment.
      x-scope: admin
  /api/notifications/status:
    get:
      summary: Last-delivery state per notification target
      description: In-memory and per-process - an operational hint, never config. Every configured
        target is listed; one that has never fired has an empty lastAttempt. Same shape as
        /api/webhooks/status (internal/notify reuses webhook.Delivery).
      tags:
      - Notifications
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/WebhookDelivery'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: settings:read
  /api/notifications/{name}/test:
    post:
      summary: Send a synthetic test notification to one target
      description: 'Sends a synthetic event and waits for the answer (5s timeout), bypassing the
        target''s events filter and the dedup window. A disabled target is still tested, so a
        receiver can be proved before it is turned on. A refused or timed-out delivery is a 200
        with ok:false - only an unknown target name is a 404. Admin scope: it makes gpm POST to an
        admin-configured URL on demand and resolves that target''s secret.'
      tags:
      - Notifications
      parameters:
      - name: name
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: The test ran; see ok/error for what the receiver did.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookTestResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No notification target with that name is configured.
        '501':
          description: Notification delivery is not wired in this deployment.
      x-scope: admin
  /auth/callback:
    get:
      summary: OIDC authorization-code callback
      description: Verifies state against the login cookie, exchanges the code, establishes the session,
        and redirects to the original return path.
      tags:
      - Auth
      parameters:
      - name: state
        in: query
        required: true
        schema:
          type: string
      - name: code
        in: query
        required: true
        schema:
          type: string
      - name: error
        in: query
        required: false
        schema:
          type: string
        description: Set by the IdP on a failed/denied authorization.
      responses:
        '302':
          description: Redirect to the original return path, with the session cookie set.
        '400':
          description: Missing state/code, or the state does not match the login cookie.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: The IdP reported an error, or the code exchange / identity checks failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security: []
  /auth/local:
    post:
      summary: Local username/password login
      description: 'Same-origin only (CSRF belt: no session exists yet for a token-based defense). Rate-limited
        per client IP.'
      tags:
      - Auth
      responses:
        '302':
          description: Redirect to the return path, with the session cookie set.
        '403':
          description: Cross-origin request blocked (failed the same-origin guard).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Too many recent failed attempts from this client IP.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Bad credentials, or local login is disabled/not configured.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /auth/local/totp:
    post:
      summary: Local login, second factor (TOTP)
      description: 'Step two of local login when GPM_LOCAL_ADMIN_TOTP_SECRET is set. Form fields: `pending`
        (the single-use token issued by POST /auth/local), `code` (6-digit TOTP), `return`. Same-origin
        only. A wrong code counts toward the SAME per-client-IP lockout as a wrong password, and the pending
        token is consumed either way.'
      tags:
      - Auth
      responses:
        '302':
          description: Redirect to the return path, with the session cookie set.
        '403':
          description: Cross-origin request blocked (failed the same-origin guard).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Too many recent failed attempts from this client IP.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Wrong, expired or already-used code, or an expired/unknown pending token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required:
              - pending
              - code
              properties:
                pending:
                  type: string
                  description: The single-use pending-login token issued by POST /auth/local. Consumed
                    whether or not the code verifies.
                code:
                  type: string
                  description: The 6-digit TOTP code from the authenticator app.
                  pattern: '^[0-9]{6}$'
                return:
                  type: string
                  description: Same-site path to redirect to on success.
  /auth/login:
    get:
      summary: Start (or choose) an admin login flow
      description: With ?idp=<name>, redirects straight to that IdentityProvider's OIDC flow. With none,
        renders an HTML chooser (SSO providers + local login form), unless exactly one SSO provider is
        configured and local login is off, in which case it redirects immediately (?select=1 forces the
        chooser, used by logout).
      tags:
      - Auth
      parameters:
      - name: idp
        in: query
        required: false
        schema:
          type: string
        description: IdentityProvider name to start an OIDC redirect immediately.
      - name: return
        in: query
        required: false
        schema:
          type: string
        description: Same-site path to return to after login. Anything else (open-redirect shapes) is
          discarded in favour of "/".
      - name: select
        in: query
        required: false
        schema:
          type: string
        description: Any non-empty value forces the chooser page even with a single SSO provider.
      responses:
        '200':
          description: HTML login chooser.
          content:
            text/html:
              schema:
                type: string
        '302':
          description: Redirect to the IdP's authorization endpoint.
        '502':
          description: The IdP could not be reached to start the flow.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security: []
  /auth/logout:
    post:
      summary: End the current admin session
      description: Same-origin only. Redirects to the login chooser (?select=1) rather than bouncing straight
        back into a still-live IdP session.
      tags:
      - Auth
      responses:
        '302':
          description: Redirect to /auth/login?select=1, with the session cookie cleared.
        '403':
          description: Cross-origin request blocked (failed the same-origin guard).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /debug/pprof/:
    get:
      summary: pprof index
      description: Only mounted when the instance is started with GPM_PPROF=1 (-pprof); a 404 here means
        profiling is off, not that this route doesn't exist. Gated by admin role, the same-origin guard,
        AND (unlike every other admin route) an explicit `admin` API-token scope check - a resource-scoped
        token gets 403, because a heap dump or /debug/pprof/cmdline carries resolved backend credentials
        in cleartext. `go tool pprof <url>` direct remote-attach does not work (no cookie, and its /debug/pprof/symbol
        POST fails CSRF); download the profile with curl -b and run pprof against the local file.
      tags:
      - Profiling (opt-in)
      responses:
        '200':
          description: OK
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: admin
  /debug/pprof/cmdline:
    get:
      summary: Process command line
      description: Only mounted when the instance is started with GPM_PPROF=1 (-pprof); a 404 here means
        profiling is off, not that this route doesn't exist. Gated by admin role, the same-origin guard,
        AND (unlike every other admin route) an explicit `admin` API-token scope check - a resource-scoped
        token gets 403, because a heap dump or /debug/pprof/cmdline carries resolved backend credentials
        in cleartext. `go tool pprof <url>` direct remote-attach does not work (no cookie, and its /debug/pprof/symbol
        POST fails CSRF); download the profile with curl -b and run pprof against the local file.
      tags:
      - Profiling (opt-in)
      responses:
        '200':
          description: OK
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: admin
  /debug/pprof/profile:
    get:
      summary: CPU profile (?seconds=N)
      description: Only mounted when the instance is started with GPM_PPROF=1 (-pprof); a 404 here means
        profiling is off, not that this route doesn't exist. Gated by admin role, the same-origin guard,
        AND (unlike every other admin route) an explicit `admin` API-token scope check - a resource-scoped
        token gets 403, because a heap dump or /debug/pprof/cmdline carries resolved backend credentials
        in cleartext. `go tool pprof <url>` direct remote-attach does not work (no cookie, and its /debug/pprof/symbol
        POST fails CSRF); download the profile with curl -b and run pprof against the local file.
      tags:
      - Profiling (opt-in)
      responses:
        '200':
          description: OK
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: admin
  /debug/pprof/symbol:
    get:
      summary: Symbol lookup (used by go tool pprof)
      description: Only mounted when the instance is started with GPM_PPROF=1 (-pprof); a 404 here means
        profiling is off, not that this route doesn't exist. Gated by admin role, the same-origin guard,
        AND (unlike every other admin route) an explicit `admin` API-token scope check - a resource-scoped
        token gets 403, because a heap dump or /debug/pprof/cmdline carries resolved backend credentials
        in cleartext. `go tool pprof <url>` direct remote-attach does not work (no cookie, and its /debug/pprof/symbol
        POST fails CSRF); download the profile with curl -b and run pprof against the local file.
      tags:
      - Profiling (opt-in)
      responses:
        '200':
          description: OK
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: admin
  /debug/pprof/trace:
    get:
      summary: Execution trace (?seconds=N)
      description: Only mounted when the instance is started with GPM_PPROF=1 (-pprof); a 404 here means
        profiling is off, not that this route doesn't exist. Gated by admin role, the same-origin guard,
        AND (unlike every other admin route) an explicit `admin` API-token scope check - a resource-scoped
        token gets 403, because a heap dump or /debug/pprof/cmdline carries resolved backend credentials
        in cleartext. `go tool pprof <url>` direct remote-attach does not work (no cookie, and its /debug/pprof/symbol
        POST fails CSRF); download the profile with curl -b and run pprof against the local file.
      tags:
      - Profiling (opt-in)
      responses:
        '200':
          description: OK
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      x-scope: admin
  /healthz:
    get:
      summary: Liveness probe
      tags:
      - Meta
      responses:
        '200':
          description: OK
          content:
            text/plain:
              schema:
                type: string
                example: ok
      security: []
  /metrics:
    get:
      summary: Prometheus metrics exposition
      description: Only mounted when the instance is started with GPM_METRICS=1 (-metrics); a 404 here
        means metrics are off, not that this route doesn't exist. Gated by admin role plus a `metrics:read`
        API-token scope check (an admin session needs no scope). The body is Prometheus text exposition
        format 0.0.4, namespaced `gpm_`; host labels are ProxyHost/StreamHost NAMES from the committed
        config, never client-supplied Host headers, and series count is capped per metric. It names hosts
        and certificates, so it is admin data - do not expose the admin listener to scrape it, scrape it
        over your own private path.
      tags:
      - Metrics (opt-in)
      responses:
        '200':
          description: OK
          content:
            text/plain:
              schema:
                type: string
                example: |
                  # HELP gpm_http_requests_total Proxy listener HTTP requests, by proxy host, method and status class.
                  # TYPE gpm_http_requests_total counter
                  gpm_http_requests_total{host="app",method="GET",status="2xx"} 12
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /version:
    get:
      summary: Build identity
      tags:
      - Meta
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VersionInfo'
      security: []
