Skip to content

fix(www): apply domain-specificity precedence in Shield rule matching - #516

Merged
mark-hoffmann merged 1 commit into
mainfrom
mark-hoffmann/shield/rule-precedence
Oct 23, 2026
Merged

mark-hoffmann merged 1 commit into
mainfrom
mark-hoffmann/shield/rule-precedence

Conversation

@mark-hoffmann

Copy link
Copy Markdown
Contributor

Problem

The Shield matching contract said overlapping rules were "first-wins" with servers ordering "specific-before-general", but neither the publisher nor the matchers implemented a precedence semantic:

  • This Worker stopped at the first version-matching rule and returned none if that rule's domains did not cover the host — even when a later rule did cover it.
  • The Shield API simulator scanned for the first domain-covered rule in feed order, and the publisher emitted rules in database order.

For a feed like [pin [email protected] for domain-a, block [email protected] global], a request from domain-b was blocked according to the simulator and served the requested version by the edge. The two implementations could disagree about whether a domain is protected, and the outcome depended on incidental rule order.

Change

packages/unpkg-www/src/shield.ts now applies explicit precedence, mirrored exactly by the Shield API (edge_matching.py, already deployed) and locked by the shared conformance fixture:

  1. Package + exact version must match (unchanged).
  2. The rule with the most specific domain match for the requesting host wins: a matching named domain beats *; a longer matching domain (app.example.com) beats a shorter parent (example.com); a rule's specificity is its strongest matching domain. Absent domains is global; an empty list covers nothing.
  3. At equal specificity, block beats pin.
  4. Remaining ties keep feed order.

Also aligned with the API matcher on malformed feed fields, which precedence makes more consequential: pins with an empty to, empty or unknown actions, a non-list from, a non-string pkg, and a present-but-non-list domains (covers nothing instead of silently widening to global) are all ignored on both sides.

No change to rule loading, KV caching, cache keys, TTLs, or the block stub. Non-Shield traffic is untouched: the matcher returns before host parsing when the package has no rules, and a none decision leaves the request byte-identical.

Behavior

Feed (same pkg+version) Request from Before After
[pin domain-a, block global] domain-a pin pin
[pin domain-a, block global] domain-b served (*) block
[block global, pin domain-a] domain-a block pin
[pin global, block domain-a] domain-a pin block
[pin domain-a, block domain-a] domain-a pin block
[pin parent-domain, pin subdomain] subdomain first-listed subdomain

(*) The old result here depended on rule order; the block was skipped whenever the domain-a rule was listed first.

Tests

  • The shared conformance fixture grows from 32 to 52 cases: domain over global in both directions and both orders, block over pin at equal specificity and globally, parent-vs-subdomain depth, mixed domain lists, absent/empty domains, Origin fallback, exact-tie feed order in both directions, uppercase/trailing-dot tokens, and the malformed-field cases above. The fixture is byte-identical to the copy in the Shield API repo, where the same cases pass.
  • Before this change, all precedence cases fail against the old matcher; the malformed-field cases were verified to diverge between the two implementations and now agree.
  • unpkg-www: 136 tests pass. pnpm run build passes.
  • Verified on staging with seeded overlapping rules for [email protected] (global block listed first, domain pins for domain-a.test/b.test, deeper pin for app.b.test): domain requests received the pinned bytes with no-store, other domains received the 403 block, deeper subdomains followed the more specific pin, and referrer-less requests were served unshielded with normal caching. The staging feed was restored to empty afterwards.

@mark-hoffmann
mark-hoffmann merged commit dc8b61f into main Oct 23, 2026
1 check passed
@mark-hoffmann
mark-hoffmann deleted the mark-hoffmann/shield/rule-precedence branch October 23, 2026 03:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant