diff --git a/PLUGIN.md b/PLUGIN.md new file mode 100644 index 0000000..acf7ed1 --- /dev/null +++ b/PLUGIN.md @@ -0,0 +1,91 @@ +# 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. diff --git a/README.md b/README.md index 4221e1d..b6762b2 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,32 @@ # Gitea -Gitea inside Officer — repositories, issues and pull requests. +Your own [Gitea](https://about.gitea.com) inside Officer — repositories, issues and pull requests, +rendered as the real thing rather than a copy of it. -**Not built yet.** This repository exists so artwork can land before the code does. +## What you get -## Assets +Gitea's own web interface, in an Officer panel. Not a reimplementation: the actual application, proxied +through Officer so it stays signed in, with an **open in a new tab** button for anything the frame +handles badly. -``` -assets/icon.png the dock tile -assets/splash.png the launch scene, for the mobile app and the credits screen -``` +## Requirements -Presence is the declaration — no manifest field. `discover.ts` reads those two exact paths, and -installing publishes the whole directory to `/plugins/gitea/`. A shipped `icon.png` wins over the -manifest's lucide glyph automatically. +A Gitea instance Officer can reach. -## What this plugin will be +## Setup -Unlike the other plugins, the UI is not a set of panels over a JSON API. Gitea already has a web -interface, so Officer proxies it and renders it in an iframe — same-origin, because a third-party iframe -cannot keep a session cookie. +Install the plugin, open **/gitea**, and set the instance URL and an API token in the connection panel. +Then sign in to Gitea inside the frame, once — the frame shows **your** Gitea account, not the owner's. -It will also ship an affordance telling agents to use plain `git` for what git already does, and `tea` -for pull requests, issues and comments — `tea` being logged in as the current user, with that user's own -permissions. +The stored token is used only for the small things around the frame. Everything you see in it is your own +session. + +## For agents + +This plugin ships an affordance: use plain `git` for clone, fetch, pull and push; use `tea` for pull +requests, issues, comments and releases. `tea` is logged in as you, with your permissions. + +## Permissions + +One permission, `gitea`, grantable per role like any other app permission. Each account reaches its own +Gitea identity. diff --git a/affordances/gitea.md b/affordances/gitea.md new file mode 100644 index 0000000..aab6fc3 --- /dev/null +++ b/affordances/gitea.md @@ -0,0 +1,12 @@ +--- +name: Gitea +description: Use when working with git repositories, or with Gitea issues and pull requests. +version: 1 +--- + +For anything git already does — clone, fetch, pull, push, branches, tags — use plain `git`. It talks to +the server directly. + +For anything git does not — pull requests, issues, comments, releases, creating a repository — use `tea`. +It is already logged in as the current user, with that user's own permissions. Do not configure it, and +do not ask the user for a token. diff --git a/api/router.ts b/api/router.ts new file mode 100644 index 0000000..70bacd6 --- /dev/null +++ b/api/router.ts @@ -0,0 +1,110 @@ +import { randomBytes } from 'node:crypto'; +import { createSidecarProxy } from '@@/sidecar/create-proxy'; +import { mountPrefix } from '@@/plugins/manifest'; +import { manifest } from '../manifest'; + +// Two doors, because an iframe cannot carry a bearer token. +// +// ── The ordinary door ── +// +// `/_config`, `/_health` and `/_api/*` are normal API calls from the panel. They go through the +// platform's gate like any plugin's, and `createSidecarProxy` forwards them. +// +// ── The iframe door ── +// +// Gitea already has a web interface, and this plugin renders it rather than reimplementing it. That +// means an `