Sloth-ninja makes company search more honest and more useful

JessicaOSS now separates former company figures from current ones, while putting underlying filing documents a click away.

workflowsecurity

Company searches no longer leave ceased people looking current. Across both the assistant and company-search views, active officers and people with significant control appear first, while resigned and ceased entries are muted, labelled with the relevant date, and counted clearly.

  • Clearer status: users can see at a glance who is active, resigned, or ceased.
  • Filing PDFs in product: where Companies House makes a filing document available, users can open it directly without leaving the workflow.
  • Safer document access: the new route validates requests, limits document size, and keeps Companies House credentials out of exposed responses.

The external Companies House link remains available, so users retain a direct route to the source register.

So what Legal teams checking company control, officer history, or filings should care because the interface now makes the distinction between current facts and historical records much harder to miss.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

3 commits from Sloth-ninja/JessicaOSS, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
70836776 Company search: surface ceased PSCs/resigned officers + view filing documents sloth-ninja 2026-07-28 ↗ GitHub
commit body
Two owner-reported company-search defects.

Defect 1 (frontend only, CompanyPanel - backs both the assistant side
panel and the /company-search tabs): the PSC renderer never read
`ceased_on`, so ceased PSCs displayed as current; resigned officers only
had inline text. Both now carry a neutral grey "Ceased"/"Resigned" pill
(shared Badge) + DD/MM/YYYY date, a de-emphasised row, active-first stable
ordering, and a muted "· N ceased/resigned" count in the section header.

Defect 2: filing-history rows had no way to open the underlying document.
New backend proxy GET /companies/:companyNumber/filing-history/
:transactionId/document walks the CH Document API chain (transaction →
document_metadata → signed content URL), streaming the PDF inline with a
25 MB guard and a host-check (SSRF defence) on the upstream-supplied URLs;
the key is dropped on the cross-origin 302 to S3 and never logged. Route
uses the sibling try/catch + companiesHouseErrorResponse/logAndRespond
pattern (guarantees a response with fixed safe details - the guarantee the
asyncHandler rule exists to provide). Frontend gains a per-item "View PDF"
affordance (blob → new tab, busy + inline-error states). Chat tool
description now tells the model to flag resigned/ceased status explicitly.

Tests: +6 lib (metadata→content helper: happy chain, 404 no-metadata,
404 no-content-link, oversize reject, off-host SSRF reject, empty-key
401) and +6 route (validateTransactionId + document route over a real
server with the CH client mocked). Backend 385/385 green; tsc + eslint
clean. No new deps, no migrations. UI screenshots pending (headless).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
aed9d357 Harden CH filing-document proxy per review (SSRF, size guard, nits) sloth-ninja 2026-07-28 ↗ GitHub
commit body
Addresses independent review (REQUEST_CHANGES) of the filing-document path.

BLOCKER - bypassable host check: the `startsWith(DOCUMENT_API_BASE)` guard
on the metadata/content links passed for suffix-domain
(`...service.gov.uk.evil.com`) and userinfo (`...service.gov.uk@evil.com`)
spoofs; since Authorization is attached to the initial request, a tampered
link would exfiltrate the CH API key and give an SSRF primitive. Replaced
both checks with `isDocumentApiUrl` - `new URL(u)` + exact
`protocol === "https:" && host === "document-api.company-information.service.gov.uk"`.
Added regression tests for both exact attack URLs (rejected 404, no
key-attached fetch).

SHOULD-FIX - size guard buffered before checking: the post-`arrayBuffer`
byteLength check could balloon memory on a lying/chunked response. Now
streams the body with a running byte counter and aborts via AbortController
the moment the 25 MB cap is exceeded (Content-Length stays as a fast path).
Test: a mocked stream exceeding the cap is aborted and maps to the fixed
generic 502.

NITS: (1) allowlist the response Content-Type (application/pdf,
application/octet-stream) - anything else forced to octet-stream so an inline
disposition never renders untrusted upstream content; test added.
(2) FilingHistoryList: handle `window.open` returning null (pop-up blocked) -
revoke the blob URL immediately and show the inline error.

Docs: corrected the DURABLE_LESSONS entry to prescribe URL-parse + exact
protocol/host equality (naming both bypass shapes) instead of startsWith;
BUILD_LOG updated to reflect the hardened guard + streaming + allowlist.

Backend: tsc clean; vitest 388/388 (+3 lib tests). Frontend: tsc + eslint
clean. No new deps; no migrations touched; API key never logged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
d77f585a Merge remote-tracking branch 'origin/main' into company-search-status-filings sloth-ninja 2026-07-28 ↗ GitHub
# Conflicts:
#	docs/BUILD_LOG.md

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-1137.md from inside the repo you want the changes in.

⬇ Download capture-thread-1137.md