@net-mesh/browser 0.37.1 → 0.38.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/CHANGELOG.md CHANGED
@@ -14,6 +14,54 @@ missing method at the call site rather than at install time. Unlike
14
14
  `@net-mesh/sdk`, this package never depends on `@net-mesh/core` — see
15
15
  the README on why it is a sibling package rather than a sub-path.
16
16
 
17
+ ## 0.38.1 — dual-stack anchors
18
+
19
+ An anchor can now serve IPv4 and IPv6 players at once
20
+ (`net-mesh anchor serve --rtc-bind 0.0.0.0:7101 --rtc-bind '[::]:7101'`).
21
+ What a page sees:
22
+
23
+ - **The anchor needs a STUN endpoint in each family too**
24
+ (`--rtc-stun-bind` of each family). Without a media permission a
25
+ browser's only candidate on a network is the server-reflexive one from
26
+ a STUN server of that family. The default ICE servers now use every
27
+ STUN endpoint the anchor publishes (`stun_addrs`).
28
+ - **Firefox on a pure IPv6-only network (no IPv4 route) is unsupported.**
29
+ It gathers nothing there, and the connect fails `ice-timeout`, not
30
+ `udp-blocked`. On 464XLAT networks, as mobile IPv6-only networks are,
31
+ it connects. Chromium works on both.
32
+ - **A probe the engine failed before it gathered reads `notRun`**, not
33
+ `unanswered`, so it can never produce `udp-blocked`.
34
+ - **Public anchors.** An anchor started with `--open-games` issues a
35
+ credential for any game id from any page, keyed on the page's origin
36
+ and the id, so no operator setup is needed. `requestCredential({
37
+ anchorUrl, game })` is unchanged. Two sites naming their game alike
38
+ get two separate games and never see each other's lobbies.
39
+ - **Connecting needs no change.** The anchor's answer offers a host
40
+ candidate per family, and the browser pairs with whichever its network
41
+ reaches.
42
+ - **`udp-blocked` is stricter and says less.** The leaf now probes
43
+ **every** endpoint the anchor published, under one deadline, and
44
+ reports `udp-blocked` only when the anchor answered over HTTPS and
45
+ **every** probe went unanswered. One answered family, an unrun probe,
46
+ or no endpoints at all stays `ice-timeout`. So an IPv6-only player
47
+ whose IPv4 probe is silent is no longer told UDP is blocked.
48
+ - **The `udp-blocked` message states the observation, not a cause.** It
49
+ now reads `no UDP response from the anchor's advertised endpoints: its
50
+ HTTPS bootstrap succeeded but STUN bindings to … went unanswered`,
51
+ because blocked UDP, a stopped UDP listener, a wrong advertised address
52
+ and loss all look the same. The `kind` is unchanged. Match on `kind`,
53
+ never on the message.
54
+ - **`UdpBlockedEvidence.probedAll`** lists every endpoint probed; `probed`
55
+ is still the first.
56
+ - **`ConnectedEvent.rtcAddrs`** lists every endpoint the anchor published,
57
+ primary first. `rtcAddr` is still the primary.
58
+ - **The Rust and TypeScript probes agree** on what "answered" means: a
59
+ server-reflexive candidate, or a STUN error response (code below 700).
60
+ The wasm probe used to ignore error responses.
61
+ - **New exports:** `classifyRtcFailureAll`, `probeStunBindings`,
62
+ `probeEventAnswers`, `stunProbeAnswered`, `candidateLineIsReflexive`,
63
+ and the `EndpointProbe` and `ProbeEvent` types.
64
+
17
65
  ## 0.37.0 — the first npm release
18
66
 
19
67
  Everything in this file ships in 0.37.0, the package's first release on npm.
package/dist/errors.d.ts CHANGED
@@ -33,13 +33,19 @@ export interface UdpBlockedEvidence {
33
33
  readonly stunProbeFailed: true;
34
34
  /** The address the probe was aimed at, so the claim names its subject. */
35
35
  readonly probed: string;
36
+ /**
37
+ * Every endpoint probed, `probed` first; each went unanswered. A
38
+ * dual-stack anchor publishes one per family, and one answered probe
39
+ * means UDP works for that family, which is never this evidence.
40
+ */
41
+ readonly probedAll: readonly string[];
36
42
  }
37
43
  /**
38
44
  * `UdpBlockedEvidence` only when both observations hold; `null`
39
45
  * otherwise, which is the caller's instruction to stay with
40
46
  * `ice-timeout`. Mirror of `UdpBlockedEvidence::new`.
41
47
  */
42
- export declare function udpBlockedEvidence(bootstrapOk: boolean, stunProbeFailed: boolean, probed: string): UdpBlockedEvidence | null;
48
+ export declare function udpBlockedEvidence(bootstrapOk: boolean, stunProbeFailed: boolean, probed: string, probedAll?: readonly string[]): UdpBlockedEvidence | null;
43
49
  /** Why an RTC attempt did not produce a DataChannel. */
