fix(shared): accept namespaced ext: tag keys in relay filters #1
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/ext-tag-filter"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The problem
swentel/nostr-phpvalidates every filter tag name against/^#[a-z_-]+$/i(Filter::validateTagName). The colons in the NIP-AMB extension grammar do not pass it:NostrFetcher::fetch()passes$filterParams['tags']straight tosetTag(), so everyext:facet filter throws before the REQ is built — andext:tag-key filtering is the primary filter mechanism for the Materialpool plugin.A version bump does not fix this. The regex is byte-identical in 1.9.2 (vendored), 1.9.3, 1.9.4 and upstream
main.The fix
validateTagNameisprivate, so it cannot be overridden.setTagispublicand is its only caller inside its own body, soExtendedFilteroverridessetTag/setTagsand inherits everything else — dynamic property storage,toArray(), and therefore the generated REQ. The wire format stays the parent's, which is what makes the override safe.The replacement pattern is
/^#[a-zA-Z0-9_.:-]+$/. The leading#stays mandatory: a key without it is silently skipped by the relay's filter parser, so the query runs unfiltered — a typo would widen the result set instead of erroring.Single-letter tags are unaffected;
#l,#Land#dalready passed upstream and still do.Evidence
Live against
wss://amb-relay.edufeed.org, throughNostrFetcheritself:The bogus-value control is part of the evidence, not decoration: if a bogus value returned the same count as a real one, the filter would be a no-op and the number meaningless.
Verified non-vacuous: delegating
setTagback toparent::setTagfails 3 of the 15.One test asserts that the upstream
Filterstill rejects#ext:.... If that test ever fails, upstream has widened its regex andExtendedFiltercan be deleted.Follow-ups, not in this PR
nostrver-se/nostr-php, so this class can eventually go away.swentel/nostr-phpis pinned>=1.5 <1.9.3and 1.9.4 is out. Upgrading pulls in the replacement for the abandonedleigh/chacha20and a request timeout property, but raises the floor to PHP 8.2 —CLAUDE.mddocuments the stack as PHP 8.1+, so that is a redistributability decision, not a mechanical bump.swentel/nostr-php validates every filter tag name against `/^#[a-z_-]+$/i` (Filter::validateTagName), so the colons in the NIP-AMB extension grammar are rejected: `#ext:ekw:schoolType:id` throws a RuntimeException before the REQ is ever built. NostrFetcher::fetch() passes `$filterParams['tags']` straight to setTag(), so every ext: facet filter — the primary filter mechanism for the Materialpool plugin — was unreachable. The regex is byte-identical in 1.9.2 (vendored), 1.9.3, 1.9.4 and upstream main, so a version bump does not fix it. validateTagName is private and cannot be overridden; setTag is public and is its only caller inside its own body, so ExtendedFilter overrides setTag and setTags and inherits everything else — dynamic property storage, toArray(), and therefore the generated REQ. The wire format stays the parent's, which is what makes the override safe. Single-letter tags are unaffected: `#l`, `#L` and `#d` already passed the upstream regex and still do. Verified against wss://amb-relay.edufeed.org through NostrFetcher itself: #ext:ekw:gradeLevel:id = <real value> -> 3 #ext:ekw:gradeLevel:id = <bogus value> -> 0 (twice) #l = ekw -> 33 (COUNT agrees: 33) kinds only, no tag filter -> 100 (page limit) A bogus value returning the same count as a real one would mean the filter is a no-op, so the control is part of the evidence, not decoration. phpunit: 15/15 in shared/. Verified non-vacuous — delegating setTag back to parent::setTag fails 3 of them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>