Commit Graph
6 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 23820ddbaf the preview never had a tab: the URL param is :section, not :tabId
Selecting a tab put its id in the URL and the preview still said "Select a tab to preview".

A plugin gets exactly two routes from the shell — /<route> and /<route>/:section — so :section is the
only param name that ever arrives, whatever the plugin means by it. This read :tabId, which was right
while it was a platform screen with its own /browser/:tabId route and stayed in place through the
extraction. The route matched, the id was in the URL, and useParams().tabId was undefined.

Nothing typechecks this: the param is a string key, and the wrong one is simply absent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 02:27:09 +00:00
pastilhasandClaude Opus 5 5d9b074ee9 the download is officer-browser-relay.zip
Named from the extension's own manifest by the installer, so it arrives in Downloads as something a
person can identify rather than as extension.zip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 01:52:25 +00:00
pastilhasandClaude Opus 5 3346b4c853 extension: normalise the address, and always https
Pasting the address you use for Officer produced http://https://platform.officer.dev:18792/ — the
scheme was taken as part of the hostname and http:// prepended anyway. It failed as "Relay not
reachable", which reads as the relay being down rather than the URL being mangled.

normalizeHost strips a scheme, a path and credentials, keeps IPv6 brackets, and lifts a port out of
the address so pasting host:9000 moves the port field instead of dropping it. The field is rewritten
with the result, so what is shown is what is stored.

The scheme is now always https, and wss for the socket, whatever was typed. The relay carries a
pairing token on every request and drives a browser someone is signed into; plaintext because a field
was left blank is not a default worth having.

CONSEQUENCE, stated rather than discovered: the relay itself still serves plain HTTP on 18792, so the
extension cannot connect until it is behind TLS. That is a proxy host away and it is the right shape —
the raw port was never reachable from anywhere but the tailnet anyway.

The unreachable message now says what was tried and that the port is normally private, instead of
"verify the server address", which is true and useless when the address is fine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 01:47:31 +00:00
pastilhasandClaude Opus 5 c20d03e9f5 the extension download is a published file, not an authed route
The route I added returned 401 to an <a download>, which Chrome reports as "Try to sign in to the
site" — a plain navigation carries no Bearer token, so that advice cannot work. The platform publishes
extension/ to /plugins/browser/extension.zip instead, which is served without auth like plugin icons.

zip.ts moved to the platform: extensions are meant to be a marketplace of them, not this plugin's
private trick.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 01:36:44 +00:00
pastilhasandClaude Opus 5 c0e0848e6a pairing moves in, and the extension download stops being a snapshot
Two halves of the same coupling, both left behind by this morning's extraction.

The pairing UI was Settings/IntegrationsSettings/BrowserRelay.tsx — a PLATFORM screen driving this
plugin's pairing, calling /browser/relay-token, and working only because the plugin happens to serve
that path. It is a panel here now, beside the tab list rather than buried in settings: it shows
whether the extension is connected and how many tabs it holds, which is the first thing anyone looks
at when a capture does not work, and that is not configuration you set once.

The download link pointed at public/browser-relay-extension.zip, a static file. That file was 58 lines
behind the source it claimed to be — built in August from something that predated a February commit,
never rebuilt, and serving an extension missing the multi-session recovery path that the same February
commit added. Anyone downloading it got the version that cannot recover from exactly the failure they
were about to hit.

So the archive is built from the extension directory per request. There is no second copy to fall
behind, which is the only durable fix for that class.

zip.ts is hand-rolled and stored rather than deflated. The route's own history says it used to shell
out to `zip`; this manifest's "nothing to declare" is worth more than 30 KB on a download that happens
once, and Chrome accepts stored entries. Timestamps are fixed at the format's zero so the same
directory always produces identical bytes — which is what makes the ETag meaningful and "did this
change" answerable by comparing two files. Verified by unzipping the output and diffing background.js
against the source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 01:20:32 +00:00
pastilhasandClaude Opus 5 52934f52dc browser, extracted from the platform — extension, relay and all
Your own Chrome, driven from Officer. Not a headless one: it attaches to the browser you are already
signed into, which is the whole difference from /scrape, and /scrape stays behind in the platform.

The relay is a sidecar now, and that is the point. It was a second Bun.serve inside the platform
process on its own port, which is exactly why it was switched off on 2026-08-13 — "the platform serves
one port". A sidecar is a process allowed its own listeners, so the objection stops applying instead of
being argued with.

The boundary across it is deliberately small: two questions, who is connected and what tabs they have,
because only those live in memory on the relay side. Screenshots, evaluation and navigation still go
straight down cdp.ts's own WebSocket to the relay's /cdp endpoint, exactly as before — routing page
bytes through the platform would be work for nothing.

Three normally-breaking changes taken now because now they are free, each already recommended in a
comment by whoever switched the relay off. Its own signing key instead of borrowing getKey('jwt'). Its
own table instead of the platform's user_integrations. And the port kept at 18792 on purpose, because
the port is an input to the token HMAC as well as an address — moving it fails every pairing silently,
and the extension blames the address. user_integrations held zero browser-relay rows, checked in
Postgres rather than assumed, so there was nothing to unpair.

The screen became panels. BrowserScreen.tsx rendered TabList and TabPreview through appType: null
placeholders; both components were already self-contained and took no props, so the screen had nothing
to pass and simply went.

Not tested end to end — no browser has been paired since the move, and that needs a Chrome and a
person. BLOCKERS.md says so rather than the commit implying otherwise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 01:04:09 +00:00