Handshake
The map announces itself, and a host may also ask. Either is sufficient.
1. Host loads the frame with ?embed=1&host={origin}
2. Map sends map.ready { protocol, campus, capabilities[], build? }
3. Host sends host.hello { sdk?, protocol? } optional
Map answers { protocol, campus, capabilities[], build? }
4. Map sends map.loaded { ...state } once campus data resolves
Steps 2 and 3 carry the same payload. A host may use whichever arrives first.
Why there are two paths
map.ready is a single message with no replay. A host that creates the frame,
awaits anything at all, and only then attaches its listener misses it entirely
and would wait forever for an announcement that already happened.
Whether a host wins that race depends on incidental ordering in its own
framework, which is not something an integration should have to depend on.
host.hello makes readiness pollable instead, so a listener attached late still
connects.
The command earns its place twice more. It reports the host's SDK build, which is what makes the deployed install base measurable and therefore makes it safe to keep old versions working rather than guessing. And it lets a host learn the protocol version and capability list before sending anything that depends on them.
Maps built before this command existed answer UNSUPPORTED. A host should treat
that as a signal to fall back to waiting for map.ready, which is what the SDK
does.
The readiness payload
interface MapReady {
protocol: number;
campus: string | null;
capabilities: string[];
build?: string;
}
campus may be null in map.ready. The bridge installs at boot, before
routing has resolved, and reads the slug from the path as a best effort.
map.loaded carries the authoritative state, and a host.hello answer is built
fresh at call time, so a host that asks later gets a better answer than one that
takes the announcement.
capabilities lists the command names this build accepts. Checking it before
sending is what turns an unsupported command into an immediate error rather than
a timeout.
build is present only on deployments built with a build identifier. Treat it
as opaque; it exists so that a bug report can be tied to a specific deploy.
Command queueing
Commands sent between map.ready and map.loaded are queued and drained in
order, each still receiving exactly one result. The queue holds fifty entries
for thirty seconds; on overflow or expiry the host receives NOT_READY rather
than silence.
Three commands skip the queue, because they concern the frame rather than campus
state and would otherwise be answered only after the condition they are about
had already passed: ui.setChrome, events.subscribe, and
events.unsubscribe. host.hello skips it for the same reason.
Malformed and unrecognised commands are also answered immediately. Waiting thirty seconds to be told that a reference was a number helps nobody.
Commands sent before map.ready are lost, since no listener exists yet. Send
setup in response to map.ready, or ask with host.hello.
Origin
The listener is attached only when three conditions hold: embed=1 is set, the
page is actually framed, and host parses as a valid origin. A listener that
answers nobody is a larger attack surface for no benefit, so it is not attached
at all rather than attached and rejecting.
Inbound messages are compared against host by exact string equality. Outbound
messages are posted to that exact origin, never to a wildcard.
Messages from a disallowed origin are dropped without a reply. Replying would
mean posting to an origin already judged untrusted, which confirms the map is
present and hands the sender a channel. ORIGIN_REJECTED therefore exists in
the error type but never reaches a host; it appears only in logs.