g1t

syntaqx/g1t

public

Git for AI scale: a forge for thousands of agents working on the same code at once.

g1t/packages/contracts/src/oauth.ts

107 lines3,780 bytes
/**
 * OAuth clients. A client is not stored anywhere: its id is its name and
 * redirect addresses, encoded. Registering one therefore writes nothing,
 * and anyone holding a client id can read what it claims to be.
 *
 * That is safe because a public client has no secret to protect: what
 * stops a stolen authorization code being used is PKCE, and what a person
 * approves is the redirect address shown to them.
 */
export type OAuthClient = {
  /** Shown to the person approving, e.g. "Claude Code". */
  name: string;
  redirectUris: string[];
};

const PREFIX = "g1c_";
const MAX_NAME_CHARS = 80;
const MAX_REDIRECTS = 5;
const MAX_URI_CHARS = 500;
/** Schemes that run or expose content instead of opening an application. */
const FORBIDDEN_SCHEMES = new Set(["javascript:", "data:", "file:", "blob:", "vbscript:", "about:"]);
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);

function toBase64Url(text: string): string {
  const bytes = new TextEncoder().encode(text);
  let binary = "";
  for (const byte of bytes) binary += String.fromCharCode(byte);
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}

function fromBase64Url(encoded: string): string {
  const binary = atob(encoded.replace(/-/g, "+").replace(/_/g, "/"));
  return new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));
}

function parse(uri: string): URL | null {
  try {
    return new URL(uri);
  } catch {
    return null;
  }
}

function isLoopback(url: URL): boolean {
  return url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
}

/**
 * Whether an application may ask to be redirected here: an https address,
 * http on this machine only, or an application's own scheme.
 */
export function isValidRedirectUri(uri: string): boolean {
  const url = parse(uri);
  if (!url || uri.length > MAX_URI_CHARS || url.hash) return false;
  if (url.protocol === "https:") return true;
  if (url.protocol === "http:") return isLoopback(url);
  return !FORBIDDEN_SCHEMES.has(url.protocol);
}

/** The client id for a client, or null if what it asks for is not allowed. */
export function encodeOAuthClient(client: OAuthClient): string | null {
  const name = client.name.trim().slice(0, MAX_NAME_CHARS) || "An application";
  const { redirectUris } = client;
  if (
    redirectUris.length === 0 ||
    redirectUris.length > MAX_REDIRECTS ||
    !redirectUris.every(isValidRedirectUri)
  ) {
    return null;
  }
  return PREFIX + toBase64Url(JSON.stringify({ n: name, r: redirectUris }));
}

export function decodeOAuthClient(clientId: string): OAuthClient | null {
  if (!clientId.startsWith(PREFIX)) return null;
  try {
    const { n, r } = JSON.parse(fromBase64Url(clientId.slice(PREFIX.length)));
    if (typeof n !== "string" || !Array.isArray(r)) return null;
    const client = { name: n, redirectUris: r.map(String) };
    // Decoding applies the same rules as encoding, so a hand-made id gains nothing.
    return encodeOAuthClient(client) ? client : null;
  } catch {
    return null;
  }
}

/**
 * Whether `uri` is one of the client's redirect addresses. Addresses must
 * match exactly, except that an application listening on this machine may
 * use any port, since it cannot know which will be free.
 */
export function isRegisteredRedirect(client: OAuthClient, uri: string): boolean {
  const asked = parse(uri);
  if (!asked) return false;
  return client.redirectUris.some((registered) => {
    if (registered === uri) return true;
    const known = parse(registered);
    return (
      known !== null &&
      isLoopback(known) &&
      isLoopback(asked) &&
      known.hostname === asked.hostname &&
      known.pathname === asked.pathname &&
      known.search === asked.search
    );
  });
}