zz_

app/cli/lib/identity.ts

zzmq.dev@gmail.com/zz / app / cli / lib / identity.ts @ 38c01725 Download
Ladder Summary
  • file The file implements identity resolution for the CLI, determining which user‑token pair should be sent to the server by checking (in order) the `ZZ_ID` env var, a file referenced by `ZZ_ID_FILE`, the current logged‑in session, or a temporary token minted for an `--as <name>` request; it also provides a best‑effort cleanup that revokes any token that was minted on the fly. **Exports** - `ResolvedIdentity` (interface) - `resolveIdentity(asName?: string): Promise<ResolvedIdentity>` - `releaseIdentity(identity: ResolvedIdentity | null): Promise<void>`
 // Resolves "who is this remote command acting as, and what credential
 // does it send" -- one place, used by `zz work`/`zz review`/
 // `zz repo submit`, in priority order:
 //
 // 1. ZZ_ID="<username>:<token>" -- for automated/headless contexts
 // with no interactive `zz login` session at all (a CI runner, a
 // container with no ~/.zz). A real, standing credential someone
 // else is managing the lifecycle of -- never minted or revoked
 // here.
 // 2. ZZ_ID_FILE=<path to a file with that same "<username>:<token>"
 // text> -- the same thing, out of an env var (visible in `ps`,
 // shell history, some CI log capture) and into a file instead.
 // 3. --as <name>: your own `zz login` session resolves who YOU are;
 // <name> expands to "<your username>[<name>]" (the standard
 // managed-agent naming -- see worker/src/accountsDb.ts's
 // computeUsername) unless it already looks fully-qualified
 // (contains "@" or "["). A FRESH token is minted for it right
 // now, using your own session's existing mint authority
 // (requireTokenMintAuthority on the server: you, or an agent you
 // created) -- and the caller is expected to revoke it when the
 // session ends (releaseIdentity below), so nothing long-lived
 // sits on disk. This is NOT server-side impersonation -- your own
 // token never acts as another agent; this mints and uses that
 // agent's own real token, scoped to one run.
 // 4. No --as: your own `zz login` session, directly (long-lived,
 // untouched -- never minted or revoked here).
 import { readFileSync } from "node:fs";
 import { apiBase, requireCredentials } from "./credentials.js";
 import { apiFetch } from "./api.js";
 import { resolveUserPath } from "./paths.js";
 
 export interface ResolvedIdentity {
  apiBase: string;
  token: string;
  username: string;
  /** True only for a token minted by resolveIdentity itself (the --as path) -- the one case the caller should revoke on exit (see releaseIdentity). Your own login (ZZ_ID included) is a standing credential someone else manages; never revoked here. */
  ephemeral: boolean;
 }
 
 function parseIdPair(raw: string, source: string): { username: string; token: string } {
  const idx = raw.indexOf(":");
  if (idx === -1) throw new Error(`${source} must be "<username>:<token>", got "${raw}"`);
  return { username: raw.slice(0, idx).trim(), token: raw.slice(idx + 1).trim() };
 }
 
 export async function resolveIdentity(asName?: string): Promise<ResolvedIdentity> {
  if (process.env.ZZ_ID) {
  const { username, token } = parseIdPair(process.env.ZZ_ID, "ZZ_ID");
  return { apiBase: apiBase(), token, username, ephemeral: false };
  }
  if (process.env.ZZ_ID_FILE) {
  const raw = readFileSync(resolveUserPath(process.env.ZZ_ID_FILE), "utf8").trim();
  const { username, token } = parseIdPair(raw, "ZZ_ID_FILE");
  return { apiBase: apiBase(), token, username, ephemeral: false };
  }
 
  const human = requireCredentials();
  if (!asName) return { apiBase: human.apiBase, token: human.token, username: human.username, ephemeral: false };
 
  const fullUsername = /[@[]/.test(asName) ? asName : `${human.username}[${asName}]`;
  const { token } = await apiFetch<{ token: string }>("POST", `/api/agents/${encodeURIComponent(fullUsername)}/tokens`, undefined, human.token);
  return { apiBase: human.apiBase, token, username: fullUsername, ephemeral: true };
 }
 
 /** Best-effort cleanup for an ephemeral (--as) identity -- revokes the token it minted, using that same token (the server's self-revoke endpoint needs no id, just the bearer presenting it). A no-op for anything resolveIdentity didn't itself mint. Swallows errors: called from exit paths (Ctrl+C, SIGTERM, a thrown error), where a failed revoke shouldn't block the process from actually exiting -- a short-lived leaked token is a minor, bounded exposure, not worth hanging the CLI over. */
 export async function releaseIdentity(identity: ResolvedIdentity | null): Promise<void> {
  if (!identity?.ephemeral) return;
  try {
  await apiFetch("POST", "/api/me/revoke", undefined, identity.token);
  } catch {
  // best-effort
  }
 }