HIP-0137 "One License" (hanzoai/hips, HIPs/hip-0137-one-license.md) puts BSD-3-Clause out of scope for hanzoai: original Hanzo work takes the standard MIT OR Apache-2.0 dual licence, and only forks keep an upstream licence. LICENSE is now the dual declaration; LICENSE-APACHE and LICENSE-MIT carry the two texts beside it. The existing copyright holder and years are preserved verbatim. package.json and the root entry of package-lock.json follow; dependency entries in the lockfile keep their own licences. NOTICE is unchanged and stays accurate — it credits esbuild, commander and TypeScript as build-time dependencies, none of which is redistributed here or constrains these terms. The v0.1.0 line in HANZO_CHANGES.md records what was true at that release and is left as history; a new entry above it records this change. Co-authored-by: Hanzo Dev <[email protected]>
5.4 KiB
@hanzo/zip
Write zip-mounted handlers in idiomatic TypeScript.
@hanzo/zip hides the HIP-0105 calling convention behind a handler()
wrapper so your code reads as a typed, ordinary function. The build
step compiles your TypeScript into a single bundled JS file that any
HIP-0105 JS host (goja today, the wasm path later) can load.
import { handler, ZipError } from "@hanzo/zip";
interface ValidateRequest {
email: string;
age: number;
}
interface ValidateResponse {
ok: true;
email: string;
age: number;
}
export const validate = handler<ValidateRequest, ValidateResponse>((req) => {
if (!req.email) {
throw new ZipError("email required", 400);
}
return {
ok: true,
email: req.email.trim().toLowerCase(),
age: req.age,
};
});
Build:
npx zip-js build
# -> writes validate.js next to extension.json
Drop the resulting .js file plus extension.json into any zip- or
Base-mounted handler directory. The goja runtime picks it up.
Quickstart
mkdir my-handler && cd my-handler
npm init -y
npm install @hanzo/zip
Create extension.json:
{
"name": "my-handler",
"version": "0.1.0",
"runtime": "goja",
"module": "my-handler.js",
"exports": ["validate"]
}
Create src/index.ts with a handler(...) export named validate,
then:
npx zip-js build
That's the whole loop.
Examples
Three end-to-end examples in examples/:
| Example | Purpose | Built size (minified) |
|---|---|---|
hello |
minimal name -> greeting |
1.5 KB |
validate-email |
port of the canonical validate fixture (byte-equivalent in the V8 host) | 1.9 KB |
policy-eval |
realistic IAM authz decision handler | 2.0 KB |
Build all:
npm install
npm run build
npm run examples
npm run verify
npm run verify loads each built bundle into a fresh V8 context the
same way the production goja host does, calls the exported globals,
and compares the JSON output bytes against the canonical reference
strings.
Cross-host byte equivalence
For the canonical validate-email fixture the output bytes are
identical across:
base/plugins/extbench/fixtures/goja-js/validate.js(plain JS reference)examples/validate-email/validate.js(this SDK's output)
Verified in a V8 context (Node's vm) per the verify-examples harness.
A note on the goja host: when the production gojavm runtime invokes a
JS handler that returns a plain object, the result is serialized via
Go's encoding/json which marshals map keys in alphabetical order. The
values are identical; the key order depends on the host. The
pre-formed string fixtures (AssemblyScript, Rust) emit ok before
error because they hand-build the JSON; an object-returning goja
handler will emit error before ok. The wire bytes are
host-dependent. Application code that depends on key order is buggy on
its face — every JSON parser is required to accept any order. Verified
end-to-end against the production gojavm runtime.
Public API
import { handler, ZipError, type Manifest } from "@hanzo/zip";
import type { RouteEnvelope, RouteResponse } from "@hanzo/zip";
| Export | Purpose |
|---|---|
handler<I, O>(fn) |
Wrap a typed (req: I) => O function into the wire shape the host calls. |
ZipError(msg, status?) |
Throw to produce a canonical {ok:false,error:msg} envelope. The optional status is honored when the handler is route-mounted via app.Module(). |
errorEnvelope(msg) |
Build the error envelope without throwing. |
Manifest |
Type for extension.json. |
RouteEnvelope<TBody> |
Wire shape passed to a route-mounted handler. |
RouteResponse<TBody> |
Wire shape a route-mounted handler may return. |
CLI
zip-js build [dir] # default dir = .
-e, --entry <path> entry TS file (default src/index.ts)
-o, --out <path> output file (default from manifest.module)
--no-minify do not minify (default minified)
Constraints on handler()
| Rule | Reason |
|---|---|
| Function MUST be synchronous | HIP-0105 invocations are synchronous. Async work belongs in HIP-0060 (FaaS), not in-process extensions. Returning a Promise produces a clear runtime error. |
| Function takes exactly one parameter | The host hands one parsed JSON payload. Multiple parameters are silently dropped. |
Errors should be ZipError |
Plain Error and thrown strings work but ZipError carries status for route mounts. |
Build pipeline
zip-js build:
- Reads
extension.json(validatesname,version,runtime,exports). - Runs esbuild on
src/index.tsas a single IIFE bundle withglobalName: "__zip". - Appends a runtime trailer (
globalThis.<fn> = __zip.<fn>for goja). - Writes the result to
manifest.module.
The output is a single-file bundle with no external dependencies
beyond ES2020 globals (which goja supports). No import, no require,
no Node built-ins. If your handler depends on a third-party npm
package, it gets bundled inline.
Sibling SDKs
@hanzo/zip is one of a family of language SDKs targeting the
HIP-0105 ABI:
zip-rs(Rust → wasm)@hanzo/zip(TypeScript → goja/wasm) — this repozip-as(AssemblyScript → wasm) — reference
All three produce artifacts the Hanzo Base / zip host can load via a
manifest-only runtime switch.
License
MIT OR Apache-2.0, at your option — per HIP-0137. See LICENSE.
Upstream credits in NOTICE.