@vielzeug/mesh 26.10.15 → 26.10.17

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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/dist/_base45.cjs.map +1 -1
  3. package/dist/_base45.js +37 -1
  4. package/dist/_base45.js.map +1 -1
  5. package/dist/_defaults.cjs.map +1 -1
  6. package/dist/_defaults.js +5 -1
  7. package/dist/_defaults.js.map +1 -1
  8. package/dist/_node.cjs.map +1 -1
  9. package/dist/_node.js +166 -1
  10. package/dist/_node.js.map +1 -1
  11. package/dist/_proof.cjs.map +1 -1
  12. package/dist/_proof.js +16 -1
  13. package/dist/_proof.js.map +1 -1
  14. package/dist/_random.cjs.map +1 -1
  15. package/dist/_random.js +14 -1
  16. package/dist/_random.js.map +1 -1
  17. package/dist/_rtc.cjs.map +1 -1
  18. package/dist/_rtc.js +41 -3
  19. package/dist/_rtc.js.map +1 -1
  20. package/dist/_sdp.cjs.map +1 -1
  21. package/dist/_sdp.js +54 -2
  22. package/dist/_sdp.js.map +1 -1
  23. package/dist/_wire.cjs.map +1 -1
  24. package/dist/_wire.js +20 -1
  25. package/dist/_wire.js.map +1 -1
  26. package/dist/codec.cjs.map +1 -1
  27. package/dist/codec.js +34 -1
  28. package/dist/codec.js.map +1 -1
  29. package/dist/errors.cjs.map +1 -1
  30. package/dist/errors.js +18 -1
  31. package/dist/errors.js.map +1 -1
  32. package/dist/guest.cjs.map +1 -1
  33. package/dist/guest.js +153 -1
  34. package/dist/guest.js.map +1 -1
  35. package/dist/host.cjs.map +1 -1
  36. package/dist/host.js +202 -1
  37. package/dist/host.js.map +1 -1
  38. package/dist/index.js +6 -1
  39. package/dist/mesh.cjs.map +1 -1
  40. package/dist/mesh.iife.js.map +1 -1
  41. package/dist/mesh.js +756 -4
  42. package/dist/mesh.js.map +1 -1
  43. package/dist/qr-codec.cjs.map +1 -1
  44. package/dist/qr-codec.js +52 -1
  45. package/dist/qr-codec.js.map +1 -1
  46. package/dist/testing/fake-rtc.cjs +1 -1
  47. package/dist/testing/fake-rtc.cjs.map +1 -1
  48. package/dist/testing/fake-rtc.js +153 -1
  49. package/dist/testing/fake-rtc.js.map +1 -1
  50. package/dist/testing.js +1 -1
  51. package/package.json +6 -5
