zip

package module
v1.23.0 Latest Latest
Warning

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

Go to latest
Published: Aug 3, 2026 License: MIT Imports: 36 Imported by: 0

README

zip

Docs: zip · part of the ZAP Protocol

The ZAP-native Go web framework. Built on Fiber v3 / fasthttp. A route is declared once, as a typed operation, and zip projects it into REST, OpenAPI, MCP, a CLI and a by-name call plane — with ZAP as the primary transport, HTTP a secondary view of the same routes.

zap-proto.io · Docs · fiber · Spec

ONE framework. ONE Listen verb. Operations declared once, served over every transport, projected into every interface.

package main

import (
    "context"
    "log"

    "github.com/zap-proto/zip"
    "github.com/zap-proto/zip/middleware"
)

// The In and Out types ARE the contract. Nothing below is written twice.
type GetUserIn struct {
    ID string `json:"id"` // binds from the :id path segment
}

type User struct {
    ID  string `json:"id"`
    Org string `json:"org"`
}

// GetUser returns one user, scoped to the caller's org.
func getUser(ctx context.Context, in *GetUserIn) (*User, error) {
    return &User{ID: in.ID, Org: zip.CallerOf(ctx).Org}, nil // gateway-minted identity
}

func main() {
    app := zip.New(zip.Config{})
    app.Use(middleware.Recover(), middleware.RequestID())

    // ONE typed op → a REST route, an OpenAPI operation, an MCP tool,
    // a `<service> <operation>` command, and a target for zip.Call.
    zip.Get(app, "/v1/users/:id", getUser)

    log.Fatal(app.Listen(":9653", "http://:8080")) // ZAP primary + HTTP extra, one verb
}

Install

go get github.com/zap-proto/zip

Module path github.com/zap-proto/zip. Requires Go 1.26+.

