@signalling/sdk
v1.0.1
Published
Browser-first JavaScript SDK for the Confrnce signalling backend (REST + DPoP + WebSocket + WebRTC). Self-contained — vendor or publish without cloning the server repo.
Maintainers
Readme
@signalling/sdk
A browser-first JavaScript SDK for the Confrnce signalling API. It is explicitly designed as an API wrapper, not a parallel stack — every public method maps onto a real route or wire-protocol artefact on the server. The integrator writes their own UI; the SDK handles auth, DPoP signing, the WebSocket handshake, WebRTC peer orchestration, and room state.
Standalone distribution
This folder is self-contained. Third parties can vendor it alone (copy the
directory, npm install, npm run build) or split it into a dedicated Git
repository — no backend or monorepo checkout is required. Protocol details
(DPoP, WebSocket handshake) are summarized under docs/ in this package.
Internal reference: If you also maintain the server repo, architectural notes may live alongside the backend (e.g.
AUTH_FLOW.md). This README is the SDK consumer entry point.
Table of contents
- Why this SDK
- What's inside
- Installation
- Quickstart
- Public API
- Backend contract this SDK wraps
- Documentation
Why this SDK
Without the SDK, an integrator has to:
- run the OIDC + PKCE handshake themselves;
- generate a non-extractable P-256 keypair, persist it in IndexedDB, never let it leave the device;
- sign every outbound HTTP request with a fresh DPoP JWT, including the
htm/htu/iat/jti/nonceclaims; - handle
Use-DPoP-Nonceretry loops; - run a two-stage WebSocket handshake (HTTP ticket + WS upgrade + DPoP first
frame within 5 s + ack), strip query / fragment from
htu, decode the ticket nonce out of the JWT body, and react to a dozen 4xxx close codes; - wire
RTCPeerConnectioninstances, fan SDP offers / answers / ICE candidates over the room WebSocket, fall back to TURN when ICE fails, surface the local screen-share lifecycle.
This SDK does all of that for you. Under the hood, every call ultimately hits the backend’s HTTP routes and WebSocket wire format your server documents.
What's inside
src/
api/ REST client (DPoP-signed; nonce-retry)
auth/ Login / bind / logout / me
dpop/ WebCrypto P-256 keypair + JWS proofs (IndexedDB-persisted)
websocket/ Ticket → upgrade → DPoP first-frame → ack
room/ REST wrapper + local clientId tracking
webrtc/ Peer orchestration over the room WebSocket
devices/ getUserMedia / getDisplayMedia / device enumeration
internal/ base64url, logger, typed event emitter (NOT exported)
types/ Public domain types + error classes
index.ts SignallingSDK + Call compositionInstallation
npm install @signalling/sdkThe package ships ESM (dist/index.mjs), CJS (dist/index.js), and TS
declarations. No runtime dependencies are required.
Quickstart
import { SignallingSDK } from "@signalling/sdk";
// 1. Initialise (eagerly loads the DPoP keypair from IndexedDB)
const sdk = await SignallingSDK.create({
baseUrl: "https://api.example.com",
authMode: "bff",
logLevel: "info",
});
// 2. Authenticate (BFF redirect → Keycloak → /auth/complete page)
if (!(await sdk.auth.isAuthenticated())) {
// First load: send the user to Keycloak.
sdk.auth.login("/dashboard");
// ...
}
// 3. On the SPA's /auth/complete page:
const bindToken = sdk.auth.bindTokenFromFragment();
if (bindToken) {
const { returnTo } = await sdk.auth.completeBind(bindToken);
window.location.assign(returnTo || "/");
}
// 4. Create or join a room.
const room = await sdk.rooms.create(); // { roomId, clientId }
const call = await sdk.calls.joinRoom({
roomId: room.roomId,
media: { audio: true, video: true },
});
// 5. React to events.
call.on("peer.joined", (p) => console.log("joined:", p.clientId));
call.on("stream.added", ({ clientId, stream }) => attachToDom(clientId, stream));
call.on("peer.left", (p) => removeFromDom(p.clientId));
call.on("connection.closed", ({ code }) => console.warn("WS closed:", code));
// 6. Tear down on unmount.
await call.leave();Public API
class SignallingSDK {
static create(config: SignallingSDKConfig): Promise<SignallingSDK>;
// Module entry points (all stateful).
auth: AuthClient; // login / completeBind / logout / me / updateProfile
rooms: RoomManager; // create / join / view / leave / screenshare / permissions
devices: DeviceManager; // getLocalStream / getScreenShare / listInventory
api: ApiClient; // raw REST surface (escape hatch)
ws: WebSocketClient; // raw WebSocket (escape hatch)
webrtc: WebRTCManager; // raw RTCPeerConnection orchestration
dpop: DPoPManager; // raw DPoP signer
logger: Logger;
// High-level orchestration: join + ws + webrtc in one call.
calls: { joinRoom(config: CallConfig): Promise<Call> };
}
class Call extends TypedEventEmitter<CallEventMap> {
readonly roomId: number | string;
readonly clientId: string;
localStream: MediaStream | null;
setMicEnabled(on: boolean): void;
setCameraEnabled(on: boolean): void;
setLocalStream(s: MediaStream | null): Promise<void>;
startScreenShare(): Promise<MediaStream>;
stopScreenShare(): Promise<void>;
leave(): Promise<void>;
}The full strongly-typed event map (CallEventMap, WebRTCEventMap,
WebSocketEventMap, AuthEventMap) is exported from the entry point.
Backend contract this SDK wraps
Every public method ultimately reaches one of:
| SDK method | Backend route |
|---------------------------------------------|-------------------------------------------------------------------|
| sdk.auth.login() | GET /auth/login (browser navigation) |
| sdk.auth.completeBind(token) | POST /auth/dpop/bind |
| sdk.auth.getCurrentUser() | GET /me |
| sdk.auth.updateProfile(...) | PATCH /me |
| sdk.auth.logout() | POST /auth/logout |
| sdk.auth.logoutAll() | POST /auth/logout-all |
| sdk.auth.openAccountConsole() | GET /auth/account (browser navigation) |
| sdk.rooms.create() | POST /createroom |
| sdk.rooms.join(id) | POST /joinroom/{roomId} |
| sdk.rooms.view(id) | GET /viewroom/{roomId} |
| sdk.rooms.leave() | DELETE /leaveroom/{roomId}/{clientId} |
| sdk.rooms.startScreenShare() | POST /room/{roomId}/{clientId}/screen-share/start |
| sdk.rooms.stopScreenShare() | POST /room/{roomId}/{clientId}/screen-share/stop |
| sdk.rooms.myPermissions() | GET /room/{roomId}/{clientId}/permissions |
| sdk.rooms.updatePermissions(...) | PUT /room/{roomId}/{clientId}/permissions |
| sdk.api.getTurnCredentials() | POST /v1/turn/credentials |
| sdk.api.searchUserByEmail(email) | GET /users/search?email=... |
| sdk.api.issueWSTicket() | POST /auth/ws-ticket |
| Internal — used by ws.connectRoom | WS /ws/{roomId}/{clientId}?ticket=... |
| Internal — used by ws.connectGlobal | WS /global/ws?ticket=... |
Every authed REST call carries a DPoP: <jws> header generated fresh by
DPoPManager. Every WS upgrade is followed by a single
{ "type": "dpop_handshake", "proof": "..." } frame; the SDK waits for
{ "type": "dpop_handshake_ack" } before passing application messages
through.
Documentation
The docs/ folder contains task-oriented guides:
docs/quickstart.md— installation, init, first calldocs/auth.md— BFF flow, Bearer mode for native apps, DPoP, completeBind, logoutdocs/deployment.md— publish + consume the SDK, Keycloak prerequisites for both modesdocs/rooms-and-calls.md— rooms, events, screen sharedocs/media.md— device enumeration, getUserMedia, TURNdocs/api-reference.md— full method indexdocs/troubleshooting.md— common errors + fixes
If you change a public method here, update the matching doc in the same PR. The docs are checked manually in code review against the shipped surface. "# signalling-js-sdk"
