Reality BridgeReferenceFor contributorsApplies from 4.2.5
The connector SDK
Cette page en françaisThis page on the GitHub wiki
What every connector shares (connectors/src/sdk.ts, ADR 0008): the interface a connector implements, the context
it is given, the vocabulary of refusals, the limits, the helpers, and how the connectors are tested without a network.
For contributors who change a connector or add one; operators read Connectors overview.
since 4.1.9
RealityConnector
Form
interface RealityConnector {
readonly id: string;
start(ctx: ConnectorContext): Promise<void>;
stop(): Promise<void>;
health(): Promise<{ ok: boolean; detail: string; since: string }>;
}Values and default
start: open its port or begin polling, with the context below.stop: stop taking input, let what is in flight finish (at mostDRAIN_MS, 5 000 ms), close every socket.health: whatGET /healthanswers (200 whenok, 503 otherwise; ahealththat throws is unhealthy).
Constraints
A connector receives, validates and normalises its input, treats it as hostile, binds it to a player, gives it a
dedupeKey, and proposes; it never runs what it reads, and never sends the Bridge what it saw.
Minimal example
The Telnet connector, connectors/src/telnet/connector.ts, is the shortest of the four.
Behaviour on an unknown field
Not applicable: an interface of the code.
Since
4.1.9.
since 4.1.9
ConnectorContext
Form
Made by createContext({ bridge, gameId, tenantId?, limits?, manifest, connector, write?, now?, retries?, fetch? }).
Values and default
| Member | What |
|---|---|
bridge | { url, token }: the Bridge and the connector's attenuated Biscuit |
gameId, tenantId | The game; the tenant (default) |
limits | ConnectorLimits |
manifest | The game's Reality manifest: its signals and connectors declarations |
log(event, fields?) | A JSON line that cannot carry content (see safeLog) |
propose(signal) | ProposedSignal → { ok: true, sequence, duplicate } or { ok: false, refusal } |
pair(code) | Confirms a pairing code (the token must carry pair(true)) → { ok: true, playerId } or a refusal |
reject(reason, fields?) | An input turned down by the connector itself: counted and logged |
metrics() | { proposed, accepted, duplicates, refused, rejectedInputs, retries }, for /metrics |
Constraints
propose refuses before the Bridge a kind or a player that is not an identifier, a dedupeKey over 256 characters
(shape), a signal the manifest does not declare (not-declared), a payload that is not flat or is too large, and a
proposal over the local quota (quota). A call that times out, loses its connection, answers 5xx or answers an
acceptance without its sequence is sent again with the same key, retries times (2 by default); a refusal is never
retried. After close(), every call is refused stopped.
Minimal example
const r = await ctx.propose({
source: 'telnet', kind: 'terminal.lamp', playerId, dedupeKey: dedupeKey('telnet', `${session}:${line}`),
payload: { command: 0, line }, receivedAt: new Date().toISOString(),
});Behaviour on an unknown field
An answer from the Bridge that is not a JSON object reads as {}.
Since
4.1.9.
since 4.1.9
ProposedSignal
Form
{ source, kind, playerId, sessionId?, dedupeKey, payload, receivedAt }.
Values and default
source: the connector family, informative; the Bridge checks the manifest's.kind: the signal, as the game declares it.dedupeKey: the same fact twice has the same key,dedupeKey(source, external id).payload: flat JSON (strings, numbers, booleans, null), no markup (<or>), at mostlimits.maxBytes. It stays in the connector; the Bridge receives its SHA-256 over canonical JSON asevidenceHash.receivedAt: ISO time on the connector's clock, sent asoccurredAt(the Bridge stamps its own).
Constraints
A nested, marked-up or oversized payload is refused (shape, too-large).
Minimal example
See ConnectorContext.
Behaviour on an unknown field
Not sent: the SDK builds the Bridge's body from the fields above only.
Since
4.1.9.
since 4.1.9
RefusalCode
Form
A fixed vocabulary, never the Bridge's free text.
Values and default
| Code | From |
|---|---|
shape, too-large, quota, not-declared, stopped | The SDK, before the Bridge (shape also for a Bridge's 400, too-large for its 413) |
denied | The Bridge's 401 or 403 |
revoked | The Bridge's 401 revoked |
player | The Bridge's 404 for a player |
pairing | A malformed code; the Bridge's 404 for a code, 409 or 410 |
pending | The Bridge's 429 pending (the player's backlog) |
bridge-quota | The Bridge's other 429 |
timeout, unreachable | No answer within timeoutMs; no connection |
bridge | Another status, or a 5xx after the retries |
Constraints
Counted in metrics().refused and logged as signal.refused with the code.
Minimal example
if (!r.ok && r.refusal === 'pending') … (the player has too many signals waiting).
Behaviour on an unknown field
A status the table does not name is bridge.
Since
4.1.9.
since 4.1.9
ConnectorLimits
Form
{ maxBytes, maxPerMinute, timeoutMs }, the configuration's limits.
Values and default
maxBytes 4 096 (a payload as canonical JSON, and the largest input read at once), maxPerMinute 60 (proposals per
sliding minute, all players together; the Bridge has its own quota per token), timeoutMs 5 000 (one call to the
Bridge).
Constraints
A limit left out keeps its default.
Minimal example
"limits": { "maxBytes": 4096, "maxPerMinute": 60, "timeoutMs": 5000 }Behaviour on an unknown field
Ignored.
Since
4.1.9.
since 4.1.9
Helpers
Form
dedupeKey(source, external), canonicalJson(value), payloadProblem(payload, maxBytes), safeLog(write, connector), within(work, ms), DRAIN_MS, DEFAULT_LIMITS.
Values and default
dedupeKey: SHA-256 of<source>:<its id outside>, in hexadecimal.canonicalJson: JSON with sorted keys, so a payload's hash does not depend on the order it was built in.payloadProblem:'shape','too-large'ornull.safeLog: numbers and booleans as they are; a string only when it is an identifier (letters, digits,.,_,:,-, at most 64), never with an@, a space or a slash; anything else is[redacted]. Field names that are not identifiers are dropped.within: resolvesdoneortimeout, whichever comes first (a drain never holds a stop longer).
Constraints
None.
Minimal example
dedupeKey('email', messageId) is the email connector's key.
Behaviour on an unknown field
Not applicable.
Since
4.1.9.
Testing without a network
All in the engine repository, one suite at a time:
# in the engine repository
npx vitest run tests/connectors-*.test.ts
npm run fuzz:connectors -- --cases=200 --seconds=30 --seed=1tests/connectors-*.test.tsrun the same contract on the four connectors against a real Bridge on a free port (tests/connectors-sdk.test.ts), their hostile fixtures (connectors/test-vectors/), Telnet and SSH under floods, slow clients and a thousand connections, and the four driven under--disallow-code-generation-from-strings.npm run fuzz:connectorsfeeds the connectors' parsers seeded mutations of their corpus (--connector=email|telnet|ssh|open-badge|all,--cases10 000 by default,--seconds60,--seed,--json).- A game's recorded replays (
games/<id>/replays/*.json: connector inputs and the signals they made) are replayed through the real connector code bytests/connectors-replays.test.ts, and proved finishable bynpm run solve:reality. npm run connector-archive-e2eplays the four from their release archive against a Bridge from its own (Linux).
Related identifiers
quest-n-click-connector,npm run connector: Connectors overview.- The Bridge's side of a proposal: Guarantees.
- How tests are chosen and run: Which tests run when; mutation testing of the connectors: Mutation testing.