Features

  • Typed ops are the APIzip.Get/Post/Put/Patch/Delete[In, Out](on, path, fn) is how a route is declared. It registers ONE operation; every interface below is derived from it, and op.invoke (decode → validate → authorize → run) is the one handler core all of them share. The In/Out types are the contract, so the document, the tool, the command and the client cannot drift from the code — there is nowhere for them to drift to.

    on is the App, any Group of it, or app.With(mw…) — a group's prefix is part of the op and With's middleware wraps its handler, so structuring the router costs nothing in schema:

    v1 := app.Group("/v1")
    zip.Get(v1, "/users/:id", getUser)               // one op at /v1/users/:id
    zip.Post(app.With(RateLimit), "/v1/keys", mint)  // gated, and still one op
    
  • Untyped routes are the escape hatch, and they cost every projectionapp.Get(path, func(c *zip.Ctx) error) registers a route and no operation. The endpoint is then in no OpenAPI document, is no MCP tool, has no command, and no service can reach it with zip.Call; it is reachable only by someone who already knows the URL. That is the right trade when the response is something a schema cannot describe — an SSE stream, a protocol upgrade, a proxied byte range, a non-JSON body — and the wrong one for everything else. If you can name what goes in and what comes out, declare it.

  • Transport is a value, not a method — one verb, app.Listen(addrs...), and the address scheme selects the transport (mirrors net.Listen):

    app.Listen(":9653")                  // ZAP (bare addr = the primary)
    app.Listen(":9653", "http://:8080")  // ZAP + HTTP in one call
    app.Listen("http://:8080")           // HTTP only
    app.Listen("/run/hanzo/app.sock")    // ZAP on a unix socket
    app.Listen("quic://:443")            // any RegisterTransport'd protocol
    

    ZAP (TLS 1.3 + post-quantum) is the default; HTTP is built in; zip.RegisterTransport(scheme, zip.Transport{Serve, Dial}) adds any future protocol with zero change to Listen or Mount. The scheme names the protocol and the address names where it is spoken — a path is a unix socket, a host:port is TCP — so one wire never needs two schemes.

  • Composition is one verbService is func(*App) error: a unit that attaches its own routes, middleware and shutdown hooks. A constructor taking dependencies and returning one is the same thing curried, so a composition root only ever sees Service.

    app.Add(billing.New(deps), search.New(deps))
    
  • Plugins — a service builds as its own binary and loads at run time — because a plugin is also just a Service, where it runs is a deployment decision rather than a code change:

    //go:embed bin/billing
    var billingBin []byte
    
    app.Add(
        billing.New(deps),                                                     // linked in
        zip.Load("/v1/billing", zip.Plugin{Name: "billing", Bin: billingBin}), // its own binary
        zip.Load("/v1/ml",      zip.Plugin{Name: "ml", Addr: mlAddr}),         // already running
    )
    

    A plugin is an ordinary zip app — no SDK, no schema — started on its own unix socket and reached over ZAP. The host links zip and a transport, never a plugin's dependency graph, so its link time doesn't grow when a plugin does, plugins build in parallel, and go:embed keeps the deployment a single artifact. app.Reload(name, bin) swaps a running plugin for a new build without dropping a request: the replacement must be listening before any traffic moves to it, so a bad build can't take the route down, and routes register once and resolve their target per request, so repeated reloads stay flat in memory. app.Mount(prefix, addr) is the same delegation without the process management.

  • One registry, five projections — one zip.Get[In, Out](app, path, fn) becomes a REST route, an OpenAPI 3.1 doc (/.well-known/openapi.json, Swagger UI at /docs), a Model Context Protocol tool at /mcp (JSON-RPC 2.0), a command line (app.CLI(), or zip.CommandsFromSpec for a client that links none of the service), and a by-name call plane other services reach with zip.Call. Same schema, same handler, one operation id addressing all five. Because each is an ordinary route, they ride every transport Listen was given — ZAP-native MCP is automatic. On by default; Config.MCP.Disabled to suppress.

                          ┌── REST route          method + path
                          ├── OpenAPI 3.1         operationId
    zip.Get[In,Out] ──op──┼── MCP tool            operationId
                          ├── CLI command         operationId
                          └── zip.Call plane      operationId
    
  • Services call services without linkingzip.DialApp("flags") opens a ZAP connection over that app's canonical unix socket ($ZIP_RUNTIME_DIR/flags.sock) and zip.Call[In, Out](ctx, c, "flags_bool", &in) invokes one op, typed both ways, with the callee's *HTTPError intact on failure. No import of the callee's package, no hand-written client, no generated one to drift. Identity is the gateway's headers forwarded (c.Forward()) plus the kernel's SO_PEERCRED view of the calling process (zip.PeerOf(ctx)) — nothing the caller can forge.

  • Precedence is a property of the pattern — routing comes from the zap-proto/fiber fork: the most specific pattern wins regardless of registration order (static ≻ :param ≻ *), and ambiguous equal-specificity overlaps panic at startup instead of silently shadowing.

  • Identity built-inc.Org() / c.User() / c.UserEmail() / c.IsAdmin() read JWT-validated X-* headers set by the gateway; handlers never parse tokens.

  • MiddlewareRecover, RequestID, Logger, Timeout, MaxBody, CORS, RateLimit, Telemetry, Breaker in zip/middleware.

  • WebSocket & streamingwsx.Upgrade(fn) over fasthttp/websocket; c.SendStreamWriter for SSE / chunked responses.

  • Extension routesapp.Module("POST /v1/eval", "wasm", "./policy") mounts a sandboxed extension (wasm / goja / pyvm / starlark / v8go / native) as a route.

  • Embedded JS/TS runtime — run a (req, res)-shaped JS/TS handler in-process via goja (pure Go, no CGO); esbuild transpiles TS ahead of it, for incremental migration to native Go.

  • Drop-in migrationapp.All("/legacy/*", zip.AdaptNetHTTP(h)) fronts any http.Handler as one wildcard route; it obeys the same precedence, so a native route added later still wins.

  • Stdlib JSON only — every JSON path goes through one internal helper backed by encoding/json/v2 when built with GOEXPERIMENT=jsonv2 (Go 1.25+), else encoding/json. No third-party JSON library.

Documentation

