@vielzeug/mesh 26.10.0 → 26.10.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 CHANGED
@@ -36,7 +36,7 @@ host.dispose();
36
36
  guest.dispose();
37
37
  ```
38
38
 
39
- Pairing payloads travel out-of-band — copy/paste, `navigator.share`, or any channel you control. `RTCPeerConnection` requires a secure context (`https:` or `localhost`).
39
+ Pairing payloads travel out-of-band: copy/paste, `navigator.share`, or any channel you control. `RTCPeerConnection` requires a secure context (`https:` or `localhost`).
40
40
 
41
41
  ## Documentation
42
42
 
@@ -47,4 +47,4 @@ Pairing payloads travel out-of-band — copy/paste, `navigator.share`, or any ch
47
47
 
48
48
  ## License
49
49
 
50
- MIT © [Helmuth Saatkamp](https://github.com/helmuthdu) — part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) monorepo.
50
+ MIT © [Helmuth Saatkamp](https://github.com/helmuthdu): part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) monorepo.
@@ -1 +1 @@
1
- {"version":3,"file":"_base45.cjs","names":[],"sources":["../src/_base45.ts"],"sourcesContent":["import { MeshPairingError } from './errors';\n\n/**\n * Base45 (RFC 9285): two bytes → three chars, one leftover byte → two chars.\n * The alphabet sits inside the QR alphanumeric charset, so QR encoders can use\n * alphanumeric mode (~5.5 bits/char) instead of byte mode — about a third\n * denser than base64 for the same payload.\n */\n\nconst ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';\nconst INDEX = new Map([...ALPHABET].map((ch, i) => [ch, i]));\n\nexport function bytesToBase45(bytes: Uint8Array): string {\n let out = '';\n for (let i = 0; i + 1 < bytes.length; i += 2) {\n const value = bytes[i] * 256 + bytes[i + 1];\n out += ALPHABET[value % 45] + ALPHABET[Math.floor(value / 45) % 45] + ALPHABET[Math.floor(value / 2025)];\n }\n if (bytes.length % 2) {\n const value = bytes[bytes.length - 1];\n out += ALPHABET[value % 45] + ALPHABET[Math.floor(value / 45)];\n }\n return out;\n}\n\nexport function base45ToBytes(text: string): Uint8Array {\n const out: number[] = [];\n let i = 0;\n while (i + 2 < text.length) {\n const value = digit(text, i) + 45 * digit(text, i + 1) + 2025 * digit(text, i + 2);\n if (value > 0xffff) throw new MeshPairingError('Malformed base45 payload');\n out.push(value >> 8, value & 0xff);\n i += 3;\n }\n if (text.length - i === 2) {\n const value = digit(text, i) + 45 * digit(text, i + 1);\n if (value > 0xff) throw new MeshPairingError('Malformed base45 payload');\n out.push(value);\n } else if (text.length - i === 1) {\n throw new MeshPairingError('Malformed base45 payload');\n }\n return new Uint8Array(out);\n}\n\nfunction digit(text: string, at: number): number {\n const value = INDEX.get(text[at]);\n if (value === undefined) throw new MeshPairingError('Malformed base45 payload');\n return value;\n}\n"],"mappings":"gCASA,IAAM,EAAW,gDACX,EAAQ,IAAI,IAAI,CAAC,GAAG,CAAQ,CAAC,CAAC,KAAK,EAAI,IAAM,CAAC,EAAI,CAAC,CAAC,CAAC,EAE3D,SAAgB,cAAc,EAA2B,CACvD,IAAI,EAAM,GACV,IAAK,IAAI,EAAI,EAAG,EAAI,EAAI,EAAM,OAAQ,GAAK,EAAG,CAC5C,IAAM,EAAQ,EAAM,GAAK,IAAM,EAAM,EAAI,GACzC,GAAO,EAAS,EAAQ,IAAM,EAAS,KAAK,MAAM,EAAQ,EAAE,EAAI,IAAM,EAAS,KAAK,MAAM,EAAQ,IAAI,EACxG,CACA,GAAI,EAAM,OAAS,EAAG,CACpB,IAAM,EAAQ,EAAM,EAAM,OAAS,GACnC,GAAO,EAAS,EAAQ,IAAM,EAAS,KAAK,MAAM,EAAQ,EAAE,EAC9D,CACA,OAAO,CACT,CAEA,SAAgB,cAAc,EAA0B,CACtD,IAAM,EAAgB,CAAC,EACnB,EAAI,EACR,KAAO,EAAI,EAAI,EAAK,QAAQ,CAC1B,IAAM,EAAQ,MAAM,EAAM,CAAC,EAAI,GAAK,MAAM,EAAM,EAAI,CAAC,EAAI,KAAO,MAAM,EAAM,EAAI,CAAC,EACjF,GAAI,EAAQ,MAAQ,MAAM,IAAI,EAAA,iBAAiB,0BAA0B,EACzE,EAAI,KAAK,GAAS,EAAG,EAAQ,GAAI,EACjC,GAAK,CACP,CACA,GAAI,EAAK,OAAS,IAAM,EAAG,CACzB,IAAM,EAAQ,MAAM,EAAM,CAAC,EAAI,GAAK,MAAM,EAAM,EAAI,CAAC,EACrD,GAAI,EAAQ,IAAM,MAAM,IAAI,EAAA,iBAAiB,0BAA0B,EACvE,EAAI,KAAK,CAAK,CAChB,MAAO,GAAI,EAAK,OAAS,IAAM,EAC7B,MAAM,IAAI,EAAA,iBAAiB,0BAA0B,EAEvD,OAAO,IAAI,WAAW,CAAG,CAC3B,CAEA,SAAS,MAAM,EAAc,EAAoB,CAC/C,IAAM,EAAQ,EAAM,IAAI,EAAK,EAAG,EAChC,GAAI,IAAU,IAAA,GAAW,MAAM,IAAI,EAAA,iBAAiB,0BAA0B,EAC9E,OAAO,CACT"}
1
+ {"version":3,"file":"_base45.cjs","names":[],"sources":["../src/_base45.ts"],"sourcesContent":["import { MeshPairingError } from './errors';\n\n/**\n * Base45 (RFC 9285): two bytes → three chars, one leftover byte → two chars.\n * The alphabet sits inside the QR alphanumeric charset, so QR encoders can use\n * alphanumeric mode (~5.5 bits/char) instead of byte mode: about a third\n * denser than base64 for the same payload.\n */\n\nconst ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';\nconst INDEX = new Map([...ALPHABET].map((ch, i) => [ch, i]));\n\nexport function bytesToBase45(bytes: Uint8Array): string {\n let out = '';\n for (let i = 0; i + 1 < bytes.length; i += 2) {\n const value = bytes[i] * 256 + bytes[i + 1];\n out += ALPHABET[value % 45] + ALPHABET[Math.floor(value / 45) % 45] + ALPHABET[Math.floor(value / 2025)];\n }\n if (bytes.length % 2) {\n const value = bytes[bytes.length - 1];\n out += ALPHABET[value % 45] + ALPHABET[Math.floor(value / 45)];\n }\n return out;\n}\n\nexport function base45ToBytes(text: string): Uint8Array {\n const out: number[] = [];\n let i = 0;\n while (i + 2 < text.length) {\n const value = digit(text, i) + 45 * digit(text, i + 1) + 2025 * digit(text, i + 2);\n if (value > 0xffff) throw new MeshPairingError('Malformed base45 payload');\n out.push(value >> 8, value & 0xff);\n i += 3;\n }\n if (text.length - i === 2) {\n const value = digit(text, i) + 45 * digit(text, i + 1);\n if (value > 0xff) throw new MeshPairingError('Malformed base45 payload');\n out.push(value);\n } else if (text.length - i === 1) {\n throw new MeshPairingError('Malformed base45 payload');\n }\n return new Uint8Array(out);\n}\n\nfunction digit(text: string, at: number): number {\n const value = INDEX.get(text[at]);\n if (value === undefined) throw new MeshPairingError('Malformed base45 payload');\n return value;\n}\n"],"mappings":"gCASA,IAAM,EAAW,gDACX,EAAQ,IAAI,IAAI,CAAC,GAAG,CAAQ,CAAC,CAAC,KAAK,EAAI,IAAM,CAAC,EAAI,CAAC,CAAC,CAAC,EAE3D,SAAgB,cAAc,EAA2B,CACvD,IAAI,EAAM,GACV,IAAK,IAAI,EAAI,EAAG,EAAI,EAAI,EAAM,OAAQ,GAAK,EAAG,CAC5C,IAAM,EAAQ,EAAM,GAAK,IAAM,EAAM,EAAI,GACzC,GAAO,EAAS,EAAQ,IAAM,EAAS,KAAK,MAAM,EAAQ,EAAE,EAAI,IAAM,EAAS,KAAK,MAAM,EAAQ,IAAI,EACxG,CACA,GAAI,EAAM,OAAS,EAAG,CACpB,IAAM,EAAQ,EAAM,EAAM,OAAS,GACnC,GAAO,EAAS,EAAQ,IAAM,EAAS,KAAK,MAAM,EAAQ,EAAE,EAC9D,CACA,OAAO,CACT,CAEA,SAAgB,cAAc,EAA0B,CACtD,IAAM,EAAgB,CAAC,EACnB,EAAI,EACR,KAAO,EAAI,EAAI,EAAK,QAAQ,CAC1B,IAAM,EAAQ,MAAM,EAAM,CAAC,EAAI,GAAK,MAAM,EAAM,EAAI,CAAC,EAAI,KAAO,MAAM,EAAM,EAAI,CAAC,EACjF,GAAI,EAAQ,MAAQ,MAAM,IAAI,EAAA,iBAAiB,0BAA0B,EACzE,EAAI,KAAK,GAAS,EAAG,EAAQ,GAAI,EACjC,GAAK,CACP,CACA,GAAI,EAAK,OAAS,IAAM,EAAG,CACzB,IAAM,EAAQ,MAAM,EAAM,CAAC,EAAI,GAAK,MAAM,EAAM,EAAI,CAAC,EACrD,GAAI,EAAQ,IAAM,MAAM,IAAI,EAAA,iBAAiB,0BAA0B,EACvE,EAAI,KAAK,CAAK,CAChB,MAAO,GAAI,EAAK,OAAS,IAAM,EAC7B,MAAM,IAAI,EAAA,iBAAiB,0BAA0B,EAEvD,OAAO,IAAI,WAAW,CAAG,CAC3B,CAEA,SAAS,MAAM,EAAc,EAAoB,CAC/C,IAAM,EAAQ,EAAM,IAAI,EAAK,EAAG,EAChC,GAAI,IAAU,IAAA,GAAW,MAAM,IAAI,EAAA,iBAAiB,0BAA0B,EAC9E,OAAO,CACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"_base45.js","names":[],"sources":["../src/_base45.ts"],"sourcesContent":["import { MeshPairingError } from './errors';\n\n/**\n * Base45 (RFC 9285): two bytes → three chars, one leftover byte → two chars.\n * The alphabet sits inside the QR alphanumeric charset, so QR encoders can use\n * alphanumeric mode (~5.5 bits/char) instead of byte mode — about a third\n * denser than base64 for the same payload.\n */\n\nconst ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';\nconst INDEX = new Map([...ALPHABET].map((ch, i) => [ch, i]));\n\nexport function bytesToBase45(bytes: Uint8Array): string {\n let out = '';\n for (let i = 0; i + 1 < bytes.length; i += 2) {\n const value = bytes[i] * 256 + bytes[i + 1];\n out += ALPHABET[value % 45] + ALPHABET[Math.floor(value / 45) % 45] + ALPHABET[Math.floor(value / 2025)];\n }\n if (bytes.length % 2) {\n const value = bytes[bytes.length - 1];\n out += ALPHABET[value % 45] + ALPHABET[Math.floor(value / 45)];\n }\n return out;\n}\n\nexport function base45ToBytes(text: string): Uint8Array {\n const out: number[] = [];\n let i = 0;\n while (i + 2 < text.length) {\n const value = digit(text, i) + 45 * digit(text, i + 1) + 2025 * digit(text, i + 2);\n if (value > 0xffff) throw new MeshPairingError('Malformed base45 payload');\n out.push(value >> 8, value & 0xff);\n i += 3;\n }\n if (text.length - i === 2) {\n const value = digit(text, i) + 45 * digit(text, i + 1);\n if (value > 0xff) throw new MeshPairingError('Malformed base45 payload');\n out.push(value);\n } else if (text.length - i === 1) {\n throw new MeshPairingError('Malformed base45 payload');\n }\n return new Uint8Array(out);\n}\n\nfunction digit(text: string, at: number): number {\n const value = INDEX.get(text[at]);\n if (value === undefined) throw new MeshPairingError('Malformed base45 payload');\n return value;\n}\n"],"mappings":"+CASA,IAAM,EAAW,gDACX,EAAQ,IAAI,IAAI,CAAC,GAAG,CAAQ,CAAC,CAAC,KAAK,EAAI,IAAM,CAAC,EAAI,CAAC,CAAC,CAAC,EAE3D,SAAgB,cAAc,EAA2B,CACvD,IAAI,EAAM,GACV,IAAK,IAAI,EAAI,EAAG,EAAI,EAAI,EAAM,OAAQ,GAAK,EAAG,CAC5C,IAAM,EAAQ,EAAM,GAAK,IAAM,EAAM,EAAI,GACzC,GAAO,EAAS,EAAQ,IAAM,EAAS,KAAK,MAAM,EAAQ,EAAE,EAAI,IAAM,EAAS,KAAK,MAAM,EAAQ,IAAI,EACxG,CACA,GAAI,EAAM,OAAS,EAAG,CACpB,IAAM,EAAQ,EAAM,EAAM,OAAS,GACnC,GAAO,EAAS,EAAQ,IAAM,EAAS,KAAK,MAAM,EAAQ,EAAE,EAC9D,CACA,OAAO,CACT,CAEA,SAAgB,cAAc,EAA0B,CACtD,IAAM,EAAgB,CAAC,EACnB,EAAI,EACR,KAAO,EAAI,EAAI,EAAK,QAAQ,CAC1B,IAAM,EAAQ,MAAM,EAAM,CAAC,EAAI,GAAK,MAAM,EAAM,EAAI,CAAC,EAAI,KAAO,MAAM,EAAM,EAAI,CAAC,EACjF,GAAI,EAAQ,MAAQ,MAAM,IAAI,EAAiB,0BAA0B,EACzE,EAAI,KAAK,GAAS,EAAG,EAAQ,GAAI,EACjC,GAAK,CACP,CACA,GAAI,EAAK,OAAS,IAAM,EAAG,CACzB,IAAM,EAAQ,MAAM,EAAM,CAAC,EAAI,GAAK,MAAM,EAAM,EAAI,CAAC,EACrD,GAAI,EAAQ,IAAM,MAAM,IAAI,EAAiB,0BAA0B,EACvE,EAAI,KAAK,CAAK,CAChB,MAAO,GAAI,EAAK,OAAS,IAAM,EAC7B,MAAM,IAAI,EAAiB,0BAA0B,EAEvD,OAAO,IAAI,WAAW,CAAG,CAC3B,CAEA,SAAS,MAAM,EAAc,EAAoB,CAC/C,IAAM,EAAQ,EAAM,IAAI,EAAK,EAAG,EAChC,GAAI,IAAU,IAAA,GAAW,MAAM,IAAI,EAAiB,0BAA0B,EAC9E,OAAO,CACT"}
1
+ {"version":3,"file":"_base45.js","names":[],"sources":["../src/_base45.ts"],"sourcesContent":["import { MeshPairingError } from './errors';\n\n/**\n * Base45 (RFC 9285): two bytes → three chars, one leftover byte → two chars.\n * The alphabet sits inside the QR alphanumeric charset, so QR encoders can use\n * alphanumeric mode (~5.5 bits/char) instead of byte mode: about a third\n * denser than base64 for the same payload.\n */\n\nconst ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';\nconst INDEX = new Map([...ALPHABET].map((ch, i) => [ch, i]));\n\nexport function bytesToBase45(bytes: Uint8Array): string {\n let out = '';\n for (let i = 0; i + 1 < bytes.length; i += 2) {\n const value = bytes[i] * 256 + bytes[i + 1];\n out += ALPHABET[value % 45] + ALPHABET[Math.floor(value / 45) % 45] + ALPHABET[Math.floor(value / 2025)];\n }\n if (bytes.length % 2) {\n const value = bytes[bytes.length - 1];\n out += ALPHABET[value % 45] + ALPHABET[Math.floor(value / 45)];\n }\n return out;\n}\n\nexport function base45ToBytes(text: string): Uint8Array {\n const out: number[] = [];\n let i = 0;\n while (i + 2 < text.length) {\n const value = digit(text, i) + 45 * digit(text, i + 1) + 2025 * digit(text, i + 2);\n if (value > 0xffff) throw new MeshPairingError('Malformed base45 payload');\n out.push(value >> 8, value & 0xff);\n i += 3;\n }\n if (text.length - i === 2) {\n const value = digit(text, i) + 45 * digit(text, i + 1);\n if (value > 0xff) throw new MeshPairingError('Malformed base45 payload');\n out.push(value);\n } else if (text.length - i === 1) {\n throw new MeshPairingError('Malformed base45 payload');\n }\n return new Uint8Array(out);\n}\n\nfunction digit(text: string, at: number): number {\n const value = INDEX.get(text[at]);\n if (value === undefined) throw new MeshPairingError('Malformed base45 payload');\n return value;\n}\n"],"mappings":"+CASA,IAAM,EAAW,gDACX,EAAQ,IAAI,IAAI,CAAC,GAAG,CAAQ,CAAC,CAAC,KAAK,EAAI,IAAM,CAAC,EAAI,CAAC,CAAC,CAAC,EAE3D,SAAgB,cAAc,EAA2B,CACvD,IAAI,EAAM,GACV,IAAK,IAAI,EAAI,EAAG,EAAI,EAAI,EAAM,OAAQ,GAAK,EAAG,CAC5C,IAAM,EAAQ,EAAM,GAAK,IAAM,EAAM,EAAI,GACzC,GAAO,EAAS,EAAQ,IAAM,EAAS,KAAK,MAAM,EAAQ,EAAE,EAAI,IAAM,EAAS,KAAK,MAAM,EAAQ,IAAI,EACxG,CACA,GAAI,EAAM,OAAS,EAAG,CACpB,IAAM,EAAQ,EAAM,EAAM,OAAS,GACnC,GAAO,EAAS,EAAQ,IAAM,EAAS,KAAK,MAAM,EAAQ,EAAE,EAC9D,CACA,OAAO,CACT,CAEA,SAAgB,cAAc,EAA0B,CACtD,IAAM,EAAgB,CAAC,EACnB,EAAI,EACR,KAAO,EAAI,EAAI,EAAK,QAAQ,CAC1B,IAAM,EAAQ,MAAM,EAAM,CAAC,EAAI,GAAK,MAAM,EAAM,EAAI,CAAC,EAAI,KAAO,MAAM,EAAM,EAAI,CAAC,EACjF,GAAI,EAAQ,MAAQ,MAAM,IAAI,EAAiB,0BAA0B,EACzE,EAAI,KAAK,GAAS,EAAG,EAAQ,GAAI,EACjC,GAAK,CACP,CACA,GAAI,EAAK,OAAS,IAAM,EAAG,CACzB,IAAM,EAAQ,MAAM,EAAM,CAAC,EAAI,GAAK,MAAM,EAAM,EAAI,CAAC,EACrD,GAAI,EAAQ,IAAM,MAAM,IAAI,EAAiB,0BAA0B,EACvE,EAAI,KAAK,CAAK,CAChB,MAAO,GAAI,EAAK,OAAS,IAAM,EAC7B,MAAM,IAAI,EAAiB,0BAA0B,EAEvD,OAAO,IAAI,WAAW,CAAG,CAC3B,CAEA,SAAS,MAAM,EAAc,EAAoB,CAC/C,IAAM,EAAQ,EAAM,IAAI,EAAK,EAAG,EAChC,GAAI,IAAU,IAAA,GAAW,MAAM,IAAI,EAAiB,0BAA0B,EAC9E,OAAO,CACT"}
package/dist/_proof.cjs CHANGED
@@ -1,2 +1,2 @@
1
- const e=require("./errors.cjs"),t=require("./_sdp.cjs");let n=require("@vielzeug/arsenal");async function createProof(r,i){let a=globalThis.crypto?.subtle;if(!a)throw new e.MeshUnsupportedError(`WebCrypto is unavailable — pairing requires a secure context`);let o=new TextEncoder,s=await a.importKey(`raw`,o.encode(r),{hash:`SHA-256`,name:`HMAC`},!1,[`sign`]),c=await a.sign(`HMAC`,s,o.encode(t.canonicalSdp(i)));return(0,n.bytesToBase64Url)(new Uint8Array(c))}exports.createProof=createProof;
1
+ const e=require("./errors.cjs"),t=require("./_sdp.cjs");let n=require("@vielzeug/arsenal");async function createProof(r,i){let a=globalThis.crypto?.subtle;if(!a)throw new e.MeshUnsupportedError(`WebCrypto is unavailable: pairing requires a secure context`);let o=new TextEncoder,s=await a.importKey(`raw`,o.encode(r),{hash:`SHA-256`,name:`HMAC`},!1,[`sign`]),c=await a.sign(`HMAC`,s,o.encode(t.canonicalSdp(i)));return(0,n.bytesToBase64Url)(new Uint8Array(c))}exports.createProof=createProof;
2
2
  //# sourceMappingURL=_proof.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"_proof.cjs","names":[],"sources":["../src/_proof.ts"],"sourcesContent":["import { bytesToBase64Url } from '@vielzeug/arsenal';\nimport { canonicalSdp } from './_sdp';\nimport { MeshUnsupportedError } from './errors';\n\n/**\n * Proof of invitation possession: HMAC-SHA-256 of the guest SDP keyed by the\n * invitation secret. The SDP is canonicalized first so the proof is stable\n * across pairing codecs that rebuild the description (see `mq2.`). The host\n * recomputes the proof without receiving the secret back.\n *\n * WebCrypto is required — a non-cryptographic fallback would make the proof\n * forgeable, which defeats its purpose. Environments without `SubtleCrypto`\n * (insecure contexts) must pair over `https://` or `localhost` instead.\n *\n * @throws {MeshUnsupportedError} When `SubtleCrypto` is unavailable.\n */\nexport async function createProof(secret: string, sdp: string): Promise<string> {\n const subtle = globalThis.crypto?.subtle;\n if (!subtle) {\n throw new MeshUnsupportedError('WebCrypto is unavailable — pairing requires a secure context');\n }\n const encoder = new TextEncoder();\n const key = await subtle.importKey('raw', encoder.encode(secret), { hash: 'SHA-256', name: 'HMAC' }, false, ['sign']);\n const signature = await subtle.sign('HMAC', key, encoder.encode(canonicalSdp(sdp)));\n return bytesToBase64Url(new Uint8Array(signature));\n}\n"],"mappings":"2FAgBA,eAAsB,YAAY,EAAgB,EAA8B,CAC9E,IAAM,EAAS,WAAW,QAAQ,OAClC,GAAI,CAAC,EACH,MAAM,IAAI,EAAA,qBAAqB,8DAA8D,EAE/F,IAAM,EAAU,IAAI,YACd,EAAM,MAAM,EAAO,UAAU,MAAO,EAAQ,OAAO,CAAM,EAAG,CAAE,KAAM,UAAW,KAAM,MAAO,EAAG,GAAO,CAAC,MAAM,CAAC,EAC9G,EAAY,MAAM,EAAO,KAAK,OAAQ,EAAK,EAAQ,OAAO,EAAA,aAAa,CAAG,CAAC,CAAC,EAClF,OAAA,EAAO,EAAA,iBAAA,CAAiB,IAAI,WAAW,CAAS,CAAC,CACnD"}
