apiserver

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: MIT Imports: 21 Imported by: 0

README

go-apiserver

The embedded REST control plane of the go-apps family: a small HTTP server every app carries so an AI agent (or any script) can drive and test the real application — engine and UI — over localhost. Pure standard library.

Used by go-notepad and go-calc.

What it gives you

  • Auth: X-API-Key header (constant-time compare) + a CIDR allowlist. The server binds to 127.0.0.1 while only loopback is allowlisted (no Windows Firewall prompt) and only opens 0.0.0.0 when a LAN IP is allowed.
  • Built-in endpoints:
    • GET /v1/health — reachability + auth probe.
    • GET /v1/ax — your app descriptor / accessibility tree (injected InfoFunc): the document that tells an agent what the app is, every control's testid, action, keyboard shortcut and risk level.
    • GET /v1/ui/state, POST /v1/ui/{press,dblclick,key,input} — the UI bridge: commands are forwarded to your UIController implementation, which operates the real frontend and reports the resulting screen state.
  • App endpoints: register your own with HandleExtra("/v1/...", handler) — they sit behind the same auth. DecodeJSON, WriteJSON and WriteErr keep them on the family's structured contract.
  • Structured errors everywhere: {"error":{code,message,status}} with stable codes (unauthorized, forbidden, invalid_json, missing_field, method_not_allowed, unknown_testid, disabled_control, ui_timeout).
  • Optional HTTPS with a self-signed certificate whose public-key pin is stable across restarts: the ECDSA key persists in CertDir, a fresh leaf is minted per start covering the current SANs, so a pinned client (curl --pinnedpubkey sha256//<pin>) never breaks.

Usage

import "github.com/viniciusbuscacio/go-apiserver"

srv := apiserver.New(myAppInfo, myUIBridge) // InfoFunc, UIController
srv.HandleExtra("/v1/stats", handleStats)   // your domain endpoints

err := srv.Start(apiserver.Config{
    Port:      8800,
    Key:       apiKey,
    Allowlist: []string{"127.0.0.1/32"},
    TLS:       false,
    CertDir:   configDir, // required when TLS is true
    AppName:   "my-app",  // TLS certificate CommonName
})
// ...
_ = srv.Stop()

A domain endpoint on the family contract:

func handleStats(w http.ResponseWriter, r *http.Request) {
    var req struct{ Text string `json:"text"` }
    if !apiserver.DecodeJSON(w, r, &req) { // POST + 1MiB cap + invalid_json
        return
    }
    result, err := compute(req.Text)
    if err != nil {
        apiserver.WriteErr(w, http.StatusUnprocessableEntity, "operation_error", err.Error())
        return
    }
    apiserver.WriteJSON(w, http.StatusOK, result)
}

UIController is whatever drives your frontend; in the Wails apps it is a small bridge that emits a ui:command event, the page performs the action against the real DOM (click by data-testid, dispatch keys, set input values), waits for the UI to settle and reports the screen state back. See go-notepad's uibridge.go + frontend/src/uibridge.ts for the reference implementation, and each app's docs/agent-api.md for the agent-facing guide.

Development

go test ./...

Documentation

Overview

Package apiserver is the embedded REST control plane of the go-apps family: a small, self-contained HTTP server guarded by an API key and an IP allowlist (CIDRs). It serves the app descriptor (/v1/ax), health, and the UI-bridge endpoints (/v1/ui/*) that let an automated client operate the real frontend; each app registers its own domain endpoints with HandleExtra. Optional HTTPS uses a self-signed certificate whose public-key pin stays stable across restarts (see tls.go).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BindHost

func BindHost(allowlist []string) string

BindHost returns "127.0.0.1" when every allowlisted CIDR is loopback (so the server stays local and no firewall prompt appears), or "0.0.0.0" when a non-loopback IP is allowed and LAN access is actually intended.

func DecodeJSON

func DecodeJSON(w http.ResponseWriter, r *http.Request, req any) bool

DecodeJSON is the request-side helper for POST endpoints (built-in and HandleExtra alike): it enforces the method, caps the body at 1 MiB and decodes the JSON into req, answering the structured 405/400 itself. Returns false when the response has already been written.

func NormalizeCIDR

func NormalizeCIDR(s string) (string, error)

NormalizeCIDR validates user input and returns the canonical CIDR. A bare IP gets a host mask (/32 or /128) so the allowlist always carries a mask.

func OutboundIP

func OutboundIP() string

OutboundIP returns the machine's primary LAN IP (no packets are sent).

func WriteErr

func WriteErr(w http.ResponseWriter, status int, code, msg string)

func WriteJSON

func WriteJSON(w http.ResponseWriter, code int, v any)

WriteJSON / WriteErr are the exported faces of the helpers below, so HandleExtra endpoints answer in the same shapes (including the structured {"error":{code,message,status}} contract).

Types

type Config

type Config struct {
	Port      int
	Key       string
	Allowlist []string
	TLS       bool   // serve HTTPS with a self-signed cert (used for LAN exposure)
	CertDir   string // where the stable TLS key lives; required when TLS is true
	AppName   string // certificate CommonName; "go-app" when empty
}

type InfoFunc

type InfoFunc func() any

InfoFunc returns the app descriptor / accessibility tree served at /v1/ax.

type Server

type Server struct {
	// contains filtered or unexported fields
}

func New

func New(info InfoFunc, ui UIController) *Server

func (*Server) Addr

func (s *Server) Addr() string

Addr is the actual bound address (host:port), valid while running.

func (*Server) CertPEM

func (s *Server) CertPEM() []byte

CertPEM is the current self-signed certificate, for `curl --cacert` export. Nil when serving plain HTTP.

func (*Server) Fingerprint

func (s *Server) Fingerprint() string

Fingerprint is the public-key pin a client fixes (base64 SHA-256 SPKI). Empty when serving plain HTTP.

func (*Server) HandleExtra

func (s *Server) HandleExtra(path string, h http.HandlerFunc)

HandleExtra registers an app-specific endpoint (e.g. "/v1/stats", "/v1/update") served behind the same key + allowlist middleware as the built-in routes. Register before Start; a running server picks it up on its next (re)start. Use DecodeJSON / WriteJSON / WriteErr inside the handler to honour the family's structured contract.

func (*Server) Running

func (s *Server) Running() bool

func (*Server) Start

func (s *Server) Start(cfg Config) error

Start binds and serves. It validates the allowlist up front so a bad CIDR surfaces to the UI instead of silently failing.

func (*Server) Stop

func (s *Server) Stop() error

type UIController

type UIController interface {
	State() (any, error)
	Press(testid string) (any, error)
	DblClick(testid string) (any, error)
	Key(key string) (any, error)
	Input(testid, value string) (any, error)
}

UIController drives the live frontend so a client can test the actual UI: read what is on screen, click controls by testid, press keys, type into inputs. Each call returns the resulting UI state. Implemented by the host app over its UI framework (in the family apps: Wails events → Vue).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL