DNS email security analyzer (DMARC, SPF, DKIM, BIMI, MTA-STS). Cloudflare Worker serving dual output: JSON API + interactive HTML report. Live at dmarc.mx | Repo: github.com/schmug/dmarcheck
- Hono framework on Cloudflare Workers
- TypeScript with
nodejs_compatflag fornode:dns - Vitest for testing
- No build step for HTML — template literal strings in src/views/
npm run dev— local dev on port 8790npm test— vitestnpm run lint— biome check (lint + format check)npm run lint:fix— biome auto-fixnpm run typecheck— tsc --noEmitnpm run deploy— wrangler deploy (use only if Git integration is disabled)- Deployment is automatic via Cloudflare Git integration on push to main
- Do NOT run
npm run deployafter pushing — it collides with the Git integration auto-deploy and causes intermittent stale deploys
src/index.ts— Hono routes, content negotiation, rate limiting middlewaresrc/dns/client.ts— DNS abstraction over node:dns (NXDOMAIN returns null)src/analyzers/— One module per protocol (dmarc, spf, dkim, bimi, mta-sts)src/orchestrator.ts— Runs all analyzers in parallel and isolates each one: a single analyzer rejection surfaces as a syntheticstatus: "fail"result instead of aborting the whole scan (per-analyzersettlewrapper, for bothscanandscanStreaming— #378)src/shared/scoring.ts— Grade computation (F if no DMARC or p=none)src/cache.ts— SSE result cachingsrc/csv.ts— CSV export for scan resultssrc/api/catalog.ts+src/api/openapi.ts— Agent discovery (RFC 9727 linkset at/.well-known/api-catalog, OpenAPI 3.1 at/openapi.json)src/views/— HTML generation via template literals (styles.ts, scripts.ts, components.ts, html.ts, favicon.ts)components.ts—generateCreature(size, mood, partyHat?)helper andgradeToMood()mappingmarkdown.ts— markdown renderings served whenAccept: text/markdown(landing, /check report, /scoring, /learn, /docs/api)
src/rate-limit.ts— Cache API-based rate limiter (10 req/IP/60s)
/.well-known/api-catalog— RFC 9727 linkset (application/linkset+json) pointing to OpenAPI + docs + health/.well-known/agent-skills/index.json— Cloudflare Agent Skills Discovery RFC v0.2.0 index. Listsscan_domainin three formats (markdown SKILL.md, OpenAPI, and MCP) with sha256 digests computed lazily over the served bytes/.well-known/agent-skills/scan-domain/SKILL.md— prose description of thescan_domainskill, served astext/markdown/.well-known/mcp/server-card.json— SEP-1649 MCP server card (pointing to the tool caller endpoint at/mcp)/openapi.json— OpenAPI 3.1 service description (application/openapi+json)/docs/api— Human-readable API reference (HTML, or markdown withAccept: text/markdown)- Every HTML page ships a
Linkheader advertising five relations (api-catalog,https://agentskills.io/rel/index,service-desc,service-doc,status) - Content negotiation:
Accept: text/markdown(or?format=md) on/,/check,/scoring,/learn,/docs/apireturns a markdown rendering (noindexed) - The client JS bundle registers a WebMCP
scan_domaintool vianavigator.modelContext.provideContext()when that API is available — silent no-op in browsers without WebMCP - Intentionally not published:
/.well-known/openid-configuration,/.well-known/oauth-authorization-server,/.well-known/oauth-protected-resource. We are not an OAuth/OIDC issuer (WorkOS AuthKit is — we're the relying party), and our protected APIs use dmarcheck-minted bearer API keys (not OAuth-issued tokens, so RFC 9728 doesn't fit).
- Each analyzer is a standalone async function returning a typed result
- DNS errors (NXDOMAIN/NODATA) return null, not exceptions
- Status is
"pass"|"warn"|"fail"for scored protocols,"info"for informational (MX) - HTML is generated server-side as template literal strings, no JSX or build step
- Client-side JS is minimal (expand/collapse, tooltips) — inline script tag
- Dark/light theme with OS-aware switching and manual toggle; orange accent (#f97316)
- DMarcus is the site mascot — an orange
@character with googly eyes and three legs (pun on DMARC) - Rendered by
generateCreature(size, mood, partyHat?)insrc/views/components.ts - Moods map to scan grades via
gradeToMood(): celebrating (A+/A), content (B), worried (C), scared (D), panicked (F) - Party hat variant with dance animation for S (perfect) grade
- Sizes: lg (landing page logo), md (grade reactions, footer), sm (nav links)
- Appears in landing page, report header, loading state, error page, and nav
- Easter egg: idle-triggered (60s) creature walks around eating page elements, panics on interaction; respects
prefers-reduced-motion - Name appears in footer ("Guarded by DMarcus"), loading text, aria labels, and README
- Social preview / OG image:
scripts/generate-icons.mjsrasterizes the OG SVG intoOG_IMAGE_PNG_BASE64(served at/og-image.png, referenced byog:image/twitter:image) and writesdocs/github-social-preview.png(1280×640). When the OG design changes, re-run the script, paste the new base64 intosrc/views/favicon.ts, commit the regenerated PNG, and re-upload it at GitHub → Settings → General → Social preview (that field has no API).
- Biome handles linting and formatting (
biome.json) - Codex hooks auto-format on edit and run tests + typecheck before commits
- Run
npm run lintto check,npm run lint:fixto auto-fix
- Runners: All CI runs on GitHub-hosted
ubuntu-latest. Do not reintroduceself-hosted— this is a public repo withpull_requesttriggers and self-hosted runners are a known RCE-on-runner pattern. - Action pinning: All actions in
.github/workflows/*are pinned by full commit SHA with a# v<version>comment. Dependabot (.github/dependabot.yml) keeps them up to date weekly. - Workflow permissions: Every workflow declares an explicit top-level
permissions:block (defaultcontents: read). Elevate at the job level only where needed (e.g.,release.ymlfor tag push). - Branch protection:
mainrequires thecheckstatus from CI to pass before merging, blocks force pushes and deletions, and requires a PR. - Secret scanning: Secret scanning, push protection, non-provider patterns, and validity checks are all enabled in repo settings. Never commit
.env, tokens, or wrangler secrets. - Input validation: User-supplied domains are restricted to
[a-z0-9.-]innormalizeDomain(src/index.ts). DKIM selectors are restricted to[A-Za-z0-9._-]inparseSelectors. HTML output never interpolates raw user input into inline<script>blocks — usedata-*attributes viaesc()instead. - MTA-STS fetch redirect mode:
src/analyzers/mta-sts.tsusesredirect: "manual"for the policy fetch. Do NOT change it to"error"— that throws in the Cloudflare Workers fetch runtime and breaks every scan (regressed twice via PRs #58 and #92)."manual"is RFC 8461 §3.3-compliant: redirects yield an opaque-redirectResponserejected by the existingresp.type === "opaqueredirect"/!resp.okguards. - Reporting: See
SECURITY.mdfor the private disclosure process.
- Migrations live in
src/db/migrations/, namedNNNN_description.sqlwith a monotonically-increasing 4-digit prefix. Pick the next prefix by listing the directory — never reuse one (PR #154 collided on0003_and had to be renamed). - Every schema change updates both
src/db/schema.sql(fresh-DB shape) and a new migration file (delta against prod). The migration is what runs against the live D1;schema.sqlis what self-hosters apply on first install. - Additive-only: new tables, new nullable or defaulted columns, new indexes. Column drops, renames, and type changes go through a two-PR expand/contract because
.github/workflows/migrate.ymland the Cloudflare Git auto-deploy run in parallel — there is no ordering guarantee between schema change and code change. - Migrations apply automatically:
.github/workflows/migrate.ymlrunswrangler d1 migrations apply dmarcheck-db --remoteafter CI passes onmain. Do not runnpx wrangler d1 execute --file=...by hand anymore. - Wrangler tracks applied migrations in the
d1_migrationstable. If a migration is added, applied manually, and then automation tries to replay it,ALTER TABLE ADD COLUMNwill fail. If you ever apply one out of band, alsoINSERT INTO d1_migrations (name) VALUES ('NNNN_description.sql')so the workflow skips it.
- Tests in
test/directory - Mock DNS client for unit tests (
vi.mock) - Test scoring boundaries and analyzer parsing
- Automated via GitHub Actions on push to main (after CI passes)
- CalVer versioning: vYYYY.M.serial (e.g., v2026.4.1)
- Changelog generated from commit history by git-cliff (
cliff.toml) - GitHub Releases page is the project changelog
- Tags are created automatically; do not create manual tags
- After committing or merging work, check open issues (
gh issue list) to see if any were resolved and should be closed - When a commit addresses an issue, close it with a comment referencing the commit hash
- Keep
AGENTS.mdandREADME.mdup to date when adding features, changing architecture, or modifying conventions AGENTS.mdis for AI assistants and contributors;README.mdis for users and self-hosters
- Cloudflare MCP server (
@cloudflare/mcp-server-cloudflare) is configured globally in~/.Codex.json - Provides tools for managing Workers, DNS, KV, D1, R2, and more from Codex
- Account ID:
f0fc4ca5b74274f7ba892e6c9ec411a7