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))); +});