Skip to content

§7.4 The runtime library

Every emitted project ships a single runtime module, out/runtime.ts, generated by bynkc. The per-context and per-test modules import their shared values and types from it — at ./runtime.js for a top-level module, or ../runtime.js repeated by directory depth. This section is the normative contract: a conforming implementation MUST emit a runtime library providing these exports with these shapes and semantics. The module is headed // Generated by bynkc — do not edit by hand. and is identical across projects.

The TypeScript blocks here are the contract, not examples; the dynamic meaning of the constructs in §7.3 is defined against them.

Both are discriminated unions over a tag field — the same shape user sum types lower to (§7.3.1). Ok, Err, and Some are constructor functions; None is a constant.

export type Result<T, E> =
| { readonly tag: "Ok"; readonly value: T }
| { readonly tag: "Err"; readonly error: E };
export type Option<T> =
| { readonly tag: "Some"; readonly value: T }
| { readonly tag: "None" };

Ok/Err are shared with HttpResult (§7.4.3); a Bynk match discriminates them on tag and the ? operator (§7.3.2) inspects tag === "Err".

The error a refined or opaque .of constructor (§6.4) returns as the Err side of its Result:

export interface ValidationError {
readonly field: string;
readonly message: string;
readonly value: unknown;
}

The built-in HTTP-result sum (§5.7), a tag-discriminated union with a constructor namespace. Its variants track the common, modern HTTP status codes (RFC 9110) — success (Ok, Created, Accepted, NoContent), redirection carrying a Location URL (Found, SeeOther, PermanentRedirect, …), and the client/server failures (BadRequest, NotFound, TooManyRequests, ServerError, …). Two 200-only variants carry a body that bypasses JSON serialisation: Streaming (an SSE-framed Stream[String]) and Raw (a Bytes body written under an author-declared content-type, with serialiseValue bypassed entirely — no codec runs). See the HTTP reference for the full table. The runtime maps each variant to an HTTP response:

ExportRole
HttpResult<T>the result type, and HttpResult the constructor namespace
httpResultToResponse(result, serialiseValue, opts?)maps a variant to a Response with the corresponding status (Ok → 200, Created → 201, NoContent → 204, BadRequest → 400, … ServerError → 500); with opts.weakEtag, an Ok response also carries a weak ETag (see below)
matchPath(pattern, path)matches a route pattern such as /orders/:id, returning the captured parameters or null
headResponse(response)rebuilds a GET response as its HEAD answer — same status and headers, empty body — without reading (and so without draining) the original body
weakETag(body)a weak validator W/"…" over a serialised body, from a synchronous non-cryptographic hash (FNV-1a)
applyCache(response, maxAgeSecs, scope)stamps Cache-Control: <scope>, max-age=<n> onto a response, in place
notModifiedIfMatch(response, request)when the response carries an ETag and the request’s If-None-Match matches, returns a 304 with an empty body copying the ETag + Cache-Control; otherwise returns the response unchanged
applySecurityHeaders(response, policy)stamps the service’s security headers onto a response, in place — X-Content-Type-Options: nosniff when policy.nosniff, and Strict-Transport-Security: max-age=<n> when policy.hstsMaxAgeSecs is set

The entry router (§7 emission) answers the RFC 9110 method contract derived from the declared routes, around the HttpResult lowering above: a request to a live path under an undeclared method is a synthesised 405 carrying an Allow header (the union of the path’s methods, plus HEAD where GET exists, plus OPTIONS); a plain (non-preflight) OPTIONS is a 204 carrying Allow; and a HEAD to a GET route runs the GET handler and returns its status and headers with an empty body (via headResponse). A path that exists under no method is still 404. These are synthesised router responses (like the CORS preflight and the 404) and do not touch the HttpResult sum — the author-returnable MethodNotAllowed variant is a distinct, deliberate deny. HEAD/OPTIONS are not author-declarable methods.

The entry router also lowers conditional caching (v0.140) around the same GET responses. For an eligible GET — one whose success representation is the JSON Ok variant — httpResultToResponse is invoked with weakEtag, deriving a weak ETag over the serialised body; notModifiedIfMatch then compares the request’s If-None-Match and, on a match, replaces the 200 with a synthesised 304 (empty body, the ETag and any Cache-Control copied across). A handler carrying an @cache annotation (§5.7.2) additionally has applyCache stamp Cache-Control: <scope>, max-age=<n>. The composition order is normative: applySecurityHeaders(applyCors(notModifiedIfMatch(applyCache(…), request), …), …) — the conditional check runs after the handler (the body must exist to hash), the CORS stamp runs around it, and the security-header stamp (§5.7.3) runs outermost, so a cross-origin 304 still carries both Access-Control-Allow-Origin and X-Content-Type-Options. The CORS and security header sets are disjoint, so their relative order is not observable. The 304, like the 405/preflight, is a synthesised router response and does not touch the HttpResult sum; Streaming, Raw, redirect, and error variants carry no ETag and are never answered 304. Non-GET responses carry the CORS and security headers but are otherwise byte-for-byte unchanged.

