Vectrum
Sign In
54 lines · 2.6 KB · TypeScript
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 });