@@ -4,6 +4,8 @@ import { createHash } from "node:crypto";
44
55const API_BASE = "https://api.cloudflare.com/client/v4" ;
66const R2_BUCKET_ITEM_WRITE = "Workers R2 Storage Bucket Item Write" ;
7+ const TOKEN_OWNER_ACCOUNT = "account" ;
8+ const TOKEN_OWNER_USER = "user" ;
79
810const mode = process . argv [ 2 ] ;
911
@@ -46,6 +48,30 @@ function endpointFor(accountId, jurisdiction) {
4648 return `https://${ accountId } .r2.cloudflarestorage.com` ;
4749}
4850
51+ function tokenOwner ( ) {
52+ const owner = optional ( "CLOUDFLARE_API_TOKEN_OWNER" , TOKEN_OWNER_ACCOUNT ) ;
53+ if ( ! [ TOKEN_OWNER_ACCOUNT , TOKEN_OWNER_USER ] . includes ( owner ) ) {
54+ throw new Error (
55+ `Unsupported CLOUDFLARE_API_TOKEN_OWNER '${ owner } '. Use '${ TOKEN_OWNER_ACCOUNT } ' or '${ TOKEN_OWNER_USER } '.` ,
56+ ) ;
57+ }
58+ return owner ;
59+ }
60+
61+ function tokenCollectionPath ( accountId ) {
62+ return tokenOwner ( ) === TOKEN_OWNER_USER
63+ ? "/user/tokens"
64+ : `/accounts/${ accountId } /tokens` ;
65+ }
66+
67+ function tokenItemPath ( accountId , tokenId ) {
68+ return `${ tokenCollectionPath ( accountId ) } /${ encodeURIComponent ( tokenId ) } ` ;
69+ }
70+
71+ function tokenPermissionGroupsPath ( accountId ) {
72+ return `${ tokenCollectionPath ( accountId ) } /permission_groups` ;
73+ }
74+
4975function bucketResource ( accountId , jurisdiction , bucketName ) {
5076 return `com.cloudflare.edge.r2.bucket.${ accountId } _${ jurisdiction } _${ bucketName } ` ;
5177}
@@ -68,6 +94,25 @@ function appendGitHubEnv(values) {
6894 } ) ;
6995}
7096
97+ async function writeShellEnvFile ( values ) {
98+ const envFile = optional ( "R2_RUNTIME_ENV_FILE" ) ;
99+ if ( ! envFile ) {
100+ return ;
101+ }
102+
103+ const { appendFileSync } = await import ( "node:fs" ) ;
104+ const lines = Object . entries ( values ) . map ( ( [ key , value ] ) => {
105+ const escaped = String ( value ) . replace ( / ' / g, "'\\''" ) ;
106+ return `${ key } ='${ escaped } '` ;
107+ } ) ;
108+ appendFileSync ( envFile , `${ lines . join ( "\n" ) } \n` , "utf8" ) ;
109+ }
110+
111+ async function exportRuntimeValues ( values ) {
112+ await appendGitHubEnv ( values ) ;
113+ await writeShellEnvFile ( values ) ;
114+ }
115+
71116async function cloudflare ( token , path , init = { } ) {
72117 const response = await fetch ( `${ API_BASE } ${ path } ` , {
73118 ...init ,
@@ -150,21 +195,24 @@ async function ensureBucket(accountId, token, bucketName, jurisdiction) {
150195 console . log ( `Created R2 bucket '${ bucketName } '.` ) ;
151196}
152197
153- async function permissionGroupId ( token , permissionName ) {
154- const payload = await cloudflare ( token , "/user/tokens/permission_groups" ) ;
198+ async function permissionGroupId ( accountId , token , permissionName ) {
199+ const payload = await cloudflare (
200+ token ,
201+ `${ tokenPermissionGroupsPath ( accountId ) } ?name=${ encodeURIComponent ( permissionName ) } ` ,
202+ ) ;
155203 const match = payload . result ?. find ( ( group ) => group . name === permissionName ) ;
156204 if ( ! match ?. id ) {
157205 throw new Error ( `Cloudflare permission group '${ permissionName } ' was not found.` ) ;
158206 }
159207 return match . id ;
160208}
161209
162- async function revokeToken ( token , tokenId , reason ) {
210+ async function revokeToken ( accountId , token , tokenId , reason ) {
163211 if ( ! tokenId ) {
164212 return ;
165213 }
166214 try {
167- await cloudflare ( token , `/user/tokens/ ${ encodeURIComponent ( tokenId ) } ` , {
215+ await cloudflare ( token , tokenItemPath ( accountId , tokenId ) , {
168216 method : "DELETE" ,
169217 } ) ;
170218 console . log ( `Revoked Cloudflare token '${ tokenId } '${ reason ? ` (${ reason } )` : "" } .` ) ;
@@ -178,7 +226,7 @@ async function revokeToken(token, tokenId, reason) {
178226}
179227
180228async function createRuntimeToken ( accountId , token , bucketName , jurisdiction ) {
181- const groupId = await permissionGroupId ( token , R2_BUCKET_ITEM_WRITE ) ;
229+ const groupId = await permissionGroupId ( accountId , token , R2_BUCKET_ITEM_WRITE ) ;
182230 const tokenName = optional ( "R2_TOKEN_NAME" , `modtale preview ${ bucketName } ` ) ;
183231 const expiresOn = optional ( "R2_TOKEN_EXPIRES_ON" ) ;
184232
@@ -199,7 +247,7 @@ async function createRuntimeToken(accountId, token, bucketName, jurisdiction) {
199247 body . expires_on = expiresOn ;
200248 }
201249
202- const payload = await cloudflare ( token , "/user/tokens" , {
250+ const payload = await cloudflare ( token , tokenCollectionPath ( accountId ) , {
203251 method : "POST" ,
204252 body : JSON . stringify ( body ) ,
205253 } ) ;
@@ -235,6 +283,7 @@ async function provision() {
235283
236284 await ensureBucket ( accountId , bucketToken , bucketName , jurisdiction ) ;
237285 await revokeToken (
286+ accountId ,
238287 tokenProvisioner ,
239288 optional ( "EXISTING_R2_RUNTIME_TOKEN_ID" ) ,
240289 "replacing runtime token" ,
@@ -247,7 +296,7 @@ async function provision() {
247296 jurisdiction ,
248297 ) ;
249298
250- await appendGitHubEnv ( {
299+ await exportRuntimeValues ( {
251300 R2_RUNTIME_ACCESS_KEY : credentials . accessKey ,
252301 R2_RUNTIME_SECRET_KEY : credentials . secretKey ,
253302 R2_RUNTIME_TOKEN_ID : credentials . tokenId ,
@@ -267,7 +316,7 @@ async function ensureOnly() {
267316 const jurisdiction = normalizeJurisdiction ( optional ( "CLOUDFLARE_R2_JURISDICTION" ) ) ;
268317
269318 await ensureBucket ( accountId , bucketToken , bucketName , jurisdiction ) ;
270- await appendGitHubEnv ( {
319+ await exportRuntimeValues ( {
271320 R2_RUNTIME_ENDPOINT : endpointFor ( accountId , jurisdiction ) ,
272321 } ) ;
273322}
@@ -301,7 +350,12 @@ async function cleanup() {
301350 }
302351
303352 if ( tokenProvisioner ) {
304- await revokeToken ( tokenProvisioner , optional ( "R2_RUNTIME_TOKEN_ID" ) , "preview cleanup" ) ;
353+ await revokeToken (
354+ accountId ,
355+ tokenProvisioner ,
356+ optional ( "R2_RUNTIME_TOKEN_ID" ) ,
357+ "preview cleanup" ,
358+ ) ;
305359 }
306360}
307361
0 commit comments