propgate docs

@propgate/dnsRecipes

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.