@twinsearth/w2m-dsh-plugin 0.5.7 → 0.5.9

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.
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Multicast DNS wire codec 鈥?a bounded, hand-written DNS message encoder and decoder.
2
+ * Multicast DNS wire codec --a bounded, hand-written DNS message encoder and decoder.
3
3
  *
4
4
  * # THE RULE
5
5
  *
6
6
  * **Discovery is a hint, never an authorisation.** A discovered device is a *candidate to ask*. It
7
- * never skips, weakens or short-circuits any existing gate 鈥?the device token, the per-machine
7
+ * never skips, weakens or short-circuits any existing gate --the device token, the per-machine
8
8
  * allow-list, the git anchors, the verdict model. Nothing in this file, in `src/agent/mdns.mjs`, or in
9
9
  * `src/plugin/mdns-discover.mjs` may treat "we saw it announce itself" as "we may run there". If a
10
10
  * future edit finds itself writing code where a discovered device is trusted *because it was
@@ -14,10 +14,10 @@
14
14
  *
15
15
  * # Clean room
16
16
  *
17
- * Written from the published rules 鈥?RFC 1035 搂4.1 (message format, name compression), RFC 1035
18
- * 搂2.3.4 (the 255-octet name limit), RFC 1035 搂3.1/搂3.2.2 (record and type definitions), RFC 2782
19
- * (SRV), RFC 6763 (DNS-SD record layout, the TXT record's key=value convention), RFC 6762 搂18
20
- * (mDNS message format) and RFC 6762 搂10 (TTL values, goodbye packets). No third-party
17
+ * Written from the published rules --RFC 1035 --4.1 (message format, name compression), RFC 1035
18
+ * --2.3.4 (the 255-octet name limit), RFC 1035 --3.1/--3.2.2 (record and type definitions), RFC 2782
19
+ * (SRV), RFC 6763 (DNS-SD record layout, the TXT record's key=value convention), RFC 6762 --18
20
+ * (mDNS message format) and RFC 6762 --10 (TTL values, goodbye packets). No third-party
21
21
  * implementation was consulted line by line and none was copied: this project is MIT and stays free of
22
22
  * third-party notice entanglement, and deriving from the specification is also what lets us *not*
23
23
  * reproduce the defects a studied implementation had.
@@ -31,7 +31,7 @@
31
31
  * scan must be seen at its new address, not at the address it had when the scan began.
32
32
  * * **a name as identity.** Nothing here decides identity at all; the codec only reports what the
33
33
  * wire said, including the exact `name` bytes as written. Identity is decided in
34
- * `src/plugin/mdns-discover.mjs`, by a `machine_id` the relay assigned 鈥?never by a matching
34
+ * `src/plugin/mdns-discover.mjs`, by a `machine_id` the relay assigned --never by a matching
35
35
  * service or host name.
36
36
  *
37
37
  * # What is implemented, and what is not
@@ -52,17 +52,17 @@
52
52
  * | guard | limit | refusal |
53
53
  * | ---------------------------- | ----- | -------------------- |
54
54
  * | label length octet | 63 | `MDNS_LABEL_TOO_LONG`|
55
- * | unsupported label kind (01/10)| 鈥? | `MDNS_LABEL_TYPE` |
55
+ * | unsupported label kind (01/10)| -- | `MDNS_LABEL_TYPE` |
56
56
  * | expanded name length | 255 | `MDNS_NAME_TOO_LONG` |
57
57
  * | compression pointer target | must be strictly earlier in the message | `MDNS_POINTER_LOOP` |
58
58
  * | compression pointer hops | 128 | `MDNS_POINTER_HOPS` |
59
59
  * | records per message | 256 | `MDNS_RECORD_COUNT` |
60
60
  * | message size | 9000 | `MDNS_MESSAGE_TOO_LARGE` |
61
- * | any read past the end | 鈥? | `MDNS_TRUNCATED` |
61
+ * | any read past the end | -- | `MDNS_TRUNCATED` |
62
62
  *
63
63
  * The two pointer guards are both load-bearing, and neither is decoration:
64
64
  *
65
- * * RFC 1035 搂4.1.4 says a pointer refers to "a prior occurrence" of the name, i.e. to an offset
65
+ * * RFC 1035 --4.1.4 says a pointer refers to "a prior occurrence" of the name, i.e. to an offset
66
66
  * *earlier* in the message. A pointer that does not point strictly backwards is the only way to
67
67
  * build a cycle, so it is refused as a loop. This alone guarantees termination.
68
68
  * * The hop counter (128, the conventional guard) is the bound on *work*, not just on loops: a
@@ -79,13 +79,13 @@ import { Buffer } from 'node:buffer';
79
79
  // Constants
80
80
  // ---------------------------------------------------------------------------------------------
81
81
 
82
- /** DNS CLASS IN (RFC 1035 搂3.2.4). */
82
+ /** DNS CLASS IN (RFC 1035 --3.2.4). */
83
83
  export const DNS_CLASS_IN = 1;
84
84
 
85
- /** DNS CLASS ANY (`*`), which mDNS queries use to ask for every type of a name (RFC 6762 搂5). */
85
+ /** DNS CLASS ANY (`*`), which mDNS queries use to ask for every type of a name (RFC 6762 --5). */
86
86
  export const DNS_CLASS_ANY = 255;
87
87
 
88
- /** Record types this layer reads. RFC 1035 搂3.2.2, RFC 1035 搂3.2.3 (AAAA is RFC 3596), RFC 2782. */
88
+ /** Record types this layer reads. RFC 1035 --3.2.2, RFC 1035 --3.2.3 (AAAA is RFC 3596), RFC 2782. */
89
89
  export const RECORD_TYPE = Object.freeze({
90
90
  A: 1,
91
91
  PTR: 12,
@@ -108,26 +108,26 @@ const TYPE_NAME = Object.freeze({
108
108
  /**
109
109
  * The most significant bit of the *rrclass* field in a response record.
110
110
  *
111
- * RFC 6762 搂10.2: "this is not a record class 0x8001" 鈥?the bit means "the rest of this RRset is
111
+ * RFC 6762 --10.2: "this is not a record class 0x8001" --the bit means "the rest of this RRset is
112
112
  * replaced", and it must be masked off before the class is compared. It is the *same bit value* as
113
113
  * {@link UNICAST_RESPONSE} in a question, but a different field with a different meaning; the two
114
114
  * constants are separate so a future edit cannot conflate them.
115
115
  */
116
116
  export const CACHE_FLUSH = 0x8000;
117
117
 
118
- /** The most significant bit of the *qclass* field in a question (RFC 6762 搂5.4, "QU" questions). */
118
+ /** The most significant bit of the *qclass* field in a question (RFC 6762 --5.4, "QU" questions). */
119
119
  export const UNICAST_RESPONSE = 0x8000;
120
120
 
121
- /** RFC 1035 搂2.3.4: a label is at most 63 octets, which is why the top two bits can be flags. */
121
+ /** RFC 1035 --2.3.4: a label is at most 63 octets, which is why the top two bits can be flags. */
122
122
  export const MAX_LABEL_BYTES = 63;
123
123
 
124
- /** RFC 1035 搂2.3.4: a name (length octets, label octets and the terminating zero) is at most 255. */
124
+ /** RFC 1035 --2.3.4: a name (length octets, label octets and the terminating zero) is at most 255. */
125
125
  export const MAX_NAME_BYTES = 255;
126
126
 
127
127
  /**
128
128
  * Largest message this codec will parse or produce.
129
129
  *
130
- * RFC 6762 搂17 asks that multicast messages fit in 1500 bytes "whenever possible" and caps them at
130
+ * RFC 6762 --17 asks that multicast messages fit in 1500 bytes "whenever possible" and caps them at
131
131
  * 9000 (`MAX_MESSAGE_BYTES` here). The cap is also a bound on parser work, which is the reason it is
132
132
  * enforced on *decode* as well: a datagram larger than this is refused before a single name is read.
133
133
  */
@@ -143,8 +143,8 @@ export const MAX_RECORDS = 256;
143
143
  export const MAX_COMPRESSION_ENTRIES = 512;
144
144
 
145
145
  /**
146
- * The two top bits of a length octet (RFC 1035 搂4.1.4): `00` is a label length, `11` is a compression
147
- * pointer, `01` and `10` are reserved. The mask is one octet wide 鈥?the 16-bit form, `0xc000`, is what
146
+ * The two top bits of a length octet (RFC 1035 --4.1.4): `00` is a label length, `11` is a compression
147
+ * pointer, `01` and `10` are reserved. The mask is one octet wide --the 16-bit form, `0xc000`, is what
148
148
  * the *writer* ORs into a pointer, and mixing the two up makes every pointer unreadable.
149
149
  */
150
150
  const LABEL_KIND_MASK = 0xc0;
@@ -153,8 +153,8 @@ const LABEL_KIND_MASK = 0xc0;
153
153
  const POINTER_WORD_MASK = 0xc000;
154
154
 
155
155
  /**
156
- * RFC 6762 搂6.7: a response to a query from a source port other than 5353 is a *legacy* unicast
157
- * response, and its TTLs "SHOULD NOT be greater than ten seconds" 鈥?the resolver that asked has no
156
+ * RFC 6762 --6.7: a response to a query from a source port other than 5353 is a *legacy* unicast
157
+ * response, and its TTLs "SHOULD NOT be greater than ten seconds" --the resolver that asked has no
158
158
  * cache-coherency protocol and would keep the record long after it stopped being true.
159
159
  */
160
160
  export const LEGACY_UNICAST_TTL = 10;
@@ -162,10 +162,10 @@ export const LEGACY_UNICAST_TTL = 10;
162
162
  /** 14 bits of pointer target, i.e. the largest offset a pointer can name. */
163
163
  const MAX_POINTER_TARGET = 0x3fff;
164
164
 
165
- /** Header size: ID, flags, and four section counts (RFC 1035 搂4.1.1). */
165
+ /** Header size: ID, flags, and four section counts (RFC 1035 --4.1.1). */
166
166
  const HEADER_BYTES = 12;
167
167
 
168
- /** Flag bits of the second header word (RFC 1035 搂4.1.1). */
168
+ /** Flag bits of the second header word (RFC 1035 --4.1.1). */
169
169
  export const FLAG = Object.freeze({
170
170
  QR: 0x8000,
171
171
  AA: 0x0400,
@@ -198,7 +198,7 @@ export const REFUSAL = Object.freeze({
198
198
  /**
199
199
  * An invalid *argument* to an encoder.
200
200
  *
201
- * The decoder never throws 鈥?it refuses. The encoder does, because its input is our own data: writing
201
+ * The decoder never throws --it refuses. The encoder does, because its input is our own data: writing
202
202
  * a packet we know to be malformed would put a lie on the wire where nobody is left to catch it.
203
203
  */
204
204
  export class MdnsWireError extends Error {
@@ -225,7 +225,7 @@ function refuse(reason, detail = undefined) {
225
225
  /**
226
226
  * Lower-case a name and drop one trailing dot, for case-insensitive comparison.
227
227
  *
228
- * DNS names are case-insensitive (RFC 1035 搂2.3.3) but the wire case is preserved everywhere else in
228
+ * DNS names are case-insensitive (RFC 1035 --2.3.3) but the wire case is preserved everywhere else in
229
229
  * this module, because a DNS-SD *instance* name is a human-readable label and "Mac Studio" is not
230
230
  * "mac studio" to the person who reads it.
231
231
  *
@@ -282,7 +282,7 @@ export function splitName(name, what = 'name') {
282
282
  if (bytes > MAX_LABEL_BYTES) {
283
283
  throw new MdnsWireError(
284
284
  REFUSAL.LABEL_TOO_LONG,
285
- `${what} ${JSON.stringify(name)} has a ${bytes}-octet label; RFC 1035 搂2.3.4 allows ${MAX_LABEL_BYTES}`,
285
+ `${what} ${JSON.stringify(name)} has a ${bytes}-octet label; RFC 1035 --2.3.4 allows ${MAX_LABEL_BYTES}`,
286
286
  );
287
287
  }
288
288
  encoded += bytes + 1;
@@ -290,14 +290,14 @@ export function splitName(name, what = 'name') {
290
290
  if (encoded > MAX_NAME_BYTES) {
291
291
  throw new MdnsWireError(
292
292
  REFUSAL.NAME_TOO_LONG,
293
- `${what} ${JSON.stringify(name)} encodes to ${encoded} octets; RFC 1035 搂2.3.4 allows ${MAX_NAME_BYTES}`,
293
+ `${what} ${JSON.stringify(name)} encodes to ${encoded} octets; RFC 1035 --2.3.4 allows ${MAX_NAME_BYTES}`,
294
294
  );
295
295
  }
296
296
  return labels;
297
297
  }
298
298
 
299
299
  /**
300
- * `a.b.c.` 鈫?encoded name, with no compression (a standalone helper for tests and for RDATA that must
300
+ * `a.b.c.` --?encoded name, with no compression (a standalone helper for tests and for RDATA that must
301
301
  * not be compressed).
302
302
  *
303
303
  * @param {string} name
@@ -315,7 +315,7 @@ export function encodeName(name, options = {}) {
315
315
  return Buffer.concat(parts);
316
316
  }
317
317
 
318
- /** `192.168.0.1` 鈫?4 octets. Strict: four decimal octets, each 0-255, no shorthand. */
318
+ /** `192.168.0.1` --?4 octets. Strict: four decimal octets, each 0-255, no shorthand. */
319
319
  export function parseIpv4(text) {
320
320
  if (typeof text !== 'string')
321
321
  throw new MdnsWireError('MDNS_A_INVALID', `IPv4 address must be a string, got ${typeof text}`);
@@ -334,13 +334,13 @@ export function parseIpv4(text) {
334
334
  return bytes;
335
335
  }
336
336
 
337
- /** 4 octets 鈫?dotted quad. */
337
+ /** 4 octets --?dotted quad. */
338
338
  export function formatIpv4(bytes) {
339
339
  return `${bytes[0]}.${bytes[1]}.${bytes[2]}.${bytes[3]}`;
340
340
  }
341
341
 
342
342
  /**
343
- * `2001:db8::1` 鈫?16 octets, with `::` expansion and an embedded IPv4 tail.
343
+ * `2001:db8::1` --?16 octets, with `::` expansion and an embedded IPv4 tail.
344
344
  *
345
345
  * Hand-written rather than borrowed: `node:net` validates an IPv6 literal but does not hand back its
346
346
  * bytes, and the only other source of bytes would be a dependency this project does not take.
@@ -359,7 +359,7 @@ export function parseIpv6(text) {
359
359
  if (value.includes('%'))
360
360
  throw new MdnsWireError('MDNS_AAAA_INVALID', `${text} carries a zone index, which is link-local only`);
361
361
  if (value.includes('.')) {
362
- // An embedded IPv4 tail (RFC 4291 搂2.2): convert it to two groups and re-parse.
362
+ // An embedded IPv4 tail (RFC 4291 --2.2): convert it to two groups and re-parse.
363
363
  const cut = value.lastIndexOf(':');
364
364
  if (cut === -1)
365
365
  throw new MdnsWireError('MDNS_AAAA_INVALID', `${text} has an IPv4 tail but no IPv6 prefix`);
@@ -395,7 +395,7 @@ export function parseIpv6(text) {
395
395
  }
396
396
 
397
397
  /**
398
- * 16 octets 鈫?text, with the longest run of zero groups collapsed to `::` (RFC 5952 搂4.2).
398
+ * 16 octets --?text, with the longest run of zero groups collapsed to `::` (RFC 5952 --4.2).
399
399
  *
400
400
  * @param {Buffer} bytes
401
401
  * @returns {string}
@@ -458,7 +458,7 @@ function readName(buffer, start) {
458
458
  /** Offset just past the name in the *original* stream, which a pointer fixes once and for all. */
459
459
  let next = null;
460
460
  let hops = 0;
461
- let expanded = 1; // the terminating zero octet counts towards the 255 (RFC 1035 搂2.3.4)
461
+ let expanded = 1; // the terminating zero octet counts towards the 255 (RFC 1035 --2.3.4)
462
462
 
463
463
  for (;;) {
464
464
  if (position >= buffer.length) return refuse(REFUSAL.TRUNCATED, { at: position, reading: 'name' });
@@ -473,7 +473,7 @@ function readName(buffer, start) {
473
473
  // is refused as a budget overrun rather than being mistaken for a loop.
474
474
  if (hops >= MAX_POINTER_HOPS)
475
475
  return refuse(REFUSAL.POINTER_HOPS, { hops, target, limit: MAX_POINTER_HOPS });
476
- // RFC 1035 搂4.1.4: a pointer names a *prior* occurrence. A pointer that does not go strictly
476
+ // RFC 1035 --4.1.4: a pointer names a *prior* occurrence. A pointer that does not go strictly
477
477
  // backwards is the only way to build a cycle, so it is refused as one. This is what makes
478
478
  // termination a property of the format rather than of the hop counter.
479
479
  if (target >= position) return refuse(REFUSAL.POINTER_LOOP, { target, position });
@@ -482,7 +482,7 @@ function readName(buffer, start) {
482
482
  continue;
483
483
  }
484
484
 
485
- // `01` and `10` are reserved label kinds (RFC 1035 搂4.1.4 defines only `00` and `11`). A packet
485
+ // `01` and `10` are reserved label kinds (RFC 1035 --4.1.4 defines only `00` and `11`). A packet
486
486
  // using them is not something to guess at.
487
487
  if ((lengthByte & LABEL_KIND_MASK) !== 0)
488
488
  return refuse(REFUSAL.LABEL_TYPE, { octet: lengthByte, at: position });
@@ -535,7 +535,7 @@ function readRdata(buffer, rdataStart, rdataEnd, type) {
535
535
  if (length === 0) return refuse(REFUSAL.RDATA_LENGTH, { type: 'PTR', length, expected: 'a name' });
536
536
  const name = readName(buffer, rdataStart);
537
537
  if (!name.ok) return name;
538
- // RFC 1035 搂4.1.4 permits PTR RDATA to be compressed, and mDNS implementations do it, so a
538
+ // RFC 1035 --4.1.4 permits PTR RDATA to be compressed, and mDNS implementations do it, so a
539
539
  // pointer is followed here (leniently) even though the encoder writes this name uncompressed.
540
540
  return { ok: true, value: { target: name.name } };
541
541
  }
@@ -611,11 +611,11 @@ function readRecord(buffer, offset, section, context) {
611
611
  name: name.name,
612
612
  type,
613
613
  type_name: typeName(type),
614
- // RFC 6762 搂10.2: the cache-flush bit is not part of the class and must be masked off.
614
+ // RFC 6762 --10.2: the cache-flush bit is not part of the class and must be masked off.
615
615
  class: rrclass & 0x7fff,
616
616
  cache_flush: (rrclass & CACHE_FLUSH) !== 0,
617
617
  ttl,
618
- // TTL 0 is a goodbye (RFC 6762 搂10.1), and it is a *value*, not an error: the consumers drop
618
+ // TTL 0 is a goodbye (RFC 6762 --10.1), and it is a *value*, not an error: the consumers drop
619
619
  // the record when they see it.
620
620
  goodbye: ttl === 0,
621
621
  rdata: Buffer.from(buffer.subarray(rdataStart, rdataEnd)),
@@ -666,7 +666,7 @@ export function decodeMessage(buffer) {
666
666
  name: name.name,
667
667
  type: buffer.readUInt16BE(offset),
668
668
  class: qclass & 0x7fff,
669
- // RFC 6762 搂5.4: the top bit of the class asks for a unicast reply ("QU").
669
+ // RFC 6762 --5.4: the top bit of the class asks for a unicast reply ("QU").
670
670
  unicast_response: (qclass & UNICAST_RESPONSE) !== 0,
671
671
  });
672
672
  offset += 4;
@@ -821,9 +821,9 @@ function createWriter() {
821
821
  *
822
822
  * Compression policy, and the reasoning behind it:
823
823
  *
824
- * * **PTR RDATA is compressed.** It is a name, RFC 1035 搂4.1.4 permits it, and every mDNS responder
825
- * on a real LAN does it 鈥?a response that did not would be larger than the peers' own.
826
- * * **SRV RDATA is not compressed.** RFC 1035 搂4.1.4 lists the types whose RDATA may be compressed
824
+ * * **PTR RDATA is compressed.** It is a name, RFC 1035 --4.1.4 permits it, and every mDNS responder
825
+ * on a real LAN does it --a response that did not would be larger than the peers' own.
826
+ * * **SRV RDATA is not compressed.** RFC 1035 --4.1.4 lists the types whose RDATA may be compressed
827
827
  * and SRV (RFC 2782) is not among them, which is the conservative reading every interoperating
828
828
  * implementation follows. The *decoder* still follows a pointer there, because refusing a packet a
829
829
  * peer legitimately built is worse than accepting one written in a style we would not choose.
@@ -863,7 +863,7 @@ function encodeRdata(writer, record) {
863
863
  * Encode a TXT RDATA: a sequence of length-prefixed character-strings, each at most 255 octets.
864
864
  *
865
865
  * An empty list is written as one zero-length string, which is the DNS-SD convention for "this service
866
- * has no attributes but does have a TXT record" (RFC 6763 搂6.1).
866
+ * has no attributes but does have a TXT record" (RFC 6763 --6.1).
867
867
  *
868
868
  * @param {object} record
869
869
  * @returns {Buffer}
@@ -876,7 +876,7 @@ function encodeTxt(record) {
876
876
  if (bytes.length > 255)
877
877
  throw new MdnsWireError(
878
878
  'MDNS_TXT_TOO_LONG',
879
- `TXT string of ${bytes.length} octets; RFC 1035 搂3.3.14 allows 255`,
879
+ `TXT string of ${bytes.length} octets; RFC 1035 --3.3.14 allows 255`,
880
880
  );
881
881
  parts.push(Buffer.from([bytes.length]), bytes);
882
882
  }
@@ -924,7 +924,7 @@ function writeRecord(writer, record, section) {
924
924
  writer.writeUint16(rrclass | (record.cache_flush === true || record.cacheFlush === true ? CACHE_FLUSH : 0));
925
925
  writer.writeUint32(ttl);
926
926
  // The length has to precede the RDATA, but a compressed name inside the RDATA needs the offsets of
927
- // everything before it 鈥?so the RDATA is written into the message and the placeholder patched after.
927
+ // everything before it --so the RDATA is written into the message and the placeholder patched after.
928
928
  const placeholder = writer.reserveUint16();
929
929
  const before = writer.length;
930
930
  const rdata = encodeRdata(writer, { ...record, type });
@@ -984,9 +984,9 @@ export function encodeMessage(message = {}) {
984
984
  /**
985
985
  * Build a service-discovery query.
986
986
  *
987
- * RFC 6762 搂5.1: a one-shot query MUST NOT be sent from source port 5353, because that port signals a
987
+ * RFC 6762 --5.1: a one-shot query MUST NOT be sent from source port 5353, because that port signals a
988
988
  * fully compliant querier. The caller owns the socket and therefore the source port; this function
989
- * only writes the message. RFC 6762 搂18.1: the ID of a multicast query is zero.
989
+ * only writes the message. RFC 6762 --18.1: the ID of a multicast query is zero.
990
990
  *
991
991
  * @param {{serviceType: string, type?: number, unicastResponse?: boolean, id?: number}} options
992
992
  * @returns {Buffer}
@@ -995,7 +995,7 @@ export function encodeQuery(options) {
995
995
  const type = options.type ?? RECORD_TYPE.PTR;
996
996
  return encodeMessage({
997
997
  id: Number.isInteger(options.id) ? options.id : 0,
998
- // 搂18.3/搂18.4/搂18.5/搂18.6: opcode 0, and the RD/RA/Z bits clear on transmission.
998
+ // --18.3/--18.4/--18.5/--18.6: opcode 0, and the RD/RA/Z bits clear on transmission.
999
999
  flags: { qr: false, opcode: 0, aa: false, tc: false, rd: false, ra: false, rcode: 0 },
1000
1000
  questions: [
1001
1001
  {
@@ -1011,7 +1011,7 @@ export function encodeQuery(options) {
1011
1011
  /**
1012
1012
  * Build a response to a question, copying the question section.
1013
1013
  *
1014
- * RFC 6762 搂6.7: a response to a query from a source port other than 5353 ("legacy") MUST repeat the
1014
+ * RFC 6762 --6.7: a response to a query from a source port other than 5353 ("legacy") MUST repeat the
1015
1015
  * query ID and the question. `legacy` therefore carries those two behaviours and the 10-second TTL cap
1016
1016
  * (see {@link capLegacyTtl}).
1017
1017
  *
@@ -1027,7 +1027,7 @@ export function encodeResponse(options) {
1027
1027
  const legacy = options.legacy === true;
1028
1028
  return encodeMessage({
1029
1029
  id: legacy && Number.isInteger(query?.id) ? query.id : 0,
1030
- // 搂18.7: the AA bit is set on responses; there is no recursion and no error code in mDNS.
1030
+ // --18.7: the AA bit is set on responses; there is no recursion and no error code in mDNS.
1031
1031
  flags: { qr: true, opcode: 0, aa: true, tc: false, rd: false, ra: false, rcode: 0 },
1032
1032
  questions:
1033
1033
  legacy && Array.isArray(query?.questions)
@@ -1035,13 +1035,13 @@ export function encodeResponse(options) {
1035
1035
  : [],
1036
1036
  answers: answers.map((record) => withTtl(record, legacy)),
1037
1037
  // The additional section carries the records a peer needs next (SRV and TXT for a PTR answer, the
1038
- // A record for an SRV answer). 搂10.2's cache-flush bit is *not* set for a legacy unicast response.
1038
+ // A record for an SRV answer). --10.2's cache-flush bit is *not* set for a legacy unicast response.
1039
1039
  additionals: additionals.map((record) => withTtl(record, legacy)),
1040
1040
  });
1041
1041
  }
1042
1042
 
1043
1043
  /**
1044
- * Apply RFC 6762 搂6.7's TTL cap and cache-flush rule to one record of a response.
1044
+ * Apply RFC 6762 --6.7's TTL cap and cache-flush rule to one record of a response.
1045
1045
  *
1046
1046
  * A legacy resolver is a plain DNS client with no cache-coherency protocol, so it must not be handed a
1047
1047
  * 75-minute TTL: "the resource record TTL given in a legacy unicast response SHOULD NOT be greater
@@ -19,34 +19,34 @@
19
19
  * # Clean room
20
20
  *
21
21
  * Written from RFC 6762 (Multicast DNS), RFC 6763 (DNS-Based Service Discovery), RFC 2782 (SRV) and
22
- * RFC 1035 §4.1 (message format). See `src/agent/mdns-wire.mjs` for the two studied-implementation
22
+ * RFC 1035 sec.4.1 (message format). See `src/agent/mdns-wire.mjs` for the two studied-implementation
23
23
  * behaviours this layer deliberately does *not* reproduce (first-writer-wins record updates, and a name
24
24
  * treated as identity).
25
25
  *
26
26
  * # What this implements, exactly
27
27
  *
28
28
  * * `buildServiceRecords` — the four records a W2M service instance publishes: a shared PTR under
29
- * `_w2m._tcp.local`, and unique SRV + TXT + A records. TTLs follow RFC 6762 §10: 120 s for records
29
+ * `_w2m._tcp.local`, and unique SRV + TXT + A records. TTLs follow RFC 6762 sec.10: 120 s for records
30
30
  * naming or containing a host name (SRV, A), 75 minutes for the others (PTR, TXT).
31
31
  * * `createResponder` — announces on start, answers matching questions, re-announces on a timer, and
32
- * sends a TTL=0 goodbye on close (RFC 6762 §10.1) so peers drop us immediately instead of waiting
32
+ * sends a TTL=0 goodbye on close (RFC 6762 sec.10.1) so peers drop us immediately instead of waiting
33
33
  * out a 75-minute TTL.
34
- * * `discoverServices` — one bounded scan with a fresh socket per call, which is what RFC 6762 §5.1
35
- * describes ("one-shot" queries from an ephemeral source port; §5.1 is also why the socket is
34
+ * * `discoverServices` — one bounded scan with a fresh socket per call, which is what RFC 6762 sec.5.1
35
+ * describes ("one-shot" queries from an ephemeral source port; sec.5.1 is also why the socket is
36
36
  * never bound to 5353).
37
37
  *
38
38
  * # Scope, stated rather than glossed
39
39
  *
40
40
  * * **IPv4 only.** `224.0.0.251` and the `udp4` socket family. IPv6 (`ff02::fb`) is not half-built
41
41
  * here; it would need its own socket, its own interface enumeration and its own tests.
42
- * * **No probing or conflict resolution** (RFC 6762 §8.1, §9). A responder here announces its
42
+ * * **No probing or conflict resolution** (RFC 6762 sec.8.1, sec.9). A responder here announces its
43
43
  * records immediately and does not defend them. Two machines configured with the same instance
44
44
  * name would both answer. That is a real limitation, and it is acceptable *only* because of the
45
45
  * rule above: a discovered name is a hint, and the pairing that actually authorises anything is
46
46
  * keyed by machine id, not by the name two machines might collide on.
47
- * * **No known-answer suppression** (RFC 6762 §7.1) and no negative responses (NSEC). A scan is one
47
+ * * **No known-answer suppression** (RFC 6762 sec.7.1) and no negative responses (NSEC). A scan is one
48
48
  * query; nothing here is a long-lived browsing client.
49
- * * **The 10-second re-announce is a deliberate deviation.** RFC 6762 §8.3 says a responder "MUST NOT
49
+ * * **The 10-second re-announce is a deliberate deviation.** RFC 6762 sec.8.3 says a responder "MUST NOT
50
50
  * send regular periodic announcements as a matter of course" and asks for at least two
51
51
  * announcements one second apart with the interval doubling. The periodic re-announce exists here
52
52
  * because this is a small, fixed set of records on a tool that expects peers to appear and vanish,
@@ -71,31 +71,31 @@ import {
71
71
  normalizeName,
72
72
  } from './mdns-wire.mjs';
73
73
 
74
- /** The IPv4 link-local multicast address every mDNS name resolves through (RFC 6762 §3). */
74
+ /** The IPv4 link-local multicast address every mDNS name resolves through (RFC 6762 sec.3). */
75
75
  export const MDNS_GROUP = '224.0.0.251';
76
76
 
77
- /** The mDNS UDP port (RFC 6762 §5). A source port of 5353 means "fully compliant querier". */
77
+ /** The mDNS UDP port (RFC 6762 sec.5). A source port of 5353 means "fully compliant querier". */
78
78
  export const MDNS_PORT = 5353;
79
79
 
80
80
  /** The service type W2M instances advertise under. */
81
81
  export const DEFAULT_SERVICE_TYPE = '_w2m._tcp.local';
82
82
 
83
- /** Default window for one scan. RFC 6762 §5.2 expects responses within about a second. */
83
+ /** Default window for one scan. RFC 6762 sec.5.2 expects responses within about a second. */
84
84
  export const DEFAULT_DISCOVERY_TIMEOUT_MS = 1500;
85
85
 
86
- /** Default re-announce period. See the header: this is a deliberate deviation from RFC 6762 §8.3. */
86
+ /** Default re-announce period. See the header: this is a deliberate deviation from RFC 6762 sec.8.3. */
87
87
  export const DEFAULT_ANNOUNCE_INTERVAL_MS = 10_000;
88
88
 
89
- /** RFC 6762 §10: 120 s where a host name is involved, 75 minutes (4500 s) for everything else. */
89
+ /** RFC 6762 sec.10: 120 s where a host name is involved, 75 minutes (4500 s) for everything else. */
90
90
  export const DEFAULT_TTL_SECONDS = Object.freeze({ ptr: 4500, srv: 120, txt: 4500, address: 120 });
91
91
 
92
- /** RFC 6762 §10.1: a goodbye is the same record with an RR TTL of zero. */
92
+ /** RFC 6762 sec.10.1: a goodbye is the same record with an RR TTL of zero. */
93
93
  export const GOODBYE_TTL = 0;
94
94
 
95
95
  /** Upper bound on instances one scan will report, so a flooding peer cannot grow this without limit. */
96
96
  export const MAX_SCAN_INSTANCES = 64;
97
97
 
98
- /** The first TXT string DNS-SD asks for (RFC 6763 §6.1). */
98
+ /** The first TXT string DNS-SD asks for (RFC 6763 sec.6.1). */
99
99
  export const TXT_VERSION = 'txtvers=1';
100
100
 
101
101
  /** TXT keys this layer reads. `mid` is the *only* one that can carry identity, and it is the relay's. */
@@ -195,7 +195,7 @@ export function localHostLabel(hostname = os.hostname()) {
195
195
  /**
196
196
  * The records one W2M service instance publishes.
197
197
  *
198
- * Layout follows DNS-SD (RFC 6763 §4–§6) with W2M's own keys in the TXT record:
198
+ * Layout follows DNS-SD (RFC 6763 sec.4–sec.6) with W2M's own keys in the TXT record:
199
199
  *
200
200
  * `_w2m._tcp.local` PTR → `<instance>._w2m._tcp.local` shared, TTL 4500
201
201
  * `<instance>._w2m._tcp.local` SRV → `<host>.local` port unique, TTL 120
@@ -217,7 +217,7 @@ export function localHostLabel(hostname = os.hostname()) {
217
217
  * @param {string|null} [options.machineId] The relay's machine id, published as `mid=`.
218
218
  * @param {string|null} [options.machineName] Display name, published as `name=`.
219
219
  * @param {object} [options.ttl] Overrides for {@link DEFAULT_TTL_SECONDS}.
220
- * @param {boolean} [options.goodbye] Emit the same records with TTL 0 (RFC 6762 §10.1).
220
+ * @param {boolean} [options.goodbye] Emit the same records with TTL 0 (RFC 6762 sec.10.1).
221
221
  * @returns {object[]}
222
222
  */
223
223
  export function buildServiceRecords(options) {
@@ -250,7 +250,7 @@ export function buildServiceRecords(options) {
250
250
  // Shared: many responders may publish a PTR for the same service type, so no cache-flush bit.
251
251
  { name: serviceType, type: RECORD_TYPE.PTR, ttl: at(ttl.ptr), target: instanceFqdn },
252
252
  // Unique: exactly one owner, so the cache-flush bit tells peers to drop older SRV/TXT/A records
253
- // for these names (RFC 6762 §10.2). This is what stops a peer that moved from keeping the old
253
+ // for these names (RFC 6762 sec.10.2). This is what stops a peer that moved from keeping the old
254
254
  // address: the new record *replaces* rather than joins.
255
255
  {
256
256
  name: instanceFqdn,
@@ -273,7 +273,7 @@ export function buildServiceRecords(options) {
273
273
  cache_flush: !goodbye,
274
274
  });
275
275
  }
276
- // A goodbye is a *deletion*. RFC 6762 §10.1 has the TTL alone carry that meaning, and §10.2's
276
+ // A goodbye is a *deletion*. RFC 6762 sec.10.1 has the TTL alone carry that meaning, and sec.10.2's
277
277
  // cache-flush bit must not be set on it: a cooperating responder's own records for the same name
278
278
  // must not be flushed along with ours.
279
279
  return goodbye ? records.map((record) => ({ ...record, cache_flush: false })) : records;
@@ -290,13 +290,13 @@ export function buildServiceRecords(options) {
290
290
  * studied implementation has: **there is no first-writer-wins and no "keep the first non-zero port".**
291
291
  *
292
292
  * * A record is identified by `(name, type, rdata)`. A later copy of the same record *replaces* the
293
- * earlier one and restarts its TTL — the definition of a cache update (RFC 1035 §3.2.1, RFC 6762
294
- * §10).
293
+ * earlier one and restarts its TTL — the definition of a cache update (RFC 1035 sec.3.2.1, RFC 6762
294
+ * sec.10).
295
295
  * * A record carrying the cache-flush bit replaces the whole RRset of that `(name, type)`: other
296
- * RDATA for the same name and type is dropped. RFC 6762 §10.2 gives other members one second of
296
+ * RDATA for the same name and type is dropped. RFC 6762 sec.10.2 gives other members one second of
297
297
  * grace for the multi-packet case; this cache has no second consumer and no peer reading it, so it
298
298
  * drops them immediately, which is the *safe* direction — a stale address never survives.
299
- * * A record with TTL 0 is a goodbye (RFC 6762 §10.1) and deletes every record with that
299
+ * * A record with TTL 0 is a goodbye (RFC 6762 sec.10.1) and deletes every record with that
300
300
  * `(name, type)` at once. RFC 6762 suggests a long-lived querier record TTL 1 and delete a second
301
301
  * later, to give cooperating responders a chance to rescue the records; for a bounded scan the
302
302
  * honest reading is "the peer said this is gone", and keeping it for a second would mean reporting
@@ -539,7 +539,7 @@ export function parseServiceInstances(input, options = {}) {
539
539
  /**
540
540
  * TXT character-strings → a key/value map.
541
541
  *
542
- * RFC 6763 §6.1: a string with no `=` is a key with an empty value; a duplicate key keeps the first
542
+ * RFC 6763 sec.6.1: a string with no `=` is a key with an empty value; a duplicate key keeps the first
543
543
  * occurrence (the RFC's rule for its own keys).
544
544
  *
545
545
  * @param {string[]} strings
@@ -764,7 +764,7 @@ export function createResponder(options = {}) {
764
764
  }
765
765
 
766
766
  /**
767
- * Multicast our records (RFC 6762 §8.3), or the TTL=0 goodbye when `goodbye` is set (§10.1).
767
+ * Multicast our records (RFC 6762 sec.8.3), or the TTL=0 goodbye when `goodbye` is set (sec.10.1).
768
768
  *
769
769
  * @param {{goodbye?: boolean}} [announceOptions]
770
770
  * @returns {Promise<object>}
@@ -903,7 +903,7 @@ export function createResponder(options = {}) {
903
903
  const message = decoded.message;
904
904
  outcome.handled = true;
905
905
 
906
- // RFC 6762 §18.3: an opcode other than zero is not a standard query and must be ignored.
906
+ // RFC 6762 sec.18.3: an opcode other than zero is not a standard query and must be ignored.
907
907
  if (message.flags.opcode !== 0) {
908
908
  ignoredOpcode += 1;
909
909
  outcome.reason = 'opcode';
@@ -924,8 +924,8 @@ export function createResponder(options = {}) {
924
924
  return outcome;
925
925
  }
926
926
 
927
- // RFC 6762 §6.7: a query whose source port is not 5353 comes from a one-shot resolver, and MUST be
928
- // answered directly, with the query id and question repeated and short TTLs. §5.4: a question with
927
+ // RFC 6762 sec.6.7: a query whose source port is not 5353 comes from a one-shot resolver, and MUST be
928
+ // answered directly, with the query id and question repeated and short TTLs. sec.5.4: a question with
929
929
  // the unicast-response bit asks for the same treatment.
930
930
  const legacy = rinfo.port !== mdnsPort;
931
931
  const unicast = legacy || match.unicast_requested;
@@ -1159,7 +1159,7 @@ function waitFor(ms, signal) {
1159
1159
  /**
1160
1160
  * One bounded mDNS scan for W2M service instances.
1161
1161
  *
1162
- * A fresh socket per call, bound to an ephemeral port: RFC 6762 §5.1 requires a one-shot query *not*
1162
+ * A fresh socket per call, bound to an ephemeral port: RFC 6762 sec.5.1 requires a one-shot query *not*
1163
1163
  * to come from port 5353, and a fresh socket also means a scan cannot be poisoned by the previous
1164
1164
  * scan's socket state. The socket is closed on every path, including a throw from a caller's abort
1165
1165
  * signal, and the whole function resolves — it never rejects.
@@ -1177,7 +1177,7 @@ function waitFor(ms, signal) {
1177
1177
  * the multicast group:5353. A test, or a network where multicast is filtered, can name a unicast
1178
1178
  * target instead — the query is identically shaped either way.
1179
1179
  * @param {string} [options.bindAddress]
1180
- * @param {boolean} [options.unicastResponse] Set the QU bit (RFC 6762 §5.4).
1180
+ * @param {boolean} [options.unicastResponse] Set the QU bit (RFC 6762 sec.5.4).
1181
1181
  * @param {AbortSignal} [options.signal]
1182
1182
  * @param {() => number} [options.now]
1183
1183
  * @returns {Promise<object>}
@@ -1223,7 +1223,7 @@ export async function discoverServices(options = {}) {
1223
1223
  bound.on('error', (error) => {
1224
1224
  socketError = `${error?.code ?? 'ERROR'}: ${error?.message ?? error}`;
1225
1225
  });
1226
- // Responses to a one-shot query are unicast back to this socket (§5.1/§6.7), but a responder on
1226
+ // Responses to a one-shot query are unicast back to this socket (sec.5.1/sec.6.7), but a responder on
1227
1227
  // the same link may also multicast its answer, which arrives here too because the socket joined
1228
1228
  // nothing and is simply addressed. Both are handled the same way, and both are hints.
1229
1229
  bound.on('message', (bytes, rinfo) => {