1
+ {"version":3,"file":"_proof.cjs","names":[],"sources":["../src/_proof.ts"],"sourcesContent":["import { bytesToBase64Url } from '@vielzeug/arsenal';\nimport { canonicalSdp } from './_sdp';\nimport { MeshUnsupportedError } from './errors';\n\n/**\n * Proof of invitation possession: HMAC-SHA-256 of the guest SDP keyed by the\n * invitation secret. The SDP is canonicalized first so the proof is stable\n * across pairing codecs that rebuild the description (see `mq2.`). The host\n * recomputes the proof without receiving the secret back.\n *\n * WebCrypto is required: a non-cryptographic fallback would make the proof\n * forgeable, which defeats its purpose. Environments without `SubtleCrypto`\n * (insecure contexts) must pair over `https://` or `localhost` instead.\n *\n * @throws {MeshUnsupportedError} When `SubtleCrypto` is unavailable.\n */\nexport async function createProof(secret: string, sdp: string): Promise<string> {\n const subtle = globalThis.crypto?.subtle;\n if (!subtle) {\n throw new MeshUnsupportedError('WebCrypto is unavailable: pairing requires a secure context');\n }\n const encoder = new TextEncoder();\n const key = await subtle.importKey('raw', encoder.encode(secret), { hash: 'SHA-256', name: 'HMAC' }, false, ['sign']);\n const signature = await subtle.sign('HMAC', key, encoder.encode(canonicalSdp(sdp)));\n return bytesToBase64Url(new Uint8Array(signature));\n}\n"],"mappings":"2FAgBA,eAAsB,YAAY,EAAgB,EAA8B,CAC9E,IAAM,EAAS,WAAW,QAAQ,OAClC,GAAI,CAAC,EACH,MAAM,IAAI,EAAA,qBAAqB,6DAA6D,EAE9F,IAAM,EAAU,IAAI,YACd,EAAM,MAAM,EAAO,UAAU,MAAO,EAAQ,OAAO,CAAM,EAAG,CAAE,KAAM,UAAW,KAAM,MAAO,EAAG,GAAO,CAAC,MAAM,CAAC,EAC9G,EAAY,MAAM,EAAO,KAAK,OAAQ,EAAK,EAAQ,OAAO,EAAA,aAAa,CAAG,CAAC,CAAC,EAClF,OAAA,EAAO,EAAA,iBAAA,CAAiB,IAAI,WAAW,CAAS,CAAC,CACnD"}
package/dist/_proof.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * across pairing codecs that rebuild the description (see `mq2.`). The host
5
5
  * recomputes the proof without receiving the secret back.
6
6
  *
7
- * WebCrypto is required — a non-cryptographic fallback would make the proof
7
+ * WebCrypto is required: a non-cryptographic fallback would make the proof
8
8
  * forgeable, which defeats its purpose. Environments without `SubtleCrypto`
9
9
  * (insecure contexts) must pair over `https://` or `localhost` instead.
10
10
  *
package/dist/_proof.js CHANGED
@@ -1,2 +1,2 @@
1
- import{MeshUnsupportedError as e}from"./errors.js";import{canonicalSdp as t}from"./_sdp.js";import{bytesToBase64Url as n}from"@vielzeug/arsenal";async function createProof(r,i){let a=globalThis.crypto?.subtle;if(!a)throw new e(`WebCrypto is unavailable — pairing requires a secure context`);let o=new TextEncoder,s=await a.importKey(`raw`,o.encode(r),{hash:`SHA-256`,name:`HMAC`},!1,[`sign`]),c=await a.sign(`HMAC`,s,o.encode(t(i)));return n(new Uint8Array(c))}export{createProof};
1
+ import{MeshUnsupportedError as e}from"./errors.js";import{canonicalSdp as t}from"./_sdp.js";import{bytesToBase64Url as n}from"@vielzeug/arsenal";async function createProof(r,i){let a=globalThis.crypto?.subtle;if(!a)throw new e(`WebCrypto is unavailable: pairing requires a secure context`);let o=new TextEncoder,s=await a.importKey(`raw`,o.encode(r),{hash:`SHA-256`,name:`HMAC`},!1,[`sign`]),c=await a.sign(`HMAC`,s,o.encode(t(i)));return n(new Uint8Array(c))}export{createProof};
2
2
  //# sourceMappingURL=_proof.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"_proof.js","names":[],"sources":["../src/_proof.ts"],"sourcesContent":["import { bytesToBase64Url } from '@vielzeug/arsenal';\nimport { canonicalSdp } from './_sdp';\nimport { MeshUnsupportedError } from './errors';\n\n/**\n * Proof of invitation possession: HMAC-SHA-256 of the guest SDP keyed by the\n * invitation secret. The SDP is canonicalized first so the proof is stable\n * across pairing codecs that rebuild the description (see `mq2.`). The host\n * recomputes the proof without receiving the secret back.\n *\n * WebCrypto is required — a non-cryptographic fallback would make the proof\n * forgeable, which defeats its purpose. Environments without `SubtleCrypto`\n * (insecure contexts) must pair over `https://` or `localhost` instead.\n *\n * @throws {MeshUnsupportedError} When `SubtleCrypto` is unavailable.\n */\nexport async function createProof(secret: string, sdp: string): Promise<string> {\n const subtle = globalThis.crypto?.subtle;\n if (!subtle) {\n throw new MeshUnsupportedError('WebCrypto is unavailable — pairing requires a secure context');\n }\n const encoder = new TextEncoder();\n const key = await subtle.importKey('raw', encoder.encode(secret), { hash: 'SHA-256', name: 'HMAC' }, false, ['sign']);\n const signature = await subtle.sign('HMAC', key, encoder.encode(canonicalSdp(sdp)));\n return bytesToBase64Url(new Uint8Array(signature));\n}\n"],"mappings":"iJAgBA,eAAsB,YAAY,EAAgB,EAA8B,CAC9E,IAAM,EAAS,WAAW,QAAQ,OAClC,GAAI,CAAC,EACH,MAAM,IAAI,EAAqB,8DAA8D,EAE/F,IAAM,EAAU,IAAI,YACd,EAAM,MAAM,EAAO,UAAU,MAAO,EAAQ,OAAO,CAAM,EAAG,CAAE,KAAM,UAAW,KAAM,MAAO,EAAG,GAAO,CAAC,MAAM,CAAC,EAC9G,EAAY,MAAM,EAAO,KAAK,OAAQ,EAAK,EAAQ,OAAO,EAAa,CAAG,CAAC,CAAC,EAClF,OAAO,EAAiB,IAAI,WAAW,CAAS,CAAC,CACnD"}
1
+ {"version":3,"file":"_proof.js","names":[],"sources":["../src/_proof.ts"],"sourcesContent":["import { bytesToBase64Url } from '@vielzeug/arsenal';\nimport { canonicalSdp } from './_sdp';\nimport { MeshUnsupportedError } from './errors';\n\n/**\n * Proof of invitation possession: HMAC-SHA-256 of the guest SDP keyed by the\n * invitation secret. The SDP is canonicalized first so the proof is stable\n * across pairing codecs that rebuild the description (see `mq2.`). The host\n * recomputes the proof without receiving the secret back.\n *\n * WebCrypto is required: a non-cryptographic fallback would make the proof\n * forgeable, which defeats its purpose. Environments without `SubtleCrypto`\n * (insecure contexts) must pair over `https://` or `localhost` instead.\n *\n * @throws {MeshUnsupportedError} When `SubtleCrypto` is unavailable.\n */\nexport async function createProof(secret: string, sdp: string): Promise<string> {\n const subtle = globalThis.crypto?.subtle;\n if (!subtle) {\n throw new MeshUnsupportedError('WebCrypto is unavailable: pairing requires a secure context');\n }\n const encoder = new TextEncoder();\n const key = await subtle.importKey('raw', encoder.encode(secret), { hash: 'SHA-256', name: 'HMAC' }, false, ['sign']);\n const signature = await subtle.sign('HMAC', key, encoder.encode(canonicalSdp(sdp)));\n return bytesToBase64Url(new Uint8Array(signature));\n}\n"],"mappings":"iJAgBA,eAAsB,YAAY,EAAgB,EAA8B,CAC9E,IAAM,EAAS,WAAW,QAAQ,OAClC,GAAI,CAAC,EACH,MAAM,IAAI,EAAqB,6DAA6D,EAE9F,IAAM,EAAU,IAAI,YACd,EAAM,MAAM,EAAO,UAAU,MAAO,EAAQ,OAAO,CAAM,EAAG,CAAE,KAAM,UAAW,KAAM,MAAO,EAAG,GAAO,CAAC,MAAM,CAAC,EAC9G,EAAY,MAAM,EAAO,KAAK,OAAQ,EAAK,EAAQ,OAAO,EAAa,CAAG,CAAC,CAAC,EAClF,OAAO,EAAiB,IAAI,WAAW,CAAS,CAAC,CACnD"}
@@ -1 +1 @@
1
- {"version":3,"file":"_random.cjs","names":[],"sources":["../src/_random.ts"],"sourcesContent":["import { bytesToBase64Url, type RandomSource, randomFloat } from '@vielzeug/arsenal';\n\n/**\n * Random bytes from `crypto.getRandomValues`, or from an injected\n * `RandomSource` for deterministic tests.\n */\nexport function randomBytes(length: number, source?: RandomSource): Uint8Array {\n const bytes = new Uint8Array(length);\n if (source) {\n for (let index = 0; index < length; index++) bytes[index] = Math.floor(randomFloat(source) * 256);\n } else {\n crypto.getRandomValues(bytes);\n }\n return bytes;\n}\n\n/** Random 128-bit base64url id — used for peer ids, session ids, and message ids. */\nexport function randomId(source?: RandomSource): string {\n return bytesToBase64Url(randomBytes(16, source));\n}\n"],"mappings":"mCAMA,SAAgB,YAAY,EAAgB,EAAmC,CAC7E,IAAM,EAAQ,IAAI,WAAW,CAAM,EACnC,GAAI,EACF,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAQ,IAAS,EAAM,GAAS,KAAK,OAAA,EAAM,EAAA,YAAA,CAAY,CAAM,EAAI,GAAG,OAEhG,OAAO,gBAAgB,CAAK,EAE9B,OAAO,CACT,CAGA,SAAgB,SAAS,EAA+B,CACtD,OAAA,EAAO,EAAA,iBAAA,CAAiB,YAAY,GAAI,CAAM,CAAC,CACjD"}
1
+ {"version":3,"file":"_random.cjs","names":[],"sources":["../src/_random.ts"],"sourcesContent":["import { bytesToBase64Url, type RandomSource, randomFloat } from '@vielzeug/arsenal';\n\n/**\n * Random bytes from `crypto.getRandomValues`, or from an injected\n * `RandomSource` for deterministic tests.\n */\nexport function randomBytes(length: number, source?: RandomSource): Uint8Array {\n const bytes = new Uint8Array(length);\n if (source) {\n for (let index = 0; index < length; index++) bytes[index] = Math.floor(randomFloat(source) * 256);\n } else {\n crypto.getRandomValues(bytes);\n }\n return bytes;\n}\n\n/** Random 128-bit base64url id: used for peer ids, session ids, and message ids. */\nexport function randomId(source?: RandomSource): string {\n return bytesToBase64Url(randomBytes(16, source));\n}\n"],"mappings":"mCAMA,SAAgB,YAAY,EAAgB,EAAmC,CAC7E,IAAM,EAAQ,IAAI,WAAW,CAAM,EACnC,GAAI,EACF,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAQ,IAAS,EAAM,GAAS,KAAK,OAAA,EAAM,EAAA,YAAA,CAAY,CAAM,EAAI,GAAG,OAEhG,OAAO,gBAAgB,CAAK,EAE9B,OAAO,CACT,CAGA,SAAgB,SAAS,EAA+B,CACtD,OAAA,EAAO,EAAA,iBAAA,CAAiB,YAAY,GAAI,CAAM,CAAC,CACjD"}
package/dist/_random.d.ts CHANGED
@@ -4,6 +4,6 @@ import { type RandomSource } from '@vielzeug/arsenal';
4
4
  * `RandomSource` for deterministic tests.
5
5
  */
6
6
  export declare function randomBytes(length: number, source?: RandomSource): Uint8Array;
7
- /** Random 128-bit base64url id — used for peer ids, session ids, and message ids. */
7
+ /** Random 128-bit base64url id: used for peer ids, session ids, and message ids. */
8
8
  export declare function randomId(source?: RandomSource): string;
9
9
  //# sourceMappingURL=_random.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"_random.d.ts","sourceRoot":"","sources":["../src/_random.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,YAAY,EAAe,MAAM,mBAAmB,CAAC;AAErF;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,YAAY,GAAG,UAAU,CAQ7E;AAED,qFAAqF;AACrF,wBAAgB,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,MAAM,CAEtD"}
