English | 한국어
Test-support library with built-in rules that verify a running server against international standards and security requirements — from inside your test runner. You don't write assertions; you point it at a URL and it judges conformance over real bytes.
The rule is the atomic unit. ashward ships the rules and runs them against a live origin — which rules to run is your choice. It probes over one hop and never sends a
Cookie; credential rules are judged from response self-contradiction, the way a browser's CORS check reasons.
bun add -d @zipbul/ashwardPoint it at your running server inside any test runner. assertOk throws — with per-clause detail — on any blocking result, so a thrown error is the one universal failure signal.
import { test } from 'bun:test';
import { ashward, assertOk } from '@zipbul/ashward';
test('my server conforms to the built-in standards + security rules', async () => {
const report = await ashward('http://localhost:3000/api'); // runs every shipped rule
assertOk(report);
});Prefer to inspect instead of throw:
const report = await ashward('http://localhost:3000/api');
report.ok(); // boolean under the default policy
for (const clause of report.results) {
console.log(clause.ruleId, clause.verdict, clause.reason);
}The target is a URL, so the server under test can be written in any language.
The package ships rules, never an opinion about which ones your app must satisfy. ashward(url) runs every rule by default; pass your own selection to scope it.
import { ashward, rules, ALL_RULES, Rule } from '@zipbul/ashward';
// hand-pick specific rules
await ashward(url, [rules.accessControlAllowOriginGrammar, rules.originReflection]);
// or filter the full registry
await ashward(
url,
ALL_RULES.filter(r => r.id !== Rule.PrivateNetworkAccessIdNameFormat),
);Every rule has a stable, domain-free kebab id (e.g. access-control-allow-origin-wildcard-with-credentials), cites its normative source, and — for security rules — carries a CWE.
HTTP/1.1 framing (RFC 9112) — the parser-discrepancy class behind request smuggling:
| Rule id | Requirement |
|---|---|
duplicate-content-length |
§6.3 — reject two divergent Content-Length headers (MUST → fail) |
cl-te-conflict |
§6.1 — Content-Length + Transfer-Encoding ought to be an error (SHOULD → warn) |
WHATWG Fetch CORS — origin-server response conformance across the Fetch/URL/RFC/PNA clauses:
- Grammar & single-generation (§1–§2) —
Access-Control-Allow-Originserialized-origin grammar,Allow-Credentialsbyte-exacttrue, token-list headers,Max-Agedelta-seconds, single generation,*-with-credentials contradictions. - Preflight (§3) — ok status, method byte-case,
*-with-credentials, credentialed-grant consistency. - Actual & redirect (§4–§5) —
Expose-Headersplacement,Locationwithout userinfo. - Private Network Access (§6, WICG draft) —
Allow-Private-Networkliteraltrue, ID/Name format. - Caching (§7) —
Vary: Originwhen the answer varies by origin, and not when it's static. - Security heuristics — credentialed origin reflection and
null-origin grants (CWE-346 / CWE-942).
Response compression (RFC 9110 / 9111 / 9530, RFC 1950 / 1952 / 8878 / 9659) — content-coding conformance on the wire:
- Headers — no
identitytoken inContent-Encoding, no coding on a bodiless status,Vary: Accept-Encodingon a negotiated resource, a strongETagweakened or distinguished across the coded and identity representations. - Byte formats — a well-formed gzip header, zlib-wrapped
deflate(never raw), andzstdwithin the 8 MiB HTTP window cap (RFC 9659) with reserved bits zero — checked over the transfer-decoded body.
x-www-form-urlencoded query parsing (WHATWG URL / Encoding, RFC 3986) — judged against a route you opt in that echoes its parsed query as an ordered pair list:
- Robustness heuristics — malformed percent-escapes, invalid UTF-8, NUL bytes, and prototype-pollution vectors must not crash the origin (CWE-20 / CWE-1321), control-guarded so a flaky endpoint can't be blamed.
- Parse correctness —
&-only separation, first-=split,+→space (form) vs. literal (uri-generic), U+FFFD on invalid UTF-8, a preserved malformed%, skipped empty sequences, and a preserved empty name.
Conditional requests (RFC 9110 §13) — precondition evaluation over 304 / 412 / 200, from the resource's own discovered validators:
- Evaluation —
If-None-Match/If-Match(weak vs. strong comparison, §8.8.3.2),If-Modified-Since/If-Unmodified-Since, all three HTTP-date formats, and the §13.2.2 precedence order. - Response shape — the required
304headers (§15.4.5), no content on a304, and the ignore rules for non-selecting methods and error statuses.
The full roster is exported as ALL_RULES (and each rule by name under rules).
The query-parser reflection rules need a route that echoes its parsed query; pass it opt-in and declare the parse mode (every other rule keeps probing the URL's own path):
await ashward('http://localhost:3000/', ALL_RULES, {
reflect: { path: '/echo', mode: 'form' }, // or 'uri-generic'
});A rule sends its crafted probe(s) and classifies the wire response:
- pass — the server did the conformant thing
- fail — a MUST / MUST NOT violation (blocks by default)
- warn — a SHOULD-level or security concern (non-blocking by default)
- skip — the clause did not apply (the header it judges was absent), with a typed reason
- inconclusive — couldn't tell (timeout, malformed response), with a typed reason
report.ok(policy) is a view over the results, not a stored flag — you decide what blocks (failOn, how to treat inconclusive). It is fail-closed on connectivity: if ashward could not reach the server (dead host, wrong URL, refused, timeout) the report is never ok, whatever the policy. A reached-but-undecidable inconclusive is non-blocking by default; set inconclusive: 'fail' to block those too.
- HTTP over plaintext only — an
https:target throws (no TLS yet); point it at your server overhttp. - One hop, detection only — no CLI, no source scanning, no exploitation. Two-hop proxy⇄backend desync is out of scope.
- Not every clause is blackbox-testable — clauses that need server intent (e.g. "the server means to share this response") are intent-bound and honestly catalogued as untestable rather than guessed.
MIT