Files
zeekay 060ee4ab6e docs: add interface/method to the .zap DSL grammar
FIX 4 (spec side) — document the `interface` service grammar alongside
`struct`. Each method takes at most one struct request and, after `returns`,
at most one struct response (either may be empty). Method ordinals are
auto-assigned 1, 2, 3, … in declaration order and are stable for the life of
the interface — appending never renumbers existing methods. Codegen emits a
typed client + abstract server + ordinal table per interface; struct emission
is unchanged (purely additive).

Also point the TypeScript codegen row at the now-shipping `zapgen` bin of
@zap-proto/zap (`npx zapgen schema.zap`).
2026-06-16 11:12:42 -07:00

5.2 KiB

zap-proto/zap-spec

Canonical, language-agnostic schemas for the ZAP wire format.

This repository defines what a ZAP message is. The language runtimes (github.com/zap-proto/go, github.com/zap-proto/zap-es, and future Rust / Swift / Kotlin runtimes) implement the format described here.

What is ZAP?

ZAP is a fixed-layout, zero-copy binary wire format. Each struct has a fixed-size header where every field sits at a compile-time offset; variable- length payloads (bytes, text, lists, nested structs) are referenced from the header by a (offset, length) pointer pair into the same message. Readers parse in O(1) — no scanning, no allocation.

Primitives

Type Size in fixed area Notes
bool 1 0 = false, 1 = true
u8 / i8 1
u16 / i16 2 little-endian
u32 / i32 4 little-endian
u64 / i64 8 little-endian
f32 / f64 4 / 8 IEEE 754
bytes 8 (offset:u32, length:u32) — payload in heap area
text 8 UTF-8, same layout as bytes
bytes_fixed[N] N inline, no indirection
list<T> 8 (offset:u32, length:u32) — payload in heap area
nested struct 4 offset:u32 — pointed-to header in heap area

Discriminator

Every ZAP message on the wire begins with a 2-byte discriminator:

+--------+--------+----------------- ... -------------------+
| TypeK  | ShapeK |  fixed header  | heap area (var-length) |
+--------+--------+----------------- ... -------------------+

TypeKind (1 byte) names a registered struct family (e.g. 0xC0 = capability). ShapeKind (1 byte) names the specific struct within that family (e.g. 0x01 = Capability, 0x02 = Revocation). The combination uniquely identifies the schema the bytes that follow conform to. The envelope writes / reads this discriminator; the body's first byte is at offset 2 of the message.

.zap DSL

Schemas are authored in a tiny DSL that the codegen tools consume directly.

package <ident>

type <name> = <primitive-or-bytes_fixed[N]>      # type alias

struct <Name> {
    <FieldName>  <type>  @<offset>               # offset in bytes from start
    ...
}

interface <Name> {
    <method>(<param>: <Struct>) returns (<param>: <Struct>)   # request + response
    <method>() returns ()                                     # no payloads
    <method>(<param>: <Struct>)                               # request, no response
    ...
}

Supported types in field position: every primitive above, plus list<T>, bytes_fixed[N], type aliases, and references to other structs by name. Comments are # to end-of-line. See capabilities.zap in this repo for a production example.

Interfaces (services)

An interface declares an RPC service: a named set of methods. Each method takes at most one struct payload as its request and, after returns, at most one struct payload as its response — either may be empty (()). Method ordinals are auto-assigned 1, 2, 3, … in declaration order; they are the integers carried on the wire envelope's method field. Appending a method never renumbers the existing ones, so the wire ordinal of an existing method is stable for the life of the interface (reordering or removing a method is a breaking change — declare a new interface instead).

Codegen emits, per interface, a typed client, an abstract server with ordinal-routed dispatch, and a method-ordinal table — over the host runtime's ZAP call envelope. Struct emission is unchanged; interface is purely additive.

Codegen tools

Each language runtime ships a codegen that consumes .zap files from this repo:

  • Go: github.com/zap-proto/go/cmd/zapgen (zapgen schema.zap > schema.go)
  • TypeScript: the zapgen bin of @zap-proto/zap (npx zapgen schema.zap)
  • Rust: github.com/zap-proto/zap-rs/cmd/zapgen-rs (forthcoming)

The DSL grammar is identical across all runtimes; the codegen emits idiomatic accessors for the host language while preserving the exact byte layout.

What lives here

File Purpose
capabilities.zap Capability schema v1.0 (object-cap auth)
capabilities_kinds.md CapKind + CaveatKind + Permission enums
SPEC.md Capability semantics, threat model, wire policy

Add new message families as <family>.zap + <family>_kinds.md.

Versioning

The schemas here are frozen at v1.0 once tagged. Any wire change requires a new struct name (e.g. Capability2) and a new ShapeKind allocation. Codegen consumers pin to a tag in their build.