@net-mesh/browser 0.37.0
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 +517 -0
- package/LICENSE-APACHE +190 -0
- package/LICENSE-MIT +21 -0
- package/README.md +666 -0
- package/dist/async-queue.d.ts +26 -0
- package/dist/async-queue.js +62 -0
- package/dist/credential.d.ts +70 -0
- package/dist/credential.js +95 -0
- package/dist/errors.d.ts +400 -0
- package/dist/errors.js +726 -0
- package/dist/events.d.ts +232 -0
- package/dist/events.js +298 -0
- package/dist/identity.d.ts +40 -0
- package/dist/identity.js +78 -0
- package/dist/index.bundle.js +2 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +46 -0
- package/dist/leader/events.d.ts +111 -0
- package/dist/leader/events.js +116 -0
- package/dist/leader/index.d.ts +14 -0
- package/dist/leader/index.js +11 -0
- package/dist/leader/session.d.ts +539 -0
- package/dist/leader/session.js +770 -0
- package/dist/leader/wasm.d.ts +123 -0
- package/dist/leader/wasm.js +39 -0
- package/dist/lobby.d.ts +183 -0
- package/dist/lobby.js +504 -0
- package/dist/local.d.ts +80 -0
- package/dist/local.js +160 -0
- package/dist/net_leaf.d.ts +1494 -0
- package/dist/net_leaf.js +3753 -0
- package/dist/net_leaf_bg.wasm +0 -0
- package/dist/net_leaf_bg.wasm.d.ts +137 -0
- package/dist/netcode/client.d.ts +102 -0
- package/dist/netcode/client.js +296 -0
- package/dist/netcode/clock.d.ts +45 -0
- package/dist/netcode/clock.js +54 -0
- package/dist/netcode/host.d.ts +118 -0
- package/dist/netcode/host.js +276 -0
- package/dist/netcode/index.d.ts +19 -0
- package/dist/netcode/index.js +14 -0
- package/dist/netcode/interpolate.d.ts +50 -0
- package/dist/netcode/interpolate.js +100 -0
- package/dist/netcode/wire.d.ts +125 -0
- package/dist/netcode/wire.js +117 -0
- package/dist/node.d.ts +908 -0
- package/dist/node.js +1085 -0
- package/dist/org.d.ts +412 -0
- package/dist/org.js +567 -0
- package/dist/peer-driver.d.ts +117 -0
- package/dist/peer-driver.js +224 -0
- package/dist/store/assembly.d.ts +176 -0
- package/dist/store/assembly.js +325 -0
- package/dist/store/chunker.d.ts +83 -0
- package/dist/store/chunker.js +190 -0
- package/dist/store/core.d.ts +136 -0
- package/dist/store/core.js +477 -0
- package/dist/store/definition.d.ts +23 -0
- package/dist/store/definition.js +83 -0
- package/dist/store/errors.d.ts +77 -0
- package/dist/store/errors.js +94 -0
- package/dist/store/host.d.ts +247 -0
- package/dist/store/host.js +675 -0
- package/dist/store/index.d.ts +50 -0
- package/dist/store/index.js +40 -0
- package/dist/store/interest.d.ts +40 -0
- package/dist/store/interest.js +77 -0
- package/dist/store/inventory.d.ts +81 -0
- package/dist/store/inventory.js +140 -0
- package/dist/store/join.d.ts +118 -0
- package/dist/store/join.js +724 -0
- package/dist/store/json.d.ts +50 -0
- package/dist/store/json.js +283 -0
- package/dist/store/ledger.d.ts +187 -0
- package/dist/store/ledger.js +292 -0
- package/dist/store/owner.d.ts +616 -0
- package/dist/store/owner.js +1711 -0
- package/dist/store/patch.d.ts +70 -0
- package/dist/store/patch.js +166 -0
- package/dist/store/player.d.ts +65 -0
- package/dist/store/player.js +261 -0
- package/dist/store/replica.d.ts +187 -0
- package/dist/store/replica.js +740 -0
- package/dist/store/state.d.ts +42 -0
- package/dist/store/state.js +165 -0
- package/dist/store/types.d.ts +200 -0
- package/dist/store/types.js +25 -0
- package/dist/store/visibility.d.ts +118 -0
- package/dist/store/visibility.js +282 -0
- package/dist/store/wire.d.ts +309 -0
- package/dist/store/wire.js +834 -0
- package/dist/stream.d.ts +174 -0
- package/dist/stream.js +238 -0
- package/dist/three/index.d.ts +109 -0
- package/dist/three/index.js +130 -0
- package/dist/udp-probe.d.ts +159 -0
- package/dist/udp-probe.js +245 -0
- package/dist/wasm.d.ts +642 -0
- package/dist/wasm.js +98 -0
- package/dist/world/border.d.ts +84 -0
- package/dist/world/border.js +99 -0
- package/dist/world/handoff.d.ts +187 -0
- package/dist/world/handoff.js +222 -0
- package/dist/world/index.d.ts +13 -0
- package/dist/world/index.js +9 -0
- package/dist/world/link.d.ts +179 -0
- package/dist/world/link.js +476 -0
- package/dist/world/regions.d.ts +154 -0
- package/dist/world/regions.js +314 -0
- package/package.json +72 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The failure-typing correction, implemented.
|
|
3
|
+
*
|
|
4
|
+
* An ICE timeout is not evidence that UDP is blocked: an anchor that
|
|
5
|
+
* is down, wrong or saturated produces exactly the same symptom. The
|
|
6
|
+
* leaf therefore surfaces `rtc: ICE did not connect…` by default, and
|
|
7
|
+
* only the two observations below may narrow it:
|
|
8
|
+
*
|
|
9
|
+
* 1. the HTTPS bootstrap to *that anchor* succeeded — it is up and
|
|
10
|
+
* addressable; and
|
|
11
|
+
* 2. a STUN binding to the `rtc_addr` *that same anchor published*
|
|
12
|
+
* went unanswered.
|
|
13
|
+
*
|
|
14
|
+
* {@link classifyRtcFailure} is a pure function of those two
|
|
15
|
+
* observations, so the rule is unit-testable without a browser, and
|
|
16
|
+
* {@link probeStunBinding} / {@link probeBootstrapReachable} are the
|
|
17
|
+
* two impure halves that produce them. Nothing else in this package
|
|
18
|
+
* may construct a `udp-blocked` error.
|
|
19
|
+
*
|
|
20
|
+
* **What the STUN probe actually keys on** (measured in headless
|
|
21
|
+
* Chromium, not assumed — see `docs/internal/spikes/S5_REPORT.md`):
|
|
22
|
+
* host candidates are gathered whatever the network does, so their
|
|
23
|
+
* presence or absence proves nothing and they are ignored. The signals
|
|
24
|
+
* that mean something are
|
|
25
|
+
*
|
|
26
|
+
* - a **server-reflexive** candidate — the STUN binding was answered,
|
|
27
|
+
* so UDP to the anchor works and the ICE failure is something else;
|
|
28
|
+
* - an `icecandidateerror` with a STUN **error code** below 700 — an
|
|
29
|
+
* error *response* came back, so packets reach the anchor;
|
|
30
|
+
* - an `icecandidateerror` with code 701, or gathering completing with
|
|
31
|
+
* no reflexive candidate at all — nothing came back.
|
|
32
|
+
*/
|
|
33
|
+
import { RtcError, type UdpBlockedEvidence } from './errors.js';
|
|
34
|
+
/** What a STUN binding attempt against the anchor's `rtc_addr` did. */
|
|
35
|
+
export type StunProbeOutcome =
|
|
36
|
+
/** A reflexive candidate came back: the binding was answered. */
|
|
37
|
+
{
|
|
38
|
+
readonly type: 'reflexive';
|
|
39
|
+
readonly address: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A STUN **error response** came back (code < 700). The anchor's ICE
|
|
43
|
+
* agent refusing an unauthenticated binding still proves the packet
|
|
44
|
+
* arrived and the reply got home, so this is *not* blocked UDP.
|
|
45
|
+
*/
|
|
46
|
+
| {
|
|
47
|
+
readonly type: 'stunError';
|
|
48
|
+
readonly code: number;
|
|
49
|
+
readonly detail: string;
|
|
50
|
+
}
|
|
51
|
+
/** Nothing came back inside the deadline. */
|
|
52
|
+
| {
|
|
53
|
+
readonly type: 'unanswered';
|
|
54
|
+
readonly detail: string;
|
|
55
|
+
}
|
|
56
|
+
/** The probe was not run — no address to aim at, or it was disabled. */
|
|
57
|
+
| {
|
|
58
|
+
readonly type: 'notRun';
|
|
59
|
+
readonly reason: string;
|
|
60
|
+
}
|
|
61
|
+
/** This engine could not run the probe at all. */
|
|
62
|
+
| {
|
|
63
|
+
readonly type: 'unsupported';
|
|
64
|
+
readonly detail: string;
|
|
65
|
+
};
|
|
66
|
+
/** Everything the classification is allowed to look at. */
|
|
67
|
+
export interface RtcFailureObservations {
|
|
68
|
+
/** Did the HTTPS bootstrap to this anchor succeed? */
|
|
69
|
+
readonly bootstrapOk: boolean;
|
|
70
|
+
/** What the STUN probe saw. */
|
|
71
|
+
readonly stunProbe: StunProbeOutcome;
|
|
72
|
+
/** The address the probe was aimed at, so the claim names its subject. */
|
|
73
|
+
readonly probed: string | null;
|
|
74
|
+
}
|
|
75
|
+
/** The only two classifications an ICE failure may end up with. */
|
|
76
|
+
export type IceFailureClassification = {
|
|
77
|
+
readonly type: 'iceTimeout';
|
|
78
|
+
} | {
|
|
79
|
+
readonly type: 'udpBlocked';
|
|
80
|
+
readonly evidence: UdpBlockedEvidence;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* The rule, pure.
|
|
84
|
+
*
|
|
85
|
+
* `udpBlocked` requires a named subject, a successful bootstrap and a
|
|
86
|
+
* STUN probe that went unanswered. Every other combination — including
|
|
87
|
+
* "we did not probe", "the probe is unsupported here" and "an error
|
|
88
|
+
* response came back" — stays `iceTimeout`.
|
|
89
|
+
*/
|
|
90
|
+
export declare function classifyRtcFailure(observations: RtcFailureObservations): IceFailureClassification;
|
|
91
|
+
/** {@link classifyRtcFailure}, as the error a caller will be rejected with. */
|
|
92
|
+
export declare function classifyRtcError(observations: RtcFailureObservations): RtcError;
|
|
93
|
+
/** Injection seam for the probe, so tests never need a real engine. */
|
|
94
|
+
export interface StunProbeOptions {
|
|
95
|
+
/** How long to wait for an answer. Default 2500 ms. */
|
|
96
|
+
readonly timeoutMs?: number;
|
|
97
|
+
/**
|
|
98
|
+
* Builds the `RTCPeerConnection` used for the probe. Defaults to the
|
|
99
|
+
* global constructor; tests pass a fake.
|
|
100
|
+
*/
|
|
101
|
+
readonly peerConnectionFactory?: (config: RTCConfiguration) => RTCPeerConnection;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Aim a STUN binding at `rtcAddr` and report what came back.
|
|
105
|
+
*
|
|
106
|
+
* Uses an `RTCPeerConnection` whose *only* ICE server is that address:
|
|
107
|
+
* the browser then sends a STUN binding request to it as part of
|
|
108
|
+
* gathering, and surfaces the outcome as either a reflexive candidate
|
|
109
|
+
* or an `icecandidateerror`. The probe connection is never used for
|
|
110
|
+
* media and is always closed.
|
|
111
|
+
*/
|
|
112
|
+
export declare function probeStunBinding(rtcAddr: string, options?: StunProbeOptions): Promise<StunProbeOutcome>;
|
|
113
|
+
/** Injection seam for the bootstrap observation. */
|
|
114
|
+
export interface BootstrapProbeOptions {
|
|
115
|
+
/** How long to wait for the anchor to answer. Default 3000 ms. */
|
|
116
|
+
readonly timeoutMs?: number;
|
|
117
|
+
/** Defaults to the global `fetch`; tests pass a fake. */
|
|
118
|
+
readonly fetchImpl?: typeof fetch;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Did the anchor answer over HTTPS?
|
|
122
|
+
*
|
|
123
|
+
* Any response counts, including 4xx and 5xx: the question is whether
|
|
124
|
+
* the anchor is up and addressable, not whether it liked this request.
|
|
125
|
+
* The request is `no-cors` on purpose — a cross-origin anchor without
|
|
126
|
+
* CORS headers would otherwise reject a perfectly delivered request
|
|
127
|
+
* and we would under-report reachability — and `no-store`, so a cached
|
|
128
|
+
* response cannot stand in for a live one.
|
|
129
|
+
*/
|
|
130
|
+
export declare function probeBootstrapReachable(bootstrapUrl: string, options?: BootstrapProbeOptions): Promise<boolean>;
|
|
131
|
+
/**
|
|
132
|
+
* `host:port` (or `[v6]:port`, or a bare host) to a `stun:` URL for
|
|
133
|
+
* the **throwaway diagnostic probe**, or `null` when it is not an
|
|
134
|
+
* address at all. A missing port means the IANA STUN port, which is
|
|
135
|
+
* what an anchor publishing a bare host would mean.
|
|
136
|
+
*
|
|
137
|
+
* This is for {@link probeStunBinding} and nothing else. It must
|
|
138
|
+
* **NOT** be used to build the anchor connection's `iceServers`:
|
|
139
|
+
* `rtc_addr` is that connection's ICE peer, and a peer cannot be its
|
|
140
|
+
* own STUN server — configuring it that way gathers no reflexive
|
|
141
|
+
* candidate and buys an ICE deadline instead of a connection. The
|
|
142
|
+
* leaf refuses it outright ({@link IceServerConflictError}).
|
|
143
|
+
*
|
|
144
|
+
* The endpoint an anchor connection should gather against is the one
|
|
145
|
+
* the anchor announces separately, as `stun_addr` on
|
|
146
|
+
* `GET /rtc/anchor`; `connect()` uses it by default, so a page needs
|
|
147
|
+
* no URL-building at all.
|
|
148
|
+
*/
|
|
149
|
+
export declare function diagnosticStunUrl(rtcAddr: string): string | null;
|
|
150
|
+
/**
|
|
151
|
+
* The reflexive address of a candidate, or `null` if it is not a
|
|
152
|
+
* reflexive one.
|
|
153
|
+
*
|
|
154
|
+
* `RTCIceCandidate.type`/`.address` are the modern surface; the SDP
|
|
155
|
+
* line is parsed as well because the fields are still nullable in
|
|
156
|
+
* engines that predate them, and a missed `srflx` would be read as
|
|
157
|
+
* blocked UDP — the exact mis-typing this module exists to prevent.
|
|
158
|
+
*/
|
|
159
|
+
export declare function reflexiveAddress(candidate: RTCIceCandidate): string | null;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The failure-typing correction, implemented.
|
|
3
|
+
*
|
|
4
|
+
* An ICE timeout is not evidence that UDP is blocked: an anchor that
|
|
5
|
+
* is down, wrong or saturated produces exactly the same symptom. The
|
|
6
|
+
* leaf therefore surfaces `rtc: ICE did not connect…` by default, and
|
|
7
|
+
* only the two observations below may narrow it:
|
|
8
|
+
*
|
|
9
|
+
* 1. the HTTPS bootstrap to *that anchor* succeeded — it is up and
|
|
10
|
+
* addressable; and
|
|
11
|
+
* 2. a STUN binding to the `rtc_addr` *that same anchor published*
|
|
12
|
+
* went unanswered.
|
|
13
|
+
*
|
|
14
|
+
* {@link classifyRtcFailure} is a pure function of those two
|
|
15
|
+
* observations, so the rule is unit-testable without a browser, and
|
|
16
|
+
* {@link probeStunBinding} / {@link probeBootstrapReachable} are the
|
|
17
|
+
* two impure halves that produce them. Nothing else in this package
|
|
18
|
+
* may construct a `udp-blocked` error.
|
|
19
|
+
*
|
|
20
|
+
* **What the STUN probe actually keys on** (measured in headless
|
|
21
|
+
* Chromium, not assumed — see `docs/internal/spikes/S5_REPORT.md`):
|
|
22
|
+
* host candidates are gathered whatever the network does, so their
|
|
23
|
+
* presence or absence proves nothing and they are ignored. The signals
|
|
24
|
+
* that mean something are
|
|
25
|
+
*
|
|
26
|
+
* - a **server-reflexive** candidate — the STUN binding was answered,
|
|
27
|
+
* so UDP to the anchor works and the ICE failure is something else;
|
|
28
|
+
* - an `icecandidateerror` with a STUN **error code** below 700 — an
|
|
29
|
+
* error *response* came back, so packets reach the anchor;
|
|
30
|
+
* - an `icecandidateerror` with code 701, or gathering completing with
|
|
31
|
+
* no reflexive candidate at all — nothing came back.
|
|
32
|
+
*/
|
|
33
|
+
import { RtcError, udpBlockedEvidence } from './errors.js';
|
|
34
|
+
/**
|
|
35
|
+
* The rule, pure.
|
|
36
|
+
*
|
|
37
|
+
* `udpBlocked` requires a named subject, a successful bootstrap and a
|
|
38
|
+
* STUN probe that went unanswered. Every other combination — including
|
|
39
|
+
* "we did not probe", "the probe is unsupported here" and "an error
|
|
40
|
+
* response came back" — stays `iceTimeout`.
|
|
41
|
+
*/
|
|
42
|
+
export function classifyRtcFailure(observations) {
|
|
43
|
+
const { bootstrapOk, stunProbe, probed } = observations;
|
|
44
|
+
if (stunProbe.type !== 'unanswered')
|
|
45
|
+
return { type: 'iceTimeout' };
|
|
46
|
+
const evidence = udpBlockedEvidence(bootstrapOk, true, probed ?? '');
|
|
47
|
+
return evidence === null ? { type: 'iceTimeout' } : { type: 'udpBlocked', evidence };
|
|
48
|
+
}
|
|
49
|
+
/** {@link classifyRtcFailure}, as the error a caller will be rejected with. */
|
|
50
|
+
export function classifyRtcError(observations) {
|
|
51
|
+
return new RtcError(classifyRtcFailure(observations));
|
|
52
|
+
}
|
|
53
|
+
const DEFAULT_PROBE_TIMEOUT_MS = 2500;
|
|
54
|
+
/** Anything at or above this `errorCode` means "no response arrived". */
|
|
55
|
+
const STUN_NO_RESPONSE_CODE = 700;
|
|
56
|
+
const DEFAULT_STUN_PORT = 3478;
|
|
57
|
+
/**
|
|
58
|
+
* Aim a STUN binding at `rtcAddr` and report what came back.
|
|
59
|
+
*
|
|
60
|
+
* Uses an `RTCPeerConnection` whose *only* ICE server is that address:
|
|
61
|
+
* the browser then sends a STUN binding request to it as part of
|
|
62
|
+
* gathering, and surfaces the outcome as either a reflexive candidate
|
|
63
|
+
* or an `icecandidateerror`. The probe connection is never used for
|
|
64
|
+
* media and is always closed.
|
|
65
|
+
*/
|
|
66
|
+
export async function probeStunBinding(rtcAddr, options = {}) {
|
|
67
|
+
const url = diagnosticStunUrl(rtcAddr);
|
|
68
|
+
if (url === null)
|
|
69
|
+
return { type: 'notRun', reason: `not an address: ${rtcAddr}` };
|
|
70
|
+
const factory = options.peerConnectionFactory ?? defaultPeerConnectionFactory();
|
|
71
|
+
if (factory === null) {
|
|
72
|
+
return { type: 'unsupported', detail: 'RTCPeerConnection is not available in this context' };
|
|
73
|
+
}
|
|
74
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
75
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
76
|
+
let pc;
|
|
77
|
+
try {
|
|
78
|
+
pc = factory({ iceServers: [{ urls: url }], iceCandidatePoolSize: 0 });
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return { type: 'unsupported', detail: String(error) };
|
|
82
|
+
}
|
|
83
|
+
let settled = false;
|
|
84
|
+
const settle = (outcome) => {
|
|
85
|
+
if (settled)
|
|
86
|
+
return;
|
|
87
|
+
settled = true;
|
|
88
|
+
resolve(outcome);
|
|
89
|
+
};
|
|
90
|
+
// A "no response" report from the engine, when it makes one. Chromium
|
|
91
|
+
// does not always: against a black-holed address it emits no error and
|
|
92
|
+
// never completes gathering, which is why the deadline below is
|
|
93
|
+
// load-bearing rather than a safety net.
|
|
94
|
+
let noResponse = null;
|
|
95
|
+
const timer = setTimeout(() => settle({ type: 'unanswered', detail: noResponse ?? 'no answer inside the probe deadline' }), timeoutMs);
|
|
96
|
+
const gatheringDone = () => {
|
|
97
|
+
settle({
|
|
98
|
+
type: 'unanswered',
|
|
99
|
+
detail: noResponse ?? 'gathering completed with no reflexive candidate',
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
pc.onicecandidate = (event) => {
|
|
103
|
+
const candidate = event.candidate;
|
|
104
|
+
if (candidate === null) {
|
|
105
|
+
gatheringDone();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Host candidates are gathered regardless of what the network does
|
|
109
|
+
// to UDP — Chromium even hides them behind an mDNS `.local` name —
|
|
110
|
+
// so only a reflexive one is evidence.
|
|
111
|
+
const reflexive = reflexiveAddress(candidate);
|
|
112
|
+
if (reflexive !== null)
|
|
113
|
+
settle({ type: 'reflexive', address: reflexive });
|
|
114
|
+
};
|
|
115
|
+
pc.onicecandidateerror = (event) => {
|
|
116
|
+
const code = event.errorCode;
|
|
117
|
+
const detail = `${code}: ${event.errorText} (${event.url})`;
|
|
118
|
+
// A STUN *error response* is still a response: the packet reached
|
|
119
|
+
// the anchor and the reply got home, so UDP is not blocked. (An
|
|
120
|
+
// anchor's ICE agent refusing an unauthenticated binding lands
|
|
121
|
+
// here; Chromium reports the STUN error number, e.g. `1` for a 401.)
|
|
122
|
+
if (code < STUN_NO_RESPONSE_CODE) {
|
|
123
|
+
settle({ type: 'stunError', code, detail });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
noResponse = detail;
|
|
127
|
+
};
|
|
128
|
+
pc.onicegatheringstatechange = () => {
|
|
129
|
+
if (pc.iceGatheringState === 'complete')
|
|
130
|
+
gatheringDone();
|
|
131
|
+
};
|
|
132
|
+
try {
|
|
133
|
+
// A data section is required or there is nothing to gather for.
|
|
134
|
+
pc.createDataChannel('net-udp-probe');
|
|
135
|
+
await pc.setLocalDescription(await pc.createOffer());
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
settle({ type: 'unsupported', detail: String(error) });
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
return await promise;
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
pc.onicecandidate = null;
|
|
146
|
+
pc.onicecandidateerror = null;
|
|
147
|
+
pc.onicegatheringstatechange = null;
|
|
148
|
+
pc.close();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const DEFAULT_BOOTSTRAP_TIMEOUT_MS = 3000;
|
|
152
|
+
/**
|
|
153
|
+
* Did the anchor answer over HTTPS?
|
|
154
|
+
*
|
|
155
|
+
* Any response counts, including 4xx and 5xx: the question is whether
|
|
156
|
+
* the anchor is up and addressable, not whether it liked this request.
|
|
157
|
+
* The request is `no-cors` on purpose — a cross-origin anchor without
|
|
158
|
+
* CORS headers would otherwise reject a perfectly delivered request
|
|
159
|
+
* and we would under-report reachability — and `no-store`, so a cached
|
|
160
|
+
* response cannot stand in for a live one.
|
|
161
|
+
*/
|
|
162
|
+
export async function probeBootstrapReachable(bootstrapUrl, options = {}) {
|
|
163
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
164
|
+
if (typeof fetchImpl !== 'function')
|
|
165
|
+
return false;
|
|
166
|
+
try {
|
|
167
|
+
await fetchImpl(bootstrapUrl, {
|
|
168
|
+
method: 'GET',
|
|
169
|
+
mode: 'no-cors',
|
|
170
|
+
cache: 'no-store',
|
|
171
|
+
redirect: 'follow',
|
|
172
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_BOOTSTRAP_TIMEOUT_MS),
|
|
173
|
+
});
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* `host:port` (or `[v6]:port`, or a bare host) to a `stun:` URL for
|
|
182
|
+
* the **throwaway diagnostic probe**, or `null` when it is not an
|
|
183
|
+
* address at all. A missing port means the IANA STUN port, which is
|
|
184
|
+
* what an anchor publishing a bare host would mean.
|
|
185
|
+
*
|
|
186
|
+
* This is for {@link probeStunBinding} and nothing else. It must
|
|
187
|
+
* **NOT** be used to build the anchor connection's `iceServers`:
|
|
188
|
+
* `rtc_addr` is that connection's ICE peer, and a peer cannot be its
|
|
189
|
+
* own STUN server — configuring it that way gathers no reflexive
|
|
190
|
+
* candidate and buys an ICE deadline instead of a connection. The
|
|
191
|
+
* leaf refuses it outright ({@link IceServerConflictError}).
|
|
192
|
+
*
|
|
193
|
+
* The endpoint an anchor connection should gather against is the one
|
|
194
|
+
* the anchor announces separately, as `stun_addr` on
|
|
195
|
+
* `GET /rtc/anchor`; `connect()` uses it by default, so a page needs
|
|
196
|
+
* no URL-building at all.
|
|
197
|
+
*/
|
|
198
|
+
export function diagnosticStunUrl(rtcAddr) {
|
|
199
|
+
const trimmed = rtcAddr.trim();
|
|
200
|
+
if (trimmed.length === 0)
|
|
201
|
+
return null;
|
|
202
|
+
if (trimmed.startsWith('stun:') || trimmed.startsWith('stuns:'))
|
|
203
|
+
return trimmed;
|
|
204
|
+
const bracketed = /^\[([0-9a-fA-F:.]+)\](?::(\d+))?$/.exec(trimmed);
|
|
205
|
+
if (bracketed) {
|
|
206
|
+
const host = bracketed[1] ?? '';
|
|
207
|
+
return `stun:[${host}]:${bracketed[2] ?? DEFAULT_STUN_PORT}`;
|
|
208
|
+
}
|
|
209
|
+
// A bare IPv6 literal has more than one colon and no brackets.
|
|
210
|
+
if ((trimmed.match(/:/g)?.length ?? 0) > 1)
|
|
211
|
+
return `stun:[${trimmed}]:${DEFAULT_STUN_PORT}`;
|
|
212
|
+
const hostPort = /^([A-Za-z0-9._-]+)(?::(\d+))?$/.exec(trimmed);
|
|
213
|
+
if (hostPort === null)
|
|
214
|
+
return null;
|
|
215
|
+
const host = hostPort[1] ?? '';
|
|
216
|
+
return `stun:${host}:${hostPort[2] ?? DEFAULT_STUN_PORT}`;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The reflexive address of a candidate, or `null` if it is not a
|
|
220
|
+
* reflexive one.
|
|
221
|
+
*
|
|
222
|
+
* `RTCIceCandidate.type`/`.address` are the modern surface; the SDP
|
|
223
|
+
* line is parsed as well because the fields are still nullable in
|
|
224
|
+
* engines that predate them, and a missed `srflx` would be read as
|
|
225
|
+
* blocked UDP — the exact mis-typing this module exists to prevent.
|
|
226
|
+
*/
|
|
227
|
+
export function reflexiveAddress(candidate) {
|
|
228
|
+
if (candidate.type === 'srflx')
|
|
229
|
+
return candidate.address ?? sdpAddress(candidate.candidate);
|
|
230
|
+
if (candidate.type !== null)
|
|
231
|
+
return null;
|
|
232
|
+
return / typ srflx /.test(candidate.candidate) ? sdpAddress(candidate.candidate) : null;
|
|
233
|
+
}
|
|
234
|
+
function sdpAddress(line) {
|
|
235
|
+
// candidate:<foundation> <component> <transport> <priority> <ip> <port> typ …
|
|
236
|
+
const fields = line.split(' ');
|
|
237
|
+
const ip = fields[4] ?? '';
|
|
238
|
+
const port = fields[5] ?? '';
|
|
239
|
+
return port.length > 0 ? `${ip}:${port}` : ip;
|
|
240
|
+
}
|
|
241
|
+
function defaultPeerConnectionFactory() {
|
|
242
|
+
if (typeof RTCPeerConnection === 'undefined')
|
|
243
|
+
return null;
|
|
244
|
+
return (config) => new RTCPeerConnection(config);
|
|
245
|
+
}
|