@basmilius/apple-common 0.8.2 → 0.9.2

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/dist/index.mjs CHANGED
@@ -1,14 +1,20 @@
1
+ import { createRequire } from "node:module";
1
2
  import { randomBytes, randomFillSync, randomUUID } from "node:crypto";
2
- import mdns from "node-dns-sd";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { Socket, createConnection } from "node:net";
3
6
  import { createInterface } from "node:readline";
7
+ import { createSocket } from "node:dgram";
8
+ import { networkInterfaces } from "node:os";
4
9
  import { EventEmitter } from "node:events";
5
- import { Socket } from "node:net";
6
10
  import { NTP, OPack, TLV8 } from "@basmilius/apple-encoding";
7
11
  import { Chacha20, Curve25519, Ed25519, hkdf } from "@basmilius/apple-encryption";
8
12
  import { SRP, SrpClient } from "fast-srp-hap";
9
- import { createSocket } from "node:dgram";
10
- import { networkInterfaces } from "node:os";
11
13
 
14
+ //#region \0rolldown/runtime.js
15
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
16
+
17
+ //#endregion
12
18
  //#region ../../node_modules/.bun/uuid@13.0.0/node_modules/uuid/dist-node/stringify.js
13
19
  const byteToHex = [];
14
20
  for (let i = 0; i < 256; ++i) byteToHex.push((i + 256).toString(16).slice(1));
@@ -53,6 +59,178 @@ function v4(options, buf, offset) {
53
59
  return _v4(options, buf, offset);
54
60
  }
55
61
 
