@robota-sdk/agent-transport-webrtc 3.0.0-beta.100
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 +694 -0
- package/LICENSE +661 -0
- package/README.md +111 -0
- package/dist/node/index.cjs +1 -0
- package/dist/node/index.d.cts +1650 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +1650 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +2 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +93 -0
- package/src/__tests__/admission-secure-by-default.test.ts +63 -0
- package/src/__tests__/admission-steps.test.ts +46 -0
- package/src/__tests__/device-mesh-relay.test.ts +441 -0
- package/src/__tests__/device-mesh.test.ts +682 -0
- package/src/__tests__/discovering-mesh-relay.test.ts +234 -0
- package/src/__tests__/enrollment-link.test.ts +103 -0
- package/src/__tests__/fake-datachannel.ts +83 -0
- package/src/__tests__/handoff-grant-gate.test.ts +403 -0
- package/src/__tests__/local-peer-proof-gate.test.ts +215 -0
- package/src/__tests__/mesh-admission-contract.test.ts +38 -0
- package/src/__tests__/mesh-discovery.test.ts +820 -0
- package/src/__tests__/mesh-fixtures.ts +148 -0
- package/src/__tests__/mesh-internet.test.ts +942 -0
- package/src/__tests__/mesh-turn-relay.test.ts +161 -0
- package/src/__tests__/negotiated-certificate.test.ts +76 -0
- package/src/__tests__/nostr-source-condition.test.ts +27 -0
- package/src/__tests__/operator-approval-gate.test.ts +308 -0
- package/src/__tests__/outbound-delivery-carrier.test.ts +155 -0
- package/src/__tests__/pairing-e2e.test.ts +237 -0
- package/src/__tests__/pairing-gate-e3.test.ts +194 -0
- package/src/__tests__/pairing-gate-e4.test.ts +110 -0
- package/src/__tests__/pairing-gate.test.ts +296 -0
- package/src/__tests__/pairing-relay-in-the-middle.test.ts +225 -0
- package/src/__tests__/rtc-channel-close.test.ts +52 -0
- package/src/__tests__/rtc-peer-candidates.test.ts +61 -0
- package/src/__tests__/session-attachment-app-view.test.ts +66 -0
- package/src/__tests__/session-attachment-budget.test.ts +136 -0
- package/src/__tests__/turn-server-bind.test.ts +59 -0
- package/src/__tests__/turn-server.test.ts +335 -0
- package/src/__tests__/turn-test-client.ts +256 -0
- package/src/__tests__/webrtc-implementation.test.ts +83 -0
- package/src/__tests__/webrtc-transport.test.ts +349 -0
- package/src/__tests__/ws-mesh-relay-client.test.ts +95 -0
- package/src/__tests__/ws-signaling-client.test.ts +108 -0
- package/src/admission-steps.ts +55 -0
- package/src/channel-delivery.ts +44 -0
- package/src/datachannel-loader.ts +86 -0
- package/src/device-mesh-node.ts +1019 -0
- package/src/discovering-mesh-relay.ts +675 -0
- package/src/enrollment-link.ts +465 -0
- package/src/handoff-grant-gate.ts +186 -0
- package/src/index.ts +139 -0
- package/src/local-peer-proof.ts +135 -0
- package/src/mainline-dht-store.ts +174 -0
- package/src/mesh-dht.ts +446 -0
- package/src/mesh-discovery.ts +87 -0
- package/src/mesh-item-store.ts +287 -0
- package/src/mesh-lan-listener.ts +193 -0
- package/src/mesh-mdns.ts +406 -0
- package/src/mesh-peer-link.ts +600 -0
- package/src/mesh-public-relays.ts +25 -0
- package/src/mesh-records.ts +357 -0
- package/src/mesh-relay.ts +98 -0
- package/src/mesh-signal.ts +116 -0
- package/src/mesh-turn-relay.ts +238 -0
- package/src/negotiated-certificate.ts +72 -0
- package/src/nostr-mesh-relay.ts +391 -0
- package/src/pairing-channel-lifecycle.ts +31 -0
- package/src/pairing-controllers.ts +81 -0
- package/src/pairing-frames.ts +34 -0
- package/src/pairing-gate-options.ts +133 -0
- package/src/pairing-gate.ts +397 -0
- package/src/rtc-peer.ts +429 -0
- package/src/session-attachment.ts +96 -0
- package/src/signaling.ts +62 -0
- package/src/stun-message.ts +341 -0
- package/src/transport-lifecycle-error.ts +11 -0
- package/src/turn-server.ts +1028 -0
- package/src/webrtc-delivery-lifecycle.ts +54 -0
- package/src/webrtc-transport-options.ts +87 -0
- package/src/webrtc-transport.ts +368 -0
- package/src/ws-mesh-relay-client.ts +117 -0
- package/src/ws-signaling-client.ts +139 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { WsMeshRelayClient } from '../ws-mesh-relay-client.js';
|
|
4
|
+
import type { IWebSocketLike } from '../ws-signaling-client.js';
|
|
5
|
+
|
|
6
|
+
const WS_CONNECTING = 0;
|
|
7
|
+
const WS_OPEN = 1;
|
|
8
|
+
const TOPIC = 't'.repeat(43);
|
|
9
|
+
|
|
10
|
+
function createFakeSocket(): IWebSocketLike & {
|
|
11
|
+
readonly sent: Record<string, unknown>[];
|
|
12
|
+
readyState: number;
|
|
13
|
+
emit(event: 'open' | 'message' | 'error' | 'close', arg?: unknown): void;
|
|
14
|
+
} {
|
|
15
|
+
const sent: Record<string, unknown>[] = [];
|
|
16
|
+
const listeners: Record<string, ((arg: unknown) => void)[]> = {};
|
|
17
|
+
return {
|
|
18
|
+
sent,
|
|
19
|
+
readyState: WS_CONNECTING,
|
|
20
|
+
send(data: string): void {
|
|
21
|
+
sent.push(JSON.parse(data) as Record<string, unknown>);
|
|
22
|
+
},
|
|
23
|
+
close(): void {
|
|
24
|
+
/* no-op for the fake */
|
|
25
|
+
},
|
|
26
|
+
on(event, handler): void {
|
|
27
|
+
(listeners[event] ??= []).push(handler);
|
|
28
|
+
},
|
|
29
|
+
emit(event, arg): void {
|
|
30
|
+
for (const h of listeners[event] ?? []) h(arg);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('WsMeshRelayClient', () => {
|
|
36
|
+
it('declares presence first on open, then flushes messages queued before it', () => {
|
|
37
|
+
const socket = createFakeSocket();
|
|
38
|
+
const client = new WsMeshRelayClient({ url: 'ws://x', createSocket: () => socket });
|
|
39
|
+
client.send(TOPIC, { hello: 1 });
|
|
40
|
+
client.declarePresence([TOPIC]);
|
|
41
|
+
expect(socket.sent).toEqual([]);
|
|
42
|
+
|
|
43
|
+
socket.readyState = WS_OPEN;
|
|
44
|
+
socket.emit('open');
|
|
45
|
+
|
|
46
|
+
expect(socket.sent).toEqual([
|
|
47
|
+
{ type: 'presence', topics: [TOPIC] },
|
|
48
|
+
{ type: 'message', topic: TOPIC, data: { hello: 1 } },
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('routes delivered messages and absences, and reports relay refusals and an unrequested close', () => {
|
|
53
|
+
const socket = createFakeSocket();
|
|
54
|
+
const onError = vi.fn();
|
|
55
|
+
const client = new WsMeshRelayClient({ url: 'ws://x', createSocket: () => socket, onError });
|
|
56
|
+
const messages: unknown[] = [];
|
|
57
|
+
const absent: string[] = [];
|
|
58
|
+
client.onMessage((topic, data) => messages.push([topic, data]));
|
|
59
|
+
client.onAbsent((topic) => absent.push(topic));
|
|
60
|
+
|
|
61
|
+
socket.emit('message', JSON.stringify({ type: 'message', topic: TOPIC, data: 'x' }));
|
|
62
|
+
socket.emit('message', JSON.stringify({ type: 'absent', topic: TOPIC }));
|
|
63
|
+
socket.emit('message', 'not json');
|
|
64
|
+
socket.emit('message', JSON.stringify({ type: 'error', reason: 'rate-limited' }));
|
|
65
|
+
socket.emit('close');
|
|
66
|
+
|
|
67
|
+
expect(messages).toEqual([[TOPIC, 'x']]);
|
|
68
|
+
expect(absent).toEqual([TOPIC]);
|
|
69
|
+
expect(onError).toHaveBeenCalledTimes(2);
|
|
70
|
+
expect(String(onError.mock.calls[0]?.[0])).toMatch(/rate-limited/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('an intentional close is not an error, and nothing is sent after it', () => {
|
|
74
|
+
const socket = createFakeSocket();
|
|
75
|
+
const onError = vi.fn();
|
|
76
|
+
const client = new WsMeshRelayClient({ url: 'ws://x', createSocket: () => socket, onError });
|
|
77
|
+
socket.readyState = WS_OPEN;
|
|
78
|
+
client.close();
|
|
79
|
+
socket.emit('close');
|
|
80
|
+
client.send(TOPIC, 'late');
|
|
81
|
+
expect(onError).not.toHaveBeenCalled();
|
|
82
|
+
expect(socket.sent).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('bounds what it queues while the relay is unreachable', () => {
|
|
86
|
+
const socket = createFakeSocket();
|
|
87
|
+
const onError = vi.fn();
|
|
88
|
+
const client = new WsMeshRelayClient({ url: 'ws://x', createSocket: () => socket, onError });
|
|
89
|
+
for (let i = 0; i < 300; i += 1) client.send(TOPIC, i);
|
|
90
|
+
expect(onError).toHaveBeenCalled();
|
|
91
|
+
socket.readyState = WS_OPEN;
|
|
92
|
+
socket.emit('open');
|
|
93
|
+
expect(socket.sent.length).toBe(256);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { WsSignalingClient, type IWebSocketLike } from '../ws-signaling-client.js';
|
|
4
|
+
import type { ISignalMessage } from '../signaling.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* REMOTE-004 B2 — `WsSignalingClient` unit tests with an injected fake socket (no network). Cover pre-open
|
|
8
|
+
* buffering + flush and explicit error surfacing (relay `error` frame, close-before-join).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const WS_CONNECTING = 0;
|
|
12
|
+
const WS_OPEN = 1;
|
|
13
|
+
|
|
14
|
+
function createFakeSocket(): IWebSocketLike & {
|
|
15
|
+
readonly sent: string[];
|
|
16
|
+
readyState: number;
|
|
17
|
+
emit(event: 'open' | 'message' | 'error' | 'close', arg?: unknown): void;
|
|
18
|
+
} {
|
|
19
|
+
const sent: string[] = [];
|
|
20
|
+
const listeners: Record<string, ((arg: unknown) => void)[]> = {};
|
|
21
|
+
return {
|
|
22
|
+
sent,
|
|
23
|
+
readyState: WS_CONNECTING,
|
|
24
|
+
send(data: string): void {
|
|
25
|
+
sent.push(data);
|
|
26
|
+
},
|
|
27
|
+
close(): void {
|
|
28
|
+
/* no-op for the fake */
|
|
29
|
+
},
|
|
30
|
+
on(event, handler): void {
|
|
31
|
+
(listeners[event] ??= []).push(handler);
|
|
32
|
+
},
|
|
33
|
+
emit(event, arg): void {
|
|
34
|
+
for (const h of listeners[event] ?? []) h(arg);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parse(raw: string): Record<string, unknown> {
|
|
40
|
+
return JSON.parse(raw) as Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('WsSignalingClient (REMOTE-004 B2)', () => {
|
|
44
|
+
it('TC-02: buffers signals produced before open and flushes them (after the join) on open', () => {
|
|
45
|
+
const socket = createFakeSocket();
|
|
46
|
+
const client = new WsSignalingClient({
|
|
47
|
+
url: 'ws://x',
|
|
48
|
+
rendezvous: 'r',
|
|
49
|
+
createSocket: () => socket,
|
|
50
|
+
});
|
|
51
|
+
// send an offer before the socket opens
|
|
52
|
+
const offer: ISignalMessage = { kind: 'offer', data: { sdp: 'v=0' } };
|
|
53
|
+
client.send(offer);
|
|
54
|
+
expect(socket.sent).toHaveLength(0); // buffered, nothing on the wire yet
|
|
55
|
+
|
|
56
|
+
socket.readyState = WS_OPEN;
|
|
57
|
+
socket.emit('open');
|
|
58
|
+
|
|
59
|
+
// first frame is the join, then the flushed offer
|
|
60
|
+
expect(parse(socket.sent[0]!)).toEqual({ type: 'join', rendezvous: 'r' });
|
|
61
|
+
expect(parse(socket.sent[1]!)).toEqual({ type: 'signal', kind: 'offer', data: { sdp: 'v=0' } });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('delivers inbound relay signals to onSignal handlers', () => {
|
|
65
|
+
const socket = createFakeSocket();
|
|
66
|
+
const client = new WsSignalingClient({
|
|
67
|
+
url: 'ws://x',
|
|
68
|
+
rendezvous: 'r',
|
|
69
|
+
createSocket: () => socket,
|
|
70
|
+
});
|
|
71
|
+
const received: ISignalMessage[] = [];
|
|
72
|
+
client.onSignal((m) => received.push(m));
|
|
73
|
+
socket.emit('message', JSON.stringify({ type: 'signal', kind: 'answer', data: { sdp: 'a' } }));
|
|
74
|
+
expect(received).toEqual([{ kind: 'answer', data: { sdp: 'a' } }]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('TC-02: surfaces a relay error frame via onError (no silent degrade)', () => {
|
|
78
|
+
const socket = createFakeSocket();
|
|
79
|
+
const onError = vi.fn();
|
|
80
|
+
new WsSignalingClient({ url: 'ws://x', rendezvous: 'r', onError, createSocket: () => socket });
|
|
81
|
+
socket.emit('message', JSON.stringify({ type: 'error', reason: 'rate-limited' }));
|
|
82
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect((onError.mock.calls[0]![0] as Error).message).toMatch(/rate-limited/);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('TC-02: surfaces a close-before-join as an explicit error', () => {
|
|
87
|
+
const socket = createFakeSocket();
|
|
88
|
+
const onError = vi.fn();
|
|
89
|
+
new WsSignalingClient({ url: 'ws://x', rendezvous: 'r', onError, createSocket: () => socket });
|
|
90
|
+
socket.emit('close');
|
|
91
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
92
|
+
expect((onError.mock.calls[0]![0] as Error).message).toMatch(/closed before/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('does NOT treat an intentional close() as an error', () => {
|
|
96
|
+
const socket = createFakeSocket();
|
|
97
|
+
const onError = vi.fn();
|
|
98
|
+
const client = new WsSignalingClient({
|
|
99
|
+
url: 'ws://x',
|
|
100
|
+
rendezvous: 'r',
|
|
101
|
+
onError,
|
|
102
|
+
createSocket: () => socket,
|
|
103
|
+
});
|
|
104
|
+
client.close();
|
|
105
|
+
socket.emit('close');
|
|
106
|
+
expect(onError).not.toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which admission step a channel still owes before the session may be exposed.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `pairing-gate.ts` because it is the one decision in that file that is a pure
|
|
5
|
+
* function of configuration and state — and because it is the ORDERING that carries the security
|
|
6
|
+
* argument, so it deserves to be readable and testable without a channel, a handshake or a session.
|
|
7
|
+
*
|
|
8
|
+
* The order is not arbitrary. Each step must run on a channel the previous one has already bound:
|
|
9
|
+
*
|
|
10
|
+
* handshake binds the CHANNEL — it terminates at the peer that knew the secret
|
|
11
|
+
* local-proof binds the ENVIRONMENT — that peer reached a 0700 directory owned by this user
|
|
12
|
+
* handoff-grant binds the TRANSFER — one user, one destination, one channel, signed
|
|
13
|
+
* operator-approval the receiving operator allows THIS connection — asked last, once every proof
|
|
14
|
+
* has run, so the operator is asked only about a peer that is who it claims
|
|
15
|
+
*
|
|
16
|
+
* Each is opt-in, and configuring two means requiring both. That is strictly more restrictive than
|
|
17
|
+
* requiring either, which is the safe direction — and it is the correct reading for a hand-off
|
|
18
|
+
* between two sessions on ONE machine, which legitimately has both to present.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** The step still owed, or null when the session may be exposed. */
|
|
22
|
+
export type TAdmissionStep = 'local-proof' | 'handoff-grant' | 'operator-approval' | null;
|
|
23
|
+
|
|
24
|
+
/** What the gate has configured. Named structurally so this module needs none of the gate's types. */
|
|
25
|
+
export interface IConfiguredAdmissionSteps {
|
|
26
|
+
readonly localPeer?: unknown;
|
|
27
|
+
readonly handoffGrant?: unknown;
|
|
28
|
+
readonly connectionApproval?: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The next step, given what is configured and what has already run.
|
|
33
|
+
*
|
|
34
|
+
* `completed` is the gate's current state rather than a set of finished steps, because the states
|
|
35
|
+
* ARE the record: reaching `handoff-grant` is only possible by having satisfied `local-proof` when
|
|
36
|
+
* one was required.
|
|
37
|
+
*/
|
|
38
|
+
export function nextAdmissionStep(
|
|
39
|
+
configured: IConfiguredAdmissionSteps,
|
|
40
|
+
completed: string,
|
|
41
|
+
): TAdmissionStep {
|
|
42
|
+
const approved = completed === 'operator-approval';
|
|
43
|
+
if (
|
|
44
|
+
configured.localPeer !== undefined &&
|
|
45
|
+
completed !== 'local-proof' &&
|
|
46
|
+
completed !== 'handoff-grant' &&
|
|
47
|
+
!approved
|
|
48
|
+
) {
|
|
49
|
+
return 'local-proof';
|
|
50
|
+
}
|
|
51
|
+
if (configured.handoffGrant !== undefined && completed !== 'handoff-grant' && !approved)
|
|
52
|
+
return 'handoff-grant';
|
|
53
|
+
if (configured.connectionApproval !== undefined && !approved) return 'operator-approval';
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The data channel's outbound delivery boundary (ARCH-030).
|
|
3
|
+
*
|
|
4
|
+
* Both places that build a bare `createSessionMessageHandler` in this package — `PairingGate`'s no-`resumeBridge`
|
|
5
|
+
* branch and `WebRtcTransport`'s no-secret branch — need exactly the same thing: serialize a
|
|
6
|
+
* `TServerMessage` onto an `RTCDataChannel` and route a send failure into that carrier's own failure
|
|
7
|
+
* policy. Written inline at both sites it was two copies of one decision, and it pushed both files past
|
|
8
|
+
* their size ceilings. One owner, named after what it is.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createOutboundDelivery } from '@robota-sdk/agent-transport';
|
|
12
|
+
|
|
13
|
+
import type { TOutboundDeliver } from '@robota-sdk/agent-transport';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The slice of `RTCDataChannel` an outbound boundary needs.
|
|
17
|
+
*
|
|
18
|
+
* `bufferedAmount` is optional because the slice is structural — a test double may omit it — but a
|
|
19
|
+
* real `RTCDataChannel` always has it, so the production path reports a real number rather than a
|
|
20
|
+
* placeholder (ARCH-030 / issue #1734).
|
|
21
|
+
*/
|
|
22
|
+
export interface IDataChannelSink {
|
|
23
|
+
send: (data: string) => void;
|
|
24
|
+
readonly bufferedAmount?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build the connection's outbound boundary over a data channel.
|
|
29
|
+
*
|
|
30
|
+
* @param channel - the data channel to serialize onto. `send` may throw once the channel is closing.
|
|
31
|
+
* @param onDeliveryError - this carrier's failure policy, invoked at most once (the boundary latches).
|
|
32
|
+
*/
|
|
33
|
+
export function createChannelDelivery(
|
|
34
|
+
channel: IDataChannelSink,
|
|
35
|
+
onDeliveryError: (error: Error, event: string) => void,
|
|
36
|
+
): TOutboundDeliver {
|
|
37
|
+
return createOutboundDelivery(
|
|
38
|
+
(message) => channel.send(JSON.stringify(message)),
|
|
39
|
+
(error, event) => onDeliveryError(error, event),
|
|
40
|
+
// Read at CALL time, not captured: `bufferedAmount` is a live property and a value read once
|
|
41
|
+
// would answer with the backpressure of the moment the boundary was built.
|
|
42
|
+
() => channel.bufferedAmount,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WebRTC implementation this transport runs on: `node-datachannel` (libdatachannel, with DTLS by
|
|
3
|
+
* OpenSSL), an optional dependency loaded lazily.
|
|
4
|
+
*
|
|
5
|
+
* It is optional because it ships a native binary per platform. Where it cannot load — not installed,
|
|
6
|
+
* or no prebuilt binary for this platform — the transport is unavailable and says so at the point of
|
|
7
|
+
* use. There is deliberately no other implementation to fall back to: channel binding relies on this
|
|
8
|
+
* DTLS stack verifying handshake signatures.
|
|
9
|
+
*/
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
|
|
12
|
+
/** The slice of a `node-datachannel` data channel this transport drives. */
|
|
13
|
+
export interface INdcDataChannel {
|
|
14
|
+
close(): void;
|
|
15
|
+
sendMessage(message: string): boolean;
|
|
16
|
+
isOpen(): boolean;
|
|
17
|
+
bufferedAmount(): number;
|
|
18
|
+
/** The label the opening side gave the channel. */
|
|
19
|
+
getLabel?(): string;
|
|
20
|
+
onOpen(callback: () => void): void;
|
|
21
|
+
onClosed(callback: () => void): void;
|
|
22
|
+
onError(callback: (error: string) => void): void;
|
|
23
|
+
onMessage(callback: (message: string | Buffer | ArrayBuffer) => void): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** An ICE server as `node-datachannel` takes it. */
|
|
27
|
+
export interface INdcIceServer {
|
|
28
|
+
readonly hostname: string;
|
|
29
|
+
readonly port: number;
|
|
30
|
+
readonly username?: string;
|
|
31
|
+
readonly password?: string;
|
|
32
|
+
readonly relayType?: 'TurnUdp' | 'TurnTcp' | 'TurnTls';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface INdcConfig {
|
|
36
|
+
iceServers: (string | INdcIceServer)[];
|
|
37
|
+
iceTransportPolicy?: 'all' | 'relay';
|
|
38
|
+
disableAutoNegotiation?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The slice of a `node-datachannel` peer connection this transport drives. */
|
|
42
|
+
export interface INdcPeerConnection {
|
|
43
|
+
close(): void;
|
|
44
|
+
setLocalDescription(type?: 'offer' | 'answer'): void;
|
|
45
|
+
setRemoteDescription(sdp: string, type: 'offer' | 'answer'): void;
|
|
46
|
+
localDescription(): { type: string; sdp: string } | null;
|
|
47
|
+
remoteFingerprint(): { value: string; algorithm: string };
|
|
48
|
+
addRemoteCandidate(candidate: string, mid: string): void;
|
|
49
|
+
createDataChannel(label: string): INdcDataChannel;
|
|
50
|
+
state(): string;
|
|
51
|
+
/** Diagnostics; not every build exposes them. */
|
|
52
|
+
iceState?(): string;
|
|
53
|
+
gatheringState?(): string;
|
|
54
|
+
signalingState?(): string;
|
|
55
|
+
onLocalDescription(callback: (sdp: string, type: string) => void): void;
|
|
56
|
+
onLocalCandidate(callback: (candidate: string, mid: string) => void): void;
|
|
57
|
+
onStateChange(callback: (state: string) => void): void;
|
|
58
|
+
onDataChannel(callback: (channel: INdcDataChannel) => void): void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The subset of the `node-datachannel` module surface this transport constructs. */
|
|
62
|
+
export interface IDataChannelModule {
|
|
63
|
+
PeerConnection: new (name: string, config: INdcConfig) => INdcPeerConnection;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Resolve a module by id. The default resolves the real module; tests inject a throwing resolver. */
|
|
67
|
+
export type TModuleResolver = (id: string) => unknown;
|
|
68
|
+
|
|
69
|
+
const defaultResolver: TModuleResolver = (id) => {
|
|
70
|
+
const requireFrom = createRequire(import.meta.url);
|
|
71
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- optional native dependency; resolved at runtime, never bundled
|
|
72
|
+
return requireFrom(id);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export function loadDataChannel(resolve: TModuleResolver = defaultResolver): IDataChannelModule {
|
|
76
|
+
try {
|
|
77
|
+
const module = resolve('node-datachannel') as Partial<IDataChannelModule> | undefined;
|
|
78
|
+
if (typeof module?.PeerConnection !== 'function') throw new Error('no PeerConnection export');
|
|
79
|
+
return module as IDataChannelModule;
|
|
80
|
+
} catch {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`WebRTC transport unavailable — the optional dependency "node-datachannel" is not installed or has no ` +
|
|
83
|
+
`prebuilt binary for this platform (${process.platform}-${process.arch}).`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|