A security + accuracy pass ahead of wider distribution.
Added
- Optional
CONNECT_SECRETsecret. When set, the browser step of the OAuth connect flow shows a one-field form and only completes when the secret matches. Without it the OSS build keeps its auto-completing single-dev-user flow — which means anyone who knows the worker URL can connect an MCP client and callrefresh_brand(spending your engine API credits). Recommended for every deployment whose URL is shared anywhere. - Per-brand
aliasesandexclude_terms(migrations/0005_brand_alias_exclude.sql,Brandinsrc/db.ts,SeedBrandInputinsrc/seed.ts). Aliases are extra terms that always count as a mention; exclude terms suppress the bare-word match on the brand name and domain root — so "Monday" the brand stops matching "monday" the weekday — while the full domain and aliases still count.extractCitationsnow matches through amentionsTermSetterm set; its exported signature is unchanged. Apply 0005 to add thealiases_json+exclude_terms_jsoncolumns; existing brands read as empty arrays and behave exactly as before. SECURITY.md— security model, trust boundaries, and private vulnerability reporting.- GitHub Actions CI (
.github/workflows/ci.yml):tsc --noEmit+ unit tests on every push and PR. - Unit test suite (
npm run test:unit, Node test runner + tsx):tests/matching.test.tscovers brand/competitor mention matching (excluded bare words, full-domain matches that survive exclusion, root terms not matching inside a larger word);tests/unit/citations.test.tsandtests/unit/scoring.test.tscover citation extraction (extractCitations,hostMatchesDomain) and score aggregation (computeOverallScore). These are the pure functions every score flows through; the smoke suite still covers the deployed Worker end-to-end. migrations/README.md— how to apply migrations and why files must never be renumbered.- README: Claude Code connect instructions (
claude mcp add --transport http), architecture diagram, security section.
Fixed
- Word-boundary brand/competitor matching in
src/openai.ts.mentionsTermusedhaystack.includes(root), a raw substring test: a brand whose domain root is a common word (monday.com→ "monday",notion.so→ "notion") false-positived on the everyday word, and any term matched inside a larger word ("motion" inside "promotional"). Matching now uses Unicode-aware boundary checks —(?<![\p{L}\p{N}])…(?![\p{L}\p{N}])— for the brand name and the domain root, while the full domain is still accepted as a high-confidence substring. The exportedextractCitationssignature is unchanged. (The homograph case — a bare root term that is itself a common word, e.g. "monday"/"notion" — is handled via the new per-brandexclude_terms.) - Linked-citation checks require the exact brand domain or a subdomain of it. Previously a substring check meant
notacme.comcounted as a link toacme.com. Applies toextractCitations, the Perplexity and AI Overviews engine-citation merge, andget_citations'cited_urlselection — all through the new sharedhostMatchesDomainhelper. - NUL → unit-separator hash delimiter in
hashPrompt(src/openai.ts). The field join used two literal NUL (\x00) bytes, which made git classify the whole file as binary and refuse to render its diffs. It now joins with the ASCII unit separator through a namedHASH_FIELD_SEP = '\x1f'constant. Hash outputs change, so existingshared_prompt_cacherows simply miss and expire on their TTL — no cache clear or migration required. get_visibility_historymatchescheck_visibility's stance on partial runs. Runs interrupted mid-flight (stuck atin_progress) now count toward history viaCOALESCE(completed_at, started_at), and runs with zero ok rows are excluded entirely instead of charting as a fake score of 0.
Changed
- Docs (
README.md,SETUP.md) now recommend OpenAI + Anthropic (Claude) as the starting engine pair instead of the Gemini free tier. The Gemini free tier 429s for brands with more than ~5 prompts (excluding it from scoring), and Google AI Overviews frequently returnsNO_AI_OVERVIEW(scored as a zero), so the cheapest documented path produced misleading first-run data. Gemini and SerpAPI stay documented as opt-in engines; engine availability is unchanged and remains key-driven (getAvailableEnginesuntouched). SEED_SECRETandCONNECT_SECRETcomparisons are constant-time.- MCP server version string now tracks the package version (was stuck at an older value).
- Runtime dependencies (
@cloudflare/workers-oauth-provider,@modelcontextprotocol/sdk,agents,zod) moved fromdevDependenciestodependencies— wrangler bundles either way, but the manifest now tells the truth.