the token lives in tea's config, once, per user
It was stored encrypted in service_connections as well. Nothing needs it there any more: the UI is an iframe carrying the browser's own Gitea session, so the only consumer left was the health probe. Meanwhile tea keeps its own copy regardless — it is what the agent uses, and what this plugin's affordance tells the agent to use. Two copies of one secret is a drift waiting to happen: rotate with `tea login` and the database quietly keeps the dead one. So the token lives once, where it is used, and saving now writes `tea login add --name officer` AS that account — a member's token lands in a home only they can read. `secret: null` on save clears whatever an earlier version stored, so an upgraded machine stops carrying a copy nothing reads. service_connections keeps the URL, which is the owner's and genuinely shared. The settings page READS the existing config first, so on a machine where the owner and their members already ran `tea login` by hand it says "tea is configured as X" rather than demanding something they have. It also says when an account has no OS user yet, since there is then nowhere to write one — the web view works regardless. Test runs `tea whoami` as that account. Deliberately not a call the platform makes with a token it read: the point is to prove what the AGENT will get, and those are now the same thing. `--name officer` so the platform never overwrites a login somebody set up by hand. Noted rather than hidden: `tea login add` takes the token in argv, visible in ps while it runs. tea offers no stdin form, and the alternative is writing its YAML by hand — a visible argument traded for a fragile format.
This commit is contained in:
+39
-2
@@ -18,6 +18,7 @@ import {
|
||||
probe,
|
||||
UnreadableCredentials,
|
||||
} from './upstream';
|
||||
import { readTeaLogin, writeTeaLogin, osUserFor, testTeaLogin } from './tea-config';
|
||||
import { API_URL } from '@@/officer-url.mjs';
|
||||
|
||||
// The officer-gitea sidecar. Owns the whole Gitea contract: the instance URL and the personal access
|
||||
@@ -129,12 +130,26 @@ async function connectionState(userId: number): Promise<Response> {
|
||||
}
|
||||
const connection = await getServiceConnection(userId, 'gitea');
|
||||
const instanceUrl = await getServiceInstanceUrl('gitea');
|
||||
|
||||
// The token's home is `tea`'s own config, not the database — see tea-config.ts. So "configured" for a
|
||||
// MEMBER means tea has a login for this instance; the row only carries the URL, which is the owner's.
|
||||
//
|
||||
// Read rather than asked for: on a machine where the owner and their members already ran `tea login`
|
||||
// by hand, the settings page should say "connected" instead of demanding something they have.
|
||||
const tea = instanceUrl ? await readTeaLogin(userId, instanceUrl) : null;
|
||||
const osUser = await osUserFor(userId);
|
||||
|
||||
return Response.json({
|
||||
configured: !!connection,
|
||||
configured: !!instanceUrl && !!tea?.hasToken,
|
||||
connection,
|
||||
instanceUrl,
|
||||
instanceConfigured: !!instanceUrl,
|
||||
isOwner: await isOwner(userId),
|
||||
// What tea already knows, so the page can say who you are without a round trip to Gitea.
|
||||
tea: tea ? { name: tea.name, user: tea.user, hasToken: tea.hasToken } : null,
|
||||
// Null means never provisioned an OS account, so there is nowhere to write a config. The page says
|
||||
// so rather than failing on save.
|
||||
canStoreToken: !!osUser || (await isOwner(userId)),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -188,12 +203,28 @@ async function handleConfig(req: Request, userId: number): Promise<Response> {
|
||||
const result = await probe({ id: existing?.id ?? 0, base, token: token ?? current?.token ?? null });
|
||||
if (!result.ok) return bad(result.error ?? 'could not reach the instance');
|
||||
|
||||
// THE TOKEN GOES TO `tea`, NOT THE DATABASE.
|
||||
//
|
||||
// It used to be stored encrypted in `service_connections`, and nothing needs it there any more: the
|
||||
// UI is an iframe carrying the browser's own Gitea session, so the only consumer left was this
|
||||
// probe. `tea` keeps its own copy regardless — it is what the agent uses, and what this plugin's
|
||||
// affordance tells the agent to use — so storing a second one is a drift waiting to happen: rotate
|
||||
// with `tea login` and the database quietly keeps the dead one.
|
||||
//
|
||||
// Written as the account, so a member's token lands in a home only they can read.
|
||||
if (token) {
|
||||
const written = await writeTeaLogin(userId, base, token);
|
||||
if (!written.ok) return bad(`could not store the token for tea: ${written.error}`);
|
||||
}
|
||||
|
||||
const connection = await saveServiceConnection({
|
||||
userId,
|
||||
service: 'gitea',
|
||||
// Null for anyone but the owner: their row is a credential, and inherits the instance on read.
|
||||
url: owner ? base : null,
|
||||
secret: token ?? undefined,
|
||||
// Deliberately NOT stored. `secret: null` clears anything an earlier version of this wrote, so a
|
||||
// machine upgraded into this design stops carrying a copy it no longer reads.
|
||||
secret: null,
|
||||
version: result.version,
|
||||
});
|
||||
// An owner's save may have moved the instance, and every member's cached base was resolved from it.
|
||||
@@ -225,6 +256,12 @@ const server = Bun.serve({
|
||||
|
||||
const config = await getGiteaConfig(userId);
|
||||
|
||||
// Prove the token works, by asking `tea` to use it as this user. Deliberately not a call the
|
||||
// platform makes with a token it read: the point is to test what the AGENT will get.
|
||||
if (url.pathname === '/_test') {
|
||||
return Response.json(await testTeaLogin(userId));
|
||||
}
|
||||
|
||||
if (url.pathname === '/_health') {
|
||||
if (!config) return Response.json({ ok: false, configured: false, error: 'not connected' }, { status: 503 });
|
||||
const started = Date.now();
|
||||
|
||||
Reference in New Issue
Block a user