@basmilius/apple-common 0.8.1 → 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,3 +1,1479 @@
1
- import{randomBytes as e,randomFillSync as t,randomUUID as n}from"node:crypto";import r from"node-dns-sd";import{createInterface as i}from"node:readline";import{EventEmitter as a}from"node:events";import{Socket as o}from"node:net";import{NTP as s,OPack as c,TLV8 as l}from"@basmilius/apple-encoding";import{Chacha20 as u,Curve25519 as d,Ed25519 as f,hkdf as p}from"@basmilius/apple-encryption";import{SRP as m,SrpClient as h}from"fast-srp-hap";import{createSocket as g}from"node:dgram";import{networkInterfaces as _}from"node:os";const v=[];for(let e=0;e<256;++e)v.push((e+256).toString(16).slice(1));function y(e,t=0){return(v[e[t+0]]+v[e[t+1]]+v[e[t+2]]+v[e[t+3]]+`-`+v[e[t+4]]+v[e[t+5]]+`-`+v[e[t+6]]+v[e[t+7]]+`-`+v[e[t+8]]+v[e[t+9]]+`-`+v[e[t+10]]+v[e[t+11]]+v[e[t+12]]+v[e[t+13]]+v[e[t+14]]+v[e[t+15]]).toLowerCase()}const b=new Uint8Array(256);let x=b.length;function S(){return x>b.length-16&&(t(b),x=0),b.slice(x,x+=16)}var C={randomUUID:n};function w(e,t,n){e||={};let r=e.random??e.rng?.()??S();if(r.length<16)throw Error(`Random bytes length must be >= 16`);if(r[6]=r[6]&15|64,r[8]=r[8]&63|128,t){if(n||=0,n<0||n+16>t.length)throw RangeError(`UUID byte range ${n}:${n+15} is out of buffer bounds`);for(let e=0;e<16;++e)t[n+e]=r[e];return t}return y(r)}function T(e,t,n){return C.randomUUID&&!t&&!e?C.randomUUID():w(e,t,n)}async function E(e){let t=i({input:process.stdin,output:process.stdout}),n=await new Promise(n=>t.question(`${e}: `,n));return t.close(),n}async function D(e){return new Promise(t=>setTimeout(t,e))}const O=`3939`,k=6e3,A=`_airplay._tcp.local`,j=`_companion-link._tcp.local`,M=`_raop._tcp.local`;var N=class e{#e;constructor(e){this.#e=e}async find(){return(await r.discover({name:this.#e})).map(e=>({id:P(e)??e.fqdn,txt:ee(e),...e}))}async findUntil(e,t=10,n=1e3){for(;t>0;){let r=await this.find(),i=r.find(t=>t.id===e);if(i)return i;console.log(),console.log(`Device not found, retrying in ${n}ms...`),console.log(r.map(e=>` ● ${e.id} (${e.fqdn})`).join(`
2
- `)),t--,await D(n)}throw Error(`Device not found after serveral tries, aborting.`)}static airplay(){return new e(A)}static companionLink(){return new e(j)}static raop(){return new e(M)}};function P(e){if(!e?.packet)return null;let{answers:t=[],additionals:n=[]}=e.packet,r=[...t,...n],i=r.find(e=>e.type===`SRV`);if(i?.rdata?.target)return i.rdata.target;if(e.address){let t=r.find(t=>(t.type===`A`||t.type===`AAAA`)&&t.rdata===e.address);if(t?.name)return t.name}let a=r.find(e=>e.type===`A`);return a?.name?a.name:e.modelName?`${e.modelName.replace(/\s+/g,`-`).replace(/[^a-zA-Z0-9-]/g,``)}.local`:null}function ee(e){if(!e.packet)return{};let{answers:t=[],additionals:n=[]}=e.packet,r=[...t,...n],i={};for(let e of r)e.type===`TXT`&&e.rdata&&Object.assign(i,e.rdata);return i}const F=Symbol(),I={resolve:()=>{},reject:e=>{}};var L=class extends a{get address(){return this.#e}get context(){return this.#n}get port(){return this.#t}get isConnected(){return this.#u===`connected`}get state(){if(this.#u===`closing`||this.#u===`failed`)return this.#u;if(!this.#l)return`disconnected`;switch(this.#l.readyState){case`opening`:return`connecting`;case`open`:return`connected`;default:return this.#u}}#e;#t;#n;#r=!1;#i=0;#a=3;#o=!0;#s=3e3;#c;#l;#u;#d;constructor(e,t,n){super(),this.#e=t,this.#t=n,this.#n=e,this.#u=`disconnected`}async connect(){if(this.#u!==`connected`){if(this.#u===`connecting`)throw Error(`A connection is already being established.`);return this.#o=!0,this.#i=0,this.#f()}}destroy(){this.#l?.destroy()}async disconnect(){if(this.#c&&=(clearTimeout(this.#c),void 0),this.#o=!1,!(!this.#l||this.#u===`disconnected`))return new Promise(e=>{this.#u=`closing`,this.#l.once(`close`,()=>{this.#p(),e()}),this.#l.end()})}debug(e){return this.#r=e,this}retry(e,t=3e3){return this.#a=e,this.#s=t,this}write(e){if(!this.#l||this.state!==`connected`||!this.#l.writable){this.emit(`error`,Error(`Cannot write to a disconnected connection.`));return}this.#l.write(e,e=>{e&&(this.#n.logger.error(`Failed to write data to socket.`),this.emit(`error`,e))})}async#f(){return new Promise((e,t)=>{this.#u=`connecting`,this.#d={resolve:e,reject:t},this.#l?.removeAllListeners(),this.#l=void 0,this.#l=new o,this.#l.setNoDelay(!0),this.#l.setTimeout(1e4),this.#l.on(`close`,this.#h.bind(this)),this.#l.on(`connect`,this.#g.bind(this)),this.#l.on(`data`,this.#_.bind(this)),this.#l.on(`end`,this.#v.bind(this)),this.#l.on(`error`,this.#y.bind(this)),this.#l.on(`timeout`,this.#b.bind(this)),this.#n.logger.net(`Connecting to ${this.#e}:${this.#t}...`),this.#l.connect({host:this.#e,port:this.#t,keepAlive:!0})})}#p(){this.#c&&=(clearTimeout(this.#c),void 0),this.#l&&=(this.#l.removeAllListeners(),this.#l.destroy(),void 0),this.#u=`disconnected`,this.#d=void 0}#m(e){if(!this.#o||this.#i>=this.#a){this.#u=`failed`,this.#d?.reject(e),this.#d=void 0;return}this.#c&&=(clearTimeout(this.#c),void 0),this.#i++,this.#n.logger.net(`Retry attempt ${this.#i} / ${this.#a} in ${this.#s}ms...`);let{resolve:t,reject:n}=this.#d??I;this.#p(),this.#c=setTimeout(async()=>{this.#c=void 0;try{this.#d={resolve:t,reject:n},await this.#f(),t()}catch{}},this.#s)}#h(e){let t=this.#u===`connected`;this.#u!==`closing`&&(this.#u=`disconnected`,this.#n.logger.net(`Connection closed (${e?`with error`:`normally`}).`)),this.emit(`close`,e),t&&this.#o&&e&&this.#m(Error(`Connection closed unexpectedly.`))}#g(){this.#u=`connected`,this.#i=0,this.#l.setKeepAlive(!0,1e4),this.#l.setTimeout(0),this.emit(`connect`),this.#d?.resolve(),this.#d=void 0}#_(e){if(this.#r){let t=Math.min(e.byteLength,64);this.#n.logger.debug(`Received ${e.byteLength} bytes of data.`),this.#n.logger.debug(`hex=${e.subarray(0,t).toString(`hex`)}`),this.#n.logger.debug(`ascii=${e.toString(`ascii`).replace(/[^\x20-\x7E]/g,`.`).substring(0,t)}`)}this.emit(`data`,e)}#v(){this.emit(`end`)}#y(e){this.#n.logger.error(`Connection error: ${e.message}`),this.listenerCount(`error`)>0?this.emit(`error`,e):this.#n.logger.warn(`No error handler registered. This is likely a bug.`,this.constructor.name,`#onError`),this.#u===`connecting`?this.#m(e):this.#u=`failed`}#b(){this.#n.logger.error(`Connection timed out.`);let e=Error(`Connection timed out.`);this.emit(`timeout`),this.#u===`connecting`?this.#m(e):(this.#u=`failed`,this.#l?.destroy())}},R=class extends L{get isEncrypted(){return!!this[F]}[F];enableEncryption(e,t){this[F]=new z(e,t)}},z=class{readKey;readCount;writeKey;writeCount;constructor(e,t){this.readCount=0,this.readKey=e,this.writeCount=0,this.writeKey=t}},B=class{get id(){return this.#e}get label(){return this.#t}#e;#t;constructor(e){this.#e=e,this.#t=`\u001b[36m[${e}]\u001b[39m`}debug(...e){H(this.#t,...e)}error(...e){U(this.#t,...e)}info(...e){W(this.#t,...e)}net(...e){G(this.#t,...e)}raw(...e){K(this.#t,...e)}warn(...e){q(this.#t,...e)}},V=class{#e=[];all(){this.#e=[`debug`,`error`,`info`,`net`,`raw`,`warn`]}disable(e){this.#e.includes(e)&&this.#e.splice(this.#e.indexOf(e),1)}enable(e){this.#e.includes(e)||this.#e.push(e)}isEnabled(e){return this.#e.includes(e)}};function H(...e){J.isEnabled(`debug`)&&console.debug(`\x1B[36m[debug]\x1B[39m`,...e)}function U(...e){J.isEnabled(`error`)&&console.error(`\x1B[31m[error]\x1B[39m`,...e)}function W(...e){J.isEnabled(`info`)&&console.info(`\x1B[32m[info]\x1B[39m`,...e)}function G(...e){J.isEnabled(`net`)&&console.info(`\x1B[33m[net]\x1B[39m`,...e)}function K(...e){J.isEnabled(`raw`)&&console.log(`\x1B[34m[raw]\x1B[39m`,...e)}function q(...e){J.isEnabled(`warn`)&&console.warn(`\x1B[33m[warn]\x1B[39m`,...e)}const J=new V;var Y=class{get deviceId(){return this.#e}get logger(){return this.#t}#e;#t;constructor(e){this.#e=e,this.#t=new B(e)}},X=class{get context(){return this.#e}#e;constructor(e){this.#e=e}tlv(e){let t=l.decode(e);return t.has(l.Value.Error)&&l.bail(t),this.#e.logger.raw(`Decoded TLV`,t),t}},Z=class extends X{#e;#t;#n;#r;#i;#a;constructor(e,t){super(e),this.#e=`basmilius/apple-protocols`,this.#t=Buffer.from(T().toUpperCase()),this.#n=t}async start(){let e=f.generateKeyPair();this.#r=Buffer.from(e.publicKey),this.#i=Buffer.from(e.secretKey)}async pin(e){let t=await this.m1(),n=await this.m2(t,await e()),r=await this.m3(n),i=await this.m4(r),a=await this.m5(i),o=await this.m6(i,a);if(!o)throw Error(`Pairing failed, could not get accessory keys.`);return o}async transient(){let e=await this.m1([[l.Value.Flags,l.Flags.TransientPairing]]),t=await this.m2(e),n=await this.m3(t),r=await this.m4(n),i=p({hash:`sha512`,key:r.sharedSecret,length:32,salt:Buffer.from(`Control-Salt`),info:Buffer.from(`Control-Read-Encryption-Key`)}),a=p({hash:`sha512`,key:r.sharedSecret,length:32,salt:Buffer.from(`Control-Salt`),info:Buffer.from(`Control-Write-Encryption-Key`)});return{pairingId:this.#t,sharedSecret:r.sharedSecret,accessoryToControllerKey:i,controllerToAccessoryKey:a}}async m1(e=[]){let t=await this.#n(`m1`,l.encode([[l.Value.Method,l.Method.PairSetup],[l.Value.State,l.State.M1],...e])),n=this.tlv(t);return{publicKey:n.get(l.Value.PublicKey),salt:n.get(l.Value.Salt)}}async m2(e,t=O){let n=await m.genKey(32);return this.#a=new h(m.params.hap,e.salt,Buffer.from(`Pair-Setup`),Buffer.from(t),n,!0),this.#a.setB(e.publicKey),{publicKey:this.#a.computeA(),proof:this.#a.computeM1()}}async m3(e){let t=await this.#n(`m3`,l.encode([[l.Value.State,l.State.M3],[l.Value.PublicKey,e.publicKey],[l.Value.Proof,e.proof]]));return{serverProof:this.tlv(t).get(l.Value.Proof)}}async m4(e){return this.#a.checkM2(e.serverProof),{sharedSecret:this.#a.computeK()}}async m5(e){let t=p({hash:`sha512`,key:e.sharedSecret,length:32,salt:Buffer.from(`Pair-Setup-Controller-Sign-Salt`,`utf8`),info:Buffer.from(`Pair-Setup-Controller-Sign-Info`,`utf8`)}),n=p({hash:`sha512`,key:e.sharedSecret,length:32,salt:Buffer.from(`Pair-Setup-Encrypt-Salt`,`utf8`),info:Buffer.from(`Pair-Setup-Encrypt-Info`,`utf8`)}),r=Buffer.concat([t,this.#t,this.#r]),i=f.sign(r,this.#i),a=l.encode([[l.Value.Identifier,this.#t],[l.Value.PublicKey,this.#r],[l.Value.Signature,Buffer.from(i)],[l.Value.Name,c.encode({name:this.#e})]]),{authTag:o,ciphertext:s}=u.encrypt(n,Buffer.from(`PS-Msg05`),null,a),d=Buffer.concat([s,o]),m=await this.#n(`m5`,l.encode([[l.Value.State,l.State.M5],[l.Value.EncryptedData,d]])),h=this.tlv(m).get(l.Value.EncryptedData),g=h.subarray(0,-16);return{authTag:h.subarray(-16),data:g,sessionKey:n}}async m6(e,t){let n=u.decrypt(t.sessionKey,Buffer.from(`PS-Msg06`),null,t.data,t.authTag),r=l.decode(n),i=r.get(l.Value.Identifier),a=r.get(l.Value.PublicKey),o=r.get(l.Value.Signature),s=p({hash:`sha512`,key:e.sharedSecret,length:32,salt:Buffer.from(`Pair-Setup-Accessory-Sign-Salt`),info:Buffer.from(`Pair-Setup-Accessory-Sign-Info`)}),c=Buffer.concat([s,i,a]);if(!f.verify(c,o,a))throw Error(`Invalid accessory signature.`);return{accessoryIdentifier:i.toString(),accessoryLongTermPublicKey:a,pairingId:this.#t,publicKey:this.#r,secretKey:this.#i}}},Q=class extends X{#e;#t;constructor(e,t){super(e),this.#e=d.generateKeyPair(),this.#t=t}async start(e){let t=await this.#n(),n=await this.#r(e.accessoryIdentifier,e.accessoryLongTermPublicKey,t);return await this.#i(e.pairingId,e.secretKey,n),await this.#a(n,e.pairingId)}async#n(){let e=await this.#t(`m1`,l.encode([[l.Value.State,l.State.M1],[l.Value.PublicKey,Buffer.from(this.#e.publicKey)]])),t=this.tlv(e),n=t.get(l.Value.PublicKey);return{encryptedData:t.get(l.Value.EncryptedData),serverPublicKey:n}}async#r(e,t,n){let r=Buffer.from(d.generateSharedSecKey(this.#e.secretKey,n.serverPublicKey)),i=p({hash:`sha512`,key:r,length:32,salt:Buffer.from(`Pair-Verify-Encrypt-Salt`),info:Buffer.from(`Pair-Verify-Encrypt-Info`)}),a=n.encryptedData.subarray(0,-16),o=n.encryptedData.subarray(-16),s=u.decrypt(i,Buffer.from(`PV-Msg02`),null,a,o),c=l.decode(s),m=c.get(l.Value.Identifier),h=c.get(l.Value.Signature);if(m.toString()!==e)throw Error(`Invalid accessory identifier. Expected ${m.toString()} to be ${e}.`);let g=Buffer.concat([n.serverPublicKey,m,this.#e.publicKey]);if(!f.verify(g,h,t))throw Error(`Invalid accessory signature.`);return{serverEphemeralPublicKey:n.serverPublicKey,sessionKey:i,sharedSecret:r}}async#i(e,t,n){let r=Buffer.concat([this.#e.publicKey,e,n.serverEphemeralPublicKey]),i=Buffer.from(f.sign(r,t)),a=l.encode([[l.Value.Identifier,e],[l.Value.Signature,i]]),{authTag:o,ciphertext:s}=u.encrypt(n.sessionKey,Buffer.from(`PV-Msg03`),null,a),c=Buffer.concat([s,o]);return await this.#t(`m3`,l.encode([[l.Value.State,l.State.M3],[l.Value.EncryptedData,c]])),{}}async#a(e,t){return{accessoryToControllerKey:Buffer.alloc(0),controllerToAccessoryKey:Buffer.alloc(0),pairingId:t,sharedSecret:e.sharedSecret}}},te=class{get port(){return this.#n}#e;#t;#n=0;constructor(){this.#e=new B(`timing-server`),this.#t=g(`udp4`),this.#t.on(`connect`,this.#r.bind(this)),this.#t.on(`error`,this.#i.bind(this)),this.#t.on(`message`,this.#o.bind(this))}close(){this.#t.close(),this.#n=0}listen(){return new Promise((e,t)=>{this.#t.once(`error`,t),this.#t.once(`listening`,()=>{this.#t.removeListener(`error`,t),this.#a(),e()}),this.#t.bind(0,e)})}#r(){this.#t.setRecvBufferSize(16384),this.#t.setSendBufferSize(16384)}#i(e){this.#e.error(`Timing server error`,e)}#a(){let{port:e}=this.#t.address();this.#n=e}#o(e,t){try{let n=s.decode(e),r=s.now(),[i,a]=s.parts(r);this.#e.info(`Timing server ntp=${r} receivedSeconds=${i} receivedFraction=${a}`);let o=s.encode({proto:n.proto,type:211,seqno:n.seqno,padding:0,reftime_sec:n.sendtime_sec,reftime_frac:n.sendtime_frac,recvtime_sec:i,recvtime_frac:a,sendtime_sec:i,sendtime_frac:a});this.#t.send(o,t.port,t.address,e=>{e&&this.#e.warn(`Timing server failed to send response to ${t.address}:${t.port}`,e)})}catch(n){this.#e.warn(`Timing server received malformed packet (${e.length} bytes) from ${t.address}:${t.port}`,n)}}};function ne(){return Math.floor(Math.random()*2**32).toString(10)}function re(){return Math.floor(Math.random()*2**64).toString(16).toUpperCase()}function ie(){return Math.floor(Math.random()*2**32).toString(10)}function $(){let e=_();for(let t of Object.values(e))if(t){for(let e of t)if(!(e.internal||e.family!==`IPv4`)&&e.address&&e.address!==`127.0.0.1`)return e.address}return null}function ae(){let e=_();for(let t of Object.values(e))if(t){for(let e of t)if(!(e.internal||e.family!==`IPv4`)&&e.mac&&e.mac!==`00:00:00:00:00:00`)return e.mac.toUpperCase()}return`00:00:00:00:00:00`}function oe(){return e(4).readUInt32BE(0)}function se(){return e(8).readBigUint64LE(0)}function ce(e){let t=Buffer.allocUnsafe(2);return t.writeUInt16BE(e,0),t}function le(e){let[t,n]=ue(e),r=Buffer.allocUnsafe(8);return r.writeUInt32LE(n,0),r.writeUInt32LE(t,4),r}function ue(e){let t=4294967295;if(e<=-1||e>9007199254740991)throw Error(`Number out of range.`);if(Math.floor(e)!==e)throw Error(`Number is not an integer.`);let n=0,r=e&4294967295,i=r<0?(e&2147483647)+2147483648:r;return e>t&&(n=(e-i)/(t+1)),[n,i]}export{A as AIRPLAY_SERVICE,O as AIRPLAY_TRANSIENT_PIN,Z as AccessoryPair,Q as AccessoryVerify,j as COMPANION_LINK_SERVICE,L as Connection,Y as Context,N as Discovery,F as ENCRYPTION,R as EncryptionAwareConnection,z as EncryptionState,k as HTTP_TIMEOUT,M as RAOP_SERVICE,te as TimingServer,ne as generateActiveRemoteId,re as generateDacpId,ie as generateSessionId,$ as getLocalIP,ae as getMacAddress,E as prompt,oe as randomInt32,se as randomInt64,J as reporter,ce as uint16ToBE,le as uint53ToLE,T as uuid,D as waitFor};
1
+ import { createRequire } from "node:module";
2
+ import { randomBytes, randomFillSync, randomUUID } from "node:crypto";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { Socket, createConnection } from "node:net";
6
+ import { createInterface } from "node:readline";
7
+ import { createSocket } from "node:dgram";
8
+ import { networkInterfaces } from "node:os";
9
+ import { EventEmitter } from "node:events";
10
+ import { NTP, OPack, TLV8 } from "@basmilius/apple-encoding";
11
+ import { Chacha20, Curve25519, Ed25519, hkdf } from "@basmilius/apple-encryption";
12
+ import { SRP, SrpClient } from "fast-srp-hap";
13
+
14
+ //#region \0rolldown/runtime.js
15
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
16
+
17
+ //#endregion
18
+ //#region ../../node_modules/.bun/uuid@13.0.0/node_modules/uuid/dist-node/stringify.js
19
+ const byteToHex = [];
20
+ for (let i = 0; i < 256; ++i) byteToHex.push((i + 256).toString(16).slice(1));
21
+ function unsafeStringify(arr, offset = 0) {
22
+ return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
23
+ }
24
+
25
+ //#endregion
26
+ //#region ../../node_modules/.bun/uuid@13.0.0/node_modules/uuid/dist-node/rng.js
27
+ const rnds8Pool = new Uint8Array(256);
28
+ let poolPtr = rnds8Pool.length;
29
+ function rng() {
30
+ if (poolPtr > rnds8Pool.length - 16) {
31
+ randomFillSync(rnds8Pool);
32
+ poolPtr = 0;
33
+ }
34
+ return rnds8Pool.slice(poolPtr, poolPtr += 16);
35
+ }
36
+
37
+ //#endregion
38
+ //#region ../../node_modules/.bun/uuid@13.0.0/node_modules/uuid/dist-node/native.js
39
+ var native_default = { randomUUID };
40
+
41
+ //#endregion
42
+ //#region ../../node_modules/.bun/uuid@13.0.0/node_modules/uuid/dist-node/v4.js
43
+ function _v4(options, buf, offset) {
44
+ options = options || {};
45
+ const rnds = options.random ?? options.rng?.() ?? rng();
46
+ if (rnds.length < 16) throw new Error("Random bytes length must be >= 16");
47
+ rnds[6] = rnds[6] & 15 | 64;
48
+ rnds[8] = rnds[8] & 63 | 128;
49
+ if (buf) {
50
+ offset = offset || 0;
51
+ if (offset < 0 || offset + 16 > buf.length) throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
52
+ for (let i = 0; i < 16; ++i) buf[offset + i] = rnds[i];
53
+ return buf;
54
+ }
55
+ return unsafeStringify(rnds);
56
+ }
57
+ function v4(options, buf, offset) {
58
+ if (native_default.randomUUID && !buf && !options) return native_default.randomUUID();
59
+ return _v4(options, buf, offset);
60
+ }
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
+
234
+ //#endregion
235
+ //#region src/cli.ts
236
+ async function prompt(message) {
237
+ const cli = createInterface({
238
+ input: process.stdin,
239
+ output: process.stdout
240
+ });
241
+ const answer = await new Promise((resolve) => cli.question(`${message}: `, resolve));
242
+ cli.close();
243
+ return answer;
244
+ }
245
+ async function waitFor(ms) {
246
+ return new Promise((resolve) => setTimeout(resolve, ms));
247
+ }
248
+
249
+ //#endregion
250
+ //#region src/const.ts
251
+ const AIRPLAY_TRANSIENT_PIN = "3939";
252
+ const HTTP_TIMEOUT = 6e3;
253
+ const SOCKET_TIMEOUT = 1e4;
254
+ const AIRPLAY_SERVICE = "_airplay._tcp.local";
255
+ const COMPANION_LINK_SERVICE = "_companion-link._tcp.local";
256
+ const RAOP_SERVICE = "_raop._tcp.local";
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
+
645
+ //#endregion
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
+ };
683
+ var Discovery = class Discovery {
684
+ static #cache = /* @__PURE__ */ new Map();
685
+ #service;
686
+ constructor(service) {
687
+ this.#service = service;
688
+ }
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;
700
+ }
701
+ async findUntil(id, tries = 10, timeout = 1e3) {
702
+ while (tries > 0) {
703
+ const devices = await this.find(false);
704
+ const device = devices.find((device) => device.id === id);
705
+ if (device) return device;
706
+ console.log();
707
+ console.log(`Device not found, retrying in ${timeout}ms...`);
708
+ console.log(devices.map((d) => ` ● ${d.id} (${d.fqdn})`).join("\n"));
709
+ tries--;
710
+ await waitFor(timeout);
711
+ }
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()];
763
+ }
764
+ static airplay() {
765
+ return new Discovery(AIRPLAY_SERVICE);
766
+ }
767
+ static companionLink() {
768
+ return new Discovery(COMPANION_LINK_SERVICE);
769
+ }
770
+ static raop() {
771
+ return new Discovery(RAOP_SERVICE);
772
+ }
773
+ };
774
+
775
+ //#endregion
776
+ //#region src/symbols.ts
777
+ const ENCRYPTION = Symbol();
778
+
779
+ //#endregion
780
+ //#region src/connection.ts
781
+ const NOOP_PROMISE_HANDLER = {
782
+ resolve: () => {},
783
+ reject: (_) => {}
784
+ };
785
+ var Connection = class extends EventEmitter {
786
+ get address() {
787
+ return this.#address;
788
+ }
789
+ get context() {
790
+ return this.#context;
791
+ }
792
+ get port() {
793
+ return this.#port;
794
+ }
795
+ get isConnected() {
796
+ return this.#state === "connected";
797
+ }
798
+ get state() {
799
+ if (this.#state === "closing" || this.#state === "failed") return this.#state;
800
+ if (!this.#socket) return "disconnected";
801
+ switch (this.#socket.readyState) {
802
+ case "opening": return "connecting";
803
+ case "open": return "connected";
804
+ default: return this.#state;
805
+ }
806
+ }
807
+ #address;
808
+ #port;
809
+ #context;
810
+ #debug = false;
811
+ #retryAttempt = 0;
812
+ #retryAttempts = 3;
813
+ #retryEnabled = true;
814
+ #retryInterval = 3e3;
815
+ #retryTimeout;
816
+ #socket;
817
+ #state;
818
+ #connectPromise;
819
+ constructor(context, address, port) {
820
+ super();
821
+ this.#address = address;
822
+ this.#port = port;
823
+ this.#context = context;
824
+ this.#state = "disconnected";
825
+ }
826
+ async connect() {
827
+ if (this.#state === "connected") return;
828
+ if (this.#state === "connecting") throw new Error("A connection is already being established.");
829
+ this.#retryEnabled = true;
830
+ this.#retryAttempt = 0;
831
+ return this.#attemptConnect();
832
+ }
833
+ destroy() {
834
+ this.#socket?.destroy();
835
+ }
836
+ async disconnect() {
837
+ if (this.#retryTimeout) {
838
+ clearTimeout(this.#retryTimeout);
839
+ this.#retryTimeout = void 0;
840
+ }
841
+ this.#retryEnabled = false;
842
+ if (!this.#socket || this.#state === "disconnected") return;
843
+ return new Promise((resolve) => {
844
+ this.#state = "closing";
845
+ this.#socket.once("close", () => {
846
+ this.#cleanup();
847
+ resolve();
848
+ });
849
+ this.#socket.end();
850
+ });
851
+ }
852
+ debug(enabled) {
853
+ this.#debug = enabled;
854
+ return this;
855
+ }
856
+ retry(attempts, interval = 3e3) {
857
+ this.#retryAttempts = attempts;
858
+ this.#retryInterval = interval;
859
+ return this;
860
+ }
861
+ write(data) {
862
+ if (!this.#socket || this.state !== "connected" || !this.#socket.writable) {
863
+ this.emit("error", /* @__PURE__ */ new Error("Cannot write to a disconnected connection."));
864
+ return;
865
+ }
866
+ this.#socket.write(data, (err) => {
867
+ if (!err) return;
868
+ this.#context.logger.error("Failed to write data to socket.");
869
+ this.emit("error", err);
870
+ });
871
+ }
872
+ async #attemptConnect() {
873
+ return new Promise((resolve, reject) => {
874
+ this.#state = "connecting";
875
+ this.#connectPromise = {
876
+ resolve,
877
+ reject
878
+ };
879
+ this.#socket?.removeAllListeners();
880
+ this.#socket = void 0;
881
+ this.#socket = new Socket();
882
+ this.#socket.setNoDelay(true);
883
+ this.#socket.setTimeout(SOCKET_TIMEOUT);
884
+ this.#socket.on("close", this.#onClose.bind(this));
885
+ this.#socket.on("connect", this.#onConnect.bind(this));
886
+ this.#socket.on("data", this.#onData.bind(this));
887
+ this.#socket.on("end", this.#onEnd.bind(this));
888
+ this.#socket.on("error", this.#onError.bind(this));
889
+ this.#socket.on("timeout", this.#onTimeout.bind(this));
890
+ this.#context.logger.net(`Connecting to ${this.#address}:${this.#port}...`);
891
+ this.#socket.connect({
892
+ host: this.#address,
893
+ port: this.#port,
894
+ keepAlive: true
895
+ });
896
+ });
897
+ }
898
+ #cleanup() {
899
+ if (this.#retryTimeout) {
900
+ clearTimeout(this.#retryTimeout);
901
+ this.#retryTimeout = void 0;
902
+ }
903
+ if (this.#socket) {
904
+ this.#socket.removeAllListeners();
905
+ this.#socket.destroy();
906
+ this.#socket = void 0;
907
+ }
908
+ this.#state = "disconnected";
909
+ this.#connectPromise = void 0;
910
+ }
911
+ #scheduleRetry(err) {
912
+ if (!this.#retryEnabled || this.#retryAttempt >= this.#retryAttempts) {
913
+ this.#state = "failed";
914
+ this.#connectPromise?.reject(err);
915
+ this.#connectPromise = void 0;
916
+ return;
917
+ }
918
+ if (this.#retryTimeout) {
919
+ clearTimeout(this.#retryTimeout);
920
+ this.#retryTimeout = void 0;
921
+ }
922
+ this.#retryAttempt++;
923
+ this.#context.logger.net(`Retry attempt ${this.#retryAttempt} / ${this.#retryAttempts} in ${this.#retryInterval}ms...`);
924
+ const { resolve, reject } = this.#connectPromise ?? NOOP_PROMISE_HANDLER;
925
+ this.#cleanup();
926
+ this.#retryTimeout = setTimeout(async () => {
927
+ this.#retryTimeout = void 0;
928
+ try {
929
+ this.#connectPromise = {
930
+ resolve,
931
+ reject
932
+ };
933
+ await this.#attemptConnect();
934
+ resolve();
935
+ } catch (retryErr) {}
936
+ }, this.#retryInterval);
937
+ }
938
+ #onClose(hadError) {
939
+ const wasConnected = this.#state === "connected";
940
+ if (this.#state !== "closing") {
941
+ this.#state = "disconnected";
942
+ this.#context.logger.net(`Connection closed (${hadError ? "with error" : "normally"}).`);
943
+ }
944
+ this.emit("close", hadError);
945
+ if (wasConnected && this.#retryEnabled && hadError) this.#scheduleRetry(/* @__PURE__ */ new Error("Connection closed unexpectedly."));
946
+ }
947
+ #onConnect() {
948
+ this.#state = "connected";
949
+ this.#retryAttempt = 0;
950
+ this.#socket.setKeepAlive(true, 1e4);
951
+ this.#socket.setTimeout(0);
952
+ this.emit("connect");
953
+ this.#connectPromise?.resolve();
954
+ this.#connectPromise = void 0;
955
+ }
956
+ #onData(data) {
957
+ if (this.#debug) {
958
+ const cutoff = Math.min(data.byteLength, 64);
959
+ this.#context.logger.debug(`Received ${data.byteLength} bytes of data.`);
960
+ this.#context.logger.debug(`hex=${data.subarray(0, cutoff).toString("hex")}`);
961
+ this.#context.logger.debug(`ascii=${data.toString("ascii").replace(/[^\x20-\x7E]/g, ".").substring(0, cutoff)}`);
962
+ }
963
+ this.emit("data", data);
964
+ }
965
+ #onEnd() {
966
+ this.emit("end");
967
+ }
968
+ #onError(err) {
969
+ this.#context.logger.error(`Connection error: ${err.message}`);
970
+ if (this.listenerCount("error") > 0) this.emit("error", err);
971
+ else this.#context.logger.warn("No error handler registered. This is likely a bug.", this.constructor.name, "#onError");
972
+ if (this.#state === "connecting") this.#scheduleRetry(err);
973
+ else this.#state = "failed";
974
+ }
975
+ #onTimeout() {
976
+ this.#context.logger.error("Connection timed out.");
977
+ const err = /* @__PURE__ */ new Error("Connection timed out.");
978
+ this.emit("timeout");
979
+ if (this.#state === "connecting") this.#scheduleRetry(err);
980
+ else {
981
+ this.#state = "failed";
982
+ this.#socket?.destroy();
983
+ }
984
+ }
985
+ };
986
+ var EncryptionAwareConnection = class extends Connection {
987
+ get isEncrypted() {
988
+ return !!this[ENCRYPTION];
989
+ }
990
+ [ENCRYPTION];
991
+ enableEncryption(readKey, writeKey) {
992
+ this[ENCRYPTION] = new EncryptionState(readKey, writeKey);
993
+ }
994
+ };
995
+ var EncryptionState = class {
996
+ readKey;
997
+ readCount;
998
+ writeKey;
999
+ writeCount;
1000
+ constructor(readKey, writeKey) {
1001
+ this.readCount = 0;
1002
+ this.readKey = readKey;
1003
+ this.writeCount = 0;
1004
+ this.writeKey = writeKey;
1005
+ }
1006
+ };
1007
+
1008
+ //#endregion
1009
+ //#region src/reporter.ts
1010
+ var Logger = class {
1011
+ get id() {
1012
+ return this.#id;
1013
+ }
1014
+ get label() {
1015
+ return this.#label;
1016
+ }
1017
+ #id;
1018
+ #label;
1019
+ constructor(id) {
1020
+ this.#id = id;
1021
+ this.#label = `\u001b[36m[${id}]\u001b[39m`;
1022
+ }
1023
+ debug(...data) {
1024
+ debug(this.#label, ...data);
1025
+ }
1026
+ error(...data) {
1027
+ error(this.#label, ...data);
1028
+ }
1029
+ info(...data) {
1030
+ info(this.#label, ...data);
1031
+ }
1032
+ net(...data) {
1033
+ net(this.#label, ...data);
1034
+ }
1035
+ raw(...data) {
1036
+ raw(this.#label, ...data);
1037
+ }
1038
+ warn(...data) {
1039
+ warn(this.#label, ...data);
1040
+ }
1041
+ };
1042
+ var Reporter = class {
1043
+ #enabled = [];
1044
+ all() {
1045
+ this.#enabled = [
1046
+ "debug",
1047
+ "error",
1048
+ "info",
1049
+ "net",
1050
+ "raw",
1051
+ "warn"
1052
+ ];
1053
+ }
1054
+ disable(group) {
1055
+ if (this.#enabled.includes(group)) this.#enabled.splice(this.#enabled.indexOf(group), 1);
1056
+ }
1057
+ enable(group) {
1058
+ if (!this.#enabled.includes(group)) this.#enabled.push(group);
1059
+ }
1060
+ isEnabled(group) {
1061
+ return this.#enabled.includes(group);
1062
+ }
1063
+ };
1064
+ function debug(...data) {
1065
+ reporter.isEnabled("debug") && console.debug(`\u001b[36m[debug]\u001b[39m`, ...data);
1066
+ }
1067
+ function error(...data) {
1068
+ reporter.isEnabled("error") && console.error(`\u001b[31m[error]\u001b[39m`, ...data);
1069
+ }
1070
+ function info(...data) {
1071
+ reporter.isEnabled("info") && console.info(`\u001b[32m[info]\u001b[39m`, ...data);
1072
+ }
1073
+ function net(...data) {
1074
+ reporter.isEnabled("net") && console.info(`\u001b[33m[net]\u001b[39m`, ...data);
1075
+ }
1076
+ function raw(...data) {
1077
+ reporter.isEnabled("raw") && console.log(`\u001b[34m[raw]\u001b[39m`, ...data);
1078
+ }
1079
+ function warn(...data) {
1080
+ reporter.isEnabled("warn") && console.warn(`\u001b[33m[warn]\u001b[39m`, ...data);
1081
+ }
1082
+ const reporter = new Reporter();
1083
+
1084
+ //#endregion
1085
+ //#region src/context.ts
1086
+ var Context = class {
1087
+ get deviceId() {
1088
+ return this.#deviceId;
1089
+ }
1090
+ get logger() {
1091
+ return this.#logger;
1092
+ }
1093
+ #deviceId;
1094
+ #logger;
1095
+ constructor(deviceId) {
1096
+ this.#deviceId = deviceId;
1097
+ this.#logger = new Logger(deviceId);
1098
+ }
1099
+ };
1100
+
1101
+ //#endregion
1102
+ //#region src/pairing.ts
1103
+ var BasePairing = class {
1104
+ get context() {
1105
+ return this.#context;
1106
+ }
1107
+ #context;
1108
+ constructor(context) {
1109
+ this.#context = context;
1110
+ }
1111
+ tlv(buffer) {
1112
+ const data = TLV8.decode(buffer);
1113
+ if (data.has(TLV8.Value.Error)) TLV8.bail(data);
1114
+ this.#context.logger.raw("Decoded TLV", data);
1115
+ return data;
1116
+ }
1117
+ };
1118
+ var AccessoryPair = class extends BasePairing {
1119
+ #name;
1120
+ #pairingId;
1121
+ #requestHandler;
1122
+ #publicKey;
1123
+ #secretKey;
1124
+ #srp;
1125
+ constructor(context, requestHandler) {
1126
+ super(context);
1127
+ this.#name = "basmilius/apple-protocols";
1128
+ this.#pairingId = Buffer.from(v4().toUpperCase());
1129
+ this.#requestHandler = requestHandler;
1130
+ }
1131
+ async start() {
1132
+ const keyPair = Ed25519.generateKeyPair();
1133
+ this.#publicKey = Buffer.from(keyPair.publicKey);
1134
+ this.#secretKey = Buffer.from(keyPair.secretKey);
1135
+ }
1136
+ async pin(askPin) {
1137
+ const m1 = await this.m1();
1138
+ const m2 = await this.m2(m1, await askPin());
1139
+ const m3 = await this.m3(m2);
1140
+ const m4 = await this.m4(m3);
1141
+ const m5 = await this.m5(m4);
1142
+ const m6 = await this.m6(m4, m5);
1143
+ if (!m6) throw new Error("Pairing failed, could not get accessory keys.");
1144
+ return m6;
1145
+ }
1146
+ async transient() {
1147
+ const m1 = await this.m1([[TLV8.Value.Flags, TLV8.Flags.TransientPairing]]);
1148
+ const m2 = await this.m2(m1);
1149
+ const m3 = await this.m3(m2);
1150
+ const m4 = await this.m4(m3);
1151
+ const accessoryToControllerKey = hkdf({
1152
+ hash: "sha512",
1153
+ key: m4.sharedSecret,
1154
+ length: 32,
1155
+ salt: Buffer.from("Control-Salt"),
1156
+ info: Buffer.from("Control-Read-Encryption-Key")
1157
+ });
1158
+ const controllerToAccessoryKey = hkdf({
1159
+ hash: "sha512",
1160
+ key: m4.sharedSecret,
1161
+ length: 32,
1162
+ salt: Buffer.from("Control-Salt"),
1163
+ info: Buffer.from("Control-Write-Encryption-Key")
1164
+ });
1165
+ return {
1166
+ pairingId: this.#pairingId,
1167
+ sharedSecret: m4.sharedSecret,
1168
+ accessoryToControllerKey,
1169
+ controllerToAccessoryKey
1170
+ };
1171
+ }
1172
+ async m1(additionalTlv = []) {
1173
+ const response = await this.#requestHandler("m1", TLV8.encode([
1174
+ [TLV8.Value.Method, TLV8.Method.PairSetup],
1175
+ [TLV8.Value.State, TLV8.State.M1],
1176
+ ...additionalTlv
1177
+ ]));
1178
+ const data = this.tlv(response);
1179
+ return {
1180
+ publicKey: data.get(TLV8.Value.PublicKey),
1181
+ salt: data.get(TLV8.Value.Salt)
1182
+ };
1183
+ }
1184
+ async m2(m1, pin = AIRPLAY_TRANSIENT_PIN) {
1185
+ const srpKey = await SRP.genKey(32);
1186
+ this.#srp = new SrpClient(SRP.params.hap, m1.salt, Buffer.from("Pair-Setup"), Buffer.from(pin), srpKey, true);
1187
+ this.#srp.setB(m1.publicKey);
1188
+ return {
1189
+ publicKey: this.#srp.computeA(),
1190
+ proof: this.#srp.computeM1()
1191
+ };
1192
+ }
1193
+ async m3(m2) {
1194
+ const response = await this.#requestHandler("m3", TLV8.encode([
1195
+ [TLV8.Value.State, TLV8.State.M3],
1196
+ [TLV8.Value.PublicKey, m2.publicKey],
1197
+ [TLV8.Value.Proof, m2.proof]
1198
+ ]));
1199
+ return { serverProof: this.tlv(response).get(TLV8.Value.Proof) };
1200
+ }
1201
+ async m4(m3) {
1202
+ this.#srp.checkM2(m3.serverProof);
1203
+ return { sharedSecret: this.#srp.computeK() };
1204
+ }
1205
+ async m5(m4) {
1206
+ const iosDeviceX = hkdf({
1207
+ hash: "sha512",
1208
+ key: m4.sharedSecret,
1209
+ length: 32,
1210
+ salt: Buffer.from("Pair-Setup-Controller-Sign-Salt", "utf8"),
1211
+ info: Buffer.from("Pair-Setup-Controller-Sign-Info", "utf8")
1212
+ });
1213
+ const sessionKey = hkdf({
1214
+ hash: "sha512",
1215
+ key: m4.sharedSecret,
1216
+ length: 32,
1217
+ salt: Buffer.from("Pair-Setup-Encrypt-Salt", "utf8"),
1218
+ info: Buffer.from("Pair-Setup-Encrypt-Info", "utf8")
1219
+ });
1220
+ const deviceInfo = Buffer.concat([
1221
+ iosDeviceX,
1222
+ this.#pairingId,
1223
+ this.#publicKey
1224
+ ]);
1225
+ const signature = Ed25519.sign(deviceInfo, this.#secretKey);
1226
+ const innerTlv = TLV8.encode([
1227
+ [TLV8.Value.Identifier, this.#pairingId],
1228
+ [TLV8.Value.PublicKey, this.#publicKey],
1229
+ [TLV8.Value.Signature, Buffer.from(signature)],
1230
+ [TLV8.Value.Name, OPack.encode({ name: this.#name })]
1231
+ ]);
1232
+ const { authTag, ciphertext } = Chacha20.encrypt(sessionKey, Buffer.from("PS-Msg05"), null, innerTlv);
1233
+ const encrypted = Buffer.concat([ciphertext, authTag]);
1234
+ const response = await this.#requestHandler("m5", TLV8.encode([[TLV8.Value.State, TLV8.State.M5], [TLV8.Value.EncryptedData, encrypted]]));
1235
+ const encryptedDataRaw = this.tlv(response).get(TLV8.Value.EncryptedData);
1236
+ const encryptedData = encryptedDataRaw.subarray(0, -16);
1237
+ return {
1238
+ authTag: encryptedDataRaw.subarray(-16),
1239
+ data: encryptedData,
1240
+ sessionKey
1241
+ };
1242
+ }
1243
+ async m6(m4, m5) {
1244
+ const data = Chacha20.decrypt(m5.sessionKey, Buffer.from("PS-Msg06"), null, m5.data, m5.authTag);
1245
+ const tlv = TLV8.decode(data);
1246
+ const accessoryIdentifier = tlv.get(TLV8.Value.Identifier);
1247
+ const accessoryLongTermPublicKey = tlv.get(TLV8.Value.PublicKey);
1248
+ const accessorySignature = tlv.get(TLV8.Value.Signature);
1249
+ const accessoryX = hkdf({
1250
+ hash: "sha512",
1251
+ key: m4.sharedSecret,
1252
+ length: 32,
1253
+ salt: Buffer.from("Pair-Setup-Accessory-Sign-Salt"),
1254
+ info: Buffer.from("Pair-Setup-Accessory-Sign-Info")
1255
+ });
1256
+ const accessoryInfo = Buffer.concat([
1257
+ accessoryX,
1258
+ accessoryIdentifier,
1259
+ accessoryLongTermPublicKey
1260
+ ]);
1261
+ if (!Ed25519.verify(accessoryInfo, accessorySignature, accessoryLongTermPublicKey)) throw new Error("Invalid accessory signature.");
1262
+ return {
1263
+ accessoryIdentifier: accessoryIdentifier.toString(),
1264
+ accessoryLongTermPublicKey,
1265
+ pairingId: this.#pairingId,
1266
+ publicKey: this.#publicKey,
1267
+ secretKey: this.#secretKey
1268
+ };
1269
+ }
1270
+ };
1271
+ var AccessoryVerify = class extends BasePairing {
1272
+ #ephemeralKeyPair;
1273
+ #requestHandler;
1274
+ constructor(context, requestHandler) {
1275
+ super(context);
1276
+ this.#ephemeralKeyPair = Curve25519.generateKeyPair();
1277
+ this.#requestHandler = requestHandler;
1278
+ }
1279
+ async start(credentials) {
1280
+ const m1 = await this.#m1();
1281
+ const m2 = await this.#m2(credentials.accessoryIdentifier, credentials.accessoryLongTermPublicKey, m1);
1282
+ await this.#m3(credentials.pairingId, credentials.secretKey, m2);
1283
+ return await this.#m4(m2, credentials.pairingId);
1284
+ }
1285
+ async #m1() {
1286
+ const response = await this.#requestHandler("m1", TLV8.encode([[TLV8.Value.State, TLV8.State.M1], [TLV8.Value.PublicKey, Buffer.from(this.#ephemeralKeyPair.publicKey)]]));
1287
+ const data = this.tlv(response);
1288
+ const serverPublicKey = data.get(TLV8.Value.PublicKey);
1289
+ return {
1290
+ encryptedData: data.get(TLV8.Value.EncryptedData),
1291
+ serverPublicKey
1292
+ };
1293
+ }
1294
+ async #m2(localAccessoryIdentifier, longTermPublicKey, m1) {
1295
+ const sharedSecret = Buffer.from(Curve25519.generateSharedSecKey(this.#ephemeralKeyPair.secretKey, m1.serverPublicKey));
1296
+ const sessionKey = hkdf({
1297
+ hash: "sha512",
1298
+ key: sharedSecret,
1299
+ length: 32,
1300
+ salt: Buffer.from("Pair-Verify-Encrypt-Salt"),
1301
+ info: Buffer.from("Pair-Verify-Encrypt-Info")
1302
+ });
1303
+ const encryptedData = m1.encryptedData.subarray(0, -16);
1304
+ const encryptedTag = m1.encryptedData.subarray(-16);
1305
+ const data = Chacha20.decrypt(sessionKey, Buffer.from("PV-Msg02"), null, encryptedData, encryptedTag);
1306
+ const tlv = TLV8.decode(data);
1307
+ const accessoryIdentifier = tlv.get(TLV8.Value.Identifier);
1308
+ const accessorySignature = tlv.get(TLV8.Value.Signature);
1309
+ if (accessoryIdentifier.toString() !== localAccessoryIdentifier) throw new Error(`Invalid accessory identifier. Expected ${accessoryIdentifier.toString()} to be ${localAccessoryIdentifier}.`);
1310
+ const accessoryInfo = Buffer.concat([
1311
+ m1.serverPublicKey,
1312
+ accessoryIdentifier,
1313
+ this.#ephemeralKeyPair.publicKey
1314
+ ]);
1315
+ if (!Ed25519.verify(accessoryInfo, accessorySignature, longTermPublicKey)) throw new Error("Invalid accessory signature.");
1316
+ return {
1317
+ serverEphemeralPublicKey: m1.serverPublicKey,
1318
+ sessionKey,
1319
+ sharedSecret
1320
+ };
1321
+ }
1322
+ async #m3(pairingId, secretKey, m2) {
1323
+ const iosDeviceInfo = Buffer.concat([
1324
+ this.#ephemeralKeyPair.publicKey,
1325
+ pairingId,
1326
+ m2.serverEphemeralPublicKey
1327
+ ]);
1328
+ const iosDeviceSignature = Buffer.from(Ed25519.sign(iosDeviceInfo, secretKey));
1329
+ const innerTlv = TLV8.encode([[TLV8.Value.Identifier, pairingId], [TLV8.Value.Signature, iosDeviceSignature]]);
1330
+ const { authTag, ciphertext } = Chacha20.encrypt(m2.sessionKey, Buffer.from("PV-Msg03"), null, innerTlv);
1331
+ const encrypted = Buffer.concat([ciphertext, authTag]);
1332
+ await this.#requestHandler("m3", TLV8.encode([[TLV8.Value.State, TLV8.State.M3], [TLV8.Value.EncryptedData, encrypted]]));
1333
+ return {};
1334
+ }
1335
+ async #m4(m2, pairingId) {
1336
+ return {
1337
+ accessoryToControllerKey: Buffer.alloc(0),
1338
+ controllerToAccessoryKey: Buffer.alloc(0),
1339
+ pairingId,
1340
+ sharedSecret: m2.sharedSecret
1341
+ };
1342
+ }
1343
+ };
1344
+
1345
+ //#endregion
1346
+ //#region src/timing.ts
1347
+ var TimingServer = class {
1348
+ get port() {
1349
+ return this.#port;
1350
+ }
1351
+ #logger;
1352
+ #socket;
1353
+ #port = 0;
1354
+ constructor() {
1355
+ this.#logger = new Logger("timing-server");
1356
+ this.#socket = createSocket("udp4");
1357
+ this.#socket.on("connect", this.#onConnect.bind(this));
1358
+ this.#socket.on("error", this.#onError.bind(this));
1359
+ this.#socket.on("message", this.#onMessage.bind(this));
1360
+ }
1361
+ close() {
1362
+ this.#socket.close();
1363
+ this.#port = 0;
1364
+ }
1365
+ listen() {
1366
+ return new Promise((resolve, reject) => {
1367
+ this.#socket.once("error", reject);
1368
+ this.#socket.once("listening", () => {
1369
+ this.#socket.removeListener("error", reject);
1370
+ this.#onListening();
1371
+ resolve();
1372
+ });
1373
+ this.#socket.bind(0, resolve);
1374
+ });
1375
+ }
1376
+ #onConnect() {
1377
+ this.#socket.setRecvBufferSize(16384);
1378
+ this.#socket.setSendBufferSize(16384);
1379
+ }
1380
+ #onError(err) {
1381
+ this.#logger.error("Timing server error", err);
1382
+ }
1383
+ #onListening() {
1384
+ const { port } = this.#socket.address();
1385
+ this.#port = port;
1386
+ }
1387
+ #onMessage(data, info) {
1388
+ try {
1389
+ const request = NTP.decode(data);
1390
+ const ntp = NTP.now();
1391
+ const [receivedSeconds, receivedFraction] = NTP.parts(ntp);
1392
+ this.#logger.info(`Timing server ntp=${ntp} receivedSeconds=${receivedSeconds} receivedFraction=${receivedFraction}`);
1393
+ const response = NTP.encode({
1394
+ proto: request.proto,
1395
+ type: 211,
1396
+ seqno: request.seqno,
1397
+ padding: 0,
1398
+ reftime_sec: request.sendtime_sec,
1399
+ reftime_frac: request.sendtime_frac,
1400
+ recvtime_sec: receivedSeconds,
1401
+ recvtime_frac: receivedFraction,
1402
+ sendtime_sec: receivedSeconds,
1403
+ sendtime_frac: receivedFraction
1404
+ });
1405
+ this.#socket.send(response, info.port, info.address, (err) => {
1406
+ if (!err) return;
1407
+ this.#logger.warn(`Timing server failed to send response to ${info.address}:${info.port}`, err);
1408
+ });
1409
+ } catch (err) {
1410
+ this.#logger.warn(`Timing server received malformed packet (${data.length} bytes) from ${info.address}:${info.port}`, err);
1411
+ }
1412
+ }
1413
+ };
1414
+
1415
+ //#endregion
1416
+ //#region src/utils.ts
1417
+ function generateActiveRemoteId() {
1418
+ return Math.floor(Math.random() * 2 ** 32).toString(10);
1419
+ }
1420
+ function generateDacpId() {
1421
+ return Math.floor(Math.random() * 2 ** 64).toString(16).toUpperCase();
1422
+ }
1423
+ function generateSessionId() {
1424
+ return Math.floor(Math.random() * 2 ** 32).toString(10);
1425
+ }
1426
+ function getLocalIP() {
1427
+ const interfaces = networkInterfaces();
1428
+ for (const iface of Object.values(interfaces)) {
1429
+ if (!iface) continue;
1430
+ for (const net of iface) {
1431
+ if (net.internal || net.family !== "IPv4") continue;
1432
+ if (net.address && net.address !== "127.0.0.1") return net.address;
1433
+ }
1434
+ }
1435
+ return null;
1436
+ }
1437
+ function getMacAddress() {
1438
+ const interfaces = networkInterfaces();
1439
+ for (const iface of Object.values(interfaces)) {
1440
+ if (!iface) continue;
1441
+ for (const net of iface) {
1442
+ if (net.internal || net.family !== "IPv4") continue;
1443
+ if (net.mac && net.mac !== "00:00:00:00:00:00") return net.mac.toUpperCase();
1444
+ }
1445
+ }
1446
+ return "00:00:00:00:00:00";
1447
+ }
1448
+ function randomInt32() {
1449
+ return randomBytes(4).readUInt32BE(0);
1450
+ }
1451
+ function randomInt64() {
1452
+ return randomBytes(8).readBigUint64LE(0);
1453
+ }
1454
+ function uint16ToBE(value) {
1455
+ const buffer = Buffer.allocUnsafe(2);
1456
+ buffer.writeUInt16BE(value, 0);
1457
+ return buffer;
1458
+ }
1459
+ function uint53ToLE(value) {
1460
+ const [upper, lower] = splitUInt53(value);
1461
+ const buffer = Buffer.allocUnsafe(8);
1462
+ buffer.writeUInt32LE(lower, 0);
1463
+ buffer.writeUInt32LE(upper, 4);
1464
+ return buffer;
1465
+ }
1466
+ function splitUInt53(number) {
1467
+ const MAX_UINT32 = 4294967295;
1468
+ if (number <= -1 || number > 9007199254740991) throw new Error("Number out of range.");
1469
+ if (Math.floor(number) !== number) throw new Error("Number is not an integer.");
1470
+ let upper = 0;
1471
+ const signbit = number & 4294967295;
1472
+ const lower = signbit < 0 ? (number & 2147483647) + 2147483648 : signbit;
1473
+ if (number > MAX_UINT32) upper = (number - lower) / (MAX_UINT32 + 1);
1474
+ return [upper, lower];
1475
+ }
1476
+
1477
+ //#endregion
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 };
3
1479
  //# sourceMappingURL=index.mjs.map