Security headers (v0.141) are stamped on every from http response, not only opt-in ones: the compiler synthesises a default policy (nosniff: true, no HSTS) for every from http service, and applySecurityHeaders stamps X-Content-Type-Options: nosniff unless the service declares security { nosniff: false }. A security { hsts: <Duration> } additionally stamps Strict-Transport-Security. The stamp applies uniformly across the HttpResult variants and the synthesised preflight, 405/OPTIONS, and 304. Content-Security-Policy and X-Frame-Options are never emitted (the surface serves bytes, not markup).

Request body limits (v0.142) are enforced differently from the header policies above: they are a request-side check, not a response-stamping helper. For a body-taking route (POST/PUT/PATCH) with an effective cap (§5.7.4), the entry router compares the request’s Content-Length against the cap inline in the route dispatch — there is no applyLimit runtime helper analogous to applyCors/ applySecurityHeaders. When Content-Length exceeds the cap the router synthesises a 413 PayloadTooLarge ({ kind: "PayloadTooLarge", details: … }, reusing the existing 413 status — the HttpResult sum is untouched) and returns it before the body is read and before the by/Bearer auth seam, the boundary posture of the 405. That synthesised 413 is then passed through the same applyCors/applySecurityHeaders stamping as every other response, so a cross-origin caller can read it. A route with no effective cap runs the body read unchanged. Because the check keys on Content-Length (which may be absent for a chunked transfer, or spoofed) it is a fast-reject paired with the platform request cap, not a hard guarantee; a streamed-read cap is a named follow-on.

QueueResult (v0.44) is the analogous built-in for the queue protocol: a non-generic tag-discriminated sum with a constructor namespace, variants Ack (confirm the message) and Retry (redeliver, carrying a String reason). A queue handler returns Effect[QueueResult]; the runtime routes Ackmsg.ack() and Retry → log the reason + msg.retry().

ExportRole
QueueResultthe verdict type and its Ack / Retry(reason) constructor namespace

Agent classes consume a Durable-Object-shaped state surface. In bundle mode and under bynkc test this is backed in memory; in workers mode it is a real Durable Object (§7.3.3).

ExportRole
DurableObjectStorage, DurableObjectStatethe storage and state interfaces an agent class consumes
KVNamespacethe Worker KV namespace shape the bynk.cloudflare binding consumes — get/put (with expirationTtl options, v0.23)/delete/list (the cursor-page shape the drain follows)
InMemoryStoragean in-memory DurableObjectStorage, used in bundle mode and tests
makeTestState(name)builds an in-memory DurableObjectState
serialiseAgentKey(value)serialises an agent key to a stable string — semantically-equal keys (records compared by sorted fields) MUST serialise identically
StateRegistry<K>a serialised-key-to-state map with getOrCreate(key) and reset(); reset() clears all state so a fresh test sees a clean slate
DurableObjectStub, DurableObjectNamespacea minimal structural view of the Cloudflare Durable Object surface
callDurableObjectMethod(stub, method, args, deps)routes a workers-mode agent method call through the stub under the /_bynk/agent/<method> protocol
makeWorkersAgent(binding, key)a typed proxy over a Durable Object stub
makeAgent(registry, binding, key, constructBundle)the single construction helper: a present binding selects the workers path, an absent one the bundle registry path, so call sites are identical across targets
makeIntegrationDoNamespace(construct)an in-process Durable-Object namespace for cross-context system-tier tests

§7.4.5 The cross-Worker boundary protocol

Section titled “§7.4.5 The cross-Worker boundary protocol”

On the workers target a cross-context call is JSON over a Service Binding, validated at the boundary (§6.5).

export type JsonValue =
| null | boolean | number | string
| JsonValue[]
| { [k: string]: JsonValue };
export type BoundaryError =
| { readonly kind: "MalformedJson"; readonly details: string }
| { readonly kind: "StructuralMismatch"; readonly path: string;
readonly expected: string; readonly actual: string }
| { readonly kind: "RefinementViolation"; readonly path: string;
readonly violation: ValidationError }
| { readonly kind: "Transport"; readonly status: number; readonly details: string };
ExportRole
ServiceBindingthe fetch-shaped binding a consumer calls
callService(binding, servicePath, argsJson, deserialiseResult, callerContext)issues the call to /_bynk/call/<servicePath>, decodes the response, and raises a BoundaryError on transport or shape failure. v0.54: callerContext (the calling context’s qualified name) is stamped into the X-Bynk-Caller header so the callee’s by c: Caller handler reads a live CallerId; the args body is unchanged
boundaryError(error)wraps a BoundaryError as a throwable Error