1
+ {"version":3,"file":"_random.d.ts","sourceRoot":"","sources":["../src/_random.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,YAAY,EAAe,MAAM,mBAAmB,CAAC;AAErF;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,YAAY,GAAG,UAAU,CAQ7E;AAED,oFAAoF;AACpF,wBAAgB,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,MAAM,CAEtD"}
@@ -1 +1 @@
1
- {"version":3,"file":"_random.js","names":[],"sources":["../src/_random.ts"],"sourcesContent":["import { bytesToBase64Url, type RandomSource, randomFloat } from '@vielzeug/arsenal';\n\n/**\n * Random bytes from `crypto.getRandomValues`, or from an injected\n * `RandomSource` for deterministic tests.\n */\nexport function randomBytes(length: number, source?: RandomSource): Uint8Array {\n const bytes = new Uint8Array(length);\n if (source) {\n for (let index = 0; index < length; index++) bytes[index] = Math.floor(randomFloat(source) * 256);\n } else {\n crypto.getRandomValues(bytes);\n }\n return bytes;\n}\n\n/** Random 128-bit base64url id — used for peer ids, session ids, and message ids. */\nexport function randomId(source?: RandomSource): string {\n return bytesToBase64Url(randomBytes(16, source));\n}\n"],"mappings":"sEAMA,SAAgB,YAAY,EAAgB,EAAmC,CAC7E,IAAM,EAAQ,IAAI,WAAW,CAAM,EACnC,GAAI,EACF,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAQ,IAAS,EAAM,GAAS,KAAK,MAAM,EAAY,CAAM,EAAI,GAAG,OAEhG,OAAO,gBAAgB,CAAK,EAE9B,OAAO,CACT,CAGA,SAAgB,SAAS,EAA+B,CACtD,OAAO,EAAiB,YAAY,GAAI,CAAM,CAAC,CACjD"}
1
+ {"version":3,"file":"_random.js","names":[],"sources":["../src/_random.ts"],"sourcesContent":["import { bytesToBase64Url, type RandomSource, randomFloat } from '@vielzeug/arsenal';\n\n/**\n * Random bytes from `crypto.getRandomValues`, or from an injected\n * `RandomSource` for deterministic tests.\n */\nexport function randomBytes(length: number, source?: RandomSource): Uint8Array {\n const bytes = new Uint8Array(length);\n if (source) {\n for (let index = 0; index < length; index++) bytes[index] = Math.floor(randomFloat(source) * 256);\n } else {\n crypto.getRandomValues(bytes);\n }\n return bytes;\n}\n\n/** Random 128-bit base64url id: used for peer ids, session ids, and message ids. */\nexport function randomId(source?: RandomSource): string {\n return bytesToBase64Url(randomBytes(16, source));\n}\n"],"mappings":"sEAMA,SAAgB,YAAY,EAAgB,EAAmC,CAC7E,IAAM,EAAQ,IAAI,WAAW,CAAM,EACnC,GAAI,EACF,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAQ,IAAS,EAAM,GAAS,KAAK,MAAM,EAAY,CAAM,EAAI,GAAG,OAEhG,OAAO,gBAAgB,CAAK,EAE9B,OAAO,CACT,CAGA,SAAgB,SAAS,EAA+B,CACtD,OAAO,EAAiB,YAAY,GAAI,CAAM,CAAC,CACjD"}
package/dist/_rtc.cjs CHANGED
@@ -1,4 +1,4 @@
1
- const e=require("./errors.cjs");var t={createPeerConnection(t){let n=globalThis.RTCPeerConnection;if(!n)throw new e.MeshUnsupportedError("WebRTC is unavailable in this environment — inject an `rtc` factory");return new n(t)}};function normalizeSdp(e){let t=e.replace(/\r?\n/g,`\r
1
+ const e=require("./errors.cjs");var t={createPeerConnection(t){let n=globalThis.RTCPeerConnection;if(!n)throw new e.MeshUnsupportedError("WebRTC is unavailable in this environment: inject an `rtc` factory");return new n(t)}};function normalizeSdp(e){let t=e.replace(/\r?\n/g,`\r
2
2
  `);return t.endsWith(`\r
3
3
  `)?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(t,n,r,i,a){return t.readyState===`open`?Promise.resolve():new Promise((o,s)=>{let c=null,timeout=()=>new e.MeshTimeoutError(`Timed out waiting ${r}ms for the data channel to open`),fail=e=>{cleanup(),s(e)},cleanup=()=>{c&&clearTimeout(c),c=null,t.removeEventListener(`open`,onOpen),t.removeEventListener(`close`,onClose),n.removeEventListener(`iceconnectionstatechange`,onIce),a.removeEventListener(`abort`,onDispose)},onOpen=()=>{cleanup(),o()},onClose=()=>fail(timeout()),onIce=()=>{let t=n.iceConnectionState;(t===`failed`||t===`closed`||t===`disconnected`)&&fail(new e.MeshConnectionError(`ICE connection ${t} before channel open`,i))},onDispose=()=>fail(new e.MeshDisposedError);c=setTimeout(()=>fail(timeout()),r),t.addEventListener(`open`,onOpen),t.addEventListener(`close`,onClose),n.addEventListener(`iceconnectionstatechange`,onIce),a.addEventListener(`abort`,onDispose,{once:!0})})}exports.nativeRtcFactory=t,exports.normalizeSdp=normalizeSdp,exports.waitChannelOpen=waitChannelOpen,exports.waitIceGathering=waitIceGathering;
4
4
  //# sourceMappingURL=_rtc.cjs.map
package/dist/_rtc.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"_rtc.cjs","names":[],"sources":["../src/_rtc.ts"],"sourcesContent":["import { MeshConnectionError, MeshDisposedError, MeshTimeoutError, MeshUnsupportedError } from './errors';\nimport type { MeshRtcFactory, RTCDataChannelLike, RTCPeerConnectionLike } from './types';\n\n/**\n * Wraps `globalThis.RTCPeerConnection`. Evaluated lazily at first use so\n * importing the package never throws in non-WebRTC environments.\n */\nexport const nativeRtcFactory: MeshRtcFactory = {\n createPeerConnection(config) {\n const Ctor = (globalThis as { RTCPeerConnection?: typeof RTCPeerConnection }).RTCPeerConnection;\n if (!Ctor) {\n throw new MeshUnsupportedError('WebRTC is unavailable in this environment — inject an `rtc` factory');\n }\n return new Ctor(config);\n },\n};\n\n/**\n * Textareas, clipboards, and share sheets normalize CRLF→LF;\n * `setRemoteDescription` requires CRLF line endings.\n */\nexport function normalizeSdp(sdp: string): string {\n const normalized = sdp.replace(/\\r?\\n/g, '\\r\\n');\n return normalized.endsWith('\\r\\n') ? normalized : `${normalized}\\r\\n`;\n}\n\n/**\n * Resolves when ICE gathering finishes or after `timeoutMs` — the caller\n * proceeds with whatever candidates were gathered (non-trickle semantics).\n */\nexport function waitIceGathering(pc: RTCPeerConnectionLike, timeoutMs: number): Promise<void> {\n if (pc.iceGatheringState === 'complete') return Promise.resolve();\n return new Promise((resolve) => {\n const cleanup = () => {\n clearTimeout(timer);\n pc.removeEventListener('icegatheringstatechange', onChange);\n };\n const onChange = () => {\n if (pc.iceGatheringState === 'complete') {\n cleanup();\n resolve();\n }\n };\n const timer = setTimeout(() => {\n cleanup();\n resolve();\n }, timeoutMs);\n pc.addEventListener('icegatheringstatechange', onChange);\n });\n}\n\n/**\n * Resolves when the data channel opens. Rejects with `MeshTimeoutError` after\n * `timeoutMs` — or when the channel closes while that deadline is still\n * pending, since a peer tearing down a never-opened channel races the same\n * deadline — `MeshConnectionError` when the ICE transport dies first, and\n * `MeshDisposedError` when `disposalSignal` aborts.\n */\nexport function waitChannelOpen(\n dc: RTCDataChannelLike,\n pc: RTCPeerConnectionLike,\n timeoutMs: number,\n peerId: string,\n disposalSignal: AbortSignal,\n): Promise<void> {\n if (dc.readyState === 'open') return Promise.resolve();\n return new Promise((resolve, reject) => {\n let timer: ReturnType<typeof setTimeout> | null = null;\n const timeout = () => new MeshTimeoutError(`Timed out waiting ${timeoutMs}ms for the data channel to open`);\n const fail = (error: Error) => {\n cleanup();\n reject(error);\n };\n const cleanup = () => {\n if (timer) clearTimeout(timer);\n timer = null;\n dc.removeEventListener('open', onOpen);\n dc.removeEventListener('close', onClose);\n pc.removeEventListener('iceconnectionstatechange', onIce);\n disposalSignal.removeEventListener('abort', onDispose);\n };\n const onOpen = () => {\n cleanup();\n resolve();\n };\n // A close while our open deadline is still armed is the timeout it\n // effectively is; mirror the guest's open-timer guard so both ends agree\n // on the cause instead of racing to report whichever event landed first.\n const onClose = () => fail(timeout());\n const onIce = () => {\n const state = pc.iceConnectionState;\n if (state === 'failed' || state === 'closed' || state === 'disconnected') {\n fail(new MeshConnectionError(`ICE connection ${state} before channel open`, peerId));\n }\n };\n const onDispose = () => fail(new MeshDisposedError());\n timer = setTimeout(() => fail(timeout()), timeoutMs);\n dc.addEventListener('open', onOpen);\n dc.addEventListener('close', onClose);\n pc.addEventListener('iceconnectionstatechange', onIce);\n disposalSignal.addEventListener('abort', onDispose, { once: true });\n });\n}\n"],"mappings":"gCAOA,IAAa,EAAmC,CAC9C,qBAAqB,EAAQ,CAC3B,IAAM,EAAQ,WAAgE,kBAC9E,GAAI,CAAC,EACH,MAAM,IAAI,EAAA,qBAAqB,qEAAqE,EAEtG,OAAO,IAAI,EAAK,CAAM,CACxB,CACF,EAMA,SAAgB,aAAa,EAAqB,CAChD,IAAM,EAAa,EAAI,QAAQ,SAAU;CAAM,EAC/C,OAAO,EAAW,SAAS;CAAM,EAAI,EAAa,GAAG,EAAW,KAClE,CAMA,SAAgB,iBAAiB,EAA2B,EAAkC,CAE5F,OADI,EAAG,oBAAsB,WAAmB,QAAQ,QAAQ,EACzD,IAAI,QAAS,GAAY,CAC9B,IAAM,YAAgB,CACpB,aAAa,CAAK,EAClB,EAAG,oBAAoB,0BAA2B,QAAQ,CAC5D,EACM,aAAiB,CACjB,EAAG,oBAAsB,aAC3B,QAAQ,EACR,EAAQ,EAEZ,EACM,EAAQ,eAAiB,CAC7B,QAAQ,EACR,EAAQ,CACV,EAAG,CAAS,EACZ,EAAG,iBAAiB,0BAA2B,QAAQ,CACzD,CAAC,CACH,CASA,SAAgB,gBACd,EACA,EACA,EACA,EACA,EACe,CAEf,OADI,EAAG,aAAe,OAAe,QAAQ,QAAQ,EAC9C,IAAI,SAAS,EAAS,IAAW,CACtC,IAAI,EAA8C,KAC5C,YAAgB,IAAI,EAAA,iBAAiB,qBAAqB,EAAU,gCAAgC,EACpG,KAAQ,GAAiB,CAC7B,QAAQ,EACR,EAAO,CAAK,CACd,EACM,YAAgB,CAChB,GAAO,aAAa,CAAK,EAC7B,EAAQ,KACR,EAAG,oBAAoB,OAAQ,MAAM,EACrC,EAAG,oBAAoB,QAAS,OAAO,EACvC,EAAG,oBAAoB,2BAA4B,KAAK,EACxD,EAAe,oBAAoB,QAAS,SAAS,CACvD,EACM,WAAe,CACnB,QAAQ,EACR,EAAQ,CACV,EAIM,YAAgB,KAAK,QAAQ,CAAC,EAC9B,UAAc,CAClB,IAAM,EAAQ,EAAG,oBACb,IAAU,UAAY,IAAU,UAAY,IAAU,iBACxD,KAAK,IAAI,EAAA,oBAAoB,kBAAkB,EAAM,sBAAuB,CAAM,CAAC,CAEvF,EACM,cAAkB,KAAK,IAAI,EAAA,iBAAmB,EACpD,EAAQ,eAAiB,KAAK,QAAQ,CAAC,EAAG,CAAS,EACnD,EAAG,iBAAiB,OAAQ,MAAM,EAClC,EAAG,iBAAiB,QAAS,OAAO,EACpC,EAAG,iBAAiB,2BAA4B,KAAK,EACrD,EAAe,iBAAiB,QAAS,UAAW,CAAE,KAAM,EAAK,CAAC,CACpE,CAAC,CACH"}
1
+ {"version":3,"file":"_rtc.cjs","names":[],"sources":["../src/_rtc.ts"],"sourcesContent":["import { MeshConnectionError, MeshDisposedError, MeshTimeoutError, MeshUnsupportedError } from './errors';\nimport type { MeshRtcFactory, RTCDataChannelLike, RTCPeerConnectionLike } from './types';\n\n/**\n * Wraps `globalThis.RTCPeerConnection`. Evaluated lazily at first use so\n * importing the package never throws in non-WebRTC environments.\n */\nexport const nativeRtcFactory: MeshRtcFactory = {\n createPeerConnection(config) {\n const Ctor = (globalThis as { RTCPeerConnection?: typeof RTCPeerConnection }).RTCPeerConnection;\n if (!Ctor) {\n throw new MeshUnsupportedError('WebRTC is unavailable in this environment: inject an `rtc` factory');\n }\n return new Ctor(config);\n },\n};\n\n/**\n * Textareas, clipboards, and share sheets normalize CRLF→LF;\n * `setRemoteDescription` requires CRLF line endings.\n */\nexport function normalizeSdp(sdp: string): string {\n const normalized = sdp.replace(/\\r?\\n/g, '\\r\\n');\n return normalized.endsWith('\\r\\n') ? normalized : `${normalized}\\r\\n`;\n}\n\n/**\n * Resolves when ICE gathering finishes or after `timeoutMs`: the caller\n * proceeds with whatever candidates were gathered (non-trickle semantics).\n */\nexport function waitIceGathering(pc: RTCPeerConnectionLike, timeoutMs: number): Promise<void> {\n if (pc.iceGatheringState === 'complete') return Promise.resolve();\n return new Promise((resolve) => {\n const cleanup = () => {\n clearTimeout(timer);\n pc.removeEventListener('icegatheringstatechange', onChange);\n };\n const onChange = () => {\n if (pc.iceGatheringState === 'complete') {\n cleanup();\n resolve();\n }\n };\n const timer = setTimeout(() => {\n cleanup();\n resolve();\n }, timeoutMs);\n pc.addEventListener('icegatheringstatechange', onChange);\n });\n}\n\n/**\n * Resolves when the data channel opens. Rejects with `MeshTimeoutError` after\n * `timeoutMs`, or when the channel closes while that deadline is still\n * pending, since a peer tearing down a never-opened channel races the same\n * deadline: `MeshConnectionError` when the ICE transport dies first, and\n * `MeshDisposedError` when `disposalSignal` aborts.\n */\nexport function waitChannelOpen(\n dc: RTCDataChannelLike,\n pc: RTCPeerConnectionLike,\n timeoutMs: number,\n peerId: string,\n disposalSignal: AbortSignal,\n): Promise<void> {\n if (dc.readyState === 'open') return Promise.resolve();\n return new Promise((resolve, reject) => {\n let timer: ReturnType<typeof setTimeout> | null = null;\n const timeout = () => new MeshTimeoutError(`Timed out waiting ${timeoutMs}ms for the data channel to open`);\n const fail = (error: Error) => {\n cleanup();\n reject(error);\n };\n const cleanup = () => {\n if (timer) clearTimeout(timer);\n timer = null;\n dc.removeEventListener('open', onOpen);\n dc.removeEventListener('close', onClose);\n pc.removeEventListener('iceconnectionstatechange', onIce);\n disposalSignal.removeEventListener('abort', onDispose);\n };\n const onOpen = () => {\n cleanup();\n resolve();\n };\n // A close while our open deadline is still armed is the timeout it\n // effectively is; mirror the guest's open-timer guard so both ends agree\n // on the cause instead of racing to report whichever event landed first.\n const onClose = () => fail(timeout());\n const onIce = () => {\n const state = pc.iceConnectionState;\n if (state === 'failed' || state === 'closed' || state === 'disconnected') {\n fail(new MeshConnectionError(`ICE connection ${state} before channel open`, peerId));\n }\n };\n const onDispose = () => fail(new MeshDisposedError());\n timer = setTimeout(() => fail(timeout()), timeoutMs);\n dc.addEventListener('open', onOpen);\n dc.addEventListener('close', onClose);\n pc.addEventListener('iceconnectionstatechange', onIce);\n disposalSignal.addEventListener('abort', onDispose, { once: true });\n });\n}\n"],"mappings":"gCAOA,IAAa,EAAmC,CAC9C,qBAAqB,EAAQ,CAC3B,IAAM,EAAQ,WAAgE,kBAC9E,GAAI,CAAC,EACH,MAAM,IAAI,EAAA,qBAAqB,oEAAoE,EAErG,OAAO,IAAI,EAAK,CAAM,CACxB,CACF,EAMA,SAAgB,aAAa,EAAqB,CAChD,IAAM,EAAa,EAAI,QAAQ,SAAU;CAAM,EAC/C,OAAO,EAAW,SAAS;CAAM,EAAI,EAAa,GAAG,EAAW,KAClE,CAMA,SAAgB,iBAAiB,EAA2B,EAAkC,CAE5F,OADI,EAAG,oBAAsB,WAAmB,QAAQ,QAAQ,EACzD,IAAI,QAAS,GAAY,CAC9B,IAAM,YAAgB,CACpB,aAAa,CAAK,EAClB,EAAG,oBAAoB,0BAA2B,QAAQ,CAC5D,EACM,aAAiB,CACjB,EAAG,oBAAsB,aAC3B,QAAQ,EACR,EAAQ,EAEZ,EACM,EAAQ,eAAiB,CAC7B,QAAQ,EACR,EAAQ,CACV,EAAG,CAAS,EACZ,EAAG,iBAAiB,0BAA2B,QAAQ,CACzD,CAAC,CACH,CASA,SAAgB,gBACd,EACA,EACA,EACA,EACA,EACe,CAEf,OADI,EAAG,aAAe,OAAe,QAAQ,QAAQ,EAC9C,IAAI,SAAS,EAAS,IAAW,CACtC,IAAI,EAA8C,KAC5C,YAAgB,IAAI,EAAA,iBAAiB,qBAAqB,EAAU,gCAAgC,EACpG,KAAQ,GAAiB,CAC7B,QAAQ,EACR,EAAO,CAAK,CACd,EACM,YAAgB,CAChB,GAAO,aAAa,CAAK,EAC7B,EAAQ,KACR,EAAG,oBAAoB,OAAQ,MAAM,EACrC,EAAG,oBAAoB,QAAS,OAAO,EACvC,EAAG,oBAAoB,2BAA4B,KAAK,EACxD,EAAe,oBAAoB,QAAS,SAAS,CACvD,EACM,WAAe,CACnB,QAAQ,EACR,EAAQ,CACV,EAIM,YAAgB,KAAK,QAAQ,CAAC,EAC9B,UAAc,CAClB,IAAM,EAAQ,EAAG,oBACb,IAAU,UAAY,IAAU,UAAY,IAAU,iBACxD,KAAK,IAAI,EAAA,oBAAoB,kBAAkB,EAAM,sBAAuB,CAAM,CAAC,CAEvF,EACM,cAAkB,KAAK,IAAI,EAAA,iBAAmB,EACpD,EAAQ,eAAiB,KAAK,QAAQ,CAAC,EAAG,CAAS,EACnD,EAAG,iBAAiB,OAAQ,MAAM,EAClC,EAAG,iBAAiB,QAAS,OAAO,EACpC,EAAG,iBAAiB,2BAA4B,KAAK,EACrD,EAAe,iBAAiB,QAAS,UAAW,CAAE,KAAM,EAAK,CAAC,CACpE,CAAC,CACH"}
package/dist/_rtc.d.ts CHANGED
@@ -10,15 +10,15 @@ export declare const nativeRtcFactory: MeshRtcFactory;
10
10
  */
11
11
  export declare function normalizeSdp(sdp: string): string;
12
12
  /**
13
- * Resolves when ICE gathering finishes or after `timeoutMs` — the caller
13
+ * Resolves when ICE gathering finishes or after `timeoutMs`: the caller
14
14
  * proceeds with whatever candidates were gathered (non-trickle semantics).
15
15
  */
16
16
  export declare function waitIceGathering(pc: RTCPeerConnectionLike, timeoutMs: number): Promise<void>;
17
17
  /**
18
18
  * Resolves when the data channel opens. Rejects with `MeshTimeoutError` after
19
- * `timeoutMs` — or when the channel closes while that deadline is still
19
+ * `timeoutMs`, or when the channel closes while that deadline is still
20
20
  * pending, since a peer tearing down a never-opened channel races the same
21
- * deadline — `MeshConnectionError` when the ICE transport dies first, and
21
+ * deadline: `MeshConnectionError` when the ICE transport dies first, and
22
22
  * `MeshDisposedError` when `disposalSignal` aborts.
23
23
  */
24
24
  export declare function waitChannelOpen(dc: RTCDataChannelLike, pc: RTCPeerConnectionLike, timeoutMs: number, peerId: string, disposalSignal: AbortSignal): Promise<void>;
package/dist/_rtc.js CHANGED
@@ -1,4 +1,4 @@
1
- import{MeshConnectionError as e,MeshDisposedError as t,MeshTimeoutError as n,MeshUnsupportedError as r}from"./errors.js";var i={createPeerConnection(e){let t=globalThis.RTCPeerConnection;if(!t)throw new r("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
1
+ import{MeshConnectionError as e,MeshDisposedError as t,MeshTimeoutError as n,MeshUnsupportedError as r}from"./errors.js";var i={createPeerConnection(e){let t=globalThis.RTCPeerConnection;if(!t)throw new r("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
2
2
  `);return t.endsWith(`\r
3
3
  `)?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(r,i,a,o,s){return r.readyState===`open`?Promise.resolve():new Promise((c,l)=>{let u=null,timeout=()=>new n(`Timed out waiting ${a}ms for the data channel to open`),fail=e=>{cleanup(),l(e)},cleanup=()=>{u&&clearTimeout(u),u=null,r.removeEventListener(`open`,onOpen),r.removeEventListener(`close`,onClose),i.removeEventListener(`iceconnectionstatechange`,onIce),s.removeEventListener(`abort`,onDispose)},onOpen=()=>{cleanup(),c()},onClose=()=>fail(timeout()),onIce=()=>{let t=i.iceConnectionState;(t===`failed`||t===`closed`||t===`disconnected`)&&fail(new e(`ICE connection ${t} before channel open`,o))},onDispose=()=>fail(new t);u=setTimeout(()=>fail(timeout()),a),r.addEventListener(`open`,onOpen),r.addEventListener(`close`,onClose),i.addEventListener(`iceconnectionstatechange`,onIce),s.addEventListener(`abort`,onDispose,{once:!0})})}export{i as nativeRtcFactory,normalizeSdp,waitChannelOpen,waitIceGathering};
4
4
  //# sourceMappingURL=_rtc.js.map
package/dist/_rtc.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"_rtc.js","names":[],"sources":["../src/_rtc.ts"],"sourcesContent":["import { MeshConnectionError, MeshDisposedError, MeshTimeoutError, MeshUnsupportedError } from './errors';\nimport type { MeshRtcFactory, RTCDataChannelLike, RTCPeerConnectionLike } from './types';\n\n/**\n * Wraps `globalThis.RTCPeerConnection`. Evaluated lazily at first use so\n * importing the package never throws in non-WebRTC environments.\n */\nexport const nativeRtcFactory: MeshRtcFactory = {\n createPeerConnection(config) {\n const Ctor = (globalThis as { RTCPeerConnection?: typeof RTCPeerConnection }).RTCPeerConnection;\n if (!Ctor) {\n throw new MeshUnsupportedError('WebRTC is unavailable in this environment — inject an `rtc` factory');\n }\n return new Ctor(config);\n },\n};\n\n/**\n * Textareas, clipboards, and share sheets normalize CRLF→LF;\n * `setRemoteDescription` requires CRLF line endings.\n */\nexport function normalizeSdp(sdp: string): string {\n const normalized = sdp.replace(/\\r?\\n/g, '\\r\\n');\n return normalized.endsWith('\\r\\n') ? normalized : `${normalized}\\r\\n`;\n}\n\n/**\n * Resolves when ICE gathering finishes or after `timeoutMs` — the caller\n * proceeds with whatever candidates were gathered (non-trickle semantics).\n */\nexport function waitIceGathering(pc: RTCPeerConnectionLike, timeoutMs: number): Promise<void> {\n if (pc.iceGatheringState === 'complete') return Promise.resolve();\n return new Promise((resolve) => {\n const cleanup = () => {\n clearTimeout(timer);\n pc.removeEventListener('icegatheringstatechange', onChange);\n };\n const onChange = () => {\n if (pc.iceGatheringState === 'complete') {\n cleanup();\n resolve();\n }\n };\n const timer = setTimeout(() => {\n cleanup();\n resolve();\n }, timeoutMs);\n pc.addEventListener('icegatheringstatechange', onChange);\n });\n}\n\n/**\n * Resolves when the data channel opens. Rejects with `MeshTimeoutError` after\n * `timeoutMs` — or when the channel closes while that deadline is still\n * pending, since a peer tearing down a never-opened channel races the same\n * deadline — `MeshConnectionError` when the ICE transport dies first, and\n * `MeshDisposedError` when `disposalSignal` aborts.\n */\nexport function waitChannelOpen(\n dc: RTCDataChannelLike,\n pc: RTCPeerConnectionLike,\n timeoutMs: number,\n peerId: string,\n disposalSignal: AbortSignal,\n): Promise<void> {\n if (dc.readyState === 'open') return Promise.resolve();\n return new Promise((resolve, reject) => {\n let timer: ReturnType<typeof setTimeout> | null = null;\n const timeout = () => new MeshTimeoutError(`Timed out waiting ${timeoutMs}ms for the data channel to open`);\n const fail = (error: Error) => {\n cleanup();\n reject(error);\n };\n const cleanup = () => {\n if (timer) clearTimeout(timer);\n timer = null;\n dc.removeEventListener('open', onOpen);\n dc.removeEventListener('close', onClose);\n pc.removeEventListener('iceconnectionstatechange', onIce);\n disposalSignal.removeEventListener('abort', onDispose);\n };\n const onOpen = () => {\n cleanup();\n resolve();\n };\n // A close while our open deadline is still armed is the timeout it\n // effectively is; mirror the guest's open-timer guard so both ends agree\n // on the cause instead of racing to report whichever event landed first.\n const onClose = () => fail(timeout());\n const onIce = () => {\n const state = pc.iceConnectionState;\n if (state === 'failed' || state === 'closed' || state === 'disconnected') {\n fail(new MeshConnectionError(`ICE connection ${state} before channel open`, peerId));\n }\n };\n const onDispose = () => fail(new MeshDisposedError());\n timer = setTimeout(() => fail(timeout()), timeoutMs);\n dc.addEventListener('open', onOpen);\n dc.addEventListener('close', onClose);\n pc.addEventListener('iceconnectionstatechange', onIce);\n disposalSignal.addEventListener('abort', onDispose, { once: true });\n });\n}\n"],"mappings":"yHAOA,IAAa,EAAmC,CAC9C,qBAAqB,EAAQ,CAC3B,IAAM,EAAQ,WAAgE,kBAC9E,GAAI,CAAC,EACH,MAAM,IAAI,EAAqB,qEAAqE,EAEtG,OAAO,IAAI,EAAK,CAAM,CACxB,CACF,EAMA,SAAgB,aAAa,EAAqB,CAChD,IAAM,EAAa,EAAI,QAAQ,SAAU;CAAM,EAC/C,OAAO,EAAW,SAAS;CAAM,EAAI,EAAa,GAAG,EAAW,KAClE,CAMA,SAAgB,iBAAiB,EAA2B,EAAkC,CAE5F,OADI,EAAG,oBAAsB,WAAmB,QAAQ,QAAQ,EACzD,IAAI,QAAS,GAAY,CAC9B,IAAM,YAAgB,CACpB,aAAa,CAAK,EAClB,EAAG,oBAAoB,0BAA2B,QAAQ,CAC5D,EACM,aAAiB,CACjB,EAAG,oBAAsB,aAC3B,QAAQ,EACR,EAAQ,EAEZ,EACM,EAAQ,eAAiB,CAC7B,QAAQ,EACR,EAAQ,CACV,EAAG,CAAS,EACZ,EAAG,iBAAiB,0BAA2B,QAAQ,CACzD,CAAC,CACH,CASA,SAAgB,gBACd,EACA,EACA,EACA,EACA,EACe,CAEf,OADI,EAAG,aAAe,OAAe,QAAQ,QAAQ,EAC9C,IAAI,SAAS,EAAS,IAAW,CACtC,IAAI,EAA8C,KAC5C,YAAgB,IAAI,EAAiB,qBAAqB,EAAU,gCAAgC,EACpG,KAAQ,GAAiB,CAC7B,QAAQ,EACR,EAAO,CAAK,CACd,EACM,YAAgB,CAChB,GAAO,aAAa,CAAK,EAC7B,EAAQ,KACR,EAAG,oBAAoB,OAAQ,MAAM,EACrC,EAAG,oBAAoB,QAAS,OAAO,EACvC,EAAG,oBAAoB,2BAA4B,KAAK,EACxD,EAAe,oBAAoB,QAAS,SAAS,CACvD,EACM,WAAe,CACnB,QAAQ,EACR,EAAQ,CACV,EAIM,YAAgB,KAAK,QAAQ,CAAC,EAC9B,UAAc,CAClB,IAAM,EAAQ,EAAG,oBACb,IAAU,UAAY,IAAU,UAAY,IAAU,iBACxD,KAAK,IAAI,EAAoB,kBAAkB,EAAM,sBAAuB,CAAM,CAAC,CAEvF,EACM,cAAkB,KAAK,IAAI,CAAmB,EACpD,EAAQ,eAAiB,KAAK,QAAQ,CAAC,EAAG,CAAS,EACnD,EAAG,iBAAiB,OAAQ,MAAM,EAClC,EAAG,iBAAiB,QAAS,OAAO,EACpC,EAAG,iBAAiB,2BAA4B,KAAK,EACrD,EAAe,iBAAiB,QAAS,UAAW,CAAE,KAAM,EAAK,CAAC,CACpE,CAAC,CACH"}
1
+ {"version":3,"file":"_rtc.js","names":[],"sources":["../src/_rtc.ts"],"sourcesContent":["import { MeshConnectionError, MeshDisposedError, MeshTimeoutError, MeshUnsupportedError } from './errors';\nimport type { MeshRtcFactory, RTCDataChannelLike, RTCPeerConnectionLike } from './types';\n\n/**\n * Wraps `globalThis.RTCPeerConnection`. Evaluated lazily at first use so\n * importing the package never throws in non-WebRTC environments.\n */\nexport const nativeRtcFactory: MeshRtcFactory = {\n createPeerConnection(config) {\n const Ctor = (globalThis as { RTCPeerConnection?: typeof RTCPeerConnection }).RTCPeerConnection;\n if (!Ctor) {\n throw new MeshUnsupportedError('WebRTC is unavailable in this environment: inject an `rtc` factory');\n }\n return new Ctor(config);\n },\n};\n\n/**\n * Textareas, clipboards, and share sheets normalize CRLF→LF;\n * `setRemoteDescription` requires CRLF line endings.\n */\nexport function normalizeSdp(sdp: string): string {\n const normalized = sdp.replace(/\\r?\\n/g, '\\r\\n');\n return normalized.endsWith('\\r\\n') ? normalized : `${normalized}\\r\\n`;\n}\n\n/**\n * Resolves when ICE gathering finishes or after `timeoutMs`: the caller\n * proceeds with whatever candidates were gathered (non-trickle semantics).\n */\nexport function waitIceGathering(pc: RTCPeerConnectionLike, timeoutMs: number): Promise<void> {\n if (pc.iceGatheringState === 'complete') return Promise.resolve();\n return new Promise((resolve) => {\n const cleanup = () => {\n clearTimeout(timer);\n pc.removeEventListener('icegatheringstatechange', onChange);\n };\n const onChange = () => {\n if (pc.iceGatheringState === 'complete') {\n cleanup();\n resolve();\n }\n };\n const timer = setTimeout(() => {\n cleanup();\n resolve();\n }, timeoutMs);\n pc.addEventListener('icegatheringstatechange', onChange);\n });\n}\n\n/**\n * Resolves when the data channel opens. Rejects with `MeshTimeoutError` after\n * `timeoutMs`, or when the channel closes while that deadline is still\n * pending, since a peer tearing down a never-opened channel races the same\n * deadline: `MeshConnectionError` when the ICE transport dies first, and\n * `MeshDisposedError` when `disposalSignal` aborts.\n */\nexport function waitChannelOpen(\n dc: RTCDataChannelLike,\n pc: RTCPeerConnectionLike,\n timeoutMs: number,\n peerId: string,\n disposalSignal: AbortSignal,\n): Promise<void> {\n if (dc.readyState === 'open') return Promise.resolve();\n return new Promise((resolve, reject) => {\n let timer: ReturnType<typeof setTimeout> | null = null;\n const timeout = () => new MeshTimeoutError(`Timed out waiting ${timeoutMs}ms for the data channel to open`);\n const fail = (error: Error) => {\n cleanup();\n reject(error);\n };\n const cleanup = () => {\n if (timer) clearTimeout(timer);\n timer = null;\n dc.removeEventListener('open', onOpen);\n dc.removeEventListener('close', onClose);\n pc.removeEventListener('iceconnectionstatechange', onIce);\n disposalSignal.removeEventListener('abort', onDispose);\n };\n const onOpen = () => {\n cleanup();\n resolve();\n };\n // A close while our open deadline is still armed is the timeout it\n // effectively is; mirror the guest's open-timer guard so both ends agree\n // on the cause instead of racing to report whichever event landed first.\n const onClose = () => fail(timeout());\n const onIce = () => {\n const state = pc.iceConnectionState;\n if (state === 'failed' || state === 'closed' || state === 'disconnected') {\n fail(new MeshConnectionError(`ICE connection ${state} before channel open`, peerId));\n }\n };\n const onDispose = () => fail(new MeshDisposedError());\n timer = setTimeout(() => fail(timeout()), timeoutMs);\n dc.addEventListener('open', onOpen);\n dc.addEventListener('close', onClose);\n pc.addEventListener('iceconnectionstatechange', onIce);\n disposalSignal.addEventListener('abort', onDispose, { once: true });\n });\n}\n"],"mappings":"yHAOA,IAAa,EAAmC,CAC9C,qBAAqB,EAAQ,CAC3B,IAAM,EAAQ,WAAgE,kBAC9E,GAAI,CAAC,EACH,MAAM,IAAI,EAAqB,oEAAoE,EAErG,OAAO,IAAI,EAAK,CAAM,CACxB,CACF,EAMA,SAAgB,aAAa,EAAqB,CAChD,IAAM,EAAa,EAAI,QAAQ,SAAU;CAAM,EAC/C,OAAO,EAAW,SAAS;CAAM,EAAI,EAAa,GAAG,EAAW,KAClE,CAMA,SAAgB,iBAAiB,EAA2B,EAAkC,CAE5F,OADI,EAAG,oBAAsB,WAAmB,QAAQ,QAAQ,EACzD,IAAI,QAAS,GAAY,CAC9B,IAAM,YAAgB,CACpB,aAAa,CAAK,EAClB,EAAG,oBAAoB,0BAA2B,QAAQ,CAC5D,EACM,aAAiB,CACjB,EAAG,oBAAsB,aAC3B,QAAQ,EACR,EAAQ,EAEZ,EACM,EAAQ,eAAiB,CAC7B,QAAQ,EACR,EAAQ,CACV,EAAG,CAAS,EACZ,EAAG,iBAAiB,0BAA2B,QAAQ,CACzD,CAAC,CACH,CASA,SAAgB,gBACd,EACA,EACA,EACA,EACA,EACe,CAEf,OADI,EAAG,aAAe,OAAe,QAAQ,QAAQ,EAC9C,IAAI,SAAS,EAAS,IAAW,CACtC,IAAI,EAA8C,KAC5C,YAAgB,IAAI,EAAiB,qBAAqB,EAAU,gCAAgC,EACpG,KAAQ,GAAiB,CAC7B,QAAQ,EACR,EAAO,CAAK,CACd,EACM,YAAgB,CAChB,GAAO,aAAa,CAAK,EAC7B,EAAQ,KACR,EAAG,oBAAoB,OAAQ,MAAM,EACrC,EAAG,oBAAoB,QAAS,OAAO,EACvC,EAAG,oBAAoB,2BAA4B,KAAK,EACxD,EAAe,oBAAoB,QAAS,SAAS,CACvD,EACM,WAAe,CACnB,QAAQ,EACR,EAAQ,CACV,EAIM,YAAgB,KAAK,QAAQ,CAAC,EAC9B,UAAc,CAClB,IAAM,EAAQ,EAAG,oBACb,IAAU,UAAY,IAAU,UAAY,IAAU,iBACxD,KAAK,IAAI,EAAoB,kBAAkB,EAAM,sBAAuB,CAAM,CAAC,CAEvF,EACM,cAAkB,KAAK,IAAI,CAAmB,EACpD,EAAQ,eAAiB,KAAK,QAAQ,CAAC,EAAG,CAAS,EACnD,EAAG,iBAAiB,OAAQ,MAAM,EAClC,EAAG,iBAAiB,QAAS,OAAO,EACpC,EAAG,iBAAiB,2BAA4B,KAAK,EACrD,EAAe,iBAAiB,QAAS,UAAW,CAAE,KAAM,EAAK,CAAC,CACpE,CAAC,CACH"}
package/dist/_sdp.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"_sdp.cjs","names":[],"sources":["../src/_sdp.ts"],"sourcesContent":["import { MeshPairingError } from './errors';\n\n/**\n * Compact SDP serialization for pairing payloads. A data-channel-only\n * SDP is mostly boilerplate; the remote side only needs the ICE credentials,\n * DTLS fingerprint, setup role, mid, and candidates. `compactSdp` extracts\n * those fields and `expandSdp` rebuilds a minimal valid SDP for\n * `setRemoteDescription`.\n *\n * `compactSdp` returns `null` for shapes it cannot safely represent\n * (multiple m-lines, missing credentials), letting callers keep the raw SDP.\n */\n\nexport interface CompactSdp {\n /** `a=candidate:` lines, prefix stripped. */\n c: string[];\n /** sha-256 fingerprint, hex without colons. */\n f: string;\n /** `a=mid`. */\n m: string;\n /** `a=max-message-size`. */\n mm?: number;\n /** `a=ice-pwd`. */\n p: string;\n /** `a=setup` role. */\n s: string;\n /** `a=sctp-port`. */\n sp?: number;\n /** `a=ice-ufrag`. */\n u: string;\n}\n\nfunction isCompactSdp(value: unknown): value is CompactSdp {\n if (typeof value !== 'object' || value === null) return false;\n const c = value as Record<string, unknown>;\n return (\n typeof c.u === 'string' &&\n typeof c.p === 'string' &&\n typeof c.f === 'string' &&\n typeof c.s === 'string' &&\n typeof c.m === 'string' &&\n Array.isArray(c.c) &&\n c.c.every((line) => typeof line === 'string')\n );\n}\n\n/** Returns the expanded SDP unchanged when `value` is already a string. */\nexport function restoreSdp(value: unknown): string {\n if (typeof value === 'string') return value;\n if (isCompactSdp(value)) return expandSdp(value);\n throw new MeshPairingError('Malformed pairing payload');\n}\n\nexport function compactSdp(sdp: string): CompactSdp | null {\n let ufrag = '';\n let pwd = '';\n let fingerprint = '';\n let setup = '';\n let mid = '0';\n let sctpPort: number | undefined;\n let maxMessageSize: number | undefined;\n const candidates: string[] = [];\n let mLines = 0;\n\n for (const line of sdp.split(/\\r?\\n/)) {\n if (line.startsWith('m=')) mLines += 1;\n else if (line.startsWith('a=ice-ufrag:')) ufrag = line.slice(12);\n else if (line.startsWith('a=ice-pwd:')) pwd = line.slice(10);\n else if (line.startsWith('a=fingerprint:sha-256 ')) fingerprint = line.slice(22).replaceAll(':', '').toUpperCase();\n else if (line.startsWith('a=setup:')) setup = line.slice(8);\n else if (line.startsWith('a=mid:')) mid = line.slice(6);\n else if (line.startsWith('a=sctp-port:')) sctpPort = Number(line.slice(12));\n else if (line.startsWith('a=max-message-size:')) maxMessageSize = Number(line.slice(19));\n else if (line.startsWith('a=candidate:')) candidates.push(line.slice(12));\n }\n\n if (mLines !== 1 || !ufrag || !pwd || !fingerprint) return null;\n return {\n c: candidates,\n f: fingerprint,\n m: mid,\n p: pwd,\n s: setup || 'actpass',\n ...(maxMessageSize ? { mm: maxMessageSize } : {}),\n ...(sctpPort ? { sp: sctpPort } : {}),\n u: ufrag,\n };\n}\n\n/**\n * Canonical signing form: the compact serialization when the SDP is\n * recognizable, else the raw text. `compactSdp` is lossless over the fields\n * it keeps, so the rebuilt `mq2.` SDP canonicalizes identically to the\n * original — a proof computed over one verifies against the other.\n */\nexport function canonicalSdp(sdp: string): string {\n const compact = compactSdp(sdp);\n return compact ? JSON.stringify(compact) : sdp;\n}\n\nexport function expandSdp(compact: CompactSdp): string {\n const colonHex = compact.f.replace(/(..)/g, '$1:').slice(0, -1);\n return `${[\n 'v=0',\n 'o=- 0 2 IN IP4 0.0.0.0',\n 's=-',\n 't=0 0',\n `a=group:BUNDLE ${compact.m}`,\n 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',\n 'c=IN IP4 0.0.0.0',\n `a=ice-ufrag:${compact.u}`,\n `a=ice-pwd:${compact.p}`,\n `a=fingerprint:sha-256 ${colonHex}`,\n `a=setup:${compact.s}`,\n `a=mid:${compact.m}`,\n `a=sctp-port:${compact.sp ?? 5000}`,\n ...(compact.mm ? [`a=max-message-size:${compact.mm}`] : []),\n ...compact.c.map((line) => `a=candidate:${line}`),\n '',\n ].join('\\r\\n')}`;\n}\n"],"mappings":"gCAgCA,SAAS,aAAa,EAAqC,CACzD,GAAI,OAAO,GAAU,WAAY,EAAgB,MAAO,GACxD,IAAM,EAAI,EACV,OACE,OAAO,EAAE,GAAM,UACf,OAAO,EAAE,GAAM,UACf,OAAO,EAAE,GAAM,UACf,OAAO,EAAE,GAAM,UACf,OAAO,EAAE,GAAM,UACf,MAAM,QAAQ,EAAE,CAAC,GACjB,EAAE,EAAE,MAAO,GAAS,OAAO,GAAS,QAAQ,CAEhD,CAGA,SAAgB,WAAW,EAAwB,CACjD,GAAI,OAAO,GAAU,SAAU,OAAO,EACtC,GAAI,aAAa,CAAK,EAAG,OAAO,UAAU,CAAK,EAC/C,MAAM,IAAI,EAAA,iBAAiB,2BAA2B,CACxD,CAEA,SAAgB,WAAW,EAAgC,CACzD,IAAI,EAAQ,GACR,EAAM,GACN,EAAc,GACd,EAAQ,GACR,EAAM,IACN,EACA,EACE,EAAuB,CAAC,EAC1B,EAAS,EAEb,IAAK,IAAM,KAAQ,EAAI,MAAM,OAAO,EAC9B,EAAK,WAAW,IAAI,EAAG,GAAU,EAC5B,EAAK,WAAW,cAAc,EAAG,EAAQ,EAAK,MAAM,EAAE,EACtD,EAAK,WAAW,YAAY,EAAG,EAAM,EAAK,MAAM,EAAE,EAClD,EAAK,WAAW,wBAAwB,EAAG,EAAc,EAAK,MAAM,EAAE,CAAC,CAAC,WAAW,IAAK,EAAE,CAAC,CAAC,YAAY,EACxG,EAAK,WAAW,UAAU,EAAG,EAAQ,EAAK,MAAM,CAAC,EACjD,EAAK,WAAW,QAAQ,EAAG,EAAM,EAAK,MAAM,CAAC,EAC7C,EAAK,WAAW,cAAc,EAAG,EAAW,OAAO,EAAK,MAAM,EAAE,CAAC,EACjE,EAAK,WAAW,qBAAqB,EAAG,EAAiB,OAAO,EAAK,MAAM,EAAE,CAAC,EAC9E,EAAK,WAAW,cAAc,GAAG,EAAW,KAAK,EAAK,MAAM,EAAE,CAAC,EAI1E,OADI,IAAW,GAAK,CAAC,GAAS,CAAC,GAAO,CAAC,EAAoB,KACpD,CACF,EACH,EAAG,EACH,EAAG,EACH,EAAG,EACH,EAAG,GAAS,UACZ,GAAI,EAAiB,CAAE,GAAI,CAAe,EAAI,CAAC,EAC/C,GAAI,EAAW,CAAE,GAAI,CAAS,EAAI,CAAC,EACnC,EAAG,CACL,CACF,CAQA,SAAgB,aAAa,EAAqB,CAChD,IAAM,EAAU,WAAW,CAAG,EAC9B,OAAO,EAAU,KAAK,UAAU,CAAO,EAAI,CAC7C,CAEA,SAAgB,UAAU,EAA6B,CACrD,IAAM,EAAW,EAAQ,EAAE,QAAQ,QAAS,KAAK,CAAC,CAAC,MAAM,EAAG,EAAE,EAC9D,MAAO,GAAG,CACR,MACA,yBACA,MACA,QACA,kBAAkB,EAAQ,IAC1B,mDACA,mBACA,eAAe,EAAQ,IACvB,aAAa,EAAQ,IACrB,yBAAyB,IACzB,WAAW,EAAQ,IACnB,SAAS,EAAQ,IACjB,eAAe,EAAQ,IAAM,MAC7B,GAAI,EAAQ,GAAK,CAAC,sBAAsB,EAAQ,IAAI,EAAI,CAAC,EACzD,GAAG,EAAQ,EAAE,IAAK,GAAS,eAAe,GAAM,EAChD,EACF,CAAC,CAAC,KAAK;CAAM,GACf"}
1
+ {"version":3,"file":"_sdp.cjs","names":[],"sources":["../src/_sdp.ts"],"sourcesContent":["import { MeshPairingError } from './errors';\n\n/**\n * Compact SDP serialization for pairing payloads. A data-channel-only\n * SDP is mostly boilerplate; the remote side only needs the ICE credentials,\n * DTLS fingerprint, setup role, mid, and candidates. `compactSdp` extracts\n * those fields and `expandSdp` rebuilds a minimal valid SDP for\n * `setRemoteDescription`.\n *\n * `compactSdp` returns `null` for shapes it cannot safely represent\n * (multiple m-lines, missing credentials), letting callers keep the raw SDP.\n */\n\nexport interface CompactSdp {\n /** `a=candidate:` lines, prefix stripped. */\n c: string[];\n /** sha-256 fingerprint, hex without colons. */\n f: string;\n /** `a=mid`. */\n m: string;\n /** `a=max-message-size`. */\n mm?: number;\n /** `a=ice-pwd`. */\n p: string;\n /** `a=setup` role. */\n s: string;\n /** `a=sctp-port`. */\n sp?: number;\n /** `a=ice-ufrag`. */\n u: string;\n}\n\nfunction isCompactSdp(value: unknown): value is CompactSdp {\n if (typeof value !== 'object' || value === null) return false;\n const c = value as Record<string, unknown>;\n return (\n typeof c.u === 'string' &&\n typeof c.p === 'string' &&\n typeof c.f === 'string' &&\n typeof c.s === 'string' &&\n typeof c.m === 'string' &&\n Array.isArray(c.c) &&\n c.c.every((line) => typeof line === 'string')\n );\n}\n\n/** Returns the expanded SDP unchanged when `value` is already a string. */\nexport function restoreSdp(value: unknown): string {\n if (typeof value === 'string') return value;\n if (isCompactSdp(value)) return expandSdp(value);\n throw new MeshPairingError('Malformed pairing payload');\n}\n\nexport function compactSdp(sdp: string): CompactSdp | null {\n let ufrag = '';\n let pwd = '';\n let fingerprint = '';\n let setup = '';\n let mid = '0';\n let sctpPort: number | undefined;\n let maxMessageSize: number | undefined;\n const candidates: string[] = [];\n let mLines = 0;\n\n for (const line of sdp.split(/\\r?\\n/)) {\n if (line.startsWith('m=')) mLines += 1;\n else if (line.startsWith('a=ice-ufrag:')) ufrag = line.slice(12);\n else if (line.startsWith('a=ice-pwd:')) pwd = line.slice(10);\n else if (line.startsWith('a=fingerprint:sha-256 ')) fingerprint = line.slice(22).replaceAll(':', '').toUpperCase();\n else if (line.startsWith('a=setup:')) setup = line.slice(8);\n else if (line.startsWith('a=mid:')) mid = line.slice(6);\n else if (line.startsWith('a=sctp-port:')) sctpPort = Number(line.slice(12));\n else if (line.startsWith('a=max-message-size:')) maxMessageSize = Number(line.slice(19));\n else if (line.startsWith('a=candidate:')) candidates.push(line.slice(12));\n }\n\n if (mLines !== 1 || !ufrag || !pwd || !fingerprint) return null;\n return {\n c: candidates,\n f: fingerprint,\n m: mid,\n p: pwd,\n s: setup || 'actpass',\n ...(maxMessageSize ? { mm: maxMessageSize } : {}),\n ...(sctpPort ? { sp: sctpPort } : {}),\n u: ufrag,\n };\n}\n\n/**\n * Canonical signing form: the compact serialization when the SDP is\n * recognizable, else the raw text. `compactSdp` is lossless over the fields\n * it keeps, so the rebuilt `mq2.` SDP canonicalizes identically to the\n * original: a proof computed over one verifies against the other.\n */\nexport function canonicalSdp(sdp: string): string {\n const compact = compactSdp(sdp);\n return compact ? JSON.stringify(compact) : sdp;\n}\n\nexport function expandSdp(compact: CompactSdp): string {\n const colonHex = compact.f.replace(/(..)/g, '$1:').slice(0, -1);\n return `${[\n 'v=0',\n 'o=- 0 2 IN IP4 0.0.0.0',\n 's=-',\n 't=0 0',\n `a=group:BUNDLE ${compact.m}`,\n 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',\n 'c=IN IP4 0.0.0.0',\n `a=ice-ufrag:${compact.u}`,\n `a=ice-pwd:${compact.p}`,\n `a=fingerprint:sha-256 ${colonHex}`,\n `a=setup:${compact.s}`,\n `a=mid:${compact.m}`,\n `a=sctp-port:${compact.sp ?? 5000}`,\n ...(compact.mm ? [`a=max-message-size:${compact.mm}`] : []),\n ...compact.c.map((line) => `a=candidate:${line}`),\n '',\n ].join('\\r\\n')}`;\n}\n"],"mappings":"gCAgCA,SAAS,aAAa,EAAqC,CACzD,GAAI,OAAO,GAAU,WAAY,EAAgB,MAAO,GACxD,IAAM,EAAI,EACV,OACE,OAAO,EAAE,GAAM,UACf,OAAO,EAAE,GAAM,UACf,OAAO,EAAE,GAAM,UACf,OAAO,EAAE,GAAM,UACf,OAAO,EAAE,GAAM,UACf,MAAM,QAAQ,EAAE,CAAC,GACjB,EAAE,EAAE,MAAO,GAAS,OAAO,GAAS,QAAQ,CAEhD,CAGA,SAAgB,WAAW,EAAwB,CACjD,GAAI,OAAO,GAAU,SAAU,OAAO,EACtC,GAAI,aAAa,CAAK,EAAG,OAAO,UAAU,CAAK,EAC/C,MAAM,IAAI,EAAA,iBAAiB,2BAA2B,CACxD,CAEA,SAAgB,WAAW,EAAgC,CACzD,IAAI,EAAQ,GACR,EAAM,GACN,EAAc,GACd,EAAQ,GACR,EAAM,IACN,EACA,EACE,EAAuB,CAAC,EAC1B,EAAS,EAEb,IAAK,IAAM,KAAQ,EAAI,MAAM,OAAO,EAC9B,EAAK,WAAW,IAAI,EAAG,GAAU,EAC5B,EAAK,WAAW,cAAc,EAAG,EAAQ,EAAK,MAAM,EAAE,EACtD,EAAK,WAAW,YAAY,EAAG,EAAM,EAAK,MAAM,EAAE,EAClD,EAAK,WAAW,wBAAwB,EAAG,EAAc,EAAK,MAAM,EAAE,CAAC,CAAC,WAAW,IAAK,EAAE,CAAC,CAAC,YAAY,EACxG,EAAK,WAAW,UAAU,EAAG,EAAQ,EAAK,MAAM,CAAC,EACjD,EAAK,WAAW,QAAQ,EAAG,EAAM,EAAK,MAAM,CAAC,EAC7C,EAAK,WAAW,cAAc,EAAG,EAAW,OAAO,EAAK,MAAM,EAAE,CAAC,EACjE,EAAK,WAAW,qBAAqB,EAAG,EAAiB,OAAO,EAAK,MAAM,EAAE,CAAC,EAC9E,EAAK,WAAW,cAAc,GAAG,EAAW,KAAK,EAAK,MAAM,EAAE,CAAC,EAI1E,OADI,IAAW,GAAK,CAAC,GAAS,CAAC,GAAO,CAAC,EAAoB,KACpD,CACF,EACH,EAAG,EACH,EAAG,EACH,EAAG,EACH,EAAG,GAAS,UACZ,GAAI,EAAiB,CAAE,GAAI,CAAe,EAAI,CAAC,EAC/C,GAAI,EAAW,CAAE,GAAI,CAAS,EAAI,CAAC,EACnC,EAAG,CACL,CACF,CAQA,SAAgB,aAAa,EAAqB,CAChD,IAAM,EAAU,WAAW,CAAG,EAC9B,OAAO,EAAU,KAAK,UAAU,CAAO,EAAI,CAC7C,CAEA,SAAgB,UAAU,EAA6B,CACrD,IAAM,EAAW,EAAQ,EAAE,QAAQ,QAAS,KAAK,CAAC,CAAC,MAAM,EAAG,EAAE,EAC9D,MAAO,GAAG,CACR,MACA,yBACA,MACA,QACA,kBAAkB,EAAQ,IAC1B,mDACA,mBACA,eAAe,EAAQ,IACvB,aAAa,EAAQ,IACrB,yBAAyB,IACzB,WAAW,EAAQ,IACnB,SAAS,EAAQ,IACjB,eAAe,EAAQ,IAAM,MAC7B,GAAI,EAAQ,GAAK,CAAC,sBAAsB,EAAQ,IAAI,EAAI,CAAC,EACzD,GAAG,EAAQ,EAAE,IAAK,GAAS,eAAe,GAAM,EAChD,EACF,CAAC,CAAC,KAAK;CAAM,GACf"}
package/dist/_sdp.d.ts CHANGED
@@ -33,7 +33,7 @@ export declare function compactSdp(sdp: string): CompactSdp | null;
33
33
  * Canonical signing form: the compact serialization when the SDP is
