Skip to content
104 linesCodeBlameRaw
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
13import type { Scope } from "./scopes";
14
15export type PermissionGroup = "repository" | "workspace" | "account";
16
17export type PermissionAccess = "none" | "read" | "write" | "admin";
18
19export type RepositorySelection = "all" | "selected" | "public";
20
21export 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. */
35export 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
67export type PermissionName = (typeof PERMISSIONS)[number]["name"];
68
69export 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. */
76export const FINE_GRAINED_MAX_LIFETIME_DAYS = 366;
77
78/** The levels a permission can be set to, least first, none excluded. */
79export 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
87const 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. */
90export 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. */
99export type TokenPermissions = Partial<Record<string, PermissionAccess>>;
100
101/** The permission named `name`. */
102export function findPermission(name: string): FineGrainedPermission | undefined {
103 return PERMISSIONS.find((permission) => permission.name === name);
104}