zip

package module
v1.37.8 Latest Latest
Warning

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

Go to latest
Published: Sep 17, 2026 License: MIT Imports: 50 Imported by: 0

README

zip

zip is a Go web framework on fasthttp. You write an operation once, as a function with a typed input and output, and zip serves it as a REST route, an OpenAPI operation, an MCP tool, a CLI command and a call other services make over ZAP.

Install

go get github.com/zap-proto/zip@latest
go list -m github.com/zap-proto/zip
github.com/zap-proto/zip v1.36.48

zip needs Go 1.26.8 or newer. go list -m -versions github.com/zap-proto/zip lists every release. A copy taken from an old module cache may be many releases behind; see Upgrading.

A service in one file

examples/local-service stores notes in SQLite and has one operation. This is its main.go:

// Command local-service is a zip service with one operation, backed by SQLite.
//
// The operation is declared once, in New, and every interface below is derived
// from that declaration:
//
//	go run . serve                          # REST, MCP and OpenAPI on 127.0.0.1:8080
//	go run . notes create --text "buy milk" # the same operation as a command
//	go run . openapi openapi.json           # the OpenAPI document, written to a file
//
// Notes are stored in notes.db in the working directory.
package main

//go:generate go tool zipdoc

import (
	"context"
	"database/sql"
	"fmt"
	"os"

	"github.com/hanzoai/sqlite"
	"github.com/zap-proto/zip"
)

// Note is one stored note.
type Note struct {
	// ID is the row id SQLite assigned.
	ID int64 `json:"id"`
	// Text is the note as written.
	Text string `json:"text"`
}

// AddIn is a note to store.
type AddIn struct {
	// Text is the note body.
	Text string `json:"text" validate:"required"`
}

// Store keeps notes in one SQLite file.
type Store struct {
	db *sql.DB
}

