🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@sitedex/web-bot-auth-lint

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sitedex/web-bot-auth-lint

Lint a Web Bot Auth key directory (/.well-known/http-message-signatures-directory) against the IETF drafts and JWK RFCs, and verify its HTTP-message signature. Zero runtime dependencies.

latest
Source
npmnpm
Version
0.2.0
Version published
Maintainers
1
Created
Source

@sitedex/web-bot-auth-lint

npm version license CI

Grade a published Web Bot Auth key directory for the mistakes that quietly break verification: an expired key, the wrong curve, a signature a verifier will reject, or a private signing key left in a file the whole internet can read. Point it at a /.well-known/http-message-signatures-directory and get back a graded report of what a verifier will reject.

For anyone who publishes a directory so crawlers can be recognized, or who audits someone else's.

Zero dependencies. Node 20+, or any runtime with Web Crypto (browsers, Cloudflare Workers, Deno). Apache-2.0.

Linting vs. signing/verifying

cloudflare/web-bot-auth signs and verifies Web Bot Auth requests. This package lints the directory you publish. It reads the JWK Set at your well-known path, checks the keys and the signature on the response, and reports what would stop a verifier from accepting it.

Standards lineage

The rules come from four places. The linter keeps them straight and tags each finding with its source (see the output model).

  • RFC 9421 — HTTP Message Signatures. The general IETF standard for signing an HTTP message. Not bot-specific; it's the mechanism underneath everything else.
  • The directory draft (draft-meunier-http-message-signatures-directory). Its own IETF draft. Defines the /.well-known/http-message-signatures-directory endpoint: you publish public keys as a JWK Set and sign the directory response.
  • Web Bot Auth (draft-meunier-web-bot-auth-architecture). The application. An agent proves who it is by signing its requests with a key from that directory.
  • The JWK family: RFC 7517 (JWK), 7518 (JWA), 7638 (JWK Thumbprint), 8037 (Ed25519 in JOSE).

Ed25519-only is not in the spec. The architecture draft ships RSA-PSS test vectors and only forbids a shared HMAC secret. Ed25519-only and HTTPS-only are constraints of the deployed verifier, chiefly Cloudflare, which accepts any valid Ed25519 key in your directory. Cloudflare and Akamai both implement the drafts; nobody owns them. So the linter attributes those two rules to the verifier, not the spec, and says so in every finding.

Install

npm i @sitedex/web-bot-auth-lint

Quickstart

The library does not fetch. You fetch the directory and its body; validateDirectory grades what you pass it. That keeps it dependency-free and lets you validate a captured directory offline.

The easy path: directoryInputFromResponse reads the response headers for you, including the signature semantics (see below).

import { validateDirectory, directoryInputFromResponse } from '@sitedex/web-bot-auth-lint';

const dirUrl = 'https://example.com/.well-known/http-message-signatures-directory';
const res = await fetch(dirUrl);

const report = await validateDirectory(directoryInputFromResponse(res, await res.text()));

console.log(report.grade); // 'pass' | 'fail'
for (const f of report.findings) {
  console.log(`[${f.grade}] ${f.check}: ${f.detail}`);
}

Fail CI on a broken directory:

if (report.grade === 'fail') process.exit(1);

validateDirectory never throws on directory content. Malformed JSON, a missing keys array, or garbage input become a fail finding (e.g. json_parse), not an exception, so a broken directory is still a graded report. You handle fetch rejections yourself, since the library doesn't fetch.

Building the input yourself

Have the pieces already (a proxied fetch, a stored response, a fixture)? Build the DirectoryInput directly:

import { validateDirectory, type DirectoryInput } from '@sitedex/web-bot-auth-lint';

const input: DirectoryInput = {
  rawBody: body,
  status: 200,
  contentType: 'application/http-message-signatures-directory+json',
  cacheControl: 'max-age=86400',
  finalUrl: dirUrl,
  redirected: false,

  // Signature fields are optional, and the undefined-vs-null distinction is
  // load-bearing:
  //   • omit them entirely (undefined) → the linter runs NO signature checks
  //     (you're grading only the JWKS document).
  //   • pass null (you looked for the header and it was absent) → the linter
  //     warns that the response is unsigned.
  signature: null,
  signatureInput: null,
  contentDigest: null,
  requestMethod: 'GET',
};

const report = await validateDirectory(input);