Two parts of the surface have no runtime export, by design:

  • Brands are compile-time TypeScript intersections, erased after type-checking (§7.3.1); the runtime library carries no brand values.
  • Effect is realised as Promise (§7.3.2); there is no Effect runtime value. Effect.pure lowers away at emission and <- lowers to await, so neither needs runtime support.

The compiler provides four prelude actors — boundary contracts available without a declaration: Visitor (auth = None; identity ()), and the Internal actors Scheduler, Producer, and Caller that back the per-protocol defaults (cron, queue, and on call respectively). Caller yields the calling-context identity; the other prelude actors carry no identity payload (()) in this increment. Prelude actors have no runtime export — like brands, an actor is a compile-time contract; the zero-crypto schemes mint no runtime verification code (see §7.3).

§7.4.8 Authenticated-scheme verifiers (v0.47, v0.51, v0.151)

Section titled “§7.4.8 Authenticated-scheme verifiers (v0.47, v0.51, v0.151)”

The authenticated schemes do export runtime verification helpers, emitted into the per-Worker runtime module and called from the boundary seam (§7.3.4a):

  • verifyBearerJwtHs256 (Bearer, v0.47) — HS256-verifies a JWT with WebCrypto (crypto.subtle.verify, constant-time), rejects any alg ≠ HS256, enforces exp/nbf (rejecting malformed NumericDate claims), and returns the sub claim — plus, since v0.53, the full verified claims object for refinement-actor authorisation — on success; any failure is reported so the seam maps it to a 401. The authorisation predicate of a refinement actor is lowered inline over those claims (no new runtime export), a failed invariant mapping to 403.
  • verifySignatureHmacSha256 (Signature, v0.51) — recomputes HMAC-SHA256 over the raw request body (or <timestamp>.<body> when a timestamp is bound) with the sourced secret and constant-time-compares (crypto.subtle.verify) against the configured signature header, accepting a bare hex digest or a sha256=<hex> prefix. When a timestamp is configured it MUST be a finite number within tolerance seconds of now; a malformed hex signature, an absent header, or a stale/non-numeric timestamp returns false. Returns a boolean — the seam maps false to a 401.
  • verifyOidcJwt (Oidc, v0.151) — verifies an asymmetrically-signed (RS256/ES256) JWT against a provider’s published JWKS. It fetches the key set (cached ~10 min, refetched on a kid miss so key rotation heals without a redeploy — the kid-miss refetch rate-limited by a ~30 s cooldown so an attacker-chosen novel kid cannot amplify into fetches; a bad signature against a published key never refetches), imports the matching signing key (use: "sig") and verifies the signature with WebCrypto (crypto.subtle.verify), rejecting alg: none and symmetric HS* algorithms (algorithm-confusion). It then enforces the trust contract — iss equals the declared issuer, aud contains the declared audience, exp is present and in the future, nbf (if present) has passed (both with a small clock-skew leeway) — and returns the sub claim (plus the full verified claims) on success; any failure is reported so the seam maps it to a 401. It takes no secret: the trust root is the provider’s public JWKS URL.

verifyBearerJwtHs256/verifySignatureHmacSha256 source their secret from env (the same channel the Secrets capability reads); the secret is used only inside the verifier and never logged. verifyOidcJwt sources no secret — it fetches the provider’s public keys over the network.

§7.4.9 Streams and connections (v0.100, v0.102+)

Section titled “§7.4.9 Streams and connections (v0.100, v0.102+)”

A Stream[T] has no runtime-library export: it lowers to a host AsyncIterable<T> emitted inline (§7.3.10), so the runtime module gains nothing for stream code. A streamed HTTP response is a standard Response whose body is the stream encoded as Server-Sent Events.

A Connection[F] is lowered against a runtime Connection<F> interface with send(frame: F): void (JSON-encodes and writes a frame) and close(): void (ends the socket). Two implementations satisfy it:

  • TestConnection<F> (bundle/test) — a capture-and-inspect channel exposing sent: F[] (the frames written to it) and closed: boolean, so a WebSocket service is fully testable under Node with no Durable Object.
  • the Workers binding — wraps a hibernatable Durable-Object WebSocket; a stored connection survives hibernation (re-associated via the platform’s serializeAttachment/getWebSockets) and is restored on rehydration, a platform-supplied guarantee the language relies on but does not implement.

Held connections are never serialised into the durable state record; on Workers a Map[K, Connection] lives in an in-memory side-table keyed alongside the durable state.

§7.4.10 Generative properties and contracts (v0.114, v0.115)

Section titled “§7.4.10 Generative properties and contracts (v0.114, v0.115)”

A generative property has no runtime-library export: like the expectation helpers, the generator, the seeded PRNG, the case loop, and the shrinker are emitted into the per-test module that declares a property (never into the deployable). This section is informative — it fixes the runtime contract the emission satisfies, not an exported API.

  • Root seed. Each run resolves one root seed: the hex value of BYNK_TEST_SEED if set (threaded by bynkc test --seed <hex>), otherwise a fresh random 32-bit value. Every property derives its own generation seed deterministically from the root seed and a stable per-property ordinal, so a run is fully determined by its root seed.
  • Generator. Each for all x: T binding carries a generator over T’s refinement domain (§5.9a): boundary values are drawn first (the refinement floor/ceiling, minimum-length strings, each sum variant), then random inhabitants. Generated subjects are branded to their type — an opaque value through its .unsafe constructor, a refined value through an inline brand cast (ADR 0182) — so a generated subject is valid by construction.
  • Case loop. The runner draws up to a bounded number of accepted cases per property; a where filter that rejects a tuple skips it without consuming a case. The body is the property’s expects, which throw an ExpectationError on failure exactly as a case body does.
  • Shrinking. On a counterexample the runner minimises each input toward its boundary (integers toward the refinement floor, strings toward minimum length, sums toward the first variant) while the predicate still fails, then reports the case count, the root seed, the shrunk tuple, and a copy-paste --seed reproduce line — reusing the expected-vs-actual renderer for the failing expect. The report rides the existing runner message field; the pinned --format json shape is unchanged.

A contracted pure function reachable from a test target is attacked by the same generative runtime (ADR 0150): the runner draws arguments over the parameters’ refinement domains, filters them by the conjunction of the function’s requires (exactly as a where filters — a rejected tuple skips without consuming a case), and calls the function. The dev/test call-site guard (§7.3.5b) asserts each ensures and throws on violation; the runner treats the guard’s contract error as a shrinkable failure, so a broken ensures reports a case count, the root seed, and a shrunk counterexample with a --seed reproduce line — identical in shape to a property failure. The attack is emitted per module alongside the guard (dev/test only), never in the release build. Int arguments are coerced to number at the call (generation produces bigint; functions do number arithmetic).

§7.4.11 The observation recorder (v0.117)

Section titled “§7.4.11 The observation recorder (v0.117)”

A case that observes a capability (ADR 0152) records its calls through a recording proxy emitted into the test module, dev/test build only. The proxy wraps the case’s deps object: for each observed capability operation, the seam function is replaced by a wrapper that appends the call to a per-operation log and then delegates to whatever stands behind the seam, returning its result unchanged.

function __bynkRecordDeps(deps: any, spec: Record<string, string[]>,
obs: { log: Record<string, { args: any[]; order: number }[]>; n: number }): any {
for (const cap of Object.keys(spec)) {
if (!deps || !deps[cap]) continue;
for (const op of spec[cap]) {
const orig = deps[cap][op];
if (typeof orig !== "function") continue;
const key = cap + "." + op;
obs.log[key] = obs.log[key] ?? [];
deps[cap][op] = (...args: any[]) => {
obs.log[key].push({ args, order: obs.n++ });
return orig.apply(deps[cap], args);
};
}
}
return deps;
}

The contract: record-then-delegate, so the arguments are logged as passed and the delegated return is untouched; the order index is monotonic across all operations, so A.op before B.op is a comparison of the first recorded orders; and the log is per-case (a fresh __obs per case), so counts are scoped to the case. The sugar and trace(Cap.op) are two views of this one log — they cannot disagree. The proxy is emitted only under bynkc test; the deploy build carries none of it.

A stub clause (ADR 0154) lowers to a stub that stands behind a capability seam in place of the real provider, emitted into the test module, dev/test build only. It sits behind the recording proxy (§7.4.11), so a case can both stub a return and observe the call at one seam.

For a stubbed operation, the clauses for that method form an ordered match table: each recorded call is tried against the argument patterns top to bottom and the first match wins (a _ pattern matches anything; a literal / value pattern compares by equality). The matched clause yields the operation’s result:

  • a returns <value> clause yields the constant value;
  • a fails clause raises the capability-fault the seam propagates as an Err (the same fault path a real provider’s failure takes), distinct from an in-band Result a case asserts directly;
  • a returns each [<outcome>, …] clause is backed by a per-call cursor: the stub holds an index that advances on each call, serving outcomes[min(i, n-1)] — so the last outcome repeats once the sequence is exhausted (steady state) and an extra call never spuriously fails the case. Each outcome is a value, a fails fault, or ok(v).

The contract: match-then-serve, deterministic and side-effect-free apart from the sequence cursor’s advance; a stub is scoped to its case (a suite-scoped stub state is instantiated fresh per case), so a cursor never carries state between cases, and precedence (case > suite > tier default) is resolved at emission, not at run time. The stub is emitted only under bynkc test; the deploy build carries none of it.