{"version":3,"file":"DiscoveredService.cjs","sources":["../../../src/browser/DiscoveredService.ts"],"sourcesContent":["import { RemoteInfo } from 'dgram';\nimport { Answer, StringAnswer } from 'dns-packet';\nimport { ResponsePacket } from 'multicast-dns';\n\nimport { decodeTXT, nameEquals, ServiceType } from '../utils';\n\n// MARK: DiscoveredService\n/**\n * Represents a discovered mDNS/DNS-SD service instance, as defined in the DNS-Based Service Discovery specification.\n *\n * A `DiscoveredService` is constructed from multiple DNS resource records typically found in an mDNS response:\n * - A {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1 | PTR record} that points to the instance's FQDN.\n * - An {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1.2 | SRV record} that provides the target host and port.\n * - A {@link https://datatracker.ietf.org/doc/html/rfc6763#section-6 | TXT record} containing key-value metadata.\n * - One or more {@link https://datatracker.ietf.org/doc/html/rfc6762#section-5.4 | A or AAAA records} for IP addresses.\n * - Optionally, additional {@link https://datatracker.ietf.org/doc/html/rfc6763#section-7.1 | subtype PTR records} for selective discovery.\n *\n * All records are typically retrieved from a single mDNS response.\n *\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4 | RFC 6763 § 4: Service Instances}\n * @see {@link https://datatracker.ietf.org/doc/html/rfc2782 | RFC 2782 - DNS SRV Records}\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6762 | RFC 6762 - Multicast DNS}\n */\nexport class DiscoveredService {\n  /**\n   * Creates a list of `DiscoveredService` instances from a DNS response packet.\n   *\n   * This function filters out records with expired TTL, finds matching PTR records for the given service name,\n   * and builds structured `DiscoveredService` objects by resolving their associated SRV, TXT, A/AAAA, and subtype PTR records.\n   *\n   * @internal\n   * @param packet - The full mDNS response packet containing DNS records.\n   * @param referer - Network information about the source of the packet.\n   * @returns An array of discovered service instances matching the given name.\n   */\n  static fromResponse(packet: ResponsePacket, referer: RemoteInfo): DiscoveredService[] {\n    const answers: Answer[] = [];\n\n    // Collect all relevant resource records with valid TTL (ignores goodbye messages)\n    for (const answer of [...packet.answers, ...packet.additionals]) {\n      if ('ttl' in answer && answer.ttl !== undefined && answer.ttl > 0) {\n        answers.push(answer);\n      }\n    }\n\n    const services: DiscoveredService[] = [];\n\n    // Find matching PTR records and construct DiscoveredService instances\n    for (const ptr of answers) {\n      if (ptr.type === 'PTR') {\n        const service = this.fromPTR(ptr, answers, referer);\n        if (service) {\n          services.push(service);\n        }\n      }\n    }\n\n    return services;\n  }\n\n  /**\n   * Constructs a `DiscoveredService` instance from a PTR record and its associated resource records.\n   *\n   * This method finds the corresponding SRV record for service connection details, optional TXT records\n   * for metadata, subtype PTR records for selective discovery, and A/AAAA records for resolving IP addresses.\n   *\n   * @internal\n   * @param ptr - The PTR record pointing to a service instance.\n   * @param answers - A list of resource records from the DNS response to search through.\n   * @param referer - The network information of the source of the response.\n   * @returns A `DiscoveredService` instance or `null` if a valid SRV record is not found.\n   */\n  private static fromPTR(ptr: StringAnswer, answers: Answer[], referer: RemoteInfo): DiscoveredService | null {\n    let service: DiscoveredService | undefined;\n\n    // Look for the SRV record that defines host and port\n    for (const answer of answers) {\n      if (answer.type === 'SRV' && nameEquals(answer.name, ptr.data)) {\n        const fqdn = answer.name;\n        const name = fqdn.split('.')[0];\n        if (name === undefined) {\n          console.error('Invalid service name', name);\n          return null;\n        }\n\n        const host = answer.data.target;\n        const port = answer.data.port;\n\n        /** @remarks Only the TTL of the PTR record is used for simplicity and compatibility */\n        service = new DiscoveredService(name, fqdn, host, port, referer, ptr.ttl, Date.now());\n      }\n    }\n\n    if (!service) return null;\n\n    // Attach TXT metadata if available\n    for (const answer of answers) {\n      if (answer.type === 'TXT' && nameEquals(answer.name, ptr.data)) {\n        service.txt = decodeTXT(answer.data, /* binary */ false);\n        service.binaryTxt = decodeTXT(answer.data, /* binary */ true);\n        service.rawTxt = answer.data;\n      }\n    }\n\n    // Collect any advertised subtypes (via _sub PTR records)\n    for (const answer of answers) {\n      if (answer.type === 'PTR' && nameEquals(answer.data, ptr.data) && answer.name.includes('._sub')) {\n        const types = ServiceType.fromString(answer.name);\n        if (types.subtype !== undefined) {\n          service.subtypes.push(types.subtype);\n        }\n      }\n    }\n\n    // Resolve host IP addresses (A/AAAA)\n    for (const answer of answers) {\n      if ((answer.type === 'A' || answer.type === 'AAAA') && service.host && nameEquals(answer.name, service.host)) {\n        service.addresses.push(answer.data);\n      }\n    }\n\n    return service;\n  }\n\n  /**\n   * The registered service type (e.g., _http, _ipp).\n   * @example \"ipp\"\n   * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1.2}\n   */\n  type?: string;\n  /**\n   * Network protocol used by the service (_tcp or _udp).\n   * @example \"tcp\"\n   * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1.2}\n   */\n  protocol?: string;\n  /**\n   * Subtypes advertised by the service for selective discovery.\n   * @example [\"scanner\", \"fax\"]\n   * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-7.1}\n   */\n  subtypes: string[] = [];\n\n  /**\n   * Resolved IPv4/IPv6 addresses of the service host.\n   * @example [\"192.168.1.42\", \"fe80::1c2a:3bff:fe4e:1234\"]\n   * @see {@link https://datatracker.ietf.org/doc/html/rfc6762#section-5.4}\n   */\n  addresses: string[] = [];\n\n  /**\n   * Decoded TXT record key-value pairs.\n   * @example { \"note\": \"Office printer\", \"paper\": \"A4\" }\n   * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-6}\n   */\n  txt?: Record<string, string>;\n  /**\n   * Decoded binary TXT record key-value pairs.\n   * @example { \"note\": <Buffer 04 6e 6f 74 65 0b 4f 66 69 63 65 20 50 72 69 6e 74 65 72> }\n   * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-6}\n   */\n  binaryTxt?: Record<string, Buffer>;\n  /**\n   * Raw TXT record data as received over the network.\n   * @example <Buffer 04 6e 6f 74 65 0b 4f 66 66 69 63 65 20 50 72 69 6e 74 65 72>\n   * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-6}\n   */\n  rawTxt?: string | Buffer | (string | Buffer)[];\n\n  /**\n   * Timer for checking TTL expiration.\n   */\n  ttlTimer?: NodeJS.Timeout;\n\n  private constructor(\n    /**\n     * Short instance name of the service (first label of the FQDN).\n     * @example \"MyPrinter\"\n     * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1.1}\n     */\n    readonly name: string,\n    /**\n     * Fully qualified domain name (e.g., MyPrinter._ipp._tcp.local).\n     * @example \"MyPrinter._ipp._tcp.local\"\n     * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1.1}\n     */\n    readonly fqdn: string,\n    /**\n     * Hostname of the machine offering the service (SRV record target).\n     * @example \"MyPrinter.local\"\n     * @see {@link https://datatracker.ietf.org/doc/html/rfc2782}\n     */\n    readonly host: string,\n    /**\n     * TCP or UDP port on which the service is running.\n     * @example 631\n     * @see {@link https://datatracker.ietf.org/doc/html/rfc2782}\n     */\n    readonly port: number,\n    /**\n     * Network info about where the service response came from.\n     * @example { address: \"192.168.1.101\", family: \"IPv4\", port: 5353, size: 412 }\n     * @see {@link https://datatracker.ietf.org/doc/html/rfc6762#section-5}\n     */\n    readonly referer: RemoteInfo,\n\n    /**\n     * Time-to-live value in seconds.\n     * @example 120\n     * @see {@link https://datatracker.ietf.org/doc/html/rfc6762#section-10}\n     */\n    readonly ttl: number | undefined,\n    /**\n     * Last time the service was seen (in milliseconds since the Unix epoch).\n     * @example 1634567890000\n     */\n    readonly lastSeen: number,\n  ) {\n    const serviceType = ServiceType.fromString(fqdn.split('.').slice(1, -1).join('.'));\n    this.type = serviceType.name;\n    this.protocol = serviceType.protocol;\n  }\n\n  get expired(): boolean {\n    return this.ttl !== undefined && Date.now() > this.lastSeen + this.ttl * 1000;\n  }\n}\n"],"names":["DiscoveredService","name","fqdn","host","port","referer","ttl","lastSeen","serviceType","ServiceType","packet","answers","answer","services","ptr","service","nameEquals","decodeTXT","types"],"mappings":"0QAuBO,MAAMA,CAAkB,CAuJrB,YAMGC,EAMAC,EAMAC,EAMAC,EAMAC,EAOAC,EAKAC,EACT,CArCS,KAAA,KAAAN,EAMA,KAAA,KAAAC,EAMA,KAAA,KAAAC,EAMA,KAAA,KAAAC,EAMA,KAAA,QAAAC,EAOA,KAAA,IAAAC,EAKA,KAAA,SAAAC,EA3EX,KAAA,SAAqB,CAAA,EAOrB,KAAA,UAAsB,CAAA,EAsEpB,MAAMC,EAAcC,EAAAA,YAAY,WAAWP,EAAK,MAAM,GAAG,EAAE,MAAM,EAAG,EAAE,EAAE,KAAK,GAAG,CAAC,EACjF,KAAK,KAAOM,EAAY,KACxB,KAAK,SAAWA,EAAY,QAC9B,CA1LA,OAAO,aAAaE,EAAwBL,EAA0C,CACpF,MAAMM,EAAoB,CAAA,EAG1B,UAAWC,IAAU,CAAC,GAAGF,EAAO,QAAS,GAAGA,EAAO,WAAW,EACxD,QAASE,GAAUA,EAAO,MAAQ,QAAaA,EAAO,IAAM,GAC9DD,EAAQ,KAAKC,CAAM,EAIvB,MAAMC,EAAgC,CAAA,EAGtC,UAAWC,KAAOH,EAChB,GAAIG,EAAI,OAAS,MAAO,CACtB,MAAMC,EAAU,KAAK,QAAQD,EAAKH,EAASN,CAAO,EAC9CU,GACFF,EAAS,KAAKE,CAAO,CAEzB,CAGF,OAAOF,CACT,CAcA,OAAe,QAAQC,EAAmBH,EAAmBN,EAA+C,CAC1G,IAAIU,EAGJ,UAAWH,KAAUD,EACnB,GAAIC,EAAO,OAAS,OAASI,EAAAA,WAAWJ,EAAO,KAAME,EAAI,IAAI,EAAG,CAC9D,MAAMZ,EAAOU,EAAO,KACdX,EAAOC,EAAK,MAAM,GAAG,EAAE,CAAC,EAC9B,GAAID,IAAS,OACX,eAAQ,MAAM,uBAAwBA,CAAI,EACnC,KAGT,MAAME,EAAOS,EAAO,KAAK,OACnBR,EAAOQ,EAAO,KAAK,KAGzBG,EAAU,IAAIf,EAAkBC,EAAMC,EAAMC,EAAMC,EAAMC,EAASS,EAAI,IAAK,KAAK,IAAA,CAAK,CACtF,CAGF,GAAI,CAACC,EAAS,OAAO,KAGrB,UAAWH,KAAUD,EACfC,EAAO,OAAS,OAASI,EAAAA,WAAWJ,EAAO,KAAME,EAAI,IAAI,IAC3DC,EAAQ,IAAME,EAAAA,UAAUL,EAAO,KAAmB,EAAA,EAClDG,EAAQ,UAAYE,EAAAA,UAAUL,EAAO,KAAmB,EAAA,EACxDG,EAAQ,OAASH,EAAO,MAK5B,UAAWA,KAAUD,EACnB,GAAIC,EAAO,OAAS,OAASI,EAAAA,WAAWJ,EAAO,KAAME,EAAI,IAAI,GAAKF,EAAO,KAAK,SAAS,OAAO,EAAG,CAC/F,MAAMM,EAAQT,EAAAA,YAAY,WAAWG,EAAO,IAAI,EAC5CM,EAAM,UAAY,QACpBH,EAAQ,SAAS,KAAKG,EAAM,OAAO,CAEvC,CAIF,UAAWN,KAAUD,GACdC,EAAO,OAAS,KAAOA,EAAO,OAAS,SAAWG,EAAQ,MAAQC,EAAAA,WAAWJ,EAAO,KAAMG,EAAQ,IAAI,GACzGA,EAAQ,UAAU,KAAKH,EAAO,IAAI,EAItC,OAAOG,CACT,CAqGA,IAAI,SAAmB,CACrB,OAAO,KAAK,MAAQ,QAAa,KAAK,MAAQ,KAAK,SAAW,KAAK,IAAM,GAC3E,CACF"}