zip

package module
v1.18.11 Latest Latest
Warning

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

Go to latest
Published: Jul 30, 2026 License: MIT Imports: 35 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"
	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 (
	OpsPortEnv = "OPS_PORT"
	OpsAddrEnv = "ZIP_OPS_ADDR"
)

OpsPortEnv names the ops listener's port. HIP-0119 §5 spells it OPS_PORT; OpsAddr also accepts a full address in ZIP_OPS_ADDR for a deployment that needs to bind one interface.

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 DefaultOpsPort = "9090"

DefaultOpsPort is the port HIP-0119 §1 names, stated once so a deployment's manifest and a local run agree. It is NOT a fallback: see OpsAddr.

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 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 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 OpsAddr added in v1.18.8

func OpsAddr() string

OpsAddr is the address this process's ops listener binds, or "" when this process does not own one:

$ZIP_OPS_ADDR   a full address, always wins
$OPS_PORT       a port, per HIP-0119 §5
otherwise       "" — this process owns no ops listener

There is deliberately NO fallback. A default would mean every process that calls Listen tries to bind 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 port is deployment configuration (HIP-0119 §5) and a deployment states it; a host does not set it on a child, so a child correctly owns none.

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

func New(cfg Config) *App

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

func (*App) Add added in v1.9.0

func (a *App) Add(services ...Service) error

Add composes services into the app in order, stopping at the first error and naming which position failed. Order matters exactly as much as it does for hand-written route registration: earlier routes are registered earlier, and specificity still decides which one answers.

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) 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. Use for one-off escape into Fiber-only APIs (rare). Prefer staying on the zip surface.

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.

func (*App) Group

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

Group creates a path-prefixed router group. The returned Router is the one way to register nested routes under a prefix — register leaves and further Groups directly on it; middleware scoped to the group goes on via its Use.

func (*App) Head

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

func (*App) Listen added in v1.1.0

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

Listen serves the app on one or more addresses and blocks until all listeners stop or the first one errors. The address scheme selects the transport; a bare address uses ZAP (DefaultScheme). This is the ONE and only way to serve a zip app — no per-transport methods.

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

func (a *App) Mount(prefix, addr string) error

Mount delegates every request under prefix to the service at addr. Listen serves here; Mount delegates there — one registry, one scheme vocabulary, opposite directions. A bare address uses ZAP (DefaultScheme), so a colocated service is mounted by address alone:

app.Mount("/v1/billing", "billing.hanzo.svc:9653")      // ZAP
app.Mount("/v1/legacy",  "http://legacy.internal:8080") // HTTP

The mounted service keeps the whole path — /v1/billing/invoices arrives as /v1/billing/invoices — so its routes ARE the surface, exactly as they are when it is linked in. Nothing is re-encoded in between: the inbound fasthttp request object is handed to the client and the reply is written straight back, so a mount costs a connection, not a copy.

This is how a service ships as its own binary. The mounting binary links zip and a transport, never the mounted service's dependency graph, so its link time does not grow when that service does, and the service redeploys without relinking it. Moving a service between Mount and a linked-in route tree is a deployment decision, not a code change.

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 root, with no prefix and no middleware wrapped around the handler.

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) Prepare added in v1.8.3

func (a *App) Prepare()

Prepare installs the deferred projections (the OpenAPI document and the MCP tool surface) without starting a listener, so a test can drive them through Fiber().Test exactly as a served app exposes them. Listen calls it too; both share one guard, so it runs at most once however it is reached.

func (*App) Put

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

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

Use registers zip-style middleware. Each Handler runs in order; calling c.Next() (via c.Continue) chains to the next handler.

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
}

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

	// 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 App.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.

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
}

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 WithStatus added in v1.18.2

func WithStatus(code 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.

A non-2xx is refused here, at declaration, rather than at request time. An error status comes from the error a handler returns (ErrNotFound and friends); letting a declaration state one too would be two places for one fact, free to disagree.

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
}

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

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

	// Fiber returns the underlying fiber.Router for one-off escape.
	Fiber() fiber.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.

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 Service added in v1.9.0

type Service func(*App) error

Service is a unit of functionality that attaches itself to an app: its routes, its middleware, its shutdown hooks. One type covers every way a unit can be composed, which is what makes where it runs a deployment decision instead of a code change:

app.Add(
    billing.New(deps),                                   // linked into this binary
    zip.Load("/v1/search", zip.Plugin{Name: "search",     // its own binary, embedded
        Bin: searchBin}),
    zip.Load("/v1/ml", zip.Plugin{Name: "ml",             // already running elsewhere
        Addr: os.Getenv("ML_ADDR")}),
)

A constructor that takes dependencies and returns a Service is the same thing curried — func(deps) Service — so a unit's dependencies are bound where they are known and the composition root only ever sees Service.

func Load added in v1.9.0

func Load(p Plugin, prefixes ...string) Service

Load returns the Service that makes p serve prefix. When p.Addr is set it is mounted 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.

It returns a Service rather than taking an *App so a plugin and a linked-in service have the SAME type, and composing either is the same line.

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.

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