Files
zip/docs/MODULE_HANDLERS.md
zeekayandGitHub 879ef2d2bb zip on Fiber v3 — canonical Hanzo Go framework (#1)
* wipe upstream zeekay/zip — rebuilding on Fiber v3

* core: zip on Fiber v3 — App, Ctx, Router, typed handlers, OpenAPI

zip wraps *fiber.App. Sinatra/Express idiom (app.Get/Post/...).
Generic zip.Get[I,O], zip.Post[I,O] etc. reflection-build OpenAPI 3.1
spec served at /.well-known/openapi.json + Swagger UI at /docs.

Ctx exposes Hanzo identity (Org/User/UserEmail/IsAdmin) from gateway
X-* headers per HIP-0026. luxfi/log per-request logger via c.Log().

Module(method+path, runtime, modulePath) mounts a HIP-0105 extension
as a Fiber route. Loader is duck-typed via zip/runtime.Loader so zip
does not pull hanzoai/base as a transitive dep — service binary
constructs *extruntime.Loader and injects via zip.Config.Loader.

go.mod: go 1.26.3 (forced by luxfi/log v1.4.3 floor; brief said
1.23.6 but luxfi canonical requires 1.26.x). Fiber v3.2.0, no major
bumps planned.

* middleware: Recover, RequestID, Logger, Timeout, MaxBody, CORS, Auth, RateLimit, Telemetry

Recover catches handler panics → luxfi/log + 500 JSON.
RequestID forwards X-Request-Id (or mints 16-byte hex).
Logger wraps each request with method/path/status/dur, enriches the
per-request luxfi/log Logger via c.SetLog().
Auth trusts gateway-minted X-* headers first; falls back to bearer
verification via injected AuthVerifier.
StripIdentityHeaders for non-gateway deployments.
RateLimit is per-org (or per-IP fallback) in-memory token bucket.
Telemetry plumbs to any O11ySink.

* adapters + wsx + zaprpc

AdaptNetHTTP/AdaptNetHTTPFunc/AdaptNetHTTPMiddleware via Fiber's
adaptor pkg. app.Mount(prefix, http.Handler) for legacy chi/gin/beego
migration. Each adapter doc'd as MIGRATION TOOL — costs ~5% vs native
Fiber dispatch.

wsx is fasthttp/websocket wrapped — wsx.Upgrade(fn) returns a
zip.Handler. Conn is *websocket.Conn from fasthttp/websocket; text/
binary/close/ping/pong message constants re-exported.

zaprpc ships Service + Registry + Dispatch. app.ZAPRegistry() returns
the per-App registry. app.ZAPListen(addr) reserves the port and logs
but the on-the-wire dispatcher is stubbed — full integration in a
follow-up PR pending zapc server type stabilization.

* examples + core tests

examples/
  hello/             — minimal Sinatra-style API
  zap-typed/         — generic typed handler + auto-OpenAPI + Swagger
  subsystem-mount/   — HIP-0106 Mount(*App, deps) idiom
  module-routes/     — app.Module() over a stub zip/runtime.Loader
  websocket/         — wsx.Upgrade echo server
  sse-streaming/     — c.SendStreamWriter Server-Sent Events
  migrate-from-gin/  — gin→zip mechanical port
  migrate-from-chi/  — chi→zip via AdaptNetHTTP
  migrate-from-beego/— beego→zip via AdaptNetHTTP

Verified live: hello binary serves /hello + /health 200s; zap-typed
serves /v1/validate + /.well-known/openapi.json with reflection-
derived schemas. Tests pass: 5 cases (basic routing, HTTPError, typed
validation, groups, identity headers).

* docs: README, HANZO_CHANGES, MIGRATION, MIDDLEWARE, MODULE_HANDLERS

README pitches zip as canonical Hanzo Go framework on Fiber v3.
HANZO_CHANGES documents the diff from upstream zeekay/zip — every
file replaced; only the name kept.
MIGRATION covers gin/chi/beego/net-http with per-route recipes.
MIDDLEWARE covers Recover/RequestID/Logger/Auth/etc.
MODULE_HANDLERS covers app.Module() over HIP-0105 with the wire
envelope and per-runtime guidance.

* ctx.Bind validates via reflection tags; tighten AuthVerifier interface

c.Bind/BindQuery/BindURI now run validate() after parse and return
*HTTPError(400) directly — handler code is one line:

	if err := c.Bind(&v); err != nil { return err }

AuthVerifier.Verify now takes context.Context (was an empty
interface{}). middleware import adjusted.

* module-routes: default Content-Type to application/json; add tests

Modules that don't set a Content-Type in their response envelope get
application/json; charset=utf-8 by default — the wire format is JSON
both directions.

Tests cover: happy path (POST /v1/eval echoed back), runtime mismatch
between caller and manifest, AllowedRuntimes refusal, nil Loader.
2026-05-18 23:36:43 -07:00

3.3 KiB

Module handlers (HIP-0105)

zip mounts HIP-0105 extension modules as routes via app.Module():

app.Module("POST /v1/policy/eval",  "wazero",   "./ext/policy")
app.Module("POST /v1/transform",    "pyvm",     "./ext/transform")
app.Module("POST /v1/webhook",      "goja",     "./ext/webhook")  // recommended for multi-tenant JS
app.Module("POST /v1/route",        "starlark", "./ext/route")

Each ./ext/<name> directory contains an extension.json manifest and the compiled artifact (e.g. validate.wasm or webhook.js). See HIP-0105 for the manifest spec and the calling conventions per runtime.

Wire format

zip builds one canonical JSON envelope per request and hands it to the guest. Same shape regardless of runtime:

{
  "method":    "POST",
  "path":      "/v1/policy/eval",
  "params":    {},
  "query":     {"q": "x"},
  "headers":   {"...": "..."},
  "body":      {"...arbitrary JSON..."},
  "org":       "hanzo",
  "user":      "z",
  "userEmail": "[email protected]"
}

The guest returns:

{
  "status":  200,
  "headers": {"X-Foo": "bar"},
  "body":    {"...response..."}
}

If the guest returns a bare JSON value (not the envelope shape), zip sends it as the response body with status 200.

Native runtimes

Per HIP-0105 the runtime names are:

Runtime Languages Sandbox Notes
native Go none compile-time linked; default for Hanzo-authored code
goja JS soft recommended for multi-tenant JS at scale (~9 KB/module)
wazero wasm hard language-agnostic (Rust/AS/TinyGo/Zig/C); pool default 4
pyvm Python soft full CPython incl. numpy/pandas; single-tenant only
starlark starlark soft config DSL
v8go JS hard experimental — not production (libv8 SIGSEGVs at scale)

AllowedRuntimes

For multi-tenant deployments, restrict which runtimes are accepted:

app := zip.New(zip.Config{
    Loader:          loader,
    AllowedRuntimes: []string{"goja", "wazero"},  // no pyvm, no v8go, no native
})

app.Module() returns an error if the manifest's runtime is not in the allowed list.

Loader injection

zip does NOT depend on hanzoai/base/plugins/extruntime. The zip/runtime.Loader interface is duck-typed:

type Loader interface {
    LoadDir(ctx context.Context, dir string) (map[string]Module, error)
    LoadOne(ctx context.Context, dir string) (Module, error)
    Runtimes() []string
}

A real service binary constructs *extruntime.Loader from hanzoai/base/plugins/extruntime with the runtimes it cares about (goja + wazero + pyvm + starlark + native), and passes it in:

loader := extruntime.NewLoader(
    nativevm.New(),
    gojavm.New(),
    wasmvm.New(),
    pyvm.New(),
)
app := zip.New(zip.Config{Loader: loader})

The duck-typed interface keeps zip lightweight (no transitive hanzoai/base dep) while letting the unified Hanzo binary wire every runtime once and share the loader across mounted subsystems.

Lifecycle

app.Module() loads the module at registration time. The module is closed on app.Shutdown() via an automatically-registered closer. There's no per-request load / hot-reload today; if a real consumer asks for it, the loader interface gains a Watch() method.