62
+ //#endregion
63
+ //#region src/airplayFeatures.ts
64
+ const AirPlayFeatureFlags = {
65
+ SupportsAirPlayVideoV1: 1n << 0n,
66
+ SupportsAirPlayPhoto: 1n << 1n,
67
+ SupportsAirPlayVideoFairPlay: 1n << 2n,
68
+ SupportsAirPlayVideoVolumeControl: 1n << 3n,
69
+ SupportsAirPlayVideoHTTPLiveStreams: 1n << 4n,
70
+ SupportsAirPlaySlideShow: 1n << 5n,
71
+ SupportsAirPlayScreen: 1n << 7n,
72
+ SupportsAirPlayAudio: 1n << 9n,
73
+ AudioRedundant: 1n << 11n,
74
+ Authentication_4: 1n << 14n,
75
+ MetadataFeatures_0: 1n << 15n,
76
+ MetadataFeatures_1: 1n << 16n,
77
+ MetadataFeatures_2: 1n << 17n,
78
+ AudioFormats_0: 1n << 18n,
79
+ AudioFormats_1: 1n << 19n,
80
+ AudioFormats_2: 1n << 20n,
81
+ AudioFormats_3: 1n << 21n,
82
+ Authentication_1: 1n << 23n,
83
+ Authentication_8: 1n << 26n,
84
+ SupportsLegacyPairing: 1n << 27n,
85
+ HasUnifiedAdvertiserInfo: 1n << 30n,
86
+ IsCarPlay: 1n << 32n,
87
+ SupportsAirPlayVideoPlayQueue: 1n << 33n,
88
+ SupportsAirPlayFromCloud: 1n << 34n,
89
+ SupportsTLS_PSK: 1n << 35n,
90
+ SupportsUnifiedMediaControl: 1n << 38n,
91
+ SupportsBufferedAudio: 1n << 40n,
92
+ SupportsPTP: 1n << 41n,
93
+ SupportsScreenMultiCodec: 1n << 42n,
94
+ SupportsSystemPairing: 1n << 43n,
95
+ IsAPValeriaScreenSender: 1n << 44n,
96
+ SupportsHKPairingAndAccessControl: 1n << 46n,
97
+ SupportsCoreUtilsPairingAndEncryption: 1n << 48n,
98
+ SupportsAirPlayVideoV2: 1n << 49n,
99
+ MetadataFeatures_3: 1n << 50n,
100
+ SupportsUnifiedPairSetupAndMFi: 1n << 51n,
101
+ SupportsSetPeersExtendedMessage: 1n << 52n,
102
+ SupportsAPSync: 1n << 54n,
103
+ SupportsWoL: 1n << 55n,
104
+ SupportsWoL2: 1n << 56n,
105
+ SupportsHangdogRemoteControl: 1n << 58n,
106
+ SupportsAudioStreamConnectionSetup: 1n << 59n,
107
+ SupportsAudioMetadataControl: 1n << 60n,
108
+ SupportsRFC2198Redundancy: 1n << 61n
109
+ };
110
+ const PASSWORD_BIT = 128n;
111
+ const LEGACY_PAIRING_BIT = 512n;
112
+ const PIN_REQUIRED_BIT = 8n;
113
+ const parseFeatures = (features) => {
114
+ const parts = features.split(",").map((part) => part.trim());
115
+ if (parts.length === 1) return BigInt(parts[0]);
116
+ if (parts.length === 2) {
117
+ const low = BigInt(parts[0]);
118
+ return BigInt(parts[1]) << 32n | low;
119
+ }
120
+ throw new Error(`Invalid features format: ${features}`);
121
+ };
122
+ const hasFeatureFlag = (features, flag) => (features & flag) !== 0n;
123
+ const describeFlags = (features) => {
124
+ const result = [];
125
+ for (const [name, flag] of Object.entries(AirPlayFeatureFlags)) if (hasFeatureFlag(features, flag)) result.push(name);
126
+ return result;
127
+ };
128
+ const getProtocolVersion = (txt) => {
129
+ const featuresStr = txt.features ?? txt.ft;
130
+ if (!featuresStr) return 1;
131
+ const features = parseFeatures(featuresStr);
132
+ if (hasFeatureFlag(features, AirPlayFeatureFlags.SupportsUnifiedMediaControl)) return 2;
133
+ if (hasFeatureFlag(features, AirPlayFeatureFlags.SupportsCoreUtilsPairingAndEncryption)) return 2;
134
+ return 1;
135
+ };
136
+ const getPairingRequirement = (txt) => {
137
+ const featuresStr = txt.features ?? txt.ft;
138
+ if (!featuresStr) return "none";
139
+ const features = parseFeatures(featuresStr);
140
+ const sf = txt.sf ? BigInt(txt.sf) : 0n;
141
+ if (hasFeatureFlag(features, AirPlayFeatureFlags.SupportsHKPairingAndAccessControl)) return "homekit";
142
+ if ((sf & PIN_REQUIRED_BIT) !== 0n) return "pin";
143
+ if (hasFeatureFlag(features, AirPlayFeatureFlags.SupportsSystemPairing)) return "transient";
144
+ if ((sf & LEGACY_PAIRING_BIT) !== 0n) return "pin";
145
+ return "none";
146
+ };
147
+ const isPasswordRequired = (txt) => {
148
+ if (txt.pw === "true") return true;
149
+ return ((txt.sf ? BigInt(txt.sf) : 0n) & PASSWORD_BIT) !== 0n;
150
+ };
151
+ const isRemoteControlSupported = (txt) => {
152
+ const featuresStr = txt.features ?? txt.ft;
153
+ if (!featuresStr) return false;
154
+ return hasFeatureFlag(parseFeatures(featuresStr), AirPlayFeatureFlags.SupportsHangdogRemoteControl);
155
+ };
156
+
157
+ //#endregion
158
+ //#region src/storage.ts
159
+ const credentialKey = (deviceId, protocol) => `${deviceId}:${protocol}`;
160
+ const serializeCredentials = (credentials) => ({
161
+ accessoryIdentifier: credentials.accessoryIdentifier,
162
+ accessoryLongTermPublicKey: credentials.accessoryLongTermPublicKey.toString("base64"),
163
+ pairingId: credentials.pairingId.toString("base64"),
164
+ publicKey: credentials.publicKey.toString("base64"),
165
+ secretKey: credentials.secretKey.toString("base64")
166
+ });
167
+ const deserializeCredentials = (stored) => ({
168
+ accessoryIdentifier: stored.accessoryIdentifier,
169
+ accessoryLongTermPublicKey: Buffer.from(stored.accessoryLongTermPublicKey, "base64"),
170
+ pairingId: Buffer.from(stored.pairingId, "base64"),
171
+ publicKey: Buffer.from(stored.publicKey, "base64"),
172
+ secretKey: Buffer.from(stored.secretKey, "base64")
173
+ });
174
+ const createEmptyData = () => ({
175
+ version: 1,
176
+ devices: {},
177
+ credentials: {}
178
+ });
179
+ var Storage = class {
180
+ #data = createEmptyData();
181
+ get data() {
182
+ return this.#data;
183
+ }
184
+ setData(data) {
185
+ this.#data = data;
186
+ }
187
+ getDevice(identifier) {
188
+ return this.#data.devices[identifier];
189
+ }
190
+ setDevice(identifier, device) {
191
+ this.#data.devices[identifier] = device;
192
+ }
193
+ removeDevice(identifier) {
194
+ delete this.#data.devices[identifier];
195
+ for (const key of Object.keys(this.#data.credentials)) if (key.startsWith(`${identifier}:`)) delete this.#data.credentials[key];
196
+ }
197
+ listDevices() {
198
+ return Object.values(this.#data.devices);
199
+ }
200
+ getCredentials(deviceId, protocol) {
201
+ const stored = this.#data.credentials[credentialKey(deviceId, protocol)];
202
+ if (!stored) return;
203
+ return deserializeCredentials(stored);
204
+ }
205
+ setCredentials(deviceId, protocol, credentials) {
206
+ this.#data.credentials[credentialKey(deviceId, protocol)] = serializeCredentials(credentials);
207
+ }
208
+ removeCredentials(deviceId, protocol) {
209
+ delete this.#data.credentials[credentialKey(deviceId, protocol)];
210
+ }
211
+ };
212
+ var JsonStorage = class extends Storage {
213
+ #path;
214
+ constructor(path) {
215
+ super();
216
+ this.#path = path ?? join(process.env.HOME ?? process.env.USERPROFILE ?? ".", ".config", "apple-protocols", "storage.json");
217
+ }
218
+ async load() {
219
+ if (!existsSync(this.#path)) return;
220
+ const raw = readFileSync(this.#path, "utf-8");
221
+ const json = JSON.parse(raw);
222
+ if (json.version === 1) this.setData(json);
223
+ }
224
+ async save() {
225
+ mkdirSync(dirname(this.#path), { recursive: true });
226
+ writeFileSync(this.#path, JSON.stringify(this.data, null, 2), "utf-8");
227
+ }
228
+ };
229
+ var MemoryStorage = class extends Storage {
230
+ async load() {}
231
+ async save() {}
232
+ };
233
+
56
234
  //#endregion
57
235
  //#region src/cli.ts
58
236
  async function prompt(message) {
@@ -77,23 +255,452 @@ const AIRPLAY_SERVICE = "_airplay._tcp.local";
77
255
  const COMPANION_LINK_SERVICE = "_companion-link._tcp.local";
78
256
  const RAOP_SERVICE = "_raop._tcp.local";
79
257
 
258
+ //#endregion
259
+ //#region src/mdns.ts
260
+ const MDNS_ADDRESS = "224.0.0.251";
261
+ const MDNS_PORT = 5353;
262
+ const QUERY_ID = 13823;
263
+ const SERVICES_PER_MSG = 3;
264
+ const QueryType = {
265
+ A: 1,
266
+ PTR: 12,
267
+ TXT: 16,
268
+ AAAA: 28,
269
+ SRV: 33,
270
+ ANY: 255
271
+ };
272
+ const encodeQName = (name) => {
273
+ const parts = [];
274
+ const labels = splitServiceName(name);
275
+ for (const label of labels) {
276
+ const encoded = Buffer.from(label, "utf-8");
277
+ if (encoded.byteLength > 63) parts.push(Buffer.from([63]), encoded.subarray(0, 63));
278
+ else parts.push(Buffer.from([encoded.byteLength]), encoded);
279
+ }
280
+ parts.push(Buffer.from([0]));
281
+ return Buffer.concat(parts);
282
+ };
283
+ const splitServiceName = (name) => {
284
+ const match = name.match(/\._[a-z]+\._(?:tcp|udp)\.local$/);
285
+ if (match) return [name.substring(0, match.index), ...match[0].substring(1).split(".")];
286
+ return name.split(".");
287
+ };
288
+ const encodeDnsHeader = (header) => {
289
+ const buf = Buffer.allocUnsafe(12);
290
+ buf.writeUInt16BE(header.id, 0);
291
+ buf.writeUInt16BE(header.flags, 2);
292
+ buf.writeUInt16BE(header.qdcount, 4);
293
+ buf.writeUInt16BE(header.ancount, 6);
294
+ buf.writeUInt16BE(header.nscount, 8);
295
+ buf.writeUInt16BE(header.arcount, 10);
296
+ return buf;
297
+ };
298
+ const encodeDnsQuestion = (name, qtype, unicastResponse = false) => {
299
+ const qname = encodeQName(name);
300
+ const suffix = Buffer.allocUnsafe(4);
301
+ suffix.writeUInt16BE(qtype, 0);
302
+ suffix.writeUInt16BE(unicastResponse ? 32769 : 1, 2);
303
+ return Buffer.concat([qname, suffix]);
304
+ };
305
+ const createQueryPackets = (services, qtype = QueryType.PTR, unicastResponse = false) => {
306
+ const packets = [];
307
+ for (let i = 0; i < services.length; i += SERVICES_PER_MSG) {
308
+ const chunk = services.slice(i, i + SERVICES_PER_MSG);
309
+ const questions = chunk.map((s) => encodeDnsQuestion(s, qtype, unicastResponse));
310
+ const header = encodeDnsHeader({
311
+ id: QUERY_ID,
312
+ flags: 0,
313
+ qdcount: chunk.length,
314
+ ancount: 0,
315
+ nscount: 0,
316
+ arcount: 0
317
+ });
318
+ packets.push(Buffer.concat([header, ...questions]));
319
+ }
320
+ return packets;
321
+ };
322
+ const decodeQName = (buf, offset) => {
323
+ const labels = [];
324
+ let currentOffset = offset;
325
+ let jumped = false;
326
+ let returnOffset = offset;
327
+ while (currentOffset < buf.byteLength) {
328
+ const length = buf[currentOffset];
329
+ if (length === 0) {
330
+ if (!jumped) returnOffset = currentOffset + 1;
331
+ break;
332
+ }
333
+ if ((length & 192) === 192) {
334
+ const pointer = (length & 63) << 8 | buf[currentOffset + 1];
335
+ if (!jumped) returnOffset = currentOffset + 2;
336
+ currentOffset = pointer;
337
+ jumped = true;
338
+ continue;
339
+ }
340
+ currentOffset++;
341
+ labels.push(buf.toString("utf-8", currentOffset, currentOffset + length));
342
+ currentOffset += length;
343
+ if (!jumped) returnOffset = currentOffset;
344
+ }
345
+ return [labels.join("."), returnOffset];
346
+ };
347
+ const decodeDnsHeader = (buf) => ({
348
+ id: buf.readUInt16BE(0),
349
+ flags: buf.readUInt16BE(2),
350
+ qdcount: buf.readUInt16BE(4),
351
+ ancount: buf.readUInt16BE(6),
352
+ nscount: buf.readUInt16BE(8),
353
+ arcount: buf.readUInt16BE(10)
354
+ });
355
+ const decodeQuestion = (buf, offset) => {
356
+ const [qname, newOffset] = decodeQName(buf, offset);
357
+ return [{
358
+ qname,
359
+ qtype: buf.readUInt16BE(newOffset),
360
+ qclass: buf.readUInt16BE(newOffset + 2)
361
+ }, newOffset + 4];
362
+ };
363
+ const decodeTxtRecord = (buf, offset, length) => {
364
+ const properties = {};
365
+ let pos = offset;
366
+ const end = offset + length;
367
+ while (pos < end) {
368
+ const strLen = buf[pos];
369
+ pos++;
370
+ if (strLen === 0 || pos + strLen > end) break;
371
+ const str = buf.toString("utf-8", pos, pos + strLen);
372
+ pos += strLen;
373
+ const eqIndex = str.indexOf("=");
374
+ if (eqIndex >= 0) properties[str.substring(0, eqIndex)] = str.substring(eqIndex + 1);
375
+ else properties[str] = "";
376
+ }
377
+ return properties;
378
+ };
379
+ const decodeSrvRecord = (buf, offset) => {
380
+ const priority = buf.readUInt16BE(offset);
381
+ const weight = buf.readUInt16BE(offset + 2);
382
+ const port = buf.readUInt16BE(offset + 4);
383
+ const [target] = decodeQName(buf, offset + 6);
384
+ return {
385
+ priority,
386
+ weight,
387
+ port,
388
+ target
389
+ };
390
+ };
391
+ const decodeResource = (buf, offset) => {
392
+ const [qname, nameEnd] = decodeQName(buf, offset);
393
+ const qtype = buf.readUInt16BE(nameEnd);
394
+ const qclass = buf.readUInt16BE(nameEnd + 2);
395
+ const ttl = buf.readUInt32BE(nameEnd + 4);
396
+ const rdLength = buf.readUInt16BE(nameEnd + 8);
397
+ const rdOffset = nameEnd + 10;
398
+ let rdata;
399
+ switch (qtype) {
400
+ case QueryType.A:
401
+ rdata = `${buf[rdOffset]}.${buf[rdOffset + 1]}.${buf[rdOffset + 2]}.${buf[rdOffset + 3]}`;
402
+ break;
403
+ case QueryType.AAAA: {
404
+ const parts = [];
405
+ for (let i = 0; i < 8; i++) parts.push(buf.readUInt16BE(rdOffset + i * 2).toString(16));
406
+ rdata = parts.join(":");
407
+ break;
408
+ }
409
+ case QueryType.PTR: {
410
+ const [name] = decodeQName(buf, rdOffset);
411
+ rdata = name;
412
+ break;
413
+ }
414
+ case QueryType.SRV:
415
+ rdata = decodeSrvRecord(buf, rdOffset);
416
+ break;
417
+ case QueryType.TXT:
418
+ rdata = decodeTxtRecord(buf, rdOffset, rdLength);
419
+ break;
420
+ default: rdata = buf.subarray(rdOffset, rdOffset + rdLength);
421
+ }
422
+ return [{
423
+ qname,
424
+ qtype,
425
+ qclass,
426
+ ttl,
427
+ rdata
428
+ }, rdOffset + rdLength];
429
+ };
430
+ const decodeDnsResponse = (buf) => {
431
+ const header = decodeDnsHeader(buf);
432
+ let offset = 12;
433
+ for (let i = 0; i < header.qdcount; i++) {
434
+ const [, newOffset] = decodeQuestion(buf, offset);
435
+ offset = newOffset;
436
+ }
437
+ const answers = [];
438
+ for (let i = 0; i < header.ancount; i++) {
439
+ const [record, newOffset] = decodeResource(buf, offset);
440
+ answers.push(record);
441
+ offset = newOffset;
442
+ }
443
+ for (let i = 0; i < header.nscount; i++) {
444
+ const [, newOffset] = decodeResource(buf, offset);
445
+ offset = newOffset;
446
+ }
447
+ const resources = [];
448
+ for (let i = 0; i < header.arcount; i++) {
449
+ const [record, newOffset] = decodeResource(buf, offset);
450
+ resources.push(record);
451
+ offset = newOffset;
452
+ }
453
+ return {
454
+ header,
455
+ answers,
456
+ resources
457
+ };
458
+ };
459
+ var ServiceCollector = class {
460
+ #ptrMap = /* @__PURE__ */ new Map();
461
+ #srvMap = /* @__PURE__ */ new Map();
462
+ #txtMap = /* @__PURE__ */ new Map();
463
+ #addressMap = /* @__PURE__ */ new Map();
464
+ addRecords(answers, resources) {
465
+ for (const record of [...answers, ...resources]) switch (record.qtype) {
466
+ case QueryType.PTR: {
467
+ const existing = this.#ptrMap.get(record.qname);
468
+ if (existing) existing.add(record.rdata);
469
+ else this.#ptrMap.set(record.qname, new Set([record.rdata]));
470
+ break;
471
+ }
472
+ case QueryType.SRV:
473
+ this.#srvMap.set(record.qname, record.rdata);
474
+ break;
475
+ case QueryType.TXT:
476
+ this.#txtMap.set(record.qname, record.rdata);
477
+ break;
478
+ case QueryType.A:
479
+ this.#addressMap.set(record.qname, record.rdata);
480
+ break;
481
+ }
482
+ }
483
+ get services() {
484
+ const results = [];
485
+ for (const [serviceType, instanceNames] of this.#ptrMap) for (const instanceQName of instanceNames) {
486
+ const srv = this.#srvMap.get(instanceQName);
487
+ if (!srv || srv.port === 0) continue;
488
+ const address = this.#addressMap.get(srv.target);
489
+ if (!address) continue;
490
+ const txt = this.#txtMap.get(instanceQName) ?? {};
491
+ const typeIndex = instanceQName.indexOf("._");
492
+ const name = typeIndex >= 0 ? instanceQName.substring(0, typeIndex) : instanceQName;
493
+ if (!results.some((s) => s.name === name && s.type === serviceType)) results.push({
494
+ name,
495
+ type: serviceType,
496
+ address,
497
+ port: srv.port,
498
+ properties: txt
499
+ });
500
+ }
501
+ return results;
502
+ }
503
+ };
504
+ const WAKE_PORTS$1 = [
505
+ 7e3,
506
+ 3689,
507
+ 49152,
508
+ 32498
509
+ ];
510
+ const knock = (address) => {
511
+ const promises = WAKE_PORTS$1.map((port) => new Promise((resolve) => {
512
+ const socket = createConnection({
513
+ host: address,
514
+ port,
515
+ timeout: 500
516
+ });
517
+ socket.on("connect", () => {
518
+ socket.destroy();
519
+ resolve();
520
+ });
521
+ socket.on("error", () => {
522
+ socket.destroy();
523
+ resolve();
524
+ });
525
+ socket.on("timeout", () => {
526
+ socket.destroy();
527
+ resolve();
528
+ });
529
+ }));
530
+ return Promise.all(promises).then(() => {});
531
+ };
532
+ const unicast = (hosts, services, timeout = 4) => {
533
+ return new Promise((resolve) => {
534
+ const queries = createQueryPackets(services);
535
+ const collector = new ServiceCollector();
536
+ const sockets = [];
537
+ let resolved = false;
538
+ const finish = () => {
539
+ if (resolved) return;
540
+ resolved = true;
541
+ clearInterval(interval);
542
+ for (const socket of sockets) try {
543
+ socket.close();
544
+ } catch {}
545
+ resolve(collector.services);
546
+ };
547
+ for (const host of hosts) {
548
+ const socket = createSocket("udp4");
549
+ sockets.push(socket);
550
+ socket.on("message", (data) => {
551
+ try {
552
+ const response = decodeDnsResponse(data);
553
+ collector.addRecords(response.answers, response.resources);
554
+ } catch {}
555
+ });
556
+ socket.on("error", () => {});
557
+ }
558
+ let interval;
559
+ Promise.all(hosts.map((h) => knock(h))).then(() => {
560
+ const sendQueries = () => {
561
+ for (let i = 0; i < hosts.length; i++) for (const query of queries) sockets[i]?.send(query, MDNS_PORT, hosts[i]);
562
+ };
563
+ sendQueries();
564
+ interval = setInterval(sendQueries, 1e3);
565
+ setTimeout(finish, timeout * 1e3);
566
+ });
567
+ });
568
+ };
569
+ const multicast = (services, timeout = 4) => {
570
+ return new Promise((resolve) => {
571
+ const collector = new ServiceCollector();
572
+ const queries = createQueryPackets(services);
573
+ const sockets = [];
574
+ let resolved = false;
575
+ let interval;
576
+ const finish = () => {
577
+ if (resolved) return;
578
+ resolved = true;
579
+ clearInterval(interval);
580
+ for (const socket of sockets) try {
581
+ socket.close();
582
+ } catch {}
583
+ resolve(collector.services);
584
+ };
585
+ const onMessage = (data) => {
586
+ try {
587
+ const response = decodeDnsResponse(data);
588
+ collector.addRecords(response.answers, response.resources);
589
+ } catch {}
590
+ };
591
+ const addSocket = (address, port) => {
592
+ return new Promise((resolveSocket) => {
593
+ const socket = createSocket({
594
+ type: "udp4",
595
+ reuseAddr: true
596
+ });
597
+ socket.on("message", onMessage);
598
+ socket.on("error", () => {
599
+ resolveSocket(null);
600
+ });
601
+ socket.bind(port, address ?? "", () => {
602
+ if (address) try {
603
+ socket.setMulticastInterface(address);
604
+ socket.addMembership(MDNS_ADDRESS, address);
605
+ } catch {}
606
+ else try {
607
+ socket.addMembership(MDNS_ADDRESS);
608
+ } catch {}
609
+ sockets.push(socket);
610
+ resolveSocket(socket);
611
+ });
612
+ });
613
+ };
614
+ const getPrivateAddresses = () => {
615
+ try {
616
+ const { networkInterfaces } = __require("node:os");
617
+ const interfaces = networkInterfaces();
618
+ const addresses = [];
619
+ for (const nets of Object.values(interfaces)) for (const net of nets) if (net.family === "IPv4" && net.internal === false) addresses.push(net.address);
620
+ return addresses;
621
+ } catch {
622
+ return [];
623
+ }
624
+ };
625
+ const setup = async () => {
626
+ await addSocket(null, MDNS_PORT);
627
+ for (const address of getPrivateAddresses()) await addSocket(address, 0);
628
+ if (sockets.length === 0) {
629
+ resolve([]);
630
+ return;
631
+ }
632
+ const sendQueries = () => {
633
+ for (const socket of sockets) for (const query of queries) try {
634
+ socket.send(query, MDNS_PORT, MDNS_ADDRESS);
635
+ } catch {}
636
+ };
637
+ sendQueries();
638
+ interval = setInterval(sendQueries, 1e3);
639
+ setTimeout(finish, timeout * 1e3);
640
+ };
641
+ setup();
642
+ });
643
+ };
644
+
80
645
  //#endregion
81
646
  //#region src/discovery.ts
647
+ const CACHE_TTL = 3e4;
648
+ const WAKE_PORTS = [
649
+ 7e3,
650
+ 3689,
651
+ 49152,
652
+ 32498
653
+ ];
654
+ const toDiscoveryResult = (service) => {
655
+ const txt = service.properties;
656
+ const featuresStr = txt.features ?? txt.ft;
657
+ const model = txt.model ?? txt.am ?? "";
658
+ const protocol = service.type.includes("._tcp") ? "tcp" : "udp";
659
+ const hostname = service.name.replace(/\s+/g, "-");
660
+ return {
661
+ id: `${hostname}.local`,
662
+ fqdn: `${hostname}.local`,
663
+ address: service.address,
664
+ modelName: model,
665
+ familyName: null,
666
+ txt,
667
+ features: featuresStr ? tryParseFeatures(featuresStr) : void 0,
668
+ service: {
669
+ port: service.port,
670
+ protocol,
671
+ type: service.type
672
+ },
673
+ packet: null
674
+ };
675
+ };
676
+ const tryParseFeatures = (features) => {
677
+ try {
678
+ return parseFeatures(features);
679
+ } catch {
680
+ return;
681
+ }
682
+ };
82
683
  var Discovery = class Discovery {
684
+ static #cache = /* @__PURE__ */ new Map();
83
685
  #service;
84
686
  constructor(service) {
85
687
  this.#service = service;
86
688
  }
87
- async find() {
88
- return (await mdns.discover({ name: this.#service })).map((result) => ({
89
- id: generateId(result) ?? result.fqdn,
90
- txt: getTxt(result),
91
- ...result
92
- }));
689
+ async find(useCache = true) {
690
+ if (useCache) {
691
+ const cached = Discovery.#cache.get(this.#service);
692
+ if (cached && cached.expiresAt > Date.now()) return cached.results;
693
+ }
694
+ const mapped = (await multicast([this.#service], 4)).map(toDiscoveryResult);
695
+ Discovery.#cache.set(this.#service, {
696
+ results: mapped,
697
+ expiresAt: Date.now() + CACHE_TTL
698
+ });
699
+ return mapped;
93
700
  }
94
701
  async findUntil(id, tries = 10, timeout = 1e3) {
95
702
  while (tries > 0) {
96
- const devices = await this.find();
703
+ const devices = await this.find(false);
97
704
  const device = devices.find((device) => device.id === id);
98
705
  if (device) return device;
99
706
  console.log();
@@ -102,7 +709,57 @@ var Discovery = class Discovery {
102
709
  tries--;
103
710
  await waitFor(timeout);
104
711
  }
105
- throw new Error("Device not found after serveral tries, aborting.");
712
+ throw new Error("Device not found after several tries, aborting.");
713
+ }
714
+ static clearCache() {
715
+ Discovery.#cache.clear();
716
+ }
717
+ static async wake(address) {
718
+ const promises = WAKE_PORTS.map((port) => new Promise((resolve) => {
719
+ const socket = createConnection({
720
+ host: address,
721
+ port,
722
+ timeout: 500
723
+ });
724
+ socket.on("connect", () => {
725
+ socket.destroy();
726
+ resolve();
727
+ });
728
+ socket.on("error", () => {
729
+ socket.destroy();
730
+ resolve();
731
+ });
732
+ socket.on("timeout", () => {
733
+ socket.destroy();
734
+ resolve();
735
+ });
736
+ }));
737
+ await Promise.all(promises);
738
+ }
739
+ static async discoverAll() {
740
+ const allServices = await multicast([
741
+ AIRPLAY_SERVICE,
742
+ COMPANION_LINK_SERVICE,
743
+ RAOP_SERVICE
744
+ ], 4);
745
+ const devices = /* @__PURE__ */ new Map();
746
+ for (const service of allServices) {
747
+ const result = toDiscoveryResult(service);
748
+ const existing = devices.get(result.id);
749
+ if (existing) {
750
+ if (service.type === AIRPLAY_SERVICE) existing.airplay = result;
751
+ else if (service.type === COMPANION_LINK_SERVICE) existing.companionLink = result;
752
+ else if (service.type === RAOP_SERVICE) existing.raop = result;
753
+ } else devices.set(result.id, {
754
+ id: result.id,
755
+ name: result.fqdn,
756
+ address: result.address,
757
+ airplay: service.type === AIRPLAY_SERVICE ? result : void 0,
758
+ companionLink: service.type === COMPANION_LINK_SERVICE ? result : void 0,
759
+ raop: service.type === RAOP_SERVICE ? result : void 0
760
+ });
761
+ }
762
+ return [...devices.values()];
106
763
  }
107
764
  static airplay() {
108
765
  return new Discovery(AIRPLAY_SERVICE);
@@ -114,29 +771,6 @@ var Discovery = class Discovery {
114
771
  return new Discovery(RAOP_SERVICE);
115
772
  }
116
773
  };
117
- function generateId(result) {
118
- if (!result?.packet) return null;
119
- const { answers = [], additionals = [] } = result.packet;
120
- const allRecords = [...answers, ...additionals];
121
- const srvRecord = allRecords.find((record) => record.type === "SRV");
122
- if (srvRecord?.rdata?.target) return srvRecord.rdata.target;
123
- if (result.address) {
124
- const addressRecord = allRecords.find((record) => (record.type === "A" || record.type === "AAAA") && record.rdata === result.address);
125
- if (addressRecord?.name) return addressRecord.name;
126
- }
127
- const aRecord = allRecords.find((record) => record.type === "A");
128
- if (aRecord?.name) return aRecord.name;
129
- if (result.modelName) return `${result.modelName.replace(/\s+/g, "-").replace(/[^a-zA-Z0-9-]/g, "")}.local`;
130
- return null;
131
- }
132
- function getTxt(result) {
133
- if (!result.packet) return {};
134
- const { answers = [], additionals = [] } = result.packet;
135
- const records = [...answers, ...additionals];
136
- const txt = {};
137
- for (const record of records) if (record.type === "TXT" && record.rdata) Object.assign(txt, record.rdata);
138
- return txt;
139
- }
140
774
 
141
775
  //#endregion
142
776
  //#region src/symbols.ts
@@ -841,5 +1475,5 @@ function splitUInt53(number) {
841
1475
  }
842
1476
 
843
1477
  //#endregion
844
- export { AIRPLAY_SERVICE, AIRPLAY_TRANSIENT_PIN, AccessoryPair, AccessoryVerify, COMPANION_LINK_SERVICE, Connection, Context, Discovery, ENCRYPTION, EncryptionAwareConnection, EncryptionState, HTTP_TIMEOUT, RAOP_SERVICE, TimingServer, generateActiveRemoteId, generateDacpId, generateSessionId, getLocalIP, getMacAddress, prompt, randomInt32, randomInt64, reporter, uint16ToBE, uint53ToLE, v4 as uuid, waitFor };
1478
+ export { AIRPLAY_SERVICE, AIRPLAY_TRANSIENT_PIN, AccessoryPair, AccessoryVerify, AirPlayFeatureFlags, COMPANION_LINK_SERVICE, Connection, Context, Discovery, ENCRYPTION, EncryptionAwareConnection, EncryptionState, HTTP_TIMEOUT, JsonStorage, MemoryStorage, RAOP_SERVICE, Storage, TimingServer, describeFlags, generateActiveRemoteId, generateDacpId, generateSessionId, getLocalIP, getMacAddress, getPairingRequirement, getProtocolVersion, hasFeatureFlag, isPasswordRequired, isRemoteControlSupported, multicast as mdnsMulticast, unicast as mdnsUnicast, parseFeatures, prompt, randomInt32, randomInt64, reporter, uint16ToBE, uint53ToLE, v4 as uuid, waitFor };
845
1479
  //# sourceMappingURL=index.mjs.map