@signalling/sdk 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +202 -0
- package/dist/index.d.mts +1568 -0
- package/dist/index.d.ts +1568 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var A=Object.defineProperty;var J=Object.getOwnPropertyDescriptor;var V=Object.getOwnPropertyNames;var H=Object.prototype.hasOwnProperty;var G=(s,t)=>{for(var e in t)A(s,e,{get:t[e],enumerable:!0})},Q=(s,t,e,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of V(t))!H.call(s,i)&&i!==e&&A(s,i,{get:()=>t[i],enumerable:!(r=J(t,i))||r.enumerable});return s};var Y=s=>Q(A({},"__esModule",{value:!0}),s);var ye={};G(ye,{ApiClient:()=>P,AuthClient:()=>k,Call:()=>U,CallInviteManager:()=>T,DPoPManager:()=>v,DeviceManager:()=>M,FriendsManager:()=>R,RoomManager:()=>I,SignallingApiError:()=>w,SignallingSDK:()=>q,SignallingWebSocketError:()=>m,WSCloseCode:()=>y,WebRTCManager:()=>C,WebSocketClient:()=>b,isFatalCloseCode:()=>x});module.exports=Y(ye);var w=class extends Error{status;code;field;body;response;constructor(t,e,r){if(super(t),this.name="SignallingApiError",this.status=e.status,this.response=e,this.body=r,r&&typeof r=="object"){let i=r;typeof i.code=="string"&&(this.code=i.code),typeof i.field=="string"&&(this.field=i.field)}}},m=class extends Error{code;constructor(t,e){super(t),this.name="SignallingWebSocketError",this.code=e}};function D(s){return E(new TextEncoder().encode(s))}function E(s){let t="";for(let e=0;e<s.length;e++)t+=String.fromCharCode(s[e]);return btoa(t).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function z(s){let t=s.replace(/-/g,"+").replace(/_/g,"/"),e=t.length%4===0?"":"=".repeat(4-t.length%4),r=atob(t+e),i=new Uint8Array(r.length);for(let n=0;n<r.length;n++)i[n]=r.charCodeAt(n);return i}function N(s){return new TextDecoder().decode(z(s))}var X="signalling-sdk-dpop",Z=1,S="keys",W="dpop-keypair",_={name:"ECDSA",namedCurve:"P-256"},ee={name:"ECDSA",hash:"SHA-256"},L=class{value;get(){return this.value}set(t){t&&typeof t=="string"&&(this.value=t)}clear(){this.value=void 0}},v=class{keypair=null;cachedJWK=null;nonces=new L;initPromise=null;async init(){return this.initPromise||(this.initPromise=this.loadOrGenerate()),this.initPromise}async loadOrGenerate(){if(typeof crypto>"u"||!crypto.subtle)throw new Error("[signalling-sdk] DPoP requires WebCrypto. Run the SDK on a secure context (HTTPS or localhost).");if(typeof indexedDB>"u"){this.keypair=await crypto.subtle.generateKey(_,!1,["sign"]),this.cachedJWK=await crypto.subtle.exportKey("jwk",this.keypair.publicKey);return}let t=await ie();if(t){this.keypair=t,this.cachedJWK=await crypto.subtle.exportKey("jwk",t.publicKey);return}this.keypair=await crypto.subtle.generateKey(_,!1,["sign"]),this.cachedJWK=await crypto.subtle.exportKey("jwk",this.keypair.publicKey);try{await ne(this.keypair)}catch{}}async generateProof(t,e,r){if(await this.init(),!this.keypair||!this.cachedJWK)throw new Error("[signalling-sdk] DPoP keypair unavailable.");let n={typ:"dpop+jwt",alg:"ES256",jwk:te(this.cachedJWK)},o={htm:t.toUpperCase(),htu:e,iat:Math.floor(Date.now()/1e3),jti:re()},a=this.nonces.get();a&&(o.nonce=a),r&&(o.ath=r);let c=D(JSON.stringify(n)),l=D(JSON.stringify(o)),p=`${c}.${l}`,g=await crypto.subtle.sign(ee,this.keypair.privateKey,new TextEncoder().encode(p));return`${p}.${E(new Uint8Array(g))}`}updateNonceFromResponse(t){let e=t.get("DPoP-Nonce")||t.get("dpop-nonce");e&&this.nonces.set(e)}setNonce(t){this.nonces.set(t)}async reset(){if(this.nonces.clear(),this.keypair=null,this.cachedJWK=null,this.initPromise=null,typeof indexedDB<"u")try{await se()}catch{}}static async accessTokenHash(t){let e=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t));return E(new Uint8Array(e))}};function te(s){return{crv:s.crv,kty:s.kty,x:s.x,y:s.y}}function re(){if(typeof crypto.randomUUID=="function")return crypto.randomUUID();let s=new Uint8Array(16);crypto.getRandomValues(s),s[6]=s[6]&15|64,s[8]=s[8]&63|128;let t=Array.from(s,e=>e.toString(16).padStart(2,"0")).join("");return`${t.slice(0,8)}-${t.slice(8,12)}-${t.slice(12,16)}-${t.slice(16,20)}-${t.slice(20)}`}function O(){return new Promise((s,t)=>{let e=indexedDB.open(X,Z);e.onupgradeneeded=()=>{let r=e.result;r.objectStoreNames.contains(S)||r.createObjectStore(S)},e.onsuccess=()=>s(e.result),e.onerror=()=>t(e.error??new Error("idb open failed"))})}async function ie(){let s=await O();return new Promise((t,e)=>{let r=s.transaction(S,"readonly"),i=r.objectStore(S).get(W);i.onsuccess=()=>t(i.result??null),i.onerror=()=>e(i.error??new Error("idb get failed")),r.oncomplete=()=>s.close()})}async function ne(s){let t=await O();return new Promise((e,r)=>{let i=t.transaction(S,"readwrite"),n=i.objectStore(S).put(s,W);n.onsuccess=()=>e(),n.onerror=()=>r(n.error??new Error("idb put failed")),i.oncomplete=()=>t.close()})}async function se(){let s=await O();return new Promise((t,e)=>{let r=s.transaction(S,"readwrite"),i=r.objectStore(S).delete(W);i.onsuccess=()=>t(),i.onerror=()=>e(i.error??new Error("idb delete failed")),r.oncomplete=()=>s.close()})}function oe(s){let t=(s.clients??[]).map(e=>({clientId:e.clientId,roomId:String(e.roomId),user:e.user,joinedAt:e.joinedAt}));return{id:String(s.id),status:s.status,host:s.host,currentScreenSharerClientId:s.currentScreenSharerClientId,clients:t,createdAt:s.createdAt,updatedAt:s.updatedAt}}var P=class{constructor(t,e,r){this.dpop=e;this.baseUrl=de(t.baseUrl),this.fetchImpl=t.fetch??globalThis.fetch.bind(globalThis),this.logger=r,this.authMode=t.authMode??"bff",this.getAccessToken=t.getAccessToken}dpop;baseUrl;fetchImpl;logger;authMode;getAccessToken;url(t){return this.baseUrl+ae(t)}get origin(){return this.baseUrl}async dpopBind(t){return{returnTo:(await this.request("/auth/dpop/bind",{method:"POST",body:JSON.stringify({bind_token:t})}))?.return_to??""}}async getCurrentUser(){return(await this.request("/me")).user}async updateProfile(t){return(await this.request("/me",{method:"PATCH",body:JSON.stringify(t)})).user}async logout(){await this.request("/auth/logout",{method:"POST"})}async logoutAll(){await this.request("/auth/logout-all",{method:"POST"})}async issueWSTicket(){return this.request("/auth/ws-ticket",{method:"POST"})}async searchUserByEmail(t){return(await this.request(`/users/search?email=${encodeURIComponent(t)}`)).user}async createRoom(){let t=await this.request("/createroom",{method:"POST"});return{roomId:String(t.roomId),clientId:t.clientId}}async joinRoom(t){let e=await this.request(`/joinroom/${encodeURIComponent(String(t))}`,{method:"POST"});return{roomId:String(e.roomId),clientId:e.clientId}}async viewRoom(t){let e=await this.request(`/viewroom/${encodeURIComponent(String(t))}`);return oe(e)}async leaveRoom(t,e){await this.request(`/leaveroom/${t}/${e}`,{method:"DELETE"})}async startScreenShare(t,e){return this.request(`/room/${encodeURIComponent(String(t))}/${encodeURIComponent(e)}/screen-share/start`,{method:"POST"})}async stopScreenShare(t,e){return this.request(`/room/${encodeURIComponent(String(t))}/${encodeURIComponent(e)}/screen-share/stop`,{method:"POST"})}async getPermissions(t,e){let r=await this.request(`/room/${encodeURIComponent(String(t))}/${encodeURIComponent(e)}/permissions`);return{...r,roomId:String(r.roomId)}}async updatePermissions(t,e,r){let i=await this.request(`/room/${encodeURIComponent(String(t))}/${encodeURIComponent(e)}/permissions`,{method:"PUT",body:JSON.stringify(r)});return{...i,roomId:String(i.roomId)}}async getTurnCredentials(t=3600){return this.request("/v1/turn/credentials",{method:"POST",body:JSON.stringify({ttl_seconds:t})})}async request(t,e={}){let r=this.url(t),i=(e.method??"GET").toUpperCase(),n;if(this.authMode==="bearer"){if(!this.getAccessToken)throw new Error("[signalling-sdk] authMode=bearer requires a getAccessToken callback in SignallingSDKConfig.");n=await this.getAccessToken()}let o=async()=>{let c=n?await v.accessTokenHash(n):void 0,l=await this.dpop.generateProof(i,r,c),p=new Headers(e.headers);e.body&&!p.has("Content-Type")&&p.set("Content-Type","application/json"),p.set("DPoP",l),n&&p.set("Authorization",`DPoP ${n}`);let g=await this.fetchImpl(r,{...e,headers:p,credentials:this.authMode==="bff"?"include":e.credentials??"omit"});return this.dpop.updateNonceFromResponse(g.headers),g},a=await o();if(a.status===401){let c=await le(a),l=c&&typeof c=="object"?c.code:void 0;(l==="use_dpop_nonce"||l==="invalid_nonce"||l==="expired_nonce")&&(this.logger.debug("DPoP nonce challenge \u2014 retrying with fresh nonce",{code:l}),a=await o())}if(!a.ok){let c=await ce(a),l=ge(c,a);throw new w(l,a,c)}if(a.status!==204&&a.headers.get("content-type")?.includes("application/json"))return await a.json()}};function ae(s){return s.startsWith("/")?s:`/${s}`}function de(s){return s.replace(/\/+$/,"")}async function ce(s){try{let t=await s.text();if(!t)return;try{return JSON.parse(t)}catch{return t}}catch{return}}async function le(s){try{return await s.clone().json()}catch{return}}function ge(s,t){if(s&&typeof s=="object"){let e=s;if(typeof e.error=="string")return e.error;if(typeof e.message=="string")return e.message}return typeof s=="string"&&s?s:`${t.status} ${t.statusText}`}var u=class{listeners=new Map;on(t,e){let r=this.listeners.get(t);return r||(r=new Set,this.listeners.set(t,r)),r.add(e),()=>this.off(t,e)}off(t,e){this.listeners.get(t)?.delete(e)}once(t,e){let r=this.on(t,i=>{r(),e(i)})}emit(t,e){let r=this.listeners.get(t);if(r)for(let i of[...r])try{i(e)}catch{}}clear(){this.listeners.clear()}};var k=class extends u{constructor(e,r,i,n){super();this.config=e;this.api=r;this.dpop=i;this.logger=n}config;api;dpop;logger;cachedUser=null;get cachedCurrentUser(){return this.cachedUser}login(e){if(this.config.authMode==="bearer")throw new Error("[signalling-sdk] auth.login() is BFF-only. In bearer mode the integrator drives the OIDC flow.");if(typeof window>"u")throw new Error("[signalling-sdk] auth.login() requires a browser environment.");let r=new URL(this.api.url("/auth/login"));e&&r.searchParams.set("redirect",e),window.location.href=r.toString()}bindTokenFromFragment(e){let r=e??(typeof window<"u"?window.location.hash:"");return r?new URLSearchParams(r.startsWith("#")?r.slice(1):r).get("bind_token"):null}async completeBind(e){if(!e)throw new Error("[signalling-sdk] completeBind: bindToken is required.");return pe(async()=>{await this.dpop.init();try{let r=await this.api.dpopBind(e);return this.logger.info("DPoP bind completed; session cookie set"),r}catch(r){this.logger.warn("DPoP bind failed \u2014 resetting keypair so retry regenerates",r);try{await this.dpop.reset()}catch(i){this.logger.warn("dpop.reset during bind-failure recovery failed",i)}throw r}})}async getCurrentUser(e=!1){if(!e&&this.cachedUser)return this.cachedUser;try{let r=await this.api.getCurrentUser();return this.cachedUser=r,this.emit("state.authenticated",r),r}catch(r){if(r instanceof w&&(r.status===401||r.status===404))return this.cachedUser=null,this.emit("state.unauthenticated",r),null;throw r}}async isAuthenticated(){return await this.getCurrentUser()!==null}async updateProfile(e){let r=await this.api.updateProfile(e);return this.cachedUser=r,this.emit("state.authenticated",r),r}async logout(){try{await this.api.logout()}finally{await this.afterSignOut()}}async logoutAll(){try{await this.api.logoutAll()}finally{await this.afterSignOut()}}openAccountConsole(){if(typeof window>"u")throw new Error("[signalling-sdk] openAccountConsole requires a browser environment.");window.location.href=this.api.url("/auth/account")}async afterSignOut(){this.cachedUser=null,await this.dpop.reset(),this.emit("state.signed-out",void 0)}},he="signalling-sdk-dpop-bind-flow",K=Promise.resolve();async function pe(s){let t=typeof navigator<"u"?navigator:void 0;if(t?.locks?.request)return t.locks.request(he,{mode:"exclusive"},s);let e=K.then(()=>s());return K=e.catch(()=>{}),e}var y={BAD_FIRST_FRAME:4400,AUTH_FAILED:4401,FORBIDDEN:4403,NOT_FOUND:4404,HANDSHAKE_TIMEOUT:4408,REPLAY:4409,INTERNAL:4500},F=new Set([1008,y.BAD_FIRST_FRAME,y.AUTH_FAILED,y.FORBIDDEN,y.NOT_FOUND,y.HANDSHAKE_TIMEOUT,y.REPLAY]);function x(s){return F.has(s)}var b=class extends u{constructor(e,r,i,n={}){super();this.api=e;this.dpop=r;this.logger=i;this.options={maxReconnects:n.maxReconnects??5,reconnectBaseDelayMs:n.reconnectBaseDelayMs??1e3}}api;dpop;logger;socket=null;target=null;acked=!1;explicitlyClosed=!1;reconnectAttempts=0;options;get isReady(){return this.acked&&this.socket?.readyState===WebSocket.OPEN}async connectRoom(e,r){return this.connect({kind:"room",roomId:e,clientId:r})}async connectGlobal(){return this.connect({kind:"global"})}async connect(e){this.target=e,this.explicitlyClosed=!1;let r=await this.api.issueWSTicket(),i=r.nonce||me(r.ticket);i&&this.dpop.setNonce(i);let n=this.urlFor(e,r),o=ue(n);return this.logger.debug("opening WebSocket",{url:n,canonicalUrl:o,target:e}),new Promise((a,c)=>{let l=new WebSocket(n);this.socket=l,this.acked=!1;let p=!1,g=d=>{p||(p=!0,d?c(d):a())};l.onopen=async()=>{try{let d=await this.dpop.generateProof("GET",o);l.send(JSON.stringify({type:"dpop_handshake",proof:d}))}catch(d){this.logger.error("DPoP first-frame send failed",d),g(d instanceof Error?d:new Error(String(d))),l.close()}},l.onmessage=d=>{let h;try{h=JSON.parse(typeof d.data=="string"?d.data:"")}catch{this.logger.warn("non-JSON frame ignored",d.data);return}if(!this.acked){if(h&&typeof h=="object"&&h.type==="dpop_handshake_ack"){this.acked=!0,this.reconnectAttempts=0,this.emit("open",void 0),g();return}let f=new m("Unexpected first frame before dpop_handshake_ack: "+JSON.stringify(h));g(f),this.emit("error",f),l.close(1008,"protocol-violation");return}if(h&&typeof h=="object"&&typeof h.type=="string"){let f=h;this.emit("message",f),this.emit(`type:${f.type}`,f)}},l.onerror=d=>{this.logger.warn("WebSocket error event",d);let h=new m("WebSocket error");this.emit("error",h),g(h)},l.onclose=d=>{if(this.acked=!1,this.socket=null,this.emit("close",{code:d.code,reason:d.reason}),this.logger.info("WebSocket closed",{code:d.code,reason:d.reason}),p||g(new m(`WebSocket closed before handshake (code=${d.code} reason=${d.reason})`,d.code)),!this.explicitlyClosed&&!F.has(d.code)&&this.reconnectAttempts<this.options.maxReconnects){let h=this.options.reconnectBaseDelayMs*2**this.reconnectAttempts;this.reconnectAttempts++,this.logger.info(`WebSocket reconnecting in ${h}ms (attempt ${this.reconnectAttempts})`),setTimeout(()=>{!this.explicitlyClosed&&this.target&&this.connect(this.target).catch(f=>this.logger.warn("reconnect failed",f))},h)}}})}send(e){if(!this.socket||this.socket.readyState!==WebSocket.OPEN)throw new m("WebSocket is not connected");if(!this.acked)throw new m("WebSocket handshake not completed yet");this.socket.send(JSON.stringify(e))}disconnect(e=1e3,r="client-disconnect"){if(this.explicitlyClosed=!0,this.target=null,this.reconnectAttempts=this.options.maxReconnects,this.socket){try{this.socket.close(e,r)}catch{}this.socket=null}this.acked=!1}urlFor(e,r){let i=this.api.origin.replace(/^http/i,"ws"),n=e.kind==="global"?"/global/ws":`/ws/${encodeURIComponent(String(e.roomId))}/${encodeURIComponent(e.clientId)}`,o=new URL(i+n);return o.searchParams.set("ticket",r.ticket),o.toString()}};function ue(s){let t=new URL(s);return t.search="",t.hash="",t.toString()}function me(s){let t=s.split(".");if(t.length<2)return null;try{let e=JSON.parse(N(t[1]));return typeof e.nonce=="string"?e.nonce:null}catch{return null}}var I=class{constructor(t,e,r){this.api=t;this.ws=e;this.logger=r}api;ws;logger;active=null;get current(){return this.active}async create(){let t=await this.api.createRoom();return this.logger.info("Room created",t),this.active={roomId:t.roomId,clientId:t.clientId},t}async join(t){let e=await this.api.joinRoom(t);return this.logger.info("Joined room",e),this.active={roomId:e.roomId,clientId:e.clientId},e}async connectSocket(){if(!this.active)throw new Error("[signalling-sdk] connectSocket: not currently in a room");await this.ws.connectRoom(this.active.roomId,this.active.clientId)}async view(t){let e=t??this.active?.roomId;if(e==null)throw new Error("[signalling-sdk] view: roomId required (no active room)");return this.api.viewRoom(e)}async leave(){let t=this.active;if(!t){this.logger.warn("leave called without an active room \u2014 no-op");return}try{this.ws.disconnect(1e3,"client-leave")}catch(e){this.logger.warn("disconnect WS during leave failed",e)}try{await this.api.leaveRoom(t.roomId,t.clientId)}finally{this.active=null}}async startScreenShare(){if(!this.active)throw new Error("[signalling-sdk] startScreenShare: not currently in a room");return this.api.startScreenShare(this.active.roomId,this.active.clientId)}async stopScreenShare(){if(!this.active)throw new Error("[signalling-sdk] stopScreenShare: not currently in a room");return this.api.stopScreenShare(this.active.roomId,this.active.clientId)}async myPermissions(){if(!this.active)throw new Error("[signalling-sdk] myPermissions: not currently in a room");return this.api.getPermissions(this.active.roomId,this.active.clientId)}async updatePermissions(t,e){if(!this.active)throw new Error("[signalling-sdk] updatePermissions: not currently in a room");return this.api.updatePermissions(this.active.roomId,t,e)}};var C=class extends u{constructor(e,r){super();this.ws=e;this.logger=r}ws;logger;peers=new Map;peerMediaStates=new Map;screenStreamIds=new Map;screenSenders=new Map;localStream=null;localScreenStream=null;iceServers=B();local=null;wsUnsubs=[];bind(e){this.unbind(),this.local=e,this.wsUnsubs.push(this.ws.on("type:sdp",r=>this.handleSdp(r))),this.wsUnsubs.push(this.ws.on("type:ice",r=>this.handleIce(r))),this.wsUnsubs.push(this.ws.on("type:signal",r=>this.handleSignal(r)))}unbind(){for(let e of this.wsUnsubs)e();this.wsUnsubs=[];for(let[e,r]of this.peers){try{r.close()}catch{}this.emit("stream.removed",{clientId:e})}this.peers.clear(),this.peerMediaStates.clear(),this.screenStreamIds.clear(),this.screenSenders.clear(),this.localScreenStream=null,this.local=null}getPeerConnections(){return this.peers}getLocalStream(){return this.localStream}getLocalScreenStream(){return this.localScreenStream}getPeerMediaStates(){return this.peerMediaStates}setTurnCredentials(e){let r=e?Array.isArray(e)?e:[e]:[];this.iceServers=[...B(),...r.map(i=>({urls:i.urls,username:i.username,credential:i.credential}))]}async setLocalStream(e){this.localStream=e;for(let r of this.peers.values()){let i=r.getSenders(),n=e?.getAudioTracks()[0]??null,o=e?.getVideoTracks()[0]??null,a=g=>{let d=this.screenSenders.values();for(let h of d)if(h.includes(g))return!0;return!1},c=i.filter(g=>!a(g)),l=c.find(g=>g.track?.kind==="audio"),p=c.find(g=>g.track?.kind==="video");l?await l.replaceTrack(n):n&&e&&r.addTrack(n,e),p?await p.replaceTrack(o):o&&e&&r.addTrack(o,e)}}async addScreenStream(e){if(this.localScreenStream&&this.localScreenStream.id===e.id){this.logger.debug("addScreenStream: same stream already shared; no-op");return}this.localScreenStream&&await this.removeScreenStream(),this.localScreenStream=e;let r=e.id,i=e.getTracks();if(i.length===0){this.logger.warn("addScreenStream: stream has no tracks");return}for(let[n,o]of this.peers){this.sendSignaling(n,"signal",{action:"screenShareStream",streamId:r});let a=[];for(let c of i)a.push(o.addTrack(c,e));if(this.screenSenders.set(n,a),o.signalingState==="stable")try{let c=await o.createOffer();await o.setLocalDescription(c),c.sdp&&this.sendSignaling(n,"sdp",{type:"offer",sdp:c.sdp})}catch(c){this.logger.error(`addScreenStream renegotiation with ${n} failed`,c)}else this.logger.debug(`addScreenStream: skipping renegotiate with ${n} (state=${o.signalingState})`)}}async removeScreenStream(){if(!this.localScreenStream)return;let e=this.localScreenStream.id;for(let[r,i]of this.peers){this.sendSignaling(r,"signal",{action:"screenShareStopped",streamId:e});let n=this.screenSenders.get(r);if(n){for(let o of n)try{i.removeTrack(o)}catch(a){this.logger.debug(`removeTrack on ${r} failed`,a)}this.screenSenders.delete(r)}}this.localScreenStream=null}broadcastMediaState(e){this.broadcastSignal({action:"mediaState",video:e.video,audio:e.audio})}broadcastMediaStopped(){this.broadcastSignal({action:"mediaStopped"})}async callPeer(e){let r=this.getOrCreatePeer(e),i=await r.createOffer();if(await r.setLocalDescription(i),!i.sdp)throw new Error("[signalling-sdk] createOffer returned no SDP");this.sendSignaling(e,"sdp",{type:"offer",sdp:i.sdp})}hangupPeer(e){let r=this.peers.get(e);if(r){try{r.close()}catch{}this.peers.delete(e)}this.screenSenders.delete(e),this.peerMediaStates.delete(e);for(let[i,n]of this.screenStreamIds)n===e&&this.screenStreamIds.delete(i);r&&(this.emit("stream.removed",{clientId:e}),this.emit("screen-share.removed",{clientId:e}))}async handleSdp(e){let r=e.from,i=e.payload;if(!i||i.type!=="offer"&&i.type!=="answer"||!i.sdp){this.logger.warn("ignoring malformed SDP frame",e);return}let n=this.getOrCreatePeer(r);try{if(await n.setRemoteDescription({type:i.type,sdp:i.sdp}),i.type==="offer"){let o=await n.createAnswer();await n.setLocalDescription(o),o.sdp&&this.sendSignaling(r,"sdp",{type:"answer",sdp:o.sdp})}}catch(o){this.logger.error(`SDP negotiation with ${r} failed`,o),this.emit("peer.failed",{clientId:r,error:o instanceof Error?o:new Error(String(o))})}}async handleIce(e){let r=e.from,i=e.payload,n=this.peers.get(r);if(!(!n||!i))try{await n.addIceCandidate(i)}catch(o){this.logger.debug("addIceCandidate failed",{peerId:r,err:o})}}handleSignal(e){e.from==="server"?this.handleServerSignal(e):this.handlePeerSignal(e)}handleServerSignal(e){let r=e.payload;r?.action==="leave"&&r.leftClientId&&this.hangupPeer(r.leftClientId)}handlePeerSignal(e){let r=e.from;if(!r||!e.payload||typeof e.payload!="object")return;let i=e.payload;switch(i.action){case"mediaState":{let n=typeof i.video=="boolean"?i.video:!0,o=typeof i.audio=="boolean"?i.audio:!0,a={video:n,audio:o};this.peerMediaStates.set(r,a),this.emit("peer.media-state-changed",{clientId:r,state:a});break}case"mediaStopped":{this.peerMediaStates.delete(r);for(let[n,o]of this.screenStreamIds)o===r&&this.screenStreamIds.delete(n);this.emit("peer.media-stopped",{clientId:r}),this.emit("stream.removed",{clientId:r}),this.emit("screen-share.removed",{clientId:r});break}case"screenShareStream":{typeof i.streamId=="string"&&this.screenStreamIds.set(i.streamId,r);break}case"screenShareStopped":{typeof i.streamId=="string"&&this.screenStreamIds.delete(i.streamId),this.emit("screen-share.removed",{clientId:r});break}default:break}}getOrCreatePeer(e){let r=this.peers.get(e);if(r)return r;let i=new RTCPeerConnection({iceServers:this.iceServers});if(i.onicecandidate=n=>{n.candidate&&this.sendSignaling(e,"ice",n.candidate.toJSON())},i.ontrack=n=>{let o=n.streams[0]??new MediaStream([n.track]);this.screenStreamIds.get(o.id)===e?this.emit("screen-share.added",{clientId:e,stream:o}):this.emit("stream.added",{clientId:e,stream:o})},i.onconnectionstatechange=()=>{this.emit("peer.connection-state",{clientId:e,state:i.connectionState}),(i.connectionState==="failed"||i.connectionState==="closed")&&this.hangupPeer(e)},this.localStream)for(let n of this.localStream.getTracks())i.addTrack(n,this.localStream);if(this.localScreenStream){let n=this.localScreenStream.id;this.sendSignaling(e,"signal",{action:"screenShareStream",streamId:n});let o=[];for(let a of this.localScreenStream.getTracks())o.push(i.addTrack(a,this.localScreenStream));this.screenSenders.set(e,o)}return this.peers.set(e,i),i}sendSignaling(e,r,i){if(!this.local){this.logger.warn("sendSignaling: no local identity bound; dropping frame");return}this.ws.send({type:r,from:this.local.clientId,to:e,payload:i})}broadcastSignal(e){for(let r of this.peers.keys())this.sendSignaling(r,"signal",e)}};function B(){return[{urls:"stun:stun.l.google.com:19302"}]}var M=class{async getLocalStream(t={audio:!0,video:!0}){return this.assertSupported("getUserMedia"),navigator.mediaDevices.getUserMedia(t)}async getUserMedia(t=!0,e=!0){return this.getLocalStream({audio:t,video:e})}async getScreenShare(t={video:!0}){if(!navigator.mediaDevices?.getDisplayMedia)throw new Error("[signalling-sdk] getDisplayMedia is not supported in this browser");return navigator.mediaDevices.getDisplayMedia(t)}async listDevices(){return this.assertSupported("enumerateDevices"),navigator.mediaDevices.enumerateDevices()}async listInventory(){let t=await this.listDevices();return{cameras:t.filter(e=>e.kind==="videoinput"),microphones:t.filter(e=>e.kind==="audioinput"),speakers:t.filter(e=>e.kind==="audiooutput")}}onDeviceChange(t){return navigator.mediaDevices?(navigator.mediaDevices.addEventListener("devicechange",t),()=>navigator.mediaDevices.removeEventListener("devicechange",t)):()=>{}}stopStream(t){if(t)for(let e of t.getTracks())try{e.stop()}catch{}}assertSupported(t){if(typeof navigator>"u"||!navigator.mediaDevices)throw new Error("[signalling-sdk] navigator.mediaDevices unavailable (insecure context or non-browser runtime)");if(!(t in navigator.mediaDevices))throw new Error(`[signalling-sdk] navigator.mediaDevices.${t} is not supported`)}};var fe=3e4,Se=300,R=class extends u{constructor(e,r,i={}){super();this.ws=e;this.logger=r;this.refreshIntervalMs=i.refreshIntervalMs??fe}ws;logger;friends=[];pending=[];wsUnsubs=[];refreshTimer=null;firstQueryTimer=null;refreshIntervalMs;bind(){this.wsUnsubs.length>0||(this.wsUnsubs.push(this.ws.on("message",e=>this.handleMessage(e))),this.wsUnsubs.push(this.ws.on("open",()=>this.schedulePostConnectQueries())),this.wsUnsubs.push(this.ws.on("close",()=>{this.clearLocalState("disconnect")})),this.ws.isReady&&this.schedulePostConnectQueries())}unbind(){for(let e of this.wsUnsubs)e();this.wsUnsubs=[],this.cancelTimers(),this.clearLocalState("unbind")}list(){return this.friends.slice()}pendingRequests(){return this.pending.slice()}refresh(){this.send({type:"friend_list",from:"",to:"",payload:{}}),this.send({type:"friend_pending",from:"",to:"",payload:{}})}sendRequest(e){if(!e)throw new Error("[signalling-sdk] sendRequest: username required");this.send({type:"friend_request",from:"",to:e,payload:{}})}accept(e){if(!e)throw new Error("[signalling-sdk] accept: friendshipId required");this.send({type:"friend_accept",from:"",to:"",payload:{friendshipId:e}})}reject(e){if(!e)throw new Error("[signalling-sdk] reject: friendshipId required");this.send({type:"friend_reject",from:"",to:"",payload:{friendshipId:e}})}remove(e){if(!e)throw new Error("[signalling-sdk] remove: friendshipId required");this.send({type:"friend_remove",from:"",to:"",payload:{friendshipId:e}})}schedulePostConnectQueries(){this.firstQueryTimer&&clearTimeout(this.firstQueryTimer),this.firstQueryTimer=setTimeout(()=>{this.refresh(),this.startRefreshLoop()},Se)}startRefreshLoop(){this.cancelRefreshOnly(),this.refreshTimer=setInterval(()=>{this.send({type:"friend_list",from:"",to:"",payload:{}})},this.refreshIntervalMs)}cancelRefreshOnly(){this.refreshTimer&&(clearInterval(this.refreshTimer),this.refreshTimer=null)}cancelTimers(){this.cancelRefreshOnly(),this.firstQueryTimer&&(clearTimeout(this.firstQueryTimer),this.firstQueryTimer=null)}clearLocalState(e){let r=this.friends.length>0,i=this.pending.length>0;this.friends=[],this.pending=[],r&&this.emit("friends.changed",{friends:this.friends.slice()}),i&&this.emit("friends.pending-changed",{pending:this.pending.slice()})}send(e){try{this.ws.send(e)}catch(r){this.logger.warn(`FriendsManager.send(${e.type}) failed`,r)}}handleMessage(e){let r=e.payload??{};switch(e.type){case"friend_list":{let i=Array.isArray(r.friends)?r.friends:[];this.friends=i.filter(n=>!!n&&typeof n=="object").map(n=>({friendshipId:String(n.friendshipId??""),username:String(n.username??""),isOnline:!!n.isOnline})).filter(n=>n.friendshipId&&n.username),this.emit("friends.changed",{friends:this.friends.slice()});break}case"friend_pending":{let i=Array.isArray(r.requests)?r.requests:[];this.pending=i.filter(n=>!!n&&typeof n=="object").map(n=>({friendshipId:String(n.friendshipId??""),requester:String(n.requester??""),createdAt:String(n.createdAt??"")})).filter(n=>n.friendshipId&&n.requester),this.emit("friends.pending-changed",{pending:this.pending.slice()});break}case"friend_request":{if(e.from&&e.from!=="server"){let i=String(r.friendshipId??""),n=String(r.requester??e.from);if(!i||this.pending.some(a=>a.friendshipId===i))break;let o={friendshipId:i,requester:n,createdAt:new Date().toISOString()};this.pending=[...this.pending,o],this.emit("friend.request-received",o),this.emit("friends.pending-changed",{pending:this.pending.slice()})}break}case"friend_accept_ack":{let i=String(r.friendshipId??""),n=String(r.friend??"");if(!i||!n)break;if(this.pending=this.pending.filter(o=>o.friendshipId!==i),this.emit("friends.pending-changed",{pending:this.pending.slice()}),!this.friends.some(o=>o.friendshipId===i)){let o={friendshipId:i,username:n,isOnline:!0};this.friends=[...this.friends,o],this.emit("friend.added",o),this.emit("friends.changed",{friends:this.friends.slice()})}break}case"friend_accepted":{let i=String(r.friendshipId??""),n=String(r.acceptedBy??e.from??"");if(!i||!n)break;if(!this.friends.some(o=>o.friendshipId===i)){let o={friendshipId:i,username:n,isOnline:!0};this.friends=[...this.friends,o],this.emit("friend.added",o),this.emit("friends.changed",{friends:this.friends.slice()})}break}case"friend_reject_ack":{let i=String(r.friendshipId??"");if(!i)break;let n=this.pending.length;this.pending=this.pending.filter(o=>o.friendshipId!==i),this.pending.length!==n&&this.emit("friends.pending-changed",{pending:this.pending.slice()});break}case"friend_remove_ack":{let i=String(r.friendshipId??"");if(!i)break;let n=this.friends.length;this.friends=this.friends.filter(o=>o.friendshipId!==i),this.friends.length!==n&&(this.emit("friend.removed",{friendshipId:i}),this.emit("friends.changed",{friends:this.friends.slice()}));break}case"friend_request_ack":break;case"error":{let i=String(r.originalType??""),n=String(r.error??"unknown error");i.startsWith("friend_")&&this.emit("friends.error",{originalType:i,error:n});break}default:break}}};var T=class extends u{constructor(e,r){super();this.ws=e;this.logger=r}ws;logger;wsUnsubs=[];bind(){this.wsUnsubs.length>0||this.wsUnsubs.push(this.ws.on("message",e=>this.handleMessage(e)))}unbind(){for(let e of this.wsUnsubs)e();this.wsUnsubs=[]}invite(e,r){if(!e)throw new Error("[signalling-sdk] invite: toUsername required");let i=String(r);if(!i)throw new Error("[signalling-sdk] invite: roomId required");try{this.ws.send({type:"call_invite",from:"",to:e,payload:{roomId:i}})}catch(n){throw this.logger.warn("CallInviteManager.invite failed",n),n}}handleMessage(e){if(e.type!=="call_invite")return;let r=e.from;if(!r||r==="server"){this.logger.warn("call_invite: ignoring frame with missing/server from",e);return}let n=(e.payload??{}).roomId;if(typeof n!="string"&&typeof n!="number"){this.logger.warn("call_invite: missing or malformed roomId",e);return}let o={callerUsername:r,roomId:String(n)};this.emit("call.invite-received",o)}};var $={silent:-1,error:0,warn:1,info:2,debug:3};function j(s){let t=e=>$[s]>=$[e];return{error:(...e)=>t("error")&&console.error("[signalling-sdk]",...e),warn:(...e)=>t("warn")&&console.warn("[signalling-sdk]",...e),info:(...e)=>t("info")&&console.info("[signalling-sdk]",...e),debug:(...e)=>t("debug")&&console.debug("[signalling-sdk]",...e)}}var U=class extends u{constructor(e,r,i,n,o,a,c){super();this.roomId=e;this.clientId=r;this.rooms=i;this.webrtc=n;this.ws=o;this.devices=a;this.auth=c}roomId;clientId;rooms;webrtc;ws;devices;auth;localStream=null;localMediaState={video:!0,audio:!0};get peers(){return this.webrtc.getPeerMediaStates()}get isAudioEnabled(){return this.localMediaState.audio}get isVideoEnabled(){return this.localMediaState.video}get localScreenStream(){return this.webrtc.getLocalScreenStream()}get isScreenSharing(){return this.webrtc.getLocalScreenStream()!==null}setMicEnabled(e){this.localStream?.getAudioTracks().forEach(r=>r.enabled=e),this.localMediaState={...this.localMediaState,audio:e},this.webrtc.broadcastMediaState(this.localMediaState)}setCameraEnabled(e){this.localStream?.getVideoTracks().forEach(r=>r.enabled=e),this.localMediaState={...this.localMediaState,video:e},this.webrtc.broadcastMediaState(this.localMediaState)}async setLocalStream(e){this.localStream=e,await this.webrtc.setLocalStream(e),e||this.webrtc.broadcastMediaStopped()}async startScreenShare(){let e=await this.devices.getScreenShare();await this.webrtc.addScreenStream(e);try{await this.rooms.startScreenShare()}catch(r){throw await this.webrtc.removeScreenStream(),this.devices.stopStream(e),r}return e.getVideoTracks()[0]?.addEventListener("ended",()=>{this.stopScreenShare().catch(()=>{})}),e}async stopScreenShare(){let e=this.webrtc.getLocalScreenStream();if(e){await this.webrtc.removeScreenStream(),this.devices.stopStream(e);try{await this.rooms.stopScreenShare()}catch{}}}sendChat(e){let r=e.trim();if(!r)throw new Error("[signalling-sdk] sendChat: text required");let i={id:ve(),from:this.clientId,fromName:this.deriveLocalDisplayName(),text:r,timestamp:new Date().toISOString()},n=this.webrtc.getPeerConnections();for(let o of n.keys())try{this.ws.send({type:"signal",from:this.clientId,to:o,payload:{action:"chat",...i}})}catch{}return i}deriveLocalDisplayName(){let e=this.auth.cachedCurrentUser;return e?`${e.firstName||""} ${e.lastName||""}`.trim()||e.username||this.clientId:this.clientId}async leave(){try{this.webrtc.broadcastMediaStopped()}catch{}this.webrtc.unbind(),this.devices.stopStream(this.localStream),this.localStream=null,await this.rooms.leave(),this.clear()}},q=class s{auth;rooms;devices;api;dpop;ws;webrtc;logger;config;activeCall=null;callBridges=[];globalWs=null;friendsManager=null;callInviteManager=null;constructor(t){this.config=t;let e=t.debug?"debug":t.logLevel??"warn";this.logger=t.logger??j(e),this.dpop=new v,this.api=new P(t,this.dpop,this.logger),this.auth=new k(t,this.api,this.dpop,this.logger),this.ws=new b(this.api,this.dpop,this.logger),this.rooms=new I(this.api,this.ws,this.logger),this.webrtc=new C(this.ws,this.logger),this.devices=new M}static async create(t){if(!t.baseUrl)throw new Error("[signalling-sdk] SignallingSDK.create: baseUrl is required");let e=new s(t);return await e.dpop.init(),e}get friends(){if(!this.friendsManager){let t=this.ensureGlobalConnected();this.friendsManager=new R(t,this.logger),this.friendsManager.bind()}return this.friendsManager}get calls(){return{joinRoom:async t=>this.joinRoom(t),invite:(t,e)=>{this.ensureCallInviteManager().invite(t,e)},on:(t,e)=>this.ensureCallInviteManager().on(t,e)}}ensureCallInviteManager(){if(!this.callInviteManager){let t=this.ensureGlobalConnected();this.callInviteManager=new T(t,this.logger),this.callInviteManager.bind()}return this.callInviteManager}ensureGlobalConnected(){return this.globalWs||(this.globalWs=new b(this.api,this.dpop,this.logger),this.globalWs.connectGlobal().catch(t=>{this.logger.warn("global WS connect failed",t)})),this.globalWs}async joinRoom(t){if(this.activeCall){this.logger.info("Replacing existing call");try{await this.activeCall.leave()}catch(n){this.logger.warn("previous leave() failed; continuing",n)}finally{for(let n of this.callBridges)n();this.callBridges=[],this.activeCall=null}}let e=await this.rooms.join(t.roomId);try{let n=await this.api.getTurnCredentials();this.webrtc.setTurnCredentials(n)}catch(n){this.logger.warn("TURN credential fetch failed; falling back to STUN-only",n),this.webrtc.setTurnCredentials(null)}let r=null;if(t.media&&(t.media.audio||t.media.video))try{r=await this.devices.getLocalStream(t.media),await this.webrtc.setLocalStream(r)}catch(n){this.logger.warn("getUserMedia failed; joining without local media",n)}await this.ws.connectRoom(e.roomId,e.clientId),this.webrtc.bind({clientId:e.clientId});let i=new U(e.roomId,e.clientId,this.rooms,this.webrtc,this.ws,this.devices,this.auth);return i.localStream=r,this.bindCallEvents(i),this.activeCall=i,i}bindCallEvents(t){for(let e of this.callBridges)e();this.callBridges=[],this.callBridges.push(this.webrtc.on("stream.added",e=>t.emit("stream.added",e))),this.callBridges.push(this.webrtc.on("stream.removed",e=>t.emit("stream.removed",e))),this.callBridges.push(this.webrtc.on("screen-share.added",e=>t.emit("screen-share.added",e))),this.callBridges.push(this.webrtc.on("screen-share.removed",e=>t.emit("screen-share.removed",e))),this.callBridges.push(this.webrtc.on("peer.media-state-changed",e=>t.emit("peer.media-state-changed",e))),this.callBridges.push(this.webrtc.on("peer.media-stopped",e=>t.emit("peer.media-stopped",e))),this.callBridges.push(this.ws.on("type:signal",e=>{let r=e.payload;e.from==="server"?this.dispatchServerSignal(t,r,e):this.dispatchPeerSignal(t,e.from,r,e)})),this.callBridges.push(this.ws.on("close",e=>t.emit("connection.closed",{code:e.code,reason:e.reason,fatal:x(e.code)}))),this.callBridges.push(this.ws.on("open",()=>t.emit("connection.reopened",void 0))),this.callBridges.push(this.ws.on("message",e=>t.emit("raw",e)))}dispatchServerSignal(t,e,r){switch(e?.action){case"join":t.emit("peer.joined",{clientId:e.newClientId,name:e.newClientName,avatarUrl:e.newClientAvatar}),this.shouldUseSfu()||this.webrtc.callPeer(e.newClientId).catch(i=>this.logger.warn("callPeer failed",i));break;case"leave":t.emit("peer.left",{clientId:e.leftClientId,name:e.leftClientName});break;case"hostChanged":t.emit("host.changed",{newHostClientId:e.newHostId});break;case"stopScreenShare":t.emit("screen-share.stop",void 0);break;case"permissionsUpdated":t.emit("permissions.updated",{canAudio:e.canAudio,canVideo:e.canVideo,canScreenShare:e.canScreenShare});break;default:t.emit("raw",r)}}dispatchPeerSignal(t,e,r,i){if(!(!r||typeof r!="object"))switch(r.action){case"chat":t.emit("chat",{id:r.id,from:i.from,fromName:r.fromName,text:r.text,timestamp:r.timestamp});break;case"mediaState":case"mediaStopped":case"screenShareStream":case"screenShareStopped":break;default:t.emit("raw",i)}}shouldUseSfu(){return!1}};function ve(){return`${Date.now()}-${Math.random().toString(36).slice(2,10)}`}0&&(module.exports={ApiClient,AuthClient,Call,CallInviteManager,DPoPManager,DeviceManager,FriendsManager,RoomManager,SignallingApiError,SignallingSDK,SignallingWebSocketError,WSCloseCode,WebRTCManager,WebSocketClient,isFatalCloseCode});
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/types/index.ts","../src/internal/base64url.ts","../src/dpop/index.ts","../src/api/client.ts","../src/internal/events.ts","../src/auth/index.ts","../src/websocket/client.ts","../src/room/manager.ts","../src/webrtc/peer.ts","../src/devices/manager.ts","../src/global/friends.ts","../src/global/invite.ts","../src/internal/logger.ts"],"sourcesContent":["/**\n * `@signalling/sdk` — JavaScript-first integration wrapper for the\n * Confrnce signalling HTTP / WebSocket API.\n *\n * This file exports the public entry point `SignallingSDK`, the\n * supporting modules, and every public type. Everything in\n * `src/internal/` is intentionally NOT re-exported.\n *\n * See `README.md` and `docs/` in this package for integration overview.\n */\n\nimport {\n CallConfig,\n CallEventMap,\n ChatMessage,\n CreateRoomResponse,\n JoinRoomResponse,\n Logger,\n PeerMediaState,\n PeerSignalPayload,\n ServerSignalPayload,\n SignallingSDKConfig,\n UserProfile,\n WSMessage,\n} from './types';\nimport { ApiClient } from './api/client';\nimport { AuthClient } from './auth';\nimport { DPoPManager } from './dpop';\nimport { isFatalCloseCode, WebSocketClient } from './websocket/client';\nimport { RoomManager } from './room/manager';\nimport { WebRTCManager } from './webrtc/peer';\nimport { DeviceManager } from './devices/manager';\nimport { FriendsManager } from './global/friends';\nimport { CallInviteManager } from './global/invite';\nimport { TypedEventEmitter } from './internal/events';\nimport { createConsoleLogger } from './internal/logger';\n\n// Public re-exports.\nexport * from './types';\nexport { ApiClient } from './api/client';\nexport { AuthClient, type AuthEventMap } from './auth';\nexport { DPoPManager } from './dpop';\nexport {\n WebSocketClient,\n WSCloseCode,\n isFatalCloseCode,\n type WebSocketClientOptions,\n type WebSocketEventMap,\n type WebSocketTarget,\n} from './websocket/client';\nexport { RoomManager, type ActiveMembership } from './room/manager';\nexport { WebRTCManager, type WebRTCEventMap, type LocalIdentity } from './webrtc/peer';\nexport { DeviceManager, type DeviceInventory } from './devices/manager';\nexport { FriendsManager } from './global/friends';\nexport { CallInviteManager } from './global/invite';\n\n/**\n * `Call` is the high-level handle returned by `sdk.calls.joinRoom`.\n * It bundles the active membership, the typed event surface, and\n * convenience methods so integrators don't juggle managers.\n *\n * Local mic / camera enable state is owned by the Call (not the SDK or\n * the React layer) because the wire protocol couples toggle UX to a\n * peer broadcast (`mediaState`). Calling `setMicEnabled` / `setCameraEnabled`\n * therefore both flips the local track AND fires the peer signal —\n * keeping the two in lockstep removes the most common source of stale\n * remote mute-indicator bugs.\n */\nexport class Call extends TypedEventEmitter<CallEventMap> {\n /** The local camera stream sent to peers — `null` if joined listen-only. */\n public localStream: MediaStream | null = null;\n\n /** Tracks the latest video/audio enable state so the toggles can broadcast it. */\n private localMediaState: PeerMediaState = { video: true, audio: true };\n\n /** @internal */\n constructor(\n public readonly roomId: number | string,\n public readonly clientId: string,\n private readonly rooms: RoomManager,\n private readonly webrtc: WebRTCManager,\n private readonly ws: WebSocketClient,\n private readonly devices: DeviceManager,\n private readonly auth: AuthClient\n ) {\n super();\n }\n\n // -------------------------------------------------------------------------\n // Read-only state — handy for React adapters.\n // -------------------------------------------------------------------------\n\n /**\n * Snapshot of remote peers' announced mic/cam state, keyed by clientId.\n * Live view backed by `WebRTCManager.getPeerMediaStates()`.\n */\n get peers(): ReadonlyMap<string, PeerMediaState> {\n return this.webrtc.getPeerMediaStates();\n }\n\n /** Local mic enable state. Reflects the last call to `setMicEnabled`. */\n get isAudioEnabled(): boolean {\n return this.localMediaState.audio;\n }\n\n /** Local camera enable state. */\n get isVideoEnabled(): boolean {\n return this.localMediaState.video;\n }\n\n /** Local screen-share stream, or `null` when not sharing. */\n get localScreenStream(): MediaStream | null {\n return this.webrtc.getLocalScreenStream();\n }\n\n /** True iff this client is currently broadcasting a screen-share stream. */\n get isScreenSharing(): boolean {\n return this.webrtc.getLocalScreenStream() !== null;\n }\n\n // -------------------------------------------------------------------------\n // Local-media controls (toggle = local + broadcast).\n // -------------------------------------------------------------------------\n\n /**\n * Toggle local audio. Disables the outgoing audio track AND fires a\n * `mediaState` signal so peers can update their mute indicators.\n * The remote audio track keeps flowing on the wire — that's expected\n * (WebRTC pauses the track at the source, not the line).\n */\n setMicEnabled(enabled: boolean): void {\n this.localStream?.getAudioTracks().forEach((t) => (t.enabled = enabled));\n this.localMediaState = { ...this.localMediaState, audio: enabled };\n this.webrtc.broadcastMediaState(this.localMediaState);\n }\n\n /** Toggle local video. Broadcasts a `mediaState` signal — see `setMicEnabled`. */\n setCameraEnabled(enabled: boolean): void {\n this.localStream?.getVideoTracks().forEach((t) => (t.enabled = enabled));\n this.localMediaState = { ...this.localMediaState, video: enabled };\n this.webrtc.broadcastMediaState(this.localMediaState);\n }\n\n /**\n * Replace the local camera stream (e.g. after a device switch). Does NOT\n * touch the screen-share stream — call `stopScreenShare` separately if\n * needed. Passing `null` stops local camera media and broadcasts a\n * `mediaStopped` signal.\n */\n async setLocalStream(stream: MediaStream | null): Promise<void> {\n this.localStream = stream;\n await this.webrtc.setLocalStream(stream);\n if (!stream) {\n this.webrtc.broadcastMediaStopped();\n }\n }\n\n // -------------------------------------------------------------------------\n // Screen share — additive (camera continues alongside).\n // -------------------------------------------------------------------------\n\n /**\n * Begin sharing the screen ALONGSIDE the existing camera tracks.\n * Picks a screen via `DeviceManager.getScreenShare`, announces the\n * stream id to every peer (`screenShareStream` signal), adds the\n * tracks, and renegotiates each peer connection — all owned by\n * `WebRTCManager.addScreenStream`. Also POSTs the screen-share-start\n * endpoint so the backend can track the active sharer per room.\n *\n * If a different client is already sharing, the backend will boot\n * them via a `stopScreenShare` server signal — we surface that to\n * the previous sharer via `'screen-share.stop'`.\n *\n * Browser `onended` (user clicks the system \"Stop sharing\" pill) is\n * wired up so we cleanly call `stopScreenShare` on that path too.\n */\n async startScreenShare(): Promise<MediaStream> {\n const screen = await this.devices.getScreenShare();\n await this.webrtc.addScreenStream(screen);\n try {\n await this.rooms.startScreenShare();\n } catch (err) {\n // Roll back the local add so we're not stuck \"sharing\" without\n // a backend ACK — surface the error to the caller.\n await this.webrtc.removeScreenStream();\n this.devices.stopStream(screen);\n throw err;\n }\n screen.getVideoTracks()[0]?.addEventListener('ended', () => {\n void this.stopScreenShare().catch(() => undefined);\n });\n return screen;\n }\n\n /** Stop the active screen share — both remotely and locally. */\n async stopScreenShare(): Promise<void> {\n const screen = this.webrtc.getLocalScreenStream();\n if (!screen) return;\n await this.webrtc.removeScreenStream();\n this.devices.stopStream(screen);\n try {\n await this.rooms.stopScreenShare();\n } catch {\n // Best-effort — local state is already updated; the backend will\n // also clear its sharer pointer on next leave or via timeout.\n }\n }\n\n // -------------------------------------------------------------------------\n // Chat — broadcast over the room WS as `signal` frames.\n // -------------------------------------------------------------------------\n\n /**\n * Broadcast a chat message to every peer in the room. Fills in\n * `fromName` from the cached `UserProfile` if available (falls back\n * to the local `clientId`). Returns the constructed `ChatMessage` so\n * the caller can append it to their own UI without waiting for any\n * echo (the SDK does NOT echo chat to the sender).\n */\n sendChat(text: string): ChatMessage {\n const trimmed = text.trim();\n if (!trimmed) {\n throw new Error('[signalling-sdk] sendChat: text required');\n }\n const message: ChatMessage = {\n id: generateChatId(),\n from: this.clientId,\n fromName: this.deriveLocalDisplayName(),\n text: trimmed,\n timestamp: new Date().toISOString(),\n };\n const peers = this.webrtc.getPeerConnections();\n for (const peerId of peers.keys()) {\n try {\n this.ws.send({\n type: 'signal',\n from: this.clientId,\n to: peerId,\n payload: { action: 'chat', ...message },\n });\n } catch {\n // Skip individual failed sends — the connection-level error\n // will surface via `connection.closed`.\n }\n }\n return message;\n }\n\n private deriveLocalDisplayName(): string {\n const user: UserProfile | null = this.auth.cachedCurrentUser;\n if (!user) return this.clientId;\n const combined = `${user.firstName || ''} ${user.lastName || ''}`.trim();\n return combined || user.username || this.clientId;\n }\n\n // -------------------------------------------------------------------------\n // Leave — graceful teardown.\n // -------------------------------------------------------------------------\n\n /** Disconnect from the room — closes WS, tears down peers, hits API. */\n async leave(): Promise<void> {\n // Tell peers we've stopped before tearing down the WS so the broadcast\n // actually goes out. If we're sharing a screen too, the receivers will\n // tear that down via `mediaStopped`.\n try {\n this.webrtc.broadcastMediaStopped();\n } catch {\n // ignore — local state is what matters next\n }\n this.webrtc.unbind();\n this.devices.stopStream(this.localStream);\n this.localStream = null;\n await this.rooms.leave();\n this.clear();\n }\n}\n\n/**\n * `SignallingSDK` — the single public entry point.\n *\n * ```ts\n * const sdk = await SignallingSDK.create({\n * baseUrl: \"https://api.example.com\",\n * authMode: \"bff\",\n * });\n *\n * await sdk.auth.login();\n * // ...after the BFF callback completes...\n * const token = sdk.auth.bindTokenFromFragment();\n * if (token) await sdk.auth.completeBind(token);\n *\n * const room = await sdk.rooms.create();\n * const call = await sdk.calls.joinRoom({\n * roomId: room.roomId,\n * media: { audio: true, video: true },\n * });\n *\n * call.on('peer.joined', (peer) => console.log('joined:', peer));\n * call.on('stream.added', ({ clientId, stream }) => attach(clientId, stream));\n * ```\n *\n * **Two WebSocket connections.** The SDK keeps the room WS (joined via\n * `calls.joinRoom`) and the global WS (used by `sdk.friends` /\n * `sdk.calls.invite`) on separate `WebSocketClient` instances —\n * concurrent, independently authenticated, independently reconnecting.\n * The global WS is lazily opened on first use.\n */\nexport class SignallingSDK {\n public readonly auth: AuthClient;\n public readonly rooms: RoomManager;\n public readonly devices: DeviceManager;\n public readonly api: ApiClient;\n public readonly dpop: DPoPManager;\n /** Room WebSocket — the `calls.joinRoom` machinery binds to this one. */\n public readonly ws: WebSocketClient;\n public readonly webrtc: WebRTCManager;\n public readonly logger: Logger;\n\n private readonly config: SignallingSDKConfig;\n\n /** Tracks the active call so a second `joinRoom` can clean up the previous one. */\n private activeCall: Call | null = null;\n\n /** Bridge subscriptions between (WS, WebRTC) → active Call. Cleared on leave. */\n private callBridges: Array<() => void> = [];\n\n // Lazy-instantiated global WS surfaces. See `ensureGlobalConnected`.\n private globalWs: WebSocketClient | null = null;\n private friendsManager: FriendsManager | null = null;\n private callInviteManager: CallInviteManager | null = null;\n\n private constructor(config: SignallingSDKConfig) {\n this.config = config;\n const logLevel = config.debug ? 'debug' : (config.logLevel ?? 'warn');\n this.logger = config.logger ?? createConsoleLogger(logLevel);\n\n this.dpop = new DPoPManager();\n this.api = new ApiClient(config, this.dpop, this.logger);\n this.auth = new AuthClient(config, this.api, this.dpop, this.logger);\n this.ws = new WebSocketClient(this.api, this.dpop, this.logger);\n this.rooms = new RoomManager(this.api, this.ws, this.logger);\n this.webrtc = new WebRTCManager(this.ws, this.logger);\n this.devices = new DeviceManager();\n }\n\n /**\n * Create and initialise a new SDK instance. Eagerly loads the DPoP\n * keypair from IndexedDB so the first authed request doesn't pay the\n * keygen cost.\n */\n static async create(config: SignallingSDKConfig): Promise<SignallingSDK> {\n if (!config.baseUrl) {\n throw new Error('[signalling-sdk] SignallingSDK.create: baseUrl is required');\n }\n const sdk = new SignallingSDK(config);\n await sdk.dpop.init();\n return sdk;\n }\n\n // -------------------------------------------------------------------------\n // sdk.friends — lazy stateful accessor to the global-WS friend protocol.\n // -------------------------------------------------------------------------\n\n /**\n * Friends + presence surface. First access opens the global WS,\n * runs the DPoP handshake, and starts the periodic friends-list\n * refresh. Subsequent accesses return the same instance.\n */\n get friends(): FriendsManager {\n if (!this.friendsManager) {\n const globalWs = this.ensureGlobalConnected();\n this.friendsManager = new FriendsManager(globalWs, this.logger);\n this.friendsManager.bind();\n }\n return this.friendsManager;\n }\n\n // -------------------------------------------------------------------------\n // sdk.calls — joinRoom + invite. Lazy global WS for invites.\n // -------------------------------------------------------------------------\n\n /**\n * High-level call orchestration:\n * - `joinRoom(config)` — combines join-over-REST, TURN fetch, local\n * media acquisition, WS handshake, and WebRTC wiring into a\n * single `Call` handle.\n * - `invite(toUsername, roomId)` — sends a `call_invite` frame on the\n * global WS so a friend gets a real-time invitation push.\n * - `on('call.invite-received', cb)` — subscribes to inbound\n * invitations from friends. Opens the global WS on first\n * subscribe so consumers don't need to manually start it.\n */\n get calls() {\n return {\n joinRoom: async (config: CallConfig): Promise<Call> => this.joinRoom(config),\n invite: (toUsername: string, roomId: number | string): void => {\n const mgr = this.ensureCallInviteManager();\n mgr.invite(toUsername, roomId);\n },\n on: <K extends 'call.invite-received'>(\n event: K,\n listener: (payload: import('./types').IncomingCallInvite) => void\n ): (() => void) => {\n const mgr = this.ensureCallInviteManager();\n return mgr.on(event, listener);\n },\n };\n }\n\n private ensureCallInviteManager(): CallInviteManager {\n if (!this.callInviteManager) {\n const globalWs = this.ensureGlobalConnected();\n this.callInviteManager = new CallInviteManager(globalWs, this.logger);\n this.callInviteManager.bind();\n }\n return this.callInviteManager;\n }\n\n /**\n * Lazily construct and connect the global WS. The global socket lives\n * for the SDK instance lifetime; it carries friend protocol frames\n * and call invites, independently of any active room.\n */\n private ensureGlobalConnected(): WebSocketClient {\n if (!this.globalWs) {\n this.globalWs = new WebSocketClient(this.api, this.dpop, this.logger);\n // Fire-and-forget: the FriendsManager / CallInviteManager wait\n // on the `open` event, so we don't need to await here.\n this.globalWs.connectGlobal().catch((err) => {\n this.logger.warn('global WS connect failed', err);\n });\n }\n return this.globalWs;\n }\n\n /**\n * Implementation of `calls.joinRoom`. Kept as a method on the SDK so\n * arrow-functions on the `calls` getter don't trap a stale `this`.\n */\n private async joinRoom(config: CallConfig): Promise<Call> {\n if (this.activeCall) {\n this.logger.info('Replacing existing call');\n try {\n await this.activeCall.leave();\n } catch (err) {\n this.logger.warn('previous leave() failed; continuing', err);\n } finally {\n for (const off of this.callBridges) off();\n this.callBridges = [];\n this.activeCall = null;\n }\n }\n\n // 1. Tell the backend we want in.\n const joined: CreateRoomResponse | JoinRoomResponse = await this.rooms.join(config.roomId);\n\n // 2. TURN — non-fatal: if the call fails we still try with public STUN.\n try {\n const cred = await this.api.getTurnCredentials();\n this.webrtc.setTurnCredentials(cred);\n } catch (err) {\n this.logger.warn('TURN credential fetch failed; falling back to STUN-only', err);\n this.webrtc.setTurnCredentials(null);\n }\n\n // 3. Local media — also non-fatal so users can join listen-only.\n let localStream: MediaStream | null = null;\n if (config.media && (config.media.audio || config.media.video)) {\n try {\n localStream = await this.devices.getLocalStream(config.media);\n await this.webrtc.setLocalStream(localStream);\n } catch (err) {\n this.logger.warn('getUserMedia failed; joining without local media', err);\n }\n }\n\n // 4. WS handshake (ticket → upgrade → DPoP first frame → ack).\n await this.ws.connectRoom(joined.roomId, joined.clientId);\n\n // 5. WebRTC signaling pump.\n this.webrtc.bind({ clientId: joined.clientId });\n\n // 6. Build the Call surface.\n const call = new Call(\n joined.roomId,\n joined.clientId,\n this.rooms,\n this.webrtc,\n this.ws,\n this.devices,\n this.auth\n );\n call.localStream = localStream;\n this.bindCallEvents(call);\n this.activeCall = call;\n return call;\n }\n\n /**\n * Wire WS / WebRTC event streams into the `Call` event map. Each\n * subscription returns its own unsubscribe; we collect them so a\n * subsequent `leave()` / `joinRoom()` can detach cleanly.\n */\n private bindCallEvents(call: Call): void {\n // Tear down any leftovers from a previous call before re-binding.\n for (const off of this.callBridges) off();\n this.callBridges = [];\n\n // Camera streams.\n this.callBridges.push(this.webrtc.on('stream.added', (e) => call.emit('stream.added', e)));\n this.callBridges.push(this.webrtc.on('stream.removed', (e) => call.emit('stream.removed', e)));\n // Screen-share streams — separate event surface.\n this.callBridges.push(this.webrtc.on('screen-share.added', (e) => call.emit('screen-share.added', e)));\n this.callBridges.push(this.webrtc.on('screen-share.removed', (e) => call.emit('screen-share.removed', e)));\n // Peer media state side channel.\n this.callBridges.push(\n this.webrtc.on('peer.media-state-changed', (e) => call.emit('peer.media-state-changed', e))\n );\n this.callBridges.push(\n this.webrtc.on('peer.media-stopped', (e) => call.emit('peer.media-stopped', e))\n );\n\n // Signal frames discriminate by `from`: server vs peer. We handle\n // server signals (join/leave/host-changed/etc.) here and route peer\n // signals through Call as either `chat` or the generic `raw` event.\n this.callBridges.push(\n this.ws.on('type:signal', (msg: WSMessage) => {\n const payload = msg.payload as\n | ServerSignalPayload\n | PeerSignalPayload\n | undefined;\n if (msg.from === 'server') {\n this.dispatchServerSignal(call, payload as ServerSignalPayload | undefined, msg);\n } else {\n this.dispatchPeerSignal(call, msg.from, payload as PeerSignalPayload | undefined, msg);\n }\n })\n );\n\n // Connection lifecycle.\n this.callBridges.push(\n this.ws.on('close', (e) =>\n call.emit('connection.closed', {\n code: e.code,\n reason: e.reason,\n fatal: isFatalCloseCode(e.code),\n })\n )\n );\n this.callBridges.push(this.ws.on('open', () => call.emit('connection.reopened', undefined)));\n this.callBridges.push(this.ws.on('message', (msg) => call.emit('raw', msg)));\n }\n\n private dispatchServerSignal(\n call: Call,\n payload: ServerSignalPayload | undefined,\n msg: WSMessage\n ): void {\n switch (payload?.action) {\n case 'join':\n call.emit('peer.joined', {\n clientId: payload.newClientId,\n name: payload.newClientName,\n avatarUrl: payload.newClientAvatar,\n });\n // Mesh mode: the established client initiates an offer to the\n // newcomer. SFU mode lets the SFU drive renegotiation.\n if (!this.shouldUseSfu()) {\n this.webrtc\n .callPeer(payload.newClientId)\n .catch((err) => this.logger.warn('callPeer failed', err));\n }\n break;\n case 'leave':\n call.emit('peer.left', {\n clientId: payload.leftClientId,\n name: payload.leftClientName,\n });\n break;\n case 'hostChanged':\n call.emit('host.changed', { newHostClientId: payload.newHostId });\n break;\n case 'stopScreenShare':\n call.emit('screen-share.stop', undefined);\n break;\n case 'permissionsUpdated':\n call.emit('permissions.updated', {\n canAudio: payload.canAudio,\n canVideo: payload.canVideo,\n canScreenShare: payload.canScreenShare,\n });\n break;\n default:\n call.emit('raw', msg);\n }\n }\n\n private dispatchPeerSignal(\n call: Call,\n _fromClientId: string,\n payload: PeerSignalPayload | undefined,\n msg: WSMessage\n ): void {\n if (!payload || typeof payload !== 'object') return;\n switch (payload.action) {\n case 'chat':\n // Forward as a typed chat event. `WebRTCManager` doesn't handle\n // chat — only Call exposes it.\n call.emit('chat', {\n id: payload.id,\n from: msg.from,\n fromName: payload.fromName,\n text: payload.text,\n timestamp: payload.timestamp,\n });\n break;\n case 'mediaState':\n case 'mediaStopped':\n case 'screenShareStream':\n case 'screenShareStopped':\n // WebRTCManager already handled these and emitted the typed\n // events Call bridges above; nothing extra to do here.\n break;\n default:\n call.emit('raw', msg);\n }\n }\n\n /**\n * Placeholder hook for the SFU rollout. Returns `false` today; will\n * become a config-driven switch once the SFU client is exposed.\n */\n private shouldUseSfu(): boolean {\n return false;\n }\n}\n\n/**\n * Mild-uniqueness chat id — `timestamp + random`. Not a UUID (no need\n * for collision resistance across orgs; chat ids are room-scoped and\n * short-lived). Generated client-side so the sender can render its\n * message immediately without round-tripping through the server.\n */\nfunction generateChatId(): string {\n return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;\n}\n","/**\n * Public types for `@signalling/sdk`.\n *\n * Every type in this file maps to a wire-protocol artefact on the\n * signalling HTTP / WebSocket API. Keep field names aligned with the\n * server’s JSON contracts. See `docs/` in this package for integration\n * context.\n */\n\n// ---------------------------------------------------------------------------\n// SDK configuration\n// ---------------------------------------------------------------------------\n\n/**\n * Authentication mode the SDK will use.\n *\n * - `bff` — Browser-for-frontend cookie flow. The backend holds Keycloak\n * tokens; the browser only carries an opaque `session_id` cookie + a\n * per-request DPoP proof. This is the default and only fully supported\n * mode in v1.\n * - `bearer` — Native client style: the integrator already owns an OIDC\n * access token and provides it via the `getAccessToken` callback. The\n * SDK adds the `Authorization: DPoP <jwt>` header and binds the proof\n * via the `ath` claim. Reserved for a later milestone.\n */\nexport type AuthMode = 'bff' | 'bearer';\n\n/**\n * Severity passed to a logger. Order matches RFC-style log levels.\n */\nexport type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug';\n\n/**\n * Optional structured logger hook. The SDK never writes to `console`\n * directly; it always goes through this interface so integrators can\n * pipe logs into their own observability stack.\n */\nexport interface Logger {\n error: (...args: unknown[]) => void;\n warn: (...args: unknown[]) => void;\n info: (...args: unknown[]) => void;\n debug: (...args: unknown[]) => void;\n}\n\n/**\n * Configuration object passed to `SignallingSDK.create()`.\n *\n * @example\n * ```ts\n * const sdk = await SignallingSDK.create({\n * baseUrl: \"https://api.example.com\",\n * authMode: \"bff\",\n * appId: \"customer-app-123\",\n * logLevel: \"info\",\n * });\n * ```\n */\nexport interface SignallingSDKConfig {\n /**\n * Fully-qualified scheme + host of the backend (e.g. `https://api.confrnce.io`).\n * Trailing slash is normalised away. The SDK derives WebSocket URLs by\n * swapping `http` → `ws` / `https` → `wss` on this value.\n */\n baseUrl: string;\n\n /**\n * Optional tenant / application identifier. Sent to the backend on\n * select endpoints when present; reserved for the multi-tenant rollout.\n */\n appId?: string;\n\n /**\n * Auth mode. Defaults to `bff`.\n */\n authMode?: AuthMode;\n\n /**\n * Bearer-mode hook. Required when `authMode === 'bearer'`; ignored\n * otherwise. Must return a fresh OIDC access token (refresh-rotated\n * by the integrator).\n */\n getAccessToken?: () => Promise<string>;\n\n /**\n * Override the global `fetch`. Useful for tests / SSR / polyfilled\n * runtimes. Defaults to `globalThis.fetch`.\n */\n fetch?: typeof fetch;\n\n /**\n * Custom logger. Defaults to a console-backed logger gated by `logLevel`.\n */\n logger?: Logger;\n\n /**\n * Minimum log level emitted by the default logger. Ignored when a\n * custom `logger` is provided. Defaults to `'warn'`.\n */\n logLevel?: LogLevel;\n\n /**\n * Convenience flag: `true` is shorthand for `logLevel: 'debug'`.\n * Setting both — `logLevel` wins.\n */\n debug?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Domain models — keep these aligned with `backend/models/*.go`\n// ---------------------------------------------------------------------------\n\n/**\n * Lifecycle status of a user account. Mirrors `models.UserStatus` in the\n * backend (`backend/models/user.go`) — values are emitted UPPERCASE on the\n * wire. Keycloak owns the authentication state; this enum is the app-level\n * status used for bans / suspensions / soft-delete that shouldn't round-trip\n * through Keycloak.\n */\nexport type UserStatus =\n | 'ACTIVE'\n | 'PENDING_VERIFICATION'\n | 'SUSPENDED'\n | 'BANNED'\n | 'DELETED';\n\n/**\n * A user record as returned by `GET /me` and `GET /users/search`.\n * Mirrors the JSON projection of `backend/models/user.go::User` — fields\n * marked `json:\"-\"` on the Go struct (`ID`, `KeycloakSubjectID`) are\n * intentionally NOT present on the wire and are NOT modeled here.\n */\nexport interface UserProfile {\n username: string;\n firstName: string;\n lastName?: string | null;\n email?: string | null;\n emailVerified?: boolean;\n phone?: string | null;\n phoneVerified?: boolean;\n avatarUrl?: string | null;\n status?: UserStatus;\n statusReason?: string | null;\n isStaff?: boolean;\n lastLoginAt?: string | null;\n createdAt?: string;\n updatedAt?: string;\n}\n\n/**\n * A connected client (one per browser tab / device) as embedded inside\n * `models.Room.Clients`. Mirrors the JSON projection of\n * `backend/models/client.go::Client` — `UserID` is `json:\"-\"` on the Go\n * struct and is NOT exposed on the wire (look up via `user.username`\n * instead).\n *\n * `roomId` is the snowflake id rendered as a string at this layer (see\n * note on `Room.id`).\n */\nexport interface RoomClient {\n clientId: string;\n roomId: string;\n user?: UserProfile;\n joinedAt?: string;\n}\n\n/**\n * Room as returned by `GET /viewroom/{roomId}`.\n * Mirrors the JSON projection of `backend/models/room.go::Room`.\n *\n * NOTE on `id`: the backend serializes the snowflake id as a JSON number\n * (`int64`). JavaScript's `number` cannot represent integers above 2^53\n * losslessly, so the SDK normalises the snowflake to `string` at the API\n * boundary. Treat the value as opaque; never do arithmetic on it. (A\n * future revision may switch to `bigint` once we add a json-bigint\n * dependency — until then the string normalisation matches the\n * conventional workaround used by every JS consumer of this API.)\n */\nexport interface Room {\n id: string;\n status: string;\n host?: UserProfile;\n currentScreenSharerClientId?: string;\n clients: RoomClient[];\n createdAt: string;\n updatedAt: string;\n}\n\n/**\n * Response of `POST /createroom` and `POST /joinroom/{roomId}`.\n * The SDK keeps these as separate types — the backend does too — even\n * though their shapes coincide today.\n *\n * `roomId` is normalised to `string` at the API boundary; see the note on\n * `Room.id`.\n */\nexport interface CreateRoomResponse {\n roomId: string;\n clientId: string;\n}\n\nexport interface JoinRoomResponse {\n roomId: string;\n clientId: string;\n}\n\n/**\n * Response of `POST /v1/turn/credentials`.\n * Mirrors `nat/internal/turn::TurnCredential` and the wire-protocol doc\n * at `docs/wire-protocol-turn-credentials.md`.\n */\nexport interface TurnCredential {\n username: string;\n credential: string;\n urls: string[];\n ttl: number;\n}\n\n/**\n * Response of `POST /auth/ws-ticket`.\n */\nexport interface WSTicket {\n ticket: string;\n nonce: string;\n expiresIn: number;\n}\n\n/**\n * Response of `GET /room/{roomId}/{clientId}/permissions`\n * and `PUT` of the same path.\n *\n * `roomId` is normalised to `string` at the API boundary; see the note\n * on `Room.id`.\n */\nexport interface ClientPermissions {\n roomId: string;\n clientId: string;\n canAudio: boolean;\n canVideo: boolean;\n canScreenShare: boolean;\n}\n\n/**\n * Body of `PATCH /me`.\n */\nexport interface UpdateProfileRequest {\n firstName?: string;\n lastName?: string;\n avatarUrl?: string;\n phone?: string;\n username?: string;\n}\n\n// ---------------------------------------------------------------------------\n// WebSocket protocol\n// ---------------------------------------------------------------------------\n\n/**\n * Wire envelope for room WebSocket messages.\n * Mirrors `backend/websocket/hub.go::WSMessage`.\n *\n * `payload` is intentionally `unknown` — message-type-specific shapes\n * live below as discriminated unions when emitted as events.\n */\nexport interface WSMessage<TPayload = unknown> {\n type: string;\n from: string;\n to: string;\n payload: TPayload;\n}\n\n/**\n * Server-originated signal payload (`type: 'signal'`, `from: 'server'`).\n * The `action` field discriminates between concrete events the backend\n * emits today. The taxonomy of \"server signals\" is kept distinct from\n * \"peer signals\" because the trust model differs — server signals are\n * authoritative; peer signals are anything one client decides to send\n * through the relay.\n */\nexport type ServerSignalPayload =\n | { action: 'join'; newClientId: string; newClientName?: string; newClientAvatar?: string }\n | { action: 'leave'; leftClientId: string; leftClientName?: string }\n | { action: 'hostChanged'; newHostId: string }\n | { action: 'stopScreenShare' }\n | {\n action: 'permissionsUpdated';\n canAudio: boolean;\n canVideo: boolean;\n canScreenShare: boolean;\n };\n\n/**\n * Peer-originated signal payload (`type: 'signal'`, `from: <clientId>`).\n * These ride the same `signal` envelope as server signals but are emitted\n * by other clients in the room, NOT the backend. Receivers should validate\n * fields and never assume authority — a malicious peer could craft any of\n * these.\n *\n * - `mediaState` is broadcast on local mic/cam toggle and tracked per\n * peer so other UIs can render mute/camera-off indicators.\n * - `mediaStopped` is broadcast when a peer ends all local media (camera\n * and screen share) — the receiver tears down both streams.\n * - `screenShareStream` announces the `MediaStream.id` the sender is about\n * to send so the receiver can route the resulting track to a separate\n * \"screen share\" surface instead of treating it as camera.\n * - `screenShareStopped` mirrors `screenShareStream` on the teardown side.\n * - `chat` is the room-WS chat message (auto-broadcast by `Call.sendChat`).\n */\nexport type PeerSignalPayload =\n | { action: 'mediaState'; video: boolean; audio: boolean }\n | { action: 'mediaStopped' }\n | { action: 'screenShareStream'; streamId: string }\n | { action: 'screenShareStopped'; streamId: string }\n | {\n action: 'chat';\n id: string;\n fromName: string;\n text: string;\n timestamp: string;\n };\n\n/**\n * SDP payload — used in `type: 'sdp'` and SFU-mode `sfu-offer` / `sfu-answer`\n * messages. Mirrors `backend/sfu::SDPPayload`.\n */\nexport interface SdpPayload {\n type: 'offer' | 'answer';\n sdp: string;\n}\n\n/**\n * ICE candidate payload — used in `type: 'ice'` and SFU-mode `sfu-ice`\n * messages. Matches the JSON serialisation produced by\n * `RTCPeerConnection.onicecandidate`.\n */\nexport interface IceCandidatePayload {\n candidate: string;\n sdpMid?: string | null;\n sdpMLineIndex?: number | null;\n usernameFragment?: string | null;\n}\n\n// ---------------------------------------------------------------------------\n// High-level call API\n// ---------------------------------------------------------------------------\n\n/**\n * Constraints accepted by `SignallingSDK.calls.joinRoom`. The SDK\n * forwards them to `getUserMedia` after acquiring the local stream.\n */\nexport interface MediaConstraints {\n audio?: boolean | MediaTrackConstraints;\n video?: boolean | MediaTrackConstraints;\n}\n\n/**\n * Configuration accepted by `sdk.calls.joinRoom`.\n */\nexport interface CallConfig {\n /** Snowflake room id returned by `sdk.rooms.create()` or shared out-of-band. */\n roomId: number | string;\n\n /** Local media constraints. Omit to skip `getUserMedia` (e.g. listen-only). */\n media?: MediaConstraints;\n\n /**\n * If true the SDK will attempt to join the room's SFU pipeline by\n * sending `sfu-join` after the WS handshake. Defaults to `false`\n * (peer-to-peer / mesh mode).\n */\n useSfu?: boolean;\n}\n\n/**\n * A single chat message delivered over the room WebSocket. Created by\n * `Call.sendChat` on the sender and emitted as the `'chat'` event on\n * each receiver. `id` is generated by the sender; `fromName` is filled\n * in from the cached user profile when present (falls back to clientId).\n */\nexport interface ChatMessage {\n id: string;\n from: string;\n fromName: string;\n text: string;\n timestamp: string;\n}\n\n/**\n * Per-peer media on/off state. Reflects the most recent `mediaState`\n * signal seen from that peer; an absent entry means the peer has not\n * yet announced its state and SHOULD be treated as `{ video: true,\n * audio: true }` (the implicit default for newly-joined clients).\n */\nexport interface PeerMediaState {\n video: boolean;\n audio: boolean;\n}\n\n/**\n * Strongly-typed event map emitted by the room `Call` object returned\n * by `sdk.calls.joinRoom`.\n */\nexport interface CallEventMap {\n /** A new peer joined the room. */\n 'peer.joined': {\n clientId: string;\n name?: string;\n avatarUrl?: string;\n };\n /** A peer left the room (intentionally or via disconnect). */\n 'peer.left': {\n clientId: string;\n name?: string;\n };\n /** The room host changed (the original host left). */\n 'host.changed': { newHostClientId: string };\n /** The server told us we are no longer the active screen-sharer. */\n 'screen-share.stop': void;\n /** The host updated our media permissions. */\n 'permissions.updated': Omit<ClientPermissions, 'roomId' | 'clientId'>;\n /** Remote camera `MediaStream` arrived from a peer. Fired once per peer. */\n 'stream.added': { clientId: string; stream: MediaStream };\n /** Remote peer's camera `MediaStream` is gone. */\n 'stream.removed': { clientId: string };\n /** Remote peer started screen sharing — emitted distinct from `stream.added`. */\n 'screen-share.added': { clientId: string; stream: MediaStream };\n /** Remote peer stopped screen sharing. */\n 'screen-share.removed': { clientId: string };\n /** Peer's mic/camera enable state changed (via their `mediaState` broadcast). */\n 'peer.media-state-changed': { clientId: string; state: PeerMediaState };\n /** Peer ended all media (camera + screen share) but is still in the room. */\n 'peer.media-stopped': { clientId: string };\n /** A chat message arrived (room WS signal with `action: 'chat'`). */\n 'chat': ChatMessage;\n /**\n * Underlying WebSocket closed. `fatal === true` when the close code is\n * one the SDK will NOT auto-retry (auth failed, room/client missing,\n * DPoP replay, etc.) — UIs should render an end-of-session state\n * rather than a spinner. See `WSCloseCode` and the `NO_RETRY_CODES`\n * set in `WebSocketClient`.\n */\n 'connection.closed': { code: number; reason: string; fatal: boolean };\n /** Underlying WebSocket reconnected after a transient drop. */\n 'connection.reopened': void;\n /** Catch-all for anything we don't model — useful for debugging. */\n 'raw': WSMessage;\n}\n\n// ---------------------------------------------------------------------------\n// Friends + call invites (global WS)\n// ---------------------------------------------------------------------------\n\n/**\n * A confirmed friend, as returned by `sdk.friends.list()` and embedded\n * in `'friend.added'` events. Mirrors the JSON projection emitted by\n * the global WS `friend_list` response — `{friendshipId, username,\n * isOnline}`.\n *\n * The backend addresses peers by **username** (not user id) on the\n * global WS — internal numeric ids are never on the wire. Pass\n * `username` back as the `to` field of any outbound frame.\n */\nexport interface Friend {\n friendshipId: string;\n /** Peer's public username — the addressing handle for global-WS frames. */\n username: string;\n isOnline: boolean;\n}\n\n/**\n * A pending incoming friend request. Returned by `sdk.friends.pending()`\n * and emitted as the payload of `'friend.request-received'` events.\n * `requester` is the requesting peer's username.\n */\nexport interface PendingFriendRequest {\n friendshipId: string;\n requester: string;\n createdAt: string;\n}\n\n/**\n * Strongly-typed event map emitted by `sdk.friends` (and the call-invite\n * surface on `sdk.calls`). Friends maintain a stateful cache inside the\n * SDK; events fire whenever that cache changes.\n */\nexport interface FriendsEventMap {\n /** Cached friends list changed (after refresh, accept, remove, etc.). */\n 'friends.changed': { friends: Friend[] };\n /** A new incoming friend request landed in the pending bucket. */\n 'friend.request-received': PendingFriendRequest;\n /** Pending requests cache changed (after refresh, accept, reject, etc.). */\n 'friends.pending-changed': { pending: PendingFriendRequest[] };\n /** A friend we previously requested has accepted us — they're now in `friends`. */\n 'friend.added': Friend;\n /** A friend was removed (either side). */\n 'friend.removed': { friendshipId: string };\n /** Backend reported an error processing one of our frames. */\n 'friends.error': { originalType: string; error: string };\n}\n\n/**\n * An incoming room invitation from a friend. Emitted by `sdk.calls` when\n * the global WS receives a `call_invite` frame addressed to us.\n */\nexport interface IncomingCallInvite {\n /** Username of the inviter (the public handle the backend addresses peers by). */\n callerUsername: string;\n /** The room id the inviter wants us to join (string-normalised snowflake). */\n roomId: string;\n}\n\n/**\n * Event map for the call-invite surface exposed on `sdk.calls`.\n */\nexport interface CallInviteEventMap {\n 'call.invite-received': IncomingCallInvite;\n}\n\n// ---------------------------------------------------------------------------\n// Error types\n// ---------------------------------------------------------------------------\n\n/**\n * Thrown by `ApiClient` when the backend returns a non-2xx status.\n * The original `Response` is preserved for callers who need to inspect\n * headers (e.g. to read the new `DPoP-Nonce`).\n */\nexport class SignallingApiError extends Error {\n /** HTTP status code returned by the backend. */\n public readonly status: number;\n /** Stable, machine-readable error code (matches `ErrorResponse.code`). */\n public readonly code?: string;\n /** Field name when this is a validation error. */\n public readonly field?: string;\n /** Raw response body, parsed as JSON when possible. */\n public readonly body: unknown;\n /** Original `Response` for advanced callers. */\n public readonly response: Response;\n\n constructor(message: string, response: Response, body: unknown) {\n super(message);\n this.name = 'SignallingApiError';\n this.status = response.status;\n this.response = response;\n this.body = body;\n if (body && typeof body === 'object') {\n const b = body as Record<string, unknown>;\n if (typeof b.code === 'string') this.code = b.code;\n if (typeof b.field === 'string') this.field = b.field;\n }\n }\n}\n\n/**\n * Thrown by `WebSocketClient` when the WS upgrade or DPoP first-frame\n * handshake fails.\n */\nexport class SignallingWebSocketError extends Error {\n /** Application close code (4400-4409 for DPoP, 4xxx-domain for room checks). */\n public readonly code?: number;\n\n constructor(message: string, code?: number) {\n super(message);\n this.name = 'SignallingWebSocketError';\n this.code = code;\n }\n}\n","/**\n * Base64URL encoding helpers used by the DPoP layer.\n *\n * Centralised in one file so the rest of the SDK never has to think\n * about the `+/` → `-_` substitution + padding strip.\n */\n\n/** Encode a string of UTF-8 text to base64url (no padding). */\nexport function b64urlString(input: string): string {\n return b64urlBytes(new TextEncoder().encode(input));\n}\n\n/** Encode raw bytes to base64url (no padding). */\nexport function b64urlBytes(bytes: Uint8Array): string {\n let bin = '';\n for (let i = 0; i < bytes.length; i++) {\n bin += String.fromCharCode(bytes[i]);\n }\n return btoa(bin).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\n/** Decode base64url back to raw bytes. */\nexport function b64urlDecodeBytes(input: string): Uint8Array {\n const padded = input.replace(/-/g, '+').replace(/_/g, '/');\n const padding = padded.length % 4 === 0 ? '' : '='.repeat(4 - (padded.length % 4));\n const bin = atob(padded + padding);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n}\n\n/** Decode base64url back to a UTF-8 string. */\nexport function b64urlDecodeString(input: string): string {\n return new TextDecoder().decode(b64urlDecodeBytes(input));\n}\n","import { b64urlBytes, b64urlString } from '../internal/base64url';\n\n/**\n * Browser-side DPoP implementation. Mirrors `backend/auth/dpop/` exactly:\n *\n * 1. Generates a non-extractable ECDSA P-256 keypair the first time the\n * SDK is used on a given browser profile.\n * 2. Persists the keypair in IndexedDB so it survives page reloads\n * (private key bytes never leave the WebCrypto subsystem).\n * 3. Builds a fresh DPoP JWS proof for every authenticated request,\n * binding it to the request's `htm`/`htu` claims and to the latest\n * server-issued `nonce`.\n * 4. Caches the most recent `DPoP-Nonce` returned by the backend so the\n * next request can include it in the proof's `nonce` claim.\n *\n * The public surface is intentionally small — `ApiClient` and the\n * `WebSocketClient` are the only consumers.\n *\n * See `docs/auth.md` for how DPoP fits into the overall login flow.\n */\n\nconst DB_NAME = 'signalling-sdk-dpop';\nconst DB_VERSION = 1;\nconst STORE = 'keys';\nconst KEY_ID = 'dpop-keypair';\n\nconst ALGO: EcKeyGenParams = { name: 'ECDSA', namedCurve: 'P-256' };\nconst SIGN_ALGO: EcdsaParams = { name: 'ECDSA', hash: 'SHA-256' };\n\n/**\n * Per-origin nonce cache. The SDK stores the most recent\n * `DPoP-Nonce` value the backend handed out so the next outgoing\n * request can echo it. The cache is intentionally a single value\n * (not a queue) because the server only ever issues monotonically\n * fresher nonces — older ones become invalid the moment a fresher\n * one is published.\n */\nclass NonceCache {\n private value: string | undefined;\n\n get(): string | undefined {\n return this.value;\n }\n\n set(value: string | null | undefined): void {\n if (value && typeof value === 'string') this.value = value;\n }\n\n clear(): void {\n this.value = undefined;\n }\n}\n\n/**\n * `DPoPManager` owns the lifetime of the device's signing key plus a\n * single nonce cache. It is meant to be instantiated exactly once per\n * `SignallingSDK` instance.\n */\nexport class DPoPManager {\n private keypair: CryptoKeyPair | null = null;\n private cachedJWK: JsonWebKey | null = null;\n private readonly nonces = new NonceCache();\n private initPromise: Promise<void> | null = null;\n\n /**\n * Eagerly load (or create) the keypair. Safe to call multiple times —\n * concurrent callers all wait on the same `Promise`. Recommended at\n * SDK boot so the first authed request doesn't pay the keygen cost.\n */\n async init(): Promise<void> {\n if (!this.initPromise) {\n this.initPromise = this.loadOrGenerate();\n }\n return this.initPromise;\n }\n\n private async loadOrGenerate(): Promise<void> {\n if (typeof crypto === 'undefined' || !crypto.subtle) {\n throw new Error(\n '[signalling-sdk] DPoP requires WebCrypto. Run the SDK on a secure context (HTTPS or localhost).'\n );\n }\n if (typeof indexedDB === 'undefined') {\n // No IDB → in-memory keypair (loses persistence across reloads).\n // Still useful in unit tests / SSR; logged via console because the\n // logger isn't wired into the DPoP layer (kept dependency-free).\n this.keypair = await crypto.subtle.generateKey(ALGO, /* extractable */ false, ['sign']);\n this.cachedJWK = await crypto.subtle.exportKey('jwk', this.keypair.publicKey);\n return;\n }\n\n const existing = await idbGet();\n if (existing) {\n this.keypair = existing;\n this.cachedJWK = await crypto.subtle.exportKey('jwk', existing.publicKey);\n return;\n }\n\n this.keypair = await crypto.subtle.generateKey(ALGO, /* extractable */ false, ['sign']);\n this.cachedJWK = await crypto.subtle.exportKey('jwk', this.keypair.publicKey);\n try {\n await idbPut(this.keypair);\n } catch {\n // IDB write failures are non-fatal — the in-memory key still works\n // for this session. The next reload will simply regenerate.\n }\n }\n\n /**\n * Build a DPoP proof JWS for the given request.\n *\n * @param method HTTP verb in upper-case (`GET`, `POST`, ...).\n * For WS upgrades pass `GET`.\n * @param url Full URL **including** query for HTTP requests, or\n * with the query stripped for WebSocket upgrades.\n * The caller should pre-canonicalise — `htu` must\n * byte-match the URL the server reconstructs.\n * @param accessTokenHash Optional `ath` claim — `b64url(SHA256(token))`.\n * Required in bearer (mobile) flows; omitted in BFF.\n */\n async generateProof(\n method: string,\n url: string,\n accessTokenHash?: string\n ): Promise<string> {\n await this.init();\n if (!this.keypair || !this.cachedJWK) {\n throw new Error('[signalling-sdk] DPoP keypair unavailable.');\n }\n\n // Strip extraneous fields the JWK exporter sometimes adds. The\n // server hashes the JWK to compute `jkt` so any extra field becomes\n // a thumbprint mismatch.\n const cleanJWK = sanitiseJWK(this.cachedJWK);\n\n const header = {\n typ: 'dpop+jwt',\n alg: 'ES256',\n jwk: cleanJWK,\n };\n const payload: Record<string, unknown> = {\n htm: method.toUpperCase(),\n htu: url,\n iat: Math.floor(Date.now() / 1000),\n jti: cryptoRandomUUID(),\n };\n const nonce = this.nonces.get();\n if (nonce) payload.nonce = nonce;\n if (accessTokenHash) payload.ath = accessTokenHash;\n\n const headerB64 = b64urlString(JSON.stringify(header));\n const payloadB64 = b64urlString(JSON.stringify(payload));\n const signingInput = `${headerB64}.${payloadB64}`;\n const sig = await crypto.subtle.sign(\n SIGN_ALGO,\n this.keypair.privateKey,\n new TextEncoder().encode(signingInput)\n );\n return `${signingInput}.${b64urlBytes(new Uint8Array(sig))}`;\n }\n\n /**\n * Update the cached nonce after every backend response. Pass the\n * `Headers` object (or anything with `get`) — `null` is a no-op.\n */\n updateNonceFromResponse(headers: Headers | { get: (n: string) => string | null }): void {\n const value = headers.get('DPoP-Nonce') || headers.get('dpop-nonce');\n if (value) this.nonces.set(value);\n }\n\n /**\n * Manually set the cached nonce. Used by `WebSocketClient` after it\n * extracts the WS-ticket-embedded nonce.\n */\n setNonce(nonce: string | null | undefined): void {\n this.nonces.set(nonce);\n }\n\n /**\n * Drop both the cached nonce and the persisted keypair. The integrator\n * should call this on logout to ensure the next login binds a fresh key.\n */\n async reset(): Promise<void> {\n this.nonces.clear();\n this.keypair = null;\n this.cachedJWK = null;\n this.initPromise = null;\n if (typeof indexedDB !== 'undefined') {\n try {\n await idbDelete();\n } catch {\n // Best-effort; we still cleared the in-memory state.\n }\n }\n }\n\n /**\n * Compute the SHA-256 base64url-encoded thumbprint of the OAuth\n * access token, suitable for use as the `ath` claim on bearer flows.\n */\n static async accessTokenHash(token: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(token));\n return b64urlBytes(new Uint8Array(digest));\n }\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Strip extras from the JWK that browsers add — `key_ops`, `ext`, `alg`.\n * The backend's thumbprint computation hashes a canonical JWK with only\n * `crv`, `kty`, `x`, `y` for EC keys, so anything else here would cause\n * a `jkt` mismatch on the server.\n */\nfunction sanitiseJWK(jwk: JsonWebKey): JsonWebKey {\n return {\n crv: jwk.crv,\n kty: jwk.kty,\n x: jwk.x,\n y: jwk.y,\n };\n}\n\n/**\n * `crypto.randomUUID` is widely available but not universal — fall back\n * to an RFC4122-formatted random UUID derived from `getRandomValues`.\n */\nfunction cryptoRandomUUID(): string {\n if (typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n bytes[6] = (bytes[6] & 0x0f) | 0x40;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n\n// ---------------------------------------------------------------------------\n// IndexedDB plumbing — single record, single store, no migrations.\n// ---------------------------------------------------------------------------\n\nfunction openDB(): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const req = indexedDB.open(DB_NAME, DB_VERSION);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains(STORE)) {\n db.createObjectStore(STORE);\n }\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error ?? new Error('idb open failed'));\n });\n}\n\nasync function idbGet(): Promise<CryptoKeyPair | null> {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, 'readonly');\n const req = tx.objectStore(STORE).get(KEY_ID);\n req.onsuccess = () => resolve((req.result as CryptoKeyPair | undefined) ?? null);\n req.onerror = () => reject(req.error ?? new Error('idb get failed'));\n tx.oncomplete = () => db.close();\n });\n}\n\nasync function idbPut(keypair: CryptoKeyPair): Promise<void> {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, 'readwrite');\n const req = tx.objectStore(STORE).put(keypair, KEY_ID);\n req.onsuccess = () => resolve();\n req.onerror = () => reject(req.error ?? new Error('idb put failed'));\n tx.oncomplete = () => db.close();\n });\n}\n\nasync function idbDelete(): Promise<void> {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, 'readwrite');\n const req = tx.objectStore(STORE).delete(KEY_ID);\n req.onsuccess = () => resolve();\n req.onerror = () => reject(req.error ?? new Error('idb delete failed'));\n tx.oncomplete = () => db.close();\n });\n}\n","import {\n ClientPermissions,\n CreateRoomResponse,\n JoinRoomResponse,\n Logger,\n Room,\n RoomClient,\n SignallingApiError,\n SignallingSDKConfig,\n TurnCredential,\n UpdateProfileRequest,\n UserProfile,\n WSTicket,\n} from '../types';\nimport { DPoPManager } from '../dpop';\n\n// ---------------------------------------------------------------------------\n// Raw wire shapes — what the backend actually emits, before the SDK normalises\n// snowflake int64 fields to strings. Kept private to this module; consumers\n// see the public `Room` / `ClientPermissions` types with `roomId: string`.\n// ---------------------------------------------------------------------------\n\ninterface RawRoomClient {\n clientId: string;\n roomId: number | string;\n user?: UserProfile;\n joinedAt?: string;\n}\n\ninterface RawRoom {\n id: number | string;\n status: string;\n host?: UserProfile;\n currentScreenSharerClientId?: string;\n clients?: RawRoomClient[] | null;\n createdAt: string;\n updatedAt: string;\n}\n\ninterface RawClientPermissions {\n roomId: number | string;\n clientId: string;\n canAudio: boolean;\n canVideo: boolean;\n canScreenShare: boolean;\n}\n\nfunction normaliseRoom(raw: RawRoom): Room {\n const clients: RoomClient[] = (raw.clients ?? []).map((c) => ({\n clientId: c.clientId,\n roomId: String(c.roomId),\n user: c.user,\n joinedAt: c.joinedAt,\n }));\n return {\n id: String(raw.id),\n status: raw.status,\n host: raw.host,\n currentScreenSharerClientId: raw.currentScreenSharerClientId,\n clients,\n createdAt: raw.createdAt,\n updatedAt: raw.updatedAt,\n };\n}\n\n/**\n * `ApiClient` is the SDK's REST surface over the signalling HTTP API.\n *\n * Every method maps 1:1 onto a documented backend route. The class only knows about\n * HTTP — nothing in here is aware of rooms, peers, or media.\n *\n * Three things differ from a vanilla `fetch`:\n *\n * 1. **DPoP injection.** Every authenticated request carries a fresh\n * `DPoP: <jws>` header generated by `DPoPManager`. Bearer flows\n * also pull an access token from the integrator's callback and add\n * `Authorization: DPoP <token>` plus the `ath` claim.\n *\n * 2. **Nonce retry.** When the backend responds with `401` and the body\n * code is `use_dpop_nonce`, the new `DPoP-Nonce` header is cached\n * and the request is retried exactly once. Anything else is\n * surfaced as a `SignallingApiError`.\n *\n * 3. **Cookie inclusion.** BFF mode sets `credentials: 'include'` so\n * the opaque `session_id` cookie travels with every request.\n *\n * The auth / DPoP threat model is summarized in `docs/auth.md`.\n */\nexport class ApiClient {\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof fetch;\n private readonly logger: Logger;\n private readonly authMode: 'bff' | 'bearer';\n private readonly getAccessToken?: () => Promise<string>;\n\n constructor(\n config: SignallingSDKConfig,\n private readonly dpop: DPoPManager,\n logger: Logger\n ) {\n this.baseUrl = stripTrailingSlash(config.baseUrl);\n this.fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);\n this.logger = logger;\n this.authMode = config.authMode ?? 'bff';\n this.getAccessToken = config.getAccessToken;\n }\n\n // -------------------------------------------------------------------------\n // Public route helpers — used by other SDK modules to build URLs.\n // -------------------------------------------------------------------------\n\n /** Absolute URL the SDK uses for a relative path. Exposed for WS/auth code. */\n url(path: string): string {\n return this.baseUrl + ensureLeadingSlash(path);\n }\n\n /** Backend base URL (origin only, no trailing slash). */\n get origin(): string {\n return this.baseUrl;\n }\n\n // -------------------------------------------------------------------------\n // Auth endpoints\n // -------------------------------------------------------------------------\n\n /**\n * `POST /auth/dpop/bind` — completes the DPoP bind step. The backend\n * consumes the `bind_token`, materialises the session row, and sets\n * the `session_id` cookie on the response. Returns the `return_to`\n * URL the integrator originally requested (or empty string if none).\n *\n * Called by `AuthClient.completeBind`.\n */\n async dpopBind(bindToken: string): Promise<{ returnTo: string }> {\n const data = await this.request<{ return_to?: string }>('/auth/dpop/bind', {\n method: 'POST',\n body: JSON.stringify({ bind_token: bindToken }),\n });\n return { returnTo: data?.return_to ?? '' };\n }\n\n /** `GET /me` — returns the authenticated user's profile. */\n async getCurrentUser(): Promise<UserProfile> {\n const data = await this.request<{ user: UserProfile }>('/me');\n return data.user;\n }\n\n /** `PATCH /me` — partial profile update. */\n async updateProfile(updates: UpdateProfileRequest): Promise<UserProfile> {\n const data = await this.request<{ user: UserProfile }>('/me', {\n method: 'PATCH',\n body: JSON.stringify(updates),\n });\n return data.user;\n }\n\n /** `POST /auth/logout` — revokes the current session at Keycloak + locally. */\n async logout(): Promise<void> {\n await this.request<unknown>('/auth/logout', { method: 'POST' });\n }\n\n /** `POST /auth/logout-all` — revokes every session for this user. */\n async logoutAll(): Promise<void> {\n await this.request<unknown>('/auth/logout-all', { method: 'POST' });\n }\n\n /** `POST /auth/ws-ticket` — short-lived JWT to open a WebSocket. */\n async issueWSTicket(): Promise<WSTicket> {\n return this.request<WSTicket>('/auth/ws-ticket', { method: 'POST' });\n }\n\n // -------------------------------------------------------------------------\n // Users\n // -------------------------------------------------------------------------\n\n /** `GET /users/search?email=...` — looks up a user by email. */\n async searchUserByEmail(email: string): Promise<UserProfile> {\n const data = await this.request<{ user: UserProfile }>(\n `/users/search?email=${encodeURIComponent(email)}`\n );\n return data.user;\n }\n\n // -------------------------------------------------------------------------\n // Rooms\n // -------------------------------------------------------------------------\n\n /**\n * `POST /createroom` — creates a new room and host client.\n *\n * The backend serializes `roomId` as a JSON number (`int64`); we\n * stringify it at the boundary so the snowflake survives the\n * `JSON.parse` → `number` round-trip. See the note on `Room.id` in\n * the types file. The stringification happens AFTER `JSON.parse`,\n * so values above 2^53 have already lost precision — matching the\n * conventional JS workaround; a future revision will switch to a\n * BigInt-aware parser.\n */\n async createRoom(): Promise<CreateRoomResponse> {\n const raw = await this.request<{ roomId: number | string; clientId: string }>(\n '/createroom',\n { method: 'POST' }\n );\n return { roomId: String(raw.roomId), clientId: raw.clientId };\n }\n\n /** `POST /joinroom/{roomId}` — joins an existing room as a new client. */\n async joinRoom(roomId: number | string): Promise<JoinRoomResponse> {\n const raw = await this.request<{ roomId: number | string; clientId: string }>(\n `/joinroom/${encodeURIComponent(String(roomId))}`,\n { method: 'POST' }\n );\n return { roomId: String(raw.roomId), clientId: raw.clientId };\n }\n\n /** `GET /viewroom/{roomId}` — returns full room state with participants. */\n async viewRoom(roomId: number | string): Promise<Room> {\n const raw = await this.request<RawRoom>(\n `/viewroom/${encodeURIComponent(String(roomId))}`\n );\n return normaliseRoom(raw);\n }\n\n /** `DELETE /leaveroom/{roomId}/{clientId}` — leaves the room. */\n async leaveRoom(roomId: number | string, clientId: string): Promise<void> {\n await this.request<unknown>(`/leaveroom/${roomId}/${clientId}`, { method: 'DELETE' });\n }\n\n // -------------------------------------------------------------------------\n // Screen share\n // -------------------------------------------------------------------------\n\n /** `POST /room/{roomId}/{clientId}/screen-share/start` */\n async startScreenShare(\n roomId: number | string,\n clientId: string\n ): Promise<{ status: string; previousSharer?: string }> {\n return this.request(\n `/room/${encodeURIComponent(String(roomId))}/${encodeURIComponent(clientId)}/screen-share/start`,\n { method: 'POST' }\n );\n }\n\n /** `POST /room/{roomId}/{clientId}/screen-share/stop` */\n async stopScreenShare(\n roomId: number | string,\n clientId: string\n ): Promise<{ status: string }> {\n return this.request(\n `/room/${encodeURIComponent(String(roomId))}/${encodeURIComponent(clientId)}/screen-share/stop`,\n { method: 'POST' }\n );\n }\n\n // -------------------------------------------------------------------------\n // Permissions\n // -------------------------------------------------------------------------\n\n /** `GET /room/{roomId}/{clientId}/permissions` */\n async getPermissions(roomId: number | string, clientId: string): Promise<ClientPermissions> {\n const raw = await this.request<RawClientPermissions>(\n `/room/${encodeURIComponent(String(roomId))}/${encodeURIComponent(clientId)}/permissions`\n );\n return { ...raw, roomId: String(raw.roomId) };\n }\n\n /** `PUT /room/{roomId}/{clientId}/permissions` (host-only). */\n async updatePermissions(\n roomId: number | string,\n clientId: string,\n updates: Partial<Pick<ClientPermissions, 'canAudio' | 'canVideo' | 'canScreenShare'>>\n ): Promise<ClientPermissions> {\n const raw = await this.request<RawClientPermissions>(\n `/room/${encodeURIComponent(String(roomId))}/${encodeURIComponent(clientId)}/permissions`,\n {\n method: 'PUT',\n body: JSON.stringify(updates),\n }\n );\n return { ...raw, roomId: String(raw.roomId) };\n }\n\n // -------------------------------------------------------------------------\n // TURN\n // -------------------------------------------------------------------------\n\n /** `POST /v1/turn/credentials` — fresh, tenant-scoped STUN/TURN credentials. */\n async getTurnCredentials(ttlSeconds: number = 3600): Promise<TurnCredential> {\n return this.request<TurnCredential>('/v1/turn/credentials', {\n method: 'POST',\n body: JSON.stringify({ ttl_seconds: ttlSeconds }),\n });\n }\n\n // -------------------------------------------------------------------------\n // Internal — DPoP-aware fetch with single-shot nonce retry.\n // -------------------------------------------------------------------------\n\n private async request<T>(path: string, init: RequestInit = {}): Promise<T> {\n const url = this.url(path);\n const method = (init.method ?? 'GET').toUpperCase();\n\n // Bearer-mode tokens are pulled fresh on every request so the\n // integrator's refresh logic stays authoritative; missing tokens\n // are surfaced as a regular 401 rather than the SDK guessing.\n let bearerToken: string | undefined;\n if (this.authMode === 'bearer') {\n if (!this.getAccessToken) {\n throw new Error(\n '[signalling-sdk] authMode=bearer requires a getAccessToken callback in SignallingSDKConfig.'\n );\n }\n bearerToken = await this.getAccessToken();\n }\n\n const send = async (): Promise<Response> => {\n const ath = bearerToken ? await DPoPManager.accessTokenHash(bearerToken) : undefined;\n const proof = await this.dpop.generateProof(method, url, ath);\n\n const headers = new Headers(init.headers);\n if (init.body && !headers.has('Content-Type')) {\n headers.set('Content-Type', 'application/json');\n }\n headers.set('DPoP', proof);\n if (bearerToken) headers.set('Authorization', `DPoP ${bearerToken}`);\n\n const response = await this.fetchImpl(url, {\n ...init,\n headers,\n credentials: this.authMode === 'bff' ? 'include' : init.credentials ?? 'omit',\n });\n this.dpop.updateNonceFromResponse(response.headers);\n return response;\n };\n\n // First attempt.\n let response = await send();\n\n // Single retry on a DPoP nonce challenge. The backend's middleware\n // emits 401 with one of three nonce-related body codes — each carries\n // a fresh `DPoP-Nonce` header which `send()` already cached:\n //\n // - `use_dpop_nonce` — the client never had a nonce, or the proof\n // was missing one entirely.\n // - `invalid_nonce` — the nonce in the proof didn't match the\n // backend's expectation (e.g. stale cache).\n // - `expired_nonce` — the nonce was valid once but the backend\n // has rotated past it.\n //\n // All three are recoverable with a single retry. Anything else is\n // surfaced as a `SignallingApiError`.\n if (response.status === 401) {\n const peeked = await peekJsonClone(response);\n const code =\n peeked && typeof peeked === 'object'\n ? (peeked as Record<string, unknown>).code\n : undefined;\n if (\n code === 'use_dpop_nonce' ||\n code === 'invalid_nonce' ||\n code === 'expired_nonce'\n ) {\n this.logger.debug('DPoP nonce challenge — retrying with fresh nonce', { code });\n response = await send();\n }\n }\n\n if (!response.ok) {\n const body = await safeJson(response);\n const message = extractErrorMessage(body, response);\n throw new SignallingApiError(message, response, body);\n }\n\n if (response.status === 204) {\n return undefined as unknown as T;\n }\n if (response.headers.get('content-type')?.includes('application/json')) {\n return (await response.json()) as T;\n }\n return undefined as unknown as T;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction ensureLeadingSlash(path: string): string {\n return path.startsWith('/') ? path : `/${path}`;\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.replace(/\\/+$/, '');\n}\n\n/** Read a Response body as JSON without throwing if it isn't JSON. */\nasync function safeJson(response: Response): Promise<unknown> {\n try {\n const text = await response.text();\n if (!text) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n } catch {\n return undefined;\n }\n}\n\n/**\n * Read the body of a response without consuming it for the caller.\n * Used by the `use_dpop_nonce` retry path so the caller can still\n * read the body if the second attempt also fails.\n */\nasync function peekJsonClone(response: Response): Promise<unknown> {\n try {\n return await response.clone().json();\n } catch {\n return undefined;\n }\n}\n\nfunction extractErrorMessage(body: unknown, response: Response): string {\n if (body && typeof body === 'object') {\n const b = body as Record<string, unknown>;\n if (typeof b.error === 'string') return b.error;\n if (typeof b.message === 'string') return b.message;\n }\n if (typeof body === 'string' && body) return body;\n return `${response.status} ${response.statusText}`;\n}\n","/**\n * Tiny strongly-typed event emitter used by `RoomManager`, `WebRTCManager`,\n * and the `Call` object returned by `sdk.calls.joinRoom`. Kept in `internal/`\n * so we can swap implementations without breaking the public API.\n *\n * `TEventMap` is intentionally typed with a loose constraint — concrete\n * event maps (`CallEventMap`, `WebRTCEventMap`) are *closed* interfaces\n * (no string-index signature) and TypeScript would reject them under\n * `extends Record<string, unknown>`. Per-method generics give callers\n * full type-safety where it matters.\n */\nexport type Listener<T> = (payload: T) => void;\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport class TypedEventEmitter<TEventMap extends Record<string, any>> {\n private listeners = new Map<keyof TEventMap, Set<Listener<TEventMap[keyof TEventMap]>>>();\n\n on<K extends keyof TEventMap>(event: K, fn: Listener<TEventMap[K]>): () => void {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(fn as Listener<TEventMap[keyof TEventMap]>);\n return () => this.off(event, fn);\n }\n\n off<K extends keyof TEventMap>(event: K, fn: Listener<TEventMap[K]>): void {\n this.listeners.get(event)?.delete(fn as Listener<TEventMap[keyof TEventMap]>);\n }\n\n once<K extends keyof TEventMap>(event: K, fn: Listener<TEventMap[K]>): void {\n const off = this.on(event, (payload) => {\n off();\n fn(payload);\n });\n }\n\n /** @internal — used only by the SDK itself, not part of the public API. */\n emit<K extends keyof TEventMap>(event: K, payload: TEventMap[K]): void {\n const set = this.listeners.get(event);\n if (!set) return;\n for (const fn of [...set]) {\n try {\n (fn as Listener<TEventMap[K]>)(payload);\n } catch {\n // Swallow — a buggy handler must not break the rest of the chain.\n }\n }\n }\n\n /** Remove every listener (e.g. on disconnect / SDK teardown). */\n clear(): void {\n this.listeners.clear();\n }\n}\n","import { ApiClient } from '../api/client';\nimport { DPoPManager } from '../dpop';\nimport {\n Logger,\n SignallingApiError,\n SignallingSDKConfig,\n UpdateProfileRequest,\n UserProfile,\n} from '../types';\nimport { TypedEventEmitter } from '../internal/events';\n\n/**\n * Events emitted by `AuthClient`.\n */\nexport interface AuthEventMap {\n /** `getCurrentUser` succeeded — payload is the resolved user. */\n 'state.authenticated': UserProfile;\n /** `getCurrentUser` returned 401 — payload is the original error. */\n 'state.unauthenticated': SignallingApiError | null;\n /** `logout` / `logoutAll` finished. */\n 'state.signed-out': void;\n}\n\n/**\n * `AuthClient` orchestrates the BFF authentication flow (see `docs/auth.md`).\n * The flow looks like this in a\n * browser:\n *\n * 1. SDK consumer calls `auth.login()` — the user is redirected to\n * `${baseUrl}/auth/login` which handles PKCE + Keycloak.\n * 2. Keycloak redirects back via `${baseUrl}/auth/callback`, which\n * writes a 60-second `bind_ticket` and redirects the browser to\n * `${frontendOrigin}/auth/complete#bind_token=…`.\n * 3. The SPA's `/auth/complete` page calls `auth.completeBind(token)`\n * which generates / loads the DPoP keypair and POSTs\n * `/auth/dpop/bind` to materialise the session cookie.\n * 4. From then on every authenticated call carries the session cookie\n * and a fresh DPoP proof, both transparently injected by `ApiClient`.\n *\n * The same SDK works in the bearer / mobile flow if the integrator\n * configures `authMode: 'bearer'` and supplies `getAccessToken`.\n */\nexport class AuthClient extends TypedEventEmitter<AuthEventMap> {\n private cachedUser: UserProfile | null = null;\n\n constructor(\n private readonly config: SignallingSDKConfig,\n private readonly api: ApiClient,\n private readonly dpop: DPoPManager,\n private readonly logger: Logger\n ) {\n super();\n }\n\n /**\n * Synchronous accessor for the cached user profile. Returns `null`\n * until `getCurrentUser()` has resolved at least once. Used by\n * downstream modules (`Call.sendChat`, friends UI) that need the\n * display name without paying the round-trip latency of a fresh\n * fetch.\n */\n get cachedCurrentUser(): UserProfile | null {\n return this.cachedUser;\n }\n\n /**\n * BFF login — navigates the user to the backend's OIDC entry point.\n * Optionally accepts a `returnTo` URL the backend will append as a\n * query parameter; the backend validates this against the configured\n * allowed origins.\n *\n * Bearer mode does not use this method — the integrator runs OIDC +\n * PKCE themselves on the native side.\n */\n login(returnTo?: string): void {\n if (this.config.authMode === 'bearer') {\n throw new Error(\n '[signalling-sdk] auth.login() is BFF-only. In bearer mode the integrator drives the OIDC flow.'\n );\n }\n if (typeof window === 'undefined') {\n throw new Error('[signalling-sdk] auth.login() requires a browser environment.');\n }\n const url = new URL(this.api.url('/auth/login'));\n if (returnTo) url.searchParams.set('redirect', returnTo);\n window.location.href = url.toString();\n }\n\n /**\n * Read the bind token from the current URL fragment (the format\n * Keycloak's callback produces: `/auth/complete#bind_token=…`). Useful\n * shortcut so SPAs can call:\n *\n * ```ts\n * const token = sdk.auth.bindTokenFromFragment();\n * if (token) await sdk.auth.completeBind(token);\n * ```\n */\n bindTokenFromFragment(fragment?: string): string | null {\n const raw = fragment ?? (typeof window !== 'undefined' ? window.location.hash : '');\n if (!raw) return null;\n const params = new URLSearchParams(raw.startsWith('#') ? raw.slice(1) : raw);\n return params.get('bind_token');\n }\n\n /**\n * Complete the DPoP bind step. Generates a keypair (if needed) and\n * POSTs the bind ticket to `/auth/dpop/bind`. On success the backend\n * sets a `session_id` cookie and returns the `return_to` URL the\n * integrator originally requested.\n *\n * **Multi-tab safety.** The whole bind transaction (keypair lookup +\n * POST + ticket consume) is serialised through the Web Locks API\n * under the name `signalling-sdk-dpop-bind-flow`. Without this, two\n * tabs that both navigate to the bind landing page race the bind\n * ticket — only one wins, the other gets `bind_ticket_consumed`. The\n * lock uses a distinct name from the keypair-generation lock inside\n * `DPoPManager` because Web Locks are NOT reentrant within a single\n * client (reusing the same name would deadlock).\n *\n * **Nonce churn.** The backend may respond with one of three nonce\n * challenge codes (`use_dpop_nonce`, `invalid_nonce`, `expired_nonce`)\n * before accepting the request. `ApiClient.request` retries\n * automatically on all three.\n *\n * **Failure recovery.** On a non-recoverable bind failure the SDK\n * resets the DPoP key state (clears the IndexedDB keypair and nonce\n * cache) so the next attempt generates fresh material — matching the\n * behaviour the desktop's hand-rolled `/auth/complete` page used to\n * implement. The original error is re-thrown either way.\n */\n async completeBind(bindToken: string): Promise<{ returnTo: string }> {\n if (!bindToken) {\n throw new Error('[signalling-sdk] completeBind: bindToken is required.');\n }\n return runUnderBindLock(async () => {\n await this.dpop.init();\n try {\n const result = await this.api.dpopBind(bindToken);\n this.logger.info('DPoP bind completed; session cookie set');\n return result;\n } catch (err) {\n this.logger.warn('DPoP bind failed — resetting keypair so retry regenerates', err);\n try {\n await this.dpop.reset();\n } catch (resetErr) {\n // Resetting is best-effort; don't shadow the real error.\n this.logger.warn('dpop.reset during bind-failure recovery failed', resetErr);\n }\n throw err;\n }\n });\n }\n\n /**\n * Resolve the currently signed-in user. Returns `null` when the\n * session is missing or expired. Caches the result so multiple\n * components can call it without thrashing the backend.\n */\n async getCurrentUser(force = false): Promise<UserProfile | null> {\n if (!force && this.cachedUser) return this.cachedUser;\n try {\n const user = await this.api.getCurrentUser();\n this.cachedUser = user;\n this.emit('state.authenticated', user);\n return user;\n } catch (err) {\n if (err instanceof SignallingApiError && (err.status === 401 || err.status === 404)) {\n this.cachedUser = null;\n this.emit('state.unauthenticated', err);\n return null;\n }\n throw err;\n }\n }\n\n /** Convenience boolean wrapper around `getCurrentUser`. */\n async isAuthenticated(): Promise<boolean> {\n return (await this.getCurrentUser()) !== null;\n }\n\n /**\n * `PATCH /me` — partial profile update. The cached `currentUser` is\n * refreshed in place and `state.authenticated` is re-emitted so any\n * React provider listening to that event re-renders the new profile\n * without an extra round-trip. The semantic is \"the canonical user\n * just changed; here it is\" — exactly what `state.authenticated`\n * means, so we reuse it instead of inventing a new event.\n */\n async updateProfile(updates: UpdateProfileRequest): Promise<UserProfile> {\n const updated = await this.api.updateProfile(updates);\n this.cachedUser = updated;\n this.emit('state.authenticated', updated);\n return updated;\n }\n\n /** `POST /auth/logout` — single-device sign-out. */\n async logout(): Promise<void> {\n try {\n await this.api.logout();\n } finally {\n await this.afterSignOut();\n }\n }\n\n /** `POST /auth/logout-all` — sign out everywhere. */\n async logoutAll(): Promise<void> {\n try {\n await this.api.logoutAll();\n } finally {\n await this.afterSignOut();\n }\n }\n\n /**\n * Navigate to Keycloak's account console (change password,\n * manage MFA, view active sessions). BFF-only.\n */\n openAccountConsole(): void {\n if (typeof window === 'undefined') {\n throw new Error('[signalling-sdk] openAccountConsole requires a browser environment.');\n }\n window.location.href = this.api.url('/auth/account');\n }\n\n private async afterSignOut(): Promise<void> {\n this.cachedUser = null;\n await this.dpop.reset();\n this.emit('state.signed-out', undefined);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Multi-tab bind serialisation\n// ---------------------------------------------------------------------------\n\n/**\n * Lock name used by `completeBind`. Distinct from the\n * `signalling-sdk-dpop-keypair` lock that `DPoPManager` uses internally\n * for keypair generation — Web Locks are not reentrant, so reusing the\n * inner name would deadlock the moment `dpop.init()` ran inside the\n * outer lock's callback.\n */\nconst BIND_FLOW_LOCK = 'signalling-sdk-dpop-bind-flow';\n\ninterface NavigatorLocksLike {\n locks?: {\n request: <T>(\n name: string,\n opts: { mode: 'exclusive' },\n cb: () => Promise<T>\n ) => Promise<T>;\n };\n}\n\n/**\n * In-process fallback mutex for environments without `navigator.locks`\n * (older browsers, jsdom, SSR). Single-tab guarantees only — but that\n * matches what a non-`locks` browser could ever provide anyway.\n */\nlet fallbackChain: Promise<unknown> = Promise.resolve();\n\nasync function runUnderBindLock<T>(fn: () => Promise<T>): Promise<T> {\n const nav: NavigatorLocksLike | undefined =\n typeof navigator !== 'undefined' ? (navigator as unknown as NavigatorLocksLike) : undefined;\n if (nav?.locks?.request) {\n return nav.locks.request(BIND_FLOW_LOCK, { mode: 'exclusive' }, fn);\n }\n const next = fallbackChain.then(() => fn());\n // Swallow rejections so the next caller in the chain isn't poisoned.\n fallbackChain = next.catch(() => undefined);\n return next;\n}\n","import { ApiClient } from '../api/client';\nimport { DPoPManager } from '../dpop';\nimport {\n Logger,\n SignallingWebSocketError,\n WSMessage,\n WSTicket,\n} from '../types';\nimport { TypedEventEmitter } from '../internal/events';\nimport { b64urlDecodeString } from '../internal/base64url';\n\n/**\n * Close codes the backend uses **after** a successful WS upgrade.\n * Close codes align with the server’s WebSocket DPoP / negotiate layers\n * (4400–4409, 4500, etc.).\n */\nexport const WSCloseCode = {\n /** First frame wasn't valid JSON / wrong type. */\n BAD_FIRST_FRAME: 4400,\n /** Ticket bad/expired or proof failed verification. */\n AUTH_FAILED: 4401,\n /** Post-upgrade authorization failed (e.g. client not in room). */\n FORBIDDEN: 4403,\n /** Post-upgrade resource lookup failed (room/client missing). */\n NOT_FOUND: 4404,\n /** No first frame within 5 seconds. */\n HANDSHAKE_TIMEOUT: 4408,\n /** First-frame `jti` already seen — replay detected. */\n REPLAY: 4409,\n /** Post-upgrade internal error. */\n INTERNAL: 4500,\n} as const;\n\n/**\n * Close codes that should NEVER trigger an automatic reconnect — they\n * indicate a permanent / programmer error and need user-visible UI.\n *\n * Exported via `isFatalCloseCode` so higher-level surfaces (e.g. the\n * `Call.connection.closed` event) can classify the close without\n * duplicating this list.\n */\nconst NO_RETRY_CODES = new Set<number>([\n 1008, // policy violation\n WSCloseCode.BAD_FIRST_FRAME,\n WSCloseCode.AUTH_FAILED,\n WSCloseCode.FORBIDDEN,\n WSCloseCode.NOT_FOUND,\n WSCloseCode.HANDSHAKE_TIMEOUT,\n WSCloseCode.REPLAY,\n]);\n\n/**\n * Returns `true` when a WebSocket close code is one the SDK refuses to\n * auto-retry. UIs should render an end-of-session state on a fatal\n * close instead of a \"reconnecting\" spinner. Mirror of the\n * `NO_RETRY_CODES` set used internally for backoff decisions.\n */\nexport function isFatalCloseCode(code: number): boolean {\n return NO_RETRY_CODES.has(code);\n}\n\n/**\n * Internal events emitted by `WebSocketClient`. Higher-level modules\n * (`RoomManager`, `WebRTCManager`) subscribe to these.\n */\nexport interface WebSocketEventMap {\n /** WS handshake (upgrade + DPoP first-frame + ack) completed. */\n open: void;\n /** Underlying socket closed. `code`/`reason` are the WS close info. */\n close: { code: number; reason: string };\n /** Network or decoding error. */\n error: Error;\n /** Any application-level message (after the handshake). */\n message: WSMessage;\n /** Catch-all bucketed by message type. Listeners for, e.g., `sdp` get only SDP frames. */\n [eventType: `type:${string}`]: WSMessage;\n}\n\n/**\n * Two endpoint shapes — the room WS and the global notification WS.\n * The SDK exposes only the room shape today; `connectGlobal` is reserved\n * for a later milestone.\n */\nexport type WebSocketTarget =\n | { kind: 'room'; roomId: number | string; clientId: string }\n | { kind: 'global' };\n\nexport interface WebSocketClientOptions {\n /** Maximum reconnect attempts after a transient drop. Default: 5. */\n maxReconnects?: number;\n /** Base delay (ms) for exponential backoff. Default: 1000. */\n reconnectBaseDelayMs?: number;\n}\n\n/**\n * Manages the lifecycle of a single authenticated WebSocket. Wraps the\n * full handshake protocol described in `docs/auth.md` and your backend’s\n * WebSocket upgrade spec:\n *\n * 1. `POST /auth/ws-ticket` — get a 10-second JWT bound to the device\n * key plus a server-issued nonce.\n * 2. Open the socket using `?ticket=<jwt>` (the only place the ticket\n * can travel — browsers can't set headers on WS upgrades).\n * 3. Within 5 seconds send the DPoP first frame\n * `{ \"type\": \"dpop_handshake\", \"proof\": \"...\" }` whose proof's\n * `htu` exactly matches the WS URL with the query stripped.\n * 4. Wait for `{ \"type\": \"dpop_handshake_ack\" }` before any application\n * frames. Any other first response = treat as a fatal error.\n *\n * After the handshake, the wire is plain `WSMessage` envelopes\n * `{ type, from, to, payload }`.\n */\nexport class WebSocketClient extends TypedEventEmitter<WebSocketEventMap> {\n private socket: WebSocket | null = null;\n private target: WebSocketTarget | null = null;\n private acked = false;\n private explicitlyClosed = false;\n private reconnectAttempts = 0;\n private readonly options: Required<WebSocketClientOptions>;\n\n constructor(\n private readonly api: ApiClient,\n private readonly dpop: DPoPManager,\n private readonly logger: Logger,\n options: WebSocketClientOptions = {}\n ) {\n super();\n this.options = {\n maxReconnects: options.maxReconnects ?? 5,\n reconnectBaseDelayMs: options.reconnectBaseDelayMs ?? 1000,\n };\n }\n\n /** True once the DPoP first-frame handshake has been acked by the server. */\n get isReady(): boolean {\n return this.acked && this.socket?.readyState === WebSocket.OPEN;\n }\n\n /**\n * Open and authenticate a room WebSocket. The `clientId` must be the\n * one returned from `POST /joinroom/{roomId}` for the same session.\n */\n async connectRoom(roomId: number | string, clientId: string): Promise<void> {\n return this.connect({ kind: 'room', roomId, clientId });\n }\n\n /**\n * Open and authenticate the global notification WebSocket.\n * Reserved for the friends / presence work; no app-level frames are\n * defined on this socket today.\n */\n async connectGlobal(): Promise<void> {\n return this.connect({ kind: 'global' });\n }\n\n private async connect(target: WebSocketTarget): Promise<void> {\n this.target = target;\n this.explicitlyClosed = false;\n\n // 1. Acquire WS ticket (DPoP-authed).\n const ticket = await this.api.issueWSTicket();\n // The ticket payload also carries a `nonce` claim that the WS first\n // frame's DPoP proof MUST echo — see backend §6.2. We pull it from\n // the body too because it's the canonical source.\n const ticketNonce = ticket.nonce || nonceFromTicketJWT(ticket.ticket);\n if (ticketNonce) this.dpop.setNonce(ticketNonce);\n\n // 2. Open the socket.\n const url = this.urlFor(target, ticket);\n const canonicalUrl = stripQueryFragment(url);\n this.logger.debug('opening WebSocket', { url, canonicalUrl, target });\n\n return new Promise<void>((resolve, reject) => {\n const ws = new WebSocket(url);\n this.socket = ws;\n this.acked = false;\n\n // Resolution / rejection runs exactly once.\n let settled = false;\n const settle = (err?: Error) => {\n if (settled) return;\n settled = true;\n if (err) reject(err);\n else resolve();\n };\n\n ws.onopen = async () => {\n try {\n // 3. Send the DPoP first frame. `htu` MUST match the canonical\n // URL the server reconstructs (no query, no fragment).\n const proof = await this.dpop.generateProof('GET', canonicalUrl);\n ws.send(JSON.stringify({ type: 'dpop_handshake', proof }));\n } catch (e) {\n this.logger.error('DPoP first-frame send failed', e);\n settle(e instanceof Error ? e : new Error(String(e)));\n ws.close();\n }\n };\n\n ws.onmessage = (event: MessageEvent) => {\n let parsed: unknown;\n try {\n parsed = JSON.parse(typeof event.data === 'string' ? event.data : '');\n } catch {\n this.logger.warn('non-JSON frame ignored', event.data);\n return;\n }\n\n // 4. Wait for ack before letting application frames through.\n if (!this.acked) {\n if (\n parsed &&\n typeof parsed === 'object' &&\n (parsed as { type?: unknown }).type === 'dpop_handshake_ack'\n ) {\n this.acked = true;\n this.reconnectAttempts = 0;\n this.emit('open', undefined);\n settle();\n return;\n }\n // Anything else before the ack is a fatal protocol error.\n const err = new SignallingWebSocketError(\n 'Unexpected first frame before dpop_handshake_ack: ' + JSON.stringify(parsed)\n );\n settle(err);\n this.emit('error', err);\n ws.close(1008, 'protocol-violation');\n return;\n }\n\n // Steady state — emit both the generic event and a per-type one.\n if (\n parsed &&\n typeof parsed === 'object' &&\n typeof (parsed as { type?: unknown }).type === 'string'\n ) {\n const msg = parsed as WSMessage;\n this.emit('message', msg);\n this.emit(`type:${msg.type}`, msg);\n }\n };\n\n ws.onerror = (event: Event) => {\n this.logger.warn('WebSocket error event', event);\n const err = new SignallingWebSocketError('WebSocket error');\n this.emit('error', err);\n // `onerror` is followed by `onclose` — that path drives reconnect.\n settle(err);\n };\n\n ws.onclose = (event: CloseEvent) => {\n this.acked = false;\n this.socket = null;\n this.emit('close', { code: event.code, reason: event.reason });\n this.logger.info('WebSocket closed', { code: event.code, reason: event.reason });\n\n // Settle the connection promise as a failure if we never acked.\n if (!settled) {\n settle(\n new SignallingWebSocketError(\n `WebSocket closed before handshake (code=${event.code} reason=${event.reason})`,\n event.code\n )\n );\n }\n\n if (\n !this.explicitlyClosed &&\n !NO_RETRY_CODES.has(event.code) &&\n this.reconnectAttempts < this.options.maxReconnects\n ) {\n const delay = this.options.reconnectBaseDelayMs * 2 ** this.reconnectAttempts;\n this.reconnectAttempts++;\n this.logger.info(\n `WebSocket reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`\n );\n setTimeout(() => {\n if (!this.explicitlyClosed && this.target) {\n this.connect(this.target).catch((e) => this.logger.warn('reconnect failed', e));\n }\n }, delay);\n }\n };\n });\n }\n\n /** Send an application frame. The handshake must have completed. */\n send(message: WSMessage): void {\n if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {\n throw new SignallingWebSocketError('WebSocket is not connected');\n }\n if (!this.acked) {\n throw new SignallingWebSocketError('WebSocket handshake not completed yet');\n }\n this.socket.send(JSON.stringify(message));\n }\n\n /** Close the socket and disable automatic reconnects. */\n disconnect(code: number = 1000, reason: string = 'client-disconnect'): void {\n this.explicitlyClosed = true;\n this.target = null;\n this.reconnectAttempts = this.options.maxReconnects;\n if (this.socket) {\n try {\n this.socket.close(code, reason);\n } catch {\n // ignore — closing a half-open socket is fine.\n }\n this.socket = null;\n }\n this.acked = false;\n }\n\n // -------------------------------------------------------------------------\n // Internals\n // -------------------------------------------------------------------------\n\n private urlFor(target: WebSocketTarget, ticket: WSTicket): string {\n const base = this.api.origin.replace(/^http/i, 'ws');\n const path =\n target.kind === 'global'\n ? '/global/ws'\n : `/ws/${encodeURIComponent(String(target.roomId))}/${encodeURIComponent(target.clientId)}`;\n const url = new URL(base + path);\n url.searchParams.set('ticket', ticket.ticket);\n return url.toString();\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Strip query and fragment from a URL — what the backend uses to canonicalise\n * `htu` for WebSocket upgrades. Implemented manually so it works in\n * environments without a polyfilled URL constructor.\n */\nfunction stripQueryFragment(url: string): string {\n const u = new URL(url);\n u.search = '';\n u.hash = '';\n return u.toString();\n}\n\n/**\n * Extract the `nonce` claim from the WS ticket JWT body. Used as a\n * fallback when the response body did not include the nonce field\n * (older backends). Returns `null` if the JWT can't be parsed.\n */\nfunction nonceFromTicketJWT(jwt: string): string | null {\n const parts = jwt.split('.');\n if (parts.length < 2) return null;\n try {\n const payload = JSON.parse(b64urlDecodeString(parts[1])) as { nonce?: unknown };\n return typeof payload.nonce === 'string' ? payload.nonce : null;\n } catch {\n return null;\n }\n}\n","import { ApiClient } from '../api/client';\nimport { WebSocketClient } from '../websocket/client';\nimport {\n ClientPermissions,\n CreateRoomResponse,\n JoinRoomResponse,\n Logger,\n Room,\n} from '../types';\n\n/**\n * Snapshot of the local membership in a room. Held by `RoomManager` for\n * the lifetime of a single `joinRoom` call so other modules\n * (`WebRTCManager`, the `Call` event surface, screen-share helpers)\n * have one source of truth for `roomId` + `clientId`.\n */\nexport interface ActiveMembership {\n roomId: number | string;\n clientId: string;\n}\n\n/**\n * `RoomManager` is the ergonomic wrapper around the room REST endpoints.\n * It also keeps\n * track of the **local** `clientId` so callers don't have to thread it\n * through every method that needs it (screen share, permissions, leave).\n *\n * It does **not** manage WebRTC peer connections — that lives in\n * `WebRTCManager`. It does **not** open the WebSocket — that's\n * `WebSocketClient`. Composition happens in the top-level `SignallingSDK`.\n */\nexport class RoomManager {\n private active: ActiveMembership | null = null;\n\n constructor(\n private readonly api: ApiClient,\n private readonly ws: WebSocketClient,\n private readonly logger: Logger\n ) {}\n\n /** Currently joined room snapshot, or `null` if not in a room. */\n get current(): ActiveMembership | null {\n return this.active;\n }\n\n /** `POST /createroom` — creates a room and returns `{ roomId, clientId }`. */\n async create(): Promise<CreateRoomResponse> {\n const result = await this.api.createRoom();\n this.logger.info('Room created', result);\n this.active = { roomId: result.roomId, clientId: result.clientId };\n return result;\n }\n\n /**\n * `POST /joinroom/{roomId}` — joins an existing room and stores the\n * resulting `clientId` for subsequent calls. Does NOT open a WebSocket;\n * use `connectSocket` for that.\n */\n async join(roomId: number | string): Promise<JoinRoomResponse> {\n const result = await this.api.joinRoom(roomId);\n this.logger.info('Joined room', result);\n this.active = { roomId: result.roomId, clientId: result.clientId };\n return result;\n }\n\n /**\n * Open the room WebSocket for the active membership. Performs the\n * full ticket → upgrade → DPoP first-frame handshake.\n *\n * Throws if the caller hasn't `create()`d / `join()`ed a room first.\n */\n async connectSocket(): Promise<void> {\n if (!this.active) {\n throw new Error('[signalling-sdk] connectSocket: not currently in a room');\n }\n await this.ws.connectRoom(this.active.roomId, this.active.clientId);\n }\n\n /** `GET /viewroom/{roomId}` — returns the full room snapshot. */\n async view(roomId?: number | string): Promise<Room> {\n const id = roomId ?? this.active?.roomId;\n if (id === undefined || id === null) {\n throw new Error('[signalling-sdk] view: roomId required (no active room)');\n }\n return this.api.viewRoom(id);\n }\n\n /**\n * `DELETE /leaveroom/{roomId}/{clientId}` + closes the WebSocket.\n * Always tries to clean up local state even if the network call fails.\n */\n async leave(): Promise<void> {\n const active = this.active;\n if (!active) {\n this.logger.warn('leave called without an active room — no-op');\n return;\n }\n try {\n this.ws.disconnect(1000, 'client-leave');\n } catch (err) {\n this.logger.warn('disconnect WS during leave failed', err);\n }\n try {\n await this.api.leaveRoom(active.roomId, active.clientId);\n } finally {\n this.active = null;\n }\n }\n\n // -------------------------------------------------------------------------\n // Screen share\n // -------------------------------------------------------------------------\n\n async startScreenShare(): Promise<{ status: string; previousSharer?: string }> {\n if (!this.active) {\n throw new Error('[signalling-sdk] startScreenShare: not currently in a room');\n }\n return this.api.startScreenShare(this.active.roomId, this.active.clientId);\n }\n\n async stopScreenShare(): Promise<{ status: string }> {\n if (!this.active) {\n throw new Error('[signalling-sdk] stopScreenShare: not currently in a room');\n }\n return this.api.stopScreenShare(this.active.roomId, this.active.clientId);\n }\n\n // -------------------------------------------------------------------------\n // Permissions\n // -------------------------------------------------------------------------\n\n /** Permissions for the local client. */\n async myPermissions(): Promise<ClientPermissions> {\n if (!this.active) {\n throw new Error('[signalling-sdk] myPermissions: not currently in a room');\n }\n return this.api.getPermissions(this.active.roomId, this.active.clientId);\n }\n\n /**\n * Update permissions for any client in the room. Fails with `403` if\n * the local user is not the host.\n */\n async updatePermissions(\n targetClientId: string,\n updates: Partial<Pick<ClientPermissions, 'canAudio' | 'canVideo' | 'canScreenShare'>>\n ): Promise<ClientPermissions> {\n if (!this.active) {\n throw new Error('[signalling-sdk] updatePermissions: not currently in a room');\n }\n return this.api.updatePermissions(this.active.roomId, targetClientId, updates);\n }\n}\n","import { WebSocketClient } from '../websocket/client';\nimport {\n IceCandidatePayload,\n Logger,\n PeerMediaState,\n PeerSignalPayload,\n SdpPayload,\n TurnCredential,\n WSMessage,\n} from '../types';\nimport { TypedEventEmitter } from '../internal/events';\n\n/**\n * Events emitted by `WebRTCManager`. Surfaced through the `Call` object\n * returned by `sdk.calls.joinRoom`.\n */\nexport interface WebRTCEventMap {\n /** A remote camera `MediaStream` arrived from `peerId`. */\n 'stream.added': { clientId: string; stream: MediaStream };\n /** The peer connection's camera stream is gone. */\n 'stream.removed': { clientId: string };\n /** Remote peer started a screen-share stream (separate surface from camera). */\n 'screen-share.added': { clientId: string; stream: MediaStream };\n /** Remote peer's screen-share stream ended. */\n 'screen-share.removed': { clientId: string };\n /** Peer broadcast a `mediaState` signal — their mic/cam enable toggled. */\n 'peer.media-state-changed': { clientId: string; state: PeerMediaState };\n /** Peer broadcast `mediaStopped` — they ended all local media. */\n 'peer.media-stopped': { clientId: string };\n /** Underlying RTCPeerConnection state change. */\n 'peer.connection-state': { clientId: string; state: RTCPeerConnectionState };\n /** A negotiation attempt failed irrecoverably. */\n 'peer.failed': { clientId: string; error: Error };\n}\n\n/**\n * Identity of the local client — supplied by the orchestrator\n * (`SignallingSDK`) so the WebRTC manager can correctly address\n * outbound `WSMessage` envelopes.\n */\nexport interface LocalIdentity {\n clientId: string;\n}\n\n/**\n * `WebRTCManager` orchestrates `RTCPeerConnection` instances on top of\n * the room WebSocket. The wire format mirrors what the room WebSocket\n * handler expects:\n *\n * - `{type: 'sdp', from, to, payload: { type, sdp } }`\n * - `{type: 'ice', from, to, payload: { candidate, ... } }`\n * - `{type: 'signal', from: 'server' | clientId, to, payload: { action, ... } }`\n *\n * SDP messages are routed peer-to-peer by the backend; the only thing\n * the server inspects is the `to` field. Each remote peer gets its own\n * `RTCPeerConnection` keyed by `clientId`.\n *\n * **Camera vs screen-share segregation.** Both streams ride the same\n * `RTCPeerConnection`. Each peer broadcasts a `screenShareStream` signal\n * carrying the `MediaStream.id` BEFORE adding its screen tracks; the\n * receiver records that id and routes the matching incoming\n * `MediaStream` to the `screen-share.added` event rather than\n * `stream.added`. A matching `screenShareStopped` signal clears the\n * mapping when the sharer ends.\n *\n * **Peer-state side channel.** Peers also broadcast `mediaState` /\n * `mediaStopped` signals (purely informational — the backend does not\n * validate or replicate them). The manager records the most recent\n * state per peer so UIs can render mute / camera-off indicators\n * without relying on the inbound track being absent (which it usually\n * isn't — `setMicEnabled(false)` only disables the track locally).\n */\nexport class WebRTCManager extends TypedEventEmitter<WebRTCEventMap> {\n private readonly peers = new Map<string, RTCPeerConnection>();\n private readonly peerMediaStates = new Map<string, PeerMediaState>();\n /** Maps each peer's announced screen `MediaStream.id` to that peer's clientId. */\n private readonly screenStreamIds = new Map<string, string>();\n /** Senders we added when we started screen-sharing — used so we can drop them on stop. */\n private readonly screenSenders = new Map<string, RTCRtpSender[]>();\n\n private localStream: MediaStream | null = null;\n private localScreenStream: MediaStream | null = null;\n private iceServers: RTCIceServer[] = defaultIceServers();\n private local: LocalIdentity | null = null;\n private wsUnsubs: Array<() => void> = [];\n\n constructor(\n private readonly ws: WebSocketClient,\n private readonly logger: Logger\n ) {\n super();\n }\n\n /**\n * Wire up the manager to a freshly authenticated WebSocket. Must be\n * called after the WS handshake completes and the local `clientId`\n * is known. Safe to call again with a different identity — the old\n * subscriptions are removed first.\n */\n bind(local: LocalIdentity): void {\n this.unbind();\n this.local = local;\n this.wsUnsubs.push(this.ws.on('type:sdp', (msg) => this.handleSdp(msg)));\n this.wsUnsubs.push(this.ws.on('type:ice', (msg) => this.handleIce(msg)));\n this.wsUnsubs.push(this.ws.on('type:signal', (msg) => this.handleSignal(msg)));\n }\n\n /** Drop all WS subscriptions and tear down every peer connection. */\n unbind(): void {\n for (const off of this.wsUnsubs) off();\n this.wsUnsubs = [];\n for (const [clientId, pc] of this.peers) {\n try {\n pc.close();\n } catch {\n // ignore — we're shutting down anyway\n }\n this.emit('stream.removed', { clientId });\n }\n this.peers.clear();\n this.peerMediaStates.clear();\n this.screenStreamIds.clear();\n this.screenSenders.clear();\n this.localScreenStream = null;\n this.local = null;\n }\n\n // -------------------------------------------------------------------------\n // Public read-only accessors (AD2 — desktop hooks for blur/bitrate/stats)\n // -------------------------------------------------------------------------\n\n /**\n * Read-only access to the live peer-connection map. Exposed for\n * integrators that need to read RTCStatsReport (`pc.getStats()`),\n * inspect senders for adaptive bitrate, or attach analyser nodes for\n * active-speaker detection — operations that are out of scope for the\n * SDK but still need first-class peer access. The returned `Map` is\n * a live view; do NOT mutate it.\n */\n getPeerConnections(): ReadonlyMap<string, RTCPeerConnection> {\n return this.peers;\n }\n\n /** Read-only access to the current local camera `MediaStream`. */\n getLocalStream(): MediaStream | null {\n return this.localStream;\n }\n\n /** Read-only access to the current local screen-share `MediaStream`, if any. */\n getLocalScreenStream(): MediaStream | null {\n return this.localScreenStream;\n }\n\n /**\n * Read-only access to the latest `mediaState` we've seen per peer. An\n * absent entry means the peer has not announced yet — treat as\n * `{ video: true, audio: true }` (the implicit default).\n */\n getPeerMediaStates(): ReadonlyMap<string, PeerMediaState> {\n return this.peerMediaStates;\n }\n\n // -------------------------------------------------------------------------\n // Mutators\n // -------------------------------------------------------------------------\n\n /** Inject TURN credentials. The next created peer connection will pick them up. */\n setTurnCredentials(credentials: TurnCredential | TurnCredential[] | null): void {\n const list = credentials\n ? Array.isArray(credentials)\n ? credentials\n : [credentials]\n : [];\n this.iceServers = [\n ...defaultIceServers(),\n ...list.map((c) => ({\n urls: c.urls,\n username: c.username,\n credential: c.credential,\n })),\n ];\n }\n\n /**\n * Replace the local outbound camera `MediaStream`. Tracks are added /\n * replaced on every existing peer connection. Screen-share tracks are\n * NOT touched — they're tracked separately via `addScreenStream`.\n */\n async setLocalStream(stream: MediaStream | null): Promise<void> {\n this.localStream = stream;\n for (const pc of this.peers.values()) {\n const senders = pc.getSenders();\n const newAudio = stream?.getAudioTracks()[0] ?? null;\n const newVideo = stream?.getVideoTracks()[0] ?? null;\n // Identify camera senders by the streams they currently transmit —\n // skip senders associated with the screen-share stream so we don't\n // accidentally replace screen tracks when the camera changes.\n const isScreenSender = (s: RTCRtpSender): boolean => {\n const senderList = this.screenSenders.values();\n for (const senders of senderList) {\n if (senders.includes(s)) return true;\n }\n return false;\n };\n const cameraSenders = senders.filter((s) => !isScreenSender(s));\n const audioSender = cameraSenders.find((s) => s.track?.kind === 'audio');\n const videoSender = cameraSenders.find((s) => s.track?.kind === 'video');\n if (audioSender) await audioSender.replaceTrack(newAudio);\n else if (newAudio && stream) pc.addTrack(newAudio, stream);\n if (videoSender) await videoSender.replaceTrack(newVideo);\n else if (newVideo && stream) pc.addTrack(newVideo, stream);\n }\n }\n\n /**\n * Begin sharing a screen `MediaStream` to all current peers\n * **alongside** the existing camera tracks (the Zoom / Meet pattern).\n *\n * Wire protocol (matches the backend's relayed signal contract):\n * 1. For each peer, send `{action: 'screenShareStream', streamId}`\n * on the room WS so the receiver knows the next `ontrack` whose\n * stream id matches is screen, not camera.\n * 2. Add the screen tracks to that peer's `RTCPeerConnection`.\n * 3. Renegotiate (createOffer / setLocalDescription / send SDP).\n *\n * Idempotent — calling twice without an intervening\n * `removeScreenStream` is a no-op.\n */\n async addScreenStream(stream: MediaStream): Promise<void> {\n if (this.localScreenStream && this.localScreenStream.id === stream.id) {\n this.logger.debug('addScreenStream: same stream already shared; no-op');\n return;\n }\n if (this.localScreenStream) {\n await this.removeScreenStream();\n }\n this.localScreenStream = stream;\n const streamId = stream.id;\n const tracks = stream.getTracks();\n if (tracks.length === 0) {\n this.logger.warn('addScreenStream: stream has no tracks');\n return;\n }\n\n for (const [peerId, pc] of this.peers) {\n // Announce streamId BEFORE addTrack so the receiver's ontrack can\n // route correctly — the screen-share signal needs to arrive before\n // (or, on a slow connection, around the same time as) the SDP.\n this.sendSignaling(peerId, 'signal', {\n action: 'screenShareStream',\n streamId,\n } satisfies PeerSignalPayload);\n\n const senders: RTCRtpSender[] = [];\n for (const track of tracks) {\n senders.push(pc.addTrack(track, stream));\n }\n this.screenSenders.set(peerId, senders);\n\n // Renegotiate. Skip if already mid-negotiation to avoid races.\n if (pc.signalingState === 'stable') {\n try {\n const offer = await pc.createOffer();\n await pc.setLocalDescription(offer);\n if (offer.sdp) {\n this.sendSignaling(peerId, 'sdp', {\n type: 'offer',\n sdp: offer.sdp,\n });\n }\n } catch (err) {\n this.logger.error(`addScreenStream renegotiation with ${peerId} failed`, err);\n }\n } else {\n this.logger.debug(`addScreenStream: skipping renegotiate with ${peerId} (state=${pc.signalingState})`);\n }\n }\n }\n\n /**\n * Stop sharing the local screen `MediaStream`. Removes the screen\n * senders from every peer connection and broadcasts\n * `screenShareStopped` so receivers can tear down their screen\n * surface immediately. Idempotent — no-op when nothing is shared.\n */\n async removeScreenStream(): Promise<void> {\n if (!this.localScreenStream) return;\n const streamId = this.localScreenStream.id;\n\n for (const [peerId, pc] of this.peers) {\n this.sendSignaling(peerId, 'signal', {\n action: 'screenShareStopped',\n streamId,\n } satisfies PeerSignalPayload);\n\n const senders = this.screenSenders.get(peerId);\n if (senders) {\n for (const sender of senders) {\n try {\n pc.removeTrack(sender);\n } catch (err) {\n this.logger.debug(`removeTrack on ${peerId} failed`, err);\n }\n }\n this.screenSenders.delete(peerId);\n }\n }\n this.localScreenStream = null;\n }\n\n /**\n * Broadcast our mic/cam enable state to every peer in the room.\n * Auto-called by `Call.setMicEnabled` / `Call.setCameraEnabled` so\n * peer UIs can render mute / camera-off indicators without inspecting\n * the inbound track (which usually keeps flowing in disabled state).\n */\n broadcastMediaState(state: PeerMediaState): void {\n this.broadcastSignal({\n action: 'mediaState',\n video: state.video,\n audio: state.audio,\n });\n }\n\n /**\n * Announce that we've ended all local media. Receivers tear down both\n * our camera and screen-share surfaces. Called by `Call.leave` and by\n * `Call.setLocalStream(null)`.\n */\n broadcastMediaStopped(): void {\n this.broadcastSignal({ action: 'mediaStopped' });\n }\n\n /**\n * Initiate an offer toward the given remote client. Used by the\n * caller side of the mesh-mode handshake (typically when a `peer.joined`\n * signal arrives and the local end is the more-recently-connected peer).\n */\n async callPeer(remoteClientId: string): Promise<void> {\n const pc = this.getOrCreatePeer(remoteClientId);\n const offer = await pc.createOffer();\n await pc.setLocalDescription(offer);\n if (!offer.sdp) throw new Error('[signalling-sdk] createOffer returned no SDP');\n this.sendSignaling(remoteClientId, 'sdp', { type: 'offer', sdp: offer.sdp });\n }\n\n /** Tear down the connection to a single peer. */\n hangupPeer(remoteClientId: string): void {\n const pc = this.peers.get(remoteClientId);\n if (pc) {\n try {\n pc.close();\n } catch {\n // ignore\n }\n this.peers.delete(remoteClientId);\n }\n // Always clean up the per-peer auxiliary maps even if there was no pc.\n this.screenSenders.delete(remoteClientId);\n this.peerMediaStates.delete(remoteClientId);\n // Clear any screenStreamIds entries pointing at this peer.\n for (const [streamId, clientId] of this.screenStreamIds) {\n if (clientId === remoteClientId) this.screenStreamIds.delete(streamId);\n }\n if (pc) {\n this.emit('stream.removed', { clientId: remoteClientId });\n this.emit('screen-share.removed', { clientId: remoteClientId });\n }\n }\n\n // -------------------------------------------------------------------------\n // Wire handlers\n // -------------------------------------------------------------------------\n\n private async handleSdp(msg: WSMessage): Promise<void> {\n const peerId = msg.from;\n const sdp = msg.payload as SdpPayload;\n if (!sdp || (sdp.type !== 'offer' && sdp.type !== 'answer') || !sdp.sdp) {\n this.logger.warn('ignoring malformed SDP frame', msg);\n return;\n }\n\n const pc = this.getOrCreatePeer(peerId);\n try {\n await pc.setRemoteDescription({ type: sdp.type, sdp: sdp.sdp });\n if (sdp.type === 'offer') {\n const answer = await pc.createAnswer();\n await pc.setLocalDescription(answer);\n if (answer.sdp) {\n this.sendSignaling(peerId, 'sdp', { type: 'answer', sdp: answer.sdp });\n }\n }\n } catch (err) {\n this.logger.error(`SDP negotiation with ${peerId} failed`, err);\n this.emit('peer.failed', {\n clientId: peerId,\n error: err instanceof Error ? err : new Error(String(err)),\n });\n }\n }\n\n private async handleIce(msg: WSMessage): Promise<void> {\n const peerId = msg.from;\n const cand = msg.payload as IceCandidatePayload;\n const pc = this.peers.get(peerId);\n if (!pc || !cand) return;\n try {\n await pc.addIceCandidate(cand as RTCIceCandidateInit);\n } catch (err) {\n // Late candidates after `setRemoteDescription` is unwound are\n // benign — log at debug to avoid noise.\n this.logger.debug('addIceCandidate failed', { peerId, err });\n }\n }\n\n /**\n * Handle every inbound `type: 'signal'` frame. The server emits some\n * signals (`from: 'server'`) and peers emit others (`from: <clientId>`).\n * We discriminate by `msg.from` because the backend's relayed\n * envelope reuses the same `type` field.\n */\n private handleSignal(msg: WSMessage): void {\n if (msg.from === 'server') {\n this.handleServerSignal(msg);\n } else {\n this.handlePeerSignal(msg);\n }\n }\n\n private handleServerSignal(msg: WSMessage): void {\n const payload = msg.payload as { action?: string; leftClientId?: string };\n if (payload?.action === 'leave' && payload.leftClientId) {\n this.hangupPeer(payload.leftClientId);\n }\n }\n\n /**\n * Peer-originated signals (relayed by the server, but content is\n * untrusted). Currently covers media on/off state, mediaStopped, and\n * screen-share streamId routing. Chat signals are intentionally NOT\n * handled here — the `Call` event surface forwards them via the\n * generic `raw` event so the WebRTC layer stays focused on RTC.\n */\n private handlePeerSignal(msg: WSMessage): void {\n const fromId = msg.from;\n if (!fromId || !msg.payload || typeof msg.payload !== 'object') return;\n const p = msg.payload as { action?: string } & Record<string, unknown>;\n switch (p.action) {\n case 'mediaState': {\n const video = typeof p.video === 'boolean' ? p.video : true;\n const audio = typeof p.audio === 'boolean' ? p.audio : true;\n const state: PeerMediaState = { video, audio };\n this.peerMediaStates.set(fromId, state);\n this.emit('peer.media-state-changed', { clientId: fromId, state });\n break;\n }\n case 'mediaStopped': {\n this.peerMediaStates.delete(fromId);\n // Clear screen-stream-id mapping pointing at this peer too — they're\n // done broadcasting anything.\n for (const [streamId, clientId] of this.screenStreamIds) {\n if (clientId === fromId) this.screenStreamIds.delete(streamId);\n }\n this.emit('peer.media-stopped', { clientId: fromId });\n this.emit('stream.removed', { clientId: fromId });\n this.emit('screen-share.removed', { clientId: fromId });\n break;\n }\n case 'screenShareStream': {\n if (typeof p.streamId === 'string') {\n this.screenStreamIds.set(p.streamId, fromId);\n }\n break;\n }\n case 'screenShareStopped': {\n if (typeof p.streamId === 'string') {\n this.screenStreamIds.delete(p.streamId);\n }\n this.emit('screen-share.removed', { clientId: fromId });\n break;\n }\n default:\n // Unknown peer-signal action — let it fall through to the generic\n // `raw` event Call exposes (handled at the SignallingSDK layer).\n break;\n }\n }\n\n // -------------------------------------------------------------------------\n // Peer factory\n // -------------------------------------------------------------------------\n\n private getOrCreatePeer(remoteClientId: string): RTCPeerConnection {\n const existing = this.peers.get(remoteClientId);\n if (existing) return existing;\n\n const pc = new RTCPeerConnection({ iceServers: this.iceServers });\n\n pc.onicecandidate = (event) => {\n if (event.candidate) {\n this.sendSignaling(remoteClientId, 'ice', event.candidate.toJSON());\n }\n };\n\n pc.ontrack = (event) => {\n const stream = event.streams[0] ?? new MediaStream([event.track]);\n // Demux camera vs screen-share based on the streamId map we've\n // built from peer `screenShareStream` announcements. The first\n // emit per stream wins; subsequent track events on the same\n // stream re-emit but consumers de-dupe by stream identity.\n const isScreen = this.screenStreamIds.get(stream.id) === remoteClientId;\n if (isScreen) {\n this.emit('screen-share.added', { clientId: remoteClientId, stream });\n } else {\n this.emit('stream.added', { clientId: remoteClientId, stream });\n }\n };\n\n pc.onconnectionstatechange = () => {\n this.emit('peer.connection-state', {\n clientId: remoteClientId,\n state: pc.connectionState,\n });\n if (pc.connectionState === 'failed' || pc.connectionState === 'closed') {\n this.hangupPeer(remoteClientId);\n }\n };\n\n if (this.localStream) {\n for (const track of this.localStream.getTracks()) {\n pc.addTrack(track, this.localStream);\n }\n }\n // Mid-call peer arrives while we are screen-sharing → add the screen\n // tracks to this new peer too, with the same protocol sequence\n // (signal → addTrack). Renegotiation is the new peer's responsibility\n // (it just called us; it'll createOffer once we're added).\n if (this.localScreenStream) {\n const streamId = this.localScreenStream.id;\n this.sendSignaling(remoteClientId, 'signal', {\n action: 'screenShareStream',\n streamId,\n } satisfies PeerSignalPayload);\n const senders: RTCRtpSender[] = [];\n for (const track of this.localScreenStream.getTracks()) {\n senders.push(pc.addTrack(track, this.localScreenStream));\n }\n this.screenSenders.set(remoteClientId, senders);\n }\n\n this.peers.set(remoteClientId, pc);\n return pc;\n }\n\n /**\n * Send a directed signaling envelope onto the WebSocket. Wraps\n * `WSMessage` so call sites can stay short and the wire format is\n * enforced in one place.\n */\n private sendSignaling(toClientId: string, type: string, payload: unknown): void {\n if (!this.local) {\n this.logger.warn('sendSignaling: no local identity bound; dropping frame');\n return;\n }\n this.ws.send({ type, from: this.local.clientId, to: toClientId, payload });\n }\n\n /**\n * Broadcast a peer-signal payload to every current peer. The relayed\n * envelope uses `type: 'signal'` with the local clientId as `from`.\n */\n private broadcastSignal(payload: PeerSignalPayload): void {\n for (const peerId of this.peers.keys()) {\n this.sendSignaling(peerId, 'signal', payload);\n }\n }\n}\n\n/** Public Google STUN servers — always present, free for anyone to use. */\nfunction defaultIceServers(): RTCIceServer[] {\n return [{ urls: 'stun:stun.l.google.com:19302' }];\n}\n","import { MediaConstraints } from '../types';\n\n/**\n * Categorised view of `MediaDeviceInfo[]` for easier consumption.\n */\nexport interface DeviceInventory {\n cameras: MediaDeviceInfo[];\n microphones: MediaDeviceInfo[];\n speakers: MediaDeviceInfo[];\n}\n\n/**\n * Thin, defensive wrapper around `navigator.mediaDevices`. Centralised\n * so the SDK doesn't sprinkle feature-detection checks at every call\n * site and so unit tests have one mocking surface to stub.\n *\n * The manager intentionally does NOT cache the local `MediaStream` —\n * that's `WebRTCManager`'s job (it has to push tracks into existing\n * peer connections when a new stream arrives).\n */\nexport class DeviceManager {\n /**\n * Acquire a stream from the user's mic / camera. Accepts either a\n * boolean shorthand (legacy SDK call sites) or full\n * `MediaTrackConstraints` for fine-grained device selection.\n */\n async getLocalStream(\n constraints: MediaConstraints = { audio: true, video: true }\n ): Promise<MediaStream> {\n this.assertSupported('getUserMedia');\n return navigator.mediaDevices.getUserMedia(constraints);\n }\n\n /** Convenience overload kept for backwards-compatibility with v0 docs. */\n async getUserMedia(audio: boolean = true, video: boolean = true): Promise<MediaStream> {\n return this.getLocalStream({ audio, video });\n }\n\n /**\n * Prompt the user for a screen-share / window-share stream.\n */\n async getScreenShare(opts: DisplayMediaStreamOptions = { video: true }): Promise<MediaStream> {\n if (!navigator.mediaDevices?.getDisplayMedia) {\n throw new Error('[signalling-sdk] getDisplayMedia is not supported in this browser');\n }\n return navigator.mediaDevices.getDisplayMedia(opts);\n }\n\n /** Raw enumeration of every connected media device. */\n async listDevices(): Promise<MediaDeviceInfo[]> {\n this.assertSupported('enumerateDevices');\n return navigator.mediaDevices.enumerateDevices();\n }\n\n /** Categorised view: cameras, microphones, speakers. */\n async listInventory(): Promise<DeviceInventory> {\n const all = await this.listDevices();\n return {\n cameras: all.filter((d) => d.kind === 'videoinput'),\n microphones: all.filter((d) => d.kind === 'audioinput'),\n speakers: all.filter((d) => d.kind === 'audiooutput'),\n };\n }\n\n /**\n * Subscribe to device-list changes (e.g. user plugged in a USB mic).\n * Returns an unsubscribe function.\n */\n onDeviceChange(cb: () => void): () => void {\n if (!navigator.mediaDevices) return () => undefined;\n navigator.mediaDevices.addEventListener('devicechange', cb);\n return () => navigator.mediaDevices.removeEventListener('devicechange', cb);\n }\n\n /**\n * Stop every track on a stream — important to call when the user\n * unmounts the local preview, otherwise the camera light stays on.\n */\n stopStream(stream: MediaStream | null | undefined): void {\n if (!stream) return;\n for (const t of stream.getTracks()) {\n try {\n t.stop();\n } catch {\n // ignore — tracks may already be ended\n }\n }\n }\n\n private assertSupported(method: 'getUserMedia' | 'enumerateDevices'): void {\n if (typeof navigator === 'undefined' || !navigator.mediaDevices) {\n throw new Error(\n '[signalling-sdk] navigator.mediaDevices unavailable (insecure context or non-browser runtime)'\n );\n }\n if (!(method in navigator.mediaDevices)) {\n throw new Error(`[signalling-sdk] navigator.mediaDevices.${method} is not supported`);\n }\n }\n}\n","import { WebSocketClient } from '../websocket/client';\nimport {\n Friend,\n FriendsEventMap,\n Logger,\n PendingFriendRequest,\n WSMessage,\n} from '../types';\nimport { TypedEventEmitter } from '../internal/events';\n\n/**\n * Default interval for the background friend-list refresh. The desktop\n * client polls every 30s; the SDK matches so online-status indicators\n * stay reasonably fresh without hammering the backend.\n */\nconst DEFAULT_REFRESH_INTERVAL_MS = 30_000;\n\n/**\n * Delay between the WS handshake completing and our first `friend_list`\n * + `friend_pending` query. Matches the desktop client — gives the\n * backend a beat to fully wire up the connection before we start\n * making requests on it. Without this, occasional first-frame queries\n * race the negotiation completion and return empty.\n */\nconst POST_CONNECT_QUERY_DELAY_MS = 300;\n\n/**\n * `FriendsManager` is the SDK's stateful wrapper around the global-WS\n * friend protocol. It owns:\n *\n * - A cache of the local user's friends and pending incoming requests.\n * - The periodic refresh that keeps online-status indicators alive.\n * - The translation between wire-shaped frames (with backend's actual\n * field names) and the SDK's typed event surface.\n *\n * The manager is **lazy** — `SignallingSDK` constructs it on first\n * access of `sdk.friends`. Construction opens the global WS, runs the\n * normal DPoP-protected handshake, and begins listening for inbound\n * frames.\n *\n * Wire shapes (matched against `backend/global/handler.go`):\n *\n * - server → us\n * `friend_list` `{friends: [{friendshipId, username, isOnline}]}`\n * `friend_pending` `{requests: [{friendshipId, requester, createdAt}]}`\n * `friend_request_ack` `{friendshipId, addressee, status}`\n * `friend_accept_ack` `{friendshipId, friend, status}`\n * `friend_reject_ack` `{friendshipId, status}`\n * `friend_remove_ack` `{friendshipId, status}`\n * `error` `{originalType, error}`\n *\n * - peer push (forwarded by server)\n * `friend_request` `{friendshipId, requester}` from = requester username\n * `friend_accepted` `{friendshipId, acceptedBy}` from = acceptor username\n *\n * - us → server\n * `friend_list` no payload\n * `friend_pending` no payload\n * `friend_request` to = addressee username\n * `friend_accept` `{friendshipId}`\n * `friend_reject` `{friendshipId}`\n * `friend_remove` `{friendshipId}`\n */\nexport class FriendsManager extends TypedEventEmitter<FriendsEventMap> {\n private friends: Friend[] = [];\n private pending: PendingFriendRequest[] = [];\n private wsUnsubs: Array<() => void> = [];\n private refreshTimer: ReturnType<typeof setInterval> | null = null;\n private firstQueryTimer: ReturnType<typeof setTimeout> | null = null;\n private readonly refreshIntervalMs: number;\n\n constructor(\n private readonly ws: WebSocketClient,\n private readonly logger: Logger,\n options: { refreshIntervalMs?: number } = {}\n ) {\n super();\n this.refreshIntervalMs = options.refreshIntervalMs ?? DEFAULT_REFRESH_INTERVAL_MS;\n }\n\n /**\n * Bind to the global WS — subscribes to inbound frames and kicks off\n * the first `friend_list` / `friend_pending` query (after a small\n * delay so the WS handshake has time to settle). Idempotent.\n */\n bind(): void {\n if (this.wsUnsubs.length > 0) return;\n\n this.wsUnsubs.push(this.ws.on('message', (msg) => this.handleMessage(msg)));\n this.wsUnsubs.push(\n this.ws.on('open', () => this.schedulePostConnectQueries())\n );\n this.wsUnsubs.push(\n this.ws.on('close', () => {\n // Drop local cache on disconnect so a reconnect re-syncs cleanly.\n this.clearLocalState('disconnect');\n })\n );\n\n // If the WS handshake already completed before we bound, fire the\n // first query now. `WebSocketClient.isReady` returns true only after\n // the DPoP first-frame ack.\n if (this.ws.isReady) this.schedulePostConnectQueries();\n }\n\n /**\n * Drop subscriptions and cancel the refresh timer. Call before\n * disposing the SDK so timers don't keep the process alive.\n */\n unbind(): void {\n for (const off of this.wsUnsubs) off();\n this.wsUnsubs = [];\n this.cancelTimers();\n this.clearLocalState('unbind');\n }\n\n // -------------------------------------------------------------------------\n // Public read API — synchronous snapshot of the SDK-owned cache.\n // -------------------------------------------------------------------------\n\n /** Current friends cache. Mutating the returned array is a no-op (it's a copy). */\n list(): Friend[] {\n return this.friends.slice();\n }\n\n /** Current pending-requests cache. */\n pendingRequests(): PendingFriendRequest[] {\n return this.pending.slice();\n }\n\n // -------------------------------------------------------------------------\n // Public command API — fires WS frames; results land via events.\n // -------------------------------------------------------------------------\n\n /** Force a refresh of both `friends` and `pending` caches. */\n refresh(): void {\n this.send({ type: 'friend_list', from: '', to: '', payload: {} });\n this.send({ type: 'friend_pending', from: '', to: '', payload: {} });\n }\n\n /** Send a friend request to a peer by their **username**. */\n sendRequest(username: string): void {\n if (!username) throw new Error('[signalling-sdk] sendRequest: username required');\n this.send({ type: 'friend_request', from: '', to: username, payload: {} });\n }\n\n /** Accept a pending friend request by friendship id. */\n accept(friendshipId: string): void {\n if (!friendshipId) throw new Error('[signalling-sdk] accept: friendshipId required');\n this.send({\n type: 'friend_accept',\n from: '',\n to: '',\n payload: { friendshipId },\n });\n }\n\n /** Reject a pending friend request by friendship id. */\n reject(friendshipId: string): void {\n if (!friendshipId) throw new Error('[signalling-sdk] reject: friendshipId required');\n this.send({\n type: 'friend_reject',\n from: '',\n to: '',\n payload: { friendshipId },\n });\n }\n\n /** Remove an existing friendship by id. */\n remove(friendshipId: string): void {\n if (!friendshipId) throw new Error('[signalling-sdk] remove: friendshipId required');\n this.send({\n type: 'friend_remove',\n from: '',\n to: '',\n payload: { friendshipId },\n });\n }\n\n // -------------------------------------------------------------------------\n // Internals\n // -------------------------------------------------------------------------\n\n private schedulePostConnectQueries(): void {\n if (this.firstQueryTimer) clearTimeout(this.firstQueryTimer);\n this.firstQueryTimer = setTimeout(() => {\n this.refresh();\n this.startRefreshLoop();\n }, POST_CONNECT_QUERY_DELAY_MS);\n }\n\n private startRefreshLoop(): void {\n this.cancelRefreshOnly();\n this.refreshTimer = setInterval(() => {\n // Only refresh the friends list (online-status churn); pending\n // requests change rarely and arrive as pushes — no need to poll.\n this.send({ type: 'friend_list', from: '', to: '', payload: {} });\n }, this.refreshIntervalMs);\n }\n\n private cancelRefreshOnly(): void {\n if (this.refreshTimer) {\n clearInterval(this.refreshTimer);\n this.refreshTimer = null;\n }\n }\n\n private cancelTimers(): void {\n this.cancelRefreshOnly();\n if (this.firstQueryTimer) {\n clearTimeout(this.firstQueryTimer);\n this.firstQueryTimer = null;\n }\n }\n\n private clearLocalState(_reason: 'disconnect' | 'unbind'): void {\n const hadFriends = this.friends.length > 0;\n const hadPending = this.pending.length > 0;\n this.friends = [];\n this.pending = [];\n if (hadFriends) this.emit('friends.changed', { friends: this.friends.slice() });\n if (hadPending) this.emit('friends.pending-changed', { pending: this.pending.slice() });\n }\n\n private send(msg: WSMessage): void {\n try {\n this.ws.send(msg);\n } catch (err) {\n this.logger.warn(`FriendsManager.send(${msg.type}) failed`, err);\n }\n }\n\n private handleMessage(msg: WSMessage): void {\n const payload = (msg.payload ?? {}) as Record<string, unknown>;\n switch (msg.type) {\n case 'friend_list': {\n const arr = Array.isArray(payload.friends) ? (payload.friends as unknown[]) : [];\n this.friends = arr\n .filter((f): f is Record<string, unknown> => !!f && typeof f === 'object')\n .map((f) => ({\n friendshipId: String(f.friendshipId ?? ''),\n username: String(f.username ?? ''),\n isOnline: Boolean(f.isOnline),\n }))\n .filter((f) => f.friendshipId && f.username);\n this.emit('friends.changed', { friends: this.friends.slice() });\n break;\n }\n case 'friend_pending': {\n const arr = Array.isArray(payload.requests) ? (payload.requests as unknown[]) : [];\n this.pending = arr\n .filter((p): p is Record<string, unknown> => !!p && typeof p === 'object')\n .map((p) => ({\n friendshipId: String(p.friendshipId ?? ''),\n requester: String(p.requester ?? ''),\n createdAt: String(p.createdAt ?? ''),\n }))\n .filter((p) => p.friendshipId && p.requester);\n this.emit('friends.pending-changed', { pending: this.pending.slice() });\n break;\n }\n case 'friend_request': {\n // Peer-push: someone asked to be our friend. Append to pending\n // if we don't already have it (dedupe — backend can also push\n // after we requested a list refresh during a slow connect).\n if (msg.from && msg.from !== 'server') {\n const friendshipId = String(payload.friendshipId ?? '');\n const requester = String(payload.requester ?? msg.from);\n if (!friendshipId) break;\n if (this.pending.some((r) => r.friendshipId === friendshipId)) break;\n const entry: PendingFriendRequest = {\n friendshipId,\n requester,\n createdAt: new Date().toISOString(),\n };\n this.pending = [...this.pending, entry];\n this.emit('friend.request-received', entry);\n this.emit('friends.pending-changed', { pending: this.pending.slice() });\n }\n break;\n }\n case 'friend_accept_ack': {\n // We accepted a request → drop from pending, add to friends.\n const friendshipId = String(payload.friendshipId ?? '');\n const friendUsername = String(payload.friend ?? '');\n if (!friendshipId || !friendUsername) break;\n this.pending = this.pending.filter((r) => r.friendshipId !== friendshipId);\n this.emit('friends.pending-changed', { pending: this.pending.slice() });\n if (!this.friends.some((f) => f.friendshipId === friendshipId)) {\n const friend: Friend = { friendshipId, username: friendUsername, isOnline: true };\n this.friends = [...this.friends, friend];\n this.emit('friend.added', friend);\n this.emit('friends.changed', { friends: this.friends.slice() });\n }\n break;\n }\n case 'friend_accepted': {\n // Someone we previously requested accepted us. They're now a friend.\n const friendshipId = String(payload.friendshipId ?? '');\n const acceptedBy = String(payload.acceptedBy ?? msg.from ?? '');\n if (!friendshipId || !acceptedBy) break;\n if (!this.friends.some((f) => f.friendshipId === friendshipId)) {\n const friend: Friend = { friendshipId, username: acceptedBy, isOnline: true };\n this.friends = [...this.friends, friend];\n this.emit('friend.added', friend);\n this.emit('friends.changed', { friends: this.friends.slice() });\n }\n break;\n }\n case 'friend_reject_ack': {\n const friendshipId = String(payload.friendshipId ?? '');\n if (!friendshipId) break;\n const before = this.pending.length;\n this.pending = this.pending.filter((r) => r.friendshipId !== friendshipId);\n if (this.pending.length !== before) {\n this.emit('friends.pending-changed', { pending: this.pending.slice() });\n }\n break;\n }\n case 'friend_remove_ack': {\n const friendshipId = String(payload.friendshipId ?? '');\n if (!friendshipId) break;\n const before = this.friends.length;\n this.friends = this.friends.filter((f) => f.friendshipId !== friendshipId);\n if (this.friends.length !== before) {\n this.emit('friend.removed', { friendshipId });\n this.emit('friends.changed', { friends: this.friends.slice() });\n }\n break;\n }\n case 'friend_request_ack':\n // We sent a request; backend confirms. No state change until the\n // peer accepts (which fires `friend_accepted` separately).\n break;\n case 'error': {\n const originalType = String(payload.originalType ?? '');\n const errMsg = String(payload.error ?? 'unknown error');\n if (originalType.startsWith('friend_')) {\n this.emit('friends.error', { originalType, error: errMsg });\n }\n break;\n }\n // Non-friends frames (dm, pong, call_invite, etc.) are ignored\n // here — `CallInviteManager` and any future managers subscribe to\n // the same WS `message` stream independently.\n default:\n break;\n }\n }\n}\n","import { WebSocketClient } from '../websocket/client';\nimport {\n CallInviteEventMap,\n IncomingCallInvite,\n Logger,\n WSMessage,\n} from '../types';\nimport { TypedEventEmitter } from '../internal/events';\n\n/**\n * `CallInviteManager` handles `call_invite` frames on the global WS.\n * Used by `sdk.calls.invite()` (outbound) and the\n * `'call.invite-received'` event on `sdk.calls` (inbound). Lives on\n * the global socket — separate from `Call`, which lives on the room\n * socket. The two are intentionally not coupled: you can receive an\n * invite while not in any call, and you can ignore the invite without\n * ever entering a call.\n *\n * Wire shape (per `backend/global/handler.go::handleCallInvite`):\n *\n * out: `{ type: 'call_invite', to: '<recipient-username>', payload: { roomId } }`\n * in: `{ type: 'call_invite', from: '<caller-username>', payload: { roomId } }`\n *\n * The backend resolves `to` (username) to a hub key server-side; the\n * sender never sees the recipient's internal id.\n */\nexport class CallInviteManager extends TypedEventEmitter<CallInviteEventMap> {\n private wsUnsubs: Array<() => void> = [];\n\n constructor(\n private readonly ws: WebSocketClient,\n private readonly logger: Logger\n ) {\n super();\n }\n\n /** Subscribe to the global WS. Idempotent. */\n bind(): void {\n if (this.wsUnsubs.length > 0) return;\n this.wsUnsubs.push(this.ws.on('message', (msg) => this.handleMessage(msg)));\n }\n\n /** Drop subscriptions. */\n unbind(): void {\n for (const off of this.wsUnsubs) off();\n this.wsUnsubs = [];\n }\n\n /**\n * Invite a friend to join the given room. The recipient is addressed\n * by username; the SDK forwards the request over the global WS and\n * the backend pushes a `call_invite` frame to the recipient.\n *\n * No delivery acknowledgement today — fire-and-forget. The backend\n * logs a warning if the recipient is offline; future revisions may\n * emit a `'call.invite-undelivered'` event so consumers can show\n * \"user is offline\" UI.\n */\n invite(toUsername: string, roomId: number | string): void {\n if (!toUsername) throw new Error('[signalling-sdk] invite: toUsername required');\n const id = String(roomId);\n if (!id) throw new Error('[signalling-sdk] invite: roomId required');\n try {\n this.ws.send({\n type: 'call_invite',\n from: '',\n to: toUsername,\n payload: { roomId: id },\n });\n } catch (err) {\n this.logger.warn('CallInviteManager.invite failed', err);\n throw err;\n }\n }\n\n private handleMessage(msg: WSMessage): void {\n if (msg.type !== 'call_invite') return;\n // The backend always sets `from` to the caller's username before\n // forwarding; we treat `from === 'server'` (or empty) as malformed.\n const callerUsername = msg.from;\n if (!callerUsername || callerUsername === 'server') {\n this.logger.warn('call_invite: ignoring frame with missing/server from', msg);\n return;\n }\n const payload = (msg.payload ?? {}) as Record<string, unknown>;\n const roomId = payload.roomId;\n if (typeof roomId !== 'string' && typeof roomId !== 'number') {\n this.logger.warn('call_invite: missing or malformed roomId', msg);\n return;\n }\n const invite: IncomingCallInvite = {\n callerUsername,\n roomId: String(roomId),\n };\n this.emit('call.invite-received', invite);\n }\n}\n","import { Logger, LogLevel } from '../types';\n\nconst ORDER: Record<LogLevel, number> = {\n silent: -1,\n error: 0,\n warn: 1,\n info: 2,\n debug: 3,\n};\n\n/**\n * Build a default console-backed `Logger` filtered by `level`.\n * Used when the integrator does not supply a custom logger.\n */\nexport function createConsoleLogger(level: LogLevel): Logger {\n const enabled = (lv: LogLevel) => ORDER[level] >= ORDER[lv];\n return {\n error: (...args: unknown[]) => enabled('error') && console.error('[signalling-sdk]', ...args),\n warn: (...args: unknown[]) => enabled('warn') && console.warn('[signalling-sdk]', ...args),\n info: (...args: unknown[]) => enabled('info') && console.info('[signalling-sdk]', ...args),\n debug: (...args: unknown[]) => enabled('debug') && console.debug('[signalling-sdk]', ...args),\n };\n}\n"],"mappings":"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","names":["index_exports","__export","ApiClient","AuthClient","Call","CallInviteManager","DPoPManager","DeviceManager","FriendsManager","RoomManager","SignallingApiError","SignallingSDK","SignallingWebSocketError","WSCloseCode","WebRTCManager","WebSocketClient","isFatalCloseCode","__toCommonJS","SignallingApiError","message","response","body","b","SignallingWebSocketError","code","b64urlString","input","b64urlBytes","bytes","bin","i","b64urlDecodeBytes","padded","padding","out","b64urlDecodeString","DB_NAME","DB_VERSION","STORE","KEY_ID","ALGO","SIGN_ALGO","NonceCache","value","DPoPManager","existing","idbGet","idbPut","method","url","accessTokenHash","header","sanitiseJWK","payload","cryptoRandomUUID","nonce","headerB64","b64urlString","payloadB64","signingInput","sig","b64urlBytes","headers","idbDelete","token","digest","jwk","bytes","hex","b","openDB","resolve","reject","req","db","tx","keypair","normaliseRoom","raw","clients","c","ApiClient","config","dpop","logger","stripTrailingSlash","path","ensureLeadingSlash","bindToken","updates","email","roomId","clientId","ttlSeconds","init","url","method","bearerToken","send","ath","DPoPManager","proof","headers","response","peeked","peekJsonClone","code","body","safeJson","message","extractErrorMessage","SignallingApiError","text","b","TypedEventEmitter","event","fn","set","off","payload","AuthClient","TypedEventEmitter","config","api","dpop","logger","returnTo","url","fragment","raw","bindToken","runUnderBindLock","result","err","resetErr","force","user","SignallingApiError","updates","updated","BIND_FLOW_LOCK","fallbackChain","fn","nav","next","WSCloseCode","NO_RETRY_CODES","isFatalCloseCode","code","WebSocketClient","TypedEventEmitter","api","dpop","logger","options","roomId","clientId","target","ticket","ticketNonce","nonceFromTicketJWT","url","canonicalUrl","stripQueryFragment","resolve","reject","ws","settled","settle","err","proof","e","event","parsed","SignallingWebSocketError","msg","delay","message","reason","base","path","u","jwt","parts","payload","b64urlDecodeString","RoomManager","api","ws","logger","result","roomId","id","active","err","targetClientId","updates","WebRTCManager","TypedEventEmitter","ws","logger","defaultIceServers","local","msg","off","clientId","pc","credentials","list","c","stream","senders","newAudio","newVideo","isScreenSender","s","senderList","cameraSenders","audioSender","videoSender","streamId","tracks","peerId","track","offer","err","sender","state","remoteClientId","sdp","answer","cand","payload","fromId","p","video","audio","existing","event","toClientId","type","DeviceManager","constraints","audio","video","opts","all","d","cb","stream","t","method","DEFAULT_REFRESH_INTERVAL_MS","POST_CONNECT_QUERY_DELAY_MS","FriendsManager","TypedEventEmitter","ws","logger","options","msg","off","username","friendshipId","_reason","hadFriends","hadPending","err","payload","arr","f","p","requester","r","entry","friendUsername","friend","acceptedBy","before","originalType","errMsg","CallInviteManager","TypedEventEmitter","ws","logger","msg","off","toUsername","roomId","id","err","callerUsername","invite","ORDER","createConsoleLogger","level","enabled","lv","args","Call","TypedEventEmitter","roomId","clientId","rooms","webrtc","ws","devices","auth","enabled","t","stream","screen","err","text","trimmed","message","generateChatId","peers","peerId","user","SignallingSDK","_SignallingSDK","config","logLevel","createConsoleLogger","DPoPManager","ApiClient","AuthClient","WebSocketClient","RoomManager","WebRTCManager","DeviceManager","sdk","globalWs","FriendsManager","toUsername","event","listener","CallInviteManager","off","joined","cred","localStream","call","msg","payload","isFatalCloseCode","_fromClientId"]}
|