44
50
  export type RtcErrorFailure = {
45
51
  readonly type: 'iceTimeout';
package/dist/errors.js CHANGED
@@ -20,10 +20,13 @@
20
20
  * otherwise, which is the caller's instruction to stay with
21
21
  * `ice-timeout`. Mirror of `UdpBlockedEvidence::new`.
22
22
  */
23
- export function udpBlockedEvidence(bootstrapOk, stunProbeFailed, probed) {
23
+ export function udpBlockedEvidence(bootstrapOk, stunProbeFailed, probed, probedAll = [probed]) {
24
24
  if (!bootstrapOk || !stunProbeFailed || probed.length === 0)
25
25
  return null;
26
- return { bootstrapOk: true, stunProbeFailed: true, probed };
26
+ if (probedAll.length === 0 || probedAll[0] !== probed || probedAll.some((addr) => addr.length === 0)) {
27
+ return null;
28
+ }
29
+ return { bootstrapOk: true, stunProbeFailed: true, probed, probedAll: [...probedAll] };
27
30
  }
28
31
  /** The base class of everything this package rejects with. */
29
32
  export class LeafError extends Error {
@@ -564,8 +567,11 @@ function rtcDisplay(failure) {
564
567
  case 'iceTimeout':
565
568
  return ICE_TIMEOUT_DISPLAY;
566
569
  case 'udpBlocked':
567
- return ("UDP appears blocked: the anchor's HTTPS bootstrap succeeded " +
568
- `but a STUN binding to ${failure.evidence.probed} was unanswered`);
570
+ // An observation, not a cause: blocked UDP, a stopped UDP
571
+ // listener, a wrong advertised address or loss all look like
572
+ // this. Byte-identical to the Rust `Display`.
573
+ return ("no UDP response from the anchor's advertised endpoints: its HTTPS bootstrap succeeded " +
574
+ `but STUN bindings to ${failure.evidence.probedAll.join(', ')} went unanswered`);
569
575
  case 'channelClosed':
570
576
  return `the DataChannel closed: ${failure.detail}`;
571
577
  case 'unsupported':
@@ -672,10 +678,12 @@ export function parseLeafError(message) {
672
678
  export function parseRtcFailure(text) {
673
679
  if (text === ICE_TIMEOUT_DISPLAY)
674
680
  return { type: 'iceTimeout' };
675
- const blocked = /^UDP appears blocked: the anchor's HTTPS bootstrap succeeded but a STUN binding to (.+) was unanswered$/.exec(text);
681
+ const blocked = /^no UDP response from the anchor's advertised endpoints: its HTTPS bootstrap succeeded but STUN bindings to (.+) went unanswered$/.exec(text);
676
682
  if (blocked) {
677
- const probed = blocked[1] ?? '';
678
- const evidence = udpBlockedEvidence(true, true, probed);
683
+ // Endpoints are `host:port` or `[v6]:port`; neither holds ", ".
684
+ const probedAll = (blocked[1] ?? '').split(', ');
685
+ const probed = probedAll[0] ?? '';
686
+ const evidence = udpBlockedEvidence(true, true, probed, probedAll);
679
687
  // The regex matched the message the Rust side only emits when it
680
688
  // holds the evidence, so `probed` is non-empty and this is
681
689
  // `UdpBlockedEvidence`; an empty subject is not evidence.
package/dist/events.d.ts CHANGED
@@ -42,6 +42,12 @@ export interface ConnectedEvent {
42
42
  * none.
43
43
  */
44
44
  readonly rtcAddr: string | null;
45
+ /**
46
+ * **Every** endpoint that anchor published, primary first: one per
47
+ * family on a dual-stack anchor, `[rtcAddr]` on any other, empty when
48
+ * it published none. The STUN probe aims at all of them.
49
+ */
50
+ readonly rtcAddrs: readonly string[];
45
51
  }
46
52
  /** The session went away. */
47
53
  export interface DisconnectedEvent {
package/dist/events.js CHANGED
@@ -72,6 +72,7 @@ export function parseEvent(json) {
72
72
  nodeIdHex: str(fields, 'node_id_hex'),
73
73
  peerNode: str(fields, 'peer_node'),
74
74
  rtcAddr: optionalStr(fields, 'rtc_addr'),
75
+ rtcAddrs: rtcAddrsOf(fields),
75
76
  };
76
77
  case 'disconnected':
77
78
  return { type: 'disconnected', reason: str(fields, 'reason') };
@@ -247,6 +248,18 @@ function str(fields, key) {
247
248
  return String(value);
248
249
  return '';
249
250
  }
251
+ /**
252
+ * `rtc_addrs` when the leaf sent a list (a dual-stack anchor), else
253
+ * `[rtc_addr]`, else empty: the rule `GET /rtc/anchor` documents.
254
+ */
255
+ function rtcAddrsOf(fields) {
256
+ const list = fields.rtc_addrs;
257
+ if (Array.isArray(list)) {
258
+ return list.filter((addr) => typeof addr === 'string' && addr.length > 0);
259
+ }
260
+ const single = optionalStr(fields, 'rtc_addr');
261
+ return single === null ? [] : [single];
262
+ }
250
263
  function optionalStr(fields, key) {
251
264
  const value = fields[key];
252
265
  return typeof value === 'string' && value.length > 0 ? value : null;