Commit Graph
10 Commits
Author SHA1 Message Date
officer dcfcf4e503 drop content-encoding when the body is rewritten
`.text()` decompresses, so the rewritten HTML goes back plain — while the
headers still said `content-encoding: gzip`. The next hop believed the header,
tried to gunzip plain text, and failed with ZlibError.

Which surfaced as "gitea unreachable": a 502 naming nothing, while a direct
curl to the sidecar returned 200 the whole time.
2026-08-19 09:13:19 +00:00
officer e3f673ecb9 log why the frame fetch failed
'gitea unreachable' on its own is not a diagnosis. The sidecar was answering
200 to a direct curl while the panel showed that message, which sent the search
to the wrong half of the system twice.
2026-08-19 09:09:07 +00:00
officer 80b9cd02e7 say when a stored token cannot be decrypted
The frame answered {"error":"internal error"} on every request. The cause was
AES-GCM failing with "Unsupported state or unable to authenticate data" — the
token in service_connections was encrypted under a key that no longer exists,
because the platform mints its encryption key on first use and does not back it
up. The same loss took slskd, memos and gitea credentials on this project's own
server earlier today.

It presented as a broken sidecar. connectionState reported `configured: true`
because a ROW exists, so the panel skipped the form and went to the frame,
where every call threw. Nothing offered a way to fix it, and the connection
form was unreachable precisely because it only appears when unconfigured.

Now a named failure. `UnreadableCredentials` is distinguished from every other
error — a database that is down keeps its own message, or a real fault would be
reported as "re-enter your token" — and the panel shows the form saying the key
changed, nothing else is lost, and the URL is still there.

