Skip to main content
Every Crosslink implementation — JavaScript, Swift, Kotlin, Rust, or a language none of the current SDKs cover — must agree on how a message is serialized, framed, and rejected. @crosslink/conformance is the referee: it runs one language-neutral corpus against any implementation and reports byte-exact differences instead of “looks right.”

The adapter contract

An implementation proves compliance by implementing four operations:
errorCode is what makes negative fixtures portable — every SDK maps its own exception type to the same small set of wire error codes, so a Swift CrosslinkProtocolError.invalidJSON and a TypeScript CrosslinkError with code invalid_message are asserted against the same expected string.

The corpus

Positive and negative fixtures live in separate files and are combined at test time:
  • packages/protocol/fixtures/messages-v1.json — one entry per message kind (req, res, err, chunk-binary, end, event, sub, cancel, ping), each with the input object, its exact canonical JSON string, and the exact frame bytes as hex.
  • packages/conformance/fixtures/invalid-v1.json — malformed inputs (truncated JSON, an unsupported protocol version, an unknown message type, a request missing its id) paired with the error code every implementation must raise.
Positive fixtures are regenerated from the TypeScript reference implementation with npm run gen:fixtures -w @crosslink/protocol — never hand-edited. Negative fixtures are curated by hand, since they encode intentionally malformed input.

Running the report

Each positive fixture is checked three ways — canonicalJson(obj) matches exactly, encodeFrame(obj) matches the expected hex bytes, and round-tripping through decodeMessage then re-encoding reproduces the same canonical string. Each negative fixture must throw, and the thrown error’s errorCode must equal the fixture’s expected code; an adapter that silently accepts malformed input is reported as a failure with actual: "accepted".
report.passed counts every check that succeeded (cases.length * 3 + invalid.length on a fully-passing adapter), so a partial pass is visible at a glance without diffing.

Wiring it into a new SDK

The TypeScript reference lives in packages/protocol, and the pattern each native SDK follows is the same one shown in Native protocol SDKs: implement the four adapter operations, load both fixture files, and call runConformance from that language’s own test runner. The Rust crate does it in sdks/rust/tests/conformance.rs, driven by cargo test; Kotlin and Swift do the equivalent in their own test targets.

What conformance does not cover

Conformance establishes wire compatibility only — that two implementations serialize and parse the same bytes for the same logical message. It does not replace:
  • Handshake tamper tests — CLX1’s signature and transcript-binding properties (see Encryption)
  • Transport end-to-end tests — actually opening a WebSocket/WebRTC connection and completing a session
  • Fuzzing — conformance fixtures are curated, not randomly generated
  • Application capability tests — permission gating happens above the wire protocol (see Capabilities and RPC)
A new SDK that passes conformance has a correct wire protocol implementation and nothing more; it is not yet a usable Crosslink client until it also implements transport selection, the handshake, and identity persistence.