Skip to content

Instantly share code, notes, and snippets.

@WalshyDev
Created July 10, 2025 16:16
Show Gist options
  • Save WalshyDev/a2d1f766ef1b079413962240edc174c3 to your computer and use it in GitHub Desktop.
Save WalshyDev/a2d1f766ef1b079413962240edc174c3 to your computer and use it in GitHub Desktop.
Workers Assets upload example with cloudflare-typescript

Workers Assets upload example with cloudflare-typescript

Setup

This example uses .env to populate the environment variables needed to process the requests. Setup a new .env file with the contents like the following:

CLOUDFLARE_ACCOUNT_ID=<ACCOUNT_ID>
CLOUDFLARE_API_TOKEN=<API_TOKEN>
ASSETS_DIRECTORY=<DIRECTORY>

With the values filled in.

Next, install the dependencies and you can start running!

$ npm install

$ npm start

This will then

Config

In the code, there are 2 bits which you may want to configure. The first is the script name, this is just a constant at the start of the main function, you may want to make this a passed in value. You may want to make this environment variable. Whatever suites your needs best.

The next is the actual backing Worker, this example uses a very basic JS Worker which simply fetches the asset. This is also a constant in the main function. You may want to make this code more complex, maybe even remove the code and go fully static site. The choice is yours :)

/*
* Generate an API token: https://developers.cloudflare.com/fundamentals/api/get-started/create-token/
* (Not Global API Key!)
*
* Find your account id: https://developers.cloudflare.com/fundamentals/setup/find-account-and-zone-ids/
*
* Set these environment variables:
* - CLOUDFLARE_API_TOKEN
* - CLOUDFLARE_ACCOUNT_ID
* - ASSETS_DIRECTORY
*/
import Cloudflare from 'cloudflare';
import { toFile } from 'cloudflare/index';
import { UploadCreateParams } from 'cloudflare/resources/workers/scripts/assets';
import { readFile } from 'node:fs/promises';
import fs from 'fs';
import crypto from 'crypto';
import { extname } from 'node:path';
import path from 'path';
import 'dotenv/config';
import { exit } from 'node:process';
const apiToken = process.env['CLOUDFLARE_API_TOKEN'] ?? '';
if (!apiToken) {
throw new Error('Please set envar CLOUDFLARE_ACCOUNT_ID');
}
const accountID = process.env['CLOUDFLARE_ACCOUNT_ID'] ?? '';
if (!accountID) {
throw new Error('Please set envar CLOUDFLARE_API_TOKEN');
}
const assetsDirectory = process.env['ASSETS_DIRECTORY'] ?? '';
if (!assetsDirectory) {
throw new Error('Please set envar ASSETS_DIRECTORY');
}
const client = new Cloudflare({
apiToken: apiToken,
});
function createManifest(directory: string): Record<string, UploadCreateParams.Manifest> {
const manifest: Record<string, UploadCreateParams.Manifest> = {};
(function processDirectory(directory: string, basePath = '') {
fs.readdirSync(directory, { withFileTypes: true }).forEach((dirent) => {
const fullPath = path.join(directory, dirent.name);
const relativePath = path.join(basePath, dirent.name);
if (dirent.isDirectory()) {
processDirectory(fullPath, relativePath);
} else {
const fileContent = fs.readFileSync(fullPath);
const extension = extname(relativePath).substring(1);
// Generate SHA-256 hash and encode in Base64
const hash = crypto
.createHash('sha256')
.update(fileContent.toString('base64') + extension)
.digest('hex')
.toString()
.slice(0, 32);
// Use forward slashes for paths in manifest
const manifestPath = `/${relativePath.replace(/\\/g, '/')}`;
manifest[manifestPath] = {
hash: hash,
size: fileContent.length,
};
}
});
})(directory);
return manifest;
}
async function main() {
const scriptName = 'my-script-with-assets';
// For simplicity in this example, we'll just create the workers script
// content directly instead of reading it from the Assets Directory (which
// would be more typical for a full Workers and Frameworks build output).
const scriptFileName = `${scriptName}.mjs`;
const scriptContent = `
export default {
async fetch(request, env, ctx) {
return env.ASSETS.fetch(request);
}
};
`;
const manifest = createManifest(assetsDirectory);
try {
const response = await client.workers.scripts.assets.upload.create(scriptName, {
account_id: accountID,
manifest: manifest,
});
const { buckets } = response;
if (!response.jwt || !buckets) {
throw new Error('There was a problem starting the Assets Upload Session');
}
if (buckets.length === 0) {
console.log('Nothing to upload!');
exit(0);
}
const uploadJwt: string = response.jwt;
const payloads: Record<string, any>[] = [];
for (const bucket of buckets) {
const payload: Record<string, any> = {};
for (const hash of bucket) {
const relativeAssetPath = Object.entries(manifest).find((record) => record[1].hash == hash)?.[0];
if (!relativeAssetPath) {
return;
}
const assetFileContents = (await readFile(path.join(assetsDirectory, relativeAssetPath))).toString(
'base64',
);
payload[hash] = assetFileContents;
}
payloads.push(payload);
}
let completionJwt: string | undefined;
// Uploads could be parallelized. For simplicity, we're just uploading one bucket at a time.
for (const payload of payloads) {
const bucketUploadReponse = await client.workers.assets.upload.create(
{
account_id: accountID,
base64: true,
body: payload,
},
{
// This API uses `Bearer: <assets_jwt>` instead of `Bearer: <api_token>`
headers: { Authorization: `Bearer ${uploadJwt}` },
},
);
// We might or might not get a new jwt to use
// See: https://developers.cloudflare.com/workers/static-assets/direct-upload
if (bucketUploadReponse?.jwt) {
completionJwt = bucketUploadReponse.jwt;
}
}
// Error if we did not get a completion token, we cannot finish the upload process without it.
if (!completionJwt) {
console.error('Did not get completion JWT');
exit(1);
}
// https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/update/
const script = await client.workers.scripts.update(scriptName, {
account_id: accountID,
// https://developers.cloudflare.com/workers/configuration/multipart-upload-metadata/
metadata: {
main_module: scriptFileName,
assets: {
config: {
not_found_handling: 'single-page-application',
},
jwt: completionJwt,
},
},
files: {
// Add main_module file. Again, you'd probably actually read this file
// from your Worker build output / assets directory.
[scriptFileName]: await toFile(Buffer.from(scriptContent), scriptFileName, {
type: 'application/javascript+module',
}),
},
});
console.log('Script and Assets uploaded successfully!');
console.log(JSON.stringify(script, null, 2));
} catch (error) {
console.error(error);
}
}
main();
{
"name": "sdk",
"version": "1.0.0",
"main": "index.ts",
"type": "module",
"scripts": {
"start": "tsx index.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"cloudflare": "^4.4.1",
"dotenv": "^17.2.0"
},
"devDependencies": {
"tsx": "^4.20.3"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment