§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.
§7.4.1 Result and Option
Section titled “§7.4.1 Result and Option”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".
§7.4.2 ValidationError
Section titled “§7.4.2 ValidationError”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;}§7.4.3 HttpResult
Section titled “§7.4.3 HttpResult”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:
| Export | Role |
|---|---|
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 Ack →
msg.ack() and Retry → log the reason + msg.retry().
| Export | Role |
|---|---|
QueueResult | the verdict type and its Ack / Retry(reason) constructor namespace |
§7.4.4 Agent state
Section titled “§7.4.4 Agent state”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).
| Export | Role |
|---|---|
DurableObjectStorage, DurableObjectState | the storage and state interfaces an agent class consumes |
KVNamespace | the 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) |
InMemoryStorage | an 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, DurableObjectNamespace | a 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 };| Export | Role |
|---|---|
ServiceBinding | the 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 |
§7.4.6 Non-exports: brands and Effect
Section titled “§7.4.6 Non-exports: brands and Effect”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.
Effectis realised asPromise(§7.3.2); there is noEffectruntime value.Effect.purelowers away at emission and<-lowers toawait, so neither needs runtime support.
§7.4.7 Prelude actors (v0.45)
Section titled “§7.4.7 Prelude actors (v0.45)”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 anyalg ≠ HS256, enforcesexp/nbf(rejecting malformed NumericDate claims), and returns thesubclaim — plus, since v0.53, the full verifiedclaimsobject 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 asha256=<hex>prefix. When a timestamp is configured it MUST be a finite number withintoleranceseconds of now; a malformed hex signature, an absent header, or a stale/non-numeric timestamp returnsfalse. Returns a boolean — the seam mapsfalseto 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 akidmiss so key rotation heals without a redeploy — thekid-miss refetch rate-limited by a ~30 s cooldown so an attacker-chosen novelkidcannot 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), rejectingalg: noneand symmetricHS*algorithms (algorithm-confusion). It then enforces the trust contract —issequals the declared issuer,audcontains the declared audience,expis present and in the future,nbf(if present) has passed (both with a small clock-skew leeway) — and returns thesubclaim (plus the full verifiedclaims) 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 exposingsent: F[](the frames written to it) andclosed: 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_SEEDif set (threaded bybynkc 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: Tbinding carries a generator overT’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.unsafeconstructor, 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
wherefilter that rejects a tuple skips it without consuming a case. The body is the property’sexpects, which throw anExpectationErroron failure exactly as acasebody 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
--seedreproduce line — reusing the expected-vs-actual renderer for the failingexpect. The report rides the existing runnermessagefield; the pinned--format jsonshape is unchanged.
Contract runner attack (v0.115)
Section titled “Contract runner attack (v0.115)”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.
§7.4.12 The stub clause (v0.118)
Section titled “§7.4.12 The stub clause (v0.118)”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
failsclause raises the capability-fault the seam propagates as anErr(the same fault path a real provider’s failure takes), distinct from an in-bandResulta 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, servingoutcomes[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, afailsfault, orok(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.