@x12i/api-hub-dns 0.0.0-stage → 0.1.0
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/README.md +10 -2
- package/dist/domain.d.ts +5 -0
- package/dist/domain.js +36 -0
- package/dist/domain.js.map +1 -0
- package/dist/fake.d.ts +24 -0
- package/dist/fake.js +25 -0
- package/dist/fake.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/mail.d.ts +38 -0
- package/dist/mail.js +166 -0
- package/dist/mail.js.map +1 -0
- package/dist/resolver.d.ts +56 -0
- package/dist/resolver.js +152 -0
- package/dist/resolver.js.map +1 -0
- package/package.json +35 -4
package/README.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @x12i/api-hub-dns
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Public DNS, mail-record, and TLS lookup helpers for the x12i API hub
|
|
4
|
+
|
|
5
|
+
ESM package for Node.js 24 or newer. Import the public name:
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import * as dns from '@x12i/api-hub-dns';
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The whole set is also available from [`@x12i/api-hub`](https://www.npmjs.com/package/@x12i/api-hub).
|
package/dist/domain.d.ts
ADDED
package/dist/domain.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { domainToASCII } from 'node:url';
|
|
2
|
+
import { isIP } from 'node:net';
|
|
3
|
+
import { HubError } from '@x12i/api-hub-core';
|
|
4
|
+
// Underscores are valid in service labels such as _dmarc, _domainkey and SRV names, though not in host names.
|
|
5
|
+
const LABEL = /^(?!-)[a-z0-9_-]{1,63}(?<!-)$/;
|
|
6
|
+
/**
|
|
7
|
+
* Normalises a caller-supplied domain (IDN to punycode, lowercase, no trailing dot) and
|
|
8
|
+
* rejects anything that is not a public-looking hostname.
|
|
9
|
+
*/
|
|
10
|
+
export function normalizeDomain(input) {
|
|
11
|
+
let text = input.trim().toLowerCase();
|
|
12
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//.test(text)) {
|
|
13
|
+
try {
|
|
14
|
+
text = new URL(text).hostname;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new HubError('invalid_input', 'Not a valid domain');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
text = text.replace(/\.$/, '');
|
|
21
|
+
if (isIP(text))
|
|
22
|
+
throw new HubError('invalid_input', 'Expected a domain name, not an IP address');
|
|
23
|
+
const ascii = domainToASCII(text);
|
|
24
|
+
if (!ascii || ascii.length > 253)
|
|
25
|
+
throw new HubError('invalid_input', 'Not a valid domain');
|
|
26
|
+
const labels = ascii.split('.');
|
|
27
|
+
if (labels.length < 2 || !labels.every((l) => LABEL.test(l)))
|
|
28
|
+
throw new HubError('invalid_input', 'Not a valid domain');
|
|
29
|
+
if (/^\d+$/.test(labels.at(-1)))
|
|
30
|
+
throw new HubError('invalid_input', 'Not a valid domain');
|
|
31
|
+
if (['localhost', 'local', 'internal', 'invalid', 'test', 'example', 'home', 'lan', 'corp', 'onion'].includes(labels.at(-1))) {
|
|
32
|
+
throw new HubError('invalid_input', 'Domain is not publicly resolvable');
|
|
33
|
+
}
|
|
34
|
+
return ascii;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=domain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain.js","sourceRoot":"","sources":["../../../../../packages/dns/src/domain.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC,8GAA8G;AAC9G,MAAM,KAAK,GAAG,+BAA+B,CAAC;AAE9C;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,IAAI,IAAI,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACtC,IAAI,yBAAyB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACzC,IAAI,CAAC;YACH,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC;QAChC,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,QAAQ,CAAC,eAAe,EAAE,oBAAoB,CAAC,CAAC;QAC5D,CAAC;IACH,CAAC;IACD,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC/B,IAAI,IAAI,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,QAAQ,CAAC,eAAe,EAAE,2CAA2C,CAAC,CAAC;IACjG,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,GAAG;QAAE,MAAM,IAAI,QAAQ,CAAC,eAAe,EAAE,oBAAoB,CAAC,CAAC;IAC5F,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAChC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,QAAQ,CAAC,eAAe,EAAE,oBAAoB,CAAC,CAAC;IACxH,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC;QAAE,MAAM,IAAI,QAAQ,CAAC,eAAe,EAAE,oBAAoB,CAAC,CAAC;IAC5F,IAAI,CAAC,WAAW,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,EAAE,CAAC;QAC9H,MAAM,IAAI,QAAQ,CAAC,eAAe,EAAE,mCAAmC,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["import { domainToASCII } from 'node:url';\nimport { isIP } from 'node:net';\nimport { HubError } from '@x12i/api-hub-core';\n\n// Underscores are valid in service labels such as _dmarc, _domainkey and SRV names, though not in host names.\nconst LABEL = /^(?!-)[a-z0-9_-]{1,63}(?<!-)$/;\n\n/**\n * Normalises a caller-supplied domain (IDN to punycode, lowercase, no trailing dot) and\n * rejects anything that is not a public-looking hostname.\n */\nexport function normalizeDomain(input: string): string {\n let text = input.trim().toLowerCase();\n if (/^[a-z][a-z0-9+.-]*:\\/\\//.test(text)) {\n try {\n text = new URL(text).hostname;\n } catch {\n throw new HubError('invalid_input', 'Not a valid domain');\n }\n }\n text = text.replace(/\\.$/, '');\n if (isIP(text)) throw new HubError('invalid_input', 'Expected a domain name, not an IP address');\n const ascii = domainToASCII(text);\n if (!ascii || ascii.length > 253) throw new HubError('invalid_input', 'Not a valid domain');\n const labels = ascii.split('.');\n if (labels.length < 2 || !labels.every((l) => LABEL.test(l))) throw new HubError('invalid_input', 'Not a valid domain');\n if (/^\\d+$/.test(labels.at(-1)!)) throw new HubError('invalid_input', 'Not a valid domain');\n if (['localhost', 'local', 'internal', 'invalid', 'test', 'example', 'home', 'lan', 'corp', 'onion'].includes(labels.at(-1)!)) {\n throw new HubError('invalid_input', 'Domain is not publicly resolvable');\n }\n return ascii;\n}\n"]}
|
package/dist/fake.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DnsBackend } from './resolver.js';
|
|
2
|
+
export interface FakeZone {
|
|
3
|
+
A?: string[];
|
|
4
|
+
AAAA?: string[];
|
|
5
|
+
CNAME?: string[];
|
|
6
|
+
MX?: {
|
|
7
|
+
exchange: string;
|
|
8
|
+
priority: number;
|
|
9
|
+
}[];
|
|
10
|
+
NS?: string[];
|
|
11
|
+
TXT?: string[][];
|
|
12
|
+
CAA?: {
|
|
13
|
+
critical: number;
|
|
14
|
+
issue?: string;
|
|
15
|
+
issuewild?: string;
|
|
16
|
+
iodef?: string;
|
|
17
|
+
}[];
|
|
18
|
+
/** Reverse names, on a zone keyed by the IP address itself. */
|
|
19
|
+
PTR?: string[];
|
|
20
|
+
}
|
|
21
|
+
/** Recorded DNS answers for tests; unknown names behave like NXDOMAIN. */
|
|
22
|
+
export declare function fakeDnsBackend(zones: Record<string, FakeZone>, options?: {
|
|
23
|
+
timeout?: string[];
|
|
24
|
+
}): DnsBackend;
|
package/dist/fake.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Recorded DNS answers for tests; unknown names behave like NXDOMAIN. */
|
|
2
|
+
export function fakeDnsBackend(zones, options = {}) {
|
|
3
|
+
const answer = (name, pick) => {
|
|
4
|
+
if (options.timeout?.includes(name))
|
|
5
|
+
return Promise.reject(Object.assign(new Error('timeout'), { code: 'ETIMEOUT' }));
|
|
6
|
+
const zone = zones[name];
|
|
7
|
+
const value = zone ? pick(zone) : undefined;
|
|
8
|
+
if (!value)
|
|
9
|
+
return Promise.reject(Object.assign(new Error('nodata'), { code: zone ? 'ENODATA' : 'ENOTFOUND' }));
|
|
10
|
+
return Promise.resolve(value);
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
resolve4: (n) => answer(n, (z) => z.A?.map((address) => ({ address, ttl: 300 }))),
|
|
14
|
+
resolve6: (n) => answer(n, (z) => z.AAAA?.map((address) => ({ address, ttl: 300 }))),
|
|
15
|
+
resolveCname: (n) => answer(n, (z) => z.CNAME),
|
|
16
|
+
resolveMx: (n) => answer(n, (z) => z.MX),
|
|
17
|
+
resolveNs: (n) => answer(n, (z) => z.NS),
|
|
18
|
+
resolveTxt: (n) => answer(n, (z) => z.TXT),
|
|
19
|
+
resolveSoa: () => Promise.reject(Object.assign(new Error('nodata'), { code: 'ENODATA' })),
|
|
20
|
+
resolveCaa: (n) => answer(n, (z) => z.CAA),
|
|
21
|
+
resolveSrv: () => Promise.reject(Object.assign(new Error('nodata'), { code: 'ENODATA' })),
|
|
22
|
+
reverse: (ip) => answer(ip, (z) => z.PTR),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=fake.js.map
|
package/dist/fake.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fake.js","sourceRoot":"","sources":["../../../../../packages/dns/src/fake.ts"],"names":[],"mappings":"AAcA,0EAA0E;AAC1E,MAAM,UAAU,cAAc,CAAC,KAA+B,EAAE,UAAkC,EAAE;IAClG,MAAM,MAAM,GAAG,CAAI,IAAY,EAAE,IAAsC,EAAgB,EAAE;QACvF,IAAI,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC;QACtH,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;QACzB,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC5C,IAAI,CAAC,KAAK;YAAE,OAAO,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;QAChH,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC,CAAC;IACF,OAAO;QACL,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;QACjF,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;QACpF,YAAY,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;QAC9C,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACxC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACxC,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;QAC1C,UAAU,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;QACzF,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;QAC1C,UAAU,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;QACzF,OAAO,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;KAC1C,CAAC;AACJ,CAAC","sourcesContent":["import type { DnsBackend } from './resolver.ts';\n\nexport interface FakeZone {\n A?: string[];\n AAAA?: string[];\n CNAME?: string[];\n MX?: { exchange: string; priority: number }[];\n NS?: string[];\n TXT?: string[][];\n CAA?: { critical: number; issue?: string; issuewild?: string; iodef?: string }[];\n /** Reverse names, on a zone keyed by the IP address itself. */\n PTR?: string[];\n}\n\n/** Recorded DNS answers for tests; unknown names behave like NXDOMAIN. */\nexport function fakeDnsBackend(zones: Record<string, FakeZone>, options: { timeout?: string[] } = {}): DnsBackend {\n const answer = <T>(name: string, pick: (z: FakeZone) => T[] | undefined): Promise<T[]> => {\n if (options.timeout?.includes(name)) return Promise.reject(Object.assign(new Error('timeout'), { code: 'ETIMEOUT' }));\n const zone = zones[name];\n const value = zone ? pick(zone) : undefined;\n if (!value) return Promise.reject(Object.assign(new Error('nodata'), { code: zone ? 'ENODATA' : 'ENOTFOUND' }));\n return Promise.resolve(value);\n };\n return {\n resolve4: (n) => answer(n, (z) => z.A?.map((address) => ({ address, ttl: 300 }))),\n resolve6: (n) => answer(n, (z) => z.AAAA?.map((address) => ({ address, ttl: 300 }))),\n resolveCname: (n) => answer(n, (z) => z.CNAME),\n resolveMx: (n) => answer(n, (z) => z.MX),\n resolveNs: (n) => answer(n, (z) => z.NS),\n resolveTxt: (n) => answer(n, (z) => z.TXT),\n resolveSoa: () => Promise.reject(Object.assign(new Error('nodata'), { code: 'ENODATA' })),\n resolveCaa: (n) => answer(n, (z) => z.CAA),\n resolveSrv: () => Promise.reject(Object.assign(new Error('nodata'), { code: 'ENODATA' })),\n reverse: (ip) => answer(ip, (z) => z.PTR),\n };\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { normalizeDomain } from './domain.js';
|
|
2
|
+
export { countSpfLookups, dkimKeyInfo, parseDkim, parseDmarc, parseSpf, SPF_LOOKUP_LIMIT, type DkimRecord, type DmarcRecord, type SpfRecord, } from './mail.js';
|
|
3
|
+
export { createDnsHelper, RECORD_TYPES, type DnsBackend, type DnsHelper, type DnsHelperOptions, type DnsRecord, type MailRecords, type RecordType, type TlsCertificateInfo, } from './resolver.js';
|
|
4
|
+
export { fakeDnsBackend, type FakeZone } from './fake.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { normalizeDomain } from "./domain.js";
|
|
2
|
+
export { countSpfLookups, dkimKeyInfo, parseDkim, parseDmarc, parseSpf, SPF_LOOKUP_LIMIT, } from "./mail.js";
|
|
3
|
+
export { createDnsHelper, RECORD_TYPES, } from "./resolver.js";
|
|
4
|
+
export { fakeDnsBackend } from "./fake.js";
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/dns/src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,eAAe,EACf,WAAW,EACX,SAAS,EACT,UAAU,EACV,QAAQ,EACR,gBAAgB,GAIjB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,eAAe,EACf,YAAY,GAQb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,cAAc,EAAiB,MAAM,WAAW,CAAC","sourcesContent":["export { normalizeDomain } from './domain.ts';\nexport {\n countSpfLookups,\n dkimKeyInfo,\n parseDkim,\n parseDmarc,\n parseSpf,\n SPF_LOOKUP_LIMIT,\n type DkimRecord,\n type DmarcRecord,\n type SpfRecord,\n} from './mail.ts';\nexport {\n createDnsHelper,\n RECORD_TYPES,\n type DnsBackend,\n type DnsHelper,\n type DnsHelperOptions,\n type DnsRecord,\n type MailRecords,\n type RecordType,\n type TlsCertificateInfo,\n} from './resolver.ts';\nexport { fakeDnsBackend, type FakeZone } from './fake.ts';\n"]}
|
package/dist/mail.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type DmarcRecord, type Dns, type SpfRecord } from '@x12i/api-hub-core';
|
|
2
|
+
export type { DmarcRecord, SpfRecord };
|
|
3
|
+
export declare function parseSpf(raw: string): SpfRecord | null;
|
|
4
|
+
export declare function parseDmarc(raw: string): DmarcRecord | null;
|
|
5
|
+
/** A DKIM key record (RFC 6376 section 3.6.1). */
|
|
6
|
+
export interface DkimRecord {
|
|
7
|
+
raw: string;
|
|
8
|
+
version: string | null;
|
|
9
|
+
keyType: string;
|
|
10
|
+
/** Base64 public key with whitespace removed; empty means the key is revoked. */
|
|
11
|
+
publicKey: string;
|
|
12
|
+
revoked: boolean;
|
|
13
|
+
hashAlgorithms: string[] | null;
|
|
14
|
+
serviceTypes: string[];
|
|
15
|
+
flags: string[];
|
|
16
|
+
testing: boolean;
|
|
17
|
+
strictSubdomains: boolean;
|
|
18
|
+
notes: string | null;
|
|
19
|
+
/** Problems with the record itself, such as a v= tag that is not first. */
|
|
20
|
+
issues: string[];
|
|
21
|
+
}
|
|
22
|
+
/** Parses a DKIM TXT record; null when it does not look like one (no v=DKIM1 and no p= tag). */
|
|
23
|
+
export declare function parseDkim(raw: string): DkimRecord | null;
|
|
24
|
+
/** Key algorithm and size from a DKIM p= value: SPKI or PKCS#1 DER for RSA, a raw 32-byte key for Ed25519 (RFC 8463). */
|
|
25
|
+
export declare function dkimKeyInfo(base64Key: string, declaredType?: string): {
|
|
26
|
+
keyType: string;
|
|
27
|
+
keyBits: number | null;
|
|
28
|
+
valid: boolean;
|
|
29
|
+
};
|
|
30
|
+
export declare const SPF_LOOKUP_LIMIT = 10;
|
|
31
|
+
/**
|
|
32
|
+
* Counts DNS-querying SPF terms through include and redirect, as receivers do (RFC 7208 section 4.6.4).
|
|
33
|
+
* Stops following includes once the limit is passed; at most 25 TXT queries are made.
|
|
34
|
+
*/
|
|
35
|
+
export declare function countSpfLookups(dns: Dns, record: string): Promise<{
|
|
36
|
+
lookups: number;
|
|
37
|
+
problems: string[];
|
|
38
|
+
}>;
|
package/dist/mail.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createPublicKey } from 'node:crypto';
|
|
2
|
+
import { HubError } from '@x12i/api-hub-core';
|
|
3
|
+
const DNS_MECHANISMS = new Set(['include', 'a', 'mx', 'ptr', 'exists']);
|
|
4
|
+
export function parseSpf(raw) {
|
|
5
|
+
const terms = raw.trim().split(/\s+/);
|
|
6
|
+
if (terms[0]?.toLowerCase() !== 'v=spf1')
|
|
7
|
+
return null;
|
|
8
|
+
const mechanisms = [];
|
|
9
|
+
let lookupCount = 0;
|
|
10
|
+
for (const term of terms.slice(1)) {
|
|
11
|
+
if (term.includes('=') && !term.includes(':')) {
|
|
12
|
+
const [key] = term.split('=');
|
|
13
|
+
if (key?.toLowerCase() === 'redirect')
|
|
14
|
+
lookupCount++;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const qualifier = (['+', '-', '~', '?'].includes(term[0]) ? term[0] : '+');
|
|
18
|
+
const body = ['+', '-', '~', '?'].includes(term[0]) ? term.slice(1) : term;
|
|
19
|
+
const [type, ...rest] = body.split(/[:/]/);
|
|
20
|
+
const lower = (type ?? '').toLowerCase();
|
|
21
|
+
if (DNS_MECHANISMS.has(lower))
|
|
22
|
+
lookupCount++;
|
|
23
|
+
mechanisms.push({ qualifier, type: lower, ...(rest.length ? { value: body.slice(type.length + 1) } : {}) });
|
|
24
|
+
}
|
|
25
|
+
const all = mechanisms.find((m) => m.type === 'all');
|
|
26
|
+
return {
|
|
27
|
+
raw,
|
|
28
|
+
mechanisms,
|
|
29
|
+
all: all ? all.qualifier : null,
|
|
30
|
+
includes: mechanisms.filter((m) => m.type === 'include' && m.value).map((m) => m.value),
|
|
31
|
+
lookupCount,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function parseDmarc(raw) {
|
|
35
|
+
const tags = new Map();
|
|
36
|
+
for (const part of raw.split(';')) {
|
|
37
|
+
const eq = part.indexOf('=');
|
|
38
|
+
if (eq > 0)
|
|
39
|
+
tags.set(part.slice(0, eq).trim().toLowerCase(), part.slice(eq + 1).trim());
|
|
40
|
+
}
|
|
41
|
+
if (tags.get('v')?.toUpperCase() !== 'DMARC1')
|
|
42
|
+
return null;
|
|
43
|
+
const policy = (v) => v && ['none', 'quarantine', 'reject'].includes(v.toLowerCase()) ? v.toLowerCase() : null;
|
|
44
|
+
const list = (v) => (v ? v.split(',').map((s) => s.trim()).filter(Boolean) : []);
|
|
45
|
+
const pct = Number(tags.get('pct') ?? 100);
|
|
46
|
+
return {
|
|
47
|
+
raw,
|
|
48
|
+
policy: policy(tags.get('p')),
|
|
49
|
+
subdomainPolicy: policy(tags.get('sp')),
|
|
50
|
+
pct: Number.isFinite(pct) ? Math.min(Math.max(pct, 0), 100) : 100,
|
|
51
|
+
rua: list(tags.get('rua')),
|
|
52
|
+
ruf: list(tags.get('ruf')),
|
|
53
|
+
adkim: tags.get('adkim')?.toLowerCase() === 's' ? 's' : 'r',
|
|
54
|
+
aspf: tags.get('aspf')?.toLowerCase() === 's' ? 's' : 'r',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const colonList = (v) => (v ? v.split(':').map((s) => s.trim().toLowerCase()).filter(Boolean) : null);
|
|
58
|
+
/** Parses a DKIM TXT record; null when it does not look like one (no v=DKIM1 and no p= tag). */
|
|
59
|
+
export function parseDkim(raw) {
|
|
60
|
+
const tags = new Map();
|
|
61
|
+
const order = [];
|
|
62
|
+
for (const part of raw.split(';')) {
|
|
63
|
+
const eq = part.indexOf('=');
|
|
64
|
+
if (eq <= 0)
|
|
65
|
+
continue;
|
|
66
|
+
const name = part.slice(0, eq).trim().toLowerCase();
|
|
67
|
+
if (!tags.has(name))
|
|
68
|
+
order.push(name);
|
|
69
|
+
tags.set(name, part.slice(eq + 1).trim());
|
|
70
|
+
}
|
|
71
|
+
const version = tags.get('v') ?? null;
|
|
72
|
+
if (version === null && !tags.has('p'))
|
|
73
|
+
return null;
|
|
74
|
+
const issues = [];
|
|
75
|
+
if (version !== null && version !== 'DKIM1')
|
|
76
|
+
issues.push(`Unknown version v=${version}`);
|
|
77
|
+
if (version !== null && order[0] !== 'v')
|
|
78
|
+
issues.push('The v= tag must come first');
|
|
79
|
+
if (!tags.has('p'))
|
|
80
|
+
issues.push('The p= tag is missing');
|
|
81
|
+
const flags = colonList(tags.get('t')) ?? [];
|
|
82
|
+
const publicKey = (tags.get('p') ?? '').replace(/\s+/g, '');
|
|
83
|
+
return {
|
|
84
|
+
raw,
|
|
85
|
+
version,
|
|
86
|
+
keyType: (tags.get('k') ?? 'rsa').toLowerCase(),
|
|
87
|
+
publicKey,
|
|
88
|
+
revoked: tags.has('p') && publicKey === '',
|
|
89
|
+
hashAlgorithms: colonList(tags.get('h')),
|
|
90
|
+
serviceTypes: colonList(tags.get('s')) ?? ['*'],
|
|
91
|
+
flags,
|
|
92
|
+
testing: flags.includes('y'),
|
|
93
|
+
strictSubdomains: flags.includes('s'),
|
|
94
|
+
notes: tags.get('n') ?? null,
|
|
95
|
+
issues,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** Key algorithm and size from a DKIM p= value: SPKI or PKCS#1 DER for RSA, a raw 32-byte key for Ed25519 (RFC 8463). */
|
|
99
|
+
export function dkimKeyInfo(base64Key, declaredType = 'rsa') {
|
|
100
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(base64Key))
|
|
101
|
+
return { keyType: declaredType, keyBits: null, valid: false };
|
|
102
|
+
const der = Buffer.from(base64Key, 'base64');
|
|
103
|
+
if (declaredType === 'ed25519')
|
|
104
|
+
return { keyType: 'ed25519', keyBits: 256, valid: der.length === 32 };
|
|
105
|
+
for (const type of ['spki', 'pkcs1']) {
|
|
106
|
+
try {
|
|
107
|
+
const key = createPublicKey({ key: der, format: 'der', type });
|
|
108
|
+
if (key.asymmetricKeyType === 'ed25519')
|
|
109
|
+
return { keyType: 'ed25519', keyBits: 256, valid: true };
|
|
110
|
+
return { keyType: key.asymmetricKeyType ?? declaredType, keyBits: key.asymmetricKeyDetails?.modulusLength ?? null, valid: true };
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Try the next encoding.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { keyType: declaredType, keyBits: null, valid: false };
|
|
117
|
+
}
|
|
118
|
+
export const SPF_LOOKUP_LIMIT = 10;
|
|
119
|
+
const MAX_SPF_QUERIES = 25;
|
|
120
|
+
async function txtOrEmpty(dns, name) {
|
|
121
|
+
try {
|
|
122
|
+
return (await dns.resolve(name, 'TXT')).map((r) => r.value);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
if (err instanceof HubError && err.code === 'invalid_input')
|
|
126
|
+
return [];
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Counts DNS-querying SPF terms through include and redirect, as receivers do (RFC 7208 section 4.6.4).
|
|
132
|
+
* Stops following includes once the limit is passed; at most 25 TXT queries are made.
|
|
133
|
+
*/
|
|
134
|
+
export async function countSpfLookups(dns, record) {
|
|
135
|
+
let lookups = 0;
|
|
136
|
+
let queries = 0;
|
|
137
|
+
const problems = [];
|
|
138
|
+
const visited = new Set();
|
|
139
|
+
async function walk(raw) {
|
|
140
|
+
const spf = parseSpf(raw);
|
|
141
|
+
if (!spf)
|
|
142
|
+
return;
|
|
143
|
+
lookups += spf.lookupCount;
|
|
144
|
+
const redirect = /(?:^|\s)redirect=(\S+)/i.exec(raw)?.[1];
|
|
145
|
+
for (const target of [...spf.includes, ...(redirect ? [redirect] : [])]) {
|
|
146
|
+
const name = target.toLowerCase();
|
|
147
|
+
if (visited.has(name)) {
|
|
148
|
+
problems.push(`include loop at ${name}`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (lookups > SPF_LOOKUP_LIMIT || queries >= MAX_SPF_QUERIES)
|
|
152
|
+
return;
|
|
153
|
+
visited.add(name);
|
|
154
|
+
queries++;
|
|
155
|
+
const records = (await txtOrEmpty(dns, name)).filter((t) => /^v=spf1(\s|$)/i.test(t));
|
|
156
|
+
if (records.length !== 1) {
|
|
157
|
+
problems.push(records.length === 0 ? `${name} has no SPF record` : `${name} has several SPF records`);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
await walk(records[0]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
await walk(record);
|
|
164
|
+
return { lookups, problems };
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=mail.js.map
|
package/dist/mail.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mail.js","sourceRoot":"","sources":["../../../../../packages/dns/src/mail.ts"],"names":[],"mappings":"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","sourcesContent":["import { createPublicKey } from 'node:crypto';\nimport { HubError, type DmarcRecord, type Dns, type SpfRecord } from '@x12i/api-hub-core';\n\nexport type { DmarcRecord, SpfRecord };\n\nconst DNS_MECHANISMS = new Set(['include', 'a', 'mx', 'ptr', 'exists']);\n\nexport function parseSpf(raw: string): SpfRecord | null {\n const terms = raw.trim().split(/\\s+/);\n if (terms[0]?.toLowerCase() !== 'v=spf1') return null;\n const mechanisms: SpfRecord['mechanisms'] = [];\n let lookupCount = 0;\n for (const term of terms.slice(1)) {\n if (term.includes('=') && !term.includes(':')) {\n const [key] = term.split('=');\n if (key?.toLowerCase() === 'redirect') lookupCount++;\n continue;\n }\n const qualifier = (['+', '-', '~', '?'].includes(term[0]!) ? term[0] : '+') as SpfRecord['all'] & string;\n const body = ['+', '-', '~', '?'].includes(term[0]!) ? term.slice(1) : term;\n const [type, ...rest] = body.split(/[:/]/);\n const lower = (type ?? '').toLowerCase();\n if (DNS_MECHANISMS.has(lower)) lookupCount++;\n mechanisms.push({ qualifier, type: lower, ...(rest.length ? { value: body.slice(type!.length + 1) } : {}) });\n }\n const all = mechanisms.find((m) => m.type === 'all');\n return {\n raw,\n mechanisms,\n all: all ? all.qualifier : null,\n includes: mechanisms.filter((m) => m.type === 'include' && m.value).map((m) => m.value!),\n lookupCount,\n };\n}\n\nexport function parseDmarc(raw: string): DmarcRecord | null {\n const tags = new Map<string, string>();\n for (const part of raw.split(';')) {\n const eq = part.indexOf('=');\n if (eq > 0) tags.set(part.slice(0, eq).trim().toLowerCase(), part.slice(eq + 1).trim());\n }\n if (tags.get('v')?.toUpperCase() !== 'DMARC1') return null;\n const policy = (v: string | undefined) =>\n v && ['none', 'quarantine', 'reject'].includes(v.toLowerCase()) ? (v.toLowerCase() as DmarcRecord['policy']) : null;\n const list = (v: string | undefined) => (v ? v.split(',').map((s) => s.trim()).filter(Boolean) : []);\n const pct = Number(tags.get('pct') ?? 100);\n return {\n raw,\n policy: policy(tags.get('p')),\n subdomainPolicy: policy(tags.get('sp')),\n pct: Number.isFinite(pct) ? Math.min(Math.max(pct, 0), 100) : 100,\n rua: list(tags.get('rua')),\n ruf: list(tags.get('ruf')),\n adkim: tags.get('adkim')?.toLowerCase() === 's' ? 's' : 'r',\n aspf: tags.get('aspf')?.toLowerCase() === 's' ? 's' : 'r',\n };\n}\n\n/** A DKIM key record (RFC 6376 section 3.6.1). */\nexport interface DkimRecord {\n raw: string;\n version: string | null;\n keyType: string;\n /** Base64 public key with whitespace removed; empty means the key is revoked. */\n publicKey: string;\n revoked: boolean;\n hashAlgorithms: string[] | null;\n serviceTypes: string[];\n flags: string[];\n testing: boolean;\n strictSubdomains: boolean;\n notes: string | null;\n /** Problems with the record itself, such as a v= tag that is not first. */\n issues: string[];\n}\n\nconst colonList = (v: string | undefined) => (v ? v.split(':').map((s) => s.trim().toLowerCase()).filter(Boolean) : null);\n\n/** Parses a DKIM TXT record; null when it does not look like one (no v=DKIM1 and no p= tag). */\nexport function parseDkim(raw: string): DkimRecord | null {\n const tags = new Map<string, string>();\n const order: string[] = [];\n for (const part of raw.split(';')) {\n const eq = part.indexOf('=');\n if (eq <= 0) continue;\n const name = part.slice(0, eq).trim().toLowerCase();\n if (!tags.has(name)) order.push(name);\n tags.set(name, part.slice(eq + 1).trim());\n }\n const version = tags.get('v') ?? null;\n if (version === null && !tags.has('p')) return null;\n const issues: string[] = [];\n if (version !== null && version !== 'DKIM1') issues.push(`Unknown version v=${version}`);\n if (version !== null && order[0] !== 'v') issues.push('The v= tag must come first');\n if (!tags.has('p')) issues.push('The p= tag is missing');\n const flags = colonList(tags.get('t')) ?? [];\n const publicKey = (tags.get('p') ?? '').replace(/\\s+/g, '');\n return {\n raw,\n version,\n keyType: (tags.get('k') ?? 'rsa').toLowerCase(),\n publicKey,\n revoked: tags.has('p') && publicKey === '',\n hashAlgorithms: colonList(tags.get('h')),\n serviceTypes: colonList(tags.get('s')) ?? ['*'],\n flags,\n testing: flags.includes('y'),\n strictSubdomains: flags.includes('s'),\n notes: tags.get('n') ?? null,\n issues,\n };\n}\n\n/** Key algorithm and size from a DKIM p= value: SPKI or PKCS#1 DER for RSA, a raw 32-byte key for Ed25519 (RFC 8463). */\nexport function dkimKeyInfo(base64Key: string, declaredType = 'rsa'): { keyType: string; keyBits: number | null; valid: boolean } {\n if (!/^[A-Za-z0-9+/]+={0,2}$/.test(base64Key)) return { keyType: declaredType, keyBits: null, valid: false };\n const der = Buffer.from(base64Key, 'base64');\n if (declaredType === 'ed25519') return { keyType: 'ed25519', keyBits: 256, valid: der.length === 32 };\n for (const type of ['spki', 'pkcs1'] as const) {\n try {\n const key = createPublicKey({ key: der, format: 'der', type });\n if (key.asymmetricKeyType === 'ed25519') return { keyType: 'ed25519', keyBits: 256, valid: true };\n return { keyType: key.asymmetricKeyType ?? declaredType, keyBits: key.asymmetricKeyDetails?.modulusLength ?? null, valid: true };\n } catch {\n // Try the next encoding.\n }\n }\n return { keyType: declaredType, keyBits: null, valid: false };\n}\n\nexport const SPF_LOOKUP_LIMIT = 10;\nconst MAX_SPF_QUERIES = 25;\n\nasync function txtOrEmpty(dns: Dns, name: string): Promise<string[]> {\n try {\n return (await dns.resolve(name, 'TXT')).map((r) => r.value);\n } catch (err) {\n if (err instanceof HubError && err.code === 'invalid_input') return [];\n throw err;\n }\n}\n\n/**\n * Counts DNS-querying SPF terms through include and redirect, as receivers do (RFC 7208 section 4.6.4).\n * Stops following includes once the limit is passed; at most 25 TXT queries are made.\n */\nexport async function countSpfLookups(dns: Dns, record: string): Promise<{ lookups: number; problems: string[] }> {\n let lookups = 0;\n let queries = 0;\n const problems: string[] = [];\n const visited = new Set<string>();\n async function walk(raw: string) {\n const spf = parseSpf(raw);\n if (!spf) return;\n lookups += spf.lookupCount;\n const redirect = /(?:^|\\s)redirect=(\\S+)/i.exec(raw)?.[1];\n for (const target of [...spf.includes, ...(redirect ? [redirect] : [])]) {\n const name = target.toLowerCase();\n if (visited.has(name)) {\n problems.push(`include loop at ${name}`);\n continue;\n }\n if (lookups > SPF_LOOKUP_LIMIT || queries >= MAX_SPF_QUERIES) return;\n visited.add(name);\n queries++;\n const records = (await txtOrEmpty(dns, name)).filter((t) => /^v=spf1(\\s|$)/i.test(t));\n if (records.length !== 1) {\n problems.push(records.length === 0 ? `${name} has no SPF record` : `${name} has several SPF records`);\n continue;\n }\n await walk(records[0]!);\n }\n }\n await walk(record);\n return { lookups, problems };\n}\n"]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type Dns, type DnsRecord, type DnsRecordType, type MailRecords, type TlsCertificateInfo } from '@x12i/api-hub-core';
|
|
2
|
+
export declare const RECORD_TYPES: readonly ["A", "AAAA", "CNAME", "MX", "NS", "TXT", "SOA", "CAA", "SRV"];
|
|
3
|
+
export type RecordType = DnsRecordType;
|
|
4
|
+
export type { DnsRecord, MailRecords, TlsCertificateInfo };
|
|
5
|
+
/** Minimal surface of node:dns Resolver, so tests can inject recorded answers. */
|
|
6
|
+
export interface DnsBackend {
|
|
7
|
+
resolve4(name: string, options: {
|
|
8
|
+
ttl: true;
|
|
9
|
+
}): Promise<{
|
|
10
|
+
address: string;
|
|
11
|
+
ttl: number;
|
|
12
|
+
}[]>;
|
|
13
|
+
resolve6(name: string, options: {
|
|
14
|
+
ttl: true;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
address: string;
|
|
17
|
+
ttl: number;
|
|
18
|
+
}[]>;
|
|
19
|
+
resolveCname(name: string): Promise<string[]>;
|
|
20
|
+
resolveMx(name: string): Promise<{
|
|
21
|
+
exchange: string;
|
|
22
|
+
priority: number;
|
|
23
|
+
}[]>;
|
|
24
|
+
resolveNs(name: string): Promise<string[]>;
|
|
25
|
+
resolveTxt(name: string): Promise<string[][]>;
|
|
26
|
+
resolveSoa(name: string): Promise<{
|
|
27
|
+
nsname: string;
|
|
28
|
+
hostmaster: string;
|
|
29
|
+
serial: number;
|
|
30
|
+
refresh: number;
|
|
31
|
+
retry: number;
|
|
32
|
+
expire: number;
|
|
33
|
+
minttl: number;
|
|
34
|
+
}>;
|
|
35
|
+
resolveCaa(name: string): Promise<{
|
|
36
|
+
critical: number;
|
|
37
|
+
issue?: string;
|
|
38
|
+
issuewild?: string;
|
|
39
|
+
iodef?: string;
|
|
40
|
+
}[]>;
|
|
41
|
+
resolveSrv(name: string): Promise<{
|
|
42
|
+
name: string;
|
|
43
|
+
port: number;
|
|
44
|
+
priority: number;
|
|
45
|
+
weight: number;
|
|
46
|
+
}[]>;
|
|
47
|
+
reverse(ip: string): Promise<string[]>;
|
|
48
|
+
}
|
|
49
|
+
export interface DnsHelperOptions {
|
|
50
|
+
backend?: DnsBackend;
|
|
51
|
+
servers?: string[];
|
|
52
|
+
timeoutMs?: number;
|
|
53
|
+
tries?: number;
|
|
54
|
+
}
|
|
55
|
+
export type DnsHelper = Dns;
|
|
56
|
+
export declare function createDnsHelper(options?: DnsHelperOptions): DnsHelper;
|
package/dist/resolver.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Resolver } from 'node:dns/promises';
|
|
2
|
+
import { isIP } from 'node:net';
|
|
3
|
+
import { connect } from 'node:tls';
|
|
4
|
+
import { DNS_RECORD_TYPES, HubError, } from '@x12i/api-hub-core';
|
|
5
|
+
import { isPublicAddress } from '@x12i/api-hub-safe-fetch';
|
|
6
|
+
import { normalizeDomain } from "./domain.js";
|
|
7
|
+
import { parseDmarc, parseSpf } from "./mail.js";
|
|
8
|
+
export const RECORD_TYPES = DNS_RECORD_TYPES;
|
|
9
|
+
const EMPTY = new Set(['ENODATA', 'ENOTFOUND', 'ENONAME', 'NOTFOUND', 'ENOTIMP']);
|
|
10
|
+
function flat(o) {
|
|
11
|
+
const out = {};
|
|
12
|
+
for (const [k, v] of Object.entries((o ?? {})))
|
|
13
|
+
out[k] = Array.isArray(v) ? v.join(', ') : String(v);
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
export function createDnsHelper(options = {}) {
|
|
17
|
+
const backend = options.backend ??
|
|
18
|
+
(() => {
|
|
19
|
+
const r = new Resolver({ timeout: options.timeoutMs ?? 3_000, tries: options.tries ?? 2 });
|
|
20
|
+
if (options.servers?.length)
|
|
21
|
+
r.setServers(options.servers);
|
|
22
|
+
return r;
|
|
23
|
+
})();
|
|
24
|
+
async function safe(fn, empty) {
|
|
25
|
+
try {
|
|
26
|
+
return await fn();
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
const code = err.code ?? '';
|
|
30
|
+
if (EMPTY.has(code))
|
|
31
|
+
return empty;
|
|
32
|
+
if (code === 'ETIMEOUT')
|
|
33
|
+
throw new HubError('timeout', 'DNS lookup timed out');
|
|
34
|
+
throw new HubError('upstream_unavailable', 'DNS lookup failed', { reason: code || 'unknown' });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function resolve(rawName, type) {
|
|
38
|
+
const name = normalizeDomain(rawName);
|
|
39
|
+
switch (type) {
|
|
40
|
+
case 'A':
|
|
41
|
+
return (await safe(() => backend.resolve4(name, { ttl: true }), [])).map((r) => ({ type, value: r.address, ttl: r.ttl }));
|
|
42
|
+
case 'AAAA':
|
|
43
|
+
return (await safe(() => backend.resolve6(name, { ttl: true }), [])).map((r) => ({ type, value: r.address, ttl: r.ttl }));
|
|
44
|
+
case 'CNAME':
|
|
45
|
+
return (await safe(() => backend.resolveCname(name), [])).map((value) => ({ type, value }));
|
|
46
|
+
case 'MX':
|
|
47
|
+
return (await safe(() => backend.resolveMx(name), []))
|
|
48
|
+
.sort((a, b) => a.priority - b.priority)
|
|
49
|
+
.map((r) => ({ type, value: r.exchange, priority: r.priority }));
|
|
50
|
+
case 'NS':
|
|
51
|
+
return (await safe(() => backend.resolveNs(name), [])).map((value) => ({ type, value }));
|
|
52
|
+
case 'TXT':
|
|
53
|
+
return (await safe(() => backend.resolveTxt(name), [])).map((parts) => ({ type, value: parts.join('') }));
|
|
54
|
+
case 'SOA': {
|
|
55
|
+
const soa = await safe(() => backend.resolveSoa(name), null);
|
|
56
|
+
return soa
|
|
57
|
+
? [{ type, value: `${soa.nsname} ${soa.hostmaster} ${soa.serial} ${soa.refresh} ${soa.retry} ${soa.expire} ${soa.minttl}` }]
|
|
58
|
+
: [];
|
|
59
|
+
}
|
|
60
|
+
case 'CAA':
|
|
61
|
+
return (await safe(() => backend.resolveCaa(name), [])).map((r) => {
|
|
62
|
+
const [tag, value] = r.issue !== undefined ? ['issue', r.issue] : r.issuewild !== undefined ? ['issuewild', r.issuewild] : ['iodef', r.iodef ?? ''];
|
|
63
|
+
return { type, value: `${r.critical} ${tag} "${value}"` };
|
|
64
|
+
});
|
|
65
|
+
case 'SRV':
|
|
66
|
+
return (await safe(() => backend.resolveSrv(name), [])).map((r) => ({
|
|
67
|
+
type,
|
|
68
|
+
value: `${r.weight} ${r.port} ${r.name}`,
|
|
69
|
+
priority: r.priority,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function mailRecords(rawDomain) {
|
|
74
|
+
const domain = normalizeDomain(rawDomain);
|
|
75
|
+
const [mx, txt, dmarcTxt] = await Promise.all([
|
|
76
|
+
safe(() => backend.resolveMx(domain), []),
|
|
77
|
+
safe(() => backend.resolveTxt(domain), []),
|
|
78
|
+
safe(() => backend.resolveTxt(`_dmarc.${domain}`), []),
|
|
79
|
+
]);
|
|
80
|
+
const spfTexts = txt.map((p) => p.join('')).filter((t) => /^v=spf1(\s|$)/i.test(t));
|
|
81
|
+
const dmarcTexts = dmarcTxt.map((p) => p.join('')).filter((t) => /^v=DMARC1/i.test(t));
|
|
82
|
+
const sorted = [...mx].sort((a, b) => a.priority - b.priority);
|
|
83
|
+
return {
|
|
84
|
+
domain,
|
|
85
|
+
mx: sorted,
|
|
86
|
+
nullMx: sorted.length === 1 && (sorted[0].exchange === '' || sorted[0].exchange === '.'),
|
|
87
|
+
spf: spfTexts.length === 1 ? parseSpf(spfTexts[0]) : null,
|
|
88
|
+
spfRecords: spfTexts.length,
|
|
89
|
+
dmarc: dmarcTexts.length === 1 ? parseDmarc(dmarcTexts[0]) : null,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function reverse(ip) {
|
|
93
|
+
const address = ip.trim();
|
|
94
|
+
if (!isIP(address))
|
|
95
|
+
throw new HubError('invalid_input', 'Expected an IPv4 or IPv6 address');
|
|
96
|
+
if (!isPublicAddress(address))
|
|
97
|
+
throw new HubError('invalid_input', 'The address is not public');
|
|
98
|
+
return (await safe(() => backend.reverse(address), [])).map((n) => n.toLowerCase().replace(/\.$/, ''));
|
|
99
|
+
}
|
|
100
|
+
async function tlsCertificate(rawHost, port = 443, timeoutMs = 8_000) {
|
|
101
|
+
const host = normalizeDomain(rawHost);
|
|
102
|
+
if (![443, 465, 587, 636, 853, 993, 995, 8443].includes(port))
|
|
103
|
+
throw new HubError('invalid_input', `Port ${port} is not allowed`);
|
|
104
|
+
const addrs = [
|
|
105
|
+
...(await safe(() => backend.resolve4(host, { ttl: true }), [])),
|
|
106
|
+
...(await safe(() => backend.resolve6(host, { ttl: true }), [])),
|
|
107
|
+
].map((r) => r.address);
|
|
108
|
+
if (addrs.length === 0)
|
|
109
|
+
throw new HubError('invalid_input', `Could not resolve host ${host}`);
|
|
110
|
+
if (!addrs.every(isPublicAddress))
|
|
111
|
+
throw new HubError('invalid_input', 'The host resolves to a non-public address');
|
|
112
|
+
const address = addrs[0];
|
|
113
|
+
return new Promise((resolvePromise, reject) => {
|
|
114
|
+
const socket = connect({ host: address, port, servername: host, rejectUnauthorized: false, timeout: timeoutMs });
|
|
115
|
+
const fail = (err) => {
|
|
116
|
+
socket.destroy();
|
|
117
|
+
reject(err);
|
|
118
|
+
};
|
|
119
|
+
socket.setTimeout(timeoutMs, () => fail(new HubError('timeout', 'TLS handshake timed out')));
|
|
120
|
+
socket.once('error', () => fail(new HubError('upstream_unavailable', 'Could not complete a TLS handshake')));
|
|
121
|
+
socket.once('secureConnect', () => {
|
|
122
|
+
const cert = socket.getPeerCertificate(false);
|
|
123
|
+
if (!cert || Object.keys(cert).length === 0)
|
|
124
|
+
return fail(new HubError('upstream_unavailable', 'No certificate presented'));
|
|
125
|
+
const validTo = new Date(cert.valid_to);
|
|
126
|
+
const info = {
|
|
127
|
+
host,
|
|
128
|
+
port,
|
|
129
|
+
remoteAddress: address,
|
|
130
|
+
authorized: socket.authorized,
|
|
131
|
+
authorizationError: socket.authorizationError ? String(socket.authorizationError) : null,
|
|
132
|
+
protocol: socket.getProtocol(),
|
|
133
|
+
subject: flat(cert.subject),
|
|
134
|
+
issuer: flat(cert.issuer),
|
|
135
|
+
subjectAltNames: (cert.subjectaltname ?? '')
|
|
136
|
+
.split(',')
|
|
137
|
+
.map((s) => s.trim().replace(/^DNS:/, ''))
|
|
138
|
+
.filter(Boolean),
|
|
139
|
+
validFrom: new Date(cert.valid_from).toISOString(),
|
|
140
|
+
validTo: validTo.toISOString(),
|
|
141
|
+
daysRemaining: Math.floor((validTo.getTime() - Date.now()) / 86_400_000),
|
|
142
|
+
serialNumber: cert.serialNumber,
|
|
143
|
+
fingerprint256: cert.fingerprint256,
|
|
144
|
+
};
|
|
145
|
+
socket.end();
|
|
146
|
+
resolvePromise(info);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return { resolve, mailRecords, reverse, tlsCertificate };
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver.js","sourceRoot":"","sources":["../../../../../packages/dns/src/resolver.ts"],"names":[],"mappings":"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","sourcesContent":["import { Resolver } from 'node:dns/promises';\nimport { isIP } from 'node:net';\nimport { connect, type PeerCertificate } from 'node:tls';\nimport {\n DNS_RECORD_TYPES,\n HubError,\n type Dns,\n type DnsRecord,\n type DnsRecordType,\n type MailRecords,\n type TlsCertificateInfo,\n} from '@x12i/api-hub-core';\nimport { isPublicAddress } from '@x12i/api-hub-safe-fetch';\nimport { normalizeDomain } from './domain.ts';\nimport { parseDmarc, parseSpf } from './mail.ts';\n\nexport const RECORD_TYPES = DNS_RECORD_TYPES;\nexport type RecordType = DnsRecordType;\nexport type { DnsRecord, MailRecords, TlsCertificateInfo };\n\n/** Minimal surface of node:dns Resolver, so tests can inject recorded answers. */\nexport interface DnsBackend {\n resolve4(name: string, options: { ttl: true }): Promise<{ address: string; ttl: number }[]>;\n resolve6(name: string, options: { ttl: true }): Promise<{ address: string; ttl: number }[]>;\n resolveCname(name: string): Promise<string[]>;\n resolveMx(name: string): Promise<{ exchange: string; priority: number }[]>;\n resolveNs(name: string): Promise<string[]>;\n resolveTxt(name: string): Promise<string[][]>;\n resolveSoa(name: string): Promise<{ nsname: string; hostmaster: string; serial: number; refresh: number; retry: number; expire: number; minttl: number }>;\n resolveCaa(name: string): Promise<{ critical: number; issue?: string; issuewild?: string; iodef?: string }[]>;\n resolveSrv(name: string): Promise<{ name: string; port: number; priority: number; weight: number }[]>;\n reverse(ip: string): Promise<string[]>;\n}\n\nexport interface DnsHelperOptions {\n backend?: DnsBackend;\n servers?: string[];\n timeoutMs?: number;\n tries?: number;\n}\n\nconst EMPTY = new Set(['ENODATA', 'ENOTFOUND', 'ENONAME', 'NOTFOUND', 'ENOTIMP']);\n\nexport type DnsHelper = Dns;\n\nfunction flat(o: unknown): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries((o ?? {}) as Record<string, unknown>)) out[k] = Array.isArray(v) ? v.join(', ') : String(v);\n return out;\n}\n\nexport function createDnsHelper(options: DnsHelperOptions = {}): DnsHelper {\n const backend: DnsBackend =\n options.backend ??\n (() => {\n const r = new Resolver({ timeout: options.timeoutMs ?? 3_000, tries: options.tries ?? 2 });\n if (options.servers?.length) r.setServers(options.servers);\n return r as unknown as DnsBackend;\n })();\n\n async function safe<T>(fn: () => Promise<T>, empty: T): Promise<T> {\n try {\n return await fn();\n } catch (err) {\n const code = (err as { code?: string }).code ?? '';\n if (EMPTY.has(code)) return empty;\n if (code === 'ETIMEOUT') throw new HubError('timeout', 'DNS lookup timed out');\n throw new HubError('upstream_unavailable', 'DNS lookup failed', { reason: code || 'unknown' });\n }\n }\n\n async function resolve(rawName: string, type: RecordType): Promise<DnsRecord[]> {\n const name = normalizeDomain(rawName);\n switch (type) {\n case 'A':\n return (await safe(() => backend.resolve4(name, { ttl: true }), [])).map((r) => ({ type, value: r.address, ttl: r.ttl }));\n case 'AAAA':\n return (await safe(() => backend.resolve6(name, { ttl: true }), [])).map((r) => ({ type, value: r.address, ttl: r.ttl }));\n case 'CNAME':\n return (await safe(() => backend.resolveCname(name), [])).map((value) => ({ type, value }));\n case 'MX':\n return (await safe(() => backend.resolveMx(name), []))\n .sort((a, b) => a.priority - b.priority)\n .map((r) => ({ type, value: r.exchange, priority: r.priority }));\n case 'NS':\n return (await safe(() => backend.resolveNs(name), [])).map((value) => ({ type, value }));\n case 'TXT':\n return (await safe(() => backend.resolveTxt(name), [])).map((parts) => ({ type, value: parts.join('') }));\n case 'SOA': {\n const soa = await safe(() => backend.resolveSoa(name), null);\n return soa\n ? [{ type, value: `${soa.nsname} ${soa.hostmaster} ${soa.serial} ${soa.refresh} ${soa.retry} ${soa.expire} ${soa.minttl}` }]\n : [];\n }\n case 'CAA':\n return (await safe(() => backend.resolveCaa(name), [])).map((r) => {\n const [tag, value] = r.issue !== undefined ? ['issue', r.issue] : r.issuewild !== undefined ? ['issuewild', r.issuewild] : ['iodef', r.iodef ?? ''];\n return { type, value: `${r.critical} ${tag} \"${value}\"` };\n });\n case 'SRV':\n return (await safe(() => backend.resolveSrv(name), [])).map((r) => ({\n type,\n value: `${r.weight} ${r.port} ${r.name}`,\n priority: r.priority,\n }));\n }\n }\n\n async function mailRecords(rawDomain: string): Promise<MailRecords> {\n const domain = normalizeDomain(rawDomain);\n const [mx, txt, dmarcTxt] = await Promise.all([\n safe(() => backend.resolveMx(domain), []),\n safe(() => backend.resolveTxt(domain), []),\n safe(() => backend.resolveTxt(`_dmarc.${domain}`), []),\n ]);\n const spfTexts = txt.map((p) => p.join('')).filter((t) => /^v=spf1(\\s|$)/i.test(t));\n const dmarcTexts = dmarcTxt.map((p) => p.join('')).filter((t) => /^v=DMARC1/i.test(t));\n const sorted = [...mx].sort((a, b) => a.priority - b.priority);\n return {\n domain,\n mx: sorted,\n nullMx: sorted.length === 1 && (sorted[0]!.exchange === '' || sorted[0]!.exchange === '.'),\n spf: spfTexts.length === 1 ? parseSpf(spfTexts[0]!) : null,\n spfRecords: spfTexts.length,\n dmarc: dmarcTexts.length === 1 ? parseDmarc(dmarcTexts[0]!) : null,\n };\n }\n\n async function reverse(ip: string): Promise<string[]> {\n const address = ip.trim();\n if (!isIP(address)) throw new HubError('invalid_input', 'Expected an IPv4 or IPv6 address');\n if (!isPublicAddress(address)) throw new HubError('invalid_input', 'The address is not public');\n return (await safe(() => backend.reverse(address), [])).map((n) => n.toLowerCase().replace(/\\.$/, ''));\n }\n\n async function tlsCertificate(rawHost: string, port = 443, timeoutMs = 8_000): Promise<TlsCertificateInfo> {\n const host = normalizeDomain(rawHost);\n if (![443, 465, 587, 636, 853, 993, 995, 8443].includes(port)) throw new HubError('invalid_input', `Port ${port} is not allowed`);\n const addrs = [\n ...(await safe(() => backend.resolve4(host, { ttl: true }), [])),\n ...(await safe(() => backend.resolve6(host, { ttl: true }), [])),\n ].map((r) => r.address);\n if (addrs.length === 0) throw new HubError('invalid_input', `Could not resolve host ${host}`);\n if (!addrs.every(isPublicAddress)) throw new HubError('invalid_input', 'The host resolves to a non-public address');\n const address = addrs[0]!;\n\n return new Promise<TlsCertificateInfo>((resolvePromise, reject) => {\n const socket = connect({ host: address, port, servername: host, rejectUnauthorized: false, timeout: timeoutMs });\n const fail = (err: HubError) => {\n socket.destroy();\n reject(err);\n };\n socket.setTimeout(timeoutMs, () => fail(new HubError('timeout', 'TLS handshake timed out')));\n socket.once('error', () => fail(new HubError('upstream_unavailable', 'Could not complete a TLS handshake')));\n socket.once('secureConnect', () => {\n const cert = socket.getPeerCertificate(false) as PeerCertificate;\n if (!cert || Object.keys(cert).length === 0) return fail(new HubError('upstream_unavailable', 'No certificate presented'));\n const validTo = new Date(cert.valid_to);\n const info: TlsCertificateInfo = {\n host,\n port,\n remoteAddress: address,\n authorized: socket.authorized,\n authorizationError: socket.authorizationError ? String(socket.authorizationError) : null,\n protocol: socket.getProtocol(),\n subject: flat(cert.subject),\n issuer: flat(cert.issuer),\n subjectAltNames: (cert.subjectaltname ?? '')\n .split(',')\n .map((s) => s.trim().replace(/^DNS:/, ''))\n .filter(Boolean),\n validFrom: new Date(cert.valid_from).toISOString(),\n validTo: validTo.toISOString(),\n daysRemaining: Math.floor((validTo.getTime() - Date.now()) / 86_400_000),\n serialNumber: cert.serialNumber,\n fingerprint256: cert.fingerprint256,\n };\n socket.end();\n resolvePromise(info);\n });\n });\n }\n\n return { resolve, mailRecords, reverse, tlsCertificate };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x12i/api-hub-dns",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Public DNS, mail-record, and TLS lookup helpers for the x12i API hub",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=24"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/x12i/apis-hub.git",
|
|
24
|
+
"directory": "packages/dns"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/x12i/apis-hub/issues"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/x12i/apis-hub",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@x12i/api-hub-core": "0.1.0",
|
|
35
|
+
"@x12i/api-hub-safe-fetch": "0.1.0"
|
|
36
|
+
}
|
|
37
|
+
}
|