From 9afbc0cbbb966799f05ed526911caacb46a586a4 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 2 Jul 2026 23:12:20 -0400 Subject: [PATCH] =?UTF-8?q?[search]=20Decode=20npub=20authors=20to=20hex?= =?UTF-8?q?=20=E2=80=94=20the=20Author=20filter=20matched=20nothing=20(npu?= =?UTF-8?q?bs)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Author field (and the author: token) passed values through as-typed, but the matcher compares against video.pubkey — HEX. So entering an npub (the only format users actually have) silently returned zero results; only raw hex worked. New normalizeAuthorValues (searchFilters.js, injectable decoder for tests): npub1* decodes via safeDecodeNpub to hex, hex passes through lowercased, and undecodable values are dropped — surfaced as parse errors on the author: token path. Applied in BOTH the parser and the filter modal's applyFilters, so the Author panel field and the token syntax now genuinely filter by creator. Tests: +2 in tests/search-filters.test.mjs (decode/passthrough/drop with an injected decoder; parser hex passthrough + garbage error). Lint + build green. Co-Authored-By: Claude Opus 4.8 --- js/search/searchFilters.js | 37 ++++++++++++++++++++++- js/ui/components/SearchFilterModal.js | 7 +++-- tests/search-filters.test.mjs | 42 +++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 4 deletions(-) diff --git a/js/search/searchFilters.js b/js/search/searchFilters.js index d0dc4474..e75a9bc5 100644 --- a/js/search/searchFilters.js +++ b/js/search/searchFilters.js @@ -1,3 +1,32 @@ +import { safeDecodeNpub } from "../utils/nostrHelpers.js"; + +const HEX64 = /^[0-9a-f]{64}$/; + +// Authors arrive as npubs (what users actually have) or hex, but videos carry +// HEX pubkeys — without decoding, npub authors silently matched nothing. +// Undecodable values are dropped (returned in `dropped` for error surfaces). +export function normalizeAuthorValues(values, { decodeNpub = safeDecodeNpub } = {}) { + const normalized = []; + const dropped = []; + for (const raw of Array.isArray(values) ? values : []) { + const value = typeof raw === "string" ? raw.trim() : ""; + if (!value) continue; + if (HEX64.test(value.toLowerCase())) { + normalized.push(value.toLowerCase()); + continue; + } + if (value.toLowerCase().startsWith("npub1")) { + const hex = typeof decodeNpub === "function" ? decodeNpub(value) : null; + if (hex && HEX64.test(hex.toLowerCase())) { + normalized.push(hex.toLowerCase()); + continue; + } + } + dropped.push(value); + } + return { normalized, dropped }; +} + const DATE_FORMAT_OPTIONS = { year: "numeric", month: "2-digit", @@ -176,7 +205,13 @@ export function parseFilterQuery(inputString = "") { errors.push({ token, message: "Author value is empty." }); break; } - filters.authorPubkeys.push(...values); + { + const { normalized, dropped } = normalizeAuthorValues(values); + filters.authorPubkeys.push(...normalized); + for (const value of dropped) { + errors.push({ token, message: `Could not decode author "${value}".` }); + } + } break; } case "tag": { diff --git a/js/ui/components/SearchFilterModal.js b/js/ui/components/SearchFilterModal.js index 5320a072..ccf0163a 100644 --- a/js/ui/components/SearchFilterModal.js +++ b/js/ui/components/SearchFilterModal.js @@ -9,7 +9,7 @@ import { buildSearchHashFromState, resetSearchFilters, } from "../../search/searchFilterState.js"; -import { DEFAULT_FILTERS } from "../../search/searchFilters.js"; +import { DEFAULT_FILTERS, normalizeAuthorValues } from "../../search/searchFilters.js"; import { setHashView } from "../../hashView.js"; import { ALLOW_NSFW_CONTENT } from "../../config.js"; @@ -349,8 +349,9 @@ function applyFilters() { filters.sort = sortSelect.value; // Author - const authors = authorInput.value.split(/[, ]+/).map(s => s.trim()).filter(Boolean); - filters.authorPubkeys = authors; + const rawAuthors = authorInput.value.split(/[, ]+/).map(s => s.trim()).filter(Boolean); + // Decode npubs to hex — videos carry hex pubkeys, so raw npubs matched nothing. + filters.authorPubkeys = normalizeAuthorValues(rawAuthors).normalized; // Tags const selectedTags = new Set(); diff --git a/tests/search-filters.test.mjs b/tests/search-filters.test.mjs index d11cb34d..586d114d 100644 --- a/tests/search-filters.test.mjs +++ b/tests/search-filters.test.mjs @@ -116,3 +116,45 @@ test("sort: trending weights views by recency", () => { ); assert.deepEqual(ranked, ["fresh-riser", "old-hit"]); }); + +// Author values arrive as npubs but videos carry hex pubkeys — without decoding, +// npub authors silently matched nothing (the "wired but wrong" filter). +// +// test_integrity_note: +// change_type: ["new_tests"] +// scenarios: +// - id: SCN-search-author-npub +// given: "author values as npub / hex / garbage" +// when: "normalizeAuthorValues runs (with an injected decoder)" +// then: "npubs decode to hex, hex passes through lowercased, garbage is dropped with errors" +// observable_outcomes: +// - "npub -> decoded hex; HEX passthrough lowercased; undecodable -> dropped" +// - "parser author: token emits decode errors for garbage" +// determinism_controls: +// - "injected decodeNpub stub (no window.NostrTools dependency)" +// anti_cheat_rationale: +// prevents: ["hard-coded return value"] +// relaxation: +// did_relax_any_assertion: false + +test("normalizeAuthorValues decodes npubs, passes hex, drops garbage", async () => { + const { normalizeAuthorValues } = await import("../js/search/searchFilters.js"); + const HEX = "a".repeat(64); + const decodeNpub = (v) => (v === "npub1good" ? HEX : null); + + const { normalized, dropped } = normalizeAuthorValues( + ["npub1good", "B".repeat(64), "npub1bad", "garbage"], + { decodeNpub }, + ); + assert.deepEqual(normalized, [HEX, "b".repeat(64)]); + assert.deepEqual(dropped, ["npub1bad", "garbage"]); +}); + +test("author: token with hex passes into filters; garbage surfaces as an error", () => { + const HEX = "c".repeat(64); + const good = parseFilterQuery(`author:${HEX}`); + assert.deepEqual(good.filters.authorPubkeys, [HEX]); + const bad = parseFilterQuery("author:not-a-key"); + assert.equal(bad.filters.authorPubkeys.length, 0); + assert.ok(bad.errors.some((e) => /decode author/i.test(e.message))); +});