64 lines · 2.4 KB
import { TOKEN_AREAS } from "../token-scope";
import type { AreaAccess, TokenArea, TokenPermissions } from "../token-scope";

/** Agents get read access everywhere they are granted unless they ask for more. */
const DEFAULT_PERMISSIONS: TokenPermissions = { contents: "read", issues: "read", pulls: "read", actions: "read" };
const MAX_REPO_SCOPES = 50;
const REPO_SCOPE = /^repo:([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/;

/** Asks for the issued token to also work for git, LFS and REST; the user must still tick it at consent. */
export const FORGE_SCOPE = "forge:api";

export const SCOPES_SUPPORTED = [
  ...TOKEN_AREAS.flatMap((area) => [`${area}:read`, `${area}:write`]),
  "repo:{owner}/{name}",
  FORGE_SCOPE,
];

export type RequestedScope = {
  permissions: TokenPermissions;
  repos: Array<{ owner: string; name: string }>;
  forge: boolean;
};

function isArea(value: string): value is TokenArea {
  return (TOKEN_AREAS as readonly string[]).includes(value);
}

/**
 * Scope grammar: `<area>:read|write` for each fine-grained token area and `repo:owner/name` to pin
 * the grant to specific repositories. No repo scopes means "all repositories", which the consent
 * screen calls out explicitly.
 */
export function parseScope(raw: string | null): RequestedScope {
  const permissions: TokenPermissions = {};
  const repos: RequestedScope["repos"] = [];
  let sawArea = false;
  let forge = false;
  for (const item of (raw ?? "").split(/\s+/).filter(Boolean)) {
    if (item === FORGE_SCOPE) {
      forge = true;