34
34
  * recognizable, else the raw text. `compactSdp` is lossless over the fields
35
35
  * it keeps, so the rebuilt `mq2.` SDP canonicalizes identically to the
36
- * original — a proof computed over one verifies against the other.
36
+ * original: a proof computed over one verifies against the other.
37
37
  */
38
38
  export declare function canonicalSdp(sdp: string): string;
39
39
  export declare function expandSdp(compact: CompactSdp): string;
package/dist/_sdp.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"_sdp.js","names":[],"sources":["../src/_sdp.ts"],"sourcesContent":["import { MeshPairingError } from './errors';\n\n/**\n * Compact SDP serialization for pairing payloads. A data-channel-only\n * SDP is mostly boilerplate; the remote side only needs the ICE credentials,\n * DTLS fingerprint, setup role, mid, and candidates. `compactSdp` extracts\n * those fields and `expandSdp` rebuilds a minimal valid SDP for\n * `setRemoteDescription`.\n *\n * `compactSdp` returns `null` for shapes it cannot safely represent\n * (multiple m-lines, missing credentials), letting callers keep the raw SDP.\n */\n\nexport interface CompactSdp {\n /** `a=candidate:` lines, prefix stripped. */\n c: string[];\n /** sha-256 fingerprint, hex without colons. */\n f: string;\n /** `a=mid`. */\n m: string;\n /** `a=max-message-size`. */\n mm?: number;\n /** `a=ice-pwd`. */\n p: string;\n /** `a=setup` role. */\n s: string;\n /** `a=sctp-port`. */\n sp?: number;\n /** `a=ice-ufrag`. */\n u: string;\n}\n\nfunction isCompactSdp(value: unknown): value is CompactSdp {\n if (typeof value !== 'object' || value === null) return false;\n const c = value as Record<string, unknown>;\n return (\n typeof c.u === 'string' &&\n typeof c.p === 'string' &&\n typeof c.f === 'string' &&\n typeof c.s === 'string' &&\n typeof c.m === 'string' &&\n Array.isArray(c.c) &&\n c.c.every((line) => typeof line === 'string')\n );\n}\n\n/** Returns the expanded SDP unchanged when `value` is already a string. */\nexport function restoreSdp(value: unknown): string {\n if (typeof value === 'string') return value;\n if (isCompactSdp(value)) return expandSdp(value);\n throw new MeshPairingError('Malformed pairing payload');\n}\n\nexport function compactSdp(sdp: string): CompactSdp | null {\n let ufrag = '';\n let pwd = '';\n let fingerprint = '';\n let setup = '';\n let mid = '0';\n let sctpPort: number | undefined;\n let maxMessageSize: number | undefined;\n const candidates: string[] = [];\n let mLines = 0;\n\n for (const line of sdp.split(/\\r?\\n/)) {\n if (line.startsWith('m=')) mLines += 1;\n else if (line.startsWith('a=ice-ufrag:')) ufrag = line.slice(12);\n else if (line.startsWith('a=ice-pwd:')) pwd = line.slice(10);\n else if (line.startsWith('a=fingerprint:sha-256 ')) fingerprint = line.slice(22).replaceAll(':', '').toUpperCase();\n else if (line.startsWith('a=setup:')) setup = line.slice(8);\n else if (line.startsWith('a=mid:')) mid = line.slice(6);\n else if (line.startsWith('a=sctp-port:')) sctpPort = Number(line.slice(12));\n else if (line.startsWith('a=max-message-size:')) maxMessageSize = Number(line.slice(19));\n else if (line.startsWith('a=candidate:')) candidates.push(line.slice(12));\n }\n\n if (mLines !== 1 || !ufrag || !pwd || !fingerprint) return null;\n return {\n c: candidates,\n f: fingerprint,\n m: mid,\n p: pwd,\n s: setup || 'actpass',\n ...(maxMessageSize ? { mm: maxMessageSize } : {}),\n ...(sctpPort ? { sp: sctpPort } : {}),\n u: ufrag,\n };\n}\n\n/**\n * Canonical signing form: the compact serialization when the SDP is\n * recognizable, else the raw text. `compactSdp` is lossless over the fields\n * it keeps, so the rebuilt `mq2.` SDP canonicalizes identically to the\n * original — a proof computed over one verifies against the other.\n */\nexport function canonicalSdp(sdp: string): string {\n const compact = compactSdp(sdp);\n return compact ? JSON.stringify(compact) : sdp;\n}\n\nexport function expandSdp(compact: CompactSdp): string {\n const colonHex = compact.f.replace(/(..)/g, '$1:').slice(0, -1);\n return `${[\n 'v=0',\n 'o=- 0 2 IN IP4 0.0.0.0',\n 's=-',\n 't=0 0',\n `a=group:BUNDLE ${compact.m}`,\n 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',\n 'c=IN IP4 0.0.0.0',\n `a=ice-ufrag:${compact.u}`,\n `a=ice-pwd:${compact.p}`,\n `a=fingerprint:sha-256 ${colonHex}`,\n `a=setup:${compact.s}`,\n `a=mid:${compact.m}`,\n `a=sctp-port:${compact.sp ?? 5000}`,\n ...(compact.mm ? [`a=max-message-size:${compact.mm}`] : []),\n ...compact.c.map((line) => `a=candidate:${line}`),\n '',\n ].join('\\r\\n')}`;\n}\n"],"mappings":"+CAgCA,SAAS,aAAa,EAAqC,CACzD,GAAI,OAAO,GAAU,WAAY,EAAgB,MAAO,GACxD,IAAM,EAAI,EACV,OACE,OAAO,EAAE,GAAM,UACf,OAAO,EAAE,GAAM,UACf,OAAO,EAAE,GAAM,UACf,OAAO,EAAE,GAAM,UACf,OAAO,EAAE,GAAM,UACf,MAAM,QAAQ,EAAE,CAAC,GACjB,EAAE,EAAE,MAAO,GAAS,OAAO,GAAS,QAAQ,CAEhD,CAGA,SAAgB,WAAW,EAAwB,CACjD,GAAI,OAAO,GAAU,SAAU,OAAO,EACtC,GAAI,aAAa,CAAK,EAAG,OAAO,UAAU,CAAK,EAC/C,MAAM,IAAI,EAAiB,2BAA2B,CACxD,CAEA,SAAgB,WAAW,EAAgC,CACzD,IAAI,EAAQ,GACR,EAAM,GACN,EAAc,GACd,EAAQ,GACR,EAAM,IACN,EACA,EACE,EAAuB,CAAC,EAC1B,EAAS,EAEb,IAAK,IAAM,KAAQ,EAAI,MAAM,OAAO,EAC9B,EAAK,WAAW,IAAI,EAAG,GAAU,EAC5B,EAAK,WAAW,cAAc,EAAG,EAAQ,EAAK,MAAM,EAAE,EACtD,EAAK,WAAW,YAAY,EAAG,EAAM,EAAK,MAAM,EAAE,EAClD,EAAK,WAAW,wBAAwB,EAAG,EAAc,EAAK,MAAM,EAAE,CAAC,CAAC,WAAW,IAAK,EAAE,CAAC,CAAC,YAAY,EACxG,EAAK,WAAW,UAAU,EAAG,EAAQ,EAAK,MAAM,CAAC,EACjD,EAAK,WAAW,QAAQ,EAAG,EAAM,EAAK,MAAM,CAAC,EAC7C,EAAK,WAAW,cAAc,EAAG,EAAW,OAAO,EAAK,MAAM,EAAE,CAAC,EACjE,EAAK,WAAW,qBAAqB,EAAG,EAAiB,OAAO,EAAK,MAAM,EAAE,CAAC,EAC9E,EAAK,WAAW,cAAc,GAAG,EAAW,KAAK,EAAK,MAAM,EAAE,CAAC,EAI1E,OADI,IAAW,GAAK,CAAC,GAAS,CAAC,GAAO,CAAC,EAAoB,KACpD,CACF,EACH,EAAG,EACH,EAAG,EACH,EAAG,EACH,EAAG,GAAS,UACZ,GAAI,EAAiB,CAAE,GAAI,CAAe,EAAI,CAAC,EAC/C,GAAI,EAAW,CAAE,GAAI,CAAS,EAAI,CAAC,EACnC,EAAG,CACL,CACF,CAQA,SAAgB,aAAa,EAAqB,CAChD,IAAM,EAAU,WAAW,CAAG,EAC9B,OAAO,EAAU,KAAK,UAAU,CAAO,EAAI,CAC7C,CAEA,SAAgB,UAAU,EAA6B,CACrD,IAAM,EAAW,EAAQ,EAAE,QAAQ,QAAS,KAAK,CAAC,CAAC,MAAM,EAAG,EAAE,EAC9D,MAAO,GAAG,CACR,MACA,yBACA,MACA,QACA,kBAAkB,EAAQ,IAC1B,mDACA,mBACA,eAAe,EAAQ,IACvB,aAAa,EAAQ,IACrB,yBAAyB,IACzB,WAAW,EAAQ,IACnB,SAAS,EAAQ,IACjB,eAAe,EAAQ,IAAM,MAC7B,GAAI,EAAQ,GAAK,CAAC,sBAAsB,EAAQ,IAAI,EAAI,CAAC,EACzD,GAAG,EAAQ,EAAE,IAAK,GAAS,eAAe,GAAM,EAChD,EACF,CAAC,CAAC,KAAK;CAAM,GACf"}
1
+ {"version":3,"file":"_sdp.js","names":[],"sources":["../src/_sdp.ts"],"sourcesContent":["import { MeshPairingError } from './errors';\n\n/**\n * Compact SDP serialization for pairing payloads. A data-channel-only\n * SDP is mostly boilerplate; the remote side only needs the ICE credentials,\n * DTLS fingerprint, setup role, mid, and candidates. `compactSdp` extracts\n * those fields and `expandSdp` rebuilds a minimal valid SDP for\n * `setRemoteDescription`.\n *\n * `compactSdp` returns `null` for shapes it cannot safely represent\n * (multiple m-lines, missing credentials), letting callers keep the raw SDP.\n */\n\nexport interface CompactSdp {\n /** `a=candidate:` lines, prefix stripped. */\n c: string[];\n /** sha-256 fingerprint, hex without colons. */\n f: string;\n /** `a=mid`. */\n m: string;\n /** `a=max-message-size`. */\n mm?: number;\n /** `a=ice-pwd`. */\n p: string;\n /** `a=setup` role. */\n s: string;\n /** `a=sctp-port`. */\n sp?: number;\n /** `a=ice-ufrag`. */\n u: string;\n}\n\nfunction isCompactSdp(value: unknown): value is CompactSdp {\n if (typeof value !== 'object' || value === null) return false;\n const c = value as Record<string, unknown>;\n return (\n typeof c.u === 'string' &&\n typeof c.p === 'string' &&\n typeof c.f === 'string' &&\n typeof c.s === 'string' &&\n typeof c.m === 'string' &&\n Array.isArray(c.c) &&\n c.c.every((line) => typeof line === 'string')\n );\n}\n\n/** Returns the expanded SDP unchanged when `value` is already a string. */\nexport function restoreSdp(value: unknown): string {\n if (typeof value === 'string') return value;\n if (isCompactSdp(value)) return expandSdp(value);\n throw new MeshPairingError('Malformed pairing payload');\n}\n\nexport function compactSdp(sdp: string): CompactSdp | null {\n let ufrag = '';\n let pwd = '';\n let fingerprint = '';\n let setup = '';\n let mid = '0';\n let sctpPort: number | undefined;\n let maxMessageSize: number | undefined;\n const candidates: string[] = [];\n let mLines = 0;\n\n for (const line of sdp.split(/\\r?\\n/)) {\n if (line.startsWith('m=')) mLines += 1;\n else if (line.startsWith('a=ice-ufrag:')) ufrag = line.slice(12);\n else if (line.startsWith('a=ice-pwd:')) pwd = line.slice(10);\n else if (line.startsWith('a=fingerprint:sha-256 ')) fingerprint = line.slice(22).replaceAll(':', '').toUpperCase();\n else if (line.startsWith('a=setup:')) setup = line.slice(8);\n else if (line.startsWith('a=mid:')) mid = line.slice(6);\n else if (line.startsWith('a=sctp-port:')) sctpPort = Number(line.slice(12));\n else if (line.startsWith('a=max-message-size:')) maxMessageSize = Number(line.slice(19));\n else if (line.startsWith('a=candidate:')) candidates.push(line.slice(12));\n }\n\n if (mLines !== 1 || !ufrag || !pwd || !fingerprint) return null;\n return {\n c: candidates,\n f: fingerprint,\n m: mid,\n p: pwd,\n s: setup || 'actpass',\n ...(maxMessageSize ? { mm: maxMessageSize } : {}),\n ...(sctpPort ? { sp: sctpPort } : {}),\n u: ufrag,\n };\n}\n\n/**\n * Canonical signing form: the compact serialization when the SDP is\n * recognizable, else the raw text. `compactSdp` is lossless over the fields\n * it keeps, so the rebuilt `mq2.` SDP canonicalizes identically to the\n * original: a proof computed over one verifies against the other.\n */\nexport function canonicalSdp(sdp: string): string {\n const compact = compactSdp(sdp);\n return compact ? JSON.stringify(compact) : sdp;\n}\n\nexport function expandSdp(compact: CompactSdp): string {\n const colonHex = compact.f.replace(/(..)/g, '$1:').slice(0, -1);\n return `${[\n 'v=0',\n 'o=- 0 2 IN IP4 0.0.0.0',\n 's=-',\n 't=0 0',\n `a=group:BUNDLE ${compact.m}`,\n 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',\n 'c=IN IP4 0.0.0.0',\n `a=ice-ufrag:${compact.u}`,\n `a=ice-pwd:${compact.p}`,\n `a=fingerprint:sha-256 ${colonHex}`,\n `a=setup:${compact.s}`,\n `a=mid:${compact.m}`,\n `a=sctp-port:${compact.sp ?? 5000}`,\n ...(compact.mm ? [`a=max-message-size:${compact.mm}`] : []),\n ...compact.c.map((line) => `a=candidate:${line}`),\n '',\n ].join('\\r\\n')}`;\n}\n"],"mappings":"+CAgCA,SAAS,aAAa,EAAqC,CACzD,GAAI,OAAO,GAAU,WAAY,EAAgB,MAAO,GACxD,IAAM,EAAI,EACV,OACE,OAAO,EAAE,GAAM,UACf,OAAO,EAAE,GAAM,UACf,OAAO,EAAE,GAAM,UACf,OAAO,EAAE,GAAM,UACf,OAAO,EAAE,GAAM,UACf,MAAM,QAAQ,EAAE,CAAC,GACjB,EAAE,EAAE,MAAO,GAAS,OAAO,GAAS,QAAQ,CAEhD,CAGA,SAAgB,WAAW,EAAwB,CACjD,GAAI,OAAO,GAAU,SAAU,OAAO,EACtC,GAAI,aAAa,CAAK,EAAG,OAAO,UAAU,CAAK,EAC/C,MAAM,IAAI,EAAiB,2BAA2B,CACxD,CAEA,SAAgB,WAAW,EAAgC,CACzD,IAAI,EAAQ,GACR,EAAM,GACN,EAAc,GACd,EAAQ,GACR,EAAM,IACN,EACA,EACE,EAAuB,CAAC,EAC1B,EAAS,EAEb,IAAK,IAAM,KAAQ,EAAI,MAAM,OAAO,EAC9B,EAAK,WAAW,IAAI,EAAG,GAAU,EAC5B,EAAK,WAAW,cAAc,EAAG,EAAQ,EAAK,MAAM,EAAE,EACtD,EAAK,WAAW,YAAY,EAAG,EAAM,EAAK,MAAM,EAAE,EAClD,EAAK,WAAW,wBAAwB,EAAG,EAAc,EAAK,MAAM,EAAE,CAAC,CAAC,WAAW,IAAK,EAAE,CAAC,CAAC,YAAY,EACxG,EAAK,WAAW,UAAU,EAAG,EAAQ,EAAK,MAAM,CAAC,EACjD,EAAK,WAAW,QAAQ,EAAG,EAAM,EAAK,MAAM,CAAC,EAC7C,EAAK,WAAW,cAAc,EAAG,EAAW,OAAO,EAAK,MAAM,EAAE,CAAC,EACjE,EAAK,WAAW,qBAAqB,EAAG,EAAiB,OAAO,EAAK,MAAM,EAAE,CAAC,EAC9E,EAAK,WAAW,cAAc,GAAG,EAAW,KAAK,EAAK,MAAM,EAAE,CAAC,EAI1E,OADI,IAAW,GAAK,CAAC,GAAS,CAAC,GAAO,CAAC,EAAoB,KACpD,CACF,EACH,EAAG,EACH,EAAG,EACH,EAAG,EACH,EAAG,GAAS,UACZ,GAAI,EAAiB,CAAE,GAAI,CAAe,EAAI,CAAC,EAC/C,GAAI,EAAW,CAAE,GAAI,CAAS,EAAI,CAAC,EACnC,EAAG,CACL,CACF,CAQA,SAAgB,aAAa,EAAqB,CAChD,IAAM,EAAU,WAAW,CAAG,EAC9B,OAAO,EAAU,KAAK,UAAU,CAAO,EAAI,CAC7C,CAEA,SAAgB,UAAU,EAA6B,CACrD,IAAM,EAAW,EAAQ,EAAE,QAAQ,QAAS,KAAK,CAAC,CAAC,MAAM,EAAG,EAAE,EAC9D,MAAO,GAAG,CACR,MACA,yBACA,MACA,QACA,kBAAkB,EAAQ,IAC1B,mDACA,mBACA,eAAe,EAAQ,IACvB,aAAa,EAAQ,IACrB,yBAAyB,IACzB,WAAW,EAAQ,IACnB,SAAS,EAAQ,IACjB,eAAe,EAAQ,IAAM,MAC7B,GAAI,EAAQ,GAAK,CAAC,sBAAsB,EAAQ,IAAI,EAAI,CAAC,EACzD,GAAG,EAAQ,EAAE,IAAK,GAAS,eAAe,GAAM,EAChD,EACF,CAAC,CAAC,KAAK;CAAM,GACf"}
@@ -1 +1 @@
1
- {"version":3,"file":"guest.cjs","names":[],"sources":["../src/guest.ts"],"sourcesContent":["import {\n DEFAULT_CHANNEL_OPEN_TIMEOUT_MS,\n DEFAULT_ICE_GATHERING_TIMEOUT_MS,\n DEFAULT_MAX_MESSAGE_BYTES,\n} from './_defaults';\nimport { createMessenger, createNodeCore, createPeerRecord, type PeerRecord } from './_node';\nimport { createProof } from './_proof';\nimport { randomId } from './_random';\nimport { nativeRtcFactory, normalizeSdp, waitIceGathering } from './_rtc';\nimport { MeshConnectionError, MeshDisposedError, type MeshError, MeshPairingError, MeshTimeoutError } from './errors';\nimport type {\n MeshGuest,\n MeshGuestOptions,\n MeshInbound,\n MeshPeer,\n MeshProtocol,\n RTCDataChannelLike,\n RTCPeerConnectionLike,\n} from './types';\n\n/**\n * Create a guest mesh node. The guest pairs with one host by consuming an\n * out-of-band invitation and returning an answer.\n */\nexport function createMeshGuest<P extends MeshProtocol>(options: MeshGuestOptions = {}): MeshGuest<P> {\n const rtc = options.rtc ?? nativeRtcFactory;\n const clock = options.clock ?? (() => Date.now());\n const maxMessageBytes = options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;\n const channelOpenTimeoutMs = options.channelOpenTimeoutMs ?? DEFAULT_CHANNEL_OPEN_TIMEOUT_MS;\n const iceGatheringTimeoutMs = options.iceGatheringTimeoutMs ?? DEFAULT_ICE_GATHERING_TIMEOUT_MS;\n\n const core = createNodeCore(options.signal);\n const messenger = createMessenger({\n clock,\n emitTap: core.emitTap,\n maxMessageBytes,\n random: options.random,\n });\n\n let hostPeer: PeerRecord | null = null;\n let openTimer: ReturnType<typeof setTimeout> | null = null;\n\n const openTimeoutError = (): MeshTimeoutError =>\n new MeshTimeoutError(`Timed out waiting ${channelOpenTimeoutMs}ms for the data channel to open`);\n\n function failPeer(peer: PeerRecord, error: MeshError): void {\n // Ignore events from a superseded peer after re-pairing replaced hostPeer.\n if (core.disposed || peer !== hostPeer || peer.status === 'failed') return;\n peer.status = 'failed';\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n // Teardown is deferred a macrotask so a peer racing its own open\n // deadline reports its timeout rather than this hangup.\n setTimeout(() => {\n peer.dc?.close();\n peer.pc?.close();\n }, 0);\n core.emitTap({ peerId: peer.id, status: 'failed', type: 'peer-status-change' });\n core.emitTap({ error, type: 'error' });\n core.emitTap({ peer: peer.publicPeer, reason: error.message, type: 'peer-left' });\n core.setStatus('failed');\n }\n\n function disconnectPeer(peer: PeerRecord, reason: string): void {\n if (core.disposed || peer !== hostPeer) return;\n if (peer.status === 'disconnected' || peer.status === 'failed') return;\n peer.status = 'disconnected';\n peer.pc?.close();\n core.emitTap({ peerId: peer.id, status: 'disconnected', type: 'peer-status-change' });\n core.emitTap({ peer: peer.publicPeer, reason, type: 'peer-left' });\n core.setStatus('disconnected');\n }\n\n function wireChannel(peer: PeerRecord, pc: RTCPeerConnectionLike, dc: RTCDataChannelLike): void {\n peer.dc = dc;\n\n dc.addEventListener('open', () => {\n if (core.disposed) return;\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n peer.status = 'connected';\n core.emitTap({ peerId: peer.id, status: 'connected', type: 'peer-status-change' });\n core.emitTap({ peer: peer.publicPeer, type: 'peer-joined' });\n core.setStatus('connected');\n });\n dc.addEventListener('message', (event) => {\n if (core.disposed) return;\n messenger.handleInbound(peer, event.data);\n });\n dc.addEventListener('close', () => {\n if (core.disposed) return;\n if (peer.status === 'connecting')\n failPeer(\n peer,\n openTimer ? openTimeoutError() : new MeshConnectionError('channel closed before opening', peer.id),\n );\n else disconnectPeer(peer, 'channel closed');\n });\n dc.addEventListener('error', () => {\n if (core.disposed) return;\n failPeer(peer, new MeshConnectionError('channel error', peer.id));\n });\n\n // The ice-state emission lives on the pairing-scoped listener above; this\n // one owns channel-lifecycle transitions only, so each ICE change emits\n // exactly one ice-state event.\n pc.addEventListener('iceconnectionstatechange', () => {\n if (core.disposed) return;\n const state = pc.iceConnectionState;\n if (state === 'failed' || state === 'closed') {\n failPeer(peer, new MeshConnectionError(`ice ${state}`, peer.id));\n } else if (state === 'disconnected' && peer.status === 'connected') {\n disconnectPeer(peer, 'ice disconnected');\n }\n });\n }\n\n const guest: MeshGuest<P> = {\n async acceptInvitation(invitation, meta) {\n core.ensureLive();\n // A terminal host peer (failed/disconnected) may be replaced by a fresh\n // pairing on the same node — listeners stay attached and keep working.\n if (hostPeer && hostPeer.status !== 'failed' && hostPeer.status !== 'disconnected') {\n throw new MeshPairingError('Guest is already paired');\n }\n if (invitation?.v !== 1) throw new MeshPairingError('Unsupported invitation version');\n if (clock() >= invitation.expiresAt) throw new MeshPairingError('Invitation expired');\n\n if (hostPeer) {\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n hostPeer.pc?.close();\n hostPeer = null;\n }\n\n core.setStatus('pairing');\n let pc: RTCPeerConnectionLike;\n try {\n pc = rtc.createPeerConnection({ iceServers: [...(options.iceServers ?? [])] });\n } catch (error) {\n core.setStatus('idle');\n throw error;\n }\n\n // The invitation carries no host identity beyond an optional display\n // name — the session id doubles as the host-side peer id for inbound\n // metadata and tap events.\n const peer = createPeerRecord(invitation.sessionId, invitation.hostName, 'host');\n peer.pc = pc;\n hostPeer = peer;\n\n pc.addEventListener('datachannel', (event) => {\n wireChannel(peer, pc, event.channel);\n });\n pc.addEventListener('iceconnectionstatechange', () => {\n if (core.disposed) return;\n core.emitTap({ peerId: peer.id, state: pc.iceConnectionState, type: 'ice-state' });\n });\n\n try {\n await pc.setRemoteDescription({ sdp: normalizeSdp(invitation.sdp), type: 'offer' });\n } catch (cause) {\n hostPeer = null;\n core.setStatus('idle');\n throw new MeshPairingError('Invalid invitation SDP', { cause });\n }\n\n let answerSdp: string;\n try {\n await pc.setLocalDescription(await pc.createAnswer());\n await waitIceGathering(pc, iceGatheringTimeoutMs);\n answerSdp = pc.localDescription?.sdp ?? '';\n } catch (cause) {\n hostPeer = null;\n pc.close();\n core.setStatus('idle');\n throw new MeshConnectionError('Failed to create answer', null, { cause });\n }\n if (core.disposed) {\n pc.close();\n throw new MeshDisposedError();\n }\n if (!answerSdp) {\n hostPeer = null;\n pc.close();\n core.setStatus('idle');\n throw new MeshConnectionError('Local description has no SDP');\n }\n\n const peerId = randomId(options.random);\n let proof: string;\n try {\n proof = await createProof(invitation.secret, answerSdp);\n } catch (cause) {\n hostPeer = null;\n pc.close();\n core.setStatus('idle');\n throw cause;\n }\n core.setStatus('connecting');\n\n // If the channel never opens (host never accepts the answer), fail loudly\n // instead of hanging in 'connecting' forever. The deadline must cover\n // the human carry-back of the answer, not just machine negotiation.\n openTimer = setTimeout(() => {\n if (hostPeer?.status === 'connecting') {\n failPeer(hostPeer, openTimeoutError());\n }\n }, channelOpenTimeoutMs);\n\n return {\n peer: meta?.name === undefined ? { id: peerId } : { id: peerId, name: meta.name },\n proof,\n sdp: answerSdp,\n sessionId: invitation.sessionId,\n v: 1,\n };\n },\n get disposalSignal() {\n return core.disposalCtrl.signal;\n },\n\n dispose() {\n if (core.disposed) return;\n core.markDisposed();\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n if (hostPeer) {\n hostPeer.dc?.close();\n hostPeer.pc?.close();\n }\n messenger.clear();\n },\n get disposed() {\n return core.disposed;\n },\n get host(): MeshPeer | null {\n return hostPeer?.publicPeer ?? null;\n },\n\n on(type, handler) {\n core.ensureLive();\n return messenger.on(type, handler as (message: MeshInbound<unknown>) => void);\n },\n\n send(type, payload) {\n core.ensureLive();\n if (!hostPeer) throw new MeshConnectionError('Guest is not paired');\n messenger.sendTo(hostPeer, type, payload);\n },\n get status() {\n return core.status;\n },\n\n tap(handler, tapOptions) {\n return core.tap(handler, tapOptions);\n },\n\n [Symbol.dispose]() {\n this.dispose();\n },\n };\n\n return guest;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"guest.cjs","names":[],"sources":["../src/guest.ts"],"sourcesContent":["import {\n DEFAULT_CHANNEL_OPEN_TIMEOUT_MS,\n DEFAULT_ICE_GATHERING_TIMEOUT_MS,\n DEFAULT_MAX_MESSAGE_BYTES,\n} from './_defaults';\nimport { createMessenger, createNodeCore, createPeerRecord, type PeerRecord } from './_node';\nimport { createProof } from './_proof';\nimport { randomId } from './_random';\nimport { nativeRtcFactory, normalizeSdp, waitIceGathering } from './_rtc';\nimport { MeshConnectionError, MeshDisposedError, type MeshError, MeshPairingError, MeshTimeoutError } from './errors';\nimport type {\n MeshGuest,\n MeshGuestOptions,\n MeshInbound,\n MeshPeer,\n MeshProtocol,\n RTCDataChannelLike,\n RTCPeerConnectionLike,\n} from './types';\n\n/**\n * Create a guest mesh node. The guest pairs with one host by consuming an\n * out-of-band invitation and returning an answer.\n */\nexport function createMeshGuest<P extends MeshProtocol>(options: MeshGuestOptions = {}): MeshGuest<P> {\n const rtc = options.rtc ?? nativeRtcFactory;\n const clock = options.clock ?? (() => Date.now());\n const maxMessageBytes = options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;\n const channelOpenTimeoutMs = options.channelOpenTimeoutMs ?? DEFAULT_CHANNEL_OPEN_TIMEOUT_MS;\n const iceGatheringTimeoutMs = options.iceGatheringTimeoutMs ?? DEFAULT_ICE_GATHERING_TIMEOUT_MS;\n\n const core = createNodeCore(options.signal);\n const messenger = createMessenger({\n clock,\n emitTap: core.emitTap,\n maxMessageBytes,\n random: options.random,\n });\n\n let hostPeer: PeerRecord | null = null;\n let openTimer: ReturnType<typeof setTimeout> | null = null;\n\n const openTimeoutError = (): MeshTimeoutError =>\n new MeshTimeoutError(`Timed out waiting ${channelOpenTimeoutMs}ms for the data channel to open`);\n\n function failPeer(peer: PeerRecord, error: MeshError): void {\n // Ignore events from a superseded peer after re-pairing replaced hostPeer.\n if (core.disposed || peer !== hostPeer || peer.status === 'failed') return;\n peer.status = 'failed';\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n // Teardown is deferred a macrotask so a peer racing its own open\n // deadline reports its timeout rather than this hangup.\n setTimeout(() => {\n peer.dc?.close();\n peer.pc?.close();\n }, 0);\n core.emitTap({ peerId: peer.id, status: 'failed', type: 'peer-status-change' });\n core.emitTap({ error, type: 'error' });\n core.emitTap({ peer: peer.publicPeer, reason: error.message, type: 'peer-left' });\n core.setStatus('failed');\n }\n\n function disconnectPeer(peer: PeerRecord, reason: string): void {\n if (core.disposed || peer !== hostPeer) return;\n if (peer.status === 'disconnected' || peer.status === 'failed') return;\n peer.status = 'disconnected';\n peer.pc?.close();\n core.emitTap({ peerId: peer.id, status: 'disconnected', type: 'peer-status-change' });\n core.emitTap({ peer: peer.publicPeer, reason, type: 'peer-left' });\n core.setStatus('disconnected');\n }\n\n function wireChannel(peer: PeerRecord, pc: RTCPeerConnectionLike, dc: RTCDataChannelLike): void {\n peer.dc = dc;\n\n dc.addEventListener('open', () => {\n if (core.disposed) return;\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n peer.status = 'connected';\n core.emitTap({ peerId: peer.id, status: 'connected', type: 'peer-status-change' });\n core.emitTap({ peer: peer.publicPeer, type: 'peer-joined' });\n core.setStatus('connected');\n });\n dc.addEventListener('message', (event) => {\n if (core.disposed) return;\n messenger.handleInbound(peer, event.data);\n });\n dc.addEventListener('close', () => {\n if (core.disposed) return;\n if (peer.status === 'connecting')\n failPeer(\n peer,\n openTimer ? openTimeoutError() : new MeshConnectionError('channel closed before opening', peer.id),\n );\n else disconnectPeer(peer, 'channel closed');\n });\n dc.addEventListener('error', () => {\n if (core.disposed) return;\n failPeer(peer, new MeshConnectionError('channel error', peer.id));\n });\n\n // The ice-state emission lives on the pairing-scoped listener above; this\n // one owns channel-lifecycle transitions only, so each ICE change emits\n // exactly one ice-state event.\n pc.addEventListener('iceconnectionstatechange', () => {\n if (core.disposed) return;\n const state = pc.iceConnectionState;\n if (state === 'failed' || state === 'closed') {\n failPeer(peer, new MeshConnectionError(`ice ${state}`, peer.id));\n } else if (state === 'disconnected' && peer.status === 'connected') {\n disconnectPeer(peer, 'ice disconnected');\n }\n });\n }\n\n const guest: MeshGuest<P> = {\n async acceptInvitation(invitation, meta) {\n core.ensureLive();\n // A terminal host peer (failed/disconnected) may be replaced by a fresh\n // pairing on the same node: listeners stay attached and keep working.\n if (hostPeer && hostPeer.status !== 'failed' && hostPeer.status !== 'disconnected') {\n throw new MeshPairingError('Guest is already paired');\n }\n if (invitation?.v !== 1) throw new MeshPairingError('Unsupported invitation version');\n if (clock() >= invitation.expiresAt) throw new MeshPairingError('Invitation expired');\n\n if (hostPeer) {\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n hostPeer.pc?.close();\n hostPeer = null;\n }\n\n core.setStatus('pairing');\n let pc: RTCPeerConnectionLike;\n try {\n pc = rtc.createPeerConnection({ iceServers: [...(options.iceServers ?? [])] });\n } catch (error) {\n core.setStatus('idle');\n throw error;\n }\n\n // The invitation carries no host identity beyond an optional display\n // name: the session id doubles as the host-side peer id for inbound\n // metadata and tap events.\n const peer = createPeerRecord(invitation.sessionId, invitation.hostName, 'host');\n peer.pc = pc;\n hostPeer = peer;\n\n pc.addEventListener('datachannel', (event) => {\n wireChannel(peer, pc, event.channel);\n });\n pc.addEventListener('iceconnectionstatechange', () => {\n if (core.disposed) return;\n core.emitTap({ peerId: peer.id, state: pc.iceConnectionState, type: 'ice-state' });\n });\n\n try {\n await pc.setRemoteDescription({ sdp: normalizeSdp(invitation.sdp), type: 'offer' });\n } catch (cause) {\n hostPeer = null;\n core.setStatus('idle');\n throw new MeshPairingError('Invalid invitation SDP', { cause });\n }\n\n let answerSdp: string;\n try {\n await pc.setLocalDescription(await pc.createAnswer());\n await waitIceGathering(pc, iceGatheringTimeoutMs);\n answerSdp = pc.localDescription?.sdp ?? '';\n } catch (cause) {\n hostPeer = null;\n pc.close();\n core.setStatus('idle');\n throw new MeshConnectionError('Failed to create answer', null, { cause });\n }\n if (core.disposed) {\n pc.close();\n throw new MeshDisposedError();\n }\n if (!answerSdp) {\n hostPeer = null;\n pc.close();\n core.setStatus('idle');\n throw new MeshConnectionError('Local description has no SDP');\n }\n\n const peerId = randomId(options.random);\n let proof: string;\n try {\n proof = await createProof(invitation.secret, answerSdp);\n } catch (cause) {\n hostPeer = null;\n pc.close();\n core.setStatus('idle');\n throw cause;\n }\n core.setStatus('connecting');\n\n // If the channel never opens (host never accepts the answer), fail loudly\n // instead of hanging in 'connecting' forever. The deadline must cover\n // the human carry-back of the answer, not just machine negotiation.\n openTimer = setTimeout(() => {\n if (hostPeer?.status === 'connecting') {\n failPeer(hostPeer, openTimeoutError());\n }\n }, channelOpenTimeoutMs);\n\n return {\n peer: meta?.name === undefined ? { id: peerId } : { id: peerId, name: meta.name },\n proof,\n sdp: answerSdp,\n sessionId: invitation.sessionId,\n v: 1,\n };\n },\n get disposalSignal() {\n return core.disposalCtrl.signal;\n },\n\n dispose() {\n if (core.disposed) return;\n core.markDisposed();\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n if (hostPeer) {\n hostPeer.dc?.close();\n hostPeer.pc?.close();\n }\n messenger.clear();\n },\n get disposed() {\n return core.disposed;\n },\n get host(): MeshPeer | null {\n return hostPeer?.publicPeer ?? null;\n },\n\n on(type, handler) {\n core.ensureLive();\n return messenger.on(type, handler as (message: MeshInbound<unknown>) => void);\n },\n\n send(type, payload) {\n core.ensureLive();\n if (!hostPeer) throw new MeshConnectionError('Guest is not paired');\n messenger.sendTo(hostPeer, type, payload);\n },\n get status() {\n return core.status;\n },\n\n tap(handler, tapOptions) {\n return core.tap(handler, tapOptions);\n },\n\n [Symbol.dispose]() {\n this.dispose();\n },\n };\n\n return guest;\n}\n"],"mappings":"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"}
package/dist/guest.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"guest.js","names":[],"sources":["../src/guest.ts"],"sourcesContent":["import {\n DEFAULT_CHANNEL_OPEN_TIMEOUT_MS,\n DEFAULT_ICE_GATHERING_TIMEOUT_MS,\n DEFAULT_MAX_MESSAGE_BYTES,\n} from './_defaults';\nimport { createMessenger, createNodeCore, createPeerRecord, type PeerRecord } from './_node';\nimport { createProof } from './_proof';\nimport { randomId } from './_random';\nimport { nativeRtcFactory, normalizeSdp, waitIceGathering } from './_rtc';\nimport { MeshConnectionError, MeshDisposedError, type MeshError, MeshPairingError, MeshTimeoutError } from './errors';\nimport type {\n MeshGuest,\n MeshGuestOptions,\n MeshInbound,\n MeshPeer,\n MeshProtocol,\n RTCDataChannelLike,\n RTCPeerConnectionLike,\n} from './types';\n\n/**\n * Create a guest mesh node. The guest pairs with one host by consuming an\n * out-of-band invitation and returning an answer.\n */\nexport function createMeshGuest<P extends MeshProtocol>(options: MeshGuestOptions = {}): MeshGuest<P> {\n const rtc = options.rtc ?? nativeRtcFactory;\n const clock = options.clock ?? (() => Date.now());\n const maxMessageBytes = options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;\n const channelOpenTimeoutMs = options.channelOpenTimeoutMs ?? DEFAULT_CHANNEL_OPEN_TIMEOUT_MS;\n const iceGatheringTimeoutMs = options.iceGatheringTimeoutMs ?? DEFAULT_ICE_GATHERING_TIMEOUT_MS;\n\n const core = createNodeCore(options.signal);\n const messenger = createMessenger({\n clock,\n emitTap: core.emitTap,\n maxMessageBytes,\n random: options.random,\n });\n\n let hostPeer: PeerRecord | null = null;\n let openTimer: ReturnType<typeof setTimeout> | null = null;\n\n const openTimeoutError = (): MeshTimeoutError =>\n new MeshTimeoutError(`Timed out waiting ${channelOpenTimeoutMs}ms for the data channel to open`);\n\n function failPeer(peer: PeerRecord, error: MeshError): void {\n // Ignore events from a superseded peer after re-pairing replaced hostPeer.\n if (core.disposed || peer !== hostPeer || peer.status === 'failed') return;\n peer.status = 'failed';\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n // Teardown is deferred a macrotask so a peer racing its own open\n // deadline reports its timeout rather than this hangup.\n setTimeout(() => {\n peer.dc?.close();\n peer.pc?.close();\n }, 0);\n core.emitTap({ peerId: peer.id, status: 'failed', type: 'peer-status-change' });\n core.emitTap({ error, type: 'error' });\n core.emitTap({ peer: peer.publicPeer, reason: error.message, type: 'peer-left' });\n core.setStatus('failed');\n }\n\n function disconnectPeer(peer: PeerRecord, reason: string): void {\n if (core.disposed || peer !== hostPeer) return;\n if (peer.status === 'disconnected' || peer.status === 'failed') return;\n peer.status = 'disconnected';\n peer.pc?.close();\n core.emitTap({ peerId: peer.id, status: 'disconnected', type: 'peer-status-change' });\n core.emitTap({ peer: peer.publicPeer, reason, type: 'peer-left' });\n core.setStatus('disconnected');\n }\n\n function wireChannel(peer: PeerRecord, pc: RTCPeerConnectionLike, dc: RTCDataChannelLike): void {\n peer.dc = dc;\n\n dc.addEventListener('open', () => {\n if (core.disposed) return;\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n peer.status = 'connected';\n core.emitTap({ peerId: peer.id, status: 'connected', type: 'peer-status-change' });\n core.emitTap({ peer: peer.publicPeer, type: 'peer-joined' });\n core.setStatus('connected');\n });\n dc.addEventListener('message', (event) => {\n if (core.disposed) return;\n messenger.handleInbound(peer, event.data);\n });\n dc.addEventListener('close', () => {\n if (core.disposed) return;\n if (peer.status === 'connecting')\n failPeer(\n peer,\n openTimer ? openTimeoutError() : new MeshConnectionError('channel closed before opening', peer.id),\n );\n else disconnectPeer(peer, 'channel closed');\n });\n dc.addEventListener('error', () => {\n if (core.disposed) return;\n failPeer(peer, new MeshConnectionError('channel error', peer.id));\n });\n\n // The ice-state emission lives on the pairing-scoped listener above; this\n // one owns channel-lifecycle transitions only, so each ICE change emits\n // exactly one ice-state event.\n pc.addEventListener('iceconnectionstatechange', () => {\n if (core.disposed) return;\n const state = pc.iceConnectionState;\n if (state === 'failed' || state === 'closed') {\n failPeer(peer, new MeshConnectionError(`ice ${state}`, peer.id));\n } else if (state === 'disconnected' && peer.status === 'connected') {\n disconnectPeer(peer, 'ice disconnected');\n }\n });\n }\n\n const guest: MeshGuest<P> = {\n async acceptInvitation(invitation, meta) {\n core.ensureLive();\n // A terminal host peer (failed/disconnected) may be replaced by a fresh\n // pairing on the same node — listeners stay attached and keep working.\n if (hostPeer && hostPeer.status !== 'failed' && hostPeer.status !== 'disconnected') {\n throw new MeshPairingError('Guest is already paired');\n }\n if (invitation?.v !== 1) throw new MeshPairingError('Unsupported invitation version');\n if (clock() >= invitation.expiresAt) throw new MeshPairingError('Invitation expired');\n\n if (hostPeer) {\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n hostPeer.pc?.close();\n hostPeer = null;\n }\n\n core.setStatus('pairing');\n let pc: RTCPeerConnectionLike;\n try {\n pc = rtc.createPeerConnection({ iceServers: [...(options.iceServers ?? [])] });\n } catch (error) {\n core.setStatus('idle');\n throw error;\n }\n\n // The invitation carries no host identity beyond an optional display\n // name — the session id doubles as the host-side peer id for inbound\n // metadata and tap events.\n const peer = createPeerRecord(invitation.sessionId, invitation.hostName, 'host');\n peer.pc = pc;\n hostPeer = peer;\n\n pc.addEventListener('datachannel', (event) => {\n wireChannel(peer, pc, event.channel);\n });\n pc.addEventListener('iceconnectionstatechange', () => {\n if (core.disposed) return;\n core.emitTap({ peerId: peer.id, state: pc.iceConnectionState, type: 'ice-state' });\n });\n\n try {\n await pc.setRemoteDescription({ sdp: normalizeSdp(invitation.sdp), type: 'offer' });\n } catch (cause) {\n hostPeer = null;\n core.setStatus('idle');\n throw new MeshPairingError('Invalid invitation SDP', { cause });\n }\n\n let answerSdp: string;\n try {\n await pc.setLocalDescription(await pc.createAnswer());\n await waitIceGathering(pc, iceGatheringTimeoutMs);\n answerSdp = pc.localDescription?.sdp ?? '';\n } catch (cause) {\n hostPeer = null;\n pc.close();\n core.setStatus('idle');\n throw new MeshConnectionError('Failed to create answer', null, { cause });\n }\n if (core.disposed) {\n pc.close();\n throw new MeshDisposedError();\n }\n if (!answerSdp) {\n hostPeer = null;\n pc.close();\n core.setStatus('idle');\n throw new MeshConnectionError('Local description has no SDP');\n }\n\n const peerId = randomId(options.random);\n let proof: string;\n try {\n proof = await createProof(invitation.secret, answerSdp);\n } catch (cause) {\n hostPeer = null;\n pc.close();\n core.setStatus('idle');\n throw cause;\n }\n core.setStatus('connecting');\n\n // If the channel never opens (host never accepts the answer), fail loudly\n // instead of hanging in 'connecting' forever. The deadline must cover\n // the human carry-back of the answer, not just machine negotiation.\n openTimer = setTimeout(() => {\n if (hostPeer?.status === 'connecting') {\n failPeer(hostPeer, openTimeoutError());\n }\n }, channelOpenTimeoutMs);\n\n return {\n peer: meta?.name === undefined ? { id: peerId } : { id: peerId, name: meta.name },\n proof,\n sdp: answerSdp,\n sessionId: invitation.sessionId,\n v: 1,\n };\n },\n get disposalSignal() {\n return core.disposalCtrl.signal;\n },\n\n dispose() {\n if (core.disposed) return;\n core.markDisposed();\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n if (hostPeer) {\n hostPeer.dc?.close();\n hostPeer.pc?.close();\n }\n messenger.clear();\n },\n get disposed() {\n return core.disposed;\n },\n get host(): MeshPeer | null {\n return hostPeer?.publicPeer ?? null;\n },\n\n on(type, handler) {\n core.ensureLive();\n return messenger.on(type, handler as (message: MeshInbound<unknown>) => void);\n },\n\n send(type, payload) {\n core.ensureLive();\n if (!hostPeer) throw new MeshConnectionError('Guest is not paired');\n messenger.sendTo(hostPeer, type, payload);\n },\n get status() {\n return core.status;\n },\n\n tap(handler, tapOptions) {\n return core.tap(handler, tapOptions);\n },\n\n [Symbol.dispose]() {\n this.dispose();\n },\n };\n\n return guest;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"guest.js","names":[],"sources":["../src/guest.ts"],"sourcesContent":["import {\n DEFAULT_CHANNEL_OPEN_TIMEOUT_MS,\n DEFAULT_ICE_GATHERING_TIMEOUT_MS,\n DEFAULT_MAX_MESSAGE_BYTES,\n} from './_defaults';\nimport { createMessenger, createNodeCore, createPeerRecord, type PeerRecord } from './_node';\nimport { createProof } from './_proof';\nimport { randomId } from './_random';\nimport { nativeRtcFactory, normalizeSdp, waitIceGathering } from './_rtc';\nimport { MeshConnectionError, MeshDisposedError, type MeshError, MeshPairingError, MeshTimeoutError } from './errors';\nimport type {\n MeshGuest,\n MeshGuestOptions,\n MeshInbound,\n MeshPeer,\n MeshProtocol,\n RTCDataChannelLike,\n RTCPeerConnectionLike,\n} from './types';\n\n/**\n * Create a guest mesh node. The guest pairs with one host by consuming an\n * out-of-band invitation and returning an answer.\n */\nexport function createMeshGuest<P extends MeshProtocol>(options: MeshGuestOptions = {}): MeshGuest<P> {\n const rtc = options.rtc ?? nativeRtcFactory;\n const clock = options.clock ?? (() => Date.now());\n const maxMessageBytes = options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;\n const channelOpenTimeoutMs = options.channelOpenTimeoutMs ?? DEFAULT_CHANNEL_OPEN_TIMEOUT_MS;\n const iceGatheringTimeoutMs = options.iceGatheringTimeoutMs ?? DEFAULT_ICE_GATHERING_TIMEOUT_MS;\n\n const core = createNodeCore(options.signal);\n const messenger = createMessenger({\n clock,\n emitTap: core.emitTap,\n maxMessageBytes,\n random: options.random,\n });\n\n let hostPeer: PeerRecord | null = null;\n let openTimer: ReturnType<typeof setTimeout> | null = null;\n\n const openTimeoutError = (): MeshTimeoutError =>\n new MeshTimeoutError(`Timed out waiting ${channelOpenTimeoutMs}ms for the data channel to open`);\n\n function failPeer(peer: PeerRecord, error: MeshError): void {\n // Ignore events from a superseded peer after re-pairing replaced hostPeer.\n if (core.disposed || peer !== hostPeer || peer.status === 'failed') return;\n peer.status = 'failed';\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n // Teardown is deferred a macrotask so a peer racing its own open\n // deadline reports its timeout rather than this hangup.\n setTimeout(() => {\n peer.dc?.close();\n peer.pc?.close();\n }, 0);\n core.emitTap({ peerId: peer.id, status: 'failed', type: 'peer-status-change' });\n core.emitTap({ error, type: 'error' });\n core.emitTap({ peer: peer.publicPeer, reason: error.message, type: 'peer-left' });\n core.setStatus('failed');\n }\n\n function disconnectPeer(peer: PeerRecord, reason: string): void {\n if (core.disposed || peer !== hostPeer) return;\n if (peer.status === 'disconnected' || peer.status === 'failed') return;\n peer.status = 'disconnected';\n peer.pc?.close();\n core.emitTap({ peerId: peer.id, status: 'disconnected', type: 'peer-status-change' });\n core.emitTap({ peer: peer.publicPeer, reason, type: 'peer-left' });\n core.setStatus('disconnected');\n }\n\n function wireChannel(peer: PeerRecord, pc: RTCPeerConnectionLike, dc: RTCDataChannelLike): void {\n peer.dc = dc;\n\n dc.addEventListener('open', () => {\n if (core.disposed) return;\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n peer.status = 'connected';\n core.emitTap({ peerId: peer.id, status: 'connected', type: 'peer-status-change' });\n core.emitTap({ peer: peer.publicPeer, type: 'peer-joined' });\n core.setStatus('connected');\n });\n dc.addEventListener('message', (event) => {\n if (core.disposed) return;\n messenger.handleInbound(peer, event.data);\n });\n dc.addEventListener('close', () => {\n if (core.disposed) return;\n if (peer.status === 'connecting')\n failPeer(\n peer,\n openTimer ? openTimeoutError() : new MeshConnectionError('channel closed before opening', peer.id),\n );\n else disconnectPeer(peer, 'channel closed');\n });\n dc.addEventListener('error', () => {\n if (core.disposed) return;\n failPeer(peer, new MeshConnectionError('channel error', peer.id));\n });\n\n // The ice-state emission lives on the pairing-scoped listener above; this\n // one owns channel-lifecycle transitions only, so each ICE change emits\n // exactly one ice-state event.\n pc.addEventListener('iceconnectionstatechange', () => {\n if (core.disposed) return;\n const state = pc.iceConnectionState;\n if (state === 'failed' || state === 'closed') {\n failPeer(peer, new MeshConnectionError(`ice ${state}`, peer.id));\n } else if (state === 'disconnected' && peer.status === 'connected') {\n disconnectPeer(peer, 'ice disconnected');\n }\n });\n }\n\n const guest: MeshGuest<P> = {\n async acceptInvitation(invitation, meta) {\n core.ensureLive();\n // A terminal host peer (failed/disconnected) may be replaced by a fresh\n // pairing on the same node: listeners stay attached and keep working.\n if (hostPeer && hostPeer.status !== 'failed' && hostPeer.status !== 'disconnected') {\n throw new MeshPairingError('Guest is already paired');\n }\n if (invitation?.v !== 1) throw new MeshPairingError('Unsupported invitation version');\n if (clock() >= invitation.expiresAt) throw new MeshPairingError('Invitation expired');\n\n if (hostPeer) {\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n hostPeer.pc?.close();\n hostPeer = null;\n }\n\n core.setStatus('pairing');\n let pc: RTCPeerConnectionLike;\n try {\n pc = rtc.createPeerConnection({ iceServers: [...(options.iceServers ?? [])] });\n } catch (error) {\n core.setStatus('idle');\n throw error;\n }\n\n // The invitation carries no host identity beyond an optional display\n // name: the session id doubles as the host-side peer id for inbound\n // metadata and tap events.\n const peer = createPeerRecord(invitation.sessionId, invitation.hostName, 'host');\n peer.pc = pc;\n hostPeer = peer;\n\n pc.addEventListener('datachannel', (event) => {\n wireChannel(peer, pc, event.channel);\n });\n pc.addEventListener('iceconnectionstatechange', () => {\n if (core.disposed) return;\n core.emitTap({ peerId: peer.id, state: pc.iceConnectionState, type: 'ice-state' });\n });\n\n try {\n await pc.setRemoteDescription({ sdp: normalizeSdp(invitation.sdp), type: 'offer' });\n } catch (cause) {\n hostPeer = null;\n core.setStatus('idle');\n throw new MeshPairingError('Invalid invitation SDP', { cause });\n }\n\n let answerSdp: string;\n try {\n await pc.setLocalDescription(await pc.createAnswer());\n await waitIceGathering(pc, iceGatheringTimeoutMs);\n answerSdp = pc.localDescription?.sdp ?? '';\n } catch (cause) {\n hostPeer = null;\n pc.close();\n core.setStatus('idle');\n throw new MeshConnectionError('Failed to create answer', null, { cause });\n }\n if (core.disposed) {\n pc.close();\n throw new MeshDisposedError();\n }\n if (!answerSdp) {\n hostPeer = null;\n pc.close();\n core.setStatus('idle');\n throw new MeshConnectionError('Local description has no SDP');\n }\n\n const peerId = randomId(options.random);\n let proof: string;\n try {\n proof = await createProof(invitation.secret, answerSdp);\n } catch (cause) {\n hostPeer = null;\n pc.close();\n core.setStatus('idle');\n throw cause;\n }\n core.setStatus('connecting');\n\n // If the channel never opens (host never accepts the answer), fail loudly\n // instead of hanging in 'connecting' forever. The deadline must cover\n // the human carry-back of the answer, not just machine negotiation.\n openTimer = setTimeout(() => {\n if (hostPeer?.status === 'connecting') {\n failPeer(hostPeer, openTimeoutError());\n }\n }, channelOpenTimeoutMs);\n\n return {\n peer: meta?.name === undefined ? { id: peerId } : { id: peerId, name: meta.name },\n proof,\n sdp: answerSdp,\n sessionId: invitation.sessionId,\n v: 1,\n };\n },\n get disposalSignal() {\n return core.disposalCtrl.signal;\n },\n\n dispose() {\n if (core.disposed) return;\n core.markDisposed();\n if (openTimer) {\n clearTimeout(openTimer);\n openTimer = null;\n }\n if (hostPeer) {\n hostPeer.dc?.close();\n hostPeer.pc?.close();\n }\n messenger.clear();\n },\n get disposed() {\n return core.disposed;\n },\n get host(): MeshPeer | null {\n return hostPeer?.publicPeer ?? null;\n },\n\n on(type, handler) {\n core.ensureLive();\n return messenger.on(type, handler as (message: MeshInbound<unknown>) => void);\n },\n\n send(type, payload) {\n core.ensureLive();\n if (!hostPeer) throw new MeshConnectionError('Guest is not paired');\n messenger.sendTo(hostPeer, type, payload);\n },\n get status() {\n return core.status;\n },\n\n tap(handler, tapOptions) {\n return core.tap(handler, tapOptions);\n },\n\n [Symbol.dispose]() {\n this.dispose();\n },\n };\n\n return guest;\n}\n"],"mappings":"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"}
package/dist/mesh.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@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 t={decode(t){let n;try{n=JSON.parse((0,e.base64UrlToText)(t.trim()))}catch(e){throw new MeshPairingError(`Malformed pairing payload`,{cause:e})}return parsePairingPayload(n)},encode(t){return(0,e.textToBase64Url)(JSON.stringify(t))}};function randomBytes(t,n){let r=new Uint8Array(t);if(n)for(let i=0;i<t;i++)r[i]=Math.floor((0,e.randomFloat)(n)*256);else crypto.getRandomValues(r);return r}function randomId(t){return(0,e.bytesToBase64Url)(randomBytes(16,t))}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(t){let n=(0,e.tapper)(),r=new AbortController,i={disposalCtrl:r,disposed:!1,emitTap(e){n.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`}),n.clear(),r.abort())},setStatus(e){i.disposed||i.status===e||(i.status=e,i.emitTap({status:e,type:`status-change`}))},status:`idle`,tap(e,t){return i.disposed?()=>{}:n.tap(e,t)}};return t&&(t.aborted?i.markDisposed():t.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(t){let n=new Map;function reject(e,n){t.emitTap({peerId:e,reason:n,type:`message-rejected`})}return{clear(){n.clear()},handleInbound(r,i){let a=r.id;if(typeof i!=`string`){reject(a,`malformed`);return}let o=(0,e.utf8Bytes)(i);if(o>t.maxMessageBytes){reject(a,`too-large`);return}let s;try{s=JSON.parse(i)}catch{reject(a,`malformed`);return}let c=toEnvelope(s);if(!c){reject(a,`malformed`);return}if(!r.dedupe.check(c.id)){reject(a,`duplicate`);return}let l=n.get(c.t);if(!l||l.size===0){reject(a,`unknown-type`);return}t.emitTap({bytes:o,messageType:c.t,peerId:a,type:`message-received`});let u={messageId:c.id,payload:c.p,peerId:a,sentAt:c.ts};for(let e of l)try{e(u)}catch(e){t.emitTap({error:new MeshError(`Message handler threw`,{cause:e}),type:`error`})}},on(e,t){let r=n.get(e);return r||(r=new Set,n.set(e,r)),r.add(t),()=>{r.delete(t),r.size===0&&n.delete(e)}},sendTo(n,r,i){let a=n.dc;if(!a||a.readyState!==`open`||n.status!==`connected`)throw new MeshConnectionError(`Peer "${n.id}" is not connected`,n.id);let o={id:randomId(t.random),p:i,t:r,ts:t.clock(),v:1},s;try{s=JSON.stringify(o)}catch(e){throw new MeshPayloadError(`Failed to serialize message "${r}"`,{cause:e})}let c=(0,e.utf8Bytes)(s);if(c>t.maxMessageBytes)throw new MeshPayloadError(`Message "${r}" is ${c} bytes (max ${t.maxMessageBytes})`);try{a.send(s)}catch(e){throw new MeshConnectionError(`Failed to send message "${r}"`,n.id,{cause:e})}t.emitTap({bytes:c,messageType:r,peerId:n.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(t,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(t),{hash:`SHA-256`,name:`HMAC`},!1,[`sign`]),o=await r.sign(`HMAC`,a,i.encode(canonicalSdp(n)));return(0,e.bytesToBase64Url)(new Uint8Array(o))}var n={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
2
+ `)}`}async function createProof(t,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(t),{hash:`SHA-256`,name:`HMAC`},!1,[`sign`]),o=await r.sign(`HMAC`,a,i.encode(canonicalSdp(n)));return(0,e.bytesToBase64Url)(new Uint8Array(o))}var n={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
3
  `);return t.endsWith(`\r
4
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??n,r=e.clock??(()=>Date.now()),i=e.maxMessageBytes??65536,a=e.channelOpenTimeoutMs??6e4,o=e.iceGatheringTimeoutMs??5e3,s=createNodeCore(e.signal),c=createMessenger({clock:r,emitTap:s.emitTap,maxMessageBytes:i,random:e.random}),l=null,u=null,openTimeoutError=()=>new MeshTimeoutError(`Timed out waiting ${a}ms for the data channel to open`);function failPeer(e,t){s.disposed||e!==l||e.status===`failed`||(e.status=`failed`,u&&=(clearTimeout(u),null),setTimeout(()=>{e.dc?.close(),e.pc?.close()},0),s.emitTap({peerId:e.id,status:`failed`,type:`peer-status-change`}),s.emitTap({error:t,type:`error`}),s.emitTap({peer:e.publicPeer,reason:t.message,type:`peer-left`}),s.setStatus(`failed`))}function disconnectPeer(e,t){s.disposed||e!==l||e.status!==`disconnected`&&e.status!==`failed`&&(e.status=`disconnected`,e.pc?.close(),s.emitTap({peerId:e.id,status:`disconnected`,type:`peer-status-change`}),s.emitTap({peer:e.publicPeer,reason:t,type:`peer-left`}),s.setStatus(`disconnected`))}function wireChannel(e,t,n){e.dc=n,n.addEventListener(`open`,()=>{s.disposed||(u&&=(clearTimeout(u),null),e.status=`connected`,s.emitTap({peerId:e.id,status:`connected`,type:`peer-status-change`}),s.emitTap({peer:e.publicPeer,type:`peer-joined`}),s.setStatus(`connected`))}),n.addEventListener(`message`,t=>{s.disposed||c.handleInbound(e,t.data)}),n.addEventListener(`close`,()=>{s.disposed||(e.status===`connecting`?failPeer(e,u?openTimeoutError():new MeshConnectionError(`channel closed before opening`,e.id)):disconnectPeer(e,`channel closed`))}),n.addEventListener(`error`,()=>{s.disposed||failPeer(e,new MeshConnectionError(`channel error`,e.id))}),t.addEventListener(`iceconnectionstatechange`,()=>{if(s.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(n,i){if(s.ensureLive(),l&&l.status!==`failed`&&l.status!==`disconnected`)throw new MeshPairingError(`Guest is already paired`);if(n?.v!==1)throw new MeshPairingError(`Unsupported invitation version`);if(r()>=n.expiresAt)throw new MeshPairingError(`Invitation expired`);l&&=(u&&=(clearTimeout(u),null),l.pc?.close(),null),s.setStatus(`pairing`);let c;try{c=t.createPeerConnection({iceServers:[...e.iceServers??[]]})}catch(e){throw s.setStatus(`idle`),e}let d=createPeerRecord(n.sessionId,n.hostName,`host`);d.pc=c,l=d,c.addEventListener(`datachannel`,e=>{wireChannel(d,c,e.channel)}),c.addEventListener(`iceconnectionstatechange`,()=>{s.disposed||s.emitTap({peerId:d.id,state:c.iceConnectionState,type:`ice-state`})});try{await c.setRemoteDescription({sdp:normalizeSdp(n.sdp),type:`offer`})}catch(e){throw l=null,s.setStatus(`idle`),new MeshPairingError(`Invalid invitation SDP`,{cause:e})}let f;try{await c.setLocalDescription(await c.createAnswer()),await waitIceGathering(c,o),f=c.localDescription?.sdp??``}catch(e){throw l=null,c.close(),s.setStatus(`idle`),new MeshConnectionError(`Failed to create answer`,null,{cause:e})}if(s.disposed)throw c.close(),new MeshDisposedError;if(!f)throw l=null,c.close(),s.setStatus(`idle`),new MeshConnectionError(`Local description has no SDP`);let p=randomId(e.random),m;try{m=await createProof(n.secret,f)}catch(e){throw l=null,c.close(),s.setStatus(`idle`),e}return s.setStatus(`connecting`),u=setTimeout(()=>{l?.status===`connecting`&&failPeer(l,openTimeoutError())},a),{peer:i?.name===void 0?{id:p}:{id:p,name:i.name},proof:m,sdp:f,sessionId:n.sessionId,v:1}},get disposalSignal(){return s.disposalCtrl.signal},dispose(){s.disposed||(s.markDisposed(),u&&=(clearTimeout(u),null),l&&(l.dc?.close(),l.pc?.close()),c.clear())},get disposed(){return s.disposed},get host(){return l?.publicPeer??null},on(e,t){return s.ensureLive(),c.on(e,t)},send(e,t){if(s.ensureLive(),!l)throw new MeshConnectionError(`Guest is not paired`);c.sendTo(l,e,t)},get status(){return s.status},tap(e,t){return s.tap(e,t)},[Symbol.dispose](){this.dispose()}}}function createMeshHost(t={}){let r=t.rtc??n,i=t.clock??(()=>Date.now()),a=t.maxMessageBytes??65536,o=t.invitationTtlMs??3e5,s=t.channelOpenTimeoutMs??6e4,c=t.iceGatheringTimeoutMs??5e3,l={label:`mesh`,ordered:!0,...t.channel},u=createNodeCore(t.signal),d=createMessenger({clock:i,emitTap:u.emitTap,maxMessageBytes:a,random:t.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(e){if(u.ensureLive(),e?.v!==1)throw new MeshPairingError(`Unsupported answer version`);if(!e.peer?.id)throw new MeshPairingError(`Answer is missing a peer id`);let n=p.get(e.sessionId);if(!n)throw new MeshPairingError(`Unknown or expired session`);if(n.answered)throw new MeshPairingError(`Invitation already answered`);if(f.has(e.peer.id))throw new MeshPairingError(`Peer "${e.peer.id}" is already connected`);if(i()>=n.expiresAt)throw expireInvitation(e.sessionId),new MeshPairingError(`Invitation expired`);let r=await createProof(n.secret,e.sdp);if(e.proof!==r)throw new MeshPairingError(`Proof mismatch`);if(!(await t.approvePeer?.(e.peer)??!0))throw u.emitTap({peerId:e.peer.id,type:`peer-rejected`}),new MeshPairingError(`Peer "${e.peer.id}" rejected by host`);if(u.emitTap({peerId:e.peer.id,type:`peer-approved`}),f.has(e.peer.id))throw new MeshPairingError(`Peer "${e.peer.id}" is already connected`);n.answered=!0,p.delete(e.sessionId),clearTimeout(n.expiryTimer);let a=createPeerRecord(e.peer.id,e.peer.name,`guest`),o=waitChannelOpen(n.dc,n.pc,s,a.id,u.disposalCtrl.signal);wirePeerTransport(a,n.pc,n.dc),f.set(a.id,a),recomputeStatus();try{await n.pc.setRemoteDescription({sdp:normalizeSdp(e.sdp),type:`answer`})}catch(e){throw leavePeer(a,`invalid answer sdp`,!0),new MeshPairingError(`Invalid answer SDP`,{cause:e})}try{await o}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(n){u.ensureLive();let a=r.createPeerConnection({iceServers:[...t.iceServers??[]]}),s=a.createDataChannel(l.label,{maxRetransmits:l.maxRetransmits,ordered:l.ordered}),d=randomId(t.random),f=(0,e.bytesToBase64Url)(randomBytes(32,t.random)),m=i()+o;try{await a.setLocalDescription(await a.createOffer()),await waitIceGathering(a,c)}catch(e){throw a.close(),new MeshConnectionError(`Failed to create offer`,null,{cause:e})}if(u.disposed)throw a.close(),new MeshDisposedError;let h=a.localDescription?.sdp;if(!h)throw a.close(),new MeshConnectionError(`Local description has no SDP`);let g=setTimeout(()=>expireInvitation(d),o);return p.set(d,{answered:!1,dc:s,expiresAt:m,expiryTimer:g,pc:a,secret:f,sessionId:d}),u.emitTap({sessionId:d,type:`invitation-created`}),recomputeStatus(),{expiresAt:m,sdp:h,secret:f,sessionId:d,v:1,...n?.name===void 0?{}:{hostName:n.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 r=`0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:`,i=new Map([...r].map((e,t)=>[e,t]));function bytesToBase45(e){let t=``;for(let n=0;n+1<e.length;n+=2){let i=e[n]*256+e[n+1];t+=r[i%45]+r[Math.floor(i/45)%45]+r[Math.floor(i/2025)]}if(e.length%2){let n=e[e.length-1];t+=r[n%45]+r[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=i.get(e[t]);if(n===void 0)throw new MeshPairingError(`Malformed base45 payload`);return n}var a=`mq2.`,o=new TextEncoder,s=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 c={async decode(e){let n=e.trim();if(n.startsWith(a)){let e;try{let t=base45ToBytes(n.slice(4));e=JSON.parse(s.decode(await pump(t,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 t.decode(n)},async encode(e){if(typeof globalThis.CompressionStream!=`function`)return t.encode(e);let n=compactSdp(e.sdp);return a+bytesToBase45(await pump(o.encode(JSON.stringify(n?{...e,sdp:n}:e)),new globalThis.CompressionStream(`deflate-raw`)))}};exports.MeshConnectionError=MeshConnectionError,exports.MeshDisposedError=MeshDisposedError,exports.MeshError=MeshError,exports.MeshPairingError=MeshPairingError,exports.MeshPayloadError=MeshPayloadError,exports.MeshTimeoutError=MeshTimeoutError,exports.MeshUnsupportedError=MeshUnsupportedError,exports.createMeshGuest=createMeshGuest,exports.createMeshHost=createMeshHost,exports.meshCodec=t,exports.meshQrCodec=c;
5
5
  //# sourceMappingURL=mesh.cjs.map