// Open opens the SQLite file at path, creating it and its table if needed.
func (s *Store) Open(path string) error {
	db, err := sqlite.OpenDB(path, nil)
	if err != nil {
		return err
	}
	if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, text TEXT NOT NULL)`); err != nil {
		db.Close()
		return err
	}
	s.db = db
	return nil
}

// Add stores a note and returns it with its id.
//
// Example: {"text": "buy milk"}
// Response: {"id": 1, "text": "buy milk"}
func (s *Store) Add(ctx context.Context, in *AddIn) (*Note, error) {
	res, err := s.db.ExecContext(ctx, `INSERT INTO notes (text) VALUES (?)`, in.Text)
	if err != nil {
		return nil, err
	}
	id, err := res.LastInsertId()
	if err != nil {
		return nil, err
	}
	return &Note{ID: id, Text: in.Text}, nil
}

// New declares the service's operations. It does not touch the database, so the
// OpenAPI document can be written by a build step that has none.
func New(s *Store) *zip.App {
	app := zip.New(zip.Config{AppName: "notes"})
	app.Post("/v1/notes", s.Add)
	return app
}

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func run() error {
	s := &Store{}
	app := New(s)
	// `local-service openapi <file>` and `local-service declare <file>` write a
	// projection and stop here, before the database is opened.
	if done, err := app.Described(); done {
		return err
	}
	if err := s.Open("notes.db"); err != nil {
		return err
	}
	if len(os.Args) > 1 && os.Args[1] == "serve" {
		return app.Listen("http://127.0.0.1:8080")
	}
	cli := app.CLI()
	cli.Out = os.Stdout
	return cli.Run(context.Background(), os.Args[1:])
}

Start it:

cd examples/local-service
go run . serve

From a second terminal, call the operation three ways. Over REST:

curl -s -X POST http://127.0.0.1:8080/v1/notes -H 'Content-Type: application/json' -d '{"text":"buy milk"}'
{"id":1,"text":"buy milk"}

As an MCP tool:

curl -s -X POST http://127.0.0.1:8080/mcp -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"post_notes","arguments":{"text":"call the bank"}}}'
{"jsonrpc":"2.0","id":3,"result":{"content":[{"text":"{\"id\":2,\"text\":\"call the bank\"}","type":"text"}]}}

As a command. It runs the handler in this process, against the same notes.db; zip's log lines go to stderr:

go run . notes create --text "water the plants"
{
  "id": 3,
  "text": "water the plants"
}

One handler and one table produced ids 1, 2 and 3. The OpenAPI document needs neither a server nor the database:

go run . openapi openapi.json

The /v1/notes entry, with its examples removed:

{
  "post": {
    "operationId": "post_notes",
    "summary": "Stores a note and returns it with its id.",
    "description": "Stores a note and returns it with its id.",
    "requestBody": {
      "required": true,
      "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AddIn"}}}
    },
    "responses": {
      "200": {"description": "ok", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Note"}}}},
      "default": {"description": "refused", "content": {"application/problem+json": {"schema": {"$ref": "#/components/schemas/problem-details"}}}}
    }
  }
}

go generate rebuilds zipdoc_gen.go from the doc comments, and go test checks the operation over REST, MCP, OpenAPI and the CLI. The directory is its own module, so the SQLite driver (github.com/hanzoai/sqlite) is not a dependency of zip. A copy outside this repository drops the replace line from its go.mod.

Projections

Everything below starts from the one declaration app.Post("/v1/notes", s.Add).

REST
Declare Get, Post, Put, Patch, Delete on an *App or a *Group, called as g.Post(path, fn, opts...) — In and Out are inferred from the handler. A route that is not a typed operation uses Raw(method, path, handlers...).
Input The JSON body (GET, HEAD and DELETE have none), then fields tagged header:"X-Name", then the query string, then path parameters. The URL wins. validate:"required" refuses a request without the field.
Options zip.WithOperationID, zip.WithSummary, zip.WithTags, zip.WithStatus(201), zip.WithResponseHeader.
Untyped routes app.Get(path, func(c *zip.Ctx) error) registers a route with no operation, so it is in none of the projections below. Use it for streams, protocol upgrades and non-JSON bodies.
Tests app.Test(httptest.NewRequest(...)) serves a request without a socket, including /mcp and the OpenAPI document.
OpenAPI
  • GET /.well-known/openapi.json (zip.SpecPath) serves OpenAPI 3.1. app.OpenAPISpec() returns the same document as a map.
  • app.Described() writes a projection when the binary runs as <binary> openapi <file> or <binary> declare <file>, and reports whether it did. Call it after registering operations and before opening a database, as run does above.
  • zip.Config{OpenAPI: zip.OpenAPIConfig{Title, Description, Version, Disabled}} sets the info block or turns the document off.
  • Every operation declares a default response: the problem-details schema under application/problem+json, or oauth-error at an address declared with zip.OAuth. SDK generators type errors from it.
  • GET /docs (zip.DocsPath) is a Swagger UI page that loads its script and stylesheet from cdn.jsdelivr.net, so it is blank without network access. The JSON document needs none.
Doc comments

cmd/zipdoc copies doc comments into the OpenAPI document, the MCP tool list and the CLI help:

go get -tool github.com/zap-proto/zip/cmd/zipdoc

Put //go:generate go tool zipdoc in the package that registers the operations and run go generate. It writes zipdoc_gen.go: the handler's comment becomes the description, its Example: and Response: lines become examples, and field comments become schema descriptions. In CI, run go tool zipdoc -check in that package's directory. It fails when zipdoc_gen.go no longer matches the source.

MCP

Each operation is a tool named by its operation id (post_notes), with its input schema as inputSchema. GET operations carry readOnlyHint: true. app.MCPTools() returns the list in-process.

curl -s -X POST http://127.0.0.1:8080/mcp -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"tools":{"listChanged":false}},"protocolVersion":"2025-11-25","serverInfo":{"name":"notes","version":""}}}

What the HTTP door does:

Transport POST /mcp: one JSON-RPC message per request, answered with one application/json message.
GET and DELETE 405 Method Not Allowed with Allow: POST. There is no SSE stream; a request that accepts only text/event-stream still gets JSON.
Sessions None. No Mcp-Session-Id, and nothing is kept between requests.
Batches Not accepted. A JSON array answers -32700 parse error.
Notifications 202 Accepted.
Methods initialize, server/discover, ping, tools/list, tools/call. No resources, prompts, sampling or logging.
Protocol version initialize answers the revision the client asked for when it is 2025-06-18, 2025-11-25 or 2026-07-28, and 2026-07-28 otherwise.
Tool errors A result with isError: true and the error text, not a JSON-RPC error.
Origin Not checked. Bind a local service to 127.0.0.1, as the example does, and serve anything else behind a gateway.

examples/local-service/.mcp.json registers the running example with Claude Code. The official MCP clients connect to it, list its tools and call them: the TypeScript SDK 1.30.0, and the Python SDK 2.2.0 through initialize and in its default auto mode.

zip.Config{MCP: zip.MCPConfig{...}} has Disabled, Path and Addr, which serves the same tools over ZAP with no HTTP listener. app.MCP is the door itself, a zapmcp.Handler.

CLI

app.CLI() returns a *zip.CLI that runs operations in this process. Set Out before Run:

cli := app.CLI()
cli.Out = os.Stdout
err := cli.Run(ctx, os.Args[1:])

A command is <service> <operation>: the first path segment after the version, then a verb from the method, so POST /v1/notes is notes create. Path parameters are arguments and the other input fields are flags. --help prints the doc comment, the flags and an example:

$ go run . notes create --help
notes notes create [flags]

Stores a note and returns it with its id.

POST /v1/notes

Flags:
  --text string  Text is the note body. (required)

Example:
  notes notes create --text "buy milk"

A client that links none of the service builds the same commands from the served document with zip.CommandsFromSpec, and sends them with zip.Remote:

remote := zip.Remote{Base: "http://127.0.0.1:8080"}
spec, err := remote.Spec(ctx)
if err != nil {
	return err
}
commands, err := zip.CommandsFromSpec(spec)
if err != nil {
	return err
}
cli := &zip.CLI{Name: "notes", Commands: commands, Invoke: remote.Invoke, Out: os.Stdout}
return cli.Run(ctx, os.Args[1:])
$ go run . notes create --text "sent over HTTP"
{
  "id": 4,
  "text": "sent over HTTP"
}

Remote.Header sets headers, such as Authorization, on every request.

Calls between services
conn, err := zip.Dial("http://127.0.0.1:8080")
if err != nil {
	return err
}
note, err := zip.Call[AddIn, Note](ctx, conn, "post_notes", &AddIn{Text: "over the call plane"})

The caller imports or restates AddIn and Note and links nothing else of the service. A refusal is a *zip.HTTPError carrying the callee's status. zip.DialApp(name) dials the unix socket at zip.SocketPath(name), which is how services on one host reach each other.

This body is ZAP, laid out in the order of the struct's fields. Add fields only at the end of an input or output struct. Reordering, inserting or retyping a field changes the wire and breaks callers built against the old layout. REST and MCP bodies are JSON, where field order does not matter.

SDKs
Target How
Go app.SDK("notes") returns a *zip.SDK. Source is a package whose Client (notes.Dial(addr), then client.PostNotes(ctx, in)) uses zip.Call; Gaps lists operations it could not express.
Rust, C++ app.RustSDK(crate), app.CppSDK(namespace).
From a schema cmd/zipgen reads a .zap schema, not Go code. zip.ZAPSchema("notes", app).String() writes one; then go run github.com/zap-proto/zip/cmd/zipgen sdk -schema notes.zap -lang go -pkg notes -o client.go. -lang also takes rust and cpp, and the other subcommands are mcp, cli, docs and zap.
Other languages Generate from /.well-known/openapi.json.
Errors

Every refusal is an RFC 9457 problem document under application/problem+json:

curl -s -X POST http://127.0.0.1:8080/v1/notes -H 'Content-Type: application/json' -d '{}'
{"detail":"field \"text\" is required","status":400,"title":"Bad Request","type":"about:blank"}
  • A handler returns zip.ErrBadRequest(msg), ErrUnauthorized, ErrForbidden, ErrNotFound, ErrConflict, ErrPaymentRequired, ErrUnprocessable, ErrInternal, or zip.Errorf(status, format, args...). &zip.HTTPError{Status: 409, Code: "taken", Msg: "..."} adds code, and .With(map[string]any{"cap": 5000}) adds members beside the standard ones.
  • Any other error is a 500 whose detail is err.Error(). Wrap errors whose text a caller should not read.
  • An address declared through zip.OAuth(router) answers RFC 6749 {"error", "error_description"} under application/json instead.
  • zip.Config{ErrorHandler: ...}, a fiber.ErrorHandler from github.com/zap-proto/fiber/v3, replaces the writer for every refusal.
  • Over MCP a refusal is isError content; CLI.Run returns it as an error.

Problem documents arrived in v1.32.0. Through v1.31.4, v1.18.x included, a refusal was {"status":400,"code":"...","error":"..."} under application/json; code that reads error reads detail now.

Security

zip validates no tokens. It reads identity from headers set by the gateway in front of it, after the gateway has checked the Hanzo IAM token: X-Org-Id, X-User-Id, X-User-Email and the others named by the zip.Header* constants. zip.CallerOf(ctx) returns them in a handler. A service reachable without that gateway in front must not trust these headers.

app.Authorize installs one rule that runs for every operation after its input is decoded and validated and before the handler, however the operation was reached: REST, MCP, zip.Call or the in-process CLI.

app.Authorize(func(ctx context.Context, op zip.Op, in any) (zip.Decision, error) {
	if zip.CallerOf(ctx).Org == "" {
		return zip.Decision{Effect: zip.Deny, Clause: "org", Reason: "no org on the request"}, nil
	}
	return zip.Decision{Effect: zip.Allow}, nil
})

A request to the example without X-Org-Id is then refused with a 403:

{"code":"org","detail":"no org on the request","status":403,"title":"Forbidden","type":"about:blank"}
  • zip.Deny is a 403 with Clause as code and Reason as detail. zip.Approve holds the operation and answers 202 with a zip.Approval; a Go caller reads it with zip.HeldOf(err). A returned error aborts the call.
  • The in-process CLI has no request, so CallerOf is empty unless the context was built with zip.WithCaller(ctx, zip.Caller{Org: "acme"}).
  • app.OnResult(fn) is told the outcome of every operation, for audit records.
  • CallerOf(ctx).IP is the socket peer. Config.TrustProxy, TrustedProxies and ProxyHeader opt in to an address a trusted proxy forwards.

Testing

TestEveryOperationIsInEveryProjection in examples/local-service/main_test.go reads the router and checks each operation against the OpenAPI document, the MCP tool list and the CLI, so an operation added later is covered with no change to the test:

for _, r := range app.Routes() {
	if r.Op == "" {
		continue // an untyped route has no projections
	}
	op := spec.Paths[zip.Template(r.Pattern)][strings.ToLower(r.Method)]
	if op.OperationID != r.Op {
		t.Errorf("%s %s: OpenAPI operationId %q, want %q", r.Method, r.Pattern, op.OperationID, r.Op)
	}
	if !tools[r.Op] {
		t.Errorf("%s %s: no MCP tool %q", r.Method, r.Pattern, r.Op)
	}
	if !commands[r.Op] {
		t.Errorf("%s %s: no CLI command %q", r.Method, r.Pattern, r.Op)
	}
}

Upgrading

Version Change
v1.32.0 Refusals are RFC 9457 problem documents under application/problem+json. Before, {"status","code","error"} under application/json.
v1.36.48 The OpenAPI document declares a default refusal response on every operation. MCP initialize answers the revision the client asked for. Doc comments reach binaries whose handlers are in package main; before, they did only under go test.

v1.36.21 is retracted.

Repository

Path
examples/local-service the service above, a module of its own
examples/hello, examples/zap-typed smaller typed services
examples/sse-streaming, examples/websocket the two untyped routes, each saying why
examples/migrate-from-chi, -gin, -beego existing apps moved onto zip
cmd/zipdoc, cmd/zipgen doc comments; code generation from .zap schemas
middleware Recover, RequestID, Timeout, MaxBody, CORS, RateLimit, NewBreaker, ProductionHeaders
wsx, js WebSocket upgrades; JavaScript and TypeScript handlers run in goja
LLM.md notes for changing zip itself

License

MIT. See LICENSE.

Documentation

Overview

Package zip is Hanzo's canonical Go web framework. Built on Fiber v3 / fasthttp. ZAP-typed handlers. Multi-language extension support via HIP-0105.

ONE framework, ZERO escape hatches. zip IS fast.

app := zip.New(zip.Config{Logger: luxlog.NewLogger("svc")})
app.Use(middleware.Recover(), middleware.RequestID())
app.Get("/health", func(c *zip.Ctx) error {
    return c.JSON(200, fiber.Map{"ok": true})
})
app.Listen(":9653", "http://:8080") // ZAP primary + HTTP extra, one verb

Public surface — types/functions exposed at the package root:

type App, Config, Ctx, Handler
func New(Config) *App
func Get[I, O](app *App, path string, fn func(ctx, *I) (*O, error))
func Post[I, O](app *App, path string, fn func(ctx, *I) (*O, error))
func Dial(addr string) (*Conn, error) / DialApp(name string) (*Conn, error)
func Call[I, O](ctx, *Conn, op string, in *I) (*O, error)
...

All other behavior lives in subpackages: `middleware`, `js`.

Index

Constants

View Source
const (
	HeaderOrg       = "X-Org-Id"
	HeaderProject   = "X-Project-Id"
	HeaderUser      = "X-User-Id"
	HeaderUserName  = "X-User-Name"
	HeaderUserEmail = "X-User-Email"
	HeaderUserOwner = "X-User-Owner"
	// HeaderActedBy names the principal ACTING when a call is made on another
	// org's behalf. Present only for an impersonation ([ActingAs]); its absence
	// is the ordinary case and means the caller is acting as itself.
	HeaderActedBy      = "X-Acted-By"
	HeaderUserAdmin    = "X-User-IsAdmin"
	HeaderUserOrgAdmin = "X-User-IsOrgAdmin"
	HeaderRequestID    = "X-Request-Id"
)

The gateway-injected identity headers, named once. Ctx.Org and friends read these, Ctx.Forward propagates exactly these, and nothing else in zip spells them.

The set is what a gateway can assert and a callee can act on, which is more than a subject: WHOSE tenant (org, project), WHICH person (id, name, email), and the two admin scopes — which are distinct authorities and must never collapse into one another. Owner names the org a principal belongs to, and a deployment that reserves one org for platform operators reads platform sudo off THAT and never off IsOrgAdmin, which says only "administers their own org". A plane that forwarded one and dropped the other would let a callee read "holds an org" as "administers it", or worse, as "administers the fleet".

View Source
const (
	SpecPath = "/.well-known/openapi.json"
	DocsPath = "/docs"
)

SpecPath and DocsPath are where an app serves its own OpenAPI document and the interactive page over it. Both are zip's control plane — a projection of the app, not a door its owner wrote — so neither appears in a Declaration and a host serves its own.

View Source
const (
	HealthPath  = "/healthz"
	ReadyPath   = "/readyz"
	MetricsPath = "/metrics"
)

The ops paths, per HIP-0119 §3. They are zip's control plane on the ops app, so they never appear in a Declaration.

View Source
const (
	// LossEmpty is a nested value whose own layout has no slots — a type whose
	// fields are all unexported. It crosses as a complete, EMPTY message: eight
	// bytes of {offset,length} pointing at nothing. time.Time is the common one.
	LossEmpty = "empty message"
	// LossPromoted is an EMBEDDED type whose field name is therefore lowercase,
	// so the layout skips it as unexported while encoding/json PROMOTES its
	// fields to the outer object. The same value crosses over JSON and does not
	// cross here, which is the shape of divergence a reader of both will not
	// think to check.
	LossPromoted = "promoted, not carried"
)

The two ways a value is dropped, both measured rather than imagined.

View Source
const (
	CauseMap       = "map"          // no map in the IDL: a key set is not a layout
	CauseAny       = "any"          // an interface field names no type at all
	CauseEmpty     = "no fields"    // nothing exported crosses, so there is no struct to declare
	CauseUnwirable = "no wire form" // chan, func, complex, a wide array, a slice of slices
	CauseReaches   = "reaches one"  // every field is fine; one of them holds a type that is not
)

The causes. One string per reason, so `sort | uniq -c` over them is a ranked list of what to fix and nothing has to be inferred from prose.

View Source
const AddrEnv = "ZIP_ADDR"

AddrEnv is the variable a host sets to tell a plugin where to listen. A plugin reads it through Addr.

View Source
const CallContentType = "application/zap"

CallContentType marks a body on the op-call plane. It names ZAP explicitly so a request that arrives with anything else is a caller that has not been updated, rather than a body silently read under the wrong layout.

View Source
const CallPath = "/.well-known/zip/op/"

CallPath is where the op-call plane is mounted. It sits under /.well-known/ with the OpenAPI document because it is control plane, not application surface: an app owns its own path space and must never find zip squatting in it.

View Source
const DefaultOpsAddr = "http://:9090"

DefaultOpsAddr is the address HIP-0119 §1 names, stated once so a deployment's manifest, a binary's flag default and a local run agree. It is the value, not a fallback: Config.OpsAddr left empty binds nothing.

A default that applied itself would mean every process calling Listen tries to bind this one port, and a plugin has more than one app in it (its edge app and its peer app) while a test process has many — so the SECOND listener in any of them dies on "address already in use", which is exactly the failure HIP-0106 §2 names. The ops address is deployment configuration (HIP-0119 §5) and a deployment states it; a host states none for a child, so a child correctly owns none.

It carries the http:// scheme because the two clients this surface exists for — a kubelet httpGet probe and a Prometheus scrape — speak HTTP and nothing else. A bare ":9090" is ZAP (DefaultScheme), and a probe against a ZAP socket is read as a frame: "GET " arrives as frame size 1195725856.

View Source
const DefaultScheme = "zap"

DefaultScheme is the transport a bare address (no "scheme://") uses. ZAP is the primary transport (TLS 1.3 + post-quantum, gRPC's replacement), so the path of least resistance is ZAP-native.

View Source
const GraphPath = "/.well-known/graph"

GraphPath is where the graph projection lives: GET returns the schema, POST runs a request against it. One path, because the schema and the executor are two views of one thing and a client that has the address has both.

View Source
const HeaderTrace = "traceparent"

The one identifier a request carries between services, and the header it travels in.

It is W3C trace context, verbatim, because the value's job is to be understood by something we did not write: a browser's fetch, an ingress, a client SDK. A private header would be a value only our own processes could read, which defeats the purpose of carrying it at all.

traceparent: 00-<32 hex trace>-<16 hex span>-<2 hex flags>

zip reads it when it arrives and mints it when it does not, so a trace is continuous across every hop that is a zip app and begins at the first one that is.

View Source
const Held = "held"

Held is the ONE word for an op a Decision stopped and nobody has answered yet. One spelling, so a caller — or a generated client reading the pair of responses the document publishes — tells a held op from a finished one without guessing.

View Source
const JSONVariant = jsonenc.Variant

JSONVariant names the JSON implementation on the edge and the semantics it writes, which zip.New logs once at startup. Per HIP-0106 the wire stack is "JSON only at edge, ZAP between services", so this is the one place the edge's encoder is stated. What it means is in jsonenc.Variant.

View Source
const ManifestVersion = 1

ManifestVersion is the shape of the document. It is a number in the document itself so a projector reading one written by an older front end says so rather than mis-reading a field that moved.

View Source
const MaxOrgLen = 128

MaxOrgLen bounds the org key. The org is the validated IAM owner claim — a short DNS-ish label — so anything longer is malformed or hostile and is refused before it can become a storage key or a namespace.

View Source
const MethodAll = "ALL"

methodAll is the sentinel [App.All] registers under: one entry answering every method, kept as one entry so the program says what was written. The router expands it; App.Declaration reads the expansion back off the router. MethodAll is the method App.Raw and Group.Raw take to answer a path whatever the verb — the rare case a proxy, a catch-all or a legacy mount needs. Every other method is its own name, as net/http spells it.

View Source
const PluginPath = "/.well-known/zip/plugin.json"

PluginPath is where a running plugin serves its own declaration, so a host that already started a child can compare what it MOUNTED against what the child SERVES. It is one of zip's control-plane routes, and a Declaration never includes one (see [App.control]).

View Source
const RuntimeDirEnv = sock.DirEnv

RuntimeDirEnv names the directory holding one socket per app. It is the one knob in the socket-path scheme; see SocketPath.

Variables

View Source
var ErrNoFilePassing = errors.New("zip: file passing needs a unix connection")

ErrNoFilePassing reports a connection that cannot carry descriptors. It is a distinct error rather than a silent zero because dropping the files while delivering the bytes is the worst outcome: the receiver gets a message that says a console was attached and no console.

Functions

func ActingAs added in v1.23.0

func ActingAs(ctx context.Context, org string) (context.Context, error)

ActingAs derives a caller acting on ANOTHER org's behalf, and is the one way to re-point the principal on a context that has a request behind it.

ctx, err := zip.ActingAs(ctx, targetOrg)

Why this is not the laundering hole

WithCaller is read only where there is no request, deliberately, so that stating an identity can never override an authenticated one. ActingAs does not weaken that: it cannot INVENT a caller, only re-point one that is already there. Called on a context with no authenticated principal it refuses, so the floor is unchanged — you can still only act with authority you arrived with.

What it adds is that the derived caller carries BOTH facts. Org becomes the org being acted upon; Caller.ActedBy records who is acting, and travels on the wire with the rest of the identity. An impersonation is therefore distinguishable from an identity at every hop and in every audit row, which is the property that makes it safe to allow at all.

What it does NOT do

It does not authorise. Whether this principal may act for that org is a policy question about roles and tenancy that belongs to the application, and zip has no view on it — an Authorizer is where that decision goes. ActingAs supplies the vocabulary and the audit trail; it does not grant anything.

func Addr added in v1.9.0

func Addr(fallback string) string

Addr returns the address this process was asked to serve on, or fallback when it was started directly rather than by a host. This is the whole plugin side of the contract.

func Address added in v1.24.8

func Address(pattern string, in any) string

A ROUTE'S ADDRESS IS A VALUE, AND ITS INPUT ALREADY CARRIES IT.

[registerTyped] binds the segments the router matched onto the op's In — /traces/:traceId puts the matched segment in the field that names itself traceId (see bindURL, urlFieldName). So by the time a handler runs, the In IS the address's parameters, decoded and named.

Address is the inverse of that binding, and it exists because the caller that has to reconstruct the concrete path — a relay that hands the call on to something that speaks HTTP — otherwise spells the address a SECOND time:

opGet(g, "/traces/:traceId", traceSpans)                   // once, at the route
relay(ctx, "GET", root+"/traces/"+in.TraceID, …)           // and again, by hand

Two spellings of one address drift, and the drift is invisible: the hand-built path is what gets requested, so a route pattern that no longer agrees with it is never consulted and never contradicted. Reading the parameters back out of the In through the SAME rule that put them there closes that by construction — there is one spelling, the one the route was registered with.

It is deliberately the inverse of bindURL and not a new rule: same field walk (wireFields, so promoted embedded fields count), same naming (urlFieldName, so `url:` beats `json:`), same case-insensitive match. A parameter the input does not name renders empty rather than panicking, which is what bindURL does with the mirror-image miss.

pattern is spelled the way the ROUTER spells it — ":name", optionally constrained (":project<guid>") or optional (":id?") — because the router's spelling is what a registration states.

func AddressOf added in v1.37.5

func AddressOf(ctx context.Context) (string, bool)

AddressOf is the address THIS call was made to, with the pattern's parameters filled in from the input — "/v1/traces/:id" answered as "/v1/traces/abc".

It is what a handler that FORWARDS needs: the pattern says which operation, the address says which resource, and a relay has to name both. It is computed from the same values Address takes, at the one seam that holds them both, so every projection gets the same answer — including a tools/call or an in-process invoke, where there is no request to read a path from.

It is deliberately NOT a field on Op. Op is what the Authorizer is handed and what App.OnResult is told, and both are narrow on purpose: the result hook is given the operation and the outcome and never the input. A concrete address carries path parameters, which are input, so putting it there would widen two contracts as a side effect of serving a third. A handler asks for it; a recorder is not handed it.

func Ask added in v1.18.8

func Ask[In, Out any](ctx context.Context, name, op string, in *In) (*Out, error)

Ask invokes one op on a named peer and returns its reply. It is the whole of what a caller needs to reach another service:

out, err := zip.Ask[plane.AuthorizeIn, plane.AuthorizeOut](
	ctx, "billing", plane.FinanceAuthorize, &in)

The peer is reached over its canonical socket (SocketPath) — no registry, no discovery, no address in the caller's configuration. The op token is the operation's one identity: the same string is its operationId, its MCP tool name and its CLI command.

Identity is not invented here. Whatever the edge asserted about the caller rides along when ctx carries a request (Ctx.Forward); a background job states one explicitly with WithCaller. There is no org argument, because an org in the argument is an org the caller chose.

The Conn is kept for the life of the process. Nothing needs closing, and a caller that closes one only drops its idle connections — the next Ask redials.

func Call added in v1.17.7

func Call[In, Out any](ctx context.Context, c *Conn, op string, in *In) (*Out, error)

Call invokes op on the app behind c and decodes its reply into Out. It is the typed round trip: In is marshalled to the op's JSON body, the callee decodes it into the very same type it declared, and its Out comes back decoded here.

A void op (one whose handler returns a nil *Out) yields a nil *Out and a nil error, matching what the handler returned.

A handler error arrives as the HTTPError the handler returned — status, code and message intact — so errors.As on the caller's side sees what the callee raised rather than a stringified copy. A transport failure is a 502.

Identity is not invented here. Whatever the gateway asserted about the caller rides along when ctx carries a request (see Ctx.Forward); WHICH process is calling is answered by the kernel at the other end, from the socket's peer credential (see Peer), not by anything this client could set.

ctx is honored to the extent the transport allows: an already-cancelled ctx fails before the wire, and the transport's own read timeout bounds the call (zap: 30s). It is not cancellable mid-flight, because abandoning a call would mean abandoning the pooled buffers it is writing into.

func Content added in v1.36.9

func Content(c *Ctx, mod time.Time, size int64, src io.Reader) error

Content writes an already-open representation to the response: Last-Modified with a conditional 304, Accept-Ranges, and either one byte range (206 with Content-Range, or 416 for a span the representation does not have) or the whole thing. It is what Static serves files with, exported for a handler that needs the rest of the answer to be its own.

The CALLER owns Content-Type and any Content-Encoding. Only the caller knows whether these bytes are the file it named or a precompressed sibling of it — a .br beside main.css is still text/css on the wire, and guessing from the stored name would label it as brotli.

Ranges need seeking, so a src that is not an io.Seeker is served whole, which RFC 9110 §14.2 permits for any Range. Content takes ownership: src is closed if it is an io.Closer, on every path including 304 and 416.

func CppZap added in v1.36.45

func CppZap(namespace string) []byte

CppZap renders the ZAP wire runtime as one self-contained C++23 header in the given namespace ("lux::zap"). The namespace is the only knob: two chains that want the codec under two names get the SAME arithmetic, not two ports.

func Delegate added in v1.18.8

func Delegate(c *Ctx, org string) context.Context

Delegate is the context a background continuation runs under: the in-flight request's caller, with the org replaced.

It is for the one case a request handler legitimately acts for another tenant — a platform operator's fan-out, a cross-org reconciliation — and it keeps the ACTING user, so the audit trail still names a person. Passing an org through an argument instead is the thing §6 forbids: an org in the argument is an org the caller chose.

The returned context carries no request, so it outlives the handler: both transports reuse the underlying request for the next call on the connection, and a goroutine holding one reads another caller's headers.

func Describe added in v1.17.0

func Describe(key string, d Doc)

Describe records documentation for one operation, under the key cmd/zipdoc built with DocKey. Generated code calls it from an init(); hand-written calls are possible but defeat the point, since the comment is then no longer the single source.

func DocKey added in v1.36.37

func DocKey(pkg, method, path string) string

DocKey is the key a package's operation is filed under. One function, so the generator that writes a key and the registry that reads one cannot spell it differently.

func Header(ctx context.Context, name string) string

Header reads one header of the request behind a handler's context, and "" when there is no request behind it — a frame that arrived over ZAP, a command, a background job.

It exists for the caller that has to REACH another service over HTTP on this caller's behalf and therefore needs the credential the caller presented, which CallerOf deliberately does not carry: Caller is the identity a request was resolved TO, and handing that back would let a service mint its own answer to the question it was supposed to ask.

func Here added in v1.26.1

func Here[In, Out any](ctx context.Context, a *App, op string, in *In) (*Out, error)

Here invokes op on a — in this process, with no wire under it.

It is Call with the Conn removed. The op is found by the same [App.opByName] the socket plane uses and run through the same invoke seam, so it validates and authorizes identically; what it does NOT do is encode the input, because there is nothing between the two ends to encode it for.

The reply is the handler's OWN value. Over a wire a caller gets a fresh copy because a wire cannot hand back anything else; here it does not, and a reply is a reply — read it, do not write it.

In and Out must be the pair the op declared. A socket call learns that from a decode failure at the far end; here it is a type assertion, and the error says so plainly.

A HELD OP IS NOT AN OUT. When the rule approves rather than allows, the handler does not run and there is no reply to hand back, so the Approval comes back on the error lane and HeldOf reads it — the same answer the 202 carries over REST and the call plane.

func ID added in v1.26.0

func ID(method, path string) string

ID is an operation's name, derived from its method and its ABSOLUTE path.

It is THE rule for that derivation and the only one. The OpenAPI operationId, the MCP tool name, the key the by-name call plane resolves and the command the CLI spells all read this function, so one operation carries one token on every surface and an agent reading the document can call what it just read.

'_' is the SEPARATOR — it encodes '/' — so any character that also folded to '_' would collide with a path boundary. That is not hypothetical: a router serving both /v1/pricing-policy and /v1/pricing/policy, under an earlier "everything non-alphanumeric becomes _" rule, collapsed the two onto one id. Hyphenated addresses are permanent (…/git-upload-pack, …/documents/delete-batch), so '-' and '.' — both legal in an operationId — are PRESERVED rather than folded, which keeps such a pair distinct: get_pricing-policy is not get_pricing_policy.

A parameter contributes "by_<name>", so /v1/a/{b} and /v1/a/b do not collapse either. Every spelling of one parameter reaches the same name: the router's (":id", and the constraint and optional marker that are matching business and not naming — see Template), the document's ("{id}"), and a wildcard, which declares no name and takes the positional one the document gives it.

This is derivation, not proof: a literal '_' in a segment can still alias a '/' (/v1/a/b_c against /v1/a/b/c). The walk VERIFIES uniqueness across the whole program and refuses to publish a duplicate — that check, not this encoding, is what makes the ids trustworthy.

THE LEADING v1 IS DROPPED, because it distinguishes nothing. A segment earns its place in an id by telling two operations apart, and this one is on every address in the program: it is a constant, and a constant repeated 2,000 times is noise carried by four surfaces at once — the document, the tool name an agent reads on every turn, the by-name key and the CLI's spelling.

Only v1 goes. A later version is the EXCEPTION and keeps its segment, which is both how it stays honest and how it stays unique: get_agents is /v1/agents and get_v2_agents is /v2/agents, so the two cannot collapse onto one name the walk would then refuse. The default is implicit; a departure from it is not.

func Local added in v1.36.43

func Local(ctx context.Context) bool

Local reports a call with no transport behind it: this process asking itself, through Here. A server that scopes by the peer's credential asks this first — there is no peer to read, and there is no one to refuse.

func LocalInvoke added in v1.17.2

func LocalInvoke(ctx context.Context, c Command, path map[string]string, body []byte) (any, error)

LocalInvoke runs the handler in this process, through the op's own invoke seam. It is the whole reason a fused binary needs no client: the command IS the handler call.

func NetworkOf added in v1.36.21

func NetworkOf(addr string) string

NetworkOf reads the plumbing off the address shape, the way the address already tells you: a filesystem path is a unix socket, anything else is host:port. The scheme names the PROTOCOL (zap) and the address names where it is spoken, so there is no second scheme for the same wire.

zap://:9653                      -> tcp
zap://billing.hanzo.svc:9653     -> tcp
zap:///run/hanzo/billing.sock    -> unix
/run/hanzo/billing.sock          -> unix (bare address, DefaultScheme)
@hanzo-billing                   -> unix (Linux abstract socket)

It is exported because it is a RULE and not an implementation detail: anything that dials one of these addresses has to make the same reading, and a second copy of it is a place where a socket path becomes a hostname. zapmcp.Dial wants the network as a value, so a client reaching an app's MCP door asks here rather than re-deriving it.

func ProjectMCP added in v1.36.49

func ProjectMCP(m Manifest) []map[string]any

ProjectMCP is m as an MCP tool list, in name order.

func ProjectOpenAPI added in v1.36.49

func ProjectOpenAPI(m Manifest) map[string]any

ProjectOpenAPI is m as an OpenAPI 3.1 document.

func Prose added in v1.18.20

func Prose(method, path string) (summary, description string, ok bool)

Prose is what an operation says about itself: the sentence for a one-line summary, and the whole doc comment for the long form. Absent when cmd/zipdoc found no comment for that address.

It is exported because the typed-op registry is NOT the only reader of this. A host that composes several apps into one document projects its LIVE ROUTER — every route, typed or not — and the untyped half has prose here and nowhere else. Without this the host would have to keep its own table of strings for routes it does not own, which is the duplication the whole package exists to remove: the sentence belongs to the service that serves the route, and this is how it travels.

The summary is derived here, not by the caller, so every projection of one operation shortens it identically.

func Query added in v1.36.36

func Query(in any) (string, error)

Query is the query string that carries in to a bodyless op.

It is the client half of the URL binder and its exact inverse: what this writes, [bindURL] reads. That matters because they are two halves of one rule and they had drifted — a list went out as `?ids=["a","b"]` and a record as `?startIndex={"utxo":...}`, neither of which the binder reads, so the argument arrived empty and the op answered 200 about nothing.

The value is taken from the op's own In, so the spelling is derived rather than agreed: a caller does not have to know that a list is comma-joined or that a record's leaves are named through it.

func RecvFiles added in v1.36.46

func RecvFiles(c net.Conn, b []byte) (int, []*os.File, error)

RecvFiles reads one message into b and returns the descriptors that came with it, already wrapped as *os.File so they are closed by the usual means.

Every received descriptor is closed if the call fails afterwards. A leaked descriptor from a rejected message is invisible until the process runs out, and then it is attributed to whatever asked for the next one.

func RegisterTransport added in v1.1.0

func RegisterTransport(scheme string, t Transport)

RegisterTransport adds (or replaces) a transport keyed by address scheme, so any future protocol slots into both Listen and Proxy with ZERO change to either API. Call before Listen or Proxy.

zip.RegisterTransport("quic", zip.Transport{
	Serve: func(addr string, h fasthttp.RequestHandler) zip.Server {
		return myquic.NewServer(addr, h)
	},
	Dial: func(addr string) zip.Client { return myquic.NewClient(addr) },
})

func RuntimeDir added in v1.17.7

func RuntimeDir() string

RuntimeDir is the directory that holds the fleet's sockets, resolved in one order:

$ZIP_RUNTIME_DIR        set explicitly — always wins
$XDG_RUNTIME_DIR/zip    a developer's per-user runtime dir (/run/user/1000)
/run/zip                the system default

The middle case is what makes a dev box work without configuration: /run/zip is not writable by a normal user and $XDG_RUNTIME_DIR is.

func SendFiles added in v1.36.46

func SendFiles(c net.Conn, b []byte, files []*os.File) error

SendFiles writes b and hands over files on the same message.

The byte is not optional. A message carrying only control data may be discarded before the receiver ever sees it, so at least one real byte must travel with the descriptors or the transfer is silently lost — which is why this refuses an empty b rather than sending something that usually works.

func SocketPath added in v1.17.7

func SocketPath(name string) string

SocketPath is the ONE canonical mapping from a service NAME to the unix socket it is reachable at:

<RuntimeDir()>/<name>.sock

Both halves use it, which is what keeps them from drifting: a service serves at zip.Addr(zip.SocketPath("flags")) and a caller reaches it with zip.DialApp("flags"). There is no registry, no discovery service and no second spelling — the name IS the address, and one environment variable moves the whole fleet.

func Template added in v1.24.8

func Template(pattern string) string

Template is pattern in the spelling a DOCUMENT writes — ":traceId" becomes "{traceId}" — which is the address's public name: the OpenAPI path, the string a reference page prints and the key a caller matches an address on.

A constraint and an optional marker are the ROUTER's business (":project<guid>", ":id?") and do not survive: they say how a segment is matched, not what it is called, and a document that published them would name a parameter no client ever writes.

A WILDCARD IS A SEGMENT TOO, and it becomes "{wildcardN}", numbered in path order. The router's "*" and "+" match greedily where a document's parameter matches one segment, so the name is the closest thing a document can say — but it is not a new judgement here: ID already spells a wildcard "wildcardN" and numbers it the same way, so the two agree by construction and an operation id derived from the router's spelling equals one derived from the document's.

Returning "*" verbatim, as this did, published a path no document template can mean, and left a caller that reads a route table and a caller that reads the registry disagreeing about the SAME address — which resolves as "no such operation" rather than as a difference of spelling.

func Tenant added in v1.18.8

func Tenant(ctx context.Context) (string, bool)

Tenant is the tenant a call may act for, or ("", false).

It returns false — not an empty string a caller might use anyway — on each of three conditions, all of which are the same defect at different depths:

no validated User claim   the org that rode along is the caller's own assertion
an empty Org              there is no tenant to act for
an Org over MaxOrgLen     malformed or hostile, and it is about to be a key

The org is returned VERBATIM apart from surrounding space: never lower-cased, never truncated. Folding collapses distinct owners ("acme", "ACME", a 32-char prefix) into one bucket, which is itself a cross-tenant break.

A handler that touches per-tenant state calls this and refuses !ok. A handler that re-derives the rule is the sixth copy this function exists to delete.

func WithCaller added in v1.18.4

func WithCaller(ctx context.Context, c Caller) context.Context

WithCaller states who a Call made with this context acts for, for a caller that has no inbound request to propagate.

The background work a service does is not all unattributed. A grant issued when an org opens, a meter that debits after the response has already gone out, a reactor draining a queue — each acts FOR a tenant, and the callee has to know which one to write to the right books. Without this the only place left to put the org is the argument, and an org in the argument is an org the caller chose: any caller could then name any tenant and be believed.

ctx := zip.WithCaller(context.Background(), zip.Caller{Org: org})
_, err := zip.Call[GrantIn, GrantOut](ctx, conn, "finance_grant", &in)

An inbound request always wins. [forwardIdentity] prefers the gateway's assertion and reads this only when there is none, so this can supply an identity where none exists but can never override or launder one — which is what keeps Ctx.Forward's guarantee intact. It is a statement by one of our own processes, trusted exactly as far as the socket's peer credential makes it trustworthy (see Peer), and never as far as a gateway's assertion.

Types

type App

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

App is the zip application. It wraps *fiber.App and exposes the zip handler signature alongside generic typed handlers.

func Load added in v1.9.0

func Load(p Plugin, prefixes ...string) (*App, error)

When p.Addr is set the remote is proxied directly. Otherwise the binary is started as a child process listening on its own unix socket in a private 0700 directory — no port to allocate, and filesystem permissions are the ACL. The child is stopped and its directory removed on Shutdown, in LIFO order with every other hook, so a host that exits cleanly leaves nothing behind.

prefixes is variadic because a service often owns more than one route subtree — o11y answers both /v1/o11y and /v1/sentry — and a single-prefix Load silently 404s the others. One call declares everything the plugin owns.

Load returns the plugin as a DEFINITION — an *App the host composes with Use, like any other. It is not a verb and not a Service: a plugin is a unit of functionality that answers addresses, which is exactly what an App is, so it arrives through the one composition verb rather than through a second one.

billing, err := zip.Load(zip.Plugin{Name: "billing", Bin: bin}, "/v1/billing")
if err != nil { return err }
app.Use(billing)

Prefix collisions between two plugins are caught where every other address collision is caught — by the walk, at build, naming both claimants and every collision at once. There is no separate prefix-claim ledger, because a second mechanism for one question is a second answer waiting to disagree.

func New

func New(cfg Config) *App

New constructs an App with the given config. Defaults are applied for any zero-valued field.

func Proxy added in v1.24.7

func Proxy(prefix, addr string, decl ...Declaration) (*App, error)

A remote service is a LEAF, not a verb.

Proxy used to be one of five composition verbs and the only one that pointed at another process. Under one program it stops being special: it appends an App like any other inclusion, whose routes proxy and whose ops forward. Every projection then reads it through the same walk — a mounted service appears in the OpenAPI document, the MCP tool list, the CLI commands and the by-name call plane because it is in the registry, not because four projections each learned about mounting.

The declaration is an INPUT, never a fetch

The remote is described by a Declaration the caller supplies. Nothing here dials at build time, and that is deliberate to the point of being the main design constraint:

  • a walk that did I/O would make App.Registry fallible, slow and untestable, and every projection downstream of it too;
  • the OpenAPI document would depend on some other process being reachable at boot, so a cold start in the wrong order publishes a smaller API;
  • the seal could not mean anything, because what the program IS would depend on what the network said at the moment it was asked.

The spec is a build input; the server is not asked what it serves. Without a declaration a proxy contributes its two proxy addresses and no ops, which is exactly what it contributed before and says honestly that nothing is known about the shape behind it. Proxy returns another service, running elsewhere, as a DEFINITION the host composes with Use like any other:

ledger, err := zip.Proxy("/v1/ledger", "ledger.hanzo.svc:9653", decl)
if err != nil { return err }
app.Use(ledger)

It is no longer a VERB on App. Delegating to another process is not a second kind of composition — it is a definition whose handlers happen to be a network away — so it arrives through the one verb and appears in every projection because it is in the registry, not because five projections each learned what mounting meant.

Why the name changed

This was Mount, and Mount already meant something else to everyone reading it: 118 of the fleet's ~140 subsystems export `func Mount(app cloud.Router, deps cloud.Deps) error`, the function by which an app registers ITSELF. One name, two unrelated shapes — a caller who knows one learns nothing about the other, and a reader seeing `Mount` in a diff cannot tell which is meant without checking the arity.

The verb was also wrong on its own terms, which is what makes the rename a correction and not a coat of paint. The doc above already says composition went to Use; a function that returns a value is a NOUN.

Proxy is what it builds. Every route it registers runs the handler this file calls `proxy`, and that handler's whole body is `forward(...)` to the address given. The term is the term of art for a local stand-in whose handlers hand the request to someone else, so it needs no gloss.

Not Remote, which was the first choice and is wrong: Remote already exists here, and correctly — it is the CALLER's side, a client that executes commands against a service running elsewhere. This is the SERVER's side: a definition that stands in for that service inside this program's own routing table. Two sides of one wire are two things, and the compiler said so.

The address scheme selects the transport exactly as App.Listen's does, so a bare address is ZAP and one registry serves both directions.

decl is optional and is a BUILD INPUT, never a fetch. Without it the remote contributes its two proxy addresses and no ops, which honestly says nothing is known about the shape behind it. With it, the remote's ops join the document, the tool list, the commands and the call plane.

func ReadZAP added in v1.36.45

func ReadZAP(name string, src []byte) ([]*App, error)

ReadZAP is the apps a .zap schema declares: one per interface, each carrying one op per method and the structs that interface reaches.

It is the exact inverse of ZAPSchema, which takes any number of apps and writes them into one file as one interface each. So a file read here and projected back names the same interfaces, with the same methods, over the same structs — the ops are addressed and the methods are not, and that is the whole of what this adds.

name is the schema's own name, used in errors and nowhere else. Each app is named for its interface.

The apps have no handlers. A schema is a contract, and the implementation of that contract is not in this process — so the ops describe themselves fully to every projection, and refuse to run.

func Serving added in v1.26.1

func Serving(name string) *App

Serving returns the App THIS PROCESS serves at name's canonical socket, or nil when the app is somewhere else.

It answers a question a caller has to be able to ask before it spends a wake or a dial on a peer that is already in memory. It is a fact about the process, not a configuration: an app is here because something in this program bound its socket.

Nil is the ordinary answer — a fleet of one-app processes is every app but one — and it means "not here", never "not deployed". Absence in the DEPLOYMENT is the router's word.

func (*App) Authorize added in v1.8.3

func (a *App) Authorize(fn Authorizer)

Authorize installs fn as the op-invoke authorization rule. It is the op-level counterpart to Use: Use wraps the whole request with transport middleware, which for a body request sees only the raw bytes; Authorize runs one decision on the DECODED typed input of every op, REST and MCP alike — the seam a mounted subsystem asks so the value it authorizes is the value the handler binds. Call once while mounting, before Listen — the field is read on serve goroutines and writing it under load is a race. A nil fn declares no rule.

IT COVERS EVERY OP THIS APP SERVES: the ones it registered itself, the ones declared on its groups, and the ones a definition it composed with App.Use brought with it. All three answer at this app's addresses, at its /mcp, at its call plane, in its graph and to its CLI, so all three are its ops. Composition is what decides that, so [adopt] settles it at build rather than each registration guessing.

A definition may declare a TIGHTER rule of its own and it wins, because [App.rule] asks its own first. It cannot declare a looser one: declaring none falls through to whatever composed it, so composing a definition can only add a rule, never drop one.

A definition composed under two rules that DISAGREE is refused at App.Build: its ops are one closure and one closure answers to one rule, so there is no honest way to serve it under both. See [adopt].

func (*App) Build added in v1.19.0

func (a *App) Build() error

Build constructs and installs a generation without starting a listener: the walk runs, every validation runs, the projections are rendered, and the router goes live — exactly what App.Listen does minus the sockets.

It RETURNS THE VERDICT, which is the whole reason it replaced Prepare. A composition can be invalid — two definitions claiming one address, a cycle — and the old Prepare returned nothing, so the only way to learn was to start a server. Now a codegen step, a test or a wiring file's own main can ask whether the program it just wrote is a program, and get every conflict at once with both claimants named.

Idempotent for the projections (they render once) and monotonic for the freeze. Listen calls it; there is no other way to build.

func (*App) CLI added in v1.17.2

func (a *App) CLI() *CLI

CLI returns the command line for everything registered on this app, executed in-process. `app.CLI().Run(ctx, os.Args[1:])` is a complete CLI for a service.

func (*App) Commands added in v1.17.2

func (a *App) Commands() []Command

Commands projects every registered typed op into a command. This is the whole derivation: no registration, no list of commands, no per-endpoint code.

func (*App) CppCLI added in v1.36.45

func (a *App) CppCLI(namespace string) (*CppCLI, error)

CppCLI renders a native C++20 command-line interface with subcommands, typed flag parsing, and help text.

func (*App) CppMCP added in v1.36.45

func (a *App) CppMCP(namespace string) (*CppMCP, error)

CppMCP renders a native C++20 Model Context Protocol (MCP) tool surface and server dispatch. Every tool is self-documenting with Doxygen comments and JSON input schema.

func (*App) CppSDK added in v1.36.45

func (a *App) CppSDK(namespace string) (*CppSDK, error)

CppSDK renders a native modern C++20 client SDK whose methods are this app's operations. Every method and struct is fully self-documenting using Doxygen doc comments.

func (*App) Declaration added in v1.18.8

func (a *App) Declaration() Declaration

Declaration projects the live router: every method+pattern the app will answer, sorted by (pattern, method) and deduplicated, plus every registered op name. It is complete by construction — a route the plugin serves and does not publish is still declared.

zip's own control plane is excluded — see [App.control]: those are per-process routes a host serves for itself, and a child claiming them would take the host's own document, agent door and op plane.

func (*App) Declares added in v1.33.2

func (a *App) Declares(method, path string) bool

Declares reports whether the app publishes method+path — whether it appears in App.Declaration, and so in the projections built from it.

A host builds its document from the ROUTER, which is a strict superset: it carries zip's own control routes and anything registered through [Undeclared]. This is how it tells those apart from the app's contract without keeping a second list of what to skip.

func (*App) Delete

func (a *App) Delete[In, Out any](path string, fn TypedHandler[In, Out], opts ...OpOption) *Operation[In, Out]

Delete declares a DELETE operation at the root.

func (*App) Described added in v1.18.8

func (a *App) Described() (bool, error)

Described writes the projection this process was asked for on the command line and reports whether it did:

<binary> openapi <file>     the app's OpenAPI subset
<binary> declare <file>     the app's routing declaration

A main calls it once, after registering every op and BEFORE opening a store or dialing a peer — a projection is a function of the code, so a describe run must not need a database:

if done, err := app.Described(); done {
	return err
}
return app.Listen(zip.Addr(":9653"))

It writes a FILE and never stdout. A plugin's own dependencies write to stdout at construction — zip.New logs a line, GORM logs queries, sqlite-vec prints a warning — and `> file` splices those into the front of the document.

One call rather than a Described()/Describe() pair, because argv is read in exactly one place and a main cannot forget to forward it.

func (*App) DocsMarkdown added in v1.36.45

func (a *App) DocsMarkdown(title string) (*DocsBundle, error)

DocsMarkdown generates @hanzo/docs compatible MDX documentation pages for all operations. It shares the core prose extracted from doc comments across Go, Rust, and C++ SDKs.

func (*App) Evict added in v1.30.0

func (a *App) Evict() int

Evict reclaims plugin processes under two bounds and reports how many it stopped: first every lazy plugin that has not served for its IdleAfter, then, while more than warm are still running, the least recently used. Zero warm means only the first bound applies.

One pass, no goroutine — the caller decides the cadence, so a host under test can drive it directly and a host in production can put it on the ticker it already has.

THE COUNT IS NOT THIS FUNCTION'S TO ENFORCE ALONE, and an earlier version of this comment claimed otherwise: warm was an ARGUMENT here, on the reasoning that the number is a fact about the host and not the library. The reasoning held; the placement did not. A bound consulted once a minute is outrun by anything that starts faster, and a fan-out door that asks every subsystem at once does — a hundred children inside ninety seconds, so the process held them all until the next sweep, stopped answering its own liveness probe, and was killed with the ceiling never applied. Warm is state now and makeRoom applies it at the start path; this still runs it, because age and count are one sweep.

Draining uses the plugin's own Drain, so a request in flight when the sweep lands finishes on the old process exactly as it would across a Reload.

func (*App) Fiber

func (a *App) Fiber() *fiber.App

Fiber returns the underlying *fiber.App, materialising the program if it has changed since the last build. Use for one-off escape into Fiber-only APIs (rare). Prefer staying on the zip surface.

It does NOT seal: inspecting the router is not executing it, and a test or a codegen step that looks must not turn the next legitimate Use into a panic.

func (*App) Frozen added in v1.19.0

func (a *App) Frozen() bool

Frozen reports whether this definition has appeared in a built generation and can therefore no longer be edited in place.

func (*App) Gather added in v1.24.4

func (a *App) Gather() ([]*metric.MetricFamily, error)

Gather makes the App a metric.Gatherer, which is the whole of what the exporter needs from it. Stating it as the standard interface rather than a method of our own is what lets the numbers go out through the exporter luxfi/metric already ships, instead of a second one written here.

func (*App) Generation added in v1.19.0

func (a *App) Generation() (uint64, bool)

Generation reports which generation is live, and whether one is.

func (*App) Get

func (a *App) Get[In, Out any](path string, fn TypedHandler[In, Out], opts ...OpOption) *Operation[In, Out]

Get declares a GET operation at the root.

func (*App) GraphQL added in v1.33.3

func (a *App) GraphQL(ctx context.Context, req GraphRequest) GraphResponse

GraphQL runs one request against this app's ops.

It takes only a context because everything a call needs is already in one: the caller's identity for the authorizer, and the request behind it for any header an op declares. That is what lets the same executor serve an HTTP POST, a test, or an in-process caller without any of them holding a transport.

func (*App) GraphQLSDL added in v1.33.3

func (a *App) GraphQLSDL() string

GraphQLSDL renders the schema this app's ops describe, in GraphQL's schema definition language.

SDL and not a Go type graph, because SDL is what every GraphQL client, code generator and editor already reads. The document is the artifact; publishing it is what makes the projection usable by anything that is not this process.

func (*App) Group

func (a *App) Group(prefix string, mw ...Handler) *Group

Group opens a group at prefix. Middleware passed here applies to every route under it.

func (*App) Lint added in v1.19.0

func (a *App) Lint() []string

Lint reports staged composition: middleware appended to a receiver AFTER an App was included in it.

It is not an error and not a warning tier. The pattern is INTENTIONAL when the two lines sit together —

app.Use(publicAPI)
app.Use(auth)
app.Use(privateAPI)

— and a latent bug when they are written far apart, in different functions or different files, where the author of the later line cannot see which subtrees they just failed to cover. No walk can tell those two apart, because the difference is authorial intent and the only evidence of it is co-location. So this reports the fact and the two call sites and lets a human read them; promoting it to an error would break the legitimate case, and hiding it would leave the dangerous one silent.

Reported per receiver, naming the included app, the middleware, and both sites.

func (*App) Listen added in v1.1.0

func (a *App) Listen(addrs ...string) error

Listen is Serve followed by Host.Wait: it starts serving and blocks until the listeners stop. The address scheme selects the transport; a bare address uses ZAP (DefaultScheme).

It is the TERMINAL spelling, and the difference from Serve is deliberate rather than an oversight: Listen hands back no host, so a program started this way cannot later be changed with Host.Include or Host.Drop. That is the right shape for a main whose program is fixed —

log.Fatal(app.Listen(":8080"))

— and the wrong one the moment anything loads at run time, which is why Serve exists and why this is one line of sugar over it rather than a second way to serve. There is one serving mechanism; this is its no-handle form.

func (*App) Logger

func (a *App) Logger() luxlog.Logger

Logger returns the App's logger.

func (*App) MCP added in v1.27.0

func (a *App) MCP(ctx context.Context, f *zapmcp.Frame) *zapmcp.Frame

MCP answers one MCP message. It is this app's tool door as a VALUE — a frame in, a frame out, nothing underneath it — which is the whole of why MCP can be served over ZAP natively:

srv := &zapmcp.Server{Network: "unix", Addr: sock, Handler: app.MCP}

HTTP is then an ADAPTER over this, not a peer of it: installMCP's route reads a JSON-RPC body into a frame, calls this, and writes the answer back. Before, the door's own signature took a fiber.Ctx, so an agent could not reach a tool without an HTTP request existing first — which put the transport underneath the protocol instead of the other way round.

The caller is read from ctx (CallerOf), never from the frame: over HTTP the adapter binds the gateway's assertion onto the context, and over ZAP a caller states one with WithCaller. The frame's own Subject says who SIGNED it, which is a different question and not one this door answers.

A nil answer means there is nothing to say — a notification. zapmcp.Server writes nothing; the HTTP adapter answers 202.

func (*App) MCPTools added in v1.17.7

func (a *App) MCPTools() []map[string]any

MCPTools is the app's full MCP tool surface, projected from the typed-op registry — the tool-list counterpart of App.OpenAPISpec, and the whole of what a plugin has to do to be an MCP server: nothing. Every typed op is one tool, named by its operation id, described by its doc comment, with the same JSON Schema the OpenAPI document carries.

Serving it is already handled: the /mcp route rides every transport the app Listens on. Read it directly when a host wants a plugin's tools in-process — composing several plugins' surfaces, filtering them, or asserting them in a test — without a round trip.

func (*App) Manifest added in v1.36.49

func (a *App) Manifest() Manifest

Manifest describes this app the way any front end would, so the projections read the same document whichever language declared the ops.

func (*App) Metrics added in v1.24.6

func (a *App) Metrics() metric.Registerer

Metrics is where an app registers instruments of its own — a domain gauge, a queue depth, a fleet probe — into the same registry the framework's request instruments live in. Everything registered here rides the same /metrics page and the same export as the built-in numbers: one registry per program, one road out, nothing extra to configure. The standard Registerer is the whole surface, so a caller holds no framework type and the registry stays owned here.

func (*App) Module

func (a *App) Module(methodPath, runtimeName, modulePath string) error

Module mounts a single HIP-0105 extension at the given method+path — the one way to put an extension on the app. The `methodPath` form is "METHOD /path" (e.g. "POST /v1/validate"), matching the idiom. `runtime` selects the backing engine ("wasm" | "goja" | "pyvm" | "starlark" | "v8go" | "native"); `modulePath` is the directory containing the extension.json manifest.

app.Module("POST /v1/policy/eval", "wasm", "./extensions/policy")
app.Module("POST /v1/transform",   "pyvm", "./extensions/transform")
app.Module("POST /v1/webhook",     "goja", "./extensions/webhook")

The extension's exported function name is inferred from the path's last non-{param} segment, lowercased (e.g. "POST /v1/policy/eval" → "eval").

A module route registers a ROUTE and no op, and cannot register one

So it is invisible to every projection: it is in no OpenAPI document, is no MCP tool, and no service can reach it with zip.Call. That is a real hole — the envelope in and out of a module IS a JSON contract — and it is stated here rather than left to be discovered, because the two reasons it cannot close today are both structural:

  1. THERE IS NO TYPE TO DERIVE A SCHEMA FROM. A typed op's schema comes from its In/Out Go types. A module is LOADED at run time out of a directory — that is the whole point of an extension — so at the moment this route is registered zip holds a Module (Name/Runtime/Exports/Invoke) and no declaration of what the module accepts or returns. Registering it as an open object would put the path in the document while saying nothing about the contract, which is not the same thing as documenting it.
  2. A TYPED OP CANNOT EXPRESS THE MODULE'S RESPONSE. A module answers with a moduleResponse — status, headers, body — and modules use it (a redirect, a 404, a non-JSON body). A typed op returns Out and zip decides the status, so routing this through registerTyped would silently drop a capability the envelope exists to provide.

What closes it is (1): an extension that DECLARES its contract, via a manifest schema surfaced on Module. The op would then carry the module's own schema and every projection follows, with the route keeping the envelope it has now. Until an extension declares one there is nothing to project, and inventing a shape for it here would be a second source of truth for a contract zip does not own.

A route whose contract IS known belongs in a typed op — declare it with [Get]/[Post] and let the module be an implementation detail behind it.

func (*App) MountGraph added in v1.33.3

func (a *App) MountGraph(path string)

MountGraph serves the graph projection at path: GET renders the schema, POST runs a request against it.

A host publishes its own address space, so where the projection ANSWERS is the host's decision while what it answers is not — the same split that puts zip's OpenAPI document at /.well-known and a product's at /v1. One implementation, mounted wherever a caller is told to look.

The schema is rendered per request rather than captured here, so a host may mount this before it finishes registering: the document always describes the registry as it stands when someone asks.

func (*App) MountGraphFor added in v1.35.0

func (a *App) MountGraphFor(path string, schema func() string, resolve Resolver)

MountGraphFor serves a graph whose schema and resolver come from elsewhere.

The parsing, the variables, the fragments, the directives and the projection are the same ones a local graph gets — only the answer to "what is this field" changes. That is the whole reason this exists: a deployment that answers with many processes has one surface and should not grow a second GraphQL implementation to describe it.

schema is called per request, so a composed schema stays current.

func (*App) Name added in v1.36.45

func (a *App) Name() string

Name is the app's Config.AppName — the one name it answers to.

Exported because a caller holding several apps has to tell them apart, and the name is the only thing that does. ReadZAP answers with one app per interface a schema declares, and picking one of them out is exactly that question.

func (*App) OAuth added in v1.37.1

func (a *App) OAuth() *Group

OAuth is Group.OAuth at the root.

func (*App) OnResult added in v1.36.46

func (a *App) OnResult(fn func(ctx context.Context, op Op, err error))

OnResult installs fn as the rule for what to do with the OUTCOME of every typed op this app serves. It is App.Authorize's counterpart: Authorize is ASKED before the handler and may refuse, OnResult is TOLD after it and may not. Both run at the one invoke seam, so both cover every projection — the REST route, tools/call at /mcp, the by-name call plane, the graph, and the in-process CLI, which is the only one no transport middleware can wrap.

That last one is why this exists. Anything a middleware records — a trail, a metric, a span — is recorded about a REQUEST, and an in-process invoke is not one. A recorder installed here is told about it under the same operation name as every other door.

fn is told the operation and the error, and nothing else. The output is deliberately absent: a hook that could read it would be a way for a response body to reach whatever the hook writes to. Call once while mounting, before Listen — the field is read on serve goroutines. A nil fn declares no hook.

func (*App) OnShutdown added in v1.4.0

func (a *App) OnShutdown(fn func(context.Context) error)

OnShutdown registers fn as a teardown hook, run during Shutdown / ShutdownWithContext. This is the one teardown primitive zip exposes: subsystems register their own cleanup at mount time, and reverse-mount teardown falls out for free (see the ordering note below).

Ordering. Hooks run LAST in the shutdown sequence — after listeners stop accepting and after in-flight requests drain — and in LIFO order (reverse registration = reverse mount order). Draining first means a subsystem's teardown never races the requests still using it; LIFO means a dependency mounted before its dependents is torn down after them.

Errors. Every hook runs even if an earlier one fails; all hook errors (and the drain error) are aggregated with errors.Join and returned from Shutdown.

Concurrency. Registration is safe from multiple goroutines. A nil fn is ignored. Registering after Shutdown has begun is a no-op: the hook is dropped (never run) and a warning is logged — there is no longer a shutdown to hook into, and running it immediately would give OnShutdown two meanings depending on timing. Register teardown at mount time, before Shutdown.

func (*App) OpenAPISpec added in v1.10.3

func (a *App) OpenAPISpec() map[string]any

OpenAPISpec returns the OpenAPI 3.1 document for every typed op registered on this app — the SAME value served at /.well-known/openapi.json.

It is exported so a service can render its published contract from the routes it actually registers, in a build step rather than from a running server. A spec generated any other way is a second source of truth, and the whole point of deriving it here is that there is only one.

func (*App) Ops added in v1.18.8

func (a *App) Ops() *App

Ops is this app's ops sibling: a second App serving /healthz, /readyz and /metrics and nothing else, so the public listener carries no ops surface and the ops listener carries no public routes.

Memoized — one sibling per app, whoever asks — because two ops apps would report two readiness answers for one process.

func (*App) Patch

func (a *App) Patch[In, Out any](path string, fn TypedHandler[In, Out], opts ...OpOption) *Operation[In, Out]

Patch declares a PATCH operation at the root.

func (*App) Peer added in v1.18.8

func (a *App) Peer() *App

Peer is this app's peer sibling: the ops a peer may invoke, served ONLY on this app's canonical socket and never on the edge.

svc.ops(app)          // the world's surface
svc.plane(app.Peer()) // the fleet's surface
return app.Listen(zip.Addr(":9653"))

App.Listen brings it up, so there is one serve verb and no second Listen to forget. It exists only when something registered on it: an app with no peer ops binds no peer socket, and one that has them binds exactly the path every caller's Ask resolves.

Memoized, because two peer apps would be two answers to one op name.

func (*App) Plugins added in v1.13.0

func (a *App) Plugins() []Status

Plugins reports every plugin this host has loaded, ordered by name so a diff between two hosts is stable. Safe to call while requests are in flight.

func (*App) Post

func (a *App) Post[In, Out any](path string, fn TypedHandler[In, Out], opts ...OpOption) *Operation[In, Out]

Post declares a POST operation at the root.

func (*App) Put

func (a *App) Put[In, Out any](path string, fn TypedHandler[In, Out], opts ...OpOption) *Operation[In, Out]

Put declares a PUT operation at the root.

func (*App) Raw added in v1.37.1

func (a *App) Raw(method, path string, handlers ...Handler) *App

Raw registers a route that is NOT a typed operation — see Group.Raw for what that means and why it is the longer spelling. Chains are in wrapping order: middleware first, the final handler last.

The method is a string because the verb NAMES belong to the typed operations:

app.Raw("GET", "/healthz", ok)

MethodAll is the one that answers a path whatever the verb.

func (*App) Reap added in v1.30.0

func (a *App) Reap(every time.Duration) (stop func())

Reap runs Evict on a ticker until the returned function is called. A host that wants the behaviour writes one line; a host that does not is unaffected, because nothing here runs unless it is asked for.

func (*App) Registry added in v1.19.0

func (a *App) Registry() []*registeredOp

Registry is the op registry as a PROJECTION: every typed op in the composition, at the path and under the id its occurrence gives it.

This is the value that replaced five verbs with one. It used to be a FIELD that composition appended to at compose time, which is why composing an app needed a verb of its own and why type-erasing one into an http.Handler destroyed the OpenAPI document, the MCP tool list, the CLI commands, the by-name call plane and the Declaration all at once. A projection cannot be destroyed by composition, because composition no longer writes it.

Lock-free once a generation is live — it is read from serving goroutines by the OpenAPI endpoint and the MCP tool listing, on every request.

Keys on the OCCURRENCE for surface (path, operationId, tags) and on the DEFINITION for types: the *registeredOp's InType/OutType are the definition's own reflect.Types, so one Invoice struct included twice is one schema, not two identical copies under two names.

func (*App) Reload added in v1.9.0

func (a *App) Reload(name string, to Plugin) error

Reload swaps the plugin named name to to, without dropping a request. The zero Plugin restarts what it is already running; Bin runs new bytes; URL+Sum pins a version or rolls one back, off disk if that digest has run before. Only the source moves: name and prefixes are fixed at Load.

The new process must be listening before any request moves to it, so a bad build returns an error and leaves the old one serving. The old one drains for Plugin.Drain, then dies.

func (*App) Routes added in v1.21.0

func (a *App) Routes() []Route

Routes is every address this program answers, as Declaration projects them: the program's own entries, absolute, without fiber's HEAD and OPTIONS shadows and without zip's control plane.

It exists so that enumerating routes does not require reaching through App.Fiber. That reach is now refused when it mutates (see checkForeignRoutes) and was always wrong when it merely read, because fiber's own GetRoutes cannot apply a doctrine zip owns — it reports shadows nobody declared and cannot tell an explicit HEAD from a generated one.

func (*App) RustCLI added in v1.36.45

func (a *App) RustCLI(crate string) (*RustCLI, error)

RustCLI renders a native Rust command-line interface with subcommands, typed flag parsing, and help text.

func (*App) RustMCP added in v1.36.45

func (a *App) RustMCP(crate string) (*RustMCP, error)

RustMCP renders a native Rust Model Context Protocol (MCP) tool surface and server dispatch. Every tool is self-documenting with rustdoc comments and JSON input schema.

func (*App) RustSDK added in v1.36.45

func (a *App) RustSDK(crate string) (*RustSDK, error)

RustSDK renders a native Rust client SDK whose methods are this app's operations. Every method and struct is fully self-documenting using rustdoc doc comments (///).

func (*App) SDK added in v1.36.20

func (a *App) SDK(pkg string) (*SDK, error)

SDK renders a Go package under pkg whose methods are this app's operations.

The emitted package imports zip and nothing else, so it compiles wherever zip does — the point of restating the types rather than importing them.

func (*App) Shutdown

func (a *App) Shutdown() error

Shutdown gracefully stops every transport, then runs teardown hooks. The process is ending, so hooks receive context.Background() — no cancellation or deadline. Use ShutdownWithContext to bound teardown. Idempotent: a second call is a no-op and hooks run at most once.

func (*App) ShutdownWithContext

func (a *App) ShutdownWithContext(ctx context.Context) error

ShutdownWithContext is Shutdown bounded by ctx: ctx bounds the in-flight drain and is passed to every teardown hook (values and deadline). Shares Shutdown's once-guard, so mixing the two still runs hooks once.

func (*App) Start added in v1.18.5

func (a *App) Start(name string) (string, error)

Start brings the plugin named name up if it is not already, and reports the address it serves on. It is what App.Reload is not: idempotent. Calling it on a running plugin returns that plugin, where Reload would start a SECOND child and retire the first.

It exists because a lazy plugin has exactly one trigger — a request reaching one of its prefixes — and a host may have another way in. Hanzo's fleet reaches an app over its own unix socket, which never touches the router, so the plugin was never started and the socket was never bound: correct by design, inert in practice. Start is that second door, and it deliberately goes through target() — the SAME single-flighted path a prefix request takes — rather than a parallel one, so a burst of first callers arriving by either door still produces exactly one child.

An Unload'ed plugin stays down: bringing one back is Reload's job, and a deliberate stop that any caller could undo would not be a stop at all.

func (*App) Test added in v1.21.0

func (a *App) Test(req *http.Request, cfg ...TestConfig) (*http.Response, error)

Test drives one request against the program without binding a socket, which is the other thing callers reached through App.Fiber for.

It builds the program if nothing is live, exactly as serving would, so a test exercises the same generation a request would — including every validation.

"Exactly as serving would" includes the DEFERRED PROJECTIONS. Serving calls prepare, which installs /mcp, the OpenAPI document, the op-call plane and the plugin route; building alone does not. Test used to skip it, so those four addresses answered 404 under test and 200 in production — a test could not see the surface it was written to check, and the closer the test got to the real program the more confidently it reported the wrong thing. Callers papered over it with an exported Prepare, which is why unexporting that broke them.

func (*App) TestCtx added in v1.7.1

func (a *App) TestCtx(method, path string) *Ctx

TestCtx returns a detached *Ctx over a synthetic request — the unit-test analog of a live request context, for calling a Handler directly. Integration tests should prefer app.Fiber().Test(req), which exercises routing and the full middleware chain; this exists for the narrower "call this one handler with locals seeded" idiom. The Ctx is not pooled; do not release it.

func (*App) Undeclared added in v1.33.0

func (a *App) Undeclared() *Group

Undeclared is Group.Undeclared at the root.

func (*App) Unload added in v1.9.0

func (a *App) Unload(name string) error

Unload stops the plugin named name. Its routes stay registered and answer 503 until a Reload brings it back — the route table is never mutated, which is what keeps repeated load/unload cycles flat.

503 rather than 404 is deliberate. 404 says "no such API", which a client is entitled to cache and stop retrying; 503 says "this API exists and is down", which is both true and retryable. Status.Disabled is how an operator tells a deliberate stop from a crash, since the wire looks the same either way.

func (*App) Use

func (a *App) Use(cs ...Component) *App

Use appends components to this app's program, in order.

Snapshot semantics

A node's middleware environment is the stack inherited at its INCLUSION SITE plus the middleware entries preceding it at its own level. Two clauses, which together are what "lexical" means here:

(a) parent-level entries written AFTER an inclusion site do not reach that
    subtree;
(b) entries written INSIDE a subtree, whenever they are written, inherit the
    environment anchored at the inclusion site.

The subtree's CONTENTS may grow until seal; its ENVIRONMENT may not. So staged composition says exactly what it looks like:

app.Use(public)
app.Use(publicAPI)   // sees public
app.Use(auth)
app.Use(privateAPI)  // sees public + auth

and a late registration inside a group is still a group registration:

v1 := app.Group("/v1")
app.Use(auth)        // parent-level, after inclusion: does NOT reach v1
v1.Get("/x", h)      // subtree-internal, late: sees v1's anchored stack

This DIVERGES from Fiber, where the stack is decided by registration time against a single flat list. Under Fiber the second example gates /v1/x; here it does not. The divergence is the point: it makes the environment a function of WHERE a thing is written rather than WHEN, so adding a line to a wiring file cannot silently change the auth seam of a subtree written elsewhere.

Use returns the App so registrations chain.

func (*App) With added in v1.5.0

func (a *App) With(mw ...Middleware) *Group

With returns a group whose subsequent leaf registrations have mw wrapped around the handler at registration time — pure composition (RateLimit(CSRF(handler))). It does NOT touch the global Use stack and does NOT route through c.Next(); it is the per-route counterpart to Use. Routes registered on the returned group still obey specificity precedence exactly like any other route.

app.With(RateLimit, CSRF).Post("/v1/keys", mintKey)

type Approval added in v1.36.45

type Approval struct {
	Status string `json:"status"` // always Held
	Clause string `json:"clause"`
	Reason string `json:"reason,omitempty"`
}

Approval is what a held op yields: the handler did NOT run, Status is Held, Clause names the clause that asked for sign-off, and Reason is what a person reads before answering it.

It carries no id, because minting and tracking an approval belongs to the authority that will answer it. zip says once, in one shape, that the op is held and why: REST and the call plane give that shape a 202, MCP and the CLI carry it in the value, the graph reports it beside the field, and a Go caller reads it off the error lane — so no projection can report a held op as a finished one.

func HeldOf added in v1.36.45

func HeldOf(err error) (*Approval, bool)

HeldOf reads the Approval back out of an error — the ONE way a caller asks whether an op was held, on any projection, with no status to match and no string to parse.

func (*Approval) Error added in v1.36.45

func (a *Approval) Error() string

Error lets an Approval travel the lane every Go caller already reads. A held op has no Out and never will have one — the handler did not run — so a projection that must answer (*Out, error) has exactly one honest shape for it. It is not a failure: nothing broke, and the next step is a person.

type Arg added in v1.17.2

type Arg struct {
	Name string // the param name, "app"
	Help string
}

Arg is one path parameter as a positional argument.

type Authorizer added in v1.8.3

type Authorizer func(ctx context.Context, op Op, in any) (Decision, error)

Authorizer authorizes a decoded, validated typed request at the op-invoke seam — the ONE point every projection of a typed handler funnels through. It runs after the request is decoded into the op's typed In and validated, and BEFORE the handler runs, over REST and MCP alike, so the value it authorizes is exactly the value the handler will act on: there is no second parse of the body for it to diverge from. in is the *In the handler will receive.

It answers a Decision, and every projection renders the three effects the same way: Allow runs the handler, Deny refuses with a 403 carrying the clause and the reason, Approve holds the op and renders it held without running it.

The error return is for a genuine failure of the CHECK ITSELF — the authority is unreachable, say. It is not how a refusal is spelled; a refusal is a Deny. A non-nil error aborts the op and is the response, so return a zip.Err* for a clean status.

type CLI added in v1.17.2

type CLI struct {
	Name     string
	Commands []Command
	Invoke   Invoker
	Out      io.Writer
}

CLI runs a derived command tree. Name is what the usage lines call the binary; Invoke defaults to LocalInvoke and Out to os.Stdout's stand-in the caller passes.

func (*CLI) Run added in v1.17.2

func (c *CLI) Run(ctx context.Context, args []string) error

Run executes one command line. It is the only entry point: help, dispatch and errors all come out of here so there is one place a CLI behaves.

type Caller added in v1.17.7

type Caller struct {
	Org       string
	Project   string
	User      string
	Name      string
	Email     string
	Owner     string
	Admin     bool
	OrgAdmin  bool
	RequestID string

	// ActedBy is the principal ACTING when this call is made on another org's
	// behalf, and empty in the ordinary case where the caller acts as itself.
	//
	// It is what makes an impersonation distinguishable from an identity: Org
	// says whose data this call is for, ActedBy says who chose to touch it. A
	// gate that must not be reachable by impersonation reads this; an audit row
	// that omits it records the wrong actor.
	ActedBy string

	// IP is where this call came FROM, and it is the one field here that is not
	// a header the caller stated about itself — it is what the connection says.
	//
	// By default it is the socket peer, which cannot be spoofed and, behind a
	// load balancer, is the load balancer. zip does not silently believe
	// X-Forwarded-For: an IP that quietly reports the proxy is worse than none,
	// because someone will rate-limit or audit on it, and an XFF that anyone can
	// set is attacker-controlled input wearing the costume of a fact.
	//
	// A deployment that IS behind a proxy opts in explicitly, and the opt-in is
	// allowlist-gated: set [Config.TrustProxy] with [Config.TrustedProxies] and
	// [Config.ProxyHeader], and the forwarded value is honoured only when the
	// peer is one of the named proxies. Anywhere else it falls back to the peer.
	//
	// Empty when there is no connection behind the context — a background
	// caller, or a command — for the same reason every other field here is:
	// nothing is known, so nothing is claimed.
	IP string
}

Caller is the gateway's assertion about who a request is for, read as one value. It is what Ctx.Forward propagates and what Call carries.

Empty fields mean the gateway said nothing — local dev, a direct ingress, or a call with no request behind it. Treat empty as unauthenticated, never as permitted. The two admin fields are separate authorities and reading one for the other is a privilege escalation: OrgAdmin says a person administers THEIR OWN org, Owner says which org that is. A deployment that reserves one org for platform operators gates its cross-tenant surfaces on Owner alone.

func CallerOf added in v1.17.7

func CallerOf(ctx context.Context) Caller

CallerOf returns the identity forwarded with this call. It is the context-shaped read of the identity an untyped handler gets field by field off its Ctx — one accessor per surface, over the same headers:

func listFlags(ctx context.Context, in *ListIn) (*ListOut, error) {
    org := zip.CallerOf(ctx).Org
    if org == "" {
        return nil, zip.ErrUnauthorized("no org")
    }
    ...
}

A context with no request behind it reads back whatever WithCaller stated on it, so what a background caller says it acts for is what the code running under that context sees — one value, written and read the same way, rather than a statement that only becomes visible one hop later.

type Client added in v1.8.4

type Client interface {
	Do(req *fasthttp.Request, resp *fasthttp.Response) error
}

Client is the call side of a transport: anything that can complete a request. *http.Transport and *fasthttp.HostClient already satisfy it, so giving a scheme a Dial is a one-liner.

type Coded added in v1.36.14

type Coded struct {
	Struct string
	Field  string
	Type   string // bytes_fixed[N], as the schema states it
}

Coded is one field the schema states and reflection refuses: bytes_fixed[N].

LayoutOf gives it an offset and a width, so the declaration here is right and a generated codec reads and writes it inline. What refuses is the reflective path, deliberately — an id is exactly the case that should force a type to declare its own wire (see Wire) rather than acquire one silently.

So this is not a gap and not a loss. It is the list of ops that need their codec GENERATED before they can cross, which is the direction the whole migration goes anyway; these are simply the ones that cannot wait.

type Command added in v1.17.2

type Command struct {
	// Service is the first non-version segment of the path ("billing"), and
	// Name is the operation token under it ("invoices-list").
	Service string
	Name    string

	// OperationID is the op's identity — the SAME token the OpenAPI document's
	// operationId, the MCP tool's name and zip.Call all address it by. A command
	// is a projection of an op, not a second thing with a second name, and this
	// is what says WHICH op it projects.
	OperationID string

	// Summary is the one-line help; Description is the full prose. Both come
	// from the handler's doc comment when cmd/zipdoc has run.
	Summary     string
	Description string

	// Method and Path are the operation's identity — the route pattern in
	// fiber's ":name" form, whatever the derivation read it from.
	Method string
	Path   string

	// Args are the path parameters, in path order, as positional arguments. The
	// URL is the addressing authority (see bindPath), so what addresses the
	// resource is positional and what modifies the request is a flag.
	Args []Arg

	// Flags are the remaining In fields.
	Flags []Flag

	// Example is the op's example input from the doc comment, rendered by the
	// help as a runnable command line. It reaches a spec-derived command either
	// from the request body or, for a bodyless method, rebuilt from the
	// parameters the document had to split it across.
	Example json.RawMessage
	// contains filtered or unexported fields
}

Command is one operation as a command line: the value both the help text and the argument parser read. It is the CLI's whole surface — a derivation fills it in, the runner consumes it, and neither knows how the other got there.

func CommandsFromSpec added in v1.17.2

func CommandsFromSpec(spec []byte) ([]Command, error)

CommandsFromSpec derives the same command tree from an OpenAPI document — which is the registry in wire form, generated by buildOpenAPI from those very ops. It is what lets a client CLI be derived without linking the service: ask a running one what it can do, and every command it offers is a projection of a route that service actually registered.

The two derivations meet in the same Command value and are read by the same runner, so a command spelled locally and a command spelled over the wire are the same command. TestCLI_SpecAndRegistryAgree pins that.

func ProjectCLI added in v1.36.49

func ProjectCLI(m Manifest) []Command

ProjectCLI is m as a command tree.

It reads the manifest rather than the document it also produces, and that is a deliberate difference from CommandsFromSpec: a document says which fields the BODY carries and which the URL does, and it cannot say that a body field opts OUT of the URL. A worker's source and its name are both spelled "script" — one in the path, one in the body — and a derivation that only sees the document drops the second as already-addressed, leaving a command that cannot send the body at all. The manifest carries both halves, so this one does not have to guess.

type Component added in v1.19.0

type Component interface {
	// contains filtered or unexported methods
}

Component is what App.Use accepts: middleware, or another App.

The set is closed — a Component is a Handler or an *App, and nothing else can implement it from outside this package. Two of the three node kinds arrive this way; a route arrives through a route method, which is why node and Component are different types rather than one type doing two jobs.

func H added in v1.19.0

func H(h Handler) Component

H adapts a bare closure to a Component.

It exists because Go will not implicitly convert `func(c *zip.Ctx) error` to an interface that only the named type Handler implements. Anything already typed as a Handler — every constructor in the middleware package, every variable declared as zip.Handler — needs no wrapper, and neither does an inline closure passed to a ROUTE method, because Get/Post/Put/Delete/All still take ...Handler. The one construct that needs H is a bare closure written inline at a Use call:

app.Use(middleware.Recover())              // already a Handler
app.Get("/x", func(c *zip.Ctx) error {…})  // route method, still ...Handler
app.Use(zip.H(func(c *zip.Ctx) error {…})) // bare closure at Use

type Config

type Config struct {
	// Logger is the luxfi/log Logger zip uses internally. Required.
	// If nil, a default one is created via luxlog.NewLogger("zip").
	Logger luxlog.Logger

	// Loader is the HIP-0105 extension runtime loader. nil disables
	// app.Module() — only native handlers will work. zip does NOT take a hard
	// dep on hanzoai/base: the consumer builds the loader and passes it in.
	// Note that [Loader] is satisfied structurally but NOT loosely — LoadDir
	// must return map[string][Module] exactly, so a backend with its own
	// Module type (e.g. hanzoai/base/plugins/extruntime) needs a thin adapter,
	// not a direct assignment. For JavaScript, import
	// [github.com/zap-proto/zip/js] — a separate import so its goja +
	// esbuild cost lands only on binaries that evaluate JS.
	Loader Loader

	// AllowedRuntimes restricts which extension runtimes app.Module()
	// will accept (e.g. ["goja","wazero"] for hard multi-tenant safety).
	// nil = allow whatever the Loader has registered.
	AllowedRuntimes []string

	// ServerHeader is sent as the Server: response header (default "zip").
	// Set to "-" to suppress.
	ServerHeader string

	// BodyLimit is the maximum request body size (default 4 MiB).
	BodyLimit int

	// AppName forwards to fiber.Config.AppName. It is also the plugin's ONE
	// name: the binary's name, its socket's stem ([SocketPath]), its
	// [Declaration].Name and its <org>-<app> IAM segment. No mapping table.
	AppName string

	// Eager says this app's work is NOT request-driven: it owns a listener, a
	// consumer or a background loop, so a host MUST start it rather than defer
	// it to the first request that reaches one of its routes.
	//
	// It is the ONE fact about an app that its router cannot show, which is why
	// it is stated here and travels in the [Declaration]. Everything else a host
	// needs to route is projected from the router itself.
	//
	// A host reads it as the ceiling on its own deferral choice
	// ([Plugin].Lazy): deferring an eager app is the mismatch §3.4 rejects.
	Eager bool

	// DisableStartupMessage suppresses Fiber's startup banner.
	DisableStartupMessage bool

	// Warm is how many plugin processes this app may hold at once. Zero means
	// unbounded, the historical behaviour. It is enforced where a plugin STARTS,
	// so it is a ceiling rather than something a sweep restores a minute later,
	// and again by [App.Evict] alongside each plugin's IdleAfter.
	//
	// The host states it because the number is a fact about the host's memory: the
	// processes are its own children in its own cgroup, and a library cannot know
	// how much of it they may have.
	Warm int

	// ErrorHandler replaces the handler that writes every refusal. Unset, a
	// refusal is an RFC 9457 problem document (application/problem+json), or
	// RFC 6749's {error, error_description} at an address declared with [OAuth].
	// See problem.go.
	ErrorHandler fiber.ErrorHandler

	// Concurrency caps the maximum number of concurrent connections the
	// server will accept. Default 0 means fasthttp's own default
	// (256*1024). Ops should cap this at the per-replica budget — see
	// `~/work/hanzo/hips/docs/SCALE_STANDARD.md`. With Hanzo's verified
	// 8 KiB/conn budget, 100_000 sits at ~800 MiB inside a 1 GiB pod.
	Concurrency int

	// SocketMode is the file mode a unix socket is bound with. Zero is 0600:
	// only the user running the app may connect, and the socket is the
	// boundary. A server that instead attests every peer from what the
	// kernel says about it — [PeerOf], [Peer.PodUID] — may open the mode,
	// because the attestation is then the boundary and the mode is only the
	// door.
	SocketMode os.FileMode

	// ReadBufferSize is fasthttp's per-conn request-read buffer (default
	// 4 KiB). Raise only for header-heavy upstreams; raising it inflates
	// the per-conn memory budget and breaks the conn-memory regression
	// gate (see SCALE_STANDARD.md §8).
	ReadBufferSize int

	// WriteBufferSize is fasthttp's per-conn response-write buffer
	// (default 4 KiB). Raise only for streaming-heavy responses; same
	// budget caveat as ReadBufferSize.
	WriteBufferSize int

	// OpsAddr is where this app's ops listener binds — /healthz, /readyz and
	// /metrics on a socket of their own, so a liveness probe never queues behind
	// public traffic and a metrics endpoint is never on a public port. Empty
	// means this process owns no ops listener, which is what a child composed
	// into a host wants: the ops port belongs to the deployment, and a child's
	// private socket is not one of the deployment's listeners (HIP-0106 §1.3(f)).
	//
	// It is an address like every other address zip takes, and the address names
	// its own transport — so an ops listener a kubelet probes and Prometheus
	// scrapes is [DefaultOpsAddr], an HTTP one. A bare ":9090" would be ZAP
	// (DefaultScheme), which those two clients cannot speak.
	OpsAddr string

	// TrustProxy says this app sits behind a proxy whose forwarded headers may
	// be believed — and it is OFF by default, deliberately.
	//
	// [Caller.IP] is the socket peer unless this is set, because X-Forwarded-For
	// is attacker-controlled input: anyone can send one. An IP that silently
	// reports the load balancer is worse than no IP at all, since someone will
	// rate-limit or audit on it.
	//
	// The opt-in is allowlist-gated rather than a boolean promise: with
	// TrustedProxies naming the proxies (IPs or CIDRs), the value in ProxyHeader
	// is honoured ONLY when the peer is one of them, and falls back to the peer
	// anywhere else. Setting TrustProxy with no TrustedProxies trusts nothing —
	// fiber skips every spoofable header — which is the safe reading of an
	// incomplete configuration.
	TrustProxy bool

	// TrustedProxies is the allowlist TrustProxy consults: proxy IPs or CIDR
	// ranges whose forwarded headers this app will believe.
	TrustedProxies []string

	// ProxyHeader names the header a trusted proxy states the caller's address
	// in — "X-Forwarded-For" for most ingress, "X-Real-IP" for some. Read only
	// when TrustProxy is set and the peer is in TrustedProxies.
	ProxyHeader string

	// OpenAPI configures the auto-generated /.well-known/openapi.json
	// served when typed handlers are registered.
	OpenAPI OpenAPIConfig

	// MCP configures the Model Context Protocol tool surface auto-derived from
	// typed handlers (Get/Post[In,Out]). Enabled by default — it's free (the
	// same op registry that feeds OpenAPI), served over every transport. Set
	// MCP.Disabled to suppress.
	MCP MCPConfig

	// Telemetry says where this app's report goes and whether it makes one. The
	// report itself is not configured: every zip app logs every request,
	// measures it, and carries trace context, because it is serving. See
	// [Telemetry].
	Telemetry Telemetry
}

Config configures the zip App. Most fields pass through to Fiber's own Config; a few zip-specific knobs control runtime loading.

type Conn added in v1.17.7

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

Conn is a handle to another zip app, dialed once and used concurrently. It holds a pooled transport, so a call on a warm Conn costs a round trip and no dial. The zero value is unusable; get one from Dial or DialApp.

func Dial added in v1.17.7

func Dial(addr string) (*Conn, error)

Dial returns a Conn to the app at addr. The address scheme selects the transport exactly as it does for App.Listen and Proxy — one registry, one vocabulary — so a bare path is ZAP over a unix socket and a bare host:port is ZAP over tcp:

zip.Dial("/run/zip/flags.sock")            // ZAP over unix
zip.Dial("flags.hanzo.svc:9653")           // ZAP over tcp
zip.Dial("https://api.hanzo.ai")           // an external edge

Dialing is lazy: the connection opens on the first Call.

func DialApp added in v1.17.7

func DialApp(name string) (*Conn, error)

DialApp returns a Conn to the named app over its canonical unix socket — the SocketPath scheme, the same one the app serves at. This is the call an aggregator makes instead of importing the app's package.

func (*Conn) Addr added in v1.17.7

func (c *Conn) Addr() string

Addr reports the address this Conn dials.

func (*Conn) Close added in v1.17.7

func (c *Conn) Close() error

Close releases the Conn's pooled connections. Calls already in flight are unaffected, and the Conn stays usable — a later Call redials.

type CppCLI added in v1.36.45

type CppCLI struct {
	Namespace string
	Header    []byte
}

CppCLI is one generated C++20 CLI header.

type CppMCP added in v1.36.45

type CppMCP struct {
	Namespace string
	Header    []byte
}

CppMCP is one generated C++20 Model Context Protocol (MCP) server header.

type CppSDK added in v1.36.45

type CppSDK struct {
	Namespace string
	Header    []byte
	Gaps      []Gap
}

CppSDK is one generated C++20 client SDK: the header content, and any gaps.

func (*CppSDK) Ops added in v1.36.45

func (s *CppSDK) Ops() int

Ops reports how many operations the C++ SDK implements.

type Ctx

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

Ctx wraps fiber.Ctx and adds the Hanzo identity surface (Org/User/Email from gateway-minted X-* headers per HIP-0026), a per-request luxfi/log logger, and typed Deps access.

func (*Ctx) App

func (c *Ctx) App() *App

App returns the parent App.

func (*Ctx) Bind

func (c *Ctx) Bind(v any) error

Bind parses the request body into v based on Content-Type (JSON by default) and runs struct-tag validation (required/min/max/minlen/maxlen). Returns a *HTTPError(400) when either step fails so handlers can return the error directly.

func (*Ctx) BindQuery

func (c *Ctx) BindQuery(v any) error

BindQuery parses query parameters into v and runs validation.

func (*Ctx) BindURI

func (c *Ctx) BindURI(v any) error

BindURI parses URL params into v and runs validation.

func (*Ctx) Body

func (c *Ctx) Body() []byte

Body returns the raw request body.

func (*Ctx) Bytes

func (c *Ctx) Bytes(code int, b []byte) error

Bytes writes raw bytes.

func (*Ctx) Context

func (c *Ctx) Context() context.Context

Context returns the standard context.Context (deadline + cancellation).

func (*Ctx) Continue

func (c *Ctx) Continue() error

Continue is an alias for Next() with the standard middleware idiom.

func (*Ctx) Fiber

func (c *Ctx) Fiber() fiber.Ctx

Fiber returns the underlying fiber.Ctx for one-off escape into Fiber-only APIs.

func (*Ctx) Forward added in v1.17.7

func (c *Ctx) Forward() context.Context

Forward returns a context that carries this request's identity onward, so a Call made with it reaches the next service as the same caller. Use it when an untyped handler calls another app; a typed handler's ctx already carries it.

out, err := zip.Call[In, Out](c.Forward(), conn, "flags_bool", &in)

This is propagation, not minting: the headers are the gateway's assertion, passed along exactly as received.

func (*Ctx) Header

func (c *Ctx) Header(name string) string

Header returns a request header.

func (*Ctx) Host added in v1.7.3

func (c *Ctx) Host() string

Host returns the request Host (authority) from the Host header, port included when present. It honors X-Forwarded-Host ONLY when the app is configured to trust proxies — which zip does NOT do (there is no TrustProxy knob on zip.Config), so a client-supplied X-Forwarded-Host is ignored and cannot spoof the value. Used for white-label brand-by-host resolution (see middleware.ProductionHeaders); keep it un-trusted-proxy so the Server brand cannot be forged from a request header.

func (*Ctx) IsAdmin

func (c *Ctx) IsAdmin() bool

IsAdmin returns the X-User-IsAdmin gateway claim as a bool.

func (*Ctx) IsOrgAdmin added in v1.18.4

func (c *Ctx) IsOrgAdmin() bool

IsOrgAdmin returns the X-User-IsOrgAdmin gateway claim: this principal administers their OWN org. It is not platform authority — see UserOwner.

func (*Ctx) JSON

func (c *Ctx) JSON(code int, v any) error

JSON writes the value as JSON with status code.

func (*Ctx) Locals

func (c *Ctx) Locals(key any, value ...any) any

Locals returns or sets a per-request value.

func (*Ctx) Log

func (c *Ctx) Log() luxlog.Logger

Log returns the request-scoped logger. Middleware that adds request_id, org, user, etc. via Locals can enrich this by calling SetLog.

func (*Ctx) Method

func (c *Ctx) Method() string

Method returns the request method.

func (*Ctx) Next

func (c *Ctx) Next() error

Next yields to the next handler in the chain. Use sparingly from zip middleware — middleware bodies usually call c.Continue() at the end, not Next() mid-handler.

func (*Ctx) NoContent

func (c *Ctx) NoContent(code int) error

NoContent writes the status code with no body.

func (*Ctx) Org

func (c *Ctx) Org() string

Org returns the X-Org-Id from the JWT-validated gateway. Empty when no gateway is in front (local dev / direct ingress).

func (*Ctx) Param

func (c *Ctx) Param(name string) string

Param returns a URL path parameter, as the caller wrote it.

func (*Ctx) Path

func (c *Ctx) Path() string

Path returns the request path.

func (*Ctx) Peer added in v1.17.7

func (c *Ctx) Peer() *Peer

Peer returns the credential of the process that made this request, or nil. The untyped-handler counterpart of PeerOf.

func (*Ctx) Project added in v1.18.4

func (c *Ctx) Project() string

Project returns the X-Project-Id from the JWT-validated gateway. It narrows the org, and is empty for a request scoped to the org as a whole.

func (*Ctx) Query

func (c *Ctx) Query(name string) string

Query returns a URL query parameter.

func (*Ctx) Redirect added in v1.7.0

func (c *Ctx) Redirect(code int, location string) error

Redirect sends an HTTP redirect to location with the given status code.

func (*Ctx) RequestID

func (c *Ctx) RequestID() string

RequestID returns the value of X-Request-Id (set by the RequestID middleware).

func (*Ctx) SendStream

func (c *Ctx) SendStream(r io.Reader) error

SendStream streams data from r to the client (e.g. for SSE).

func (*Ctx) SendStreamWriter

func (c *Ctx) SendStreamWriter(fn func(w *bufio.Writer)) error

SendStreamWriter writes streaming output via a bufio.Writer (Server-Sent Events / chunked transfer). Forwards to fiber.Ctx.SendStreamWriter.

func (*Ctx) SetContext added in v1.7.3

func (c *Ctx) SetContext(ctx context.Context)

SetContext replaces the request's context.Context — the boundary idiom: a middleware derives a request-scoped context (values, gates, deadlines) ONCE and every later c.Context() returns it. One context per request, one setter.

func (*Ctx) SetHeader

func (c *Ctx) SetHeader(name, value string)

SetHeader sets a response header.

func (*Ctx) SetLog

func (c *Ctx) SetLog(l luxlog.Logger)

SetLog replaces the request logger (typically by middleware that wants to attach request-id / org / user fields).

func (*Ctx) Status

func (c *Ctx) Status(code int) *Ctx

Status sets the response status. Chains.

func (*Ctx) String

func (c *Ctx) String(code int, s string) error

String writes a plain-text response.

func (*Ctx) User

func (c *Ctx) User() string

User returns the X-User-Id from the JWT-validated gateway.

func (*Ctx) UserEmail

func (c *Ctx) UserEmail() string

UserEmail returns the X-User-Email from the JWT-validated gateway.

func (*Ctx) UserName added in v1.18.4

func (c *Ctx) UserName() string

UserName returns the X-User-Name from the JWT-validated gateway — the minted name, where User is the opaque id.

func (*Ctx) UserOwner added in v1.18.4

func (c *Ctx) UserOwner() string

UserOwner returns the X-User-Owner gateway claim: the org this principal belongs to. A deployment reserving one org for platform operators gates its cross-tenant surfaces on this and never on IsOrgAdmin.

type Decision added in v1.36.45

type Decision struct {
	Effect Effect
	Clause string
	Reason string
}

Decision is what an Authorizer answers with.

Clause names which clause of the rule reached the outcome — a stable token a client dispatches on, written as the refusal's `code` over REST — and Reason is the sentence a person reads. Both are meaningful on a Deny and on an Approve and are ignored on an Allow, which withheld nothing and so has nothing to explain.

It is written as a literal, which is the whole notation:

return zip.Decision{Effect: zip.Allow}, nil
return zip.Decision{Effect: zip.Deny, Clause: "owner", Reason: "not the owner"}, nil
return zip.Decision{Effect: zip.Approve, Clause: "spend.cap", Reason: "over the cap"}, nil

type Declaration added in v1.18.8

type Declaration struct {
	Name   string   `json:"name"`
	Eager  bool     `json:"eager,omitempty"`
	Routes []Route  `json:"routes"`
	Ops    []string `json:"ops,omitempty"`
}

Declaration is what a plugin tells a host: who it is, whether it must be running before the first request arrives, every route pattern its router holds, and every op name it answers on the call plane.

It is the whole of what a host needs in order to route to a repository it does not build. There is no Prefixes field and no Remainder flag: a plugin that owns a version remainder declares the catch-all route it actually registered ("/v1/*"), so the fact lives in one place — the router.

type Doc added in v1.17.0

type Doc struct {
	// Description is the handler's doc comment, minus any Example/Response lines.
	Description string

	// Fields maps a JSON field name to its doc comment, for both In and Out.
	// Keyed by "TypeName.jsonField" so an In and an Out field of the same name
	// do not collide.
	Fields map[string]string

	// Example and Response are the request and response bodies from the comment.
	// Raw JSON so they land in the spec exactly as written and are wrong loudly
	// rather than quietly if malformed.
	Example  json.RawMessage
	Response json.RawMessage
}

Doc is what cmd/zipdoc extracted for one operation.

type DocsBundle added in v1.36.45

type DocsBundle struct {
	Pages map[string]string // filename (e.g. "get-validators.mdx") -> content
	Index string            // index.mdx content
}

DocsBundle represents generated documentation pages formatted for @hanzo/docs.

type Effect added in v1.36.45

type Effect uint8

Effect is the outcome of authorizing an op, and there are three of them: run it, refuse it, or hold it until a person answers. That is the whole vocabulary, so every projection renders exactly these three and none can invent a fourth the others would not know how to show.

const (
	Allow   Effect = iota // run the handler
	Deny                  // refuse before the handler runs
	Approve               // hold the op for a person; the handler does not run
)

type Field added in v1.36.14

type Field struct {
	Name   string
	Type   string
	Offset int
}

Field is one slot: an IDL type at a byte offset.

type FieldDesc added in v1.36.49

type FieldDesc struct {
	// Name is the field as the source declares it; JSON is what the wire calls
	// it, "-" for a field the body does not carry.
	Name string `json:"name"`
	JSON string `json:"json"`

	// URL is the name a URL carries it under and Header the request header it
	// reads. Both empty means it rides the body alone.
	URL    string `json:"url,omitempty"`
	Header string `json:"header,omitempty"`

	Required bool   `json:"required,omitempty"`
	Doc      string `json:"doc,omitempty"`

	Type TypeRef `json:"type"`

	// Spell is this field's type as the SOURCE LANGUAGE writes it. It is the
	// one string in a manifest that is deliberately not neutral, and it is read
	// by exactly one reader: the ledger the ZAP projection writes for the person
	// who has to go and change that declaration. Sending them to
	// "map[string]info.LP" when the file says HashMap<String, Lp> is worse than
	// saying nothing.
	Spell string `json:"spell,omitempty"`
}

FieldDesc is one field of a struct.

type Flag added in v1.17.2

type Flag struct {
	Name     string // the flag, kebab-cased and without the dashes: "organization-id"
	Field    string // the JSON field it sets: "organizationId"
	Type     string // string | integer | number | boolean | json
	Help     string
	Required bool
}

Flag is one In field as a flag.

type Gap added in v1.36.14

type Gap struct {
	Op    string // the op's name, the one [ID] gives it
	Field string // Type.field — where the value that cannot cross sits
	Go    string // the Go type there
	Cause string // why it cannot cross, from the fixed vocabulary below
}

Gap is one op the IDL cannot express, and the reason.

This is the cost of going ZAP-native stated as a number rather than as a worry. Cause groups them: it is the same string for every field that fails the same way, so counting by cause says which single change buys the most.

type GraphError added in v1.33.3

type GraphError struct {
	Message string `json:"message"`
	Path    []any  `json:"path,omitempty"`
}

GraphError is one thing that went wrong, and where.

type GraphRequest added in v1.33.3

type GraphRequest struct {
	Query     string         `json:"query"`
	Variables map[string]any `json:"variables,omitempty"`
	Operation string         `json:"operationName,omitempty"`
}

GraphRequest is a GraphQL request as every client sends one.

type GraphResponse added in v1.33.3

type GraphResponse struct {
	Data   any          `json:"data,omitempty"`
	Errors []GraphError `json:"errors,omitempty"`
}

GraphResponse is a GraphQL response.

Data is absent when execution never began — a document that would not parse, an operation that could not be chosen — and present, though it may hold nulls, once it did. That distinction is the whole shape of a GraphQL answer, so it is also what the HTTP handler reads to choose a status code.

type Group added in v1.37.1

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

Group is a prefix, the middleware that holds under it, and the options every operation declared there carries. App.Group opens one; Group.Group nests.

func (*Group) Authorize added in v1.37.2

func (g *Group) Authorize(fn Authorizer) *Group

Authorize installs fn as the rule over the ops declared on THIS group, and only those. It is App.Authorize at a prefix, with the same semantics: asked at invoke on the decoded input, a tighter rule than the one composed above it wins, and declaring none here falls through to that one rather than dropping it.

The distinction is the whole point of having it. A subsystem that guards its own writes must put the rule on the GROUP it guards: on the group, every op under it answers the rule; on the host app, the rule becomes the HOST's, and App.Authorize covers every op the host serves — including a sibling subsystem's, whose ops would then be judged by rules written about a principal this guard never attached. Refusing every valid call to an unrelated subsystem is the failure that shape produces, and putting the rule nowhere is the other one: an authenticated write that nothing checked.

func (*Group) Defaults added in v1.37.1

func (g *Group) Defaults(opts ...OpOption) *Group

Defaults carries any OpOption as a default for this group's operations. An option the operation itself sets is applied after this one and wins.

func (*Group) Delete added in v1.37.1

func (g *Group) Delete[In, Out any](path string, fn TypedHandler[In, Out], opts ...OpOption) *Operation[In, Out]

Delete declares a DELETE operation. A DELETE addresses what it deletes with its URL and carries no request body — see [hasBody].

func (*Group) Get added in v1.37.1

func (g *Group) Get[In, Out any](path string, fn TypedHandler[In, Out], opts ...OpOption) *Operation[In, Out]

Get declares a GET operation.

func (*Group) Group added in v1.37.1

func (g *Group) Group(prefix string, mw ...Handler) *Group

Group nests a group inside this one: the prefixes compose, and the outer group's options are carried into the inner one rather than shared with it, so a tag added below does not reach back up.

func (*Group) OAuth added in v1.37.1

func (g *Group) OAuth() *Group

OAuth marks every route declared on the group it returns as answering RFC 6749 §5.2 rather than RFC 9457, so the vocabulary is a property of where a route was declared and not of what its handler returns.

func (*Group) Patch added in v1.37.1

func (g *Group) Patch[In, Out any](path string, fn TypedHandler[In, Out], opts ...OpOption) *Operation[In, Out]

Patch declares a PATCH operation.

func (*Group) Post added in v1.37.1

func (g *Group) Post[In, Out any](path string, fn TypedHandler[In, Out], opts ...OpOption) *Operation[In, Out]

Post declares a POST operation.

func (*Group) Put added in v1.37.1

func (g *Group) Put[In, Out any](path string, fn TypedHandler[In, Out], opts ...OpOption) *Operation[In, Out]

Put declares a PUT operation.

func (*Group) Raw added in v1.37.1

func (g *Group) Raw(method, path string, handlers ...Handler) *Group

Raw registers a route that is NOT a typed operation: a webhook that answers bytes, a stream, an upgrade, a health probe. There is no In and no Out to infer, so it is not projected into the document, the tool list or the SDKs — see [registeredOp].

It is deliberately the longer spelling. Opting out of the typed model is the exception, and it should read like one.

func (*Group) Tag added in v1.37.1

func (g *Group) Tag(tags ...string) *Group

Tag adds tags to every operation declared under this group.

func (*Group) Undeclared added in v1.37.1

func (g *Group) Undeclared() *Group

Undeclared marks every route declared on the group it returns as serving but left out of App.Declaration.

func (*Group) Use added in v1.37.1

func (g *Group) Use(cs ...Component) *Group

Use composes middleware, or another App included by reference, into this group. It is the ONE composition verb.

func (*Group) With added in v1.37.1

func (g *Group) With(mw ...Middleware) *Group

With returns a group whose every leaf handler is wrapped by mw — the chain composes around the TERMINAL, never around the middleware preceding it, so specificity precedence is untouched and a leaf registered on the returned group later is wrapped too.

It differs from Group.Use, which puts middleware in the chain as an entry of its own. Reach for With when the chain must hold at the leaf, for Use when it holds over the group.

type HTTPError

type HTTPError struct {
	Status int
	Code   string
	Msg    string

	// Detail is what a refusal carries BESIDES its message, and it exists
	// because a typed op's only way to refuse is to RETURN an error — so
	// without it, a route whose non-2xx answer has a shape cannot be a typed op
	// at all.
	//
	// That is not hypothetical. Six subsystems route around its absence: a
	// prepaid gate answers 402 naming the cap and the balance, a plugin build
	// answers 422 carrying the diagnostics that say why it failed, a degraded
	// probe answers 503 carrying its report. Each was left untyped for this one
	// reason — losing its schema, its prose, its MCP tool and its CLI command —
	// and three separate envelopes were hand-rolled to carry a body beside an
	// error that could not.
	//
	// It is an EXTENSION in the RFC 9457 sense: the members a problem document
	// may carry beyond its own. Rendered by MERGING rather than nesting, so a
	// reader sees one object instead of a body filed under a key it has to know
	// to look in — and the document's own members are written last, so a domain
	// key called status cannot displace the refusal's own.
	Detail map[string]any
}

HTTPError is the canonical error type zip understands. Returning one is how a handler refuses; how the refusal is WRITTEN belongs to the address it happened at, and lives in problem.go.

The three members map onto RFC 9457 exactly: Status is `status`, Msg is `detail`, and Code is the extension member a client dispatches on. They map onto RFC 6749 §5.2 just as exactly — Code is `error`, Msg is `error_description` — which is why an OAuth endpoint needs no error type of its own. One value, two vocabularies, chosen by the endpoint.

func ErrBadRequest

func ErrBadRequest(msg string) *HTTPError

Common shortcuts.

func ErrConflict

func ErrConflict(msg string) *HTTPError

func ErrForbidden

func ErrForbidden(msg string) *HTTPError

func ErrInternal

func ErrInternal(msg string) *HTTPError

func ErrNotFound

func ErrNotFound(msg string) *HTTPError

func ErrPaymentRequired added in v1.31.3

func ErrPaymentRequired(msg string) *HTTPError

ErrPaymentRequired is the prepaid gate's refusal, and it is a shortcut here because the surfaces that refuse this way are exactly the ones that must say WHY — a cap, a balance, a ledger — which is what HTTPError.With carries.

func ErrUnauthorized

func ErrUnauthorized(msg string) *HTTPError

func ErrUnprocessable added in v1.31.3

func ErrUnprocessable(msg string) *HTTPError

ErrUnprocessable is the refusal for a request that parsed and cannot be acted on — source that will not build, a document failing its own schema. Its diagnostics belong ON the refusal rather than beside it.

func Errorf

func Errorf(status int, format string, args ...any) *HTTPError

Errorf builds an HTTPError with the given status and message.

func (*HTTPError) Error

func (e *HTTPError) Error() string

func (*HTTPError) MarshalJSON added in v1.31.3

func (e *HTTPError) MarshalJSON() ([]byte, error)

MarshalJSON writes the RFC 9457 problem document, the same one the error handler serves — see [HTTPError.problem].

func (*HTTPError) With added in v1.31.3

func (e *HTTPError) With(detail map[string]any) *HTTPError

With attaches extension members to a refusal and returns it, so an op refuses in one expression:

return zip.ErrPaymentRequired("spend cap exceeded").
	With(map[string]any{"cap": 5000, "spent": 5127})

It MERGES rather than replaces: a gate naming the cap and a meter naming the ledger are two facts about one refusal.

type Handler

type Handler func(c *Ctx) error

Handler is zip's request handler signature. Returning an error causes Fiber's error chain to write a JSON response.

func AdaptNetHTTP

func AdaptNetHTTP(h http.Handler) Handler

AdaptNetHTTP wraps an http.Handler so it can be served on a zip router as an ordinary zip.Handler. To front a whole foreign subtree, take a Group for the prefix and register a wildcard on it — this is THE way to bring net/http code onto zip:

app.Group("/legacy").All("/*", zip.AdaptNetHTTP(httpHandler))

Group is what carries the prefix; Proxy does not do this. Proxy delegates a prefix to a REMOTE address over a transport, so it takes a string, not a handler. A local handler is composed, not delegated.

The wildcard stays an ordinary route, so most-specific-wins precedence still applies: a route registered on the group afterwards still beats it for its exact path.

g := app.Group("/legacy")
g.All("/*", zip.AdaptNetHTTP(httpHandler)) // everything else
g.Get("/health", nativeHealth)             // still wins

A bare func is adapted through http.HandlerFunc, which is itself an http.Handler — there is no separate func-shaped adapter:

app.Get("/legacy", zip.AdaptNetHTTP(http.HandlerFunc(myFunc)))

Migration tool — costs ~5% perf vs native Fiber. Replace with native zip handlers when feasible.

func AdaptNetHTTPMiddleware

func AdaptNetHTTPMiddleware(mw func(http.Handler) http.Handler) Handler

AdaptNetHTTPMiddleware wraps a stdlib middleware (func(http.Handler) http.Handler) as a zip.Handler — the net/http-middleware bridge. Because it IS middleware, it goes on a Group's Use rather than on a route:

app.Group("/legacy").Use(zip.AdaptNetHTTPMiddleware(mw))

Migration tool — costs ~5% perf vs native Fiber.

func Static added in v1.5.0

func Static(fsys fs.FS, opts ...StaticOption) Handler

Static returns a leaf Handler that serves files from fsys. Register it on a wildcard route; the "*" capture selects the file:

app.Get("/assets/*", zip.Static(assets))                       // embed.FS
app.Get("/app/*", zip.Static(os.DirFS("dist"), zip.WithIndex("index.html")))

Contract:

  • The subpath is cleaned and checked with fs.ValidPath; any ".." escape or absolute path is rejected fail-closed with 404 — Static can never read outside fsys.
  • A missing file yields c.Next(), so a later more-specific route or a SPA catch-all still wins — never a 500.
  • Sets Content-Type (by extension), Content-Length and Last-Modified; honours HEAD and If-Modified-Since (304).
  • Advertises Accept-Ranges and serves one byte range per request: 206 with Content-Range, 416 for a span the file does not have, and the whole file for a Range it declines to honour. If-Range is respected. A file whose fs.File cannot seek is always served whole.
  • Nothing else — no compression, no directory listing.

fsys is any fs.FS: an embed.FS for baked-in assets or os.DirFS(dir) for a directory on disk. Both are traversal-safe by construction; the fs.ValidPath gate is defence in depth on top of that.

func Terminal added in v1.20.2

func Terminal(what string, h Handler) Handler

Terminal marks h as a handler that CLAIMS an address, and returns it.

Middleware may answer. A gate that cannot refuse is not a gate, so a handler in Use position is free to write a response and return without calling Next — that is what an auth check, a rate limiter, a cache hit and a maintenance refusal all are, and it is the ordinary case rather than a loophole.

What it may not do is claim an address. Only a route method says WHERE a handler answers; Use says only what runs beneath it. A gate answering a request that already matched a route adds no address, so no projection can disagree with the router — but a leaf like Static in Use position answers EVERY address beneath it with a file, including ones nothing declared. That is the one shape refused, and this marker is how the walk can tell it apart. Enforcing it needs terminality to be a property the walk can READ, and a func value carries nothing a walk can read: two closures are the same type, and matching on the name of the constructor that built one would bless zip's own leaves and condemn nobody else's.

So the constructor declares it, once, at the only place that knows what it built. Every leaf constructor in this module goes through here —

func Static(fsys fs.FS, opts ...StaticOption) Handler {
    return Terminal("zip.Static", func(c *Ctx) error { … })
}

— and so can a leaf constructor anywhere else, which is why this is exported. The [structural] check then refuses any of them in Use position, naming what and where. The handler comes back UNCHANGED and unwrapped: marking costs a leaf nothing on the served path, and `app.Get("/assets/*", zip.Static(assets))` is the same registration it always was.

what names the constructor for the diagnostic ("zip.Static"), because a refusal that says only "a terminal handler" leaves the reader to guess which argument on the line it means.

type HeaderCoder added in v1.22.0

type HeaderCoder interface{ ResponseHeaders() map[string]string }

HeaderCoder is how an answer states the response headers it carries. The headers ride the value the handler already returns, so there is no mutable slot on the request:

func (r *ReportOut) ResponseHeaders() map[string]string {
    return map[string]string{"Cache-Control": "no-store"}
}

A name the op did not declare is refused at the seam. The document publishes the declared set, so writing anything else tells a caller one thing and sends another — the same rule an undeclared status obeys.

Transports without a response of their own

The headers are written wherever the op OWNS the response: over REST, and over the by-name call plane, where one request carries one op. They are NOT written over MCP, because one HTTP response there carries a whole JSON-RPC envelope that may hold several results, and there is no per-op response to put them on. That is a defined meaning, not an oversight: the declared set still appears in the tool schema, so an agent can see what the op says about itself, and the answer's value is identical on every transport.

type Host added in v1.20.0

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

A program describes an application; a HOST runs one.

host, err := zip.Serve(app, ":8080")   // build, validate, freeze, serve
host.Include(payments)                 // live change: next generation
host.Drop(users)
host.Reload(billingV2)
host.Close()

Include, Drop and Reload are HOST verbs, not App verbs. App.Use extends a program; Include publishes a new generation of a running one. Different worlds, different receivers — and the receiver is the whole of the difference, because it decides which questions are askable.

What host anchoring deleted

When Include lived on *App, a change could land on a definition that several hosts had composed, and answering "which hosts does this reach?" needed a process-level registry of live servers, reachability filtering over each one's walk, a lock order, and an all-or-nothing commitment across them. Anchoring the verb to the host makes that question unaskable: host.Include affects one host's tree, and a host knows its own generations.

A SHARED DEFINITION IS IMMUTABLE EVERYWHERE. Changing a shared subsystem means building a new version and Reloading it at each host that wants it. Hosts on different versions mid-rollout is ordinary deployment reality, not a hazard this runtime papers over — and the guarantee that machinery replaced conceded in its own text that cutover was never simultaneous.

func Serve added in v1.20.0

func Serve(app *App, addrs ...string) (*Host, error)

Serve builds the program, validates it completely, freezes every definition it reaches, and starts serving it on the given addresses.

A program that does not compose never serves and never freezes anything: the error carries every conflict, with trails, and nothing is installed.

It returns once the build has succeeded and the listeners have been STARTED — not once they are bound. Binding is asynchronous and the transports do not report it, so a caller that must know the socket is accepting should dial it or read the readiness endpoint. Reporting "bound" from here would be the same lie App.listening tells: it is incremented when the servers are constructed, which is before any of them has touched a socket.

func (*Host) App added in v1.20.0

func (h *Host) App() *App

App returns the program this host is running. It is frozen: compose before serving, or go through the host's verbs.

func (*Host) Close added in v1.20.0

func (h *Host) Close() error

Close stops every listener and runs teardown hooks.

func (*Host) CloseWithContext added in v1.20.0

func (h *Host) CloseWithContext(ctx context.Context) error

CloseWithContext is Close bounded by ctx.

func (*Host) Drop added in v1.20.0

func (h *Host) Drop(defs ...*App) error

Drop publishes a new generation without the named definitions.

Identity is the POINTER, which is what makes this expressible: an entry is a reference, and the definition is the thing being named. It drops every reference the host's own program holds — a definition reached through a group is referenced by the GROUP, so dropping it means dropping the group.

Routing-level only. Go's plugin package has no Close and never unloads a .so, so to reclaim memory run the subsystem out of process behind Proxy.

func (*Host) Generation added in v1.20.0

func (h *Host) Generation() uint64

Generation reports which generation is live.

func (*Host) Include added in v1.20.0

func (h *Host) Include(cs ...Component) error

Include publishes a new generation with cs composed in.

Transactional: the next generation is built and validated completely before anything swaps, so a plugin whose patterns collide with the live set fails the build — with trails — and the previous generation keeps serving.

func (*Host) Registry added in v1.20.0

func (h *Host) Registry() []*registeredOp

Registry reflects the LIVE generation — the ops it publishes, at the paths and under the ids its occurrences give them.

func (*Host) Reload added in v1.20.0

func (h *Host) Reload(next ...*App) error

Reload swaps a subsystem for a new version of itself, in one generation.

A definition is frozen once it has served, so there is no such thing as changing one in place — a new version is a NEW definition, and Reload is drop the old plus include the new, atomically. Matching is by Config.AppName, which is the one name a definition carries.

func (*Host) Wait added in v1.20.1

func (h *Host) Wait() error

Wait blocks until the listeners stop, and returns why.

App.Listen is exactly Serve followed by this, which is the whole of the relationship between them: there is ONE way to start serving a program, and Listen is the terminal spelling of it for a main that will never change the program at run time.

type Interface added in v1.36.14

type Interface struct {
	Name    string
	Methods []*Method
}

Interface is one app's declared service: its ops, as methods, in name order.

type Invoker added in v1.17.2

type Invoker func(ctx context.Context, c Command, path map[string]string, body []byte) (any, error)

Invoker performs one parsed command. path holds the positional arguments by parameter name and body is the JSON built from the flags — exactly what a typed op's invoke seam takes, so a command runs through the same decode, validate and authorize path as a REST request or an MCP tool call.

type Layout added in v1.36.47

type Layout struct {
	Path    string   // the package's import path
	Package string   // its package clause
	Types   []string // the types declared, alphabetically
	Source  []byte

	// Stated is true when every type here ALREADY answers for its own bytes.
	// A generator writing into one module reads types from several, and the
	// ones it cannot write to are only a problem while they are still silent —
	// so this is what tells "another module has to release" from "another
	// module already did".
	Stated bool
}

Layout is one emitted file: the Go source stating the wire of every type in one package, and the names of the types it states.

func Layouts added in v1.36.47

func Layouts(roots ...reflect.Type) ([]Layout, error)

Layouts renders a Wire implementation for each of roots and for every struct nested below them, grouped by the package that owns the type.

A nested value is reached BY ITS METHOD, so the set is closed downward: a parent that states its wire needs every struct it holds to state one too, id or no id. The exception is a value with no slots — see [empty] — which crosses as bytes the parent writes inline.

A type the derivation refuses — one holding a map, an interface, or a fixed array of anything but bytes — comes back as an error naming it, because there is no layout to state and a codec that guessed one would speak a wire nobody reads. Answering that is a change to the TYPE, which is not a generator's to make.

type Loader added in v1.18.7

type Loader interface {
	// LoadDir scans a directory for extension manifests and returns
	// loaded modules keyed by manifest name.
	LoadDir(ctx context.Context, dir string) (map[string]Module, error)

	// LoadOne loads a single extension by directory. zip uses this for
	// [App.Module], which mounts ONE extension at one route. Implementers
	// may implement it by calling LoadDir on the parent and selecting
	// the result.
	LoadOne(ctx context.Context, dir string) (Module, error)

	// Runtimes returns the registered runtime names ("goja", "wazero",
	// "v8go", "pyvm", "starlark", "native").
	Runtimes() []string
}

Loader loads extensions for App.Module. It is the zip-side projection of the HIP-0105 extension runtime contract and it lives HERE, at the root, because the root is what consumes it: Config.Loader holds one and App.Module calls it.

The interface is duck-typed and zip does NOT import an implementation — so a consumer that never evaluates JavaScript never compiles one. Build your loader and pass it in:

app := zip.New(zip.Config{Loader: myLoader})

Duck-typed here means structural, not loose: LoadDir must return map[string]Module EXACTLY. A backend that declares its own Module type (hanzoai/base/plugins/extruntime does) therefore satisfies this through a thin adapter that returns Module, not by direct assignment — Go requires identical result types, and an equivalent method set is not enough.

The in-tree JavaScript implementation is package github.com/zap-proto/zip/js — goja to evaluate and esbuild to bundle. It is a SEPARATE import on purpose: that cost belongs to the binaries that ask for JS, not to every consumer of zip.

type Loss added in v1.36.14

type Loss struct {
	Struct string // the declaration the field sits in
	Field  string
	Go     string // the Go type whose value is dropped
	Cause  string // LossEmpty or LossPromoted
}

Loss is one field that is part of the declaration and carries NOTHING.

It is worse than a Gap and worse than an Opacity, because neither the layout nor the encoder nor the schema reports anything: the field is present, the message is well formed, and the value is simply not in it. A caller reads a zero and cannot tell it from a zero that was sent.

type MCPConfig added in v1.1.0

type MCPConfig struct {
	// Disabled suppresses the /mcp route (MCP is on by default — it's free).
	Disabled bool
	// Path overrides the HTTP mount path (default "/mcp").
	Path string
	// Addr is where this app serves MCP over ZAP, natively — no HTTP listener,
	// no request, no status code, just frames. Empty binds nothing, exactly as
	// [Config.OpsAddr] does: an address is deployment configuration and a
	// deployment states it.
	//
	//	zip.MCPConfig{Addr: zip.SocketPath("flags-mcp")}
	//
	// The door itself is [App.MCP], so a deployment that wants it somewhere this
	// does not reach serves that handler with a zapmcp.Server of its own. This is
	// the convenience, not a second mechanism.
	Addr string
	// Name is the server name reported to MCP clients (default AppName, else "zip").
	Name string
	// Source is the door's PER-CALLER half: the tools that exist because of who
	// is asking, which no build-time projection can hold. Nil — the default —
	// leaves the door exactly the typed-op projection, answered as bytes.
	Source Source
	// Identify says WHO is on a ZAP-native connection, and is the other half of
	// [Source] on that wire.
	//
	// Over HTTP the gateway's assertion arrives in headers and the route reads it.
	// On [Addr] a frame is a frame: there is nothing to read, so nothing states
	// the caller unless a deployment does — and [App.list] answers the build-time
	// bytes to a caller it cannot name. A door declaring both and no Identify is
	// therefore refused at [App.Listen] rather than served half: the tools that
	// exist because of who is asking are exactly the ones such a door drops, and
	// it drops them while answering, logging and probing healthy.
	//
	// The context carries the connection the frame arrived on ([zapmcp.Conn]),
	// which is what a deployment reads to decide: the kernel's peer credential
	// over a unix socket — the one thing about a caller the caller does not get
	// to state — the socket it came in on, its key material. zip does not choose
	// among those, for the same reason zapmcp hands over the connection rather
	// than a digest of it.
	//
	//	MCP: zip.MCPConfig{Addr: sock, Source: rows, Identify: func(ctx context.Context) zip.Caller {
	//	    return zip.Caller{Org: orgOfUID(zapmcp.Conn(ctx))}
	//	}}
	//
	// The zero [Caller] means anonymous, which leaves the door exactly the
	// build-time projection — the honest answer for a peer this deployment
	// cannot attest. It is not consulted on the HTTP route, where the assertion
	// is already on the request and a second authority is one too many.
	Identify func(ctx context.Context) Caller
}

MCPConfig configures the auto-derived MCP surface.

type Manifest added in v1.36.49

type Manifest struct {
	Manifest int `json:"manifest"`

	// App is the name the app declared for itself. It names the ZAP interface
	// and titles the document when nothing else does.
	App         string `json:"app"`
	Title       string `json:"title,omitempty"`
	Description string `json:"description,omitempty"`
	Version     string `json:"version,omitempty"`

	// Ops are this app's typed operations, in registration order — the same
	// order [App.Registry] returns, so a projection that sorts sorts once.
	Ops []ManifestOp `json:"ops"`

	// Types are every type the ops reach, each described once and referred to
	// by id. A type reached by two ops is one entry both point at, which is
	// what makes one type one schema across every projection.
	Types []TypeDesc `json:"types"`
}

Manifest is one app, described so that anything can project it.

func (Manifest) Check added in v1.36.49

func (m Manifest) Check() error

Check refuses a description a projection would silently mis-read.

A field naming a type nobody described is a dangling reference, and it is caught HERE rather than in the document, where it would have become a schema of `{}` that every generated client reads as "any value".

func (*Manifest) Settle added in v1.36.49

func (m *Manifest) Settle()

Settle fills in what a front end may leave to the projector.

An op's ID is the one thing a front end must NOT decide for itself. It is ID's rule, and a rule stated in three languages is a rule that will be spelled three ways — so a manifest may arrive without one and this settles it, which is how a Rust op and a Go op at the same address come out carrying the same token. A front end that has an explicit id (an operation renamed on purpose) keeps it.

type ManifestOp added in v1.36.49

type ManifestOp struct {
	Method string `json:"method"`
	Path   string `json:"path"`

	// ID is the op's identity — the OpenAPI operationId, the MCP tool name, the
	// CLI's spelling and the key the call plane resolves. It is filled in by the
	// front end from the SAME rule [ID] states, so a manifest that names an op
	// names it the way every other surface does.
	ID string `json:"id"`

	Summary string `json:"summary,omitempty"`
	// Description is the handler's whole doc comment, minus the Example and
	// Response lines. The summary falls back to its first sentence.
	Description string `json:"description,omitempty"`

	Tags            []string `json:"tags,omitempty"`
	Statuses        []int    `json:"statuses,omitempty"`
	ResponseHeaders []string `json:"responseHeaders,omitempty"`

	// Pkg is the package that REGISTERED the op — where the registration is
	// written, which is the source its prose was read from, not wherever the
	// handler happens to be declared. Origin is the app that declared it when it
	// arrived by composition. Together they say WHO owns this op, which is what
	// keeps two services' /height apart.
	Pkg    string `json:"pkg,omitempty"`
	Origin string `json:"origin,omitempty"`

	// In and Out are TypeDesc ids. Empty means the op takes or answers nothing.
	In  string `json:"in,omitempty"`
	Out string `json:"out,omitempty"`

	// Example and Response are the request and answer the doc comment wrote,
	// raw so they land in the document exactly as written.
	Example  json.RawMessage `json:"example,omitempty"`
	Response json.RawMessage `json:"response,omitempty"`

	// Gated says an Authorizer is in force, so the document publishes the held
	// answer beside the finished one.
	Gated bool `json:"gated,omitempty"`

	// OAuth says the operation answers at an OAuth address, so its refusal is an
	// RFC 6749 error rather than an RFC 9457 problem document.
	OAuth bool `json:"oauth,omitempty"`
}

ManifestOp is one operation.

type Method added in v1.36.14

type Method struct {
	Name    string
	Request string
	Reply   string

	// Doc is the handler's own doc comment, the same sentence the OpenAPI
	// document and the MCP tool description carry. It is read from the doc
	// registry rather than restated, so a schema and a document that disagree
	// about what a method does cannot happen: there is one sentence.
	Doc string
}

Method is one op: its name from ID, and the structs it takes and returns. An empty Request or Reply is an op with no input or no answer, which the IDL writes as an empty parameter list rather than as a struct with no fields.

type Middleware added in v1.5.0

type Middleware = func(next Handler) Handler

Middleware is a composable request transformer in the classic wrapping form: given the next Handler it returns a Handler that runs around it. This is a DIFFERENT tool from Use — they do different jobs and compose freely:

  • Use(Handler...) registers GLOBAL / prefix middleware. It runs for every matched route (or every route under a Group) in DECLARATION order and chains via c.Next(). Reach for it for ambient cross-cutting concerns that apply broadly: logging, recovery, request-id.

  • Middleware + With + Chain wrap ONE leaf handler explicitly, at registration time, with no c.Next() indirection. Reach for it when a specific endpoint needs a specific pipeline:

    app.With(RateLimit, CSRF).Post("/v1/keys", mintKey)

    wraps mintKey as RateLimit(CSRF(mintKey)): RateLimit is outermost and runs first, CSRF next, the handler last; any layer short-circuits by returning without calling next.

A Middleware body is written by hand, no framework glue:

func RequireCSRF(next zip.Handler) zip.Handler {
    return func(c *zip.Ctx) error {
        if !validCSRF(c) {
            return c.String(403, "bad csrf") // short-circuit
        }
        return next(c) // continue
    }
}

func Chain added in v1.5.0

func Chain(mw ...Middleware) Middleware

Chain composes middleware left-to-right into one Middleware. Chain(a, b, c) nests as a(b(c(handler))): a is outermost (runs first inbound, last outbound), c innermost, wrapping the handler directly. Chain() with no arguments is the identity middleware.

type Module added in v1.18.7

type Module interface {
	Name() string
	Runtime() string
	Exports() []string
	Invoke(ctx context.Context, fn string, payload []byte) ([]byte, error)
	Close() error
}

Module is one loaded extension — the zip-side projection of the HIP-0105 Module contract. App.Module mounts one of these at one route.

type Op added in v1.8.3

type Op struct {
	Method      string
	Path        string
	OperationID string
}

Op identifies a typed handler to an Authorizer, so a decision can key on the operation as well as on the input.

AS DECLARED, which for Path is the path written at the registration plus whatever prefix the registrar reported — not the address the composition finally answers at. A definition declared on a App.Group leaves its prefix to the walk (see [App.OpScope]), so an op written as Get(app.Group("/v1"), "/x") reports "/x" here and answers at "/v1/x". OperationID is the resolved id — the explicit WithOperationID, else the method+path default.

So a rule that must key on WHERE an op answers should take the prefix into its registrar (a Router that reports one) or declare the path whole, rather than read an absolute address off this. Method and OperationID carry no such qualification and are the same value everywhere.

func OpOf added in v1.37.4

func OpOf(ctx context.Context) (Op, bool)

OpOf is the operation whose handler is running, and whether there is one.

The path is the PATTERN the operation is served at — a group's prefix composed with the leaf, resolved for the occurrence answering this call, which is not knowable at registration. A definition composed under two hosts has two addresses and one handler, so it is a property of the CALL.

That is why this is a context value rather than something a group hands out at declaration: a prefix read at registration is the local join, and an address built from it named something nothing served the moment the definition was composed twice.

There is no operation exactly when a handler is called directly rather than through the declaration that registered it — in a unit test, say — which is worth naming rather than papering over with a guessed address.

type OpOption

type OpOption func(*registeredOp)

OpOption configures a typed handler registration (OpenAPI metadata).

func WithOperationID

func WithOperationID(id string) OpOption

WithOperationID sets the operation ID in OpenAPI.

func WithResponseHeader added in v1.22.0

func WithResponseHeader(names ...string) OpOption

WithResponseHeader declares the headers this op may set on its answer.

zip.Get(app, "/v1/report", report,
    zip.WithResponseHeader("Cache-Control"))

It is the response half of WithStatus, and it exists for the same reason: a response header a caller can rely on — a cache directive, a payment challenge, a Set-Cookie — is part of the contract, so it belongs in the document rather than in a context slot some middleware writes on the way out. That slot is invisible to OpenAPI, to generated SDKs and to the tool schema, which is exactly how a browser session cookie came to be set by a route no projection described.

The VALUE is stated by the answer, via HeaderCoder.

func WithStatus added in v1.18.2

func WithStatus(codes ...int) OpOption

WithStatus declares the status a SUCCESSFUL op answers with — 201 for an op that creates a resource, 202 for one that accepts work it has not finished. Without it an op answers 200, or 204 when its handler returns a nil Out.

It is declared on the op because the status is part of the CONTRACT, and the contract is what the registry projects: the OpenAPI document keys its response on this code, so a generated SDK expects the status the service actually sends. Setting it per request instead — reaching around the framework from inside a handler — writes a contract detail into a side channel no projection can read, which is how a document comes to say 200 about a route that has always answered 201.

The status is an HTTP notion, so it applies to the REST route and the document and to nothing else: an MCP tools/call, a CLI command and a zip.Call all carry their own outcome and are untouched.

Declaring a non-2xx

A declared non-2xx is for an op that answers a failure with its OWN TYPED BODY — a 409 carrying the conflicting record, a 404 carrying what was searched for. It is not a second way to spell an error: ErrNotFound and friends remain how a handler returns a failure, and they render the RFC 9457 problem document every refusal is written in.

The difference is which body reaches the wire. An error returns the problem document; a declared status returns the op's Out type, described in the document under that code like any other response. Reach for the error unless the caller genuinely needs a typed body a problem document cannot carry — two ways to spell the same failure is exactly the drift this package spends its doc comments avoiding.

func WithSummary

func WithSummary(s string) OpOption

WithSummary sets the operation summary in OpenAPI.

func WithTags

func WithTags(tags ...string) OpOption

WithTags sets the operation tags in OpenAPI.

type Opacity added in v1.36.14

type Opacity struct {
	Struct string // the declaration the field sits in
	Field  string
	Go     string // the Go type whose name is lost
	List   bool   // it is the ELEMENT of a list that is opaque
}

Opacity is one field whose TYPE the schema cannot name, though its bytes cross exactly.

A nested value is carried as a complete ZAP message inside an 8-byte {offset,length} slot — `bytes` — where the IDL's own nested struct is a 4-byte relative offset. Those are different slots, so naming the type here would state a width the encoder does not write and move every field after it. The bytes are therefore right and the name is gone: a peer reading this schema learns that something crosses without learning what.

It is not a Gap: the op IS in the schema and its wire is exact. It is the measure of how much of the shape the schema can carry today, and it closes the day a nested value crosses as the IDL's own struct.

type OpenAPIConfig

type OpenAPIConfig struct {
	// Title appears in the OpenAPI info block.
	Title string
	// Description appears in the OpenAPI info block.
	Description string
	// Version appears in the OpenAPI info block (e.g. "v1.0.0").
	Version string
	// Disabled suppresses the /.well-known/openapi.json route and /docs.
	Disabled bool
}

OpenAPIConfig configures the auto-generated /.well-known/openapi.json endpoint zip serves when typed handlers are registered.

type Operation added in v1.37.0

type Operation[In, Out any] struct {
	// contains filtered or unexported fields
}

Operation is a declared typed route. Its methods carry the metadata that cannot be inferred — everything else (method, path, input, output, the handler's documentation) is already known from the declaration.

It is the same record the authorizer sees as Op and the document publishes; this is the handle the declaring code holds.

func (*Operation[In, Out]) ID added in v1.37.0

func (o *Operation[In, Out]) ID(id string) *Operation[In, Out]

ID sets the operation id. Without one the method and path name the operation.

func (*Operation[In, Out]) Status added in v1.37.0

func (o *Operation[In, Out]) Status(codes ...int) *Operation[In, Out]

Status declares the success codes this operation answers with — see WithStatus for what more than one means.

func (*Operation[In, Out]) Summary added in v1.37.0

func (o *Operation[In, Out]) Summary(s string) *Operation[In, Out]

Summary sets the one-line summary the document publishes.

func (*Operation[In, Out]) Tag added in v1.37.0

func (o *Operation[In, Out]) Tag(tags ...string) *Operation[In, Out]

Tag adds tags to this operation, after any its group declared.

func (*Operation[In, Out]) With added in v1.37.0

func (o *Operation[In, Out]) With(opts ...OpOption) *Operation[In, Out]

With applies any OpOption, so the chain never becomes the only way to say something the option vocabulary already says.

type Peer added in v1.17.7

type Peer struct {
	PID int
	UID int
	GID int
}

Peer is the kernel's attestation of the process at the other end of a unix socket: what it is, not what it claims. Read it to decide whether a caller may reach an op at all — a coarse, infrastructure-level gate under the per-user authorization that Authorizer performs on the decoded input.

func PeerOf added in v1.17.7

func PeerOf(ctx context.Context) *Peer

PeerOf returns the credential of the process that made this call, or nil when there is nothing to attest — the request arrived over tcp, or the host OS does not report peer credentials (see peerOf).

It is the typed-handler counterpart of Ctx.Peer: a typed op reads it off the ctx it was handed.

if p := zip.PeerOf(ctx); p == nil || p.UID != wantUID {
    return nil, zip.ErrForbidden("not a fleet peer")
}

func (*Peer) PodUID added in v1.36.42

func (p *Peer) PodUID() string

PodUID returns the UID of the Kubernetes pod the peer process runs in, read from the cgroup the kubelet placed it in, or "" for a process outside any pod. The kernel wrote the cgroup and the kubelet named it, so like the peer itself it is a fact about the caller and not a value the caller states.

func (*Peer) String added in v1.17.7

func (p *Peer) String() string

String renders a peer for a log line.

type Plugin added in v1.9.0

type Plugin struct {
	Name string   // identifies it in log lines and names its socket
	Addr string   // already running here — start nothing, just mount
	Bin  []byte   // the binary, normally go:embed'd
	Path string   // ...or where it lives on disk
	URL  string   // ...or a release artifact to fetch (requires Sum)
	Args []string // passed after argv[0]
	Env  []string // added to the child's environment

	// Sum is the hex SHA-256 of the binary at URL, and is REQUIRED with it.
	// Fetching code over a network and executing it is the one place a plugin
	// host becomes an arbitrary-code-execution vector, so an unverified
	// download is refused rather than trusted.
	//
	// It doubles as the cache key: a binary already present under this digest
	// is reused, so a restart costs no download and a rollback to a previously
	// run version is free and offline.
	Sum string

	// Dir is where an embedded binary is extracted and its socket created.
	// Empty means the system temp dir, which on many hosts is a tmpfs — i.e.
	// RAM. A plugin binary is tens to hundreds of megabytes, so extracting one
	// there spends real memory and fails outright when the tmpfs is full. Point
	// this at disk for anything but a small plugin.
	Dir string

	// Start bounds how long to wait for the plugin to listen. Zero means 10s.
	// A plugin that has not bound by then is a startup failure, not a slow
	// one — nothing is mounted onto a process that never came up.
	Start time.Duration

	// Drain is how long a replaced process keeps serving after a Reload, so
	// requests already in flight on it finish. Zero means 5s.
	Drain time.Duration

	// IdleAfter stops a LAZY plugin that has not served for this long, to be
	// started again by the next request that needs it. Zero means never, which
	// is the historical behaviour. See idle.go: without it a host pays for
	// every plugin it has ever served, so resident memory tracks the size of
	// the catalog rather than the traffic.
	IdleAfter time.Duration

	// Lazy defers starting the child until the first request actually reaches
	// one of its prefixes. Routes register at Load either way, so the surface
	// is identical — only the process is deferred.
	//
	// This is what makes many plugins affordable. A host composing 69 services
	// eagerly pays 69 processes, 69 resident sets and 69 startup times at boot
	// for a set that is mostly idle; lazily it pays for the ones traffic
	// actually reaches. The cost moves to the first request, which is why it is
	// opt-in: a latency-critical prefix should stay eager.
	Lazy bool

	// Tools is this plugin's MCP catalogue: the JSON array [App.MCPTools]
	// projects, captured at BUILD time and normally go:embed'd beside Bin. Given
	// it, the host serves every plugin's tools on its own /mcp door and forwards
	// a tools/call to the owner.
	//
	// It is a byte slice and not a call because MCPTools is in-process: a host
	// cannot ask a plugin that is not running, and asking would defeat Lazy —
	// tools/list is the method an MCP client calls constantly, so a host that
	// woke every child to answer it would pay 112 processes for a question with
	// a build-time answer. The answer IS build-time: the same typed-op registry
	// that emits the plugin's OpenAPI document emits this.
	//
	// The catalogue can only be INCOMPLETE, never wrong: the child's own registry
	// answers the call, so a name the host no longer serves yields that child's
	// -32602 rather than a mis-dispatch. Two plugins declaring one tool name is
	// refused at Load, for the same reason a duplicate prefix is — a name is
	// dispatch, so a duplicate is unroutable.
	//
	// The forward target is this plugin's own MCP path, which means a plugin that
	// moved it (MCPConfig.Path) or turned it off (MCPConfig.Disabled) must not
	// ship a catalogue: the host would name tools the child does not answer at
	// that path. Leave Tools empty and the plugin is simply not on the host's
	// door.
	Tools []byte

	// MCPPath is where this plugin serves its own MCP door, when it is not zip's
	// default. Set it only alongside a matching MCPConfig.Path in the plugin.
	MCPPath string

	// Open declares this plugin's catalogue INCOMPLETE BY CONSTRUCTION: it also
	// serves tools that exist because of WHO is asking — a tenant's own rows,
	// which no build-time projection can hold. Tools stays what it is (the part
	// that IS build-time); Open is how the rest gets onto the host's door.
	//
	// Given it, the host asks this plugin — and only this plugin — for the
	// caller's own tools on a tools/list that NAMES a caller, and hands it a
	// tools/call no catalogue claimed. An anonymous list still costs a memcpy and
	// starts nothing: a per-caller answer needs a caller, so there is nothing to
	// ask when nobody is asking.
	//
	// At most ONE plugin may be open, for the reason two may not own one tool
	// name: an unclaimed name has to resolve somewhere, and two candidates make
	// it ambiguous. Load refuses the second, naming the first.
	Open bool
}

Plugin is a service that ships as its own binary. Exactly one of Addr, Bin, or Path says where to find it:

Addr — already running there; nothing is started, and Reload does not apply
Bin  — the binary itself, normally go:embed'd
Path — the binary on disk

type Projection added in v1.18.8

type Projection string

Projection names one document an app can emit from its own op registry. The verb on the command line IS the name here, and both write a FILE.

const (
	// OpenAPI is the app's own OpenAPI subset, composed upward into the fleet
	// document. Never carved out of the fleet document by prefix: that makes
	// the fleet the source and the plugin a derivative, which is how a
	// catch-all silently swallows a neighbour's routes.
	OpenAPI Projection = "openapi"

	// Declare is the routing declaration a host discovers (see [Declaration]).
	Declare Projection = "declare"
)

type Remote added in v1.17.2

type Remote struct {
	Base   string            // "https://api.hanzo.ai", "http://:8080", a unix socket path…
	Header map[string]string // sent on every request (Authorization, …)
}

Remote executes a command against a running zip service, and reads that service's registry off the document it derives from it. Base is an address in the same form Proxy takes — the scheme selects the transport, so a command runs over ZAP, HTTP or anything else RegisterTransport'd without the CLI knowing which.

func (Remote) Invoke added in v1.17.2

func (r Remote) Invoke(ctx context.Context, c Command, path map[string]string, body []byte) (any, error)

Invoke sends one command to the service. Its signature is Invoker's, so it drops into a CLI wherever LocalInvoke would.

func (Remote) Spec added in v1.17.2

func (r Remote) Spec(ctx context.Context) ([]byte, error)

Spec fetches the OpenAPI document — the registry in wire form. Pair it with CommandsFromSpec to build a command tree for a service this binary does not link.

type Rename added in v1.36.14

type Rename struct {
	Op     string // the id [ID] gives it, and every other surface uses
	Method string // what this schema had to call it
}

Rename is one op whose method name had to differ from its id.

type Resolver added in v1.35.0

type Resolver func(ctx context.Context, kind, field string, args map[string]any) (any, error)

Resolver answers one root field for a graph this app serves but does not own.

kind is "query" or "mutation" — the operation the field was selected under. It is passed because only the resolver knows whether a foreign field is safe to read: this app has no registry entry to check, so the rule that a write is not reachable as a read has to be enforced where the knowledge is.

args arrive with variables already substituted, named as the schema publishes them. The value returned is projected through the selection set exactly as a local op's result is.

type Route added in v1.18.8

type Route struct {
	Method  string `json:"method"`
	Pattern string `json:"pattern"`

	// Op is the operation id this pattern answers under, empty when the route is
	// untyped. It is what makes a remote [Proxy] able to contribute OPS and
	// not merely addresses: the mounting app is handed the declaration inline and
	// reads the ids out of it, instead of asking the remote what it serves.
	Op string `json:"op,omitempty"`
}

Route is one pattern in the ROUTER's own spelling — ":id", not "{id}". The host mounts what the router matches, so the two must be the same string.

type RustCLI added in v1.36.45

type RustCLI struct {
	Crate  string
	Source []byte
}

RustCLI is one generated Rust CLI module.

type RustMCP added in v1.36.45

type RustMCP struct {
	Crate  string
	Source []byte
}

RustMCP is one generated Rust Model Context Protocol (MCP) server module.

type RustSDK added in v1.36.45

type RustSDK struct {
	Crate  string
	Source []byte
	Gaps   []Gap
}

RustSDK is one generated Rust crate/module: the source, and any gaps.

func (*RustSDK) Ops added in v1.36.45

func (s *RustSDK) Ops() int

Ops reports how many operations the Rust SDK implements.

type SDK added in v1.36.20

type SDK struct {
	Package string
	Source  []byte
	Gaps    []Gap
}

SDK is one generated Go package: the source, and the ops that are not in it.

Gaps is not decoration. An operation named there has NO method in Source, and a generator that emitted one anyway — with the field it could not express quietly dropped — would publish a client that compiles and lies. Same rule as Schema.Gaps, for the same reason, and mostly the same list: an op the wire refuses is an op no client can call.

func (*SDK) Ops added in v1.36.20

func (s *SDK) Ops() int

Ops is how many operations the package can call.

type Schema added in v1.36.14

type Schema struct {
	Package    string
	Structs    []*Struct
	Interfaces []*Interface
	Gaps       []Gap
	// Renamed is every op whose id the IDL's lexer cannot spell. Not a gap — the
	// op IS in the schema — but a divergence between the name it carries here and
	// the one it carries on every other surface, which is worth saying out loud.
	Renamed []Rename
	// Opaque is every field that crosses correctly and arrives WITHOUT ITS NAME:
	// a nested value, which this wire carries as a complete message inside an
	// {offset,length} slot. See [Opacity].
	Opaque []Opacity
	// Dropped is every field whose VALUE does not cross at all, on a type the
	// layout accepts. See [Loss] — it is the most expensive list here, because
	// nothing fails.
	Dropped []Loss
	// Coded is every field that this schema states correctly and the REFLECTIVE
	// encoder will not carry. See [Coded].
	Coded []Coded
}

Schema is one .zap file: the structs the ops reach, the interfaces they form, and the ops that could not be expressed at all.

func ProjectZAP added in v1.36.49

func ProjectZAP(pkg string, ms ...Manifest) *Schema

ProjectZAP is m as a ZAP schema.

func ZAPSchema added in v1.36.14

func ZAPSchema(pkg string, apps ...*App) *Schema

ZAPSchema renders the ZAP IDL that apps' typed ops describe, under pkg.

The text is the artifact — it is what zapgen compiles into accessors, and publishing it is what lets anything that is not this process speak to it without a copy of these Go types. The Schema.Gaps are the other half of the answer and are not decoration: an op named there is NOT in the text.

ONE INTERFACE PER APP, and one struct namespace across all of them. An interface is a declared service and its method ordinals are positional, so a fleet rendered as one interface would renumber every service's methods when any service gained one. Types are the opposite: an Application is an Application wherever it is reached, so it is declared once and referred to by name — the same split the JSON document already makes between an operationId, which belongs to the occurrence, and a schema, which belongs to the type.

func (*Schema) Blocked added in v1.36.14

func (s *Schema) Blocked() int

Blocked is how many DISTINCT ops are absent from the schema. len(Gaps) counts reasons, and one op can have several.

func (*Schema) Ops added in v1.36.14

func (s *Schema) Ops() int

Ops is how many ops the schema expresses, across every interface.

func (*Schema) String added in v1.36.14

func (s *Schema) String() string

type SchemaDescriber added in v1.36.11

type SchemaDescriber interface {
	JSONSchema() map[string]any
}

SchemaDescriber is implemented by a type that writes its own JSON and can say what that JSON looks like. Without it a marshaler is published as `{}`: true, because the fields are not the wire form, but useless to anything generating a client — every such field arrives as an untyped value.

The shape is a JSON Schema object, the same vocabulary the rest of the document speaks. A numeric carried as a quoted decimal, which is the usual reason a type marshals itself, says so directly:

func (Uint64) JSONSchema() map[string]any {
    return map[string]any{"type": "string", "pattern": "^[0-9]+$"}
}

Implement it on the value, not the pointer, so a field of either form is described. Returning nil means "no shape to state" and falls back to `{}`.

type Server added in v1.1.0

type Server interface {
	ListenAndServe() error
	Close() error
}

Server is a running transport listener bound to one address. Both zap-proto/http.Server and the built-in HTTP server satisfy it, as does any custom transport.

type Shape added in v1.36.13

type Shape = zapenc.Shape

Shape is a whole ZAP message: its slots in declaration order, and its size.

func LayoutOf added in v1.36.13

func LayoutOf(t reflect.Type) (Shape, error)

LayoutOf derives the wire layout of a struct type: which offset each field takes, how wide it is, and what the IDL calls it.

It is ONE derivation with three readers, and that is the whole reason it is exported. The op-call plane ENCODES against it; a .zap schema generated from these types must STATE it, field by field, as `Name type @Offset`; and a code generator emitting a Wire implementation must EMIT it as constants. Derived a second time, those three describe three different wires and only one of them is spoken — a schema is then a document about a protocol nobody runs, and a generated codec is a rolling deploy that reads its own messages and nobody else's.

Two details a second derivation gets wrong, both measured on this fleet's own types. Offsets are ALIGNED, not packed: a field begins at a multiple of its own width, so a bool at 0 puts the string after it at 8 and not at 1. And a nested value is EIGHT bytes and its IDL type is `bytes`, because it crosses as a complete ZAP message in an {offset,length} slot rather than as a four-byte relative offset. Packing, or giving a nested value four bytes, moves every field after it.

bytes_fixed[N] is laid out here and is NOT carried by the reflective layout. The layout is what a schema and a generator need; refusing to encode it is what makes an id — an ids.ID is [32]byte — force a type to declare its own wire rather than quietly acquiring a reflective one.

type Slot added in v1.36.13

type Slot = zapenc.Slot

Slot is where one field of a ZAP message sits on the wire, and what it is. See LayoutOf.

type Source added in v1.18.14

type Source interface {
	// Tools are the caller's own tool descriptors: {name, description, inputSchema},
	// the same shape [App.MCPTools] projects.
	Tools(ctx context.Context) []map[string]any
	// Call runs one of them with the raw JSON arguments object and returns its
	// JSON-encodable result.
	Call(ctx context.Context, name string, args json.RawMessage) (any, error)
}

Source is the half of an MCP door that depends on the CALLER.

A typed op is a value known at build time, so zip projects it into a tool once and serves the array as bytes. A tenant's own capabilities are rows: they exist because of who is asking, so they cannot be projected and cannot be cached across callers. Both are tools on ONE door — this is how the second kind gets there, without a second registry in front of it.

Tools is called on every tools/list that carries a caller, so an implementation answers from data it already has and never fans out. Call runs a name the build-time catalogue did not claim; returning an error is reported to the model as MCP isError content, exactly like a typed op's error, so a refusal is something it can read and react to.

The context is the one the request is being served on — the same value a typed op receives — so an implementation reads its caller from there and never from the arguments, which the caller wrote.

type StaticOption added in v1.5.0

type StaticOption func(*staticConfig)

StaticOption configures Static. Zero options serve fsys as-is.

func WithFallback added in v1.7.2

func WithFallback(name string) StaticOption

WithFallback serves name (e.g. "index.html") when the requested file does not exist — the SPA deep-link idiom: client-side routes resolve to the app shell instead of 404/next-route. Traversal-invalid paths still fail closed.

func WithIndex added in v1.5.0

func WithIndex(name string) StaticOption

WithIndex serves name (e.g. "index.html") for directory and root requests. Without it a directory request falls through via c.Next().

func WithStripPrefix added in v1.5.0

func WithStripPrefix(prefix string) StaticOption

WithStripPrefix derives the fs path from the request path with prefix removed, instead of from the route's "*" capture. Use it when the captured subpath is not the fs path — e.g. a versioned URL served from an unversioned tree: WithStripPrefix("/static/v2/") maps /static/v2/app.js to app.js.

type Status added in v1.17.4

type Status struct {
	Name string `json:"name"`

	// Prefix is the FIRST subtree this plugin answers — the one a log line
	// names it by. Prefixes is every subtree, and a plugin may own several.
	// Reporting only the first would understate the blast radius of taking
	// this plugin down, which is the question a fleet view exists to answer.
	Prefix   string   `json:"prefix"`
	Prefixes []string `json:"prefixes,omitempty"`

	// Source is where the binary came from: "embedded", "path", "url", or
	// "remote" for an instance this host did not start.
	Source string `json:"source"`

	// Version is the artifact's SHA-256 when it was installed from a URL —
	// the only version identifier that cannot drift from the bits actually
	// running, since it IS the bits. Empty for the other sources.
	Version string `json:"version,omitempty"`

	// Addr is the socket or address serving it.
	Addr string `json:"addr,omitempty"`

	// PID is the child process, or 0 when this host did not start it.
	PID int `json:"pid,omitempty"`

	// Running is false after Unload, or after a child exited and no Reload has
	// replaced it. Its routes stay registered and answer 503, so a false here
	// is the difference between "not deployed" and "deployed but down".
	Running bool `json:"running"`

	// Disabled is true when Unload stopped it deliberately, as opposed to it
	// having crashed. Both answer 503, so without this an operator cannot tell
	// a maintenance window from an outage — and would page for the former.
	Disabled bool `json:"disabled,omitempty"`

	// Since is when the CURRENT instance started — it resets on Reload, so it
	// reports the age of what is running, not of the mount.
	Since time.Time `json:"since,omitzero"`

	// Reloads counts successful swaps since Load. A climbing number on one
	// host and not its peers is the signal that a rollout is uneven.
	Reloads int `json:"reloads"`

	// Restarts counts times the supervisor brought this plugin back after it
	// died on its own. Distinct from Reloads, which are deliberate: a nonzero
	// Restarts is a plugin crashing, and a climbing one is a crash loop.
	Restarts int `json:"restarts"`

	// Error is why the last start attempt failed, empty when none has. It is the
	// answer Running=false cannot give: a lazy plugin that has never been asked and
	// one whose every start dies both report Running=false, and only this separates
	// them. Cleared when an instance comes up.
	Error string `json:"error,omitempty"`

	// Usage is what this plugin costs right now, read from the kernel.
	Usage Usage `json:"usage,omitzero"`
}

Status is one loaded plugin as the host currently sees it.

type StatusCoder added in v1.21.0

type StatusCoder interface{ StatusCode() int }

StatusCoder is how an op with MORE THAN ONE declared success status says which one this answer is. The OUTPUT VALUE states it:

func (r *CreateOut) StatusCode() int {
    if r.Existed { return 200 }
    return 201
}

zip.Post(app, "/v1/things", create, zip.WithStatus(200, 201))

The status is a property of the ANSWER, so it rides the value the handler already returns. It is deliberately not a mutable slot on the request: a slot is a side channel, and a side channel is invisible to every projection, which is the whole defect this closes.

A code the op did not declare is refused at the seam rather than written to the wire, because the document publishes exactly the declared set and a generated client expects nothing else.

type Struct added in v1.36.14

type Struct struct {
	Name   string
	Fields []Field
	Size   int
	// From is the Go type this was derived from. It is not in the emitted text —
	// the schema is the contract and the Go type is one implementation of it —
	// but it is what a reader of the gap list needs in order to find the
	// declaration to change.
	From string
}

Struct is one declared struct: its fields in slot order, and the byte size of its fixed area. Both come from LayoutOf.

type Telemetry added in v1.24.4

type Telemetry struct {
	// Spans is where this app's server spans go, as host:port.
	Spans string

	// Logs is where this app's request line goes, as host:port.
	//
	// Only zip's OWN request line ships here — the same line, with the same
	// fields, that the app's logger already writes. An app's other logging is
	// its own: its luxlog.Logger writes wherever the app pointed it and is
	// never intercepted, teed or rewritten by the framework. Shipping a
	// program's every log line off the box is one decision for the logger to
	// make, in luxfi/log, once, for everything that logs; taking it here would
	// mean zip quietly making it for 125 programs that never asked.
	Logs string

	// Metrics is where the numbers go, as host:port.
	Metrics string

	// Off stops this app reporting at all: no line, no numbers, no spans, no
	// trace context. It is for the app that must not emit — a test double, a
	// one-shot command — and it is the only switch of its kind, so "is this app
	// instrumented" has one answer and one place to read it.
	Off bool
}

Telemetry is where this app's three reports go. Every field may stay empty forever, and an empty field means that signal is not exported.

THE ADDRESS IS THE SWITCH, and it is the switch on both ends: the collector states exactly this rule for receiving — an empty address means that signal is not received in this process — so one sentence describes the producer and the consumer, and there is no enable flag on either side that can disagree with an address. A program with nothing stated still logs and still counts; it simply has nowhere to send, which costs it no goroutine, no connection and no port.

A field is read first, and the matching variable in the environment second, so a deployment that states an address in its manifest and a process that inherits one from its host agree on which wins — the explicit one:

Spans     O11Y_SPANS_ADDR
Logs      O11Y_LOGS_ADDR
Metrics   O11Y_METRICS_ADDR

Three names rather than one base address with the three conventional ports derived from it (spans 4317, logs 4318, metrics 4319). The collector binds one listener per signal at one address per signal, refuses two signals that name the same address, and receives whichever signals have one — so deriving two addresses from a third would assert an adjacency it does not promise, and would make "logs are off, spans are on" unsayable. Sibling names also state which port belongs to which signal at the place a deployment sets it, and a batch sent to the wrong ear is dropped by a receiver that decodes one message type, silently on both sides.

What is reported is not configurable at all, because a fleet whose members report different shapes cannot be read as a fleet.

type TestConfig added in v1.24.0

type TestConfig struct {
	// Timeout bounds the request. Zero means the underlying default (1s).
	Timeout time.Duration

	// FailOnTimeout returns an error when Timeout is exceeded, rather than a
	// 408 response. Default true, matching the router.
	FailOnTimeout bool
}

TestConfig bounds a App.Test request. It is zip's own type on purpose: Test is what callers are told to use INSTEAD of reaching through App.Fiber, and a signature naming fiber sent them straight back to the escape hatch it replaces — so every test that wanted a timeout imported the router this package exists to keep out of its callers.

Two fields because two are what the estate uses. A knob nobody turns is a promise about the router underneath.

type Transport added in v1.8.4

type Transport struct {
	Serve func(addr string, handler fasthttp.RequestHandler) Server
	Dial  func(addr string) Client
}

Transport is one address scheme in both directions: Serve terminates bytes arriving here, Dial originates bytes going there. They are the same concept — a wire — so they live in one value rather than two registries that drift. A scheme may leave either half nil; Listen and Proxy each say which they needed.

type TypeDesc added in v1.36.49

type TypeDesc struct {
	// ID is how fields refer to this type within one manifest.
	ID string `json:"id"`
	// Name is the type's declared name, empty for an anonymous one.
	Name string `json:"name,omitempty"`
	// Pkg is the module or crate the type was declared in. It is what tells two
	// Config types apart when both reach one document.
	Pkg string `json:"pkg,omitempty"`

	// Kind is what a reader must do with it: "struct" has fields, "list" and
	// "map" hold something, "scalar" is a value. It is STRUCTURAL and says
	// nothing about the wire form — a type that states its own form says so in
	// JSON, and the two are separate because they disagree on purpose: a quoted
	// decimal is a string to a reader and eight bytes to a layout.
	Kind string `json:"kind"`

	// Spell is this type as the source language writes it. See [FieldDesc.Spell].
	Spell string `json:"spell,omitempty"`

	// JSON is the schema this type states for ITSELF — the answer a type gives
	// when its fields are not its wire form. Present for a quoted number, a
	// timestamp, a raw message. A projection that has one uses it and does not
	// look inside.
	JSON json.RawMessage `json:"json,omitempty"`

	// Text says this type reads and writes itself as one word, so a URL can
	// carry it whatever it is made of.
	Text bool `json:"text,omitempty"`

	// Repr is the fixed-layout spelling of a scalar: bool, i8..i64, u8..u64,
	// f32, f64, text, bytes, bytes_fixed[N].
	Repr string `json:"repr,omitempty"`

	// Elem is what a named list or map holds. A named slice is still a list of
	// something, and a description that forgot what would publish an array of
	// untyped objects.
	Elem *TypeRef `json:"elem,omitempty"`

	// Len is a list's fixed length, 0 for one that has none. A fixed-length
	// list of anything but bytes has no place in a layout — an element is one
	// value at one width — so the length is what tells the two apart.
	Len int `json:"len,omitempty"`

	Fields []FieldDesc `json:"fields,omitempty"`
}

TypeDesc is one type, described by what it is made of and by what it looks like on the wire — which are not always the same thing, and that is the reason both are here. A quoted decimal is a u64 to a fixed layout and a string to a JSON reader; a description that could only say one of those would make one of the two projections wrong.

type TypeRef added in v1.36.49

type TypeRef struct {
	Prim string   `json:"prim,omitempty"`
	Ref  string   `json:"ref,omitempty"`
	List *TypeRef `json:"list,omitempty"`
	// Len is a fixed-length list's length. See [TypeDesc.Len].
	Len int `json:"len,omitempty"`
	// Map is a map's VALUE type; a key is a string on every wire that has maps.
	Map *TypeRef `json:"map,omitempty"`
	// Opt says the value may be absent — a pointer, an Option, a nullable.
	Opt bool `json:"opt,omitempty"`
}

TypeRef names a field's type: a primitive by its spelling, a described type by its id, or a container around either.

type TypedHandler

type TypedHandler[In, Out any] func(ctx context.Context, in *In) (*Out, error)

TypedHandler is the generic handler signature: takes an *In, returns (*Out, error). zip generates OpenAPI 3.1 spec from the In/Out types and registers a Fiber route that unmarshals body → In, runs the handler, and marshals Out → JSON response.

type Usage added in v1.15.0

type Usage struct {
	// CPU is total user+system time this instance has consumed since it
	// started. It only ever climbs, so a rate is the interesting derivative.
	CPU time.Duration `json:"cpuNs,omitzero"`
	// RSS is resident memory in bytes — the honest number for "what does this
	// plugin cost", as opposed to virtual size.
	RSS int64 `json:"rssBytes,omitzero"`
	// Threads and FDs are the two limits a busy service hits first, and both
	// are leaks when they climb without bound.
	Threads int `json:"threads,omitzero"`
	FDs     int `json:"fds,omitzero"`
}

Usage is a plugin's resource cost, measured rather than self-reported.

This exists because a plugin is a process. A linked-in subsystem cannot be measured this way at any price — one process has one RSS and one CPU clock, and nothing inside it can be attributed to a subsystem. Running a service as its own process makes "which app is eating the memory" a question with an exact answer instead of an argument.

Zero means not measured (no /proc, or nothing running), not measured-as-zero.

type Wire added in v1.36.12

type Wire interface {
	MarshalZAP() ([]byte, error)
	UnmarshalZAP([]byte) error
}

Wire is implemented by a type that states its own ZAP wire form instead of having one derived from it.

The op-call plane carries In and Out as ZAP (see Call), and derives the layout from the Go type: fields take slots in declaration order, each aligned to its own width, and a field IS its offset. The derivation is cached; the COPY is not. Every call reflects over the value, sizes a builder by guess and walks the fields again — work whose answer was fixed the moment the type was declared.

type Started struct {
    Addr  string `json:"addr"  zap:"0"`
    Known bool   `json:"known" zap:"8"`
}

func (s *Started) MarshalZAP() ([]byte, error) { … ob.SetText(0, s.Addr) … }
func (s *Started) UnmarshalZAP(b []byte) error { … s.Addr = o.Text(0) … }

The two methods are what a hand-written ZAP codec is, with the offsets as constants. They are meant to be GENERATED from the same `zap:` tags a .zap schema is generated from, so one declaration answers for the wire, the schema and the code, and the derivation happens at build time where its cost is paid once.

Both methods are required together. A type carrying only one would encode from generated offsets and decode by reflection — two answers to where its layout lives, and the pair would disagree the first time a field moved.

It also carries what the derivation cannot express. A fixed-size array is refused outright by the reflective encoder, so an ids.ID — [32]byte, the bytes_fixed[32] of a .zap schema — crosses the plane on a codec or not at all. A type implementing Wire writes it inline (zap.ObjectBuilder.SetBytesFixed) and reads it back as a slice of the buffer that arrived (zap.Object.BytesFixed). Layouts writes that pair for a service's own types, and App.SDK writes it for the restatement a generated client holds.

The compatibility rule does not change: the layout is still the type, so reordering, inserting or retyping a field changes the wire for every peer. Append at the end, and only at the end.

Directories

Path Synopsis
cmd
zipc command
zipc projects an app, from a description of it rather than from a program.
zipc projects an app, from a description of it rather than from a program.
zipdoc command
Command zipdoc makes a doc comment the spec.
Command zipdoc makes a doc comment the spec.
zipgen command
Command zipgen generates native Go, Rust, and C++ SDKs, MCP servers, and @hanzo/docs.
Command zipgen generates native Go, Rust, and C++ SDKs, MCP servers, and @hanzo/docs.
corpus
info
What a node tells anyone who asks — the conformance corpus, in Go.
What a node tells anyone who asks — the conformance corpus, in Go.
examples
express-in-zip command
express-in-zip — the proof point: a legacy Express-shaped TypeScript handler running inside zip with ZERO rewrite.
express-in-zip — the proof point: a legacy Express-shaped TypeScript handler running inside zip with ZERO rewrite.
hello command
Hello, zip — minimal example.
Hello, zip — minimal example.
migrate-from-beego command
migrate-from-beego example — beego → zip migration via http.Handler adapter.
migrate-from-beego example — beego → zip migration via http.Handler adapter.
migrate-from-chi command
migrate-from-chi example — chi → zip via stdlib adapter.
migrate-from-chi example — chi → zip via stdlib adapter.
migrate-from-gin command
migrate-from-gin example — mechanical port of a gin-style API to zip.
migrate-from-gin example — mechanical port of a gin-style API to zip.
module-routes command
module-routes example — mount HIP-0105 extension modules as routes.
module-routes example — mount HIP-0105 extension modules as routes.
plugin/billing command
The plugin.
The plugin.
plugin/host command
The host.
The host.
sse-streaming command
sse-streaming example — Server-Sent Events via c.SendStreamWriter.
sse-streaming example — Server-Sent Events via c.SendStreamWriter.
subsystem-mount command
subsystem-mount example — HIP-0106 Mount(app, deps) idiom.
subsystem-mount example — HIP-0106 Mount(app, deps) idiom.
websocket command
websocket example — chat-style echo server.
websocket example — chat-style echo server.
zap-typed command
zap-typed example — typed handler with auto-generated OpenAPI spec.
zap-typed example — typed handler with auto-generated OpenAPI spec.
internal
addr
Package addr spells an address, once, for everything that reads one.
Package addr spells an address, once, for everything that reads one.
attribution/buckets
Package buckets writes registrations in every form zip offers, so a test can ask which package each one is filed under.
Package buckets writes registrations in every form zip offers, so a test can ask which package each one is filed under.
attribution/meter
Package meter composes handlers somewhere other than where they are registered, which is the shape that decides whether an op's prose is found.
Package meter composes handlers somewhere other than where they are registered, which is the shape that decides whether an op's prose is found.
jsonenc
Package jsonenc is zip's single JSON entry point.
Package jsonenc is zip's single JSON entry point.
jsontag
Package jsontag resolves a struct field's name on the wire.
Package jsontag resolves a struct field's name on the wire.
layout
Package layout holds the types that [zip.Layouts] is proved against.
Package layout holds the types that [zip.Layouts] is proved against.
o11y
Package o11y is the shape of what a zip app sends to the collector, and the one place that knows it.
Package o11y is the shape of what a zip app sends to the collector, and the one place that knows it.
sock
Package sock is the fleet's socket-path scheme, in one place.
Package sock is the fleet's socket-path scheme, in one place.
testplugin command
Command testplugin is a minimal zip plugin used by the reload tests.
Command testplugin is a minimal zip plugin used by the reload tests.
zapenc
Package zapenc encodes a Go value as a ZAP message and reads one back.
Package zapenc encodes a Go value as a ZAP message and reads one back.
zipdoc
Package zipdoc extracts the documentation a service has already written — the doc comments on its typed handlers and on the fields of their In/Out types — and emits the zip.Describe calls that carry it into the OpenAPI spec and the MCP tool list at run time.
Package zipdoc extracts the documentation a service has already written — the doc comments on its typed handlers and on the fields of their In/Out types — and emits the zip.Describe calls that carry it into the OpenAPI spec and the MCP tool list at run time.
esbuild.go wraps esbuild's pure-Go API (no CGO) so zip can compile TS / modern-JS handler source down to ES5 that the embedded goja VM executes.
esbuild.go wraps esbuild's pure-Go API (no CGO) so zip can compile TS / modern-JS handler source down to ES5 that the embedded goja VM executes.
Package middleware ships zip's canonical generic middleware stack.
Package middleware ships zip's canonical generic middleware stack.
Package wsx provides Fiber-v3-compatible WebSocket support via fasthttp/websocket.
Package wsx provides Fiber-v3-compatible WebSocket support via fasthttp/websocket.

Jump to

Keyboard shortcuts

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