# 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](protocol.md) for the full connection sequence and [wire](sdk-wire.md) 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

<!-- example: protocol -->
```js
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
```