directoryInputFromResponse gets this right for you: an absent Signature header comes back as null (you looked, it's unsigned), so you get the unsigned-directory warning automatically.

CLI

For a 30-second check without writing code, run it with npx. The CLI is the one piece that fetches (the library never does), so this is the whole thing:

npx @sitedex/web-bot-auth-lint example.com

Pass a bare domain and it expands to the well-known directory URL. Pass a full URL and it fetches that as-is. It prints a report grouped by authority, colorized by grade.

--json prints the raw DirectoryReport for scripting or piping into jq:

npx @sitedex/web-bot-auth-lint example.com --json

The exit code is the CI contract: 0 when the directory passes, 1 when it fails, 2 on a usage or fetch error. Color follows your terminal, and turns off when the output is piped or when NO_COLOR is set. The fetch is a well-behaved one: a 10-second timeout, a capped body read, and a descriptive User-Agent.

The output model

Every finding has two independent axes:

  • grade (pass | warn | fail): how bad it is.
  • authority: who says so. Lets you separate a real spec violation from Cloudflare's house rules from our own advice.

A fail under spec-must is a genuine standards violation. The same fail under linter-opinion is just advice.

interface DirectoryReport {
  grade: 'pass' | 'fail';   // fail if ANY finding failed; warns don't demote it
  findings: Finding[];
  keys: KeySummary[];
  fetchedAt: string;        // ISO timestamp
}

interface Finding {
  check: string;            // stable id, e.g. 'private_key_material'
  grade: 'pass' | 'warn' | 'fail';
  detail: string;           // human-readable; safe to render as a text node
  authority: Authority;
  specRef?: string;         // citation token, e.g. 'RFC 8037 §2'
  hero?: boolean;           // true only on the leaked-private-key finding
}

interface KeySummary {
  kid: string | null;
  crv: string | null;
  kty: string | null;
  daysToExpiry: number | null;  // floored; null when the key has no `exp`
  thumbprintMatches: boolean;   // does kid equal the key's RFC 7638 thumbprint
}

Only a fail finding demotes the report; a warn-only directory still passes. specRef resolves to a datatracker URL via the exported specRefUrl().

Example report (pass with warnings)

A valid Ed25519 key that expires soon, served without a Cache-Control header. The directory still verifies, so grade is 'pass', with two warn findings (passing findings trimmed):

{
  "grade": "pass",
  "fetchedAt": "2026-07-02T14:07:00.000Z",
  "keys": [
    {
      "kid": "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs",
      "crv": "Ed25519",
      "kty": "OKP",
      "daysToExpiry": 9,
      "thumbprintMatches": true
    }
  ],
  "findings": [
    // ...passing checks (https, reachable, x_valid, private_key_material, ...) omitted
    {
      "check": "exp_hygiene",
      "grade": "warn",
      "detail": "Key expires soon: NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs (9 day(s)).",
      "authority": "linter-opinion",
      "specRef": "draft-meunier-http-message-signatures-directory-05 §3"
    },
    {
      "check": "cache_control",
      "grade": "warn",
      "detail": "No Cache-Control header. Add one so verifiers can cache the directory instead of refetching every time.",
      "authority": "linter-opinion"
    }
  ]
}

The authority axis

authorityMeaning
spec-mustA MUST in RFC 9421, the JWK RFCs, or the directory draft. Breaking it is a genuine violation.
spec-shouldA SHOULD in those same standards. Worth following, not strictly required.
verifier-requirementNot in any spec. Enforced by the verifier that reads your directory, chiefly Cloudflare (Ed25519-only, HTTPS-only).
linter-opinionOur own hardening advice. A suggestion.

Presentational helpers

These ship with the package so a web UI, a CLI, and an OG-card renderer render the same words:

  • CHECK_LABELS: Record<string, string> — each check id to a short label (private_key_material"Private key material").
  • AUTHORITY_LABELS: Record<Authority, { label: string; blurb: string }> — each authority to a heading and one line of context. Read AUTHORITY_LABELS[f.authority].label, not the object itself.
  • AUTHORITY_ORDER: Authority[] — the four authorities, most-authoritative first, for rendering grouped sections in a stable order.
  • DIRECTORY_SIGNATURE_TAG — the tag string a directory-response signature must carry (what signature_tag checks for).

The checks

Every check id, grouped. ✱ marks a sibling-aware check: Cloudflare uses any valid Ed25519 key it finds, so one bad key beside a usable one is a warn. It fails only when no usable key is left.

Transport & document

Check idWhat it checksWorst gradeAuthority
httpsServed over HTTPSfailverifier-requirement
reachableReturns HTTP 200failverifier-requirement
not_redirectedServed directly, no redirectwarnlinter-opinion
well_known_pathLives at the well-known pathwarnverifier-requirement
content_typeCorrect media type (…-directory+json)warnspec-must
json_parseBody is valid JSONfailspec-must
keys_presentHas a usable keys arrayfailspec-must
malformed_entriesEvery keys entry is a JWK objectwarnspec-should
keys_cappedKey count within the linter's evaluation capwarnlinter-opinion

Key hygiene

Check idWhat it checksWorst gradeAuthority
kty_okpKey type is OKPfailverifier-requirement
crv_ed25519Curve is Ed25519failverifier-requirement
x_validPublic key is a 32-byte Ed25519 value ✱failspec-must
thumbprint_kidkid equals the key's RFC 7638 thumbprint (its fingerprint)warnlinter-opinion
unique_kidsKey IDs are distinct within the setwarnspec-should
cache_controlCache-Control lets a verifier cache the directorywarnlinter-opinion
nbf_hygieneNo usable key is stuck before its not-before time ✱failspec-should
exp_hygieneNo usable key is expired, and none expires within 14 days ✱failspec-should / linter-opinion
private_key_materialNo private signing key is published (the headline)failspec-must

private_key_material is why this tool exists. If any private JWK member (d, p, q, dp, dq, qi, oth, k) appears in a published key, anyone who reads the directory can impersonate the bot. Always a hard fail, and it carries hero: true for prominent rendering.

Response signature (structural)

The shape of the signature the origin puts on the directory response. Pure, no crypto, always safe to run.

Check idWhat it checksWorst gradeAuthority
response_signatureThe response is signed at allwarnverifier-requirement
signature_tagThe signature's tag is the directory tagfailspec-must
signature_keyid_thumbprintThe signature's key ID is the thumbprint of an advertised keyfailspec-must

Response signature (cryptographic)

Check idWhat it checksWorst gradeAuthority
signature_verifiedThe Ed25519 signature actually verifiesfailverifier-requirement
signature_freshnessA valid signature hasn't expired or isn't post-datedwarnverifier-requirement

signature_verified only emits fail on a provably bad signature: the header parsed, every covered component was rebuilt byte-for-byte, the key is Ed25519, and the check returned false. Any uncertainty (an unparseable header, a component it can't rebuild, a non-Ed25519 algorithm, a runtime without Ed25519) is a warn, "could not verify". A correctly-signed directory can never be failed.

signature_freshness runs only after a signature verifies. A valid-but-expired signature is a warn, not a fail: it isn't forged, but a verifier that enforces expiry will likely reject it.

Two things to know before you ship

  • Escape Finding.detail before rendering it as HTML. It's safe as a text node, but some strings echo values from the directory (a kid, a served path). Escape it if you build raw HTML.
  • A pass means self-consistent, not authentic. A verified signature proves the response is cryptographically consistent with the directory's own advertised key. It does not prove the bot is who it claims. That check happens at request time, against a directory the verifier independently trusts.

Versioning

This release targets draft-meunier-http-message-signatures-directory-05 and RFC 9421. Drafts move. When a new draft changes behavior, check ids can be added, renamed, or regraded. While the package is pre-1.0, treat check ids as semi-stable: new ids may land in a minor release, and any rename or regrade will be called out in the changelog. If you gate CI on specific check ids, pin an exact version.

In production

Runs in production behind Sitedex's hosted checker at sitedex.dev/tools/keycheck. Same code, same findings.

Contributing

A new check is one add(check, grade, detail, authority, specRef?) call in validateDirectory (src/index.ts), or a SigFinding in src/signature.ts for a signature check. Two rules the test suite enforces:

  • Every check id needs a CHECK_LABELS entry in src/refs.ts, or a label-coverage test fails CI.
  • Every finding must declare an authority. If the rule is a real MUST/SHOULD, cite it with a specRef token that resolves through specRefUrl(). If it's Cloudflare's behavior or your own advice, say so; don't dress advice up as a standard.

License

Apache-2.0 © Sitedex

Keywords

web-bot-auth

FAQs

Package last updated on 06 Jul 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts