Skip to content

Fix directory broken logos, ignored ?limit=, and missing screenshots - #563

Merged
ralyodio merged 2 commits into
masterfrom
worktree-directory-fixes
Sep 22, 2026
Merged

ralyodio merged 2 commits into
masterfrom
worktree-directory-fixes

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Three directory defects found while verifying the rasterly swap in #562, plus one test fix they exposed.

1. logo_url came back as "data:,"

A page can declare <link rel="icon" href="data:,"> to suppress the favicon request — example.com does. That parses as a valid URL and passes z.string().url(), so it was stored as a listing's logo and rendered as a broken image.

Favicon and og:image hrefs now go through toHttpUrl() (new, in src/lib/meta-url.ts), which resolves them against the page and keeps them only if the result is http(s). The /favicon.ico last resort is HEAD-checked rather than asserted, so a site that serves no favicon yields an empty logo instead of a link that 404s. The submit form already does if (logoUrl) body.logo_url = logoUrl, so an empty value is simply omitted.

2. GET /api/directory ignored ?limit=

It hard-coded a page size of 20 and dropped the param. Every other list route in the app reads limit through parsePaginationParam, so this one now does too: default 20 (unchanged), max 50, and per_page reports what was actually applied. The directory endpoints aren't in openapi.json, so there's no spec to update.

3. Listings with no screenshot

scripts/backfill-screenshots.ts captures them through rasterly, reusing the storage path shape the route writes. Supports --dry-run and --limit, prints the quota remaining after each render, and stops when the API says the budget is gone.

Already run against production: 2 listings were missing screenshots (Client Kit CrawlProof Receiver, OpenClaw Docs). Both captured; GET /api/directory now reports none missing. rasterly quota: 92 of 100 left this month.

4. The test fix

UserReviews.test.tsx — "shows loading state when loading more" queues a response that resolves 100ms later, asserts synchronously, then ends. The deferred promise lands after jsdom teardown, so setReviews throws an unhandled window is not defined that fails the entire run.

This was latent: it only surfaced because adding a test file elsewhere in this PR shifted vitest's scheduling. Master is green today, but was one file away from going red at any time. Verified by running the full suite on unmodified fc3cc72 (clean) and on this branch before the fix (1 unhandled error, exit 1).

Verification

  • pnpm type-check clean; eslint exits 0 on all changed files
  • pnpm test:run — 223 files / 2143 tests pass, no unhandled errors, exit 0
  • 11 new tests: 6 for toHttpUrl (data:/about:/javascript: rejection, relative and protocol-relative resolution, unparseable input), 5 for directory pagination (limit honoured, offset paging, clamping, default)

🤖 Generated with Claude Code

ralyodio and others added 2 commits September 22, 2026 14:08
Three fixes to the directory, all found while verifying the rasterly swap.

A page can declare <link rel="icon" href="data:,"> to suppress the favicon
request. That parses as a valid URL and z.string().url() accepts it, so it was
stored as logo_url and rendered as a broken image; example.com submits this way.
Favicon and og:image hrefs now go through toHttpUrl(), which resolves them
against the page and keeps them only if they end up http(s). The /favicon.ico
last resort is HEAD-checked instead of asserted, so a site without one yields an
empty logo rather than a link that 404s.

GET /api/directory hard-coded a page size of 20 and silently dropped ?limit=.
Every other list route in the app reads limit through parsePaginationParam, so
this one now does too: default 20 (unchanged), max 50, and per_page in the
response reports what was actually applied.

scripts/backfill-screenshots.ts captures the listings that predate screenshots,
through rasterly, reusing the storage path shape the route writes. It honours
--dry-run and --limit, prints the quota left after each render and stops when
the API says the budget is gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"shows loading state when loading more" queues a response that resolves 100ms
later, asserts the Loading... label synchronously and then ends. The deferred
promise resolves after the test is over, so loadMore's setReviews runs against a
torn-down jsdom and vitest reports an unhandled "window is not defined" that
fails the whole run.

It only surfaced when adding a test file elsewhere shifted the scheduling, so
the suite was one file away from going red at any time. Waiting for the label to
clear keeps the state update inside the test's lifetime.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

45 finding(s)

HIGH/CRITICAL: 1 | MEDIUM: 8 | LOW: 36

Severity Rule Location
HIGH js-ssrf-outbound-request scripts/scan-all-skills.ts:38
MEDIUM js-open-redirect src/app/agent-login/AgentLoginForm.tsx:38
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.tsx:79
MEDIUM js-open-redirect src/app/dashboard/subscription/page.tsx:90
MEDIUM js-open-redirect src/app/dashboard/subscription/page.tsx:113
MEDIUM js-open-redirect src/app/dashboard/subscription/page.tsx:144
MEDIUM js-open-redirect src/components/funding/FundingClient.tsx:137
MEDIUM js-dynamic-code-execution src/lib/skills/metadata-extract.ts:300
MEDIUM js-dynamic-code-execution src/lib/skills/security-scan.ts:48
LOW secret-generic-credential cli/src/commands/auth.test.ts:66
LOW secret-generic-credential cli/src/commands/auth.test.ts:85
LOW secret-generic-api-key docs/agents/integration-guide.md:893
LOW secret-generic-credential src/app/api/auth/login/route.test.ts:53
LOW secret-generic-credential src/app/api/auth/login/route.test.ts:68
LOW secret-generic-credential src/app/api/auth/login/route.test.ts:87
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:158
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:182
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:193
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:232
LOW js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:212
LOW js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:223
LOW js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:239
LOW secret-generic-credential src/lib/api.test.ts:126
LOW secret-generic-credential src/lib/api.test.ts:131
LOW js-dynamic-code-execution src/lib/skills/composite-scanner.test.ts:106
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:36
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:44
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:66
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:81
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:94
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:103
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:118
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:144
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:162
LOW js-dynamic-code-execution src/lib/skills/security-scan.test.ts:28
LOW js-dynamic-code-execution src/lib/skills/url-import.test.ts:178
LOW js-dynamic-code-execution src/lib/skills/url-import.test.ts:191
LOW secret-generic-credential src/lib/validations.test.ts:148
LOW secret-generic-credential src/lib/validations.test.ts:512
LOW secret-generic-credential src/lib/validations.test.ts:523
LOW secret-generic-credential src/lib/validations.test.ts:538
LOW secret-generic-credential src/lib/validations.test.ts:548
LOW secret-generic-credential src/lib/validations.test.ts:557
LOW secret-generic-credential src/lib/validations.test.ts:567
LOW secret-generic-credential src/lib/validations.test.ts:582

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 98f582f into master Sep 22, 2026
6 checks passed
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