1/**
2 * Cloudflare expresses an API token's grants as *policies*: one entry per
3 * resource scope, each listing the permission groups allowed on it. The
4 * catalog hands back a flat list of permission groups, so minting a token
5 * means bucketing those groups by the scope they belong to.
6 */
7
8/**
9 * One `{effect, permissionGroups, resources}` entry in a token's policy list.
10 * Mutable to match the shape Cloudflare's create-token request expects.
11 */
12export interface TokenPolicy {
13 effect: "allow";
14 permissionGroups: Array<{ id: string }>;
15 resources: Record<string, string>;
16}
17
18/** A permission group as returned by Cloudflare's token-permissions catalog. */
19export interface PermissionGroup {
20 readonly id: string;
21 readonly name: string;
22 readonly category?: string;
23 readonly scopes: ReadonlyArray<string>;
24 /** Whether this group's scope maps onto a policy we know how to express. */
25 readonly selectable: boolean;
26}
27
28/** Scopes we know how to turn into a policy; anything else is not offerable. */
29export const selectableScopes: ReadonlySet<string> = new Set([
30 "com.cloudflare.api.account",
31 "com.cloudflare.api.account.zone",
32 "com.cloudflare.api.user",
33 "com.cloudflare.edge.r2.bucket",
34]);
35
36/**
37 * Bucket permission groups by resource scope into Cloudflare's policy shape.
38 * Groups whose scope has no known bucket are dropped — they cannot be granted.
39 */
40export const tokenPolicies = (
41 accountIds: ReadonlyArray<string>,
42 userId: string,
43 groups: ReadonlyArray<PermissionGroup>,
44): TokenPolicy[] => {
45 const buckets: Record<string, TokenPolicy> = {
46 "com.cloudflare.api.account": {
47 effect: "allow",
48 permissionGroups: [],
49 resources: Object.fromEntries(
50 accountIds.map((id) => [`com.cloudflare.api.account.${id}`, "*"]),
51 ),
52 },
53 "com.cloudflare.api.account.zone": {
54 effect: "allow",
55 permissionGroups: [],
56 resources: { "com.cloudflare.api.account.zone.*": "*" },
57 },
58 "com.cloudflare.api.user": {
59 effect: "allow",
60 permissionGroups: [],
61 resources: { [`com.cloudflare.api.user.${userId}`]: "*" },
62 },
63 "com.cloudflare.edge.r2.bucket": {
64 effect: "allow",
65 permissionGroups: [],
66 resources: { "com.cloudflare.edge.r2.bucket.*": "*" },
67 },
68 };
69 const seen = new Set<string>();
70 for (const group of groups) {
71 const bucket = buckets[group.scopes[0]!];
72 if (bucket === undefined || seen.has(group.id)) continue;
73 seen.add(group.id);
74 bucket.permissionGroups.push({ id: group.id });
75 }
76 return Object.values(buckets).filter(
77 (policy) => policy.permissionGroups.length > 0,
78 );
79};