@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,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STUN messages (RFC 8489) as TURN (RFC 8656) uses them over UDP: the header, the attributes a
|
|
3
|
+
* relay reads and writes, MESSAGE-INTEGRITY with a long-term key, FINGERPRINT, and ChannelData.
|
|
4
|
+
*
|
|
5
|
+
* Everything decoded here is hostile input: a malformed message decodes to `undefined` rather than
|
|
6
|
+
* throwing, and no length is trusted beyond the bytes actually received.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
|
|
9
|
+
import { isIPv4, isIPv6 } from 'node:net';
|
|
10
|
+
|
|
11
|
+
export const MAGIC_COOKIE = 0x2112a442;
|
|
12
|
+
const HEADER_BYTES = 20;
|
|
13
|
+
const MAX_MESSAGE_BYTES = 65_535;
|
|
14
|
+
|
|
15
|
+
export const StunMethod = {
|
|
16
|
+
Binding: 0x001,
|
|
17
|
+
Allocate: 0x003,
|
|
18
|
+
Refresh: 0x004,
|
|
19
|
+
Send: 0x006,
|
|
20
|
+
Data: 0x007,
|
|
21
|
+
CreatePermission: 0x008,
|
|
22
|
+
ChannelBind: 0x009,
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
export const StunClass = {
|
|
26
|
+
Request: 0,
|
|
27
|
+
Indication: 1,
|
|
28
|
+
Success: 2,
|
|
29
|
+
Error: 3,
|
|
30
|
+
} as const;
|
|
31
|
+
export type TStunClass = (typeof StunClass)[keyof typeof StunClass];
|
|
32
|
+
|
|
33
|
+
export const StunAttr = {
|
|
34
|
+
MappedAddress: 0x0001,
|
|
35
|
+
Username: 0x0006,
|
|
36
|
+
MessageIntegrity: 0x0008,
|
|
37
|
+
ErrorCode: 0x0009,
|
|
38
|
+
UnknownAttributes: 0x000a,
|
|
39
|
+
ChannelNumber: 0x000c,
|
|
40
|
+
Lifetime: 0x000d,
|
|
41
|
+
XorPeerAddress: 0x0012,
|
|
42
|
+
Data: 0x0013,
|
|
43
|
+
Realm: 0x0014,
|
|
44
|
+
Nonce: 0x0015,
|
|
45
|
+
XorRelayedAddress: 0x0016,
|
|
46
|
+
RequestedAddressFamily: 0x0017,
|
|
47
|
+
EvenPort: 0x0018,
|
|
48
|
+
RequestedTransport: 0x0019,
|
|
49
|
+
DontFragment: 0x001a,
|
|
50
|
+
MessageIntegritySha256: 0x001c,
|
|
51
|
+
PasswordAlgorithm: 0x001d,
|
|
52
|
+
Userhash: 0x001e,
|
|
53
|
+
XorMappedAddress: 0x0020,
|
|
54
|
+
ReservationToken: 0x0022,
|
|
55
|
+
Priority: 0x0024,
|
|
56
|
+
UseCandidate: 0x0025,
|
|
57
|
+
Fingerprint: 0x8028,
|
|
58
|
+
} as const;
|
|
59
|
+
|
|
60
|
+
export interface IStunAttribute {
|
|
61
|
+
readonly type: number;
|
|
62
|
+
readonly value: Buffer;
|
|
63
|
+
/** Where the attribute's header starts in the message. */
|
|
64
|
+
readonly offset: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface IStunMessage {
|
|
68
|
+
readonly method: number;
|
|
69
|
+
readonly cls: TStunClass;
|
|
70
|
+
readonly transactionId: Buffer;
|
|
71
|
+
readonly attributes: readonly IStunAttribute[];
|
|
72
|
+
/** The whole message as received. */
|
|
73
|
+
readonly raw: Buffer;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ITransportAddress {
|
|
77
|
+
readonly address: string;
|
|
78
|
+
readonly port: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function messageType(method: number, cls: TStunClass): number {
|
|
82
|
+
return (
|
|
83
|
+
(method & 0x000f) |
|
|
84
|
+
((method & 0x0070) << 1) |
|
|
85
|
+
((method & 0x0f80) << 2) |
|
|
86
|
+
((cls & 1) << 4) |
|
|
87
|
+
((cls & 2) << 7)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Whether the first bytes look like a STUN message (the two top bits zero, the magic cookie). */
|
|
92
|
+
export function isStunMessage(data: Buffer): boolean {
|
|
93
|
+
return (
|
|
94
|
+
data.length >= HEADER_BYTES && (data[0]! & 0xc0) === 0 && data.readUInt32BE(4) === MAGIC_COOKIE
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Decode a STUN message; `undefined` when it is not a well-formed one. */
|
|
99
|
+
export function decodeStun(data: Buffer): IStunMessage | undefined {
|
|
100
|
+
if (!isStunMessage(data)) return undefined;
|
|
101
|
+
const type = data.readUInt16BE(0);
|
|
102
|
+
const length = data.readUInt16BE(2);
|
|
103
|
+
if (length % 4 !== 0 || HEADER_BYTES + length > data.length) return undefined;
|
|
104
|
+
const method = (type & 0x000f) | ((type & 0x00e0) >> 1) | ((type & 0x3e00) >> 2);
|
|
105
|
+
const cls = (((type & 0x0010) >> 4) | ((type & 0x0100) >> 7)) as TStunClass;
|
|
106
|
+
const attributes: IStunAttribute[] = [];
|
|
107
|
+
let offset = HEADER_BYTES;
|
|
108
|
+
const end = HEADER_BYTES + length;
|
|
109
|
+
while (offset < end) {
|
|
110
|
+
if (offset + 4 > end) return undefined;
|
|
111
|
+
const attrType = data.readUInt16BE(offset);
|
|
112
|
+
const attrLength = data.readUInt16BE(offset + 2);
|
|
113
|
+
if (offset + 4 + attrLength > end) return undefined;
|
|
114
|
+
attributes.push({
|
|
115
|
+
type: attrType,
|
|
116
|
+
value: data.subarray(offset + 4, offset + 4 + attrLength),
|
|
117
|
+
offset,
|
|
118
|
+
});
|
|
119
|
+
offset += 4 + attrLength + ((4 - (attrLength % 4)) % 4);
|
|
120
|
+
}
|
|
121
|
+
if (offset !== end) return undefined;
|
|
122
|
+
return {
|
|
123
|
+
method,
|
|
124
|
+
cls,
|
|
125
|
+
transactionId: Buffer.from(data.subarray(8, 20)),
|
|
126
|
+
attributes,
|
|
127
|
+
raw: data.subarray(0, end),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The first attribute of `type`, if any. */
|
|
132
|
+
export function attribute(message: IStunMessage, type: number): Buffer | undefined {
|
|
133
|
+
return message.attributes.find((a) => a.type === type)?.value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function attributes(message: IStunMessage, type: number): Buffer[] {
|
|
137
|
+
return message.attributes.filter((a) => a.type === type).map((a) => a.value);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The message as MESSAGE-INTEGRITY covers it: attributes after it are dropped (RFC 8489 §14.5), since
|
|
142
|
+
* anyone on the path can append them. A message without one is returned as it is.
|
|
143
|
+
*/
|
|
144
|
+
export function integrityProtected(message: IStunMessage): IStunMessage {
|
|
145
|
+
const at = message.attributes.findIndex((a) => a.type === StunAttr.MessageIntegrity);
|
|
146
|
+
if (at < 0) return message;
|
|
147
|
+
return { ...message, attributes: message.attributes.slice(0, at + 1) };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Decode an XOR-*-ADDRESS value; `undefined` when malformed. */
|
|
151
|
+
export function decodeXorAddress(
|
|
152
|
+
value: Buffer,
|
|
153
|
+
transactionId: Buffer,
|
|
154
|
+
): ITransportAddress | undefined {
|
|
155
|
+
if (value.length < 8) return undefined;
|
|
156
|
+
const family = value[1];
|
|
157
|
+
const port = value.readUInt16BE(2) ^ (MAGIC_COOKIE >>> 16);
|
|
158
|
+
if (family === 0x01 && value.length === 8) {
|
|
159
|
+
const raw = value.readUInt32BE(4) ^ MAGIC_COOKIE;
|
|
160
|
+
const address = [raw >>> 24, (raw >>> 16) & 0xff, (raw >>> 8) & 0xff, raw & 0xff].join('.');
|
|
161
|
+
return { address, port };
|
|
162
|
+
}
|
|
163
|
+
if (family === 0x02 && value.length === 20) {
|
|
164
|
+
const mask = Buffer.alloc(16);
|
|
165
|
+
mask.writeUInt32BE(MAGIC_COOKIE, 0);
|
|
166
|
+
transactionId.copy(mask, 4);
|
|
167
|
+
const groups: string[] = [];
|
|
168
|
+
for (let i = 0; i < 16; i += 2) {
|
|
169
|
+
groups.push(((value.readUInt16BE(4 + i) ^ mask.readUInt16BE(i)) & 0xffff).toString(16));
|
|
170
|
+
}
|
|
171
|
+
return { address: groups.join(':'), port };
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function ipv6Bytes(address: string): Buffer {
|
|
177
|
+
const [head = '', tail = ''] = address.split('::') as [string, string?];
|
|
178
|
+
const left = head.length > 0 ? head.split(':') : [];
|
|
179
|
+
const right = address.includes('::') && tail.length > 0 ? tail.split(':') : [];
|
|
180
|
+
const groups = address.includes('::')
|
|
181
|
+
? [...left, ...Array<string>(8 - left.length - right.length).fill('0'), ...right]
|
|
182
|
+
: left;
|
|
183
|
+
const out = Buffer.alloc(16);
|
|
184
|
+
groups.forEach((group, i) => out.writeUInt16BE(parseInt(group, 16) & 0xffff, i * 2));
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Encode an address as an XOR-*-ADDRESS value. */
|
|
189
|
+
export function encodeXorAddress(address: ITransportAddress, transactionId: Buffer): Buffer {
|
|
190
|
+
const port = (address.port ^ (MAGIC_COOKIE >>> 16)) & 0xffff;
|
|
191
|
+
if (isIPv4(address.address)) {
|
|
192
|
+
const out = Buffer.alloc(8);
|
|
193
|
+
out[1] = 0x01;
|
|
194
|
+
out.writeUInt16BE(port, 2);
|
|
195
|
+
const raw = address.address.split('.').reduce((acc, part) => (acc << 8) | Number(part), 0);
|
|
196
|
+
out.writeUInt32BE((raw ^ MAGIC_COOKIE) >>> 0, 4);
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
if (!isIPv6(address.address)) throw new Error(`not an IP address: ${address.address}`);
|
|
200
|
+
const out = Buffer.alloc(20);
|
|
201
|
+
out[1] = 0x02;
|
|
202
|
+
out.writeUInt16BE(port, 2);
|
|
203
|
+
const mask = Buffer.alloc(16);
|
|
204
|
+
mask.writeUInt32BE(MAGIC_COOKIE, 0);
|
|
205
|
+
transactionId.copy(mask, 4);
|
|
206
|
+
const bytes = ipv6Bytes(address.address);
|
|
207
|
+
for (let i = 0; i < 16; i += 1) out[4 + i] = bytes[i]! ^ mask[i]!;
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The long-term credential key (RFC 8489 §9.2.2): MD5(username ":" realm ":" password). */
|
|
212
|
+
export function longTermKey(username: string, realm: string, password: string): Buffer {
|
|
213
|
+
return createHash('md5').update(`${username}:${realm}:${password}`, 'utf8').digest();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Whether the message carries a MESSAGE-INTEGRITY that `key` produced. Only the attributes before it
|
|
218
|
+
* count, with the header length adjusted as the sender computed it.
|
|
219
|
+
*/
|
|
220
|
+
export function verifyIntegrity(message: IStunMessage, key: Buffer): boolean {
|
|
221
|
+
const at = message.attributes.find((a) => a.type === StunAttr.MessageIntegrity);
|
|
222
|
+
if (at === undefined || at.value.length !== 20) return false;
|
|
223
|
+
const covered = Buffer.from(message.raw.subarray(0, at.offset));
|
|
224
|
+
covered.writeUInt16BE(at.offset - HEADER_BYTES + 24, 2);
|
|
225
|
+
const expected = createHmac('sha1', key).update(covered).digest();
|
|
226
|
+
return timingSafeEqual(expected, at.value);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const CRC_TABLE = (() => {
|
|
230
|
+
const table = new Uint32Array(256);
|
|
231
|
+
for (let n = 0; n < 256; n += 1) {
|
|
232
|
+
let c = n;
|
|
233
|
+
for (let k = 0; k < 8; k += 1) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
234
|
+
table[n] = c >>> 0;
|
|
235
|
+
}
|
|
236
|
+
return table;
|
|
237
|
+
})();
|
|
238
|
+
|
|
239
|
+
function crc32(data: Buffer): number {
|
|
240
|
+
let crc = 0xffffffff;
|
|
241
|
+
for (const byte of data) crc = CRC_TABLE[(crc ^ byte) & 0xff]! ^ (crc >>> 8);
|
|
242
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export interface IStunEncodeOptions {
|
|
246
|
+
/** Append MESSAGE-INTEGRITY under this long-term key. */
|
|
247
|
+
readonly integrityKey?: Buffer;
|
|
248
|
+
/** Append FINGERPRINT (default true). */
|
|
249
|
+
readonly fingerprint?: boolean;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Encode a STUN message. */
|
|
253
|
+
export function encodeStun(
|
|
254
|
+
method: number,
|
|
255
|
+
cls: TStunClass,
|
|
256
|
+
transactionId: Buffer,
|
|
257
|
+
attrs: readonly { readonly type: number; readonly value: Buffer }[],
|
|
258
|
+
options: IStunEncodeOptions = {},
|
|
259
|
+
): Buffer {
|
|
260
|
+
const parts: Buffer[] = [];
|
|
261
|
+
const header = Buffer.alloc(HEADER_BYTES);
|
|
262
|
+
header.writeUInt16BE(messageType(method, cls), 0);
|
|
263
|
+
header.writeUInt32BE(MAGIC_COOKIE, 4);
|
|
264
|
+
transactionId.copy(header, 8, 0, 12);
|
|
265
|
+
parts.push(header);
|
|
266
|
+
const push = (type: number, value: Buffer): void => {
|
|
267
|
+
const head = Buffer.alloc(4);
|
|
268
|
+
head.writeUInt16BE(type, 0);
|
|
269
|
+
head.writeUInt16BE(value.length, 2);
|
|
270
|
+
parts.push(head, value, Buffer.alloc((4 - (value.length % 4)) % 4));
|
|
271
|
+
};
|
|
272
|
+
for (const a of attrs) push(a.type, a.value);
|
|
273
|
+
const current = (): Buffer => Buffer.concat(parts);
|
|
274
|
+
if (options.integrityKey !== undefined) {
|
|
275
|
+
const covered = current();
|
|
276
|
+
covered.writeUInt16BE(covered.length - HEADER_BYTES + 24, 2);
|
|
277
|
+
push(
|
|
278
|
+
StunAttr.MessageIntegrity,
|
|
279
|
+
createHmac('sha1', options.integrityKey).update(covered).digest(),
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (options.fingerprint !== false) {
|
|
283
|
+
const covered = current();
|
|
284
|
+
covered.writeUInt16BE(covered.length - HEADER_BYTES + 8, 2);
|
|
285
|
+
const value = Buffer.alloc(4);
|
|
286
|
+
value.writeUInt32BE((crc32(covered) ^ 0x5354554e) >>> 0, 0);
|
|
287
|
+
push(StunAttr.Fingerprint, value);
|
|
288
|
+
}
|
|
289
|
+
const out = current();
|
|
290
|
+
out.writeUInt16BE(out.length - HEADER_BYTES, 2);
|
|
291
|
+
if (out.length > MAX_MESSAGE_BYTES) throw new Error('STUN message too large');
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** An ERROR-CODE value. */
|
|
296
|
+
export function errorCode(code: number, reason: string): Buffer {
|
|
297
|
+
const text = Buffer.from(reason, 'utf8');
|
|
298
|
+
const out = Buffer.alloc(4 + text.length);
|
|
299
|
+
out[2] = Math.floor(code / 100);
|
|
300
|
+
out[3] = code % 100;
|
|
301
|
+
text.copy(out, 4);
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** The code of an ERROR-CODE value; `undefined` when malformed. */
|
|
306
|
+
export function readErrorCode(value: Buffer | undefined): number | undefined {
|
|
307
|
+
if (value === undefined || value.length < 4) return undefined;
|
|
308
|
+
return (value[2]! & 0x07) * 100 + value[3]!;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function uint32(value: number): Buffer {
|
|
312
|
+
const out = Buffer.alloc(4);
|
|
313
|
+
out.writeUInt32BE(value >>> 0, 0);
|
|
314
|
+
return out;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Whether the first bytes are a ChannelData message (the two top bits `01`). */
|
|
318
|
+
export function isChannelData(data: Buffer): boolean {
|
|
319
|
+
return data.length >= 4 && (data[0]! & 0xc0) === 0x40;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Channel numbers a client may bind (RFC 8656 §12). */
|
|
323
|
+
export const MIN_CHANNEL = 0x4000;
|
|
324
|
+
export const MAX_CHANNEL = 0x4fff;
|
|
325
|
+
|
|
326
|
+
export function decodeChannelData(
|
|
327
|
+
data: Buffer,
|
|
328
|
+
): { readonly channel: number; readonly payload: Buffer } | undefined {
|
|
329
|
+
if (!isChannelData(data)) return undefined;
|
|
330
|
+
const channel = data.readUInt16BE(0);
|
|
331
|
+
const length = data.readUInt16BE(2);
|
|
332
|
+
if (4 + length > data.length) return undefined;
|
|
333
|
+
return { channel, payload: data.subarray(4, 4 + length) };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function encodeChannelData(channel: number, payload: Buffer): Buffer {
|
|
337
|
+
const header = Buffer.alloc(4);
|
|
338
|
+
header.writeUInt16BE(channel, 0);
|
|
339
|
+
header.writeUInt16BE(payload.length, 2);
|
|
340
|
+
return Buffer.concat([header, payload, Buffer.alloc((4 - (payload.length % 4)) % 4)]);
|
|
341
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ITransportLifecycleError } from '@robota-sdk/agent-interface-transport';
|
|
2
|
+
|
|
3
|
+
export function createTransportLifecycleError(
|
|
4
|
+
code: ITransportLifecycleError['code'],
|
|
5
|
+
): ITransportLifecycleError {
|
|
6
|
+
return Object.assign(new Error(`WebRtcTransport ${code}.`), {
|
|
7
|
+
name: 'TransportLifecycleError' as const,
|
|
8
|
+
code,
|
|
9
|
+
transportName: 'webrtc',
|
|
10
|
+
});
|
|
11
|
+
}
|