From 0b34efcc25270d021baf481f0629a1581d28baf3 Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Tue, 7 Oct 2025 23:50:55 -0700 Subject: [PATCH 1/5] chore: add s3 signed URL example for AccountApiToken --- .../providers/cloudflare/account-api-token.md | 190 +++++++++++++++--- 1 file changed, 161 insertions(+), 29 deletions(-) diff --git a/alchemy-web/src/content/docs/providers/cloudflare/account-api-token.md b/alchemy-web/src/content/docs/providers/cloudflare/account-api-token.md index f6e60d9f0..9614a4aad 100644 --- a/alchemy-web/src/content/docs/providers/cloudflare/account-api-token.md +++ b/alchemy-web/src/content/docs/providers/cloudflare/account-api-token.md @@ -1,13 +1,13 @@ --- title: AccountApiToken -description: Learn how to create and manage Cloudflare Account API Tokens using Alchemy for secure access to the Cloudflare API. +description: Learn how to create and manage Cloudflare Account API Tokens using Alchemy for secure access to the Cloudflare API and R2 storage. --- -Creates a [Cloudflare API Token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with specified permissions and access controls. +Creates a [Cloudflare API Token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with specified permissions and access controls. Account API Tokens are the primary way to generate S3-compatible credentials for R2 storage, enabling pre-signed URLs and direct S3 API access. ## Minimal Example -Create a basic API token with read-only permissions. +Create a basic API token with read-only permissions for zones. ```ts import { AccountApiToken } from "alchemy/cloudflare"; @@ -26,9 +26,126 @@ const token = await AccountApiToken("readonly-token", { }); ``` +## R2 Bucket with Pre-Signed URLs + +Create an API token for R2 bucket access and use it to generate pre-signed URLs. This is the recommended way to provide temporary access to R2 objects. + +```ts +import { AccountApiToken, R2Bucket } from "alchemy/cloudflare"; +import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; + +// Create R2 bucket +const bucket = await R2Bucket("my-bucket", { + name: "my-bucket", +}); + +// Create API token with R2 permissions +const r2Token = await AccountApiToken("r2-access-token", { + name: "R2 Access Token", + policies: [ + { + effect: "allow", + permissionGroups: [ + "Workers R2 Storage Read", + "Workers R2 Storage Write", + ], + resources: { + [`com.cloudflare.edge.r2.bucket.${process.env.CLOUDFLARE_ACCOUNT_ID}_${bucket.jurisdiction ?? "default"}_${bucket.name}`]: + "*", + }, + }, + ], +}); + +// Configure S3 client with the token +const s3Client = new S3Client({ + region: "auto", + endpoint: `https://${process.env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`, + credentials: { + accessKeyId: r2Token.accessKeyId.unencrypted, + secretAccessKey: r2Token.secretAccessKey.unencrypted, + }, +}); + +// Generate a pre-signed URL for an object +const command = new GetObjectCommand({ + Bucket: bucket.name, + Key: "path/to/file.pdf", +}); + +const signedUrl = await getSignedUrl(s3Client, command, { + expiresIn: 3600, // URL expires in 1 hour +}); + +console.log("Pre-signed URL:", signedUrl); +``` + +## R2 Storage in Worker + +Bind an R2 bucket directly to a Worker for runtime access, and use an API token for generating pre-signed URLs from within the Worker. + +```ts +import { Worker, AccountApiToken, R2Bucket } from "alchemy/cloudflare"; + +const bucket = await R2Bucket("storage", { + name: "my-storage", +}); + +const r2Token = await AccountApiToken("r2-token", { + name: "R2 Token for Signed URLs", + policies: [ + { + effect: "allow", + permissionGroups: ["Workers R2 Storage Read"], + resources: { + [`com.cloudflare.edge.r2.bucket.${process.env.CLOUDFLARE_ACCOUNT_ID}_default_${bucket.name}`]: + "*", + }, + }, + ], +}); + +await Worker("file-server", { + name: "file-server", + bindings: { + BUCKET: bucket, + R2_ACCESS_KEY_ID: r2Token.accessKeyId, + R2_SECRET_ACCESS_KEY: r2Token.secretAccessKey, + }, + script: ` + import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; + import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; + + export default { + async fetch(request, env) { + // Generate pre-signed URL + const s3 = new S3Client({ + region: "auto", + endpoint: "https://${process.env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com", + credentials: { + accessKeyId: env.R2_ACCESS_KEY_ID, + secretAccessKey: env.R2_SECRET_ACCESS_KEY, + }, + }); + + const command = new GetObjectCommand({ + Bucket: "my-storage", + Key: "file.pdf", + }); + + const signedUrl = await getSignedUrl(s3, command, { expiresIn: 3600 }); + + return Response.json({ url: signedUrl }); + } + } + `, +}); +``` + ## With Time and IP Restrictions -Create a token with time-based and IP address restrictions. +Create a token with time-based and IP address restrictions for enhanced security. ```ts import { AccountApiToken } from "alchemy/cloudflare"; @@ -55,19 +172,19 @@ const restrictedToken = await AccountApiToken("restricted-token", { }); ``` -## R2 Storage Access Token +## Account-Level Permissions -Create a token with R2 storage write permissions. +Create a token with broad account-level permissions for R2 storage operations. ```ts import { AccountApiToken } from "alchemy/cloudflare"; -const storageToken = await AccountApiToken("account-access-token", { - name: "alchemy-account-access-token", +const accountToken = await AccountApiToken("account-access-token", { + name: "Account R2 Token", policies: [ { effect: "allow", - permissionGroups: ["Workers R2 Storage Write"], + permissionGroups: ["Workers R2 Storage Write", "Workers R2 Storage Read"], resources: { "com.cloudflare.api.account": "*", }, @@ -76,31 +193,46 @@ const storageToken = await AccountApiToken("account-access-token", { }); ``` -## Bind to a Worker +## Understanding Access Credentials -Use the token in a Worker binding. +The `AccountApiToken` resource provides S3-compatible credentials through two properties: -```ts -import { Worker, AccountApiToken } from "alchemy/cloudflare"; +- **`accessKeyId`**: The token's ID, used as the AWS access key ID +- **`secretAccessKey`**: A SHA-256 hash of the token value, used as the AWS secret access key + +These credentials work with any S3-compatible client library, including the AWS SDK. -const token = await AccountApiToken("api-token", { - name: "Worker API Token", +```ts +const token = await AccountApiToken("my-token", { + name: "My Token", policies: [ - { - effect: "allow", - permissionGroups: ["Zone Read"], - resources: { - "com.cloudflare.api.account.zone.*": "*", - }, - }, + /* ... */ ], }); -await Worker("my-worker", { - name: "my-worker", - script: "console.log('Hello, world!')", - bindings: { - API_TOKEN: token, - }, -}); +// Use with S3 SDK +console.log("Access Key ID:", token.accessKeyId.unencrypted); +console.log("Secret Access Key:", token.secretAccessKey.unencrypted); ``` + +## Important Notes + +:::tip[Pre-signed URLs] +Pre-signed URLs are the recommended way to provide temporary access to R2 objects. They're more secure than public access and work with existing S3 tooling. +::: + +:::caution[API Token Requirements] +Creating Account API Tokens programmatically requires either: + +- A Global API Key +- An API Token with permission to create other API Tokens + +See the [Cloudflare setup guide](/guides/cloudflare/#api-token) for more details. +::: + +:::note[Resource Naming] +When specifying R2 bucket resources, use the format: +`com.cloudflare.edge.r2.bucket.{accountId}_{jurisdiction}_{bucketName}` + +For buckets without a jurisdiction, use `default` as the jurisdiction value. +::: From 7bfca8d3e30ce0fbdafcaeb43180b1b884be803e Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Wed, 8 Oct 2025 00:03:36 -0700 Subject: [PATCH 2/5] update docs --- .../providers/cloudflare/account-api-token.md | 64 +------------------ 1 file changed, 1 insertion(+), 63 deletions(-) diff --git a/alchemy-web/src/content/docs/providers/cloudflare/account-api-token.md b/alchemy-web/src/content/docs/providers/cloudflare/account-api-token.md index 9614a4aad..788c9e6a2 100644 --- a/alchemy-web/src/content/docs/providers/cloudflare/account-api-token.md +++ b/alchemy-web/src/content/docs/providers/cloudflare/account-api-token.md @@ -28,7 +28,7 @@ const token = await AccountApiToken("readonly-token", { ## R2 Bucket with Pre-Signed URLs -Create an API token for R2 bucket access and use it to generate pre-signed URLs. This is the recommended way to provide temporary access to R2 objects. +Create an API token for R2 bucket access and use it to generate pre-signed URLs. ```ts import { AccountApiToken, R2Bucket } from "alchemy/cloudflare"; @@ -81,68 +81,6 @@ const signedUrl = await getSignedUrl(s3Client, command, { console.log("Pre-signed URL:", signedUrl); ``` -## R2 Storage in Worker - -Bind an R2 bucket directly to a Worker for runtime access, and use an API token for generating pre-signed URLs from within the Worker. - -```ts -import { Worker, AccountApiToken, R2Bucket } from "alchemy/cloudflare"; - -const bucket = await R2Bucket("storage", { - name: "my-storage", -}); - -const r2Token = await AccountApiToken("r2-token", { - name: "R2 Token for Signed URLs", - policies: [ - { - effect: "allow", - permissionGroups: ["Workers R2 Storage Read"], - resources: { - [`com.cloudflare.edge.r2.bucket.${process.env.CLOUDFLARE_ACCOUNT_ID}_default_${bucket.name}`]: - "*", - }, - }, - ], -}); - -await Worker("file-server", { - name: "file-server", - bindings: { - BUCKET: bucket, - R2_ACCESS_KEY_ID: r2Token.accessKeyId, - R2_SECRET_ACCESS_KEY: r2Token.secretAccessKey, - }, - script: ` - import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; - import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; - - export default { - async fetch(request, env) { - // Generate pre-signed URL - const s3 = new S3Client({ - region: "auto", - endpoint: "https://${process.env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com", - credentials: { - accessKeyId: env.R2_ACCESS_KEY_ID, - secretAccessKey: env.R2_SECRET_ACCESS_KEY, - }, - }); - - const command = new GetObjectCommand({ - Bucket: "my-storage", - Key: "file.pdf", - }); - - const signedUrl = await getSignedUrl(s3, command, { expiresIn: 3600 }); - - return Response.json({ url: signedUrl }); - } - } - `, -}); -``` - ## With Time and IP Restrictions Create a token with time-based and IP address restrictions for enhanced security. From 7601691a9db48ac2a3ef1d6d4e83ba589143eecb Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Wed, 8 Oct 2025 00:03:57 -0700 Subject: [PATCH 3/5] remove id in accountid --- .../src/content/docs/providers/cloudflare/account-id.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/alchemy-web/src/content/docs/providers/cloudflare/account-id.md b/alchemy-web/src/content/docs/providers/cloudflare/account-id.md index 6ef410c42..ac7da8dee 100644 --- a/alchemy-web/src/content/docs/providers/cloudflare/account-id.md +++ b/alchemy-web/src/content/docs/providers/cloudflare/account-id.md @@ -12,7 +12,7 @@ Get the account ID from environment variables or API token: ```ts import { AccountId } from "alchemy/cloudflare"; -const accountId = await AccountId("my-account"); +const accountId = await AccountId(); ``` ## With Explicit API Key @@ -22,7 +22,7 @@ Provide an API key and email directly: ```ts import { AccountId } from "alchemy/cloudflare"; -const accountId = await AccountId("my-account", { +const accountId = await AccountId(, { apiKey: alchemy.secret(process.env.CF_API_KEY), email: "user@example.com", }); @@ -35,7 +35,7 @@ Use the account ID with a Worker: ```ts import { Worker, AccountId } from "alchemy/cloudflare"; -const accountId = await AccountId("my-account"); +const accountId = await AccountId(); await Worker("my-worker", { name: "my-worker", From b6a9f133c163b66a67a59a97c0bbc554ac8a7e8c Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Wed, 8 Oct 2025 00:26:02 -0700 Subject: [PATCH 4/5] add catuon --- .../content/docs/providers/cloudflare/account-api-token.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/alchemy-web/src/content/docs/providers/cloudflare/account-api-token.md b/alchemy-web/src/content/docs/providers/cloudflare/account-api-token.md index 788c9e6a2..fe8797e89 100644 --- a/alchemy-web/src/content/docs/providers/cloudflare/account-api-token.md +++ b/alchemy-web/src/content/docs/providers/cloudflare/account-api-token.md @@ -5,6 +5,10 @@ description: Learn how to create and manage Cloudflare Account API Tokens using Creates a [Cloudflare API Token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with specified permissions and access controls. Account API Tokens are the primary way to generate S3-compatible credentials for R2 storage, enabling pre-signed URLs and direct S3 API access. +:::caution +Account API Tokens can not be created with OAuth tokens because of a Cloudflare limitation. use the Global API Key or an API Token instead. See the [Cloudflare Auth guide](/guides/cloudflare) for more details. +::: + ## Minimal Example Create a basic API token with read-only permissions for zones. From 5c924ef3350a5c2aa30b9dae9b627382f5331ec7 Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Thu, 9 Oct 2025 15:22:32 -0700 Subject: [PATCH 5/5] remove useless notes --- .../providers/cloudflare/account-api-token.md | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/alchemy-web/src/content/docs/providers/cloudflare/account-api-token.md b/alchemy-web/src/content/docs/providers/cloudflare/account-api-token.md index fe8797e89..44857afbd 100644 --- a/alchemy-web/src/content/docs/providers/cloudflare/account-api-token.md +++ b/alchemy-web/src/content/docs/providers/cloudflare/account-api-token.md @@ -156,25 +156,3 @@ const token = await AccountApiToken("my-token", { console.log("Access Key ID:", token.accessKeyId.unencrypted); console.log("Secret Access Key:", token.secretAccessKey.unencrypted); ``` - -## Important Notes - -:::tip[Pre-signed URLs] -Pre-signed URLs are the recommended way to provide temporary access to R2 objects. They're more secure than public access and work with existing S3 tooling. -::: - -:::caution[API Token Requirements] -Creating Account API Tokens programmatically requires either: - -- A Global API Key -- An API Token with permission to create other API Tokens - -See the [Cloudflare setup guide](/guides/cloudflare/#api-token) for more details. -::: - -:::note[Resource Naming] -When specifying R2 bucket resources, use the format: -`com.cloudflare.edge.r2.bucket.{accountId}_{jurisdiction}_{bucketName}` - -For buckets without a jurisdiction, use `default` as the jurisdiction value. -:::