@sdxc/doh 2026.10.25 → 2026.11.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -75,6 +75,18 @@ let verdict = await mayIssue({ domain: "*.example.com", issuer: "letsencrypt.org
75
75
  if (!verdict.allowed) verdict.reason; // "not-authorized", "forbidden", "lookup-failed", …
76
76
  ```
77
77
 
78
+ ## Command line
79
+
80
+ ```sh
81
+ bunx @sdxc/doh lookup example.com --type MX
82
+ ```
83
+
84
+ With [`@sdxc/kit`](https://www.npmjs.com/package/@sdxc/kit) installed, `sdxc pkg doh …` runs the same command line, using the version your project installs.
85
+
86
+ `lookup` prints a dig-style answer section, or the typed answer with `--json`, and exits 1 on
87
+ NXDOMAIN, an empty answer or a resolver error. `--resolver` takes `cloudflare`, `google` or a DoH
88
+ URL, and `--wire` speaks RFC 8484. `bunx @sdxc/doh --help` lists every flag.
89
+
78
90
  ## API
79
91
 
80
92
  ### `resolve(name, type, options?): Promise<Result<DoH.Answer<Type>, DoHError>>`
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The `sdxc-doh` command line as a contract: `lookup` asks a DNS-over-HTTPS resolver for one
3
+ * name's records of one type. Help, usage errors and the handler's typed `ctx.arguments` and
4
+ * `ctx.flags` all come from this one declaration.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ /** Every command `sdxc-doh` takes. */
10
+ export declare const DOH_COMMANDS: import("@sdxc/cli").Group<Omit<{
11
+ readonly description: "sdxc-doh — typed DNS over HTTPS lookups";
12
+ readonly lookup: import("@sdxc/cli").Command<{
13
+ readonly name: import("@sdxc/cli").Argument<string>;
14
+ }, {
15
+ readonly type: import("@sdxc/cli").ValueFlag<string, true, false, false>;
16
+ readonly resolver: import("@sdxc/cli").ValueFlag<string, true, false, false>;
17
+ readonly wire: import("@sdxc/cli").Flag<boolean>;
18
+ readonly dnssec: import("@sdxc/cli").Flag<boolean>;
19
+ readonly json: import("@sdxc/cli").Flag<boolean>;
20
+ }>;
21
+ }, "flags" | "description" | "hidden" | "arguments" | "tasks">, Record<never, never>, Record<never, never>>;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The `sdxc-doh` command line as a contract: `lookup` asks a DNS-over-HTTPS resolver for one
3
+ * name's records of one type. Help, usage errors and the handler's typed `ctx.arguments` and
4
+ * `ctx.flags` all come from this one declaration.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import { argument, command, commands, flag } from "@sdxc/cli";
10
+ import { canonicalType, typeCode } from "@sdxc/zone-file";
11
+ import * as s from "remix/data-schema";
12
+ /**
13
+ * A record type as a mnemonic or `TYPEnnn`, answered in its canonical spelling, so `mx` and
14
+ * `TYPE15` both query MX and an unknown mnemonic is a usage error naming what was typed.
15
+ */
16
+ const RECORD_TYPE = s.createSchema(function validate(value, context) {
17
+ if (typeof value === "string" && typeCode(value) !== null) {
18
+ return { value: canonicalType(value) };
19
+ }
20
+ return s.fail(`expects a record type such as A, AAAA, MX or TYPE65; got ${JSON.stringify(value)}`, context.path);
21
+ });
22
+ /**
23
+ * A resolver as `cloudflare`, `google` or an `https:` DoH endpoint URL; the names come back
24
+ * lowercased and a URL as typed.
25
+ */
26
+ const RESOLVER = s.createSchema(function validate(value, context) {
27
+ if (typeof value === "string") {
28
+ let lower = value.toLowerCase();
29
+ if (lower === "cloudflare" || lower === "google")
30
+ return { value: lower };
31
+ if (URL.canParse(value) && new URL(value).protocol === "https:")
32
+ return { value };
33
+ }
34
+ return s.fail(`expects cloudflare, google or an https:// URL; got ${JSON.stringify(value)}`, context.path);
35
+ });
36
+ /** Every command `sdxc-doh` takes. */
37
+ export const DOH_COMMANDS = commands({
38
+ description: "sdxc-doh — typed DNS over HTTPS lookups",
39
+ lookup: command({
40
+ description: "Look up a name's records of one type and print them as a dig-style answer section. Exits 1 on NXDOMAIN, an empty answer or a resolver error, and 2 when the resolver cannot be reached.",
41
+ arguments: {
42
+ name: argument(s.string(), {
43
+ description: "The domain name, in its ASCII (punycode) form",
44
+ }),
45
+ },
46
+ flags: {
47
+ type: flag.string("type", {
48
+ short: "t",
49
+ valueName: "type",
50
+ default: "A",
51
+ schema: RECORD_TYPE,
52
+ description: "The record type: A, AAAA, MX, TXT, CNAME, NS, CAA, SOA, HTTPS, …",
53
+ }),
54
+ resolver: flag.string("resolver", {
55
+ short: "r",
56
+ valueName: "url",
57
+ default: "cloudflare",
58
+ schema: RESOLVER,
59
+ description: "cloudflare, google or a DoH endpoint URL",
60
+ }),
61
+ wire: flag.boolean("wire", {
62
+ description: "Speak RFC 8484 wire format instead of the JSON API, which also reports extended DNS errors",
63
+ }),
64
+ dnssec: flag.boolean("dnssec", {
65
+ description: "Set the DO bit, asking for DNSSEC records alongside the answer",
66
+ }),
67
+ json: flag.boolean("json", {
68
+ description: "Print the answer as JSON: { name, type, records, unparsed, chain, ttl, authenticated, … }, or { name, type, error } on failure",
69
+ }),
70
+ },
71
+ examples: [
72
+ "lookup example.com",
73
+ "lookup example.com --type MX",
74
+ "lookup _dmarc.example.com --type TXT --json",
75
+ "lookup example.com --type AAAA --resolver google --wire",
76
+ ],
77
+ }),
78
+ });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The `sdxc-doh lookup` handler: one `resolve` call printed as a dig-style answer section, or
3
+ * as JSON. NXDOMAIN, an empty answer and a resolver error exit 1; a resolver that never
4
+ * answered exits 2, since the lookup itself could not run.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ /**
10
+ * Prints the chain, the records and any unparsed records in answer order; the answer section
11
+ * goes to stdout and the closing summary to stderr, so piping keeps only records.
12
+ */
13
+ declare const _default: import("@sdxc/cli").CommandAction<import("@sdxc/cli").Command<{
14
+ readonly name: import("@sdxc/cli").Argument<string>;
15
+ }, {
16
+ readonly type: import("@sdxc/cli").ValueFlag<string, true, false, false>;
17
+ readonly resolver: import("@sdxc/cli").ValueFlag<string, true, false, false>;
18
+ readonly wire: import("@sdxc/cli").Flag<boolean>;
19
+ readonly dnssec: import("@sdxc/cli").Flag<boolean>;
20
+ readonly json: import("@sdxc/cli").Flag<boolean>;
21
+ }>, []>;
22
+ export default _default;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The `sdxc-doh lookup` handler: one `resolve` call printed as a dig-style answer section, or
3
+ * as JSON. NXDOMAIN, an empty answer and a resolver error exit 1; a resolver that never
4
+ * answered exits 2, since the lookup itself could not run.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import { createCommand, exit, success } from "@sdxc/cli";
10
+ import { isFailure } from "@sdxc/result";
11
+ import { formatRecordData } from "@sdxc/zone-file";
12
+ import { DOH_COMMANDS } from "./cli-contract.js";
13
+ import { TransportError } from "./errors.js";
14
+ import { resolve } from "./resolve.js";
15
+ import { CLOUDFLARE, CLOUDFLARE_WIRE, GOOGLE, GOOGLE_WIRE } from "./resolvers.js";
16
+ /**
17
+ * The resolver a `--resolver` value and `--wire` name: the public ones by name, in the format
18
+ * asked for, and any other value as an endpoint URL.
19
+ */
20
+ function pickResolver(resolver, wire) {
21
+ if (resolver === "cloudflare")
22
+ return wire ? CLOUDFLARE_WIRE : CLOUDFLARE;
23
+ if (resolver === "google")
24
+ return wire ? GOOGLE_WIRE : GOOGLE;
25
+ return { url: resolver, format: wire ? "wire" : "json" };
26
+ }
27
+ /** One record as a zone-file line: absolute owner, TTL, class, type and canonical data. */
28
+ function line(record) {
29
+ let data = formatRecordData(record);
30
+ return `${record.name}.\t${record.ttl}\tIN\t${record.type}\t${data}`;
31
+ }
32
+ /** A failure as data: its class name, message and the own fields a script may branch on (`ttl`, `rcode`, `status`, …). */
33
+ function describeError(error) {
34
+ return { name: error.name, message: error.message, ...Object.fromEntries(Object.entries(error)) };
35
+ }
36
+ /**
37
+ * Prints the chain, the records and any unparsed records in answer order; the answer section
38
+ * goes to stdout and the closing summary to stderr, so piping keeps only records.
39
+ */
40
+ export default createCommand(DOH_COMMANDS.lookup, async (ctx) => {
41
+ let { name } = ctx.arguments;
42
+ let { type, json } = ctx.flags;
43
+ let answer = await resolve(name, type, {
44
+ resolver: pickResolver(ctx.flags.resolver, ctx.flags.wire === true),
45
+ dnssec: ctx.flags.dnssec === true,
46
+ signal: ctx.signal,
47
+ });
48
+ if (isFailure(answer)) {
49
+ let code = answer.error instanceof TransportError ? 2 : 1;
50
+ if (json) {
51
+ ctx.stdout(`${JSON.stringify({ name, type, error: describeError(answer.error) }, null, 2)}\n`);
52
+ return exit(code);
53
+ }
54
+ return exit(code, answer.error.message);
55
+ }
56
+ let { records, unparsed, chain } = answer.data;
57
+ let empty = records.length === 0 && unparsed.length === 0;
58
+ if (json) {
59
+ ctx.stdout(`${JSON.stringify(answer.data, null, 2)}\n`);
60
+ return empty ? exit(1) : success();
61
+ }
62
+ if (empty)
63
+ return exit(1, `${answer.data.name} has no ${type} records (NODATA)`);
64
+ let lines = [...chain, ...records, ...unparsed].map(line);
65
+ ctx.stdout(`;; ANSWER SECTION:\n${lines.join("\n")}\n`);
66
+ ctx.stderr(`;; Query time: ${Math.round(answer.data.durationMs)} msec\n`);
67
+ return success();
68
+ });
package/dist/cli.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The `sdxc-doh` command-line program: the contract in `cli-contract.ts` with `lookup` mapped
4
+ * onto its handler, which loads only when a lookup runs. Exit codes: 0 records found, 1
5
+ * NXDOMAIN, an empty answer or a resolver error, 2 usage or an unreachable resolver.
6
+ *
7
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
8
+ * @copyright Sergio Xalambrí 2026
9
+ */
10
+ /** The `sdxc-doh` program, run by the bin entry below and by tests through `invoke()`. */
11
+ export declare const cli: import("@sdxc/cli").CLI<import("@sdxc/cli").Group<Omit<{
12
+ readonly description: "sdxc-doh — typed DNS over HTTPS lookups";
13
+ readonly lookup: import("@sdxc/cli").Command<{
14
+ readonly name: import("@sdxc/cli").Argument<string>;
15
+ }, {
16
+ readonly type: import("@sdxc/cli").ValueFlag<string, true, false, false>;
17
+ readonly resolver: import("@sdxc/cli").ValueFlag<string, true, false, false>;
18
+ readonly wire: import("@sdxc/cli").Flag<boolean>;
19
+ readonly dnssec: import("@sdxc/cli").Flag<boolean>;
20
+ readonly json: import("@sdxc/cli").Flag<boolean>;
21
+ }>;
22
+ }, "flags" | "description" | "hidden" | "arguments" | "tasks">, Record<never, never>, Record<never, never>>, []>;
package/dist/cli.js ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The `sdxc-doh` command-line program: the contract in `cli-contract.ts` with `lookup` mapped
4
+ * onto its handler, which loads only when a lookup runs. Exit codes: 0 records found, 1
5
+ * NXDOMAIN, an empty answer or a resolver error, 2 usage or an unreachable resolver.
6
+ *
7
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
8
+ * @copyright Sergio Xalambrí 2026
9
+ */
10
+ import { createCLI, lazy } from "@sdxc/cli";
11
+ import { DOH_COMMANDS } from "./cli-contract.js";
12
+ /** The `sdxc-doh` program, run by the bin entry below and by tests through `invoke()`. */
13
+ export const cli = createCLI({ name: "sdxc-doh", commands: DOH_COMMANDS });
14
+ cli.map(DOH_COMMANDS.lookup, lazy(() => import("./cli-lookup.js")));
15
+ if (import.meta.main) {
16
+ process.exit(await cli.run(process.argv.slice(2)));
17
+ }
package/package.json CHANGED
@@ -1,21 +1,25 @@
1
1
  {
2
2
  "name": "@sdxc/doh",
3
- "version": "2026.10.25",
3
+ "version": "2026.11.10",
4
4
  "description": "Typed DNS over HTTPS lookups",
5
5
  "license": "MIT",
6
+ "bin": {
7
+ "sdxc-doh": "dist/cli.js"
8
+ },
6
9
  "type": "module",
7
10
  "exports": {
8
11
  ".": "./dist/index.js",
9
12
  "./caa": "./dist/caa.js"
10
13
  },
11
14
  "dependencies": {
15
+ "@sdxc/cli": "2026.11.10",
12
16
  "@sdxc/result": "2026.10.13",
13
- "@sdxc/zone-file": "2026.10.25"
17
+ "@sdxc/zone-file": "2026.11.10"
14
18
  },
15
19
  "peerDependencies": {
16
- "remix": "3.1.0"
20
+ "remix": "3.2.0"
17
21
  },
18
- "gitHead": "1440b969c0a3e3808dfcee3f17d2ef43915f32a8",
22
+ "gitHead": "6191b60a13885b1d826884d574c4a393f9b8eeee",
19
23
  "publishConfig": {
20
24
  "access": "public"
21
25
  },