# Referência de API [#referência-de-api] > Pergunta: "qual a assinatura exata de `createHost`/`joinRoom`/etc.?" **Gerada** a partir do TSDoc dos três pontos de entrada do pacote (`.`, `./react`, `./qr`) — não editar à mão. Rode `cd packages/event-room && npm run docs:generate` depois de mudar uma assinatura ou comentário público; `npm run build` (no mesmo diretório) falha se este arquivo divergir da regeneração. ## Core (`@sofya-sdk/event-room`) [#core-sofya-sdkevent-room] ### `AccessMode` [#accessmode] ```ts export type AccessMode = 'open' | 'code' | 'approval' | 'approval_and_code'; ``` ### `buildJoinUrl` [#buildjoinurl] Builds the URL a host prints into a QR: `guestUrl` with the join target hung on its query string. Never appends a path and never resolves relatively against `guestUrl` — `new URL('join', 'myapp:')` throws and `new URL('join', 'myapp://')` produces a triple slash (19-esquema-do-guesturl F3) — so the integrator's address is always the final address, and the library only ever calls `searchParams.set` on it. Never `append`, either: it would duplicate the key and the guest's `get` would read the old value back. `buildJoinUrl(guestUrl: string, target: JoinTarget): string` ### `buildServerError` [#buildservererror] Builds the shared `EventRoomError` for a server-sent code at a call site that has a promise to reject. `buildServerError(code: string, message?: string, context?: Record): EventRoomError` ### `classifyServerError` [#classifyservererror] Classifies a `code` (+ optional raw `message`) the server sent with no specific call already narrowing it — the generic, never-silenced "service/unknown failure" observation this ticket owns. An unknown code always resolves non-terminal/non-retryable with a generic remedy plus the raw message attached, never dropped. `classifyServerError(code: string, message?: string): ResolvedServerError` ### `CONTRACT_DIGEST_MAP_BUDGET_BYTES` [#contract_digest_map_budget_bytes] `const CONTRACT_DIGEST_MAP_BUDGET_BYTES: number` ### `ContractEvent` [#contractevent] ```ts export interface ContractEvent { readonly layer: 'shape' | 'standard-schema' | 'opaque'; readonly digest: string | 'opaque'; validate(value: unknown): ValidationResult; } ``` ### `ContractEventInput` [#contracteventinput] ```ts export type ContractEventInput = ShapeNode | StandardSchemaV1 | OpaquePayload; ``` ### `ContractMapInput` [#contractmapinput] ```ts export type ContractMapInput = Record; ``` ### `createErrorDeduper` [#createerrordeduper] `createErrorDeduper(): ErrorDeduper` ### `createHost` [#createhost] `createHost` opens the transport and the first room in a single call (spec §2.2/D1 of ticket 18): `startRoom` exists only for restarts. `createHost(options: CreateHostOptions): Promise<{ host: Host; room: Room; }>` ### `CreateHostOptions` [#createhostoptions] ```ts export interface CreateHostOptions { url: string; credential: string; guestUrl: string; displayName?: string; protocolEvent?: string; peerReadinessTimeoutMs?: number; room: RoomArgs; contract?: EventContract; _connection?: HostConnectionFactory; } ``` ### `CreateRoomResult` [#createroomresult] ```ts export interface CreateRoomResult { roomId: string; accessMode: AccessMode; effectiveMaxGuests: number | null; joinPayload: Record; creatorToken: string; } ``` ### `defineContract` [#definecontract] `defineContract(map: C): EventContract` ### `deriveDisplayStatus` [#derivedisplaystatus] Read-only view for a single display indicator. Never the source of truth — callers that need both axes read `getStatus()` directly. `deriveDisplayStatus(status: RoomSessionStatus): TransportState | MembershipState` ### `DisplayNameRequiredError` [#displaynamerequirederror] `class DisplayNameRequiredError extends EventRoomError` `new DisplayNameRequiredError(accessMode: AccessMode): DisplayNameRequiredError` ### `EndedReason` [#endedreason] The 9 terminal reasons. `contract_mismatch` and `protocol_version_mismatch` are asymmetric by construction: only a `RoomSessionStateMachine` can reach them — there is no server verb to expel an already-admitted guest, so the host's own connection (which has no membership axis at all) never reaches this terminal value either way. ```ts export type EndedReason = "left" | "room_closed" | "room_expired" | "creator_replaced" | "rejected" | "idle_timeout" | "control_lost" | "contract_mismatch" | "protocol_version_mismatch"; ``` ### `ErrorCatalogEntry` [#errorcatalogentry] ```ts export interface ErrorCatalogEntry { category: EventRoomErrorCategory; remedy: string; retryable?: boolean; } ``` ### `ErrorDeduper` [#errordeduper] Deduplication (10-superficie-de-erro): an unsolicited `{type:'error', code}` describing a still-open condition and a later signal reporting the exact same `code` again are one underlying condition, not two — a caller should see it reported once, not spammed on every wire frame. Reporting a *different* code, or the same code again after something cleared the condition (room closed/recreated, control resumed, etc.), always goes through. Deliberately tiny and call-site-owned — this is not a generic error handler; each connection wires its own deduper instance around whichever of its own signals (a repeated error frame, a close that follows one for the same reason) it knows can coincide. ```ts export interface ErrorDeduper { shouldReport(code: string): boolean; clear(): void; } ``` ### `EventContract` [#eventcontract] ```ts export type EventContract = { readonly events: { [K in keyof C]: ContractEvent>; }; readonly digestMap: Readonly>; readonly _payloads: { [K in keyof C]: InferEventPayload; }; }; ``` ### `EventRoomError` [#eventroomerror] `class EventRoomError extends Error` `new EventRoomError(init: EventRoomErrorInit): EventRoomError` ```ts readonly code: string; readonly category: EventRoomErrorCategory; readonly remedy: string; readonly retryable: boolean; readonly serverMessage?: string; readonly context?: Record; ``` ### `EventRoomErrorCategory` [#eventroomerrorcategory] Minimal error shape used by `defineContract` (ticket 04). The full catalog (registry of \~30 codes, deduplication, anti-divergence check) is ticket 10's job — this is deliberately just enough of the agreed shape (`code`, `category`, `remedy`, `retryable`, `context?`) for the two synchronous "usage" refusals this ticket owns, so ticket 10 can absorb it without a breaking shape change. ```ts export type EventRoomErrorCategory = 'usage' | 'flow' | 'ended' | 'infra'; ``` ### `EventRoomErrorInit` [#eventroomerrorinit] ```ts export interface EventRoomErrorInit { code: string; category: EventRoomErrorCategory; remedy: string; retryable?: boolean; serverMessage?: string; context?: Record; } ``` ### `getEventPayloadBudgetBytes` [#geteventpayloadbudgetbytes] The exact number of bytes left for `payload` once the envelope's own `kind`/`name` framing is subtracted from the 16 KiB wire budget — the budget this ticket exposes to the caller instead of making them discover it by trial and error. `name` is required to be ASCII (server regex `^[a-zA-Z0-9_.:-]{1,48}$`), so the JSON scaffolding around it is exactly one byte per character and the arithmetic below is exact, not an estimate. `getEventPayloadBudgetBytes(name: string): number` ### `GuestCredentialRequiredError` [#guestcredentialrequirederror] Every one of these extends the shared `EventRoomError` shape (`code`/`category`/`remedy`/`retryable`) so callers can catch on that base class alone; each subclass just fixes its own `code`/`category`/ `remedy` and adds whatever extra context is useful. Every one of these is thrown synchronously, before any I/O. `class GuestCredentialRequiredError extends EventRoomError` `new GuestCredentialRequiredError(): GuestCredentialRequiredError` ### `GuestHandle` [#guesthandle] ```ts export interface GuestHandle { sendEvent(name: string, payload: unknown): void; getStatus(): RoomSessionStatus; close(): void; } ``` ### `GuestRole` [#guestrole] `ClientRole` is an open string on the wire — the server never validates it (`room_registry.py`). This enum exists only on the SDK, so `joinRoom` checks it at runtime too, not just via the static type. ```ts export type GuestRole = 'desktop' | 'mic' | 'unknown'; ``` ### `GuestRoleRequiredError` [#guestrolerequirederror] `class GuestRoleRequiredError extends EventRoomError` `new GuestRoleRequiredError(): GuestRoleRequiredError` ### `GuestUrlReservedParamError` [#guesturlreservedparamerror] `class GuestUrlReservedParamError extends EventRoomError` `new GuestUrlReservedParamError(param: string): GuestUrlReservedParamError` ```ts readonly param: string; ``` ### `Host` [#host] ```ts export interface Host { readonly transport: HostTransportStateMachine; startRoom(args: RoomArgs): Promise>; close(): Promise; } ``` ### `HostConnection` [#hostconnection] The seam between this module's lifecycle rules and the wire. Deliberately shaped like the 05 prototype's `connectHost`/handle split (`transport.mjs`), translated into something DI-friendly: every method that talks to the server is async and named for the server verb it performs. ```ts export interface HostConnection { connect(): Promise; isOpen(): boolean; create(args: { roomId: string; accessMode: AccessMode; maxGuests?: number; }): Promise<{ ok: true; result: CreateRoomResult; } | { ok: false; collision: boolean; }>; closeRoom(): Promise; disconnectTransport(): void; resume(args: { roomId: string; creatorToken: string; }): Promise<{ ok: true; } | { ok: false; reason: 'control_lost'; }>; onJoinRequest(cb: (request: JoinRequest) => void): void; approveJoin(requestId: string): void; rejectJoin(requestId: string): void; onNonTerminalObservation(cb: (observation: NonTerminalObservation) => void): void; onTerminal(cb: (reason: 'control_lost' | 'room_expired') => void): void; onTransportDropped(cb: () => void): void; sendEvent(name: string, payload: unknown): void; onAppEvent(cb: (event: { name: string; payload: unknown; }) => void): void; getPeerFingerprint(): ContractDigestMap | null; resetForNewRoom(): void; } ``` ### `HostConnectionFactory` [#hostconnectionfactory] ```ts export interface HostConnectionFactory { (init: { url: string; credential: string; displayName?: string; protocolEvent?: string; contractDigestMap?: ContractDigestMap; }): HostConnection; } ``` ### `HostTransportStateMachine` [#hosttransportstatemachine] The host's own connection has only the transport axis. It has no membership and therefore no `ended` — by type, not just by runtime refusal. `class HostTransportStateMachine` `new HostTransportStateMachine(): HostTransportStateMachine` ```ts readonly transportAxis: unknown; ``` ### `InferShape` [#infershape] ```ts export type InferShape = S extends PrimitiveToken ? InferPrimitive : S extends readonly [ infer Only ] ? InferShape[] : S extends readonly [ infer A, ...infer Rest ] ? InferShape | InferShape : S extends readonly (infer El)[] ? InferShape[] : S extends { readonly [key: string]: ShapeNode; } ? { [K in keyof S]: InferShape; } : never; ``` ### `InvalidGuestUrlError` [#invalidguesturlerror] `class InvalidGuestUrlError extends EventRoomError` `new InvalidGuestUrlError(guestUrl: string): InvalidGuestUrlError` ### `InvalidJoinTargetError` [#invalidjointargeterror] `class InvalidJoinTargetError extends EventRoomError` `new InvalidJoinTargetError(reason: string): InvalidJoinTargetError` ### `JOIN_TARGET_FORMAT_VERSION` [#join_target_format_version] The one query-param format version this parser recognizes (09-superficie-guest / 08-qr-e-handoff). `const JOIN_TARGET_FORMAT_VERSION: "1" = '1'` ### `JoinCodeRequiredError` [#joincoderequirederror] Thrown wherever a `JoinTarget` is accepted as a value (not parsed from a URL) and its `accessMode` requires a `joinCode` that is missing or empty: `buildJoinUrl` and `joinRoom`'s hand-built-target path (`./guest.ts`) both throw this exact class, via `assertJoinCodePresentIfRequired` below, so the requirement is enforced identically wherever a `JoinTarget` value enters the library rather than being re-derived per call site. `class JoinCodeRequiredError extends EventRoomError` `new JoinCodeRequiredError(accessMode: AccessMode): JoinCodeRequiredError` ```ts readonly accessMode: AccessMode; ``` ### `JoinFailedError` [#joinfailederror] Wraps whatever `{type:'error', code, message}` the server sent back for this join attempt. Always rejects `joinRoom`'s own promise — this is the "journey condition rejects the causing call" category (`'flow'`) regardless of the code's canonical catalog bucket, since it is, by construction, the response to this specific call. The remedy still comes from the shared catalog (10-superficie-de-erro) — including the "usage code from the server = library bug" remedy override — so the same code never gets a different explanation depending on where it surfaces. `class JoinFailedError extends EventRoomError` `new JoinFailedError(code: string, serverMessage?: string): JoinFailedError` ### `JoinRejectedError` [#joinrejectederror] Rejects the joinRoom call that caused it (spec §5.2: "an error that responds to a call returns via that call"). Never retried automatically — a fresh admission is a new joinRoom call. `class JoinRejectedError extends EventRoomError` `new JoinRejectedError(): JoinRejectedError` ### `JoinRequest` [#joinrequest] ```ts export interface JoinRequest { id: string; displayName?: string; } ``` ### `joinRoom` [#joinroom] The guest's single entry verb. Nothing here survives a reload: `target`, `credential`, and whatever the server hands back live only in this call's closure and in the returned handle's in-memory state — there is no `localStorage`/`sessionStorage` write anywhere in this module. Deliberately not declared `async`: an `async function` returns a `Promise` even for a throw on its very first line, which turns a usage error into a rejected promise instead of a synchronous throw. Validating here, before handing off to the `async` connection work below, is what makes the synchronous-throw guarantee real rather than incidental. `joinRoom(args: JoinRoomArgs): Promise` ### `JoinRoomArgs` [#joinroomargs] ```ts export interface JoinRoomArgs { url: string; credential: string; target: JoinTarget; displayName?: string; role: GuestRole; contract?: EventContract; protocolEvent?: string; pendingApprovalTimeoutMs?: number; } ``` ### `JoinTarget` [#jointarget] ```ts export interface JoinTarget { readonly roomId: string; readonly accessMode: AccessMode; readonly joinCode?: string; } ``` ### `MalformedJoinTargetError` [#malformedjointargeterror] `class MalformedJoinTargetError extends EventRoomError` `new MalformedJoinTargetError(reason: string): MalformedJoinTargetError` ```ts readonly reason: string; ``` ### `MAX_EVENT_PAYLOAD_BYTES` [#max_event_payload_bytes] The wire limit is a server constant (16 KiB, measured on the compact JSON of `payload` only — see spec §3.7 / harness ticket 17 findings). The contract's digest map travels inside the handshake envelope (ticket 05), so it has to leave "folga" (headroom) for the envelope framing, `protocolVersion`, and future contract growth. Half the wire limit is a generous, easy-to-explain margin — not a measured number, a deliberately conservative one. `const MAX_EVENT_PAYLOAD_BYTES: number` ### `MembershipState` [#membershipstate] ```ts export type MembershipState = "unassociated" | "associating" | "awaiting_approval" | "member" | "ended"; ``` ### `NonTerminalObservation` [#nonterminalobservation] ```ts export interface NonTerminalObservation { code: string; known: boolean; } ``` ### `OpaquePayload` [#opaquepayload] ```ts export interface OpaquePayload { readonly [OPAQUE_MARKER]: true; readonly __type?: T; } ``` ### `parseJoinTarget` [#parsejointarget] Reads a scanned QR/deep-link string (or an equivalent string assembled for a typed code) and returns the target it encodes, or throws a named error — never silently. Same function for both origins: the QR is just UX over the identical `{roomId, accessMode, joinCode?}` shape a typed code assembles by hand. Deliberately reads only `searchParams` — never `.host`, `.hostname`, `.pathname`, or `.protocol` — because a custom (non-special) scheme assigns the same text segment to different `URL` properties depending on slash count, and does not case-fold `.host` the way `http(s)` does (19-esquema-do-guesturl F2). Comparing the scanned string against an expected `guestUrl` is therefore impossible, not just unperformed: there is no canonical form to compare against. `parseJoinTarget(scanned: string): JoinTarget` ### `payload` [#payload] Type-only opt-out: no runtime validation, documented as giving up the fingerprint's network for this event. `payload(): OpaquePayload` ### `PayloadTooLargeError` [#payloadtoolargeerror] Thrown by `sendEvent` when the serialized envelope — measured exactly like the server measures it, not the outer wire frame — exceeds the 16 KiB budget. There is no slicing: the app owns the semantics of what it sends, the library only owns the byte count. `class PayloadTooLargeError extends EventRoomError` `new PayloadTooLargeError(actualBytes: number, budgetBytes: number): PayloadTooLargeError` ```ts readonly actualBytes: number; readonly budgetBytes: number; ``` ### `REFUSED_GUEST_URL_SCHEMES` [#refused_guest_url_schemes] The fixed, closed, non-configurable blacklist of schemes a `guestUrl` may never use (20-politica-de-esquema-do-guesturl, decision from Gabriel). Each either executes content, embeds content, or isn't an application destination at all (`file:`) — never a plausible deep-link scheme. The list does not grow by reflex: `about:`, `chrome:`, `ms-settings:` etc. all parse cleanly and are deliberately left out, because the guard defends against integrator error (a `guestUrl` template that became a `data:` URL by accident), not against an adversary who already controls the host's own code. `const REFUSED_GUEST_URL_SCHEMES: readonly ["javascript", "data", "blob", "vbscript", "file"]` ### `RESERVED_EVENT_PREFIX` [#reserved_event_prefix] Prefix the library reserves for its own envelope `kind`s (spec §2.4 D6). `const RESERVED_EVENT_PREFIX: "sofya." = 'sofya.'` ### `RESERVED_JOIN_URL_PARAMS` [#reserved_join_url_params] The four query params `joinRoom`'s wire format reserves on a `guestUrl` (09-superficie-guest / 19-esquema-do-guesturl / 20-politica-de-esquema-do-guesturl). Fixed, closed: not something an integrator's own query params are allowed to collide with (`buildJoinUrl` fails loud rather than silently overwriting). `const RESERVED_JOIN_URL_PARAMS: readonly ["v", "roomId", "accessMode", "joinCode"]` ### `ResolvedServerError` [#resolvedservererror] ```ts export interface ResolvedServerError { code: string; category: EventRoomErrorCategory; remedy: string; retryable: boolean; known: boolean; } ``` ### `Room` [#room] ```ts export interface Room { readonly roomId: string; readonly accessMode: AccessMode; readonly effectiveMaxGuests: number | null; readonly joinPayload: Record; readonly session: RoomSessionStateMachine; onJoinRequest(cb: (request: JoinRequest) => void): void; approveJoin(requestId: string): void; rejectJoin(requestId: string): void; onObservation(cb: (observation: NonTerminalObservation) => void): void; send(name: K, payload: EventContract['_payloads'][K]): void; on(name: K, cb: (payload: EventContract['_payloads'][K]) => void): void; close(): Promise; } ``` ### `RoomArgs` [#roomargs] The exact same type is used for the `room` block of `createHost` and as `startRoom`'s whole argument (spec §2.2/D3 of ticket 18) — no default inheritance between calls is possible because there is only one type, and every call site must fill it out fresh. ```ts export interface RoomArgs { accessMode: AccessMode; roomId?: string; maxGuests?: number; } ``` ### `RoomSessionStateMachine` [#roomsessionstatemachine] `class RoomSessionStateMachine` `new RoomSessionStateMachine(): RoomSessionStateMachine` ```ts readonly transportAxis: unknown; ``` ### `RoomSessionStatus` [#roomsessionstatus] ```ts export interface RoomSessionStatus { transport: TransportState; membership: MembershipState; endedReason: EndedReason | null; } ``` ### `ShapeNode` [#shapenode] The shape mini-language: a primitive token, an object (nested), or an array. A single-element array is "array of that shape"; two-or-more is a simple union of alternatives. This convention is this ticket's own call — the spec left the exact grammar in the fog (docs/specs/.../spec.md §2.4). ```ts export type ShapeNode = PrimitiveToken | readonly ShapeNode[] | { readonly [key: string]: ShapeNode; }; ``` ### `TransportState` [#transportstate] The state machine at the heart of a room session (07-maquina-de-estados). Two axes, never flattened: `transport` (nothing terminal — offline is rest, not death) and `membership` (one terminal value, `ended`, with a discriminated `reason`). The host's own connection uses `HostTransportStateMachine` instead, which has only the transport axis and therefore no way to express `ended` at all — the host's own end of life is a different kind of terminality (object death, ticket 08/21), not a value on this axis. ```ts export type TransportState = "offline" | "connecting" | "online" | "retrying"; ``` ### `UnknownGuestRoleError` [#unknownguestroleerror] `class UnknownGuestRoleError extends EventRoomError` `new UnknownGuestRoleError(role: string): UnknownGuestRoleError` ```ts readonly role: string; ``` ### `UnsupportedGuestUrlSchemeError` [#unsupportedguesturlschemeerror] `class UnsupportedGuestUrlSchemeError extends EventRoomError` `new UnsupportedGuestUrlSchemeError(scheme: string): UnsupportedGuestUrlSchemeError` ```ts readonly scheme: string; readonly refused: readonly string[]; ``` ### `ValidationResult` [#validationresult] ```ts export type ValidationResult = { success: true; data: T; } | { success: false; issues?: readonly unknown[]; }; ``` ## React hooks (`@sofya-sdk/event-room/react`) [#react-hooks-sofya-sdkevent-roomreact] ### `useGuest` [#useguest] Joins a room for the lifetime of the calling component, using whatever `args` were passed on first render. Same one-shot shape as `useHost`: `joinRoom` is a single verb, not something this hook re-invokes on prop changes. `useGuest(args: JoinRoomArgs): UseGuestResult` ### `UseGuestResult` [#useguestresult] ```ts export interface UseGuestResult { guest: GuestHandle | null; error: unknown; } ``` ### `useHost` [#usehost] Opens a host + its first room for the lifetime of the calling component, using whatever `options` were passed on first render — matching `createHost`'s own one-shot shape (§2.2/D1: it opens the transport and the first room in a single call). Re-render with different `options` does not restart anything; call `createHost` directly for that kind of control. `useHost(options: CreateHostOptions): UseHostResult` ### `UseHostResult` [#usehostresult] ```ts export interface UseHostResult { host: Host | null; room: Room | null; error: unknown; } ``` ## QR image (`@sofya-sdk/event-room/qr`) [#qr-image-sofya-sdkevent-roomqr] ### `generateQrImageDataUrl` [#generateqrimagedataurl] Renders `url` (a `buildJoinUrl` output) as a `data:image/png;base64,...` URL, directly usable as an ``. Never touches the wire, never reads or writes `target`/credential state — this is presentation only. `generateQrImageDataUrl(url: string, options?: GenerateQrImageOptions): Promise` ### `GenerateQrImageOptions` [#generateqrimageoptions] ```ts export interface GenerateQrImageOptions { size?: number; margin?: number; } ``` ### `QR_SUPPORTED_RUNTIME` [#qr_supported_runtime] The one runtime this subpath supports, declared explicitly rather than left to whatever the underlying `qrcode` dependency happens to run on. Generating a QR *image* for display is a browser-UI concern; enforced by `generateQrImageDataUrl` checking for `document` before doing anything else, so a caller using this from a non-browser runtime gets a named error instead of a dependency-specific crash. `const QR_SUPPORTED_RUNTIME: "browser"` ### `QrUrlRequiredError` [#qrurlrequirederror] `class QrUrlRequiredError extends EventRoomError` `new QrUrlRequiredError(): QrUrlRequiredError` ### `UnsupportedQrRuntimeError` [#unsupportedqrruntimeerror] `class UnsupportedQrRuntimeError extends EventRoomError` `new UnsupportedQrRuntimeError(): UnsupportedQrRuntimeError`