import type { RecordModel } from "pocketbase"; // Who does something: a person, or an agent acting for one. Agents are the connections a person // approved for /mcp (an OAuth grant, or an API token sent as a bearer token), and git pushes with // such a grant's access token. What an agent does is still done as its person (it has their // access), but recorded as the agent's, so it can be told apart, reviewed and undone. // // Like lib/git.ts this is used by the SSH server, so it must not import anything from next/*. /** An agent: `key` names the connection for good, `name` is what to call it. */ export type AgentRef = { key: string; name: string }; export type Actor = { userId: string; agent: AgentRef | null }; /** OAuth clients are registered per installation, but the same one could be approved by two people. */ export const appAgentKey = (clientId: string, userId: string) => `app_${clientId}_${userId}`; export const tokenAgentKey = (tokenId: string) => `tok_${tokenId}`; const AGENT_KEY_RE = /^(?:app_[a-z0-9]{15}_[a-z0-9]{15}|tok_[a-z0-9]{15})$/; export const isAgentKey = (value: string) => AGENT_KEY_RE.test(value); /** The OAuth client and person an app agent's key names, or null for token agents. */ export function parseAppAgentKey(key: string) { const m = /^app_([a-z0-9]{15})_([a-z0-9]{15})$/.exec(key); return m ? { clientId: m[1], userId: m[2] } : null; } /** The agent fields records store about who acted. */ export const agentFields = (agent: AgentRef | null | undefined) => ({ agent: agent?.key ?? "", agent_name: agent?.name ?? "" }); /** The agent a record was written by, from its `agent` and `agent_name` fields. */ export const agentOf = (r: RecordModel, field = "agent"): AgentRef | null => r[field] ? { key: r[field], name: r[`${field}_name`] || "Agent" } : null; // --------------------------------------------------------------------------- // Commits /** * Commits an agent makes are authored by its person and committed by the agent. The committer's * address names the connection, so the web UI can show which agent it was and offer to undo it. */ export function agentCommitter(agent: AgentRef) { return { name: agent.name, email: `agent+${agent.key}@noreply.vectrum` }; } const COMMITTER_RE = /^agent\+((?:app|tok)_[a-z0-9_]+)@noreply\.vectrum$/; /** The agent's key if a commit's committer address is an agent's. */ export function agentKeyFromEmail(email: string) { return COMMITTER_RE.exec(email)?.[1] ?? null; } /** A person acting themselves (in the web UI, or pushing with their own key or token). */ export const person = (userId: string): Actor => ({ userId, agent: null });