Reference

Protocol API reference

The @aventara/core/protocol entry point - AvProtocol.bind and its members, request and response types, headers, httpStatus, scalarFormats, isOperationResponse and normalizeEntrypoint.

Read as Markdown

@aventara/core/protocol is Aventara's HTTP protocol as passive data and pure functions. It mounts nothing and imports no HTTP library; a host adapts its server to it. @aventara/nest is such a host. Use this entry point to write another one (Hono, Koa, a serverless handler), or to test the protocol in process. For the wire format itself see HTTP protocol.

Code
import { AvProtocol } from "@aventara/core/protocol";
import type { AvProtocolRequest, AvProtocolResponse, AvProtocolBinding } from "@aventara/core/protocol";

The main @aventara/core entry exports no protocol names. AvProtocol is a namespace with exactly these members (verified): bind, headers, httpStatus, isOperationResponse, normalizeEntrypoint, scalarFormats.

Types

AvProtocolRequest

What a host hands the protocol. path is relative to the api mount path (/_contract, /_resources/User/find/many).

Code
type AvProtocolRequest = {
  readonly method: string;
  readonly path: string;
  readonly query?: string;   // the URL's query without its "?", as received; read only by a cached read's GET
  readonly headers: Readonly<Record<string, string | readonly string[] | undefined>>;
  readonly body:
    | { readonly kind: "raw"; readonly content: string | Uint8Array }   // the protocol enforces size, content type and JSON
    | { readonly kind: "parsed"; readonly value: unknown };            // your parser already did
};

Header names are matched case-insensitively. A query string is not part of path: pass it as query. Only a cached read's GET reads it (q and v); every other route ignores it. "" is the same as no query.

AvProtocolResponse

Code
type AvProtocolResponse = {
  readonly status: number;
  readonly headers: Readonly<Record<string, string>>;
  readonly body: string;     // a JSON string; "" for 304
};

Every framework response carries Content-Type: application/json (not on 304) and Aventara-Request-Id (not on a public cached read). A 405 adds Allow. A contract response adds ETag and Cache-Control: no-cache. Every answer to a GET adds X-Content-Type-Options: nosniff; a cached read's adds its caching headers (HTTP protocol).

AvProtocol.bind(framework, options?)

Code
const protocol: AvProtocolBinding = AvProtocol.bind(framework);   // once, at startup
const agents = AvProtocol.bind(framework, { origin: "agent" });   // for a host that serves AI agents