The full guide — Ctx reference, the route-precedence contract, middleware, extension-runtime mounts, and versioning — is at zap-proto.dev/docs/zip. Runnable examples live in examples/.

Start with examples/hello (two typed ops, four projections) and examples/zap-typed. examples/sse-streaming and examples/websocket are the two that stay untyped, and each says why at the top of the file — a stream and an upgrade have no single value for an Out to be. The migrate-from-* examples show the port as two steps: mechanical first, typed second, because stopping after the first leaves you with exactly the surface you were migrating away from.

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 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 codec.

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 JSONVariant = jsonenc.Variant

JSONVariant reports which JSON implementation zip is using in this build — "encoding/json/v2" when compiled with GOEXPERIMENT=jsonv2, "encoding/json" otherwise. Exposed for cmd/cloud startup logs and for tests that need to assert the variant. Per HIP-0106 the wire stack is "JSON only at edge, ZAP between services"; this constant tells operators which JSON impl is on the edge.

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 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 = "ZIP_RUNTIME_DIR"

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

Variables

This section is empty.

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 Alias added in v1.18.22

func Alias(reg func(string, ...Handler) Router, canonical, legacy string, h Handler)

Alias registers ONE handler at TWO addresses — the canonical one and a legacy spelling kept reachable for consumers pinned to it.

A path segment names a THING; the HTTP method says what is being done to it. The verb-noun addresses a service inherits (`send-verification-code`, `set-preferred-mfa`, …) say the verb twice, and they are what a customer reads in a CLI's help, in every generated SDK method name and on every docs page. The canonical noun is what the published document leads with; the legacy spelling stays reachable so nothing breaks while consumers move.

One handler VALUE, two addresses: there is no second implementation to keep in step, and no forward that could answer differently from the thing it forwards to. When the last pinned consumer moves, the legacy half is deleted and nothing else changes.

It lives here rather than in each service because cmd/zipdoc has to recognise it. A registration made inside a helper is invisible to a pass that reads router.Get(path, handler) calls, so a service that rolled its own alias helper silently lost the prose for BOTH addresses — the exact defect this package exists to prevent. Being zip's, both halves carry the handler's doc comment.

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 (zaphttp: 30s). It is not cancellable mid-flight, because abandoning a call would mean abandoning the pooled buffers it is writing into.

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 Delete

func Delete[In, Out any](on OpTarget, path string, fn TypedHandler[In, Out], opts ...OpOption)

Delete registers a DELETE typed handler at path. A DELETE addresses what it deletes with its URL and carries no request body — see [hasBody].

func Describe added in v1.17.0

func Describe(methodPath string, d Doc)

Describe records documentation for one operation. 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 Get

func Get[In, Out any](on OpTarget, path string, fn TypedHandler[In, Out], opts ...OpOption)

Get registers a GET typed handler at path, on the App or on any Router of it — a Group's prefix is part of the op's path, so a group-structured app declares typed ops without spelling its prefix out per route.

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 Patch

func Patch[In, Out any](on OpTarget, path string, fn TypedHandler[In, Out], opts ...OpOption)

Patch registers a PATCH typed handler at path.

func Post

func Post[In, Out any](on OpTarget, path string, fn TypedHandler[In, Out], opts ...OpOption)

Post registers a POST typed handler at path.

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 Put

func Put[In, Out any](on OpTarget, path string, fn TypedHandler[In, Out], opts ...OpOption)

Put registers a PUT typed handler at path.

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 Mount with ZERO change to either API. Call before Listen or Mount.

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 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 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 Mount added in v1.19.0

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

A remote service is a LEAF, not a verb.

Mount 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 mount 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. Mount returns another service, running elsewhere, as a DEFINITION the host composes with Use like any other:

ledger, err := zip.Mount("/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.

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 New

func New(cfg Config) *App

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

func (*App) All

func (a *App) All(path string, handlers ...Handler) Router

All registers a handler for any HTTP method.

func (*App) Authorize added in v1.8.3

func (a *App) Authorize(fn Authorizer)

Authorize installs fn as the op-invoke authorization hook. 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 gates on so the value it authorizes is the value the handler binds. Call once while mounting, before Listen. A nil fn clears it (every decoded request then runs unauthorized).

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) 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) Delete

