diff --git a/frontend/packages/swarm-ui/src/pages/LinkMatrixAccountForm.tsx b/frontend/packages/swarm-ui/src/pages/LinkMatrixAccountForm.tsx index e27f5750..081c0b81 100644 --- a/frontend/packages/swarm-ui/src/pages/LinkMatrixAccountForm.tsx +++ b/frontend/packages/swarm-ui/src/pages/LinkMatrixAccountForm.tsx @@ -1,11 +1,17 @@ -// — writes a matrix account credential (token + -// optional homeserver) for one agent into the swarm secret store. -// PUTs `/api/hives/{hive}/agents/{agent}/matrix-accounts/{account}` per -// the contract atlas posted on the OpenBao swarm-secret-store adoption -// issue: `{ token, homeserver? }` body, 200 on success, 400/503/500 on -// the documented failure arms — all handled generically via -// `readApiError`/`ApiErrorPanel`, same as every other form here, since -// the response is `problem+json` regardless of which arm fired. +// — writes a matrix account credential for one +// agent into the swarm secret store, in one of two modes (mirrors +// swarm-controller's `PutMatrixAccountRequest`): +// token — paste an already-obtained bearer token, optional homeserver. +// password — user id + password; swarm-controller logs into the +// (required, here) homeserver itself and stores the token +// that comes back. The password is sent once, over this +// PUT, straight to swarm-controller — never held here past +// the request, never sent to the hive. +// PUTs `/api/hives/{hive}/agents/{agent}/matrix-accounts/{account}` — +// 200 (`{ user_id? }`) on success, 400/503/500 on the documented failure +// arms — all handled generically via `readApiError`/`ApiErrorPanel`, +// same as every other form here, since the response is `problem+json` +// regardless of which arm fired. // // No "linked accounts" list to show first: no route exposes one (a // credential store shouldn't hand a secret back out anyway), so this is @@ -15,12 +21,6 @@ // mara's rulings on that issue — there's no assignment step here yet, // just the write. // -// Deliberately built against the contract before the endpoint exists -// (atlas: "it does not change when the implementation lands") — until -// item 2 merges this 404s, same as any other page pointed at a route -// that isn't live yet. Nothing here depends on the backend being up to -// be correct. -// // Rendered inside a `Dialog` from `AgentsPage`, one per row — same // mount shape as `CreateAgentForm`, own file for the same reason (a // multi-field form with real submit-state handling is more than @@ -31,9 +31,12 @@ import { ApiErrorPanel } from "@hive/shared/api-error-panel.js"; import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js"; import { Panel } from "../ui/panel/Panel.js"; import { TextField } from "../ui/text-field/TextField.js"; +import { SelectField } from "../ui/select-field/SelectField.js"; import { Button } from "../ui/button/Button.js"; import "./LinkMatrixAccountForm.css"; +type Mode = "token" | "password"; + // The store's own key-space, not `hive_types::Ident` — atlas's measured // gap on the issue: an agent name is an `Ident` (lowercase-only) but an // account name is a bare attrset key under `matrixAccounts`, so it @@ -45,9 +48,18 @@ const ACCOUNT_PATTERN = "[A-Za-z0-9_\\-]{1,63}"; type SubmitState = | { status: "idle" } | { status: "submitting" } - | { status: "done" } + // `userId` is set in password mode (the server resolves it from the + // login response) and `undefined` in token mode — see + // `PutMatrixAccountResponse`'s own doc for why token mode doesn't + // spend a round trip resolving one. + | { status: "done"; userId?: string } | { status: "error"; problem: ProblemDetails }; +const MODE_OPTIONS = [ + { value: "token", label: "paste a token" }, + { value: "password", label: "log in with a password" }, +]; + export function LinkMatrixAccountForm({ hive, agent, @@ -56,7 +68,10 @@ export function LinkMatrixAccountForm({ agent: string; }) { const [account, setAccount] = useState(""); + const [mode, setMode] = useState("token"); const [token, setToken] = useState(""); + const [userId, setUserId] = useState(""); + const [password, setPassword] = useState(""); const [homeserver, setHomeserver] = useState(""); const [result, setResult] = useState({ status: "idle" }); @@ -69,26 +84,43 @@ export function LinkMatrixAccountForm({ { method: "PUT", headers: { "content-type": "application/json" }, - body: JSON.stringify({ - token, - // Omitted rather than sent empty — the contract marks it - // optional, and an empty string isn't "no homeserver", it's - // a homeserver named "". - homeserver: homeserver.trim() || undefined, - }), + body: JSON.stringify( + mode === "token" + ? { + mode, + token, + // Omitted rather than sent empty — the contract marks + // it optional in token mode, and an empty string + // isn't "no homeserver", it's a homeserver named "". + homeserver: homeserver.trim() || undefined, + } + : { + mode, + user_id: userId, + password, + // Required in password mode — the field below enforces + // that before this branch is ever reachable. + homeserver: homeserver.trim(), + }, + ), }, ); if (!r.ok) { setResult({ status: "error", problem: await readApiError(r) }); return; } - setResult({ status: "done" }); - // Token cleared on success — nothing left needing it in the form - // once the store holds it, and a secret sitting in a field after - // the action that used it is exactly the kind of stale-credential - // risk the whole store exists to avoid. Account name kept, so the - // success message below reads against what actually landed. + const body = (await r.json().catch(() => ({}))) as { + user_id?: string; + }; + setResult({ status: "done", userId: body.user_id }); + // Secrets cleared on success — nothing left needing them in the + // form once the store holds the derived credential, and a secret + // sitting in a field after the action that used it is exactly the + // kind of stale-credential risk the whole store exists to avoid. + // Account name kept, so the success message below reads against + // what actually landed. setToken(""); + setPassword(""); } catch (err) { setResult({ status: "error", problem: { detail: String(err) } }); } @@ -118,20 +150,54 @@ export function LinkMatrixAccountForm({ required onInput={setAccount} /> - setMode(v as Mode)} /> + {mode === "token" ? ( + + ) : ( + <> + + + + )}