import type { PermissionsSpec } from "./spec"; export const TOKEN_SCOPES = [ "contents", "actions", "issues", "pull-requests", "deployments", "statuses", "id-token", ] as const; export type TokenScope = (typeof TOKEN_SCOPES)[number]; export type AccessLevel = "read" | "write"; export type TokenPermissions = Partial>; const DEFAULT_PERMISSIONS: TokenPermissions = { contents: "read", actions: "read", issues: "read", "pull-requests": "read", deployments: "read", statuses: "read", }; function isScope(value: string): value is TokenScope { return (TOKEN_SCOPES as readonly string[]).includes(value); } function expand(spec: PermissionsSpec): TokenPermissions { if (spec === "read-all" || spec === "write-all") { const level: AccessLevel = spec === "read-all" ? "read" : "write"; return Object.fromEntries(TOKEN_SCOPES.filter((scope) => scope !== "id-token").map((scope) => [scope, level])); } const out: TokenPermissions = {}; for (const [scope, level] of Object.entries(spec)) { if (isScope(scope) && level !== "none") out[scope] = level; } return out; } /** Job token permissions: the job's `permissions` override the workflow's, which override the restricted default. */ export function resolvePermissions( workflow: PermissionsSpec | undefined, job: PermissionsSpec | undefined, ): TokenPermissions { const spec = job ?? workflow; return spec === undefined ? { ...DEFAULT_PERMISSIONS } : expand(spec); } /** Runs whose code comes from a fork are capped at read access and never receive `id-token`. */ export function restrictPermissions(permissions: TokenPermissions): TokenPermissions { const out: TokenPermissions = {}; for (const [scope, level] of Object.entries(permissions)) { if (isScope(scope) && scope !== "id-token" && level) out[scope] = "read"; } return out; } export function allows(permissions: TokenPermissions, scope: TokenScope, needed: AccessLevel): boolean { const level = permissions[scope]; if (!level) return false; return needed === "read" || level === "write"; }