package/dist/mesh.js CHANGED
@@ -1,5 +1,757 @@
1
- import{base64UrlToText as e,bytesToBase64Url as t,randomFloat as n,tapper as r,textToBase64Url as i,utf8Bytes as a}from"@vielzeug/arsenal";var MeshError=class extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}},MeshPairingError=class extends MeshError{},MeshConnectionError=class extends MeshError{peerId;constructor(e,t=null,n){super(e,n),this.peerId=t}},MeshPayloadError=class extends MeshError{},MeshTimeoutError=class extends MeshError{},MeshDisposedError=class extends MeshError{constructor(e=`Mesh node`,t){super(`${e} is disposed`,t)}},MeshUnsupportedError=class extends MeshError{};function isRecord(e){return typeof e==`object`&&!!e}function isInvitation(e){return e.v===1&&typeof e.sessionId==`string`&&typeof e.secret==`string`&&typeof e.expiresAt==`number`&&typeof e.sdp==`string`}function isAnswer(e){return e.v===1&&typeof e.sessionId==`string`&&typeof e.proof==`string`&&isRecord(e.peer)&&typeof e.peer.id==`string`&&e.peer.id!==``&&(e.peer.name===void 0||typeof e.peer.name==`string`)&&typeof e.sdp==`string`}function parsePairingPayload(e){if(!isRecord(e)||e.v!==1)throw new MeshPairingError(`Unsupported pairing payload version`);if(isInvitation(e)||isAnswer(e))return e;throw new MeshPairingError(`Unrecognized pairing payload shape`)}var o={decode(t){let n;try{n=JSON.parse(e(t.trim()))}catch(e){throw new MeshPairingError(`Malformed pairing payload`,{cause:e})}return parsePairingPayload(n)},encode(e){return i(JSON.stringify(e))}};function randomBytes(e,t){let r=new Uint8Array(e);if(t)for(let i=0;i<e;i++)r[i]=Math.floor(n(t)*256);else crypto.getRandomValues(r);return r}function randomId(e){return t(randomBytes(16,e))}function toEnvelope(e){if(typeof e!=`object`||!e)return null;let t=e;return t.v!==1||typeof t.id!=`string`||typeof t.t!=`string`||typeof t.ts!=`number`?null:e}var MessageDedupe=class{seen=new Set;order=[];capacity;constructor(e=256){this.capacity=e}check(e){return!this.seen.has(e)&&(this.seen.add(e),this.order.push(e),this.order.length>this.capacity&&this.seen.delete(this.order.shift()),!0)}};function createNodeCore(e){let t=r(),n=new AbortController,i={disposalCtrl:n,disposed:!1,emitTap(e){t.emit(e)},ensureLive(){if(i.disposed)throw new MeshDisposedError},markDisposed(){i.disposed||(i.disposed=!0,i.status=`disposed`,i.emitTap({status:`disposed`,type:`status-change`}),i.emitTap({type:`dispose`}),t.clear(),n.abort())},setStatus(e){i.disposed||i.status===e||(i.status=e,i.emitTap({status:e,type:`status-change`}))},status:`idle`,tap(e,n){return i.disposed?()=>{}:t.tap(e,n)}};return e&&(e.aborted?i.markDisposed():e.addEventListener(`abort`,()=>i.markDisposed(),{once:!0})),i}function createPeerRecord(e,t,n){let r={dc:null,dedupe:new MessageDedupe,id:e,name:t,pc:null,publicPeer:{get id(){return r.id},get name(){return r.name},get role(){return r.role},get status(){return r.status}},role:n,status:`connecting`};return r}function createMessenger(e){let t=new Map;function reject(t,n){e.emitTap({peerId:t,reason:n,type:`message-rejected`})}return{clear(){t.clear()},handleInbound(n,r){let i=n.id;if(typeof r!=`string`){reject(i,`malformed`);return}let o=a(r);if(o>e.maxMessageBytes){reject(i,`too-large`);return}let s;try{s=JSON.parse(r)}catch{reject(i,`malformed`);return}let c=toEnvelope(s);if(!c){reject(i,`malformed`);return}if(!n.dedupe.check(c.id)){reject(i,`duplicate`);return}let l=t.get(c.t);if(!l||l.size===0){reject(i,`unknown-type`);return}e.emitTap({bytes:o,messageType:c.t,peerId:i,type:`message-received`});let u={messageId:c.id,payload:c.p,peerId:i,sentAt:c.ts};for(let t of l)try{t(u)}catch(t){e.emitTap({error:new MeshError(`Message handler threw`,{cause:t}),type:`error`})}},on(e,n){let r=t.get(e);return r||(r=new Set,t.set(e,r)),r.add(n),()=>{r.delete(n),r.size===0&&t.delete(e)}},sendTo(t,n,r){let i=t.dc;if(!i||i.readyState!==`open`||t.status!==`connected`)throw new MeshConnectionError(`Peer "${t.id}" is not connected`,t.id);let o={id:randomId(e.random),p:r,t:n,ts:e.clock(),v:1},s;try{s=JSON.stringify(o)}catch(e){throw new MeshPayloadError(`Failed to serialize message "${n}"`,{cause:e})}let c=a(s);if(c>e.maxMessageBytes)throw new MeshPayloadError(`Message "${n}" is ${c} bytes (max ${e.maxMessageBytes})`);try{i.send(s)}catch(e){throw new MeshConnectionError(`Failed to send message "${n}"`,t.id,{cause:e})}e.emitTap({bytes:c,messageType:n,peerId:t.id,type:`message-sent`})}}}function isCompactSdp(e){if(typeof e!=`object`||!e)return!1;let t=e;return typeof t.u==`string`&&typeof t.p==`string`&&typeof t.f==`string`&&typeof t.s==`string`&&typeof t.m==`string`&&Array.isArray(t.c)&&t.c.every(e=>typeof e==`string`)}function restoreSdp(e){if(typeof e==`string`)return e;if(isCompactSdp(e))return expandSdp(e);throw new MeshPairingError(`Malformed pairing payload`)}function compactSdp(e){let t=``,n=``,r=``,i=``,a=`0`,o,s,c=[],l=0;for(let u of e.split(/\r?\n/))u.startsWith(`m=`)?l+=1:u.startsWith(`a=ice-ufrag:`)?t=u.slice(12):u.startsWith(`a=ice-pwd:`)?n=u.slice(10):u.startsWith(`a=fingerprint:sha-256 `)?r=u.slice(22).replaceAll(`:`,``).toUpperCase():u.startsWith(`a=setup:`)?i=u.slice(8):u.startsWith(`a=mid:`)?a=u.slice(6):u.startsWith(`a=sctp-port:`)?o=Number(u.slice(12)):u.startsWith(`a=max-message-size:`)?s=Number(u.slice(19)):u.startsWith(`a=candidate:`)&&c.push(u.slice(12));return l!==1||!t||!n||!r?null:{c,f:r,m:a,p:n,s:i||`actpass`,...s?{mm:s}:{},...o?{sp:o}:{},u:t}}function canonicalSdp(e){let t=compactSdp(e);return t?JSON.stringify(t):e}function expandSdp(e){let t=e.f.replace(/(..)/g,`$1:`).slice(0,-1);return`${[`v=0`,`o=- 0 2 IN IP4 0.0.0.0`,`s=-`,`t=0 0`,`a=group:BUNDLE ${e.m}`,`m=application 9 UDP/DTLS/SCTP webrtc-datachannel`,`c=IN IP4 0.0.0.0`,`a=ice-ufrag:${e.u}`,`a=ice-pwd:${e.p}`,`a=fingerprint:sha-256 ${t}`,`a=setup:${e.s}`,`a=mid:${e.m}`,`a=sctp-port:${e.sp??5e3}`,...e.mm?[`a=max-message-size:${e.mm}`]:[],...e.c.map(e=>`a=candidate:${e}`),``].join(`\r
2
- `)}`}async function createProof(e,n){let r=globalThis.crypto?.subtle;if(!r)throw new MeshUnsupportedError(`WebCrypto is unavailable: pairing requires a secure context`);let i=new TextEncoder,a=await r.importKey(`raw`,i.encode(e),{hash:`SHA-256`,name:`HMAC`},!1,[`sign`]),o=await r.sign(`HMAC`,a,i.encode(canonicalSdp(n)));return t(new Uint8Array(o))}var s={createPeerConnection(e){let t=globalThis.RTCPeerConnection;if(!t)throw new MeshUnsupportedError("WebRTC is unavailable in this environment: inject an `rtc` factory");return new t(e)}};function normalizeSdp(e){let t=e.replace(/\r?\n/g,`\r
3
- `);return t.endsWith(`\r
4
- `)?t:`${t}\r\n`}function waitIceGathering(e,t){return e.iceGatheringState===`complete`?Promise.resolve():new Promise(n=>{let cleanup=()=>{clearTimeout(r),e.removeEventListener(`icegatheringstatechange`,onChange)},onChange=()=>{e.iceGatheringState===`complete`&&(cleanup(),n())},r=setTimeout(()=>{cleanup(),n()},t);e.addEventListener(`icegatheringstatechange`,onChange)})}function waitChannelOpen(e,t,n,r,i){return e.readyState===`open`?Promise.resolve():new Promise((a,o)=>{let s=null,timeout=()=>new MeshTimeoutError(`Timed out waiting ${n}ms for the data channel to open`),fail=e=>{cleanup(),o(e)},cleanup=()=>{s&&clearTimeout(s),s=null,e.removeEventListener(`open`,onOpen),e.removeEventListener(`close`,onClose),t.removeEventListener(`iceconnectionstatechange`,onIce),i.removeEventListener(`abort`,onDispose)},onOpen=()=>{cleanup(),a()},onClose=()=>fail(timeout()),onIce=()=>{let e=t.iceConnectionState;(e===`failed`||e===`closed`||e===`disconnected`)&&fail(new MeshConnectionError(`ICE connection ${e} before channel open`,r))},onDispose=()=>fail(new MeshDisposedError);s=setTimeout(()=>fail(timeout()),n),e.addEventListener(`open`,onOpen),e.addEventListener(`close`,onClose),t.addEventListener(`iceconnectionstatechange`,onIce),i.addEventListener(`abort`,onDispose,{once:!0})})}function createMeshGuest(e={}){let t=e.rtc??s,n=e.clock??(()=>Date.now()),r=e.maxMessageBytes??65536,i=e.channelOpenTimeoutMs??6e4,a=e.iceGatheringTimeoutMs??5e3,o=createNodeCore(e.signal),c=createMessenger({clock:n,emitTap:o.emitTap,maxMessageBytes:r,random:e.random}),l=null,u=null,openTimeoutError=()=>new MeshTimeoutError(`Timed out waiting ${i}ms for the data channel to open`);function failPeer(e,t){o.disposed||e!==l||e.status===`failed`||(e.status=`failed`,u&&=(clearTimeout(u),null),setTimeout(()=>{e.dc?.close(),e.pc?.close()},0),o.emitTap({peerId:e.id,status:`failed`,type:`peer-status-change`}),o.emitTap({error:t,type:`error`}),o.emitTap({peer:e.publicPeer,reason:t.message,type:`peer-left`}),o.setStatus(`failed`))}function disconnectPeer(e,t){o.disposed||e!==l||e.status!==`disconnected`&&e.status!==`failed`&&(e.status=`disconnected`,e.pc?.close(),o.emitTap({peerId:e.id,status:`disconnected`,type:`peer-status-change`}),o.emitTap({peer:e.publicPeer,reason:t,type:`peer-left`}),o.setStatus(`disconnected`))}function wireChannel(e,t,n){e.dc=n,n.addEventListener(`open`,()=>{o.disposed||(u&&=(clearTimeout(u),null),e.status=`connected`,o.emitTap({peerId:e.id,status:`connected`,type:`peer-status-change`}),o.emitTap({peer:e.publicPeer,type:`peer-joined`}),o.setStatus(`connected`))}),n.addEventListener(`message`,t=>{o.disposed||c.handleInbound(e,t.data)}),n.addEventListener(`close`,()=>{o.disposed||(e.status===`connecting`?failPeer(e,u?openTimeoutError():new MeshConnectionError(`channel closed before opening`,e.id)):disconnectPeer(e,`channel closed`))}),n.addEventListener(`error`,()=>{o.disposed||failPeer(e,new MeshConnectionError(`channel error`,e.id))}),t.addEventListener(`iceconnectionstatechange`,()=>{if(o.disposed)return;let n=t.iceConnectionState;n===`failed`||n===`closed`?failPeer(e,new MeshConnectionError(`ice ${n}`,e.id)):n===`disconnected`&&e.status===`connected`&&disconnectPeer(e,`ice disconnected`)})}return{async acceptInvitation(r,s){if(o.ensureLive(),l&&l.status!==`failed`&&l.status!==`disconnected`)throw new MeshPairingError(`Guest is already paired`);if(r?.v!==1)throw new MeshPairingError(`Unsupported invitation version`);if(n()>=r.expiresAt)throw new MeshPairingError(`Invitation expired`);l&&=(u&&=(clearTimeout(u),null),l.pc?.close(),null),o.setStatus(`pairing`);let c;try{c=t.createPeerConnection({iceServers:[...e.iceServers??[]]})}catch(e){throw o.setStatus(`idle`),e}let d=createPeerRecord(r.sessionId,r.hostName,`host`);d.pc=c,l=d,c.addEventListener(`datachannel`,e=>{wireChannel(d,c,e.channel)}),c.addEventListener(`iceconnectionstatechange`,()=>{o.disposed||o.emitTap({peerId:d.id,state:c.iceConnectionState,type:`ice-state`})});try{await c.setRemoteDescription({sdp:normalizeSdp(r.sdp),type:`offer`})}catch(e){throw l=null,o.setStatus(`idle`),new MeshPairingError(`Invalid invitation SDP`,{cause:e})}let f;try{await c.setLocalDescription(await c.createAnswer()),await waitIceGathering(c,a),f=c.localDescription?.sdp??``}catch(e){throw l=null,c.close(),o.setStatus(`idle`),new MeshConnectionError(`Failed to create answer`,null,{cause:e})}if(o.disposed)throw c.close(),new MeshDisposedError;if(!f)throw l=null,c.close(),o.setStatus(`idle`),new MeshConnectionError(`Local description has no SDP`);let p=randomId(e.random),m;try{m=await createProof(r.secret,f)}catch(e){throw l=null,c.close(),o.setStatus(`idle`),e}return o.setStatus(`connecting`),u=setTimeout(()=>{l?.status===`connecting`&&failPeer(l,openTimeoutError())},i),{peer:s?.name===void 0?{id:p}:{id:p,name:s.name},proof:m,sdp:f,sessionId:r.sessionId,v:1}},get disposalSignal(){return o.disposalCtrl.signal},dispose(){o.disposed||(o.markDisposed(),u&&=(clearTimeout(u),null),l&&(l.dc?.close(),l.pc?.close()),c.clear())},get disposed(){return o.disposed},get host(){return l?.publicPeer??null},on(e,t){return o.ensureLive(),c.on(e,t)},send(e,t){if(o.ensureLive(),!l)throw new MeshConnectionError(`Guest is not paired`);c.sendTo(l,e,t)},get status(){return o.status},tap(e,t){return o.tap(e,t)},[Symbol.dispose](){this.dispose()}}}function createMeshHost(e={}){let n=e.rtc??s,r=e.clock??(()=>Date.now()),i=e.maxMessageBytes??65536,a=e.invitationTtlMs??3e5,o=e.channelOpenTimeoutMs??6e4,c=e.iceGatheringTimeoutMs??5e3,l={label:`mesh`,ordered:!0,...e.channel},u=createNodeCore(e.signal),d=createMessenger({clock:r,emitTap:u.emitTap,maxMessageBytes:i,random:e.random}),f=new Map,p=new Map,m=!1,h=!1;function recomputeStatus(){let e=p.size>0?`pairing`:m?`disconnected`:h?`failed`:`idle`;for(let t of f.values()){if(t.status===`connected`){e=`connected`;break}t.status===`connecting`&&(e=`connecting`)}u.setStatus(e)}function expireInvitation(e){let t=p.get(e);t&&(p.delete(e),clearTimeout(t.expiryTimer),t.pc.close(),u.emitTap({sessionId:e,type:`invitation-expired`}),recomputeStatus())}function setPeerStatus(e,t){e.status!==t&&(e.status=t,u.emitTap({peerId:e.id,status:t,type:`peer-status-change`}),recomputeStatus())}function leavePeer(e,t,n=!1){f.has(e.id)&&(f.delete(e.id),e.status=n?`failed`:`disconnected`,e.pc?.close(),e.dc?.close(),u.emitTap({peerId:e.id,status:e.status,type:`peer-status-change`}),u.emitTap({peer:e.publicPeer,reason:t,type:`peer-left`}),n||(m=!0),h||=n,recomputeStatus())}function wirePeerTransport(e,t,n){e.pc=t,e.dc=n,n.addEventListener(`message`,t=>{u.disposed||d.handleInbound(e,t.data)}),n.addEventListener(`close`,()=>{u.disposed||leavePeer(e,`channel closed`)}),n.addEventListener(`error`,()=>{u.disposed||leavePeer(e,`channel error`,!0)}),t.addEventListener(`iceconnectionstatechange`,()=>{if(u.disposed)return;let n=t.iceConnectionState;u.emitTap({peerId:e.id,state:n,type:`ice-state`}),n===`failed`||n===`closed`?leavePeer(e,`ice ${n}`,n===`failed`):n===`disconnected`&&e.status===`connected`&&setPeerStatus(e,`disconnected`)})}function markPeerConnected(e){m=!0,f.set(e.id,e),setPeerStatus(e,`connected`),u.emitTap({peer:e,type:`peer-joined`})}return{async acceptAnswer(t){if(u.ensureLive(),t?.v!==1)throw new MeshPairingError(`Unsupported answer version`);if(!t.peer?.id)throw new MeshPairingError(`Answer is missing a peer id`);let n=p.get(t.sessionId);if(!n)throw new MeshPairingError(`Unknown or expired session`);if(n.answered)throw new MeshPairingError(`Invitation already answered`);if(f.has(t.peer.id))throw new MeshPairingError(`Peer "${t.peer.id}" is already connected`);if(r()>=n.expiresAt)throw expireInvitation(t.sessionId),new MeshPairingError(`Invitation expired`);let i=await createProof(n.secret,t.sdp);if(t.proof!==i)throw new MeshPairingError(`Proof mismatch`);if(!(await e.approvePeer?.(t.peer)??!0))throw u.emitTap({peerId:t.peer.id,type:`peer-rejected`}),new MeshPairingError(`Peer "${t.peer.id}" rejected by host`);if(u.emitTap({peerId:t.peer.id,type:`peer-approved`}),f.has(t.peer.id))throw new MeshPairingError(`Peer "${t.peer.id}" is already connected`);n.answered=!0,p.delete(t.sessionId),clearTimeout(n.expiryTimer);let a=createPeerRecord(t.peer.id,t.peer.name,`guest`),s=waitChannelOpen(n.dc,n.pc,o,a.id,u.disposalCtrl.signal);wirePeerTransport(a,n.pc,n.dc),f.set(a.id,a),recomputeStatus();try{await n.pc.setRemoteDescription({sdp:normalizeSdp(t.sdp),type:`answer`})}catch(e){throw leavePeer(a,`invalid answer sdp`,!0),new MeshPairingError(`Invalid answer SDP`,{cause:e})}try{await s}catch(e){let t=!(e instanceof MeshDisposedError);throw leavePeer(a,e instanceof Error?e.message:`open failed`,t),e}return markPeerConnected(a),a.publicPeer},broadcast(e,t,n){u.ensureLive();for(let r of f.values())r.status!==`connected`||n?.except?.includes(r.id)||d.sendTo(r,e,t)},async createInvitation(i){u.ensureLive();let o=n.createPeerConnection({iceServers:[...e.iceServers??[]]}),s=o.createDataChannel(l.label,{maxRetransmits:l.maxRetransmits,ordered:l.ordered}),d=randomId(e.random),f=t(randomBytes(32,e.random)),m=r()+a;try{await o.setLocalDescription(await o.createOffer()),await waitIceGathering(o,c)}catch(e){throw o.close(),new MeshConnectionError(`Failed to create offer`,null,{cause:e})}if(u.disposed)throw o.close(),new MeshDisposedError;let h=o.localDescription?.sdp;if(!h)throw o.close(),new MeshConnectionError(`Local description has no SDP`);let g=setTimeout(()=>expireInvitation(d),a);return p.set(d,{answered:!1,dc:s,expiresAt:m,expiryTimer:g,pc:o,secret:f,sessionId:d}),u.emitTap({sessionId:d,type:`invitation-created`}),recomputeStatus(),{expiresAt:m,sdp:h,secret:f,sessionId:d,v:1,...i?.name===void 0?{}:{hostName:i.name}}},get disposalSignal(){return u.disposalCtrl.signal},dispose(){if(!u.disposed){u.markDisposed();for(let e of p.values())clearTimeout(e.expiryTimer),e.pc.close();p.clear();for(let e of f.values())e.dc?.close(),e.pc?.close();f.clear(),d.clear()}},get disposed(){return u.disposed},kick(e,t){u.ensureLive();let n=f.get(e);if(!n)throw new MeshConnectionError(`Unknown peer "${e}"`,e);leavePeer(n,t??`kicked`)},on(e,t){return u.ensureLive(),d.on(e,t)},get peers(){return f},send(e,t,n){u.ensureLive();let r=f.get(e);if(!r)throw new MeshConnectionError(`Unknown peer "${e}"`,e);d.sendTo(r,t,n)},get status(){return u.status},tap(e,t){return u.tap(e,t)},[Symbol.dispose](){this.dispose()}}}var c=`0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:`,l=new Map([...c].map((e,t)=>[e,t]));function bytesToBase45(e){let t=``;for(let n=0;n+1<e.length;n+=2){let r=e[n]*256+e[n+1];t+=c[r%45]+c[Math.floor(r/45)%45]+c[Math.floor(r/2025)]}if(e.length%2){let n=e[e.length-1];t+=c[n%45]+c[Math.floor(n/45)]}return t}function base45ToBytes(e){let t=[],n=0;for(;n+2<e.length;){let r=digit(e,n)+45*digit(e,n+1)+2025*digit(e,n+2);if(r>65535)throw new MeshPairingError(`Malformed base45 payload`);t.push(r>>8,r&255),n+=3}if(e.length-n===2){let r=digit(e,n)+45*digit(e,n+1);if(r>255)throw new MeshPairingError(`Malformed base45 payload`);t.push(r)}else if(e.length-n===1)throw new MeshPairingError(`Malformed base45 payload`);return new Uint8Array(t)}function digit(e,t){let n=l.get(e[t]);if(n===void 0)throw new MeshPairingError(`Malformed base45 payload`);return n}var u=`mq2.`,d=new TextEncoder,f=new TextDecoder;function decompressionStream(e){if(typeof globalThis.DecompressionStream!=`function`)throw new MeshUnsupportedError(`DecompressionStream is not available in this environment`);return new globalThis.DecompressionStream(e)}async function pump(e,t){let n=t.writable.getWriter(),r=n.write(e).then(()=>n.close()).catch(()=>{}),i=[],a=t.readable.getReader();for(;;){let{done:e,value:t}=await a.read();if(e)break;i.push(t)}await r;let o=new Uint8Array(i.reduce((e,t)=>e+t.length,0)),s=0;for(let e of i)o.set(e,s),s+=e.length;return o}var p={async decode(e){let t=e.trim();if(t.startsWith(u)){let e;try{let n=base45ToBytes(t.slice(4));e=JSON.parse(f.decode(await pump(n,decompressionStream(`deflate-raw`))))}catch(e){throw e instanceof MeshUnsupportedError||e instanceof MeshPairingError?e:new MeshPairingError(`Malformed pairing payload`,{cause:e})}return isRecord(e)&&(e={...e,sdp:restoreSdp(e.sdp)}),parsePairingPayload(e)}return o.decode(t)},async encode(e){if(typeof globalThis.CompressionStream!=`function`)return o.encode(e);let t=compactSdp(e.sdp);return u+bytesToBase45(await pump(d.encode(JSON.stringify(t?{...e,sdp:t}:e)),new globalThis.CompressionStream(`deflate-raw`)))}};export{MeshConnectionError,MeshDisposedError,MeshError,MeshPairingError,MeshPayloadError,MeshTimeoutError,MeshUnsupportedError,createMeshGuest,createMeshHost,o as meshCodec,p as meshQrCodec};
1
+ import { base64UrlToText as e, bytesToBase64Url as t, randomFloat as n, tapper as r, textToBase64Url as i, utf8Bytes as a } from "@vielzeug/arsenal";
2
+ //#region src/errors.ts
3
+ var MeshError = class extends Error {
4
+ constructor(e, t) {
5
+ super(e, t), this.name = new.target.name, Object.setPrototypeOf(this, new.target.prototype);
6
+ }
7
+ }, MeshPairingError = class extends MeshError {}, MeshConnectionError = class extends MeshError {
8
+ peerId;
9
+ constructor(e, t = null, n) {
10
+ super(e, n), this.peerId = t;
11
+ }
12
+ }, MeshPayloadError = class extends MeshError {}, MeshTimeoutError = class extends MeshError {}, MeshDisposedError = class extends MeshError {
13
+ constructor(e = "Mesh node", t) {
14
+ super(`${e} is disposed`, t);
15
+ }
16
+ }, MeshUnsupportedError = class extends MeshError {};
17
+ //#endregion
18
+ //#region src/codec.ts
19
+ function isRecord(e) {
20
+ return typeof e == "object" && !!e;
21
+ }
22
+ function isInvitation(e) {
23
+ return e.v === 1 && typeof e.sessionId == "string" && typeof e.secret == "string" && typeof e.expiresAt == "number" && typeof e.sdp == "string";
24
+ }
25
+ function isAnswer(e) {
26
+ return e.v === 1 && typeof e.sessionId == "string" && typeof e.proof == "string" && isRecord(e.peer) && typeof e.peer.id == "string" && e.peer.id !== "" && (e.peer.name === void 0 || typeof e.peer.name == "string") && typeof e.sdp == "string";
27
+ }
28
+ function parsePairingPayload(e) {
29
+ if (!isRecord(e) || e.v !== 1) throw new MeshPairingError("Unsupported pairing payload version");
30
+ if (isInvitation(e) || isAnswer(e)) return e;
31
+ throw new MeshPairingError("Unrecognized pairing payload shape");
32
+ }
33
+ var o = {
34
+ decode(t) {
35
+ let n;
36
+ try {
37
+ n = JSON.parse(e(t.trim()));
38
+ } catch (e) {
39
+ throw new MeshPairingError("Malformed pairing payload", { cause: e });
40
+ }
41
+ return parsePairingPayload(n);
42
+ },
43
+ encode(e) {
44
+ return i(JSON.stringify(e));
45
+ }
46
+ };
47
+ //#endregion
48
+ //#region src/_random.ts
49
+ function randomBytes(e, t) {
50
+ let r = new Uint8Array(e);
51
+ if (t) for (let i = 0; i < e; i++) r[i] = Math.floor(n(t) * 256);
52
+ else crypto.getRandomValues(r);
53
+ return r;
54
+ }
55
+ function randomId(e) {
56
+ return t(randomBytes(16, e));
57
+ }
58
+ //#endregion
59
+ //#region src/_wire.ts
60
+ function toEnvelope(e) {
61
+ if (typeof e != "object" || !e) return null;
62
+ let t = e;
63
+ return t.v !== 1 || typeof t.id != "string" || typeof t.t != "string" || typeof t.ts != "number" ? null : e;
64
+ }
65
+ var MessageDedupe = class {
66
+ seen = /* @__PURE__ */ new Set();
67
+ order = [];
68
+ capacity;
69
+ constructor(e = 256) {
70
+ this.capacity = e;
71
+ }
72
+ check(e) {
73
+ return !this.seen.has(e) && (this.seen.add(e), this.order.push(e), this.order.length > this.capacity && this.seen.delete(this.order.shift()), !0);
74
+ }
75
+ };
76
+ //#endregion
77
+ //#region src/_node.ts
78
+ function createNodeCore(e) {
79
+ let t = r(), n = new AbortController(), i = {
80
+ disposalCtrl: n,
81
+ disposed: !1,
82
+ emitTap(e) {
83
+ t.emit(e);
84
+ },
85
+ ensureLive() {
86
+ if (i.disposed) throw new MeshDisposedError();
87
+ },
88
+ markDisposed() {
89
+ i.disposed || (i.disposed = !0, i.status = "disposed", i.emitTap({
90
+ status: "disposed",
91
+ type: "status-change"
92
+ }), i.emitTap({ type: "dispose" }), t.clear(), n.abort());
93
+ },
94
+ setStatus(e) {
95
+ i.disposed || i.status === e || (i.status = e, i.emitTap({
96
+ status: e,
97
+ type: "status-change"
98
+ }));
99
+ },
100
+ status: "idle",
101
+ tap(e, n) {
102
+ return i.disposed ? () => {} : t.tap(e, n);
103
+ }
104
+ };
105
+ return e && (e.aborted ? i.markDisposed() : e.addEventListener("abort", () => i.markDisposed(), { once: !0 })), i;
106
+ }
107
+ function createPeerRecord(e, t, n) {
108
+ let r = {
109
+ dc: null,
110
+ dedupe: new MessageDedupe(),
111
+ id: e,
112
+ name: t,
113
+ pc: null,
114
+ publicPeer: {
115
+ get id() {
116
+ return r.id;
117
+ },
118
+ get name() {
119
+ return r.name;
120
+ },
121
+ get role() {
122
+ return r.role;
123
+ },
124
+ get status() {
125
+ return r.status;
126
+ }
127
+ },
128
+ role: n,
129
+ status: "connecting"
130
+ };
131
+ return r;
132
+ }
133
+ function createMessenger(e) {
134
+ let t = /* @__PURE__ */ new Map();
135
+ function reject(t, n) {
136
+ e.emitTap({
137
+ peerId: t,
138
+ reason: n,
139
+ type: "message-rejected"
140
+ });
141
+ }
142
+ return {
143
+ clear() {
144
+ t.clear();
145
+ },
146
+ handleInbound(n, r) {
147
+ let i = n.id;
148
+ if (typeof r != "string") {
149
+ reject(i, "malformed");
150
+ return;
151
+ }
152
+ let o = a(r);
153
+ if (o > e.maxMessageBytes) {
154
+ reject(i, "too-large");
155
+ return;
156
+ }
157
+ let s;
158
+ try {
159
+ s = JSON.parse(r);
160
+ } catch {
161
+ reject(i, "malformed");
162
+ return;
163
+ }
164
+ let c = toEnvelope(s);
165
+ if (!c) {
166
+ reject(i, "malformed");
167
+ return;
168
+ }
169
+ if (!n.dedupe.check(c.id)) {
170
+ reject(i, "duplicate");
171
+ return;
172
+ }
173
+ let l = t.get(c.t);
174
+ if (!l || l.size === 0) {
175
+ reject(i, "unknown-type");
176
+ return;
177
+ }
178
+ e.emitTap({
179
+ bytes: o,
180
+ messageType: c.t,
181
+ peerId: i,
182
+ type: "message-received"
183
+ });
184
+ let u = {
185
+ messageId: c.id,
186
+ payload: c.p,
187
+ peerId: i,
188
+ sentAt: c.ts
189
+ };
190
+ for (let t of l) try {
191
+ t(u);
192
+ } catch (t) {
193
+ e.emitTap({
194
+ error: new MeshError("Message handler threw", { cause: t }),
195
+ type: "error"
196
+ });
197
+ }
198
+ },
199
+ on(e, n) {
200
+ let r = t.get(e);
201
+ return r || (r = /* @__PURE__ */ new Set(), t.set(e, r)), r.add(n), () => {
202
+ r.delete(n), r.size === 0 && t.delete(e);
203
+ };
204
+ },
205
+ sendTo(t, n, r) {
206
+ let i = t.dc;
207
+ if (!i || i.readyState !== "open" || t.status !== "connected") throw new MeshConnectionError(`Peer "${t.id}" is not connected`, t.id);
208
+ let o = {
209
+ id: randomId(e.random),
210
+ p: r,
211
+ t: n,
212
+ ts: e.clock(),
213
+ v: 1
214
+ }, s;
215
+ try {
216
+ s = JSON.stringify(o);
217
+ } catch (e) {
218
+ throw new MeshPayloadError(`Failed to serialize message "${n}"`, { cause: e });
219
+ }
220
+ let c = a(s);
221
+ if (c > e.maxMessageBytes) throw new MeshPayloadError(`Message "${n}" is ${c} bytes (max ${e.maxMessageBytes})`);
222
+ try {
223
+ i.send(s);
224
+ } catch (e) {
225
+ throw new MeshConnectionError(`Failed to send message "${n}"`, t.id, { cause: e });
226
+ }
227
+ e.emitTap({
228
+ bytes: c,
229
+ messageType: n,
230
+ peerId: t.id,
231
+ type: "message-sent"
232
+ });
233
+ }
234
+ };
235
+ }
236
+ //#endregion
237
+ //#region src/_sdp.ts
238
+ function isCompactSdp(e) {
239
+ if (typeof e != "object" || !e) return !1;
240
+ let t = e;
241
+ return typeof t.u == "string" && typeof t.p == "string" && typeof t.f == "string" && typeof t.s == "string" && typeof t.m == "string" && Array.isArray(t.c) && t.c.every((e) => typeof e == "string");
242
+ }
243
+ function restoreSdp(e) {
244
+ if (typeof e == "string") return e;
245
+ if (isCompactSdp(e)) return expandSdp(e);
246
+ throw new MeshPairingError("Malformed pairing payload");
247
+ }
248
+ function compactSdp(e) {
249
+ let t = "", n = "", r = "", i = "", a = "0", o, s, c = [], l = 0;
250
+ for (let u of e.split(/\r?\n/)) u.startsWith("m=") ? l += 1 : u.startsWith("a=ice-ufrag:") ? t = u.slice(12) : u.startsWith("a=ice-pwd:") ? n = u.slice(10) : u.startsWith("a=fingerprint:sha-256 ") ? r = u.slice(22).replaceAll(":", "").toUpperCase() : u.startsWith("a=setup:") ? i = u.slice(8) : u.startsWith("a=mid:") ? a = u.slice(6) : u.startsWith("a=sctp-port:") ? o = Number(u.slice(12)) : u.startsWith("a=max-message-size:") ? s = Number(u.slice(19)) : u.startsWith("a=candidate:") && c.push(u.slice(12));
251
+ return l !== 1 || !t || !n || !r ? null : {
252
+ c,
253
+ f: r,
254
+ m: a,
255
+ p: n,
256
+ s: i || "actpass",
257
+ ...s ? { mm: s } : {},
258
+ ...o ? { sp: o } : {},
259
+ u: t
260
+ };
261
+ }
262
+ function canonicalSdp(e) {
263
+ let t = compactSdp(e);
264
+ return t ? JSON.stringify(t) : e;
265
+ }
266
+ function expandSdp(e) {
267
+ let t = e.f.replace(/(..)/g, "$1:").slice(0, -1);
268
+ return `${[
269
+ "v=0",
270
+ "o=- 0 2 IN IP4 0.0.0.0",
271
+ "s=-",
272
+ "t=0 0",
273
+ `a=group:BUNDLE ${e.m}`,
274
+ "m=application 9 UDP/DTLS/SCTP webrtc-datachannel",
275
+ "c=IN IP4 0.0.0.0",
276
+ `a=ice-ufrag:${e.u}`,
277
+ `a=ice-pwd:${e.p}`,
278
+ `a=fingerprint:sha-256 ${t}`,
279
+ `a=setup:${e.s}`,
280
+ `a=mid:${e.m}`,
281
+ `a=sctp-port:${e.sp ?? 5e3}`,
282
+ ...e.mm ? [`a=max-message-size:${e.mm}`] : [],
283
+ ...e.c.map((e) => `a=candidate:${e}`),
284
+ ""
285
+ ].join("\r\n")}`;
286
+ }
287
+ //#endregion
288
+ //#region src/_proof.ts
289
+ async function createProof(e, n) {
290
+ let r = globalThis.crypto?.subtle;
291
+ if (!r) throw new MeshUnsupportedError("WebCrypto is unavailable: pairing requires a secure context");
292
+ let i = new TextEncoder(), a = await r.importKey("raw", i.encode(e), {
293
+ hash: "SHA-256",
294
+ name: "HMAC"
295
+ }, !1, ["sign"]), o = await r.sign("HMAC", a, i.encode(canonicalSdp(n)));
296
+ return t(new Uint8Array(o));
297
+ }
298
+ //#endregion
299
+ //#region src/_rtc.ts
300
+ var s = { createPeerConnection(e) {
301
+ let t = globalThis.RTCPeerConnection;
302
+ if (!t) throw new MeshUnsupportedError("WebRTC is unavailable in this environment: inject an `rtc` factory");
303
+ return new t(e);
304
+ } };
305
+ function normalizeSdp(e) {
306
+ let t = e.replace(/\r?\n/g, "\r\n");
307
+ return t.endsWith("\r\n") ? t : `${t}\r\n`;
308
+ }
309
+ function waitIceGathering(e, t) {
310
+ return e.iceGatheringState === "complete" ? Promise.resolve() : new Promise((n) => {
311
+ let cleanup = () => {
312
+ clearTimeout(r), e.removeEventListener("icegatheringstatechange", onChange);
313
+ }, onChange = () => {
314
+ e.iceGatheringState === "complete" && (cleanup(), n());
315
+ }, r = setTimeout(() => {
316
+ cleanup(), n();
317
+ }, t);
318
+ e.addEventListener("icegatheringstatechange", onChange);
319
+ });
320
+ }
321
+ function waitChannelOpen(e, t, n, r, i) {
322
+ return e.readyState === "open" ? Promise.resolve() : new Promise((a, o) => {
323
+ let s = null, timeout = () => new MeshTimeoutError(`Timed out waiting ${n}ms for the data channel to open`), fail = (e) => {
324
+ cleanup(), o(e);
325
+ }, cleanup = () => {
326
+ s && clearTimeout(s), s = null, e.removeEventListener("open", onOpen), e.removeEventListener("close", onClose), t.removeEventListener("iceconnectionstatechange", onIce), i.removeEventListener("abort", onDispose);
327
+ }, onOpen = () => {
328
+ cleanup(), a();
329
+ }, onClose = () => fail(timeout()), onIce = () => {
330
+ let e = t.iceConnectionState;
331
+ (e === "failed" || e === "closed" || e === "disconnected") && fail(new MeshConnectionError(`ICE connection ${e} before channel open`, r));
332
+ }, onDispose = () => fail(new MeshDisposedError());
333
+ s = setTimeout(() => fail(timeout()), n), e.addEventListener("open", onOpen), e.addEventListener("close", onClose), t.addEventListener("iceconnectionstatechange", onIce), i.addEventListener("abort", onDispose, { once: !0 });
334
+ });
335
+ }
336
+ //#endregion
337
+ //#region src/guest.ts
338
+ function createMeshGuest(e = {}) {
339
+ let t = e.rtc ?? s, n = e.clock ?? (() => Date.now()), r = e.maxMessageBytes ?? 65536, i = e.channelOpenTimeoutMs ?? 6e4, a = e.iceGatheringTimeoutMs ?? 5e3, o = createNodeCore(e.signal), c = createMessenger({
340
+ clock: n,
341
+ emitTap: o.emitTap,
342
+ maxMessageBytes: r,
343
+ random: e.random
344
+ }), l = null, u = null, openTimeoutError = () => new MeshTimeoutError(`Timed out waiting ${i}ms for the data channel to open`);
345
+ function failPeer(e, t) {
346
+ o.disposed || e !== l || e.status === "failed" || (e.status = "failed", u &&= (clearTimeout(u), null), setTimeout(() => {
347
+ e.dc?.close(), e.pc?.close();
348
+ }, 0), o.emitTap({
349
+ peerId: e.id,
350
+ status: "failed",
351
+ type: "peer-status-change"
352
+ }), o.emitTap({
353
+ error: t,
354
+ type: "error"
355
+ }), o.emitTap({
356
+ peer: e.publicPeer,
357
+ reason: t.message,
358
+ type: "peer-left"
359
+ }), o.setStatus("failed"));
360
+ }
361
+ function disconnectPeer(e, t) {
362
+ o.disposed || e !== l || e.status !== "disconnected" && e.status !== "failed" && (e.status = "disconnected", e.pc?.close(), o.emitTap({
363
+ peerId: e.id,
364
+ status: "disconnected",
365
+ type: "peer-status-change"
366
+ }), o.emitTap({
367
+ peer: e.publicPeer,
368
+ reason: t,
369
+ type: "peer-left"
370
+ }), o.setStatus("disconnected"));
371
+ }
372
+ function wireChannel(e, t, n) {
373
+ e.dc = n, n.addEventListener("open", () => {
374
+ o.disposed || (u &&= (clearTimeout(u), null), e.status = "connected", o.emitTap({
375
+ peerId: e.id,
376
+ status: "connected",
377
+ type: "peer-status-change"
378
+ }), o.emitTap({
379
+ peer: e.publicPeer,
380
+ type: "peer-joined"
381
+ }), o.setStatus("connected"));
382
+ }), n.addEventListener("message", (t) => {
383
+ o.disposed || c.handleInbound(e, t.data);
384
+ }), n.addEventListener("close", () => {
385
+ o.disposed || (e.status === "connecting" ? failPeer(e, u ? openTimeoutError() : new MeshConnectionError("channel closed before opening", e.id)) : disconnectPeer(e, "channel closed"));
386
+ }), n.addEventListener("error", () => {
387
+ o.disposed || failPeer(e, new MeshConnectionError("channel error", e.id));
388
+ }), t.addEventListener("iceconnectionstatechange", () => {
389
+ if (o.disposed) return;
390
+ let n = t.iceConnectionState;
391
+ n === "failed" || n === "closed" ? failPeer(e, new MeshConnectionError(`ice ${n}`, e.id)) : n === "disconnected" && e.status === "connected" && disconnectPeer(e, "ice disconnected");
392
+ });
393
+ }
394
+ return {
395
+ async acceptInvitation(r, s) {
396
+ if (o.ensureLive(), l && l.status !== "failed" && l.status !== "disconnected") throw new MeshPairingError("Guest is already paired");
397
+ if (r?.v !== 1) throw new MeshPairingError("Unsupported invitation version");
398
+ if (n() >= r.expiresAt) throw new MeshPairingError("Invitation expired");
399
+ l &&= (u &&= (clearTimeout(u), null), l.pc?.close(), null), o.setStatus("pairing");
400
+ let c;
401
+ try {
402
+ c = t.createPeerConnection({ iceServers: [...e.iceServers ?? []] });
403
+ } catch (e) {
404
+ throw o.setStatus("idle"), e;
405
+ }
406
+ let d = createPeerRecord(r.sessionId, r.hostName, "host");
407
+ d.pc = c, l = d, c.addEventListener("datachannel", (e) => {
408
+ wireChannel(d, c, e.channel);
409
+ }), c.addEventListener("iceconnectionstatechange", () => {
410
+ o.disposed || o.emitTap({
411
+ peerId: d.id,
412
+ state: c.iceConnectionState,
413
+ type: "ice-state"
414
+ });
415
+ });
416
+ try {
417
+ await c.setRemoteDescription({
418
+ sdp: normalizeSdp(r.sdp),
419
+ type: "offer"
420
+ });
421
+ } catch (e) {
422
+ throw l = null, o.setStatus("idle"), new MeshPairingError("Invalid invitation SDP", { cause: e });
423
+ }
424
+ let f;
425
+ try {
426
+ await c.setLocalDescription(await c.createAnswer()), await waitIceGathering(c, a), f = c.localDescription?.sdp ?? "";
427
+ } catch (e) {
428
+ throw l = null, c.close(), o.setStatus("idle"), new MeshConnectionError("Failed to create answer", null, { cause: e });
429
+ }
430
+ if (o.disposed) throw c.close(), new MeshDisposedError();
431
+ if (!f) throw l = null, c.close(), o.setStatus("idle"), new MeshConnectionError("Local description has no SDP");
432
+ let p = randomId(e.random), m;
433
+ try {
434
+ m = await createProof(r.secret, f);
435
+ } catch (e) {
436
+ throw l = null, c.close(), o.setStatus("idle"), e;
437
+ }
438
+ return o.setStatus("connecting"), u = setTimeout(() => {
439
+ l?.status === "connecting" && failPeer(l, openTimeoutError());
440
+ }, i), {
441
+ peer: s?.name === void 0 ? { id: p } : {
442
+ id: p,
443
+ name: s.name
444
+ },
445
+ proof: m,
446
+ sdp: f,
447
+ sessionId: r.sessionId,
448
+ v: 1
449
+ };
450
+ },
451
+ get disposalSignal() {
452
+ return o.disposalCtrl.signal;
453
+ },
454
+ dispose() {
455
+ o.disposed || (o.markDisposed(), u &&= (clearTimeout(u), null), l && (l.dc?.close(), l.pc?.close()), c.clear());
456
+ },
457
+ get disposed() {
458
+ return o.disposed;
459
+ },
460
+ get host() {
461
+ return l?.publicPeer ?? null;
462
+ },
463
+ on(e, t) {
464
+ return o.ensureLive(), c.on(e, t);
465
+ },
466
+ send(e, t) {
467
+ if (o.ensureLive(), !l) throw new MeshConnectionError("Guest is not paired");
468
+ c.sendTo(l, e, t);
469
+ },
470
+ get status() {
471
+ return o.status;
472
+ },
473
+ tap(e, t) {
474
+ return o.tap(e, t);
475
+ },
476
+ [Symbol.dispose]() {
477
+ this.dispose();
478
+ }
479
+ };
480
+ }
481
+ //#endregion
482
+ //#region src/host.ts
483
+ function createMeshHost(e = {}) {
484
+ let n = e.rtc ?? s, r = e.clock ?? (() => Date.now()), i = e.maxMessageBytes ?? 65536, a = e.invitationTtlMs ?? 3e5, o = e.channelOpenTimeoutMs ?? 6e4, c = e.iceGatheringTimeoutMs ?? 5e3, l = {
485
+ label: "mesh",
486
+ ordered: !0,
487
+ ...e.channel
488
+ }, u = createNodeCore(e.signal), d = createMessenger({
489
+ clock: r,
490
+ emitTap: u.emitTap,
491
+ maxMessageBytes: i,
492
+ random: e.random
493
+ }), f = /* @__PURE__ */ new Map(), p = /* @__PURE__ */ new Map(), m = !1, h = !1;
494
+ function recomputeStatus() {
495
+ let e = p.size > 0 ? "pairing" : m ? "disconnected" : h ? "failed" : "idle";
496
+ for (let t of f.values()) {
497
+ if (t.status === "connected") {
498
+ e = "connected";
499
+ break;
500
+ }
501
+ t.status === "connecting" && (e = "connecting");
502
+ }
503
+ u.setStatus(e);
504
+ }
505
+ function expireInvitation(e) {
506
+ let t = p.get(e);
507
+ t && (p.delete(e), clearTimeout(t.expiryTimer), t.pc.close(), u.emitTap({
508
+ sessionId: e,
509
+ type: "invitation-expired"
510
+ }), recomputeStatus());
511
+ }
512
+ function setPeerStatus(e, t) {
513
+ e.status !== t && (e.status = t, u.emitTap({
514
+ peerId: e.id,
515
+ status: t,
516
+ type: "peer-status-change"
517
+ }), recomputeStatus());
518
+ }
519
+ function leavePeer(e, t, n = !1) {
520
+ f.has(e.id) && (f.delete(e.id), e.status = n ? "failed" : "disconnected", e.pc?.close(), e.dc?.close(), u.emitTap({
521
+ peerId: e.id,
522
+ status: e.status,
523
+ type: "peer-status-change"
524
+ }), u.emitTap({
525
+ peer: e.publicPeer,
526
+ reason: t,
527
+ type: "peer-left"
528
+ }), n || (m = !0), h ||= n, recomputeStatus());
529
+ }
530
+ function wirePeerTransport(e, t, n) {
531
+ e.pc = t, e.dc = n, n.addEventListener("message", (t) => {
532
+ u.disposed || d.handleInbound(e, t.data);
533
+ }), n.addEventListener("close", () => {
534
+ u.disposed || leavePeer(e, "channel closed");
535
+ }), n.addEventListener("error", () => {
536
+ u.disposed || leavePeer(e, "channel error", !0);
537
+ }), t.addEventListener("iceconnectionstatechange", () => {
538
+ if (u.disposed) return;
539
+ let n = t.iceConnectionState;
540
+ u.emitTap({
541
+ peerId: e.id,
542
+ state: n,
543
+ type: "ice-state"
544
+ }), n === "failed" || n === "closed" ? leavePeer(e, `ice ${n}`, n === "failed") : n === "disconnected" && e.status === "connected" && setPeerStatus(e, "disconnected");
545
+ });
546
+ }
547
+ function markPeerConnected(e) {
548
+ m = !0, f.set(e.id, e), setPeerStatus(e, "connected"), u.emitTap({
549
+ peer: e,
550
+ type: "peer-joined"
551
+ });
552
+ }
553
+ return {
554
+ async acceptAnswer(t) {
555
+ if (u.ensureLive(), t?.v !== 1) throw new MeshPairingError("Unsupported answer version");
556
+ if (!t.peer?.id) throw new MeshPairingError("Answer is missing a peer id");
557
+ let n = p.get(t.sessionId);
558
+ if (!n) throw new MeshPairingError("Unknown or expired session");
559
+ if (n.answered) throw new MeshPairingError("Invitation already answered");
560
+ if (f.has(t.peer.id)) throw new MeshPairingError(`Peer "${t.peer.id}" is already connected`);
561
+ if (r() >= n.expiresAt) throw expireInvitation(t.sessionId), new MeshPairingError("Invitation expired");
562
+ let i = await createProof(n.secret, t.sdp);
563
+ if (t.proof !== i) throw new MeshPairingError("Proof mismatch");
564
+ if (!(await e.approvePeer?.(t.peer) ?? !0)) throw u.emitTap({
565
+ peerId: t.peer.id,
566
+ type: "peer-rejected"
567
+ }), new MeshPairingError(`Peer "${t.peer.id}" rejected by host`);
568
+ if (u.emitTap({
569
+ peerId: t.peer.id,
570
+ type: "peer-approved"
571
+ }), f.has(t.peer.id)) throw new MeshPairingError(`Peer "${t.peer.id}" is already connected`);
572
+ n.answered = !0, p.delete(t.sessionId), clearTimeout(n.expiryTimer);
573
+ let a = createPeerRecord(t.peer.id, t.peer.name, "guest"), s = waitChannelOpen(n.dc, n.pc, o, a.id, u.disposalCtrl.signal);
574
+ wirePeerTransport(a, n.pc, n.dc), f.set(a.id, a), recomputeStatus();
575
+ try {
576
+ await n.pc.setRemoteDescription({
577
+ sdp: normalizeSdp(t.sdp),
578
+ type: "answer"
579
+ });
580
+ } catch (e) {
581
+ throw leavePeer(a, "invalid answer sdp", !0), new MeshPairingError("Invalid answer SDP", { cause: e });
582
+ }
583
+ try {
584
+ await s;
585
+ } catch (e) {
586
+ let t = !(e instanceof MeshDisposedError);
587
+ throw leavePeer(a, e instanceof Error ? e.message : "open failed", t), e;
588
+ }
589
+ return markPeerConnected(a), a.publicPeer;
590
+ },
591
+ broadcast(e, t, n) {
592
+ u.ensureLive();
593
+ for (let r of f.values()) r.status !== "connected" || n?.except?.includes(r.id) || d.sendTo(r, e, t);
594
+ },
595
+ async createInvitation(i) {
596
+ u.ensureLive();
597
+ let o = n.createPeerConnection({ iceServers: [...e.iceServers ?? []] }), s = o.createDataChannel(l.label, {
598
+ maxRetransmits: l.maxRetransmits,
599
+ ordered: l.ordered
600
+ }), d = randomId(e.random), f = t(randomBytes(32, e.random)), m = r() + a;
601
+ try {
602
+ await o.setLocalDescription(await o.createOffer()), await waitIceGathering(o, c);
603
+ } catch (e) {
604
+ throw o.close(), new MeshConnectionError("Failed to create offer", null, { cause: e });
605
+ }
606
+ if (u.disposed) throw o.close(), new MeshDisposedError();
607
+ let h = o.localDescription?.sdp;
608
+ if (!h) throw o.close(), new MeshConnectionError("Local description has no SDP");
609
+ let g = setTimeout(() => expireInvitation(d), a);
610
+ return p.set(d, {
611
+ answered: !1,
612
+ dc: s,
613
+ expiresAt: m,
614
+ expiryTimer: g,
615
+ pc: o,
616
+ secret: f,
617
+ sessionId: d
618
+ }), u.emitTap({
619
+ sessionId: d,
620
+ type: "invitation-created"
621
+ }), recomputeStatus(), {
622
+ expiresAt: m,
623
+ sdp: h,
624
+ secret: f,
625
+ sessionId: d,
626
+ v: 1,
627
+ ...i?.name === void 0 ? {} : { hostName: i.name }
628
+ };
629
+ },
630
+ get disposalSignal() {
631
+ return u.disposalCtrl.signal;
632
+ },
633
+ dispose() {
634
+ if (!u.disposed) {
635
+ u.markDisposed();
636
+ for (let e of p.values()) clearTimeout(e.expiryTimer), e.pc.close();
637
+ p.clear();
638
+ for (let e of f.values()) e.dc?.close(), e.pc?.close();
639
+ f.clear(), d.clear();
640
+ }
641
+ },
642
+ get disposed() {
643
+ return u.disposed;
644
+ },
645
+ kick(e, t) {
646
+ u.ensureLive();
647
+ let n = f.get(e);
648
+ if (!n) throw new MeshConnectionError(`Unknown peer "${e}"`, e);
649
+ leavePeer(n, t ?? "kicked");
650
+ },
651
+ on(e, t) {
652
+ return u.ensureLive(), d.on(e, t);
653
+ },
654
+ get peers() {
655
+ return f;
656
+ },
657
+ send(e, t, n) {
658
+ u.ensureLive();
659
+ let r = f.get(e);
660
+ if (!r) throw new MeshConnectionError(`Unknown peer "${e}"`, e);
661
+ d.sendTo(r, t, n);
662
+ },
663
+ get status() {
664
+ return u.status;
665
+ },
666
+ tap(e, t) {
667
+ return u.tap(e, t);
668
+ },
669
+ [Symbol.dispose]() {
670
+ this.dispose();
671
+ }
672
+ };
673
+ }
674
+ //#endregion
675
+ //#region src/_base45.ts
676
+ var c = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:", l = new Map([...c].map((e, t) => [e, t]));
677
+ function bytesToBase45(e) {
678
+ let t = "";
679
+ for (let n = 0; n + 1 < e.length; n += 2) {
680
+ let r = e[n] * 256 + e[n + 1];
681
+ t += c[r % 45] + c[Math.floor(r / 45) % 45] + c[Math.floor(r / 2025)];
682
+ }
683
+ if (e.length % 2) {
684
+ let n = e[e.length - 1];
685
+ t += c[n % 45] + c[Math.floor(n / 45)];
686
+ }
687
+ return t;
688
+ }
689
+ function base45ToBytes(e) {
690
+ let t = [], n = 0;
691
+ for (; n + 2 < e.length;) {
692
+ let r = digit(e, n) + 45 * digit(e, n + 1) + 2025 * digit(e, n + 2);
693
+ if (r > 65535) throw new MeshPairingError("Malformed base45 payload");
694
+ t.push(r >> 8, r & 255), n += 3;
695
+ }
696
+ if (e.length - n === 2) {
697
+ let r = digit(e, n) + 45 * digit(e, n + 1);
698
+ if (r > 255) throw new MeshPairingError("Malformed base45 payload");
699
+ t.push(r);
700
+ } else if (e.length - n === 1) throw new MeshPairingError("Malformed base45 payload");
701
+ return new Uint8Array(t);
702
+ }
703
+ function digit(e, t) {
704
+ let n = l.get(e[t]);
705
+ if (n === void 0) throw new MeshPairingError("Malformed base45 payload");
706
+ return n;
707
+ }
708
+ //#endregion
709
+ //#region src/qr-codec.ts
710
+ var u = "mq2.", d = new TextEncoder(), f = new TextDecoder();
711
+ function decompressionStream(e) {
712
+ if (typeof globalThis.DecompressionStream != "function") throw new MeshUnsupportedError("DecompressionStream is not available in this environment");
713
+ return new globalThis.DecompressionStream(e);
714
+ }
715
+ async function pump(e, t) {
716
+ let n = t.writable.getWriter(), r = n.write(e).then(() => n.close()).catch(() => {}), i = [], a = t.readable.getReader();
717
+ for (;;) {
718
+ let { done: e, value: t } = await a.read();
719
+ if (e) break;
720
+ i.push(t);
721
+ }
722
+ await r;
723
+ let o = new Uint8Array(i.reduce((e, t) => e + t.length, 0)), s = 0;
724
+ for (let e of i) o.set(e, s), s += e.length;
725
+ return o;
726
+ }
727
+ var p = {
728
+ async decode(e) {
729
+ let t = e.trim();
730
+ if (t.startsWith(u)) {
731
+ let e;
732
+ try {
733
+ let n = base45ToBytes(t.slice(4));
734
+ e = JSON.parse(f.decode(await pump(n, decompressionStream("deflate-raw"))));
735
+ } catch (e) {
736
+ throw e instanceof MeshUnsupportedError || e instanceof MeshPairingError ? e : new MeshPairingError("Malformed pairing payload", { cause: e });
737
+ }
738
+ return isRecord(e) && (e = {
739
+ ...e,
740
+ sdp: restoreSdp(e.sdp)
741
+ }), parsePairingPayload(e);
742
+ }
743
+ return o.decode(t);
744
+ },
745
+ async encode(e) {
746
+ if (typeof globalThis.CompressionStream != "function") return o.encode(e);
747
+ let t = compactSdp(e.sdp);
748
+ return u + bytesToBase45(await pump(d.encode(JSON.stringify(t ? {
749
+ ...e,
750
+ sdp: t
751
+ } : e)), new globalThis.CompressionStream("deflate-raw")));
752
+ }
753
+ };
754
+ //#endregion
755
+ export { MeshConnectionError, MeshDisposedError, MeshError, MeshPairingError, MeshPayloadError, MeshTimeoutError, MeshUnsupportedError, createMeshGuest, createMeshHost, o as meshCodec, p as meshQrCodec };
756
+
5
757
  //# sourceMappingURL=mesh.js.map