protocol reference
Validate JSON messages, invite links and input at a trust boundary. Import from @controller-plus/sdk/protocol, the package root, or /sdk/src/protocol.mjs. See Wire protocol v1 for the full connection sequence and wire for binary codecs.
Constants and errors
| Export | Value |
|---|---|
VERSION |
1, the JSON protocol version. |
SDK_VERSION |
'0.1.0', the source SDK/browser version, separate from app versions. |
MAX_FRAME_BYTES |
16,384, UTF-8 bytes per client JSON frame. |
CONTROL_INPUT_LIMITS |
Frozen { axisCount: 4, buttonCount: 16, motionMagnitude: 100 }. |
APP_PLATFORMS |
Frozen ['ios', 'android', 'web', 'desktop']. |
NETWORKS |
Frozen ['wifi', 'cellular', 'ethernet', 'other', 'unknown']. |
new ProtocolError(code, message) |
Error with name: 'ProtocolError', supplied code and message. |
Frame validation
parseFrame(raw: string | ArrayBuffer | ArrayBufferView): object decodes UTF-8, checks byte length, parses JSON and requires a non-array object, string type, and v === 1. Returns the parsed object, retaining other fields. It does not validate a specific message type.
validateHello(frame): frame requires type: 'hello' and role host, player or spectator. Normal joins require string room and token, each 1–256 characters. This checks shape, not whether credentials are authentic. It does not replace parseFrame's version check.
| Optional hello field | Validation |
|---|---|
resume |
String at most 256 characters. |
name |
String at most 24 characters. |
capabilities |
At most 24 strings, each at most 48 characters. |
app |
Supported platform and valid plain version; invalid values are deleted, not rejected. |
network |
One of NETWORKS; invalid values are deleted. |
pairing |
Player-only { id, secret }; ID [A-Za-z0-9_-]{8,64}, secret string 20–128 characters. Replaces room/token checks; only supporting desktop endpoints accept saved pairing. |
remember |
{ device, name?, platform? }; device uses the pairing ID syntax, name ≤48, platform ≤24. |
validateEvent(frame): frame requires event name [a-z][a-z0-9_.-]{0,47} and a non-array object payload. Missing payload is set to {}. It mutates and returns the frame. It does not validate game rules, the frame's type, or its envelope version.
Input validation
applyInput(previous, frame): ControlState returns new arrays, using prior values (or neutral) for absent fields. It requires an integer seq from 0 to 2,147,483,647 but does not decide whether it is newer; the host owns ordering.
| Field | Validation |
|---|---|
axes |
If present, exactly four finite numbers −1…1. |
buttons |
If present, exactly sixteen finite numbers 0…1. |
motion.gyro, motion.accel |
If present, three finite numbers −100…100 each. |
motion.orientation |
If present, four finite numbers −1…1. Quaternion normalization is not checked. |
Absent motion retains a clone of prior motion. Present motion replaces it with only recognized vectors; {} clears the old vectors. Unknown input fields are ignored. This is JSON delta behavior; every binary input frame is a full state.
Join URLs and manifests
joinURL(origin, room, token, { path = '/ws', page = '/join' } = {}): string creates an HTTP(S) URL with v, room, token in its fragment and a path field only for nondefault paths. path must match ^/(?!/)[A-Za-z0-9_/-]{1,127}$. The builder validates the scheme/path, but not the room/token syntax: use actual credentials from your host.
parseJoinURL(value): { v: 1, origin, endpoint, room, token } requires HTTP(S), no username/password, URL length ≤2048, room [A-Z0-9]{4,12}, token [A-Za-z0-9_-]{20,128}, and v=1. Duplicate v, room, token, path, tlsPort or pin is rejected. The returned ws/wss endpoint uses the same origin and validated path. Pin/TLS fields are not applied by this JavaScript parser; native pinned desktop pairing has its own implementation.
manifest(game, origin, room): ConnectionManifest returns version 1, session ID, game { id, name }, one developer-or-local WebSocket route at /ws with scope developer, priority 0 and codec json-v1. Policy is { allowOurRelay: false, desktopControl: false }. Required capability is input.touch; optional capabilities are input.gamepad, sensor.gyroscope, sensor.accelerometer. controls is copied from game; privateView defaults to 'none'. This helper has no custom path option.
App versions
| Signature | Return and defaults |
|---|---|
parseVersion(value): number[] | null |
Three numeric components, each 1–4 digits. Rejects prefixes, suffixes and prereleases. |
compareVersions(a, b): -1 | 0 | 1 |
Compare components numerically; invalid version treated as 0.0.0. |
validateRequires(requires): object | null |
Null/undefined or empty requirements return null. Only controller allowed: a version string or a map of ios/android versions. Returns a copy; invalid input throws invalid_requires. |
updateNeeded(app, requires): string | null |
Minimum version if an identified iOS/Android app is too old, otherwise null. Browser, missing app and other platforms pass. Invalid required version produces no gate. |
Errors
| Code | Cause |
|---|---|
frame_too_large, invalid_json, invalid_frame, version_mismatch |
Envelope cannot be parsed/accepted. |
invalid_hello, invalid_name, invalid_capabilities, invalid_resume |
Invalid hello fields. |
invalid_sequence, invalid_axes, invalid_buttons, invalid_motion |
Invalid input fields. |
invalid_event |
Bad event name or non-object body. |
invalid_endpoint, invalid_qr |
Invalid join address/path. URL construction can also throw a native TypeError. |
invalid_requires |
Invalid minimum-app requirements. |
This module emits no events and performs no network requests. Authentication/authorization and rate limits belong to the host.
Validate a handshake and input
import { joinURL, parseJoinURL, parseFrame, validateHello, validateEvent, applyInput,
manifest, parseVersion, compareVersions, validateRequires, updateNeeded
} from '@controller-plus/sdk/protocol';
const url = joinURL('https://game.example', 'ABC123', 'example_player_token_123456');
const address = parseJoinURL(url);
const hello = validateHello(parseFrame(JSON.stringify({
v: 1, type: 'hello', role: 'player', room: address.room, token: address.token, name: 'Ana',
})));
const input = applyInput(null, { seq: 0, axes: [1, 0, 0, 0] });
console.log(hello.name, input.axes, validateEvent({ event: 'vote', payload: { target: 'p2' } }));
console.log(manifest({ id: 'quiz', name: 'Quiz', controls: {} }, address.origin, address.room));
const requires = validateRequires({ controller: '1.0.0' });
console.log(parseVersion('1.0.0'), compareVersions('1.0.0', '0.9.0'));
console.log(updateNeeded({ platform: 'ios', version: '0.9.0' }, requires)); // 1.0.0