bind takes the AvFramework from createFramework and returns a binding derived from its client contract. With { origin: "agent" } the binding is derived from the agent contract instead: surface() lists the agent's routes, /_contract serves the agent contract, the identity headers must carry the agent contract's hash (the client's is 409 A2005), and every request it decodes runs as the agent, under the agent layer's pipelines. The request and response shapes are the same. An MCP server uses this binding.

Code
type AvProtocolBinding = {
  surface():                 readonly SurfaceRoute[];
  handleOperation(request):  Promise<AvProtocolResponse>;
  handleTransaction(request): Promise<AvProtocolResponse>;
  handleAll(request, options?: { state?; signal?: AbortSignal }):
    Promise<AvProtocolResponse | { status: 200; headers: Record<string, string>; body: AsyncIterable<string> }>;
  encodeContract(request):   AvProtocolResponse;
  answerAbsent(request):     AvProtocolResponse | undefined;
  decodeOperation(request):  DecodedOperation;
  decodeTransaction(request): DecodedTransaction;
  encodeOperation(response, requestId, decoded?): AvProtocolResponse;
  encodeTransaction(response, requestId): AvProtocolResponse;
  failure: /* transport-level failure builder, below */;
};
MemberUse it toNotes
surface()List the routes to mount.Each entry is { kind: "operation", method: "POST", path: "/_resources/<Resource>/<family>/<variant>", resource, family, variant } (a read with a cache stage has a second one with method: "GET", right after it), then { kind: "contract", method: "GET", path: "/_contract" }, then { kind: "transactions", method: "POST", path: "/_transactions" } only when the client contract is interactive, and { kind: "all", method: "POST", path: "/_all" } whenever an operation is advertised. Only advertised operations appear, so a hidden Resource has no route. Paths are relative to the api mount path. Frozen.
handleOperation(request)Decode, execute and encode one operation request, a cached read's GET included.Resolves to a response; never rejects.
handleTransaction(request)The same for /_transactions.
handleAll(request, { signal })The same for /_all (Operation Sets).body is a string, or, when the request sent Accept: application/x-ndjson, an async iterable of lines: write each as it comes. Abort signal when the client disconnects: operations not yet started never start.
encodeContract(request)Answer GET /_contract.Synchronous. Handles If-None-Match: a matching ETag answers 304 with no body.
answerAbsent(request)Answer any other path under the protocol's _ namespace (400 A2000 for an unknown /_* path, 405 with Allow for a wrong method on a protocol route).Returns undefined for a path outside the namespace; that one is yours to answer.
decodeOperation(request) / decodeTransaction(request)Decode a POST without executing (a GET is 405 here; a cached read goes to handleOperation).{ ok: true, request, requestId } | { ok: false, response, requestId }. For hosts that run their own steps between decoding and execution. request is an AvClientOperationRequest | AvAgentOperationRequest (AvClientTransactionRequest | AvAgentTransactionRequest for decodeTransaction), exported from @aventara/core; pass it to framework.execute / framework.executeTransaction.
encodeOperation(response, requestId, decoded?) / encodeTransaction(response, requestId)Encode an AvOperationResponse / AvTransactionResponse you obtained yourself (for example from framework.execute).
failure(code, requestId?, allow?)Build a complete response for a transport-level failure your host detects itself.Codes: "A2000", "A2010", "A2011" and "A2012" (which requires allow, the permitted methods).

Verified, in process:

Code
const protocol = AvProtocol.bind(framework);
protocol.surface().length;          // 48 = 45 operations + _contract + _transactions + _all
await protocol.handleOperation({
  method: "POST",
  path: "/_resources/User/find/count",
  headers: { "content-type": "application/json", "aventara-protocol-version": "1", "aventara-contract-hash": hash },
  body: { kind: "raw", content: "{}" },
});
// { status: 200, headers: { "Content-Type": "application/json", "Aventara-Request-Id": "26253298-..." }, body: '{"data":10,"code":"A1000","cause":null}' }

protocol.encodeContract({ method: "GET", path: "/_contract", headers: { "if-none-match": `"${hash}"` }, body: { kind: "raw", content: "" } });
// { status: 304, headers: { ETag: '"sha256:..."', "Cache-Control": "no-cache", "X-Content-Type-Options": "nosniff", "Aventara-Request-Id": "..." }, body: "" }

protocol.answerAbsent({ method: "POST", path: "/_nope", headers: {}, body: { kind: "raw", content: "" } });
// { status: 400, ... body: '{"data":null,"code":"A2000","cause":{"message":"The request is malformed and cannot be interpreted."}}' }
protocol.answerAbsent({ method: "GET", path: "/health", headers: {}, body: { kind: "raw", content: "" } });   // undefined

protocol.failure("A2012", "rid-1", ["POST"]);
// { status: 405, headers: { "Content-Type": "application/json", "Aventara-Request-Id": "rid-1", Allow: "POST" }, body: '{"data":null,"code":"A2012","cause":{"message":"The HTTP method is not allowed on this framework route."}}' }

Authentication stays a pipeline: pass what it needs through headers; a guard reads ctx.$transport.headers.

A complete Express host is in HTTP protocol.

AvProtocol.headers

The protocol's header names, as data.

Code
AvProtocol.headers;
// { protocolVersion: "Aventara-Protocol-Version",
//   contractHash:    "Aventara-Contract-Hash",
//   requestId:       "Aventara-Request-Id" }
KeyHeaderRequired on _resources and _transactionsValue
protocolVersionAventara-Protocol-Versionyes1
contractHashAventara-Contract-Hashyessha256: plus 64 lowercase hex characters, from /_contract (protocol.hash).
requestIdAventara-Request-Idno1 to 128 visible ASCII characters. Becomes the correlation id; otherwise one is minted. An invalid value is replaced by a minted one (verified).

AvProtocol.httpStatus

The status each A-code carries, as frozen data. The status is a function of the code alone, and a rolled-back transaction answers its failing step's status.

Code
AvProtocol.httpStatus.A2013;   // 409
StatusCodes
200A1000 A1001 A1004 A1005 A1006 A1007 A1008 A1009
201A1002 A1003
400A2000 A2006
401A4000
403A4001 A4002
404A2001 A2002 A2003
405A2012
409A2005 A2008 A2013 A2014
413A2010
415A2011
422A2004 A2007 A2009
500A3000 A3001 A3002 A3003 A3004

Total over all 33 codes. Meanings: Error codes.

AvProtocol.scalarFormats

The wire grammar of every scalar whose JSON form is a string, as anchored regular-expression source strings (so they survive JSON and can be emitted into a client). Frozen.

ScalarPattern sourceWire example
bigint^-?(?:0|[1-9]\d*)$"9007199254740993"
decimal^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$"10.5"
datetime^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$"2026-10-06T22:42:46.119Z"
bytes^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"AAEC"

A grammar is the shape only: a datetime must also round-trip toISOString() exactly, and bytes must be canonical base64 (unused bits zero). int, boolean, string and json travel as ordinary JSON values.

Code
new RegExp(AvProtocol.scalarFormats.datetime).test("2026-10-06T22:42:46.119Z");   // true

AvProtocol.isOperationResponse

Code
isOperationResponse(value: unknown): value is AvOperationResponse

Whether a parsed response body is a framework envelope. HTTP status plays no part. A value is an envelope when it is an object with an own data and an allocated code; for an A1xxx code cause is exactly null (and data is null for A1001); for every other code data is null and cause is an object with a string message, optional issues (each with an allocated V-code, a string message, an optional path of strings and integers) and optional non-negative integer operation. Members the protocol does not define are ignored.

Code
AvProtocol.isOperationResponse({ data: 1, code: "A1000", cause: null });   // true
AvProtocol.isOperationResponse({ data: 1, code: "A1000" });               // false (no cause)
AvProtocol.isOperationResponse("x");                                      // false

A client resolves an envelope's A1xxx and throws its other codes; anything that is not an envelope is a transport error.

AvProtocol.normalizeEntrypoint

Code
normalizeEntrypoint(value: string):
  | { ok: true; path: "" | `/${string}` }
  | { ok: false; reason: EntrypointRefusalReason; message: string }

The rule the server applies to api (and mcp) and the generator applies to the mount path of a URL. Only the slash spelling is coerced; anything else that is not canonical is refused, naming the defect and never echoing the value.

InputResult
"api", "/api/"{ ok: true, path: "/api" }
"//api//v1/"{ ok: true, path: "/api/v1" }
"", "/"{ ok: true, path: "" } (the root)
"api?x=1"{ ok: false, reason: "query", ... }
"/a b"{ ok: false, reason: "whitespace", ... }
"/a/../b"{ ok: false, reason: "dot-dot-segment", ... }
"/é"{ ok: false, reason: "non-ascii", ... }

Refusal reasons: whitespace, scheme, query, fragment, dot-dot-segment, dot-segment, backslash, non-ascii, percent-encoding, character. They are checked in that order, so each value has one reason. The message always ends a mount path is /-separated segments of ASCII letters, digits and -._~!$&'()*+,;=:@ only.

See also