dns-doctor 0.2.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/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # dns-doctor — DNS Doctor from the terminal
2
+
3
+ Scan, fix and verify a domain's DNS without leaving the shell: email
4
+ authentication (SPF, DMARC, DKIM), MX, DNS health, blacklists, domain/SSL expiry
5
+ and propagation read from six locations on four continents. Every fix record
6
+ comes from a deterministic, validating engine — RFC grammar plus the SPF
7
+ 10-lookup counter — **never an AI guess**, and a human approves every DNS change.
8
+
9
+ ```
10
+ npx dns-doctor scan example.com
11
+ ```
12
+
13
+ No account, no key. The CLI calls the public DNS Doctor API
14
+ (https://dnsdoctor.dev) and prints what it returns.
15
+
16
+ ## Commands
17
+
18
+ Every tool the DNS Doctor MCP server exposes has a command here.
19
+
20
+ ```
21
+ scan & fix
22
+ dns-doctor scan <domain> [--fresh] the seven-check report (persisted, or fresh on a miss)
23
+ dns-doctor dmarc-upgrade <domain> the next safe DMARC step as a record, or why there is none
24
+ dns-doctor parked <domain> --confirm-no-mail the hardening pack for a domain that sends no mail
25
+ dns-doctor signup-url <domain> a sign-up link to hand the domain's owner for monitoring
26
+
27
+ records
28
+ dns-doctor dmarc-validate "<record>" every tag checked, with the safe next step
29
+ dns-doctor dmarc-generate <none|quarantine|reject> [--rua <email>] [--subdomain-policy <p>] [--strict]
30
+ dns-doctor spf-count <domain | "v=spf1 …"> DNS lookups against the 10-lookup limit
31
+ dns-doctor spf-audit <domain> the SPF include tree with registration and lookup findings
32
+ dns-doctor dkim <domain> --selector <s> a DKIM selector's published key
33
+ dns-doctor report-parse <file.xml|.gz|.zip> one DMARC aggregate report as a table
34
+
35
+ live DNS
36
+ dns-doctor record <domain> --kind <spf|dmarc|txt|mx|cname|a|aaaa> [--host <label>]
37
+ dns-doctor propagation <name> [--type A] [--expect <value>]
38
+ dns-doctor reverse-dns <ip>
39
+
40
+ monitoring (DNSDOCTOR_API_TOKEN required)
41
+ dns-doctor alerts [--domain d] [--since t] [--before t] [--alert-type t] [--limit n]
42
+ dns-doctor readiness <domain>
43
+ ```
44
+
45
+ Add `--json` to any command to print the API response verbatim — the path for
46
+ scripts and agents.
47
+
48
+ ## Exit codes
49
+
50
+ | Code | Meaning |
51
+ |---|---|
52
+ | 0 | nothing failing |
53
+ | 1 | a failing verdict (a failed check, an invalid or over-limit record, a name not propagated, a reverse name that does not point back, nameservers that disagree) |
54
+ | 2 | an error — including every transient: rate limit, a paid-retry offer (HTTP 402), an outage. **Never read 2 as a verdict about the domain.** |
55
+
56
+ ## Example
57
+
58
+ ```
59
+ $ dns-doctor scan example.com
60
+ example.com (scanned 2026-09-06T10:12:03Z)
61
+ FAIL dmarc No DMARC record found — receivers can't verify your mail
62
+ fix (paste verbatim, a human must approve the DNS change):
63
+ v=DMARC1; p=none; np=reject
64
+ WARN dkim No DKIM key found at the common selectors
65
+ PASS spf SPF record is valid
66
+
67
+ report: https://dnsdoctor.dev/scan/example.com
68
+ ```
69
+
70
+ The record printed under `fix` is the API's bytes. The CLI never composes,
71
+ reformats or "improves" a record; SPF is diagnose-only by design (an SPF edit
72
+ can silently de-authorise a real sender), so no SPF record is ever proposed.
73
+
74
+ ## Environment
75
+
76
+ - `DNSDOCTOR_API_TOKEN` — optional bearer token (mint one in the DNS Doctor
77
+ dashboard); raises the anonymous budget. Never required for the commands above.
78
+ - `DNSDOCTOR_API_BASE` — override the API origin for local runs.
79
+
80
+ Past the free per-caller allowance the API answers HTTP 402 with an x402 offer
81
+ (USDC on Base). This CLI holds no wallet: it reports the offer and exits 2. Pay
82
+ with an x402 client or wait and retry.
83
+
84
+ ## Also
85
+
86
+ - Hosted MCP server: `https://dnsdoctor.dev/mcp` · stdio client: `npx -y @dnsdoctor/mcp`
87
+ - Methodology: https://dnsdoctor.dev/methodology · API: https://dnsdoctor.dev/openapi.json
88
+
89
+ Apache-2.0.
package/dist/api.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The CLI's request layer — the same two rules as the `@dnsdoctor/mcp` client:
3
+ *
4
+ * 1. **Relay, never compose.** Every response is returned untouched; the
5
+ * record strings a command prints are the API's bytes.
6
+ * 2. **A transport failure is never a verdict.** 429/402/5xx/network faults are
7
+ * `transient` errors and exit 2 — a script must never read them as "the
8
+ * domain failed".
9
+ *
10
+ * Deliberately a small copy of the MCP client's layer rather than an import:
11
+ * the client's `package.json` version is a pinned carrier across every
12
+ * published artifact, and a CLI release must not force that wave. The backend
13
+ * test `tests/mcp_server/test_cli_routes.py` pins this package's `ROUTES` to
14
+ * the real app so the two layers cannot drift onto different paths.
15
+ */
16
+ export declare const DEFAULT_API_BASE = "https://dnsdoctor.dev";
17
+ export declare const TRANSIENT_SUFFIX = "retry; this is not a verdict about the domain";
18
+ export declare const RATE_LIMITED_MESSAGE = "rate limited \u2014 slow down and retry";
19
+ export declare const PAYMENT_REQUIRED_MESSAGE = "past the free per-caller allowance \u2014 the API offered a paid retry over x402 (USDC on Base); pay with an x402 client, or wait and retry";
20
+ export declare class ApiError extends Error {
21
+ readonly status: number | null;
22
+ readonly transient: boolean;
23
+ constructor(message: string, status: number | null, transient: boolean);
24
+ }
25
+ /** Base URL for the DNS Doctor API; `DNSDOCTOR_API_BASE` overrides for local runs. */
26
+ export declare function apiBase(): string;
27
+ export declare function packageVersion(): string;
28
+ /**
29
+ * The UA every call carries — this CLI's only attribution. `api/agents.py::
30
+ * agent_family` is an allowlist, and `dnsdoctor-cli` is its entry: without
31
+ * this marker the CLI channel would be invisible in the agent-request metric.
32
+ */
33
+ export declare function userAgent(): string;
34
+ export declare function toApiError(response: Response): Promise<ApiError>;
35
+ /** GET a JSON resource. `path` is API-root-relative and already encoded. */
36
+ export declare function getJson(path: string): Promise<unknown>;
37
+ /** POST a JSON body and return the parsed response, untouched. */
38
+ export declare function postJson(path: string, body: unknown): Promise<unknown>;
39
+ /**
40
+ * POST one file as multipart (`dmarc-report-parse`). The API reads bytes only;
41
+ * the filename is cosmetic. Node ≥ 20 ships FormData/Blob natively.
42
+ */
43
+ export declare function postFile(path: string, field: string, bytes: Uint8Array, filename: string): Promise<unknown>;
package/dist/api.js ADDED
@@ -0,0 +1,142 @@
1
+ /**
2
+ * The CLI's request layer — the same two rules as the `@dnsdoctor/mcp` client:
3
+ *
4
+ * 1. **Relay, never compose.** Every response is returned untouched; the
5
+ * record strings a command prints are the API's bytes.
6
+ * 2. **A transport failure is never a verdict.** 429/402/5xx/network faults are
7
+ * `transient` errors and exit 2 — a script must never read them as "the
8
+ * domain failed".
9
+ *
10
+ * Deliberately a small copy of the MCP client's layer rather than an import:
11
+ * the client's `package.json` version is a pinned carrier across every
12
+ * published artifact, and a CLI release must not force that wave. The backend
13
+ * test `tests/mcp_server/test_cli_routes.py` pins this package's `ROUTES` to
14
+ * the real app so the two layers cannot drift onto different paths.
15
+ */
16
+ import { readFileSync } from "node:fs";
17
+ export const DEFAULT_API_BASE = "https://dnsdoctor.dev";
18
+ export const TRANSIENT_SUFFIX = "retry; this is not a verdict about the domain";
19
+ export const RATE_LIMITED_MESSAGE = "rate limited — slow down and retry";
20
+ export const PAYMENT_REQUIRED_MESSAGE = "past the free per-caller allowance — the API offered a paid retry over x402 (USDC on Base); pay with an x402 client, or wait and retry";
21
+ export class ApiError extends Error {
22
+ status;
23
+ transient;
24
+ constructor(message, status, transient) {
25
+ super(message);
26
+ this.status = status;
27
+ this.transient = transient;
28
+ this.name = "ApiError";
29
+ }
30
+ }
31
+ /** Base URL for the DNS Doctor API; `DNSDOCTOR_API_BASE` overrides for local runs. */
32
+ export function apiBase() {
33
+ const raw = process.env.DNSDOCTOR_API_BASE?.trim();
34
+ const base = raw ? raw : DEFAULT_API_BASE;
35
+ return base.replace(/\/+$/, "");
36
+ }
37
+ /** A bearer token raises the anonymous budget; never prompted for, never required. */
38
+ function authHeaders() {
39
+ const token = process.env.DNSDOCTOR_API_TOKEN?.trim();
40
+ return token ? { Authorization: `Bearer ${token}` } : {};
41
+ }
42
+ export function packageVersion() {
43
+ try {
44
+ const raw = readFileSync(new URL("../package.json", import.meta.url), "utf8");
45
+ const version = JSON.parse(raw).version;
46
+ return typeof version === "string" ? version : "0.0.0";
47
+ }
48
+ catch {
49
+ return "0.0.0";
50
+ }
51
+ }
52
+ /**
53
+ * The UA every call carries — this CLI's only attribution. `api/agents.py::
54
+ * agent_family` is an allowlist, and `dnsdoctor-cli` is its entry: without
55
+ * this marker the CLI channel would be invisible in the agent-request metric.
56
+ */
57
+ export function userAgent() {
58
+ return `dnsdoctor-cli/${packageVersion()} (+https://dnsdoctor.dev)`;
59
+ }
60
+ function detailOf(body) {
61
+ if (typeof body !== "object" || body === null)
62
+ return null;
63
+ const detail = body.detail;
64
+ if (typeof detail === "string")
65
+ return detail;
66
+ if (detail === undefined || detail === null)
67
+ return null;
68
+ return JSON.stringify(detail);
69
+ }
70
+ async function readBody(response) {
71
+ try {
72
+ return await response.json();
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
78
+ export async function toApiError(response) {
79
+ const detail = detailOf(await readBody(response));
80
+ const status = response.status;
81
+ if (status === 429) {
82
+ return new ApiError(detail ? `${RATE_LIMITED_MESSAGE} (${detail})` : RATE_LIMITED_MESSAGE, status, true);
83
+ }
84
+ if (status === 402)
85
+ return new ApiError(PAYMENT_REQUIRED_MESSAGE, status, true);
86
+ if (status === 503) {
87
+ return new ApiError(`DNS Doctor could not complete the lookup (transient) — ${TRANSIENT_SUFFIX}`, status, true);
88
+ }
89
+ if (status >= 500) {
90
+ return new ApiError(`DNS Doctor returned a server error (HTTP ${status}) — ${TRANSIENT_SUFFIX}`, status, true);
91
+ }
92
+ // 4xx — the API's own `detail` is the answer (422 malformed input, opaque 404). Verbatim.
93
+ return new ApiError(detail ?? `DNS Doctor request failed (HTTP ${status})`, status, false);
94
+ }
95
+ async function send(path, init) {
96
+ const url = `${apiBase()}${path}`;
97
+ let response;
98
+ try {
99
+ response = await fetch(url, {
100
+ ...init,
101
+ headers: {
102
+ Accept: "application/json",
103
+ "User-Agent": userAgent(),
104
+ ...authHeaders(),
105
+ ...(init.headers ?? {}),
106
+ },
107
+ });
108
+ }
109
+ catch (error) {
110
+ const cause = error instanceof Error ? error.message : String(error);
111
+ throw new ApiError(`Could not reach the DNS Doctor API (${cause}) — ${TRANSIENT_SUFFIX}`, null, true);
112
+ }
113
+ if (!response.ok)
114
+ throw await toApiError(response);
115
+ const body = await readBody(response);
116
+ if (body === null) {
117
+ throw new ApiError(`DNS Doctor returned an unreadable response (HTTP ${response.status}) — ${TRANSIENT_SUFFIX}`, response.status, true);
118
+ }
119
+ return body;
120
+ }
121
+ /** GET a JSON resource. `path` is API-root-relative and already encoded. */
122
+ export function getJson(path) {
123
+ return send(path, { method: "GET" });
124
+ }
125
+ /** POST a JSON body and return the parsed response, untouched. */
126
+ export function postJson(path, body) {
127
+ return send(path, {
128
+ method: "POST",
129
+ headers: { "Content-Type": "application/json" },
130
+ body: JSON.stringify(body),
131
+ });
132
+ }
133
+ /**
134
+ * POST one file as multipart (`dmarc-report-parse`). The API reads bytes only;
135
+ * the filename is cosmetic. Node ≥ 20 ships FormData/Blob natively.
136
+ */
137
+ export function postFile(path, field, bytes, filename) {
138
+ const form = new FormData();
139
+ form.append(field, new Blob([bytes], { type: "application/octet-stream" }), filename);
140
+ return send(path, { method: "POST", body: form });
141
+ }
142
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Human output. Everything printed here is read off the API response:
3
+ * statuses, titles, record strings (verbatim, never reformatted), URLs.
4
+ *
5
+ * Ordering is the one editorial act — failing checks first, the way the report
6
+ * page reads — and the exit code is a pure function of the API's own verdicts.
7
+ */
8
+ /** `scan`: the `ScanReport` (+ `next_steps`) as a worst-first check list. */
9
+ export declare function formatReport(body: unknown): string;
10
+ /** `propagation`: verdict, coverage, one line per location. */
11
+ export declare function formatPropagation(body: unknown): string;
12
+ /** `dmarc-upgrade`: the record verbatim when there is one, else the rationale. */
13
+ export declare function formatDmarcUpgrade(body: unknown): string;
14
+ /** `reverse-dns`: verdict, PTR, the addresses the name resolves to. */
15
+ export declare function formatReverseDns(body: unknown): string;
16
+ /** `spf-audit`: the walk's findings, then the lookup total. */
17
+ export declare function formatSpfAudit(body: unknown): string;
18
+ /** `spf-count`: the count against the limit, the findings, the record. */
19
+ export declare function formatSpfCount(body: unknown): string;
20
+ /** `dmarc-validate` / `dmarc-generate`: validity, findings, the record(s) verbatim. */
21
+ export declare function formatDmarcRecord(body: unknown): string;
22
+ /** `dkim`: the status line, the parsed key when present, the record verbatim. */
23
+ export declare function formatDkim(body: unknown): string;
24
+ /** `report-parse`: the report's totals and one line per source. */
25
+ export declare function formatReportParse(body: unknown): string;
26
+ /** `record`: the authoritative answer beside what the caches still hold. */
27
+ export declare function formatRecord(body: unknown): string;
28
+ /** `parked`: the pack verbatim, or the refusal and its rationale. */
29
+ export declare function formatParked(body: unknown): string;
30
+ /** `signup-url`: the two links verbatim and the message. */
31
+ export declare function formatSignupUrl(body: unknown): string;
32
+ /** `alerts`: one line per alert, newest first as the API orders them. */
33
+ export declare function formatAlerts(body: unknown): string;
34
+ /** `readiness`: ready or not, the blockers, the next record verbatim. */
35
+ export declare function formatReadiness(body: unknown): string;
36
+ export declare function render(command: string, body: unknown): string;
37
+ /**
38
+ * Exit code from the API's own verdicts: 0 = nothing failing, 1 = a failing
39
+ * verdict. Transient errors never reach here (they exit 2 in `main`).
40
+ */
41
+ export declare function exitCodeFor(command: string, body: unknown): 0 | 1;
package/dist/format.js ADDED
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Human output. Everything printed here is read off the API response:
3
+ * statuses, titles, record strings (verbatim, never reformatted), URLs.
4
+ *
5
+ * Ordering is the one editorial act — failing checks first, the way the report
6
+ * page reads — and the exit code is a pure function of the API's own verdicts.
7
+ */
8
+ const STATUS_ORDER = { fail: 0, warn: 1, temperror: 2, info: 3, pass: 4 };
9
+ const STATUS_WORD = {
10
+ fail: "FAIL",
11
+ warn: "WARN",
12
+ temperror: "TEMP",
13
+ info: "INFO",
14
+ pass: "PASS",
15
+ };
16
+ const RECORD_NOTE = "paste verbatim; a human must approve the DNS change";
17
+ function str(value) {
18
+ return typeof value === "string" ? value : null;
19
+ }
20
+ function rec(obj) {
21
+ return typeof obj === "object" && obj !== null ? obj : {};
22
+ }
23
+ function list(value) {
24
+ return Array.isArray(value) ? value : [];
25
+ }
26
+ function checksOf(report) {
27
+ return list(report.checks);
28
+ }
29
+ /** `scan`: the `ScanReport` (+ `next_steps`) as a worst-first check list. */
30
+ export function formatReport(body) {
31
+ const top = rec(body);
32
+ const report = rec(top.report ?? top);
33
+ const lines = [];
34
+ const domain = str(report.domain) ?? "?";
35
+ if (report.not_registered === true) {
36
+ return `${domain}: not registered — no DNS records exist, so no check ran.`;
37
+ }
38
+ lines.push(`${domain}${str(report.scanned_at) ? ` (scanned ${str(report.scanned_at)})` : ""}`);
39
+ const checks = [...checksOf(report)].sort((a, b) => (STATUS_ORDER[str(a.status) ?? ""] ?? 9) - (STATUS_ORDER[str(b.status) ?? ""] ?? 9));
40
+ for (const check of checks) {
41
+ const status = str(check.status) ?? "?";
42
+ const word = STATUS_WORD[status] ?? status.toUpperCase();
43
+ lines.push(` ${word.padEnd(4)} ${(str(check.check) ?? "").padEnd(8)} ${str(check.title) ?? ""}`);
44
+ for (const detail of list(check.details))
45
+ if (typeof detail === "string")
46
+ lines.push(` · ${detail}`);
47
+ const fix = str(check.fix_record);
48
+ if (fix) {
49
+ lines.push(` fix (${RECORD_NOTE}):`);
50
+ lines.push(` ${fix}`);
51
+ }
52
+ }
53
+ const reportUrl = str(rec(top.next_steps).report_url);
54
+ if (reportUrl)
55
+ lines.push(` report: ${reportUrl}`);
56
+ return lines.join("\n");
57
+ }
58
+ /** `propagation`: verdict, coverage, one line per location. */
59
+ export function formatPropagation(body) {
60
+ const result = rec(body);
61
+ const lines = [];
62
+ lines.push(`${str(result.name) ?? "?"} ${str(result.record_type) ?? ""}: ${str(result.verdict) ?? "?"} — ${result.vantage_reached ?? "?"} of ${result.vantage_total ?? "?"} locations answered`);
63
+ for (const row of list(result.vantages)) {
64
+ const v = rec(row);
65
+ const label = `${str(v.label) ?? str(v.region) ?? "?"}`.padEnd(18);
66
+ if (v.reached !== true) {
67
+ lines.push(` ${label} unreached${str(v.detail) ? ` — ${str(v.detail)}` : ""}`);
68
+ continue;
69
+ }
70
+ const cells = list(v.cells).map((c) => {
71
+ const cell = rec(c);
72
+ const answers = list(cell.answers).join(",");
73
+ return { resolver: str(cell.resolver) ?? "?", status: str(cell.status) ?? "?", answers };
74
+ });
75
+ const agree = cells.length > 0 && cells.every((c) => c.status === cells[0]?.status && c.answers === cells[0]?.answers);
76
+ if (agree) {
77
+ // One line when every resolver at the location said the same thing —
78
+ // the reader wants the answer, not three copies of it.
79
+ const first = cells[0];
80
+ lines.push(` ${label} ${first?.status}${first?.answers ? ` ${first.answers}` : ""} (${cells.length} resolver${cells.length === 1 ? "" : "s"} agree)`);
81
+ continue;
82
+ }
83
+ lines.push(` ${label} ${cells.map((c) => `${c.resolver}=${c.status}${c.answers ? ` ${c.answers}` : ""}`).join(" ")}`);
84
+ }
85
+ return lines.join("\n");
86
+ }
87
+ /** `dmarc-upgrade`: the record verbatim when there is one, else the rationale. */
88
+ export function formatDmarcUpgrade(body) {
89
+ const result = rec(body);
90
+ const lines = [`${str(result.domain) ?? "?"}: current policy ${str(result.current_policy) ?? "none published"}`];
91
+ const record = str(result.record);
92
+ if (record) {
93
+ lines.push(` record (TXT at _dmarc; ${RECORD_NOTE}):`, ` ${record}`);
94
+ }
95
+ else {
96
+ lines.push(" no record recommended");
97
+ }
98
+ const rationale = str(result.rationale);
99
+ if (rationale)
100
+ lines.push(` ${rationale}`);
101
+ return lines.join("\n");
102
+ }
103
+ /** `reverse-dns`: verdict, PTR, the addresses the name resolves to. */
104
+ export function formatReverseDns(body) {
105
+ const result = rec(body);
106
+ return [
107
+ `${str(result.ip) ?? "?"}: ${str(result.verdict) ?? "?"}`,
108
+ ` reverse name: ${str(result.ptr) ?? "(none)"}`,
109
+ ` resolves to: ${list(result.addresses).join(", ") || "(nothing)"}`,
110
+ ].join("\n");
111
+ }
112
+ /** `spf-audit`: the walk's findings, then the lookup total. */
113
+ export function formatSpfAudit(body) {
114
+ const result = rec(body);
115
+ const lines = [`${str(result.domain) ?? "?"}: ${result.total_lookups ?? "?"} lookups${result.truncated === true ? " (walk truncated)" : ""}`];
116
+ const findings = list(result.findings);
117
+ if (findings.length === 0)
118
+ lines.push(" no findings");
119
+ for (const f of findings) {
120
+ const finding = rec(f);
121
+ lines.push(` ${String(finding.severity ?? "").toUpperCase().padEnd(5)} ${str(finding.domain) ?? ""}: ${str(finding.message) ?? ""}`);
122
+ }
123
+ const record = str(result.record);
124
+ if (record)
125
+ lines.push(` record: ${record}`);
126
+ return lines.join("\n");
127
+ }
128
+ /** `spf-count`: the count against the limit, the findings, the record. */
129
+ export function formatSpfCount(body) {
130
+ const result = rec(body);
131
+ const lines = [`${result.lookup_count ?? "?"} of ${result.lookup_limit ?? 10} DNS lookups${result.recursive === true ? " (recursive)" : ""}`];
132
+ for (const f of list(result.findings))
133
+ if (typeof f === "string")
134
+ lines.push(` · ${f}`);
135
+ const record = str(result.record);
136
+ if (record)
137
+ lines.push(` record: ${record}`);
138
+ return lines.join("\n");
139
+ }
140
+ /** `dmarc-validate` / `dmarc-generate`: validity, findings, the record(s) verbatim. */
141
+ export function formatDmarcRecord(body) {
142
+ const result = rec(body);
143
+ const lines = [result.valid === true ? "valid" : "not valid"];
144
+ const policy = str(result.policy);
145
+ if (policy)
146
+ lines[0] += ` · p=${policy}`;
147
+ for (const f of list(result.findings)) {
148
+ const finding = rec(f);
149
+ lines.push(` ${String(finding.level ?? "").toUpperCase().padEnd(5)} ${str(finding.message) ?? ""}`);
150
+ }
151
+ const record = str(result.record);
152
+ if (record)
153
+ lines.push(` record (${RECORD_NOTE}):`, ` ${record}`);
154
+ const upgrade = str(result.upgrade_record);
155
+ if (upgrade)
156
+ lines.push(` next step (${RECORD_NOTE}):`, ` ${upgrade}`);
157
+ const note = str(result.upgrade_note);
158
+ if (note)
159
+ lines.push(` ${note}`);
160
+ return lines.join("\n");
161
+ }
162
+ /** `dkim`: the status line, the parsed key when present, the record verbatim. */
163
+ export function formatDkim(body) {
164
+ const result = rec(body);
165
+ const lines = [`${str(result.selector) ?? "?"}._domainkey.${str(result.domain) ?? "?"}: ${str(result.status) ?? "?"} — ${str(result.title) ?? ""}`];
166
+ const key = rec(result.key);
167
+ if (Object.keys(key).length > 0) {
168
+ lines.push(` key: ${str(key.algorithm) ?? "?"} · ${key.bits ?? "?"} bit · ${key.revoked === true ? "revoked" : "active"}${key.testing === true ? " · testing" : ""}`);
169
+ }
170
+ const record = str(result.record);
171
+ if (record)
172
+ lines.push(` record: ${record}`);
173
+ return lines.join("\n");
174
+ }
175
+ /** `report-parse`: the report's totals and one line per source. */
176
+ export function formatReportParse(body) {
177
+ const result = rec(body);
178
+ const lines = [
179
+ `${str(result.org_name) ?? "?"} → ${str(result.domain) ?? "?"} ${str(result.date_begin) ?? ""} – ${str(result.date_end) ?? ""}`,
180
+ ` ${result.total_messages ?? "?"} messages · ${result.aligned_pct ?? "?"}% aligned · published ${str(result.published_policy) ?? "?"}`,
181
+ ];
182
+ for (const s of list(result.sources)) {
183
+ const source = rec(s);
184
+ lines.push(` ${String(source.ip ?? "?").padEnd(40)} ${String(source.total ?? "?").padStart(6)} aligned ${source.aligned_pct ?? "?"}%`);
185
+ }
186
+ return lines.join("\n");
187
+ }
188
+ /** `record`: the authoritative answer beside what the caches still hold. */
189
+ export function formatRecord(body) {
190
+ const result = rec(body);
191
+ const auth = rec(result.authoritative);
192
+ const lines = [
193
+ `${str(result.name) ?? "?"} ${str(result.rdtype) ?? ""}: ${result.in_sync === true ? "in sync" : `caches still clearing (up to ${result.max_wait_seconds ?? "?"} s)`}`,
194
+ ` authoritative: ${list(auth.values).join(", ") || "(none)"} TTL ${auth.ttl ?? "?"} ${auth.nameservers_queried ?? "?"} NS${auth.ns_in_agreement === false ? " — DISAGREE" : ""}`,
195
+ ];
196
+ for (const c of list(result.cached)) {
197
+ const cache = rec(c);
198
+ lines.push(` ${String(cache.resolver ?? "?").padEnd(14)} ${list(cache.values).join(", ") || "(none)"} ${cache.ttl_remaining ?? "?"} s left`);
199
+ }
200
+ return lines.join("\n");
201
+ }
202
+ /** `parked`: the pack verbatim, or the refusal and its rationale. */
203
+ export function formatParked(body) {
204
+ const result = rec(body);
205
+ const lines = [`${str(result.domain) ?? "?"}`];
206
+ const records = result.records;
207
+ if (Array.isArray(records) && records.length > 0) {
208
+ lines.push(` hardening pack (${RECORD_NOTE}):`);
209
+ for (const r of records) {
210
+ const row = rec(r);
211
+ lines.push(` ${str(row.host) ?? ""} ${str(row.record_type) ?? ""} ${str(row.value) ?? str(row.record) ?? ""}`);
212
+ }
213
+ }
214
+ else {
215
+ lines.push(" no pack issued");
216
+ }
217
+ const rationale = str(result.rationale);
218
+ if (rationale)
219
+ lines.push(` ${rationale}`);
220
+ const note = str(result.apply_note);
221
+ if (note)
222
+ lines.push(` ${note}`);
223
+ return lines.join("\n");
224
+ }
225
+ /** `signup-url`: the two links verbatim and the message. */
226
+ export function formatSignupUrl(body) {
227
+ const result = rec(body);
228
+ return [
229
+ `signup: ${str(result.signup_url) ?? "?"}`,
230
+ `report: ${str(result.report_url) ?? "?"}`,
231
+ `${str(result.message) ?? ""}`,
232
+ ].join("\n");
233
+ }
234
+ /** `alerts`: one line per alert, newest first as the API orders them. */
235
+ export function formatAlerts(body) {
236
+ const result = rec(body);
237
+ const alerts = list(result.alerts);
238
+ if (alerts.length === 0)
239
+ return "no alerts";
240
+ const lines = alerts.map((a) => {
241
+ const alert = rec(a);
242
+ return `${String(alert.created_at ?? "").slice(0, 19)} ${String(alert.domain ?? "").padEnd(24)} ${String(alert.type ?? "").padEnd(20)} ${str(alert.summary) ?? ""}${str(alert.delivery_class) ? ` [${str(alert.delivery_class)}]` : ""}`;
243
+ });
244
+ const next = str(result.next_before);
245
+ if (next)
246
+ lines.push(` more: --before ${next}`);
247
+ return lines.join("\n");
248
+ }
249
+ /** `readiness`: ready or not, the blockers, the next record verbatim. */
250
+ export function formatReadiness(body) {
251
+ const result = rec(body);
252
+ const progress = rec(result.progress);
253
+ const lines = [
254
+ `${result.ready === true ? "ready" : "not ready"} for ${str(result.next_step) ?? "the next step"} — ${result.total_messages ?? "?"} messages over ${result.window_days ?? "?"} days${progress.progress_pct !== undefined ? ` · ${progress.progress_pct}%` : ""}`,
255
+ ];
256
+ for (const b of list(result.blockers)) {
257
+ const blocker = rec(b);
258
+ lines.push(` blocker: ${str(blocker.source) ?? ""} ${str(blocker.metric) ?? ""}${str(blocker.fix_hint) ? ` — ${str(blocker.fix_hint)}` : ""}`);
259
+ }
260
+ const next = str(result.next_record);
261
+ if (next)
262
+ lines.push(` next record (${RECORD_NOTE}):`, ` ${next}`);
263
+ return lines.join("\n");
264
+ }
265
+ const RENDERERS = {
266
+ scan: formatReport,
267
+ propagation: formatPropagation,
268
+ "dmarc-upgrade": formatDmarcUpgrade,
269
+ "reverse-dns": formatReverseDns,
270
+ "spf-audit": formatSpfAudit,
271
+ "spf-count": formatSpfCount,
272
+ "dmarc-validate": formatDmarcRecord,
273
+ "dmarc-generate": formatDmarcRecord,
274
+ dkim: formatDkim,
275
+ "report-parse": formatReportParse,
276
+ record: formatRecord,
277
+ parked: formatParked,
278
+ "signup-url": formatSignupUrl,
279
+ alerts: formatAlerts,
280
+ readiness: formatReadiness,
281
+ };
282
+ export function render(command, body) {
283
+ const renderer = RENDERERS[command];
284
+ return renderer ? renderer(body) : JSON.stringify(body, null, 2);
285
+ }
286
+ /**
287
+ * Exit code from the API's own verdicts: 0 = nothing failing, 1 = a failing
288
+ * verdict. Transient errors never reach here (they exit 2 in `main`).
289
+ */
290
+ export function exitCodeFor(command, body) {
291
+ const result = rec(body);
292
+ switch (command) {
293
+ case "scan":
294
+ return checksOf(rec(result.report ?? result)).some((c) => str(c.status) === "fail") ? 1 : 0;
295
+ case "propagation": {
296
+ const verdict = str(result.verdict);
297
+ return verdict === "not_propagated" || verdict === "inconsistent" ? 1 : 0;
298
+ }
299
+ case "reverse-dns":
300
+ return str(result.verdict) === "confirmed" ? 0 : 1;
301
+ case "spf-audit":
302
+ return list(result.findings).some((f) => String(rec(f).severity ?? "").toLowerCase() === "high") ? 1 : 0;
303
+ case "spf-count":
304
+ return result.over_limit === true || result.record_valid === false || result.has_pass_all === true ? 1 : 0;
305
+ case "dmarc-validate":
306
+ return result.valid === false ? 1 : 0;
307
+ case "record":
308
+ return rec(result.authoritative).ns_in_agreement === false ? 1 : 0;
309
+ default:
310
+ return 0;
311
+ }
312
+ }
313
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `dns-doctor` — the DNS Doctor API from the terminal, every tool the MCP
4
+ * server exposes as a command.
5
+ *
6
+ * `--json` prints the API response verbatim (the path for scripts and agents).
7
+ * Exit codes: 0 nothing failing · 1 a failing verdict · 2 error, incl. every
8
+ * transient (rate limit, 402 offer, outage) — never read 2 as a verdict.
9
+ */
10
+ export interface Parsed {
11
+ command: string;
12
+ target: string | undefined;
13
+ json: boolean;
14
+ fresh: boolean;
15
+ type: string;
16
+ expect: string | undefined;
17
+ selector: string | undefined;
18
+ kind: string | undefined;
19
+ host: string | undefined;
20
+ rua: string | undefined;
21
+ subdomainPolicy: string | undefined;
22
+ strict: boolean;
23
+ confirmNoMail: boolean;
24
+ domain: string | undefined;
25
+ since: string | undefined;
26
+ before: string | undefined;
27
+ alertType: string | undefined;
28
+ limit: string | undefined;
29
+ }
30
+ export declare function parse(argv: string[]): Parsed | "help" | "version";
31
+ /** One request per command — the endpoint's own field names, nothing renamed. */
32
+ export declare function run(p: Parsed): Promise<unknown>;
33
+ export declare function main(argv: string[]): Promise<number>;
package/dist/index.js ADDED
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `dns-doctor` — the DNS Doctor API from the terminal, every tool the MCP
4
+ * server exposes as a command.
5
+ *
6
+ * `--json` prints the API response verbatim (the path for scripts and agents).
7
+ * Exit codes: 0 nothing failing · 1 a failing verdict · 2 error, incl. every
8
+ * transient (rate limit, 402 offer, outage) — never read 2 as a verdict.
9
+ */
10
+ import { readFileSync } from "node:fs";
11
+ import { basename } from "node:path";
12
+ import { parseArgs } from "node:util";
13
+ import { ApiError, DEFAULT_API_BASE, getJson, packageVersion, postFile, postJson } from "./api.js";
14
+ import { exitCodeFor, render } from "./format.js";
15
+ import { COMMANDS, reportPath, routeFor } from "./routes.js";
16
+ const USAGE = `dns-doctor ${packageVersion()} — scan, fix and verify a domain's DNS (${DEFAULT_API_BASE})
17
+
18
+ usage: dns-doctor <command> [target] [options]
19
+
20
+ scan & fix
21
+ scan <domain> [--fresh] the seven-check report (persisted, or a fresh scan on a miss)
22
+ dmarc-upgrade <domain> the next safe DMARC policy step as a record, or why there is none
23
+ parked <domain> --confirm-no-mail the three-record hardening pack for a domain that sends no mail
24
+ signup-url <domain> a sign-up link to hand the domain's owner for monitoring
25
+
26
+ records
27
+ dmarc-validate "<record>" every tag of a DMARC record checked, with the safe next step
28
+ dmarc-generate <none|quarantine|reject> [--rua <email>] [--subdomain-policy <p>] [--strict]
29
+ spf-count <domain | "<SPF record>"> DNS lookups against the 10-lookup limit
30
+ spf-audit <domain> the SPF include tree with registration and lookup findings
31
+ dkim <domain> --selector <s> a DKIM selector's published key
32
+ report-parse <file.xml|.gz|.zip> one DMARC aggregate report as a table
33
+
34
+ live DNS
35
+ record <domain> --kind <spf|dmarc|txt|mx|cname|a|aaaa> [--host <label>]
36
+ authoritative answer beside two public caches, with TTLs
37
+ propagation <name> [--type A] [--expect <value>]
38
+ one name read from six locations on four continents
39
+ reverse-dns <ip> forward-confirmed reverse DNS for a mail server address
40
+
41
+ monitoring (DNSDOCTOR_API_TOKEN required)
42
+ alerts [--domain d] [--since t] [--before t] [--alert-type t] [--limit n]
43
+ readiness <domain> enforcement readiness from aggregate-report evidence
44
+
45
+ options
46
+ --json print the API response verbatim
47
+ --version, --help
48
+
49
+ Fix records are generated by a deterministic engine and printed verbatim; a human
50
+ approves every DNS change. SPF is diagnose-only: no SPF record is ever proposed.`;
51
+ const TARGETLESS = new Set(["alerts"]);
52
+ export function parse(argv) {
53
+ const { values, positionals } = parseArgs({
54
+ args: argv,
55
+ allowPositionals: true,
56
+ options: {
57
+ json: { type: "boolean", default: false },
58
+ fresh: { type: "boolean", default: false },
59
+ type: { type: "string", default: "A" },
60
+ expect: { type: "string" },
61
+ selector: { type: "string" },
62
+ kind: { type: "string" },
63
+ host: { type: "string" },
64
+ rua: { type: "string" },
65
+ "subdomain-policy": { type: "string" },
66
+ strict: { type: "boolean", default: false },
67
+ "confirm-no-mail": { type: "boolean", default: false },
68
+ domain: { type: "string" },
69
+ since: { type: "string" },
70
+ before: { type: "string" },
71
+ "alert-type": { type: "string" },
72
+ limit: { type: "string" },
73
+ help: { type: "boolean", default: false },
74
+ version: { type: "boolean", default: false },
75
+ },
76
+ });
77
+ if (values.version)
78
+ return "version";
79
+ const [command, target] = positionals;
80
+ if (values.help || !command || !COMMANDS.includes(command))
81
+ return "help";
82
+ if (!target && !TARGETLESS.has(command))
83
+ return "help";
84
+ return {
85
+ command,
86
+ target,
87
+ json: values.json,
88
+ fresh: values.fresh,
89
+ type: values.type,
90
+ expect: values.expect,
91
+ selector: values.selector,
92
+ kind: values.kind,
93
+ host: values.host,
94
+ rua: values.rua,
95
+ subdomainPolicy: values["subdomain-policy"],
96
+ strict: values.strict,
97
+ confirmNoMail: values["confirm-no-mail"],
98
+ domain: values.domain,
99
+ since: values.since,
100
+ before: values.before,
101
+ alertType: values["alert-type"],
102
+ limit: values.limit,
103
+ };
104
+ }
105
+ function need(value, what) {
106
+ if (!value)
107
+ throw new ApiError(`${what} is required — see dns-doctor --help`, null, false);
108
+ return value;
109
+ }
110
+ function query(path, params) {
111
+ const search = new URLSearchParams();
112
+ for (const [key, value] of Object.entries(params))
113
+ if (value !== undefined)
114
+ search.set(key, value);
115
+ const encoded = search.toString();
116
+ return encoded ? `${path}?${encoded}` : path;
117
+ }
118
+ /** One request per command — the endpoint's own field names, nothing renamed. */
119
+ export async function run(p) {
120
+ const path = (command) => routeFor(command).path;
121
+ const target = p.target ?? "";
122
+ switch (p.command) {
123
+ case "scan":
124
+ return p.fresh ? postJson(path("scan"), { domain: target }) : getJson(reportPath(target));
125
+ case "dmarc-upgrade":
126
+ return postJson(path("dmarc-upgrade"), { domain: target });
127
+ case "signup-url":
128
+ return postJson(path("signup-url"), { domain: target });
129
+ case "parked": {
130
+ // The gate is the human's: only the domain's owner can assert it sends no
131
+ // mail, and only the API decides whether the evidence permits the pack.
132
+ if (!p.confirmNoMail) {
133
+ throw new ApiError("parked requires --confirm-no-mail: only the domain's owner can assert that it sends no mail", null, false);
134
+ }
135
+ const body = { domain: target, confirm_no_mail: true };
136
+ if (p.rua !== undefined)
137
+ body.rua_email = p.rua;
138
+ return postJson(path("parked"), body);
139
+ }
140
+ case "dmarc-validate":
141
+ return postJson(path("dmarc-validate"), { record: target });
142
+ case "dmarc-generate": {
143
+ const body = { policy: target, strict_alignment: p.strict };
144
+ if (p.rua !== undefined)
145
+ body.rua_email = p.rua;
146
+ if (p.subdomainPolicy !== undefined)
147
+ body.subdomain_policy = p.subdomainPolicy;
148
+ return postJson(path("dmarc-generate"), body);
149
+ }
150
+ case "spf-count":
151
+ // A pasted record has spaces or '='; a domain has neither. No record
152
+ // literal here — the API decides what a record is.
153
+ return postJson(path("spf-count"), /[\s=]/.test(target.trim()) ? { record: target } : { domain: target });
154
+ case "spf-audit":
155
+ return postJson(path("spf-audit"), { domain: target });
156
+ case "dkim":
157
+ return postJson(path("dkim"), { domain: target, selector: need(p.selector, "--selector") });
158
+ case "report-parse": {
159
+ const route = routeFor("report-parse");
160
+ return postFile(route.path, route.field, readFileSync(target), basename(target));
161
+ }
162
+ case "record": {
163
+ const body = { domain: target, kind: need(p.kind, "--kind") };
164
+ if (p.host !== undefined)
165
+ body.host = p.host;
166
+ return postJson(path("record"), body);
167
+ }
168
+ case "propagation": {
169
+ const body = { name: target, record_type: p.type };
170
+ if (p.expect !== undefined)
171
+ body.expected_value = p.expect;
172
+ return postJson(path("propagation"), body);
173
+ }
174
+ case "reverse-dns":
175
+ return postJson(path("reverse-dns"), { ip: target });
176
+ case "alerts":
177
+ return getJson(query(path("alerts"), { domain: p.domain, since: p.since, before: p.before, type: p.alertType, limit: p.limit }));
178
+ case "readiness":
179
+ return getJson(query(path("readiness"), { domain: target }));
180
+ default:
181
+ throw new Error(`unknown command ${p.command}`);
182
+ }
183
+ }
184
+ export async function main(argv) {
185
+ const parsed = parse(argv);
186
+ if (parsed === "version") {
187
+ process.stdout.write(`${packageVersion()}\n`);
188
+ return 0;
189
+ }
190
+ if (parsed === "help") {
191
+ process.stdout.write(`${USAGE}\n`);
192
+ return argv.includes("--help") ? 0 : 2;
193
+ }
194
+ try {
195
+ const body = await run(parsed);
196
+ process.stdout.write(`${parsed.json ? JSON.stringify(body, null, 2) : render(parsed.command, body)}\n`);
197
+ return exitCodeFor(parsed.command, body);
198
+ }
199
+ catch (error) {
200
+ const message = error instanceof Error ? error.message : String(error);
201
+ process.stderr.write(`dns-doctor: ${message}\n`);
202
+ return 2;
203
+ }
204
+ }
205
+ const invokedDirectly = typeof process.argv[1] === "string" && import.meta.url === new URL(`file://${process.argv[1]}`).href;
206
+ if (invokedDirectly) {
207
+ main(process.argv.slice(2)).then((code) => {
208
+ process.exitCode = code;
209
+ });
210
+ }
211
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Command → REST endpoint. The CLI translates nothing: each command sends the
3
+ * endpoint's own field names and prints what comes back.
4
+ *
5
+ * Every one of the 16 MCP tools has a command here, so the CLI and the MCP
6
+ * clients cover the same API. Pinned by the backend's
7
+ * `tests/mcp_server/test_cli_routes.py` against the real app (path exists,
8
+ * field names are the request model's, and the path SET equals the MCP
9
+ * client's) — a renamed route fails CI before it 404s a published CLI. Keep
10
+ * the literal shape: that test parses it.
11
+ */
12
+ export interface Route {
13
+ kind: "json" | "report" | "query" | "upload";
14
+ path?: string;
15
+ field?: string;
16
+ }
17
+ export declare function routeFor(command: string): Route;
18
+ /** The persisted-report path; the one route whose path is built from an argument. */
19
+ export declare function reportPath(domain: string): string;
20
+ export declare const COMMANDS: string[];
package/dist/routes.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Command → REST endpoint. The CLI translates nothing: each command sends the
3
+ * endpoint's own field names and prints what comes back.
4
+ *
5
+ * Every one of the 16 MCP tools has a command here, so the CLI and the MCP
6
+ * clients cover the same API. Pinned by the backend's
7
+ * `tests/mcp_server/test_cli_routes.py` against the real app (path exists,
8
+ * field names are the request model's, and the path SET equals the MCP
9
+ * client's) — a renamed route fails CI before it 404s a published CLI. Keep
10
+ * the literal shape: that test parses it.
11
+ */
12
+ const ROUTES = {
13
+ scan: { kind: "json", path: "/api/v1/scan" },
14
+ report: { kind: "report" },
15
+ "dmarc-upgrade": { kind: "json", path: "/api/v1/dmarc-upgrade" },
16
+ "signup-url": { kind: "json", path: "/api/v1/signup-url" },
17
+ "spf-count": { kind: "json", path: "/api/tools/spf-count" },
18
+ "dmarc-validate": { kind: "json", path: "/api/tools/dmarc-validate" },
19
+ "dmarc-generate": { kind: "json", path: "/api/tools/dmarc-generate" },
20
+ dkim: { kind: "json", path: "/api/tools/dkim-check" },
21
+ "report-parse": { kind: "upload", path: "/api/tools/dmarc-report-parse", field: "file" },
22
+ record: { kind: "json", path: "/api/tools/check-record" },
23
+ propagation: { kind: "json", path: "/api/tools/propagation-check" },
24
+ "reverse-dns": { kind: "json", path: "/api/tools/reverse-dns-check" },
25
+ "spf-audit": { kind: "json", path: "/api/tools/spf-audit" },
26
+ parked: { kind: "json", path: "/api/tools/parked-domain-records" },
27
+ alerts: { kind: "query", path: "/api/v1/alerts" },
28
+ readiness: { kind: "query", path: "/api/v1/readiness" },
29
+ };
30
+ export function routeFor(command) {
31
+ const route = ROUTES[command];
32
+ if (!route)
33
+ throw new Error(`no route for command '${command}'`);
34
+ return route;
35
+ }
36
+ /** The persisted-report path; the one route whose path is built from an argument. */
37
+ export function reportPath(domain) {
38
+ return `/api/v1/report/${encodeURIComponent(domain)}`;
39
+ }
40
+ export const COMMANDS = Object.keys(ROUTES).filter((c) => c !== "report");
41
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,MAAM,MAAM,GAA0B;IACpC,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE;IAC5C,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC1B,eAAe,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,uBAAuB,EAAE;IAChE,YAAY,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,oBAAoB,EAAE;IAC1D,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,sBAAsB,EAAE;IAC3D,gBAAgB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,2BAA2B,EAAE;IACrE,gBAAgB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,2BAA2B,EAAE;IACrE,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,uBAAuB,EAAE;IACrD,cAAc,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,+BAA+B,EAAE,KAAK,EAAE,MAAM,EAAE;IACxF,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,yBAAyB,EAAE;IACzD,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,8BAA8B,EAAE;IACnE,aAAa,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,8BAA8B,EAAE;IACrE,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,sBAAsB,EAAE;IAC3D,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,kCAAkC,EAAE;IAClE,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE;IACjD,SAAS,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,mBAAmB,EAAE;CACxD,CAAC;AAEF,MAAM,UAAU,QAAQ,CAAC,OAAe;IACtC,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9B,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,OAAO,GAAG,CAAC,CAAC;IACjE,OAAO,KAAK,CAAC;AACf,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,UAAU,CAAC,MAAc;IACvC,OAAO,kBAAkB,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC;AACxD,CAAC;AAED,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "dns-doctor",
3
+ "version": "0.2.0",
4
+ "description": "Command-line DNS Doctor — scan, fix and verify a domain's DNS from the terminal: SPF, DMARC, DKIM, MX, DNS health, blacklists, expiry and six-vantage propagation, over the public DNS Doctor API. Fix records come from a deterministic engine, never an AI guess.",
5
+ "license": "Apache-2.0",
6
+ "author": {
7
+ "name": "DNS Doctor",
8
+ "url": "https://dnsdoctor.dev"
9
+ },
10
+ "homepage": "https://dnsdoctor.dev/methodology",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/dnsdoctor/cli.git"
14
+ },
15
+ "keywords": [
16
+ "dns",
17
+ "dmarc",
18
+ "spf",
19
+ "dkim",
20
+ "email",
21
+ "deliverability",
22
+ "propagation",
23
+ "cli"
24
+ ],
25
+ "type": "module",
26
+ "bin": {
27
+ "dns-doctor": "dist/index.js"
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "engines": {
33
+ "node": ">=20"
34
+ },
35
+ "scripts": {
36
+ "build": "tsc",
37
+ "prepack": "npm run build",
38
+ "test": "vitest run",
39
+ "typecheck": "tsc --noEmit"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^22.15.0",
43
+ "typescript": "^5.9.2",
44
+ "vitest": "^3.2.4"
45
+ }
46
+ }