{"version":3,"file":"MDNSServer.cjs","sources":["../../../src/utils/MDNSServer.ts"],"sourcesContent":["import { EventEmitter } from 'events';\nimport { RecordType, SrvAnswer, StringAnswer, TxtAnswer } from 'dns-packet';\nimport deepEqual from 'fast-deep-equal';\nimport mDNS, { MulticastDNS, QueryPacket } from 'multicast-dns';\n\nimport { nameEquals } from './dns-utils';\n\n/**\n * Represents a DNS Resource Record relevant for mDNS responses.\n */\nexport type MDNSRecord = StringAnswer | TxtAnswer | SrvAnswer;\n\ntype EventMap = {\n  responded: [packet: mDNS.ResponseOutgoingPacket, error: Error | null, bytes?: number];\n};\n\n// MARK: MDNSServer\n/**\n * An mDNS server that responds to DNS queries on the local network.\n *\n * Maintains a registry of published records and automatically responds\n * to queries using the multicast-dns protocol.\n *\n * Emits:\n * - `responded` when a response is sent to a query\n *\n * @example\n * ```ts\n * const server = new MDNSServer({}, console.error)\n * server.register([{ name: '_http._tcp.local', type: 'PTR', data: 'MyService._http._tcp.local' }])\n * ```\n *\n * @see https://datatracker.ietf.org/doc/html/rfc6762\n */\nexport class MDNSServer extends EventEmitter<EventMap> {\n  mdns: MulticastDNS;\n\n  private typeToRecords = new Map<RecordType, MDNSRecord[]>();\n\n  /**\n   * @param options - Configuration passed to the underlying `multicast-dns` instance.\n   */\n  constructor(options: mDNS.Options) {\n    super();\n\n    this.mdns = mDNS(options);\n    this.mdns.setMaxListeners(0);\n    this.mdns.on('query', q => this.respond(q));\n  }\n\n  /**\n   * Registers a list of DNS records to be used in future responses.\n   *\n   * Duplicate records (based on type, name, and deep equality of data) are ignored.\n   *\n   * @param records - An array of mDNS records to register.\n   */\n  register(records: MDNSRecord[]): void {\n    function isDuplicate(a: MDNSRecord, b: MDNSRecord): boolean {\n      return a.type === b.type && a.name === b.name && deepEqual(a.data, b.data);\n    }\n\n    for (const record of records) {\n      const type = record.type;\n      const records = this.typeToRecords.get(type) ?? [];\n      if (records.every(r => !isDuplicate(r, record))) {\n        records.push(record);\n      }\n      this.typeToRecords.set(type, records);\n    }\n  }\n\n  /**\n   * Unregisters a set of previously registered DNS records.\n   *\n   * Matching is done by `record.name` only (not deeply).\n   *\n   * @param records - Records to remove from the server registry.\n   */\n  unregister(records: MDNSRecord[]): void {\n    for (const record of records) {\n      const type = record.type;\n      const records = this.typeToRecords.get(type) ?? [];\n      const filtered = records.filter(r => r.name !== record.name);\n      if (filtered.length > 0) {\n        this.typeToRecords.set(type, filtered);\n      } else {\n        this.typeToRecords.delete(type);\n      }\n    }\n  }\n\n  // MARK: private\n  /**\n   * Returns all records of a given type matching a name.\n   *\n   * If the name is not fully qualified (no dot), only the first segment of the record name is compared.\n   *\n   * @param type - DNS record type.\n   * @param name - Name to match against.\n   * @returns Matching records.\n   */\n  private getRecordsOf(type: RecordType, name: string): MDNSRecord[] {\n    const records: MDNSRecord[] = [];\n    for (const record of this.typeToRecords.get(type) ?? []) {\n      const targetName = name.includes('.') ? record.name : record.name.split('.')[0];\n      if (targetName !== undefined && nameEquals(targetName, name)) {\n        records.push(record);\n      }\n    }\n    return records;\n  }\n\n  /**\n   * Responds to incoming mDNS queries with matching registered records.\n   * Handles `ANY` queries and populates additional records for `PTR → SRV → A/AAAA + TXT` dependencies,\n   *\n   * @param query - The incoming mDNS query packet.\n   *\n   * @see {@link https://www.rfc-editor.org/rfc/rfc6763#section-12}\n   */\n  private respond(query: QueryPacket): void {\n    for (const question of query.questions) {\n      const queryType = question.type as RecordType | 'ANY';\n      const queryName = question.name;\n\n      let answers: MDNSRecord[];\n      if (queryType === 'ANY') {\n        answers = Array.from(this.typeToRecords.keys()).flatMap(type => this.getRecordsOf(type, queryName));\n      } else {\n        answers = this.getRecordsOf(queryType, queryName);\n      }\n\n      if (answers.length === 0) continue;\n\n      const additionals: MDNSRecord[] = [];\n      if (queryType !== 'ANY') {\n        for (const answer of answers) {\n          // Add SRV and TXT records if PTR is present\n          if (answer.type === 'PTR') {\n            additionals.push(...this.getRecordsOf('SRV', answer.data));\n            additionals.push(...this.getRecordsOf('TXT', answer.data));\n          }\n        }\n\n        // Collect unique SRV targets to enrich with A/AAAA\n        const targets = new Set<string>();\n        for (const record of additionals) {\n          if (record.type === 'SRV') {\n            const target = record.data.target;\n            targets.add(target);\n          }\n        }\n\n        for (const target of targets) {\n          additionals.push(...this.getRecordsOf('A', target));\n          additionals.push(...this.getRecordsOf('AAAA', target));\n        }\n      }\n\n      const packet: mDNS.ResponseOutgoingPacket = { answers, additionals };\n      this.mdns.respond(packet, (...args) => {\n        this.emit('responded', packet, ...args);\n      });\n    }\n  }\n}\n"],"names":["MDNSServer","EventEmitter","options","mDNS","q","records","isDuplicate","a","b","deepEqual","record","type","filtered","name","targetName","nameEquals","query","question","queryType","queryName","answers","additionals","answer","targets","target","packet","args"],"mappings":"+LAkCO,MAAMA,UAAmBC,EAAAA,YAAuB,CAQrD,YAAYC,EAAuB,CACjC,MAAA,EANF,KAAQ,kBAAoB,IAQ1B,KAAK,KAAOC,EAAKD,CAAO,EACxB,KAAK,KAAK,gBAAgB,CAAC,EAC3B,KAAK,KAAK,GAAG,WAAc,KAAK,QAAQE,CAAC,CAAC,CAC5C,CASA,SAASC,EAA6B,CACpC,SAASC,EAAYC,EAAeC,EAAwB,CAC1D,OAAOD,EAAE,OAASC,EAAE,MAAQD,EAAE,OAASC,EAAE,MAAQC,EAAUF,EAAE,KAAMC,EAAE,IAAI,CAC3E,CAEA,UAAWE,KAAUL,EAAS,CAC5B,MAAMM,EAAOD,EAAO,KACdL,EAAU,KAAK,cAAc,IAAIM,CAAI,GAAK,CAAA,EAC5CN,EAAQ,MAAM,GAAK,CAACC,EAAY,EAAGI,CAAM,CAAC,GAC5CL,EAAQ,KAAKK,CAAM,EAErB,KAAK,cAAc,IAAIC,EAAMN,CAAO,CACtC,CACF,CASA,WAAWA,EAA6B,CACtC,UAAWK,KAAUL,EAAS,CAC5B,MAAMM,EAAOD,EAAO,KAEdE,GADU,KAAK,cAAc,IAAID,CAAI,GAAK,CAAA,GACvB,UAAY,EAAE,OAASD,EAAO,IAAI,EACvDE,EAAS,OAAS,EACpB,KAAK,cAAc,IAAID,EAAMC,CAAQ,EAErC,KAAK,cAAc,OAAOD,CAAI,CAElC,CACF,CAYQ,aAAaA,EAAkBE,EAA4B,CACjE,MAAMR,EAAwB,CAAA,EAC9B,UAAWK,KAAU,KAAK,cAAc,IAAIC,CAAI,GAAK,GAAI,CACvD,MAAMG,EAAaD,EAAK,SAAS,GAAG,EAAIH,EAAO,KAAOA,EAAO,KAAK,MAAM,GAAG,EAAE,CAAC,EAC1EI,IAAe,QAAaC,EAAAA,WAAWD,EAAYD,CAAI,GACzDR,EAAQ,KAAKK,CAAM,CAEvB,CACA,OAAOL,CACT,CAUQ,QAAQW,EAA0B,CACxC,UAAWC,KAAYD,EAAM,UAAW,CACtC,MAAME,EAAYD,EAAS,KACrBE,EAAYF,EAAS,KAE3B,IAAIG,EAOJ,GANIF,IAAc,MAChBE,EAAU,MAAM,KAAK,KAAK,cAAc,KAAA,CAAM,EAAE,QAAQT,GAAQ,KAAK,aAAaA,EAAMQ,CAAS,CAAC,EAElGC,EAAU,KAAK,aAAaF,EAAWC,CAAS,EAG9CC,EAAQ,SAAW,EAAG,SAE1B,MAAMC,EAA4B,CAAA,EAClC,GAAIH,IAAc,MAAO,CACvB,UAAWI,KAAUF,EAEfE,EAAO,OAAS,QAClBD,EAAY,KAAK,GAAG,KAAK,aAAa,MAAOC,EAAO,IAAI,CAAC,EACzDD,EAAY,KAAK,GAAG,KAAK,aAAa,MAAOC,EAAO,IAAI,CAAC,GAK7D,MAAMC,MAAc,IACpB,UAAWb,KAAUW,EACnB,GAAIX,EAAO,OAAS,MAAO,CACzB,MAAMc,EAASd,EAAO,KAAK,OAC3Ba,EAAQ,IAAIC,CAAM,CACpB,CAGF,UAAWA,KAAUD,EACnBF,EAAY,KAAK,GAAG,KAAK,aAAa,IAAKG,CAAM,CAAC,EAClDH,EAAY,KAAK,GAAG,KAAK,aAAa,OAAQG,CAAM,CAAC,CAEzD,CAEA,MAAMC,EAAsC,CAAE,QAAAL,EAAS,YAAAC,CAAA,EACvD,KAAK,KAAK,QAAQI,EAAQ,IAAIC,IAAS,CACrC,KAAK,KAAK,YAAaD,EAAQ,GAAGC,CAAI,CACxC,CAAC,CACH,CACF,CACF"}