func (a *App) Delete(path string, handlers ...Handler) Router

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) 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) 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(path string, handlers ...Handler) Router

Get / Post / Put / Patch / Delete / Head / Options / All register routes. Chains are in wrapping order: middleware first, the final handler last.

These still take ...Handler and always will. A bare closure written inline is a *Handler by conversion, so nothing about route registration changes when composition widens — zip.H is needed only at App.Use.

func (*App) Group

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

Group returns a NEW App, included in a at prefix.

A group is not a third kind of thing. It is an App with a prefix, referenced from the parent's program like any other — which is why "a scope" and "a sub-application" are one mechanism here and two in every framework that grew them separately. Middleware handed to Group, or added to the returned App later, is scoped to it (see App.Use on snapshot semantics).

One definition at two prefixes is two Group calls, so the prefix is visible at each inclusion site rather than hidden inside a reference type:

app.Group("/v1").Use(billing)
app.Group("/admin").Use(billing)

The return type is Router, not *App, even though what comes back IS an *App. Go has no return-type covariance, so a concrete return here forces EVERY implementor to hand back an *App — and a decorator's group must stay decorated, which means returning itself around the group, not the bare group. v1.18 had this right; narrowing it in v1.19 is what made hanzoai/commerce's mintRouter and hanzoai/cloud's scope unimplementable, the same way `Fiber() *fiber.App` did. An abstraction that only its own package can implement is not one.

func (*App) Head

func (a *App) Head(path string, handlers ...Handler) Router

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) 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) 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) 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) OpScope added in v1.18.1

func (a *App) OpScope() OpScope

OpScope makes the App itself a place a typed op can be declared. The prefix is not reported here: a group's prefix is a property of WHERE the group is included, and one definition may be included in two places, so the absolute path is computed by the walk and never baked into the op.

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) Options

func (a *App) Options(path string, handlers ...Handler) Router

func (*App) Patch

func (a *App) Patch(path string, handlers ...Handler) Router

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(path string, handlers ...Handler) Router

func (*App) Put

func (a *App) Put(path string, handlers ...Handler) Router

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 Graft 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) 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 ...fiber.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.

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) 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) Router

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) Router

With returns a Router whose subsequent leaf registrations (Get/Post/…/All) 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 Router still obey specificity precedence exactly like any other route.

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

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) 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.

Returning a non-nil error aborts the op before the handler runs, and that error is the response — return a zip.Err* (e.g. ErrForbidden) 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. *zaphttp.Transport and *fasthttp.HostClient already satisfy it, so giving a scheme a Dial is a one-liner.

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.

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

	// ErrorHandler is the catch-all error handler. Defaults to zip.errorHandler
	// which renders {error, code, status} JSON.
	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

	// 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
}

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 Mount — 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 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.

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 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 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 HTTPError

type HTTPError struct {
	Status int    `json:"status"`
	Code   string `json:"code,omitempty"`
	Msg    string `json:"error"`
}

HTTPError is the canonical error type zip understands. Returning one causes the error handler to send a JSON {error, code, status} body.

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 ErrUnauthorized

func ErrUnauthorized(msg string) *HTTPError

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

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; Mount does not do this. Mount 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. Mount 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). Nothing else — no compression, no byte ranges, 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 ANSWERS an address, and returns it.

A Handler may never terminate a request — that is the rule the three node kinds encode, and the reason Use takes handlers that WRAP while an address is something only a route method can say. 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 Mount.

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 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 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 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 mount path (default "/mcp").
	Path 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
}

MCPConfig configures the auto-derived MCP surface.

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 is the stable identity of a registered typed handler, handed to an Authorizer so the decision can key on the operation as well as the input. OperationID is the resolved id the OpenAPI document and the MCP tool surface share — the explicit WithOperationID, else the method+path default.

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 standard {status, code, error} envelope every client already parses.

