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 +202 -0
- package/README.md +89 -0
- package/dist/api.d.ts +43 -0
- package/dist/api.js +142 -0
- package/dist/api.js.map +1 -0
- package/dist/format.d.ts +41 -0
- package/dist/format.js +313 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +211 -0
- package/dist/index.js.map +1 -0
- package/dist/routes.d.ts +20 -0
- package/dist/routes.js +41 -0
- package/dist/routes.js.map +1 -0
- package/package.json +46 -0
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
|
package/dist/api.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,MAAM,CAAC,MAAM,gBAAgB,GAAG,uBAAuB,CAAC;AAExD,MAAM,CAAC,MAAM,gBAAgB,GAAG,+CAA+C,CAAC;AAChF,MAAM,CAAC,MAAM,oBAAoB,GAAG,oCAAoC,CAAC;AACzE,MAAM,CAAC,MAAM,wBAAwB,GACnC,wIAAwI,CAAC;AAE3I,MAAM,OAAO,QAAS,SAAQ,KAAK;IAGf;IACA;IAHlB,YACE,OAAe,EACC,MAAqB,EACrB,SAAkB;QAElC,KAAK,CAAC,OAAO,CAAC,CAAC;QAHC,WAAM,GAAN,MAAM,CAAe;QACrB,cAAS,GAAT,SAAS,CAAS;QAGlC,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;IACzB,CAAC;CACF;AAED,sFAAsF;AACtF,MAAM,UAAU,OAAO;IACrB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,IAAI,EAAE,CAAC;IACnD,MAAM,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,gBAAgB,CAAC;IAC1C,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAClC,CAAC;AAED,sFAAsF;AACtF,SAAS,WAAW;IAClB,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,EAAE,IAAI,EAAE,CAAC;IACtD,OAAO,KAAK,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;QAC9E,MAAM,OAAO,GAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAA2B,CAAC,OAAO,CAAC;QACnE,OAAO,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC;IACjB,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS;IACvB,OAAO,iBAAiB,cAAc,EAAE,2BAA2B,CAAC;AACtE,CAAC;AAED,SAAS,QAAQ,CAAC,IAAa;IAC7B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC3D,MAAM,MAAM,GAAI,IAA6B,CAAC,MAAM,CAAC;IACrD,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC;IAC9C,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACzD,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;AAChC,CAAC;AAED,KAAK,UAAU,QAAQ,CAAC,QAAkB;IACxC,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,QAAkB;IACjD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;IAClD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;IAC/B,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACnB,OAAO,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,oBAAoB,KAAK,MAAM,GAAG,CAAC,CAAC,CAAC,oBAAoB,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;IAC3G,CAAC;IACD,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,IAAI,QAAQ,CAAC,wBAAwB,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;IAChF,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACnB,OAAO,IAAI,QAAQ,CAAC,0DAA0D,gBAAgB,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;IAClH,CAAC;IACD,IAAI,MAAM,IAAI,GAAG,EAAE,CAAC;QAClB,OAAO,IAAI,QAAQ,CAAC,4CAA4C,MAAM,OAAO,gBAAgB,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;IACjH,CAAC;IACD,0FAA0F;IAC1F,OAAO,IAAI,QAAQ,CAAC,MAAM,IAAI,mCAAmC,MAAM,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;AAC7F,CAAC;AAED,KAAK,UAAU,IAAI,CAAC,IAAY,EAAE,IAAiB;IACjD,MAAM,GAAG,GAAG,GAAG,OAAO,EAAE,GAAG,IAAI,EAAE,CAAC;IAClC,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAC1B,GAAG,IAAI;YACP,OAAO,EAAE;gBACP,MAAM,EAAE,kBAAkB;gBAC1B,YAAY,EAAE,SAAS,EAAE;gBACzB,GAAG,WAAW,EAAE;gBAChB,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;aACxB;SACF,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,KAAK,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACrE,MAAM,IAAI,QAAQ,CAAC,uCAAuC,KAAK,OAAO,gBAAgB,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IACxG,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,MAAM,MAAM,UAAU,CAAC,QAAQ,CAAC,CAAC;IACnD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACtC,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,MAAM,IAAI,QAAQ,CAChB,oDAAoD,QAAQ,CAAC,MAAM,OAAO,gBAAgB,EAAE,EAC5F,QAAQ,CAAC,MAAM,EACf,IAAI,CACL,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,OAAO,CAAC,IAAY;IAClC,OAAO,IAAI,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;AACvC,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,IAAa;IAClD,OAAO,IAAI,CAAC,IAAI,EAAE;QAChB,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;KAC3B,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,KAAa,EAAE,KAAiB,EAAE,QAAgB;IACvF,MAAM,IAAI,GAAG,IAAI,QAAQ,EAAE,CAAC;IAC5B,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,IAAI,CAAC,CAAC,KAAiB,CAAC,EAAE,EAAE,IAAI,EAAE,0BAA0B,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC;IAClG,OAAO,IAAI,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;AACpD,CAAC"}
|
package/dist/format.d.ts
ADDED
|
@@ -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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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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"}
|
package/dist/routes.d.ts
ADDED
|
@@ -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
|
+
}
|