Files
gitea/PLUGIN.md
T
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

4.8 KiB

Gitea — the extraction record

Extracted on 2026-08-19, the eleventh plugin out — and the first that is not a move. plugins/EXTRACTING-A-PLUGIN.md is the runbook for the ordinary case; this was not it.

The old UI was deleted, not moved

Seventeen files under apps/Gitea/ reimplemented a fraction of an interface Gitea already has, and every one of them was a promise to keep pace with a project that moves faster than this plugin will. They were deleted rather than carried across.

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

There was no regression window either: gitea's router had been commented out of hono.ts since 2026-08-13 with api: [], and officer-gitea was not running anywhere. The screen rendered and every request 404'd. Nothing working was lost because nothing was working.

Why a proxy, and not simply an iframe

Officer and Gitea are different registrable domains. An iframe pointed straight at gitea.officer.dev gets a login page whose session cookie the browser discards as third-party — a frame that can never stay signed in.

Served through Officer's own origin, the cookie is first-party and survives. X-Frame-Options and Content-Security-Policy are stripped on the way through, which is safe only because the frame is the same origin the browser has already authenticated.

Whose Gitea account you are looking at

Your own. /_web/* forwards the browser's own cookies and injects nothing.

That is the one thing here not to get wrong. The sidecar holds an API token, and injecting it into the web proxy would show every user the OWNER'S view of every repository. The token is used only by /_api/*, for the small things around the frame.

The consequence is that each person signs in to Gitea themselves, once, inside the frame. Gitea's reverse-proxy authentication (ENABLE_REVERSE_PROXY_AUTHENTICATION, the same shape as Radicale's http_x_remote_user) would remove that step, and is deliberately not used: it trusts a header completely, and this instance is network-reachable rather than loopback-only.

Two doors

gated by used by
/_config, /_health, /_api/* the platform's own gate the panel
/_web/* a minted, path-scoped cookie the iframe

An iframe's sub-resources — every stylesheet, script and avatar — are fetched with no Authorization header, so /_web/* is declared in unauthenticatedPaths and authenticates itself.

The cookie holds 32 random bytes held in memory against the user who asked for it, not dev-proxy's constant =1, which anyone who knows the cookie name can set. use() verifies it and sets user on the context — not a convenience: createSidecarProxy reads ctx.get('user').id to fill X-Officer-User, and the sidecar refuses a request without it. On this path the platform's gate never ran, so the cookie is where the account comes from.

What that protects is narrow, and worth stating plainly: Gitea authenticates the person itself, so an unauthenticated caller reaching /_web/ sees a login page. This stops Officer being used as an open proxy to it. A manners problem, cheaply fixed.

What moved

Was Is now
src/servers/sidecar/gitea/* (2 files) sidecar/* — plus the new /_web proxy
src/servers/api/gitea/router.ts api/router.ts — plus the cookie door
src/workspaces/officerdev/src/apps/Gitea/* (17 files) deleted
src/apps/officer-web/Screens/Dashboard/Gitea/* deleted
— web/GiteaFrame.tsx, one panel

It also stopped being a core dock item. Gitea was hardcoded in CORE_DOCK_ITEMS, the only installable feature that was; its tile now arrives from assets/icon.png like every other plugin's.

Not self-contained

The connection lives in core service_connections, and 'gitea' stays in the platform's ServiceName union — the same coupling transmission, soulseek and the wallet have. Gitea is the reason that union has the "member rows carry only a token and inherit the owner's URL" shape at all.

Affordance

Ships one, in affordances/: use plain git for what git already does, and tea for pull requests, issues and comments. Nothing copies it into capabilities/affordances/ yet — plugin contribution is listed in TODO.md and gitea is its first customer.

tea itself is not provisioned per user yet either. Note the trap for whoever does it: the Debian package named tea is a graphical text editor, not Gitea's CLI, and Bun.which('tea') cannot tell them apart.