Verified by the platform itself refusing to decrypt: this is the state that
machine has been in all day.
2026-08-19 09:00:03 +00:00
officer 0d88a1259b put the plugin's own routes ahead of the proxy's catch-all
/session and /_web/* were both dead. createSidecarProxy registers all('/*')
when it is CALLED, so anything added to proxy.router afterwards never matches —
hono goes in registration order and the wildcard had already claimed
everything.

The symptom was `[gitea] proxy POST /session -> 500`: forwarded to a sidecar
with no such route, answered "internal error", and looking exactly like the
sidecar being broken. Which is what the panel showed.

A comment two lines above the bug claimed these were "registered BEFORE the
proxy's /*". They were not, and writing that down did not make it so.

Now a router of our own with the proxy's mounted under it at the end, so
everything above genuinely wins.
2026-08-19 08:51:46 +00:00
officer e7f05f66d2 declare tea as a release binary
The dependency this plugin actually wants, and the reason the platform grew
release dependencies: tea is not in any distribution, and Debian has an
unrelated package of that name — a graphical text editor. `packages` is empty
on purpose, because every entry in it would install the wrong program.

`identify` runs `tea --version` and looks for `go-sdk`, which Gitea's CLI
prints and the text editor does not. Bun.which cannot tell them apart, and a
namesake reported as satisfied is worse than one reported missing.

Pinned at 0.15.1 rather than latest: an install that silently fetches whatever
is current is not reproducible.
2026-08-19 08:48:31 +00:00
officer b1b9e2afc3 a way to actually connect it
Deleting the old UI took its connection panel with it, so the plugin installed,
opened, and had nowhere to type a URL. The frame would have loaded and answered
503 on every request inside it — which reads as Gitea being down rather than as
nobody having said where Gitea is.

The panel now asks for the connection FIRST and shows the form when there is
none.

Two halves, and which one you get depends on who you are: the URL is the
owner's, one instance per server, and everyone else supplies only a token and
inherits it. Not a UI choice — service_connections stores a member's row with a
null URL for exactly this reason, so "members never see the instance URL" is a
property of the schema rather than a filter somebody has to remember.

Defaults to http://localhost:3000 and says why in the field: the sidecar
fetches SERVER-side, so localhost means the machine Officer runs on. It needs
no TLS and never leaves the box, where the public URL takes the same traffic
out and back.
2026-08-19 08:36:05 +00:00
officer 00243b312a rewrite Gitea's URLs so the frame stays inside the proxy
Gitea believes it lives at /. Its pages are full of root-absolute links, and
inside the frame those resolve against OFFICER'S origin — the page renders
perfectly and falls apart on the first click.

Four places a URL hides, each failing differently: href/src/action attributes,
window.config.appSubUrl, Location headers, and absolute URLs built from
ROOT_URL.

appSubUrl is the highest-value line. Gitea already supports being hosted under
a subpath and its client JS prefixes every URL it builds with that value, so
filling it in makes Gitea correct ITSELF rather than by interception.

The ROOT_URL case needs Gitea's public origin, which the sidecar does not have
— it fetches from loopback. Rather than add a second setting that can disagree
with Gitea's own, appUrlOrigin reads it out of the page: Gitea emits "appUrl"
in the same config blob as appSubUrl.

Not rewritten, deliberately: JavaScript bodies, because a blanket rewrite of
absolute paths inside a script corrupts strings that are not URLs; and clone
URLs, which must keep working for whoever copies them.

/_web/* now forwards by hand rather than through createSidecarProxy. That
forwarder passes three headers and nothing else, on purpose — it is shared by
every sidecar and its restraint is what makes it safe to reuse. It strips
cookies, and this path is nothing but cookies. Loosening it for gitea would
loosen it for the wallet.

Eighteen tests, the only part of this plugin verifiable without a running
Gitea. One caught a real bug: rewriteAbsolute turns a full URL into <base>/path
and the attribute pass then prefixed it again.
2026-08-19 08:31:28 +00:00
officer f680e25f82 gitea: the plugin, rendering Gitea's own interface
Eleventh plugin out, and the first that is not a move. The seventeen files that
reimplemented a fraction of Gitea's interface were deleted rather than carried
across — each one a promise to keep pace with a project that moves faster than
this plugin will.

That removed a platform change nobody now has to build. useGiteaLocation read
:owner/:name/:tab/:item from five route patterns and a plugin gets two, so
extracting as-is would have needed the plugin system to declare extra routes.
Nothing inside an iframe needs a route, so the requirement went with the screen.

No regression window: gitea's router had been commented out of hono.ts since
2026-08-13 with api: [], and officer-gitea was running nowhere. The screen
rendered and every request 404'd.

The proxy is what makes the iframe possible, not decoration. Officer and Gitea
are different registrable domains, so a frame pointed straight at the instance
gets a login page whose cookie the browser drops as third-party. Through
Officer's origin it is first-party and survives; X-Frame-Options and CSP are
stripped, safe only because the frame is the origin the browser already
authenticated.

/_web/* forwards the browser's own cookies and injects NOTHING. The sidecar
holds an API token and injecting it there would show every user the owner's
view of every repository. It is used only by /_api/*.

Two doors, because an iframe's sub-resources carry no Authorization header:
the ordinary routes go through the platform's gate, and /_web/* is declared
unauthenticated and authenticates itself with a minted, path-scoped cookie —
32 random bytes against the user who asked, not dev-proxy's constant =1 which
anyone knowing the cookie name can set. Verifying it also sets `user`, because
createSidecarProxy fills X-Officer-User from it and the sidecar refuses a
request without one.

Ships an affordance: git for what git does, tea for pull requests and issues.
Nothing copies it into capabilities/ yet — gitea is that mechanism's first
customer.
2026-08-19 07:58:55 +00:00
Claude fbc85acaca art: icon and splash, drawn to the current brief
git + tea: a green glazed teacup with the branch diagram pressed into its face
as a maker's emblem, and a tea-bench scene for the splash.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 08:47:39 +01:00
officer 9f12b58597 the gitea plugin repository, awaiting its artwork
Empty on purpose. Created so assets/icon.png and assets/splash.png can land
before the plugin does — presence is the declaration, so nothing needs
registering once they are here.
2026-08-19 07:46:32 +00:00