@propgate/dns › Recipes
Recipes
Three complete files, each one thing a caller of the library actually needs to do.
Check one domain and switch on the verdict
The common case: build a profile, run it, and branch on the result. pass
and warn both satisfy the profile; fail and indeterminate both leave it
unsatisfied, and the difference between those two is "broken" versus "could
not tell", which is worth a different message to whoever reads the log.
import { runChecks, sendingOnly } from "@propgate/dns";
const profile = sendingOnly({
dkimSelectors: ["resend"],
spfInclude: "_spf.resend.com",
});
const result = await runChecks({
domain: "customer.example",
profile,
resolver: { target: { address: "8.8.8.8", port: 53 } },
});
switch (result.verdict) {
case "pass":
console.log(`${result.domain}: every check passed`);
break;
case "warn":
console.log(`${result.domain}: passing, with warnings worth reading`);
break;
case "fail":
console.log(`${result.domain}: not configured correctly`);
break;
case "indeterminate":
console.log(`${result.domain}: could not be checked right now — try again`);
break;
}Query a specific resolver, on a non-standard port
Nothing in runChecks or query assumes port 53. resolver.target is
always { address, port, transport }, so pointing every lookup at a resolver
container, or any nameserver that happens to run somewhere other than 53, is
the same call as pointing at a public resolver:
import { runChecks, webOnly } from "@propgate/dns";
// A resolver container listening on a non-standard port, the way this
// repo's own DNS fixture tier does. Production nameservers still listen on
// 53, but nothing about the resolver assumes that — the target is always
// { address, port, transport }.
const result = await runChecks({
domain: "customer.example",
profile: webOnly({ caaIssuer: "letsencrypt.org" }),
resolver: {
target: { address: "127.0.0.1", port: 8053, transport: "udp" },
},
});
console.log(result.verdict);This is the same reason the fixture tier behind packages/dns's own tests
needs target to carry a port at all: it serves real DNS on real port 53
across several loopback addresses normally, and on a high port instead on
macOS, where only one loopback address is available. A resolver that
hardcoded either the address or the port could not run against both.
Read the lookups behind a finding
A verdict and a code are enough for a switch statement. They are not
enough for a support reply. Every CheckOutcome carries its own
derivation: every lookup made, the name, why it was made, and what came
back. Explaining a result to a customer does not mean re-running the check by
hand to reconstruct what happened.
import { fullMail, outcomeFor, runChecks } from "@propgate/dns";
const result = await runChecks({
domain: "customer.example",
profile: fullMail({ dkimSelectors: ["resend"] }),
resolver: { target: { address: "8.8.8.8", port: 53 } },
});
const dkim = outcomeFor(result, "dkim");
if (dkim === undefined) {
console.log("DKIM was not asked for on this profile");
} else if (dkim.verdict === "pass") {
console.log("DKIM is set up correctly");
} else {
// The findings say what's wrong; the lookups say how we know. A customer
// asking "why does your dashboard say this is broken" gets the name
// queried, what came back, and which finding that answer produced —
// nothing here has to be re-derived by re-running the check by hand.
for (const finding of dkim.findings) {
console.log(finding.code, "—", finding.evidence.detail ?? finding.evidence.observed);
}
for (const lookup of dkim.lookups) {
console.log(lookup.name, lookup.purpose, "->", lookup.outcome.status);
}
}outcomeFor returns undefined when the profile never asked about that check
kind, which is a skipped check rather than a passing one. That is why the
sample checks for it before reading .verdict.