Skip to content

Commit a851a2c

Browse files
ToriLindsayopencode-agent[bot]ranbel
authored
[Images] Add backend clarification for signed image URLs (#28258)
* [Images] Add backend clarification for signed image URLs * Clarification * [Images] Use wrangler secret for signing key and fix code example bugs - Replace hardcoded KEY constant with env.IMAGES_SIGNING_KEY binding - Add note guiding users to store keys via wrangler secret put - Fix event.request.url bug (should be request.url in fetch handler) - Add TypeScript types to function signatures - Minor clarity improvements to prose and code comments * Apply suggestion from @ranbel Co-authored-by: ranbel <101146722+ranbel@users.noreply.github.com> --------- Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: ranbel <101146722+ranbel@users.noreply.github.com>
1 parent 322df3f commit a851a2c

1 file changed

Lines changed: 22 additions & 11 deletions

File tree

src/content/docs/images/manage-images/serve-images/serve-private-images.mdx

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,35 @@ Private images do not currently support custom paths.
2222

2323
:::
2424

25-
The example below uses a Worker that takes in a regular URL without a signed token and returns a tokenized URL that expires after one day. You can, however, set this expiration period to whatever you need, by changing the const `EXPIRATION` value.
25+
## Generate signed URLs from your backend
26+
27+
Signed URLs are generated server-side to protect your signing key. The example below uses a Cloudflare Worker, but the same signing logic can be implemented in any backend environment (Node.js, Python, PHP, Go, etc.).
28+
29+
The Worker accepts a regular Images URL and returns a signed URL that expires after one day. Adjust the `EXPIRATION` value to set a different expiry period.
30+
31+
:::note
32+
Never hardcode your signing key in source code. Store it as a secret using [`npx wrangler secret put`](/workers/wrangler/commands/#secret) and access it via the `env` parameter. For more information, refer to [Secrets](/workers/configuration/secrets/).
33+
:::
2634

2735
<TypeScriptExample>
2836

2937
```ts
30-
const KEY = "YOUR_KEY_FROM_IMAGES_DASHBOARD";
3138
const EXPIRATION = 60 * 60 * 24; // 1 day
3239

33-
const bufferToHex = (buffer) =>
40+
const bufferToHex = (buffer: ArrayBuffer) =>
3441
[...new Uint8Array(buffer)]
3542
.map((x) => x.toString(16).padStart(2, "0"))
3643
.join("");
3744

38-
async function generateSignedUrl(url) {
45+
async function generateSignedUrl(
46+
url: URL,
47+
signingKey: string,
48+
): Promise<Response> {
3949
// `url` is a full imagedelivery.net URL
4050
// e.g. https://imagedelivery.net/cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile
4151

4252
const encoder = new TextEncoder();
43-
const secretKeyData = encoder.encode(KEY);
53+
const secretKeyData = encoder.encode(signingKey);
4454
const key = await crypto.subtle.importKey(
4555
"raw",
4656
secretKeyData,
@@ -49,38 +59,39 @@ async function generateSignedUrl(url) {
4959
["sign"],
5060
);
5161

52-
// Attach the expiration value to the `url`
62+
// Attach the expiration value to the URL
5363
const expiry = Math.floor(Date.now() / 1000) + EXPIRATION;
5464
url.searchParams.set("exp", expiry);
5565
// `url` now looks like
5666
// https://imagedelivery.net/cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile?exp=1631289275
5767

5868
const stringToSign = url.pathname + "?" + url.searchParams.toString();
59-
// for example, /cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile?exp=1631289275
69+
// e.g. /cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile?exp=1631289275
6070

61-
// Generate the signature
71+
// Generate the HMAC signature
6272
const mac = await crypto.subtle.sign(
6373
"HMAC",
6474
key,
6575
encoder.encode(stringToSign),
6676
);
6777
const sig = bufferToHex(new Uint8Array(mac).buffer);
6878

69-
// And attach it to the `url`
79+
// Attach the signature to the URL
7080
url.searchParams.set("sig", sig);
7181

7282
return new Response(url);
7383
}
7484

7585
export default {
7686
async fetch(request, env, ctx): Promise<Response> {
77-
const url = new URL(event.request.url);
87+
const url = new URL(request.url);
7888
const imageDeliveryURL = new URL(
7989
url.pathname
8090
.slice(1)
8191
.replace("https:/imagedelivery.net", "https://imagedelivery.net"),
8292
);
83-
return generateSignedUrl(imageDeliveryURL);
93+
// IMAGES_SIGNING_KEY is set via `npx wrangler secret put IMAGES_SIGNING_KEY`
94+
return generateSignedUrl(imageDeliveryURL, env.IMAGES_SIGNING_KEY);
8495
},
8596
} satisfies ExportedHandler<Env>;
8697
```

0 commit comments

Comments
 (0)