| 1 | /** |
| 2 | * Fine-grained personal access tokens: each permission, as the form and the |
| 3 | * API name it, and the g1t scopes each level gives. Mirrors |
| 4 | * `crates/contracts/src/fine_grained.rs`, which is the source of truth; a |
| 5 | * Rust test keeps the table here the same. |
| 6 | * |
| 7 | * A fine-grained token has one resource owner (your own account, or one |
| 8 | * workspace), reaches all, selected or only public repositories of it, |
| 9 | * and has a level for each permission. Its scopes are stored and checked |
| 10 | * as a classic token's are. |
| 11 | */ |
| 12 | |
| 13 | import type { Scope } from "./scopes"; |
| 14 | |
| 15 | export type PermissionGroup = "repository" | "workspace" | "account"; |
| 16 | |
| 17 | export type PermissionAccess = "none" | "read" | "write" | "admin"; |
| 18 | |
| 19 | export type RepositorySelection = "all" | "selected" | "public"; |
| 20 | |
| 21 | export type FineGrainedPermission = { |
| 22 | name: string; |
| 23 | label: string; |
| 24 | group: PermissionGroup; |
| 25 | about: string; |
| 26 | /** The scopes reading gives; empty when it cannot be read only. */ |
| 27 | read: readonly Scope[]; |
| 28 | /** The scopes writing gives, besides reading's. */ |
| 29 | write: readonly Scope[]; |
| 30 | /** The scopes admin gives, besides writing's; empty when it has none. */ |
| 31 | admin: readonly Scope[]; |
| 32 | }; |
| 33 | |
| 34 | /** Every permission, in the order the form shows them. */ |
| 35 | export const PERMISSIONS = [ |
| 36 | { name: "actions", label: "Actions", group: "repository", about: "Workflow runs, jobs, logs and artifacts: reading them, and running, cancelling and rerunning workflows", read: ["workflows:read"], write: ["workflows:write"], admin: [] }, |
| 37 | { name: "administration", label: "Administration", group: "repository", about: "Repository settings, rulesets, who has access and deploy keys; renaming, archiving, transferring and deleting", read: ["repo:read", "access:read"], write: ["repo:admin", "access:admin"], admin: [] }, |
| 38 | { name: "agents", label: "g1t agents", group: "repository", about: "Putting g1t's agents to work and messaging them, which uses the workspace's money", read: [], write: ["agents:run"], admin: [] }, |
| 39 | { name: "checks", label: "Checks", group: "repository", about: "Check runs and check suites on commits. Shares its scopes with Commit statuses", read: ["checks:read"], write: ["checks:write"], admin: [] }, |
| 40 | { name: "contents", label: "Contents", group: "repository", about: "Code, branches, commits and releases: cloning and fetching, pushing, and publishing releases", read: ["code:read"], write: ["code:write", "repo:write"], admin: [] }, |
| 41 | { name: "deployments", label: "Deployments", group: "repository", about: "Deployments and their statuses", read: ["deployments:read"], write: ["deployments:write"], admin: [] }, |
| 42 | { name: "environments", label: "Environments", group: "repository", about: "Environments, and their secrets and variables", read: ["deployments:read", "secrets:read"], write: ["secrets:admin"], admin: [] }, |
| 43 | { name: "issues", label: "Issues", group: "repository", about: "Issues, their comments, labels and milestones, and plans", read: ["issues:read"], write: ["issues:write"], admin: [] }, |
| 44 | { name: "memory", label: "Memory and context", group: "repository", about: "Recalling memory and searching the workspace's context, and saving memory for the next agent", read: ["memory:read"], write: ["memory:write"], admin: [] }, |
| 45 | { name: "metadata", label: "Metadata", group: "repository", about: "Seeing repositories and searching them. Always read", read: ["repo:read"], write: [], admin: [] }, |
| 46 | { name: "packages", label: "Packages", group: "repository", about: "Pulling private packages, publishing them, and (admin) deleting packages and versions", read: ["packages:read"], write: ["packages:write"], admin: ["packages:delete"] }, |
| 47 | { name: "pages", label: "Pages", group: "repository", about: "Deployments on g1t.page. Shares its scopes with Deployments", read: ["deployments:read"], write: ["deployments:write"], admin: [] }, |
| 48 | { name: "pull_requests", label: "Pull requests", group: "repository", about: "Pull requests, their reviews, changes, sessions and merge queues", read: ["pull_requests:read"], write: ["pull_requests:write"], admin: [] }, |
| 49 | { name: "secrets", label: "Secrets", group: "repository", about: "Actions secrets: listing them (never their values), setting and deleting them. Shares its scopes with Variables", read: ["secrets:read"], write: ["secrets:admin"], admin: [] }, |
| 50 | { name: "security_events", label: "Security events and alerts", group: "repository", about: "Code scanning, secret scanning and vulnerability alerts, SARIF uploads and security settings", read: ["security:read"], write: ["security:write"], admin: [] }, |
| 51 | { name: "statuses", label: "Commit statuses", group: "repository", about: "Statuses on commits. Shares its scopes with Checks", read: ["checks:read"], write: ["checks:write"], admin: [] }, |
| 52 | { name: "variables", label: "Variables", group: "repository", about: "Actions variables: reading, setting and deleting them. Shares its scopes with Secrets", read: ["secrets:read"], write: ["secrets:admin"], admin: [] }, |
| 53 | { name: "webhooks", label: "Webhooks", group: "repository", about: "Webhooks and their deliveries", read: ["webhooks:read"], write: ["webhooks:admin"], admin: [] }, |
| 54 | { name: "workflows", label: "Workflows", group: "repository", about: "Adding, changing and deleting workflow files under .g1t/workflows and .github/workflows. Write only", read: [], write: ["workflow_files:write"], admin: [] }, |
| 55 | { name: "members", label: "Members", group: "workspace", about: "The workspace's people, invitations and teams", read: ["workspace:read"], write: ["workspace:admin"], admin: [] }, |
| 56 | { name: "workspace_administration", label: "Administration", group: "workspace", about: "The workspace's settings, integrations, rulesets and base permission", read: ["workspace:read", "access:read"], write: ["workspace:admin", "access:admin"], admin: [] }, |
| 57 | { name: "workspace_billing", label: "Billing", group: "workspace", about: "Usage, budget, AI credit and invoices, and (write) changing the budget and buying credit", read: ["billing:read"], write: ["billing:write"], admin: [] }, |
| 58 | { name: "models", label: "AI Gateway", group: "workspace", about: "AI Gateway requests: seeing them, and sending requests, which uses the workspace's AI credit", read: ["models:read"], write: ["models:write"], admin: [] }, |
| 59 | { name: "self_hosted_runners", label: "Self-hosted runners", group: "workspace", about: "Runners, their groups and settings", read: ["runners:read"], write: ["runners:admin"], admin: [] }, |
| 60 | { name: "workspace_secrets", label: "Secrets", group: "workspace", about: "The workspace's Actions secrets. Shares its scopes with the repository Secrets permission", read: ["secrets:read"], write: ["secrets:admin"], admin: [] }, |
| 61 | { name: "workspace_webhooks", label: "Webhooks", group: "workspace", about: "The workspace's webhooks. Shares its scopes with the repository Webhooks permission", read: ["webhooks:read"], write: ["webhooks:admin"], admin: [] }, |
| 62 | { name: "email_addresses", label: "Email addresses", group: "account", about: "Your email addresses and email settings, invites and invitations", read: ["account:read"], write: ["account:write"], admin: [] }, |
| 63 | { name: "starring", label: "Starring", group: "account", about: "Stars and pinned projects. Shares its scopes with Email addresses", read: ["account:read"], write: ["account:write"], admin: [] }, |
| 64 | { name: "notifications", label: "Notifications", group: "account", about: "Your inbox, subscriptions and watched repositories", read: ["notifications:read"], write: ["notifications:write"], admin: [] }, |
| 65 | ] as const satisfies readonly FineGrainedPermission[]; |
| 66 | |
| 67 | export type PermissionName = (typeof PERMISSIONS)[number]["name"]; |
| 68 | |
| 69 | export const PERMISSION_GROUPS: { group: PermissionGroup; label: string; about: string }[] = [ |
| 70 | { group: "repository", label: "Repository permissions", about: "What it may do in the repositories it reaches." }, |
| 71 | { group: "workspace", label: "Workspace permissions", about: "What it may do with the workspace itself." }, |
| 72 | { group: "account", label: "Account permissions", about: "What it may do with your own account." }, |
| 73 | ]; |
| 74 | |
| 75 | /** The longest a fine-grained token may last, whatever a workspace allows. */ |
| 76 | export const FINE_GRAINED_MAX_LIFETIME_DAYS = 366; |
| 77 | |
| 78 | /** The levels a permission can be set to, least first, none excluded. */ |
| 79 | export function permissionLevels(permission: FineGrainedPermission): PermissionAccess[] { |
| 80 | const levels: PermissionAccess[] = []; |
| 81 | if (permission.read.length > 0) levels.push("read"); |
| 82 | if (permission.write.length > 0) levels.push("write"); |
| 83 | if (permission.admin.length > 0) levels.push("admin"); |
| 84 | return levels; |
| 85 | } |
| 86 | |
| 87 | const ORDER: Record<PermissionAccess, number> = { none: 0, read: 1, write: 2, admin: 3 }; |
| 88 | |
| 89 | /** The scopes a level of a permission gives, lower levels' included. */ |
| 90 | export function permissionScopes(permission: FineGrainedPermission, access: PermissionAccess): Scope[] { |
| 91 | const scopes: Scope[] = []; |
| 92 | if (ORDER[access] >= ORDER.read) scopes.push(...permission.read); |
| 93 | if (ORDER[access] >= ORDER.write) scopes.push(...permission.write); |
| 94 | if (ORDER[access] >= ORDER.admin) scopes.push(...permission.admin); |
| 95 | return scopes; |
| 96 | } |
| 97 | |
| 98 | /** A token's permissions: each name's level; names left out are none. */ |
| 99 | export type TokenPermissions = Partial<Record<string, PermissionAccess>>; |
| 100 | |
| 101 | /** The permission named `name`. */ |
| 102 | export function findPermission(name: string): FineGrainedPermission | undefined { |
| 103 | return PERMISSIONS.find((permission) => permission.name === name); |
| 104 | } |