The difference is which body reaches the wire. An error returns the envelope; 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 it cannot get from the envelope — 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 OpScope added in v1.18.1

type OpScope struct {
	// App owns the op registry. Every op ends up on exactly one.
	App *App

	// Prefix is prepended to the op's path, as a Group's prefix is prepended to
	// an ordinary route's.
	Prefix string

	// Middleware wraps the op's handler. nil means none — the common case, and
	// the one that costs nothing.
	Middleware Middleware
}

OpScope is where an op declared on a router lands: the App whose registry holds it, the path prefix its route sits under, and the middleware composed around its handler.

type OpTarget added in v1.18.0

type OpTarget interface {
	OpScope() OpScope
}

OpTarget is a place a typed op can be declared: the App, or any Router of it — a Group, or the result of With. Get and friends take one of these, so `zip.Get(app, …)` and `zip.Get(v1, …)` are the same declaration with the same meaning, and a group-structured app does not spell its prefix out per route to have typed ops.

Every Router is one, so a Router that DECORATES another must implement it, and must implement it faithfully. A decorator that gates the routes it registers has to return that gate in Middleware, or a typed op declared through it is registered ungated — the decorator's whole purpose, silently skipped. Embedding the wrapped Router and overriding this method is the shape: the embedded one answers for everything else.

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 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) 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

	// 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 Mount 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 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 [Mount] 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 Router

type Router interface {
	// Every Router is somewhere a typed op can be declared, so `zip.Get(v1, …)`
	// takes a Group as readily as it takes the App.
	OpTarget

	// Use is the ONE composition verb, so it takes a [Component] — middleware,
	// or another App included by reference. It is the only signature that
	// widened; every route method below still takes ...Handler.
	Use(cs ...Component) Router

	// Route registration takes ONE chain in wrapping order: zero or more
	// middleware first, the final handler LAST. fiber wants handler-first;
	// splitChain flips it in exactly one place.
	Get(path string, handlers ...Handler) Router
	Post(path string, handlers ...Handler) Router
	Put(path string, handlers ...Handler) Router
	Patch(path string, handlers ...Handler) Router
	Delete(path string, handlers ...Handler) Router
	Head(path string, handlers ...Handler) Router
	Options(path string, handlers ...Handler) Router
	All(path string, handlers ...Handler) Router

	// Group creates a group and returns it as a Router. A group IS an app with a
	// prefix — the same definition kind, included by reference like any other —
	// and one mechanism covers "a scope" and "a sub-application" where
	// frameworks that grew them separately have two. What it must NOT do is
	// promise the concrete *App: a decorator's group has to stay decorated, so
	// it returns itself around the group. See [App.Group].
	Group(prefix string, handlers ...Handler) Router
}

Router is the path-mounting surface shared by *App and Group. All concrete routes flow through toFiberHandler — fiber.Ctx is the underlying type the framework's users never see directly.

That last sentence is why nothing here names fiber. This interface used to carry `Fiber() *fiber.App` "for one-off escape", and it cost far more than it bought: a Router is the type a DECORATOR implements — the whole point of OpTarget's note above — and a decorator wraps something, so it has no *fiber.App of its own to return. Requiring one made the interface unimplementable outside this package. Every real decorator either delegated the method blindly (zip's own wrapRouter did exactly that) or could not satisfy it at all, which is what stalled the v1.19 adoption in hanzoai/cloud and hanzoai/commerce.

The escape hatch itself was never the problem; putting it on the ABSTRACTION was. It lives on the concrete App — see App.Fiber — where a caller that genuinely needs the underlying router still reaches it, and where wanting it forces you to hold an *App rather than quietly widening every Router in the estate. Prefer App.Routes and App.Test, which serve the two things callers actually reached through Fiber() for — enumerating addresses and driving a request — and which do it through the program rather than through whichever generation's router happened to be current.

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 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"`

	// 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 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 Mount each say which they needed.

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.

Directories

Path Synopsis
cmd
zipdoc command
Command zipdoc makes a doc comment the spec.
Command zipdoc makes a doc comment the spec.
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
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.
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