Skip to main content

bynk_check/wire_format/
reference.rs

1//! #1969: renders `reference/wire-format.md`, the published reference for wire
2//! format version 1.
3//!
4//! Every fact the page states that the compiler also uses is read from the
5//! compiler: the version, headers and check order from [`super`], the base-type
6//! encodings from [`crate::wire`], and the contract hash from
7//! [`crate::contract`], computed on [`EXAMPLE_SOURCE`]. The page's examples
8//! ([`EXAMPLES`]) are executed: `bynkc/tests/wire_reference.rs` compiles
9//! [`EXAMPLE_SOURCE`] to Workers, runs every example through the generated
10//! codecs on Node, and checks the emitted hashes and dispatch against this page.
11//! Regenerate the page with
12//! `BYNK_BLESS=1 cargo test -p bynkc --test it wire_reference::`.
13
14use std::collections::HashMap;
15use std::fmt::Write as _;
16use std::sync::Arc;
17
18use bynk_syntax::ast::{BaseType, CommonsItem, SourceUnit, TypeDecl};
19
20use super::{
21    ACCEPTED_WIRE_VERSIONS, CALL_CHECKS, CALL_METHOD, CALL_PATH_PREFIX, CALLER_HEADER,
22    CONTRACT_HEADER, EDGE_FROM_PREFIX, EDGE_TO_PREFIX, HEADERS, MAX_BODY_BYTES, SIGNATURE_HEADER,
23    SIGNATURE_WINDOW_SECONDS, SIGNING_SCHEME, TIMESTAMP_HEADER, WIRE_HEADER, WIRE_VERSION,
24    edge_key_from, edge_key_to, signing_input,
25};
26use crate::contract::{FNV1A64_OFFSET, FNV1A64_PRIME, contract_hash, service_normal_form};
27use crate::resolver::cross_context_service_for;
28use crate::wire::{BaseGuard, JsonKind, WIRE_DISCRIMINANT, field_base_guards, json_kind_of};
29
30/// The context the page's examples and worked hash are computed on.
31pub const EXAMPLE_SOURCE: &str = r#"context wire.sample
32
33type Qty = Int where NonNegative
34
35type Shape =
36  | Circle(radius: Float)
37  | Dot
38
39type Line = {
40  sku: String,
41  qty: Int where Positive,
42  wait: Duration,
43  delay: Option[Duration],
44  at: Instant,
45  blob: Bytes,
46  note: Option[String],
47  tags: List[String],
48  stock: Map[String, Int],
49  rush: Bool,
50}
51
52service quote {
53  on call(line: Line, size: Qty) -> Effect[Result[Shape, String]] {
54    Ok(Dot)
55  }
56}
57
58type Tagged[T] = {
59  n: Int where Positive,
60  v: T,
61}
62
63service who {
64  on call(t: Tagged[String]) -> Effect[Result[String, String]] by c: Caller {
65    Ok(c.identity)
66  }
67}
68
69service ping {
70  on call() -> Effect[Result[(), String]] {
71    Ok(())
72  }
73}
74
75actor Legacy { auth = Foreign(secret = "LEGACY_CALL_KEY") }
76
77service legacy {
78  on call(n: Int) -> Effect[Result[Int, String]] by Legacy {
79    Ok(n)
80  }
81}
82"#;
83
84/// What decoding an example's JSON does.
85pub enum Outcome {
86    /// Decodes, and the decoded value encodes back as this JSON.
87    Accepted { encodes_as: &'static str },
88    /// Refused with this error `kind` at this `path`.
89    Rejected {
90        kind: &'static str,
91        path: &'static str,
92    },
93}
94
95/// One executed example: `json` decoded as `ty`.
96pub struct Example {
97    /// The Bynk type, as the page shows it.
98    pub ty: &'static str,
99    /// The generated codec suffix the test drives (`__deserialise_<codec>`).
100    pub codec: &'static str,
101    pub json: &'static str,
102    pub outcome: Outcome,
103    pub note: &'static str,
104}
105
106const LINE: &str = r#"{"sku":"A-1","qty":2,"wait":1500,"delay":{"kind":"Some","value":250},"at":1767225600000,"blob":"aGk=","note":{"kind":"Some","value":"gift"},"tags":["new"],"stock":[["lon",3]],"rush":false}"#;
107
108/// The page's examples, each run through the generated codecs by
109/// `bynkc/tests/wire_reference.rs`.
110pub const EXAMPLES: &[Example] = &[
111    Example {
112        ty: "Line",
113        codec: "Line",
114        json: LINE,
115        outcome: Outcome::Accepted { encodes_as: LINE },
116        note: "Every field present. A record is an object keyed by field name.",
117    },
118    Example {
119        ty: "Line",
120        codec: "Line",
121        json: r#"{"sku":"A-1","qty":2,"wait":1500,"at":1767225600000,"blob":"","tags":[],"stock":[],"rush":true,"extra":1}"#,
122        outcome: Outcome::Accepted {
123            encodes_as: r#"{"sku":"A-1","qty":2,"wait":1500,"delay":{"kind":"None"},"at":1767225600000,"blob":"","note":{"kind":"None"},"tags":[],"stock":[],"rush":true}"#,
124        },
125        note: "An absent `Option` field is `None`. An unknown key is ignored.",
126    },
127    Example {
128        ty: "Line",
129        codec: "Line",
130        json: r#"{"sku":"A-1","qty":2,"wait":1500,"at":1767225600000,"blob":"","note":"gift","tags":[],"stock":[],"rush":true}"#,
131        outcome: Outcome::Accepted {
132            encodes_as: r#"{"sku":"A-1","qty":2,"wait":1500,"delay":{"kind":"None"},"at":1767225600000,"blob":"","note":{"kind":"Some","value":"gift"},"tags":[],"stock":[],"rush":true}"#,
133        },
134        note: "A bare value where an `Option` is expected is `Some`.",
135    },
136    Example {
137        ty: "Line",
138        codec: "Line",
139        json: r#"{"sku":"A-1","qty":0,"wait":1500,"at":1767225600000,"blob":"","tags":[],"stock":[],"rush":true}"#,
140        outcome: Outcome::Rejected {
141            kind: "RefinementViolation",
142            path: "$.qty",
143        },
144        note: "An inline field refinement (`qty: Int where Positive`) is checked.",
145    },
146    Example {
147        ty: "Line",
148        codec: "Line",
149        json: r#"{"sku":"A-1","qty":1.5,"wait":1500,"at":1767225600000,"blob":"","tags":[],"stock":[],"rush":true}"#,
150        outcome: Outcome::Rejected {
151            kind: "StructuralMismatch",
152            path: "$.qty",
153        },
154        note: "An `Int` is a whole number.",
155    },
156    Example {
157        ty: "Line",
158        codec: "Line",
159        json: r#"{"sku":"A-1","qty":2,"wait":1.5,"at":1767225600000,"blob":"","tags":[],"stock":[],"rush":true}"#,
160        outcome: Outcome::Rejected {
161            kind: "StructuralMismatch",
162            path: "$.wait",
163        },
164        note: "A `Duration` is whole milliseconds.",
165    },
166    Example {
167        ty: "Line",
168        codec: "Line",
169        json: r#"{"sku":"A-1","qty":2,"wait":1500,"delay":{"kind":"Some","value":0.5},"at":1767225600000,"blob":"","tags":[],"stock":[],"rush":true}"#,
170        outcome: Outcome::Rejected {
171            kind: "StructuralMismatch",
172            path: "$.delay.value",
173        },
174        note: "So is a `Duration` inside an `Option`, a `List` or a variant.",
175    },
176    Example {
177        ty: "Line",
178        codec: "Line",
179        json: r#"{"sku":"A-1","qty":2,"wait":1500,"at":1767225600000,"blob":"%%","tags":[],"stock":[],"rush":true}"#,
180        outcome: Outcome::Rejected {
181            kind: "StructuralMismatch",
182            path: "$.blob",
183        },
184        note: "`Bytes` is padded base64.",
185    },
186    Example {
187        ty: "Line",
188        codec: "Line",
189        json: r#"{"sku":"A-1","qty":2,"wait":1500,"at":1767225600000,"blob":"","tags":[],"stock":{"lon":3},"rush":true}"#,
190        outcome: Outcome::Rejected {
191            kind: "StructuralMismatch",
192            path: "$.stock",
193        },
194        note: "A `Map` is an array of pairs, never an object.",
195    },
196    Example {
197        ty: "Qty",
198        codec: "Qty",
199        json: "-1",
200        outcome: Outcome::Rejected {
201            kind: "RefinementViolation",
202            path: "$",
203        },
204        note: "A named refined type is its base value, checked against its predicates.",
205    },
206    Example {
207        ty: "Tagged[String]",
208        codec: "Tagged_String",
209        json: r#"{"n":0,"v":"x"}"#,
210        outcome: Outcome::Rejected {
211            kind: "RefinementViolation",
212            path: "$.n",
213        },
214        note: "A generic record's inline refinements are checked in every instantiation.",
215    },
216    Example {
217        ty: "Shape",
218        codec: "Shape",
219        json: r#"{"kind":"Circle","radius":0.5}"#,
220        outcome: Outcome::Accepted {
221            encodes_as: r#"{"kind":"Circle","radius":0.5}"#,
222        },
223        note: "A variant's payload fields sit beside `kind`.",
224    },
225    Example {
226        ty: "Shape",
227        codec: "Shape",
228        json: r#"{"kind":"Circle","radius":1e999}"#,
229        outcome: Outcome::Rejected {
230            kind: "StructuralMismatch",
231            path: "$.radius",
232        },
233        note: "A `Float` is finite; `1e999` parses as infinity.",
234    },
235    Example {
236        ty: "Shape",
237        codec: "Shape",
238        json: r#"{"kind":"Square"}"#,
239        outcome: Outcome::Rejected {
240            kind: "StructuralMismatch",
241            path: "$",
242        },
243        note: "An unknown variant is refused.",
244    },
245    Example {
246        ty: "Result[Shape, String]",
247        codec: "Result_Shape_String",
248        json: r#"{"kind":"Ok","value":{"kind":"Dot"}}"#,
249        outcome: Outcome::Accepted {
250            encodes_as: r#"{"kind":"Ok","value":{"kind":"Dot"}}"#,
251        },
252        note: "",
253    },
254    Example {
255        ty: "Result[Shape, String]",
256        codec: "Result_Shape_String",
257        json: r#"{"kind":"Err","error":"sold out"}"#,
258        outcome: Outcome::Accepted {
259            encodes_as: r#"{"kind":"Err","error":"sold out"}"#,
260        },
261        note: "A domain error is a `200` carrying `Err`.",
262    },
263    Example {
264        ty: "Result[(), String]",
265        codec: "Result_Unit_String",
266        json: r#"{"kind":"Ok","value":null}"#,
267        outcome: Outcome::Accepted {
268            encodes_as: r#"{"kind":"Ok","value":null}"#,
269        },
270        note: "`()` is `null`.",
271    },
272];
273
274/// The page's worked contract-hash example: each `on call` service of
275/// [`EXAMPLE_SOURCE`] with its normal form and hash, in source order.
276pub fn example_contracts() -> Vec<(String, String, String)> {
277    let tokens = bynk_syntax::lexer::tokenize(EXAMPLE_SOURCE).expect("the example source lexes");
278    let unit = match bynk_syntax::parser::parse_unit(&tokens, EXAMPLE_SOURCE) {
279        Ok(SourceUnit::Context(c)) => c,
280        other => panic!("the example source is a context: {other:?}"),
281    };
282    let types: HashMap<String, Arc<TypeDecl>> = unit
283        .items
284        .iter()
285        .filter_map(|i| match i {
286            CommonsItem::Type(t) => Some((t.name.name.clone(), Arc::new(t.clone()))),
287            _ => None,
288        })
289        .collect();
290    unit.items
291        .iter()
292        .filter_map(|i| match i {
293            CommonsItem::Service(s) => cross_context_service_for(&s.name.name, s),
294            _ => None,
295        })
296        .map(|svc| {
297            let form = service_normal_form(&svc, &types);
298            let hash = contract_hash(&form);
299            (svc.name, form, hash)
300        })
301        .collect()
302}
303
304fn json_kind_name(k: JsonKind) -> &'static str {
305    match k {
306        JsonKind::Number => "number",
307        JsonKind::String => "string",
308        JsonKind::Boolean => "boolean",
309        JsonKind::Object => "object",
310        JsonKind::Array => "array",
311        JsonKind::Null => "null",
312    }
313}
314
315/// The base-type rows of the encoding table, read off [`crate::wire`]'s model:
316/// the JSON kind each base type wires as and the guards its decode applies.
317fn base_type_rows(out: &mut String) {
318    const BASES: [(BaseType, &str); 7] = [
319        (BaseType::Int, "an integer"),
320        (BaseType::Float, "a number"),
321        (BaseType::Bool, "`true` or `false`"),
322        (BaseType::String, "a string"),
323        (
324            BaseType::Bytes,
325            "standard base64 with padding (RFC 4648 §4)",
326        ),
327        (BaseType::Duration, "whole milliseconds"),
328        (BaseType::Instant, "milliseconds since the Unix epoch"),
329    ];
330    for (base, value) in BASES {
331        let kind = json_kind_of(base);
332        let rule = if base == BaseType::Bytes {
333            "valid base64; anything else is `StructuralMismatch` (`expected: \"base64 string\"`)"
334                .to_string()
335        } else {
336            let mut rule = format!("JSON {}", json_kind_name(kind));
337            for guard in field_base_guards(base) {
338                rule.push_str(match guard {
339                    BaseGuard::Integral => {
340                        ", and `Number.isSafeInteger` (whole, within ±(2^53 − 1)); otherwise `StructuralMismatch` (`expected: \"safe integer\"`)"
341                    }
342                    BaseGuard::Finite => {
343                        ", and finite; otherwise `StructuralMismatch` (`expected: \"finite number\"`)"
344                    }
345                });
346            }
347            rule
348        };
349        let encode = match base {
350            BaseType::Int | BaseType::Duration | BaseType::Instant => {
351                "an unsafe value throws in the sender (a `500`), never a lossy number"
352            }
353            BaseType::Float => "a non-finite value throws in the sender",
354            _ => "as is",
355        };
356        let _ = writeln!(
357            out,
358            "| `{}` | {} ({value}) | {rule} | {encode} |",
359            base.name(),
360            json_kind_name(kind),
361        );
362    }
363}
364
365fn status_rows(out: &mut String) {
366    for (i, check) in CALL_CHECKS.iter().enumerate() {
367        let response = check.response();
368        let _ = writeln!(
369            out,
370            "| {} | {} | `{}` | {response} |",
371            i + 1,
372            check.condition(),
373            check.status(),
374        );
375    }
376}
377
378/// The whole page, in mdBook form (`# H1` title, relative `.md` links).
379pub fn render_reference() -> String {
380    let mut out = String::new();
381    let accepted = ACCEPTED_WIRE_VERSIONS
382        .iter()
383        .map(|v| format!("`{v}`"))
384        .collect::<Vec<_>>()
385        .join(", ");
386
387    let from_example = edge_key_from("shop.orders").unwrap_or_default();
388    let to_example = edge_key_to("shop.payment").unwrap_or_default();
389    let input_example = signing_input(
390        "shop.orders",
391        "shop.payment",
392        CALL_METHOD,
393        &format!("{CALL_PATH_PREFIX}authorise"),
394        "0123456789abcdef",
395        "1",
396        "1760000000",
397        "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
398    );
399    let _ = write!(
400        out,
401        r#"# Wire format (version {WIRE_VERSION})
402
403<!-- GENERATED FILE — do not edit by hand.
404     Source: bynk-check/src/wire_format.rs and wire_format/reference.rs, via bynkc/tests/wire_reference.rs.
405     Regenerate with: BYNK_BLESS=1 cargo test -p bynkc --test it wire_reference:: -->
406
407How one Bynk context calls another context's `on call` service over a
408Cloudflare Service Binding: the request, the checks the callee makes, the
409responses, the JSON encoding of every type that can cross, and the contract
410hash. Code that is not Bynk can call a Bynk service by speaking this format.
411
412This page is generated from the compiler. The header names, the check order
413and the status codes are the constants the emitter builds the call dispatch
414from; the encoding table is read from the compiler's wire model; and every
415example below is run through the codecs the compiler generates for the
416example context, by `bynkc/tests/wire_reference.rs`.
417
418## Version and stability
419
420This is wire format version **{WIRE_VERSION}**. It is versioned on its own, not
421with the compiler: a compiler release changes it only by adding a new wire
422version.
423
424- **Within a version, changes are additive only.** A new status code, a new
425  error `kind` or a new optional header may appear. A caller must treat an
426  error `kind` it does not know as an error.
427- **Anything else is a new version:** changing an encoding, a check, a status,
428  an existing error body, or the contract hash's normal form or function.
429- **The caller states its version** in `{WIRE_HEADER}`; an absent header means
430  `1`. **The callee accepts a range** (this compiler's callees accept
431  {accepted}) and refuses any other version with the versions it accepts, so a
432  version is retired after an overlap window, not on a flag day.
433- **The signature is part of the version:** its headers, its signing input
434  and its window. The keys are not; they belong to a deployment.
435
436What the format does **not** cover: the TypeScript the compiler emits, the
437in-memory representation of values, and the Worker's other entry points
438(`from http` routes, events, queues). Those follow the compiler's own version.
439
440## Request
441
442```text
443{CALL_METHOD} {CALL_PATH_PREFIX}<service>
444```
445
446`<service>` is the service's name as declared (`service quote` is
447`{CALL_PATH_PREFIX}quote`). The host is ignored: a Bynk caller sends
448`http://internal{CALL_PATH_PREFIX}<service>` over the Service Binding.
449
450| Header | Required | Meaning |
451|---|---|---|
452"#
453    );
454    for h in HEADERS {
455        let _ = writeln!(out, "| `{}` | {} | {} |", h.name, h.required, h.meaning);
456    }
457    let _ = write!(
458        out,
459        r#"
460**Body.** JSON, encoding the handler's parameters:
461
462- **no parameters:** any JSON value; Bynk callers send `{{}}`;
463- **one parameter:** the parameter's value itself, not wrapped;
464- **two or more:** an object keyed by parameter name. Unknown keys are ignored.
465
466So `on call(line: Line, size: Qty)` takes `{{"line": {{…}}, "size": 3}}`, and
467`on call(size: Qty)` takes `3`. Renaming or reordering parameters is a contract
468change (see [Contract hash](#contract-hash)).
469
470## Checks and responses
471
472The callee makes these checks in this order. The first that fails answers the
473request.
474
475| # | Check | Status on failure | Body on failure |
476|---|---|---|---|
477"#
478    );
479    status_rows(&mut out);
480    let _ = write!(
481        out,
482        r#"
483If every check passes, the handler runs:
484
485- **`200`**, `content-type: application/json`, with the handler's return value
486  encoded as below. A domain error is a `200` too: `Result[T, E]`'s `Err` is a
487  value, not a status.
488- **`500`**, text `Internal Server Error`, if the handler faults (including
489  encoding a value the format cannot carry, such as an unsafe `Int`). The
490  callee logs the fault; the body says nothing more. The response carries
491  `X-Bynk-Fault: <context>`, the callee's qualified name, so the caller can
492  say which Worker faulted. Only a `/_bynk/call/` fault is marked.
493
494A status not listed here (a platform error between the Workers, for example)
495is a transport failure. A Bynk caller reports a `409` `ContractMismatch` and a
496marked `500` `CalleeFaulted` by name, and every other non-`2xx` as
497`Transport`, with the status.
498
499### Error bodies
500
501Every JSON error body is an object with a `kind`:
502
503| `kind` | Status | Fields |
504|---|---|---|
505| `UnsupportedWireVersion` | `400` | `accepted`: the wire versions the callee accepts |
506| `ContractMismatch` | `409` | `service`, `expected` (the callee's hash), `actual` (the header as received) |
507| `MalformedJson` | `400` | `details` |
508| `StructuralMismatch` | `400` | `path`, `expected`, `actual`: the value had the wrong shape |
509| `RefinementViolation` | `400` | `path`, `violation`: `{{"field", "message", "value"}}`, a value of the right shape broke a predicate |
510
511`path` is a JSONPath-like location in the body: `$` is the whole body (one
512parameter), `$.<param>` a parameter (several), then `.<field>` or `[<index>]`
513inward. An unknown variant fails at the sum's own `path`, with
514`expected: "sum variant kind"` and the `kind` that was sent as `actual`.
515
516## Encoding
517
518Each type has one JSON encoding, the same in both directions. A value is
519decoded by checking its shape, then any predicates.
520
521### Base types
522
523| Type | JSON | Decode accepts | Encode |
524|---|---|---|---|
525"#
526    );
527    base_type_rows(&mut out);
528    let _ = write!(
529        out,
530        r#"| `()` | `null` | anything; the value is not inspected | `null` |
531
532`Int`, `Duration` and `Instant` are limited to JavaScript's safe-integer range
533on the wire, so every JSON parser reads them exactly.
534
535### Composite types
536
537| Type | JSON |
538|---|---|
539| record | an object keyed by field name. A missing field decodes as its default when it has one, as `None` when it is an `Option`, and otherwise fails. Unknown keys are ignored. |
540| sum (`enum` included) | an object with `"{WIRE_DISCRIMINANT}"`: the variant's name, beside the variant's payload fields: `{{"{WIRE_DISCRIMINANT}":"Circle","radius":0.5}}`, `{{"{WIRE_DISCRIMINANT}":"Dot"}}` |
541| `Result[T, E]` | `{{"{WIRE_DISCRIMINANT}":"Ok","value":<T>}}` or `{{"{WIRE_DISCRIMINANT}":"Err","error":<E>}}` |
542| `Option[T]` | `{{"{WIRE_DISCRIMINANT}":"Some","value":<T>}}` or `{{"{WIRE_DISCRIMINANT}":"None"}}`. A sender may also send `null` or leave the field out for `None`, or send the bare value for `Some`; Bynk always encodes the tagged form |
543| `List[T]` | an array |
544| `Map[K, V]` | an array of `[key, value]` pairs, in insertion order |
545| refined type (`type Qty = Int where NonNegative`) | its base type's encoding. The decoder checks the predicates, refusing with `RefinementViolation` |
546| opaque type | its base type's encoding. A caller cannot see the predicate, so only the owning context checks it |
547| inline field refinement (`qty: Int where Positive`) | the field's encoding. The decoder checks the predicates, like a refined type's |
548| generic instantiation (`Page[Line]`) | the declaration's encoding with the arguments substituted |
549
550The in-memory discriminant of a Bynk sum is `tag`; on the wire it is
551`"{WIRE_DISCRIMINANT}"`. The encoding never depends on the in-memory layout.
552
553### Examples
554
555Each row is decoded by the codec the compiler generates for `wire.sample`
556below, then encoded again. Rejections show the error's `kind` and `path`.
557
558```bynk
559"#
560    );
561    out.push_str(EXAMPLE_SOURCE.trim_end());
562    out.push_str("\n```\n\n| Type | JSON sent | Outcome | Note |\n|---|---|---|---|\n");
563    for ex in EXAMPLES {
564        let outcome = match &ex.outcome {
565            Outcome::Accepted { encodes_as } if *encodes_as == ex.json => {
566                "accepted; encodes back the same".to_string()
567            }
568            Outcome::Accepted { encodes_as } => {
569                format!("accepted; encodes back as `{encodes_as}`")
570            }
571            Outcome::Rejected { kind, path } => format!("`{kind}` at `{path}`"),
572        };
573        let _ = writeln!(
574            out,
575            "| `{}` | `{}` | {outcome} | {} |",
576            ex.ty, ex.json, ex.note
577        );
578    }
579
580    let contracts = example_contracts();
581    let _ = write!(
582        out,
583        r#"
584## Contract hash
585
586`X-Bynk-Contract` carries a hash of the service's signature, so a callee can
587refuse a caller that was compiled against a different version of it (`409`).
588It detects skew; it is not a credential and not a security control.
589
590**Normal form.** One line per service:
591
592```text
593<service>(<param>: <type>, …) -> <type>
594```
595
596- Parameters keep their names and order. `Effect[T]` is written as `T`.
597- Base types are their names; `()` is `()`; `List[T]`, `Option[T]`,
598  `Map[K, V]`, `Result[T, E]` keep their arguments in order.
599- A named type is its name followed by its structure, so both a rename and a
600  change of shape change the hash:
601  - a record is `Name{{f: T, …}}`, each field written `f: T` (or
602    `f: T where P, …` with an inline refinement), the fields sorted by that
603    text's UTF-8 bytes and joined with `, `;
604  - a sum is `Name|A|B(T, …)`: each variant written as its name, followed by
605    its payload types in order in parentheses if it has any, the variants
606    sorted by that text's UTF-8 bytes, each preceded by `|`;
607  - a refined type is its name and base run together, then its predicates:
608    `type Qty = Int where NonNegative` is `QtyInt where NonNegative` (with no
609    predicates, `QtyInt ` with a trailing space);
610  - an opaque type is `NameBase opaque`, without its predicates;
611  - a generic instantiation is the declaration's form with the arguments
612    substituted, followed by `[A, …]`;
613  - a type met again inside its own expansion is `@Name`.
614- Predicates are a set: written as below, sorted by their UTF-8 bytes, with
615  duplicates removed, and joined with `, `. They are `Positive`,
616  `NonNegative`, `InRange(a, b)`, `InRangeF(a, b)`, `MinLength(n)`,
617  `MaxLength(n)`, `Length(n)` and `Matches("…")`; `NonEmpty` is written
618  `MinLength(1)`. Bounds are written as decimal numbers in their shortest
619  round-trip form (`01` is `1`). A `Matches` pattern is quoted, with `\`, `"`
620  and control characters backslash-escaped.
621- No spaces other than those shown; no doc comments, defaults, `given` or
622  `by` clauses.
623
624**Hash.** FNV-1a, 64-bit, over the normal form's UTF-8 bytes: start from the
625offset basis `0x{FNV1A64_OFFSET:016x}`; for each byte, XOR it in, then
626multiply by the prime `0x{FNV1A64_PRIME:016x}` modulo 2^64. The result is
627written as 16 lowercase hex digits.
628
629For `wire.sample` above, each service's normal form, byte for byte, and its
630hash:
631"#
632    );
633    for (name, form, hash) in &contracts {
634        let _ = write!(out, "\n`{name}`, hash `{hash}`:\n\n```text\n{form}\n```\n");
635    }
636    let _ = write!(
637        out,
638        r#"
639| Changes the hash | Does not |
640|---|---|
641| the service's name; a parameter's name, order or type; the return type | field order; variant order; predicate order or duplicates |
642| a field added, removed, renamed or retyped; a variant added, removed, renamed or re-shaped | an opaque type's predicates |
643| a refined type's or an inline field refinement's predicates | doc comments; `given`; `by c: Caller` |
644| renaming a named type | field defaults |
645
646The normal form and the hash function are part of the wire version: a client
647built once embeds hashes, so they change only with a new wire version.
648
649## Authentication
650
651Every call is signed. The callee refuses a call whose signature does not verify
652with a bare `401`, before it looks at anything but the method and the body's
653size, so a sender without a key learns nothing: not which services exist, not
654a contract hash.
655
656**Keys.** One key per *edge*, a caller and a callee. The callee holds it as
657`{EDGE_FROM_PREFIX}<CALLER>` and the caller as `{EDGE_TO_PREFIX}<CALLEE>`, where
658`<CALLER>` and `<CALLEE>` are the qualified context names upper-cased with each
659dot written `__` (`shop.orders` → `{from_example}`, `shop.payment` →
660`{to_example}`). A name the key can be named from is dot-separated segments of
661lowercase letters and digits, each starting with a letter, with single
662underscores between runs. `bynk deploy` mints each key and sets it on both
663Workers; `bynk dev` uses a fixed local key.
664
665**Signing.** The caller sends `{CALLER_HEADER}` (its qualified name),
666`{TIMESTAMP_HEADER}` (whole seconds since the Unix epoch) and `{SIGNATURE_HEADER}`:
667HMAC-SHA256, under the edge's key, of the *signing input*, written as 64
668lowercase hex digits. The signing input is these fields, one per line,
669separated by `\n` with no trailing newline:
670
6711. `{SIGNING_SCHEME}`
6722. the caller, as sent in `{CALLER_HEADER}`
6733. the callee's qualified context name
6744. the method
6755. the URL's pathname exactly as sent, with no query string and no
676   percent-normalisation
6776. `{CONTRACT_HEADER}` as sent, empty when absent
6787. `{WIRE_HEADER}` as sent, `1` when absent
6798. `{TIMESTAMP_HEADER}` as sent
6809. the SHA-256 of the body's bytes, 64 lowercase hex digits
681
682For example, `shop.orders` calling `authorise` on `shop.payment` with an empty
683body signs:
684
685```text
686{input_example}
687```
688
689**Verifying.** The callee reads the body under its cap (1 MiB, {MAX_BODY_BYTES}
690bytes, else `413`), picks the key it holds for the edge from `{CALLER_HEADER}`,
691and refuses (`401`) when it holds none, when `{TIMESTAMP_HEADER}` is more than
692{SIGNATURE_WINDOW_SECONDS} seconds from its clock either way, or when the
693signature does not match, compared in constant time. So:
694
695- **A key's holder can claim only its own name.** A caller's name is the
696  handler's `c.identity`, proved by the key, not claimed.
697- **A key binds one callee.** The callee's name is signed, so a request cannot
698  be replayed against another callee, even one that holds the same key.
699- **Every header the callee acts on is signed**, so none can be changed in
700  flight: not the contract, not the wire version.
701- **A captured call can be replayed inside the window.** The timestamp bounds
702  replay; nothing stops it within those {SIGNATURE_WINDOW_SECONDS} seconds. An
703  `on call` handler with side effects that must happen once should make itself
704  idempotent (the `Idempotency` capability).
705- **An undeclared caller cannot call.** Without a key on the callee there is
706  nothing to verify against, whether or not the handler reads its caller.
707
708### Foreign callers
709
710Code that is not Bynk calls a context the same way, once the context declares
711it as a **foreign caller**:
712
713```text
714actor Legacy {{ auth = Foreign(secret = "LEGACY_CALL_KEY") }}
715
716service legacy {{
717  on call(n: Int) -> Effect[Result[Int, String]] by Legacy {{ Ok(n) }}
718}}
719```
720
721The foreign caller sends its actor's name (`Legacy`) as `{CALLER_HEADER}` and
722signs exactly as above, under the declared secret, which the callee holds as
723`LEGACY_CALL_KEY`. `bynk deploy` mints that secret and prints the
724`wrangler secret put` that hands it to the foreign Worker's operator. An
725actor's name starts with a capital letter, so it is never a context's: a
726foreign caller is checked under its own secret and can never pass for a Bynk
727context, nor a context for it.
728
729A verified caller still has to be one the service's handler admits. A Bynk
730context is admitted by `Caller` (the default, with no `by`), a foreign caller
731only by its own actor; `by who: Caller | Legacy` admits both and tells them
732apart. Anyone else gets a bare `403`.
733
734A TypeScript Worker need not speak any of this by hand. For each context with
735a foreign caller the build emits a typed client, `bynk-client.ts`, which
736signs, stamps the contract hash and encodes exactly as this page describes;
737see [TypeScript client](/book/reference/typescript-client/).
738"#
739    );
740    out
741}