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: `: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; continue; } const repo = REPO_SCOPE.exec(item); if (repo) { if (repos.length < MAX_REPO_SCOPES && !repos.some((r) => r.owner === repo[1] && r.name === repo[2])) repos.push({ owner: repo[1], name: repo[2] }); continue; } const [area, access] = item.split(":"); if (!isArea(area) || (access !== "read" && access !== "write")) continue; sawArea = true; if (permissions[area] !== "write") permissions[area] = access; } return { permissions: sawArea ? permissions : { ...DEFAULT_PERMISSIONS }, repos, forge }; } export function formatScope(permissions: TokenPermissions, repos: string[], forge: boolean): string { const areas = TOKEN_AREAS.flatMap((area) => { const access: AreaAccess | undefined = permissions[area]; return access ? [`${area}:${access}`] : []; }); return [...areas, ...repos.map((repo) => `repo:${repo}`), ...(forge ? [FORGE_SCOPE] : [])].join(" "); }