Summary
The Cloudflare Images and Cloudflare Stream media-provider runtimes document *EnvVar options as runtime environment-variable names, but they resolve those names only from cloudflare:workers env. Under a Node adapter, exported process.env values are ignored and provider operations fail as missing credentials.
This is both an implementation defect for an advertised adapter-neutral config shape and a documentation ambiguity: the error says to set an environment variable, and the provider examples say the default/custom names are read at runtime, without saying that only Workers bindings are consulted.
Minimal reproduction (Node adapter)
// astro.config.mjs
import node from '@astrojs/node';
import { cloudflareImages } from '@emdash-cms/cloudflare';
import { defineConfig } from 'astro/config';
import emdash from 'emdash/astro';
export default defineConfig({
adapter: node({ mode: 'standalone' }),
integrations: [
emdash({
mediaProviders: [
cloudflareImages({
accountIdEnvVar: 'CF_ACCOUNT_ID',
accountHashEnvVar: 'CF_IMAGES_ACCOUNT_HASH',
apiTokenEnvVar: 'CF_IMAGES_TOKEN',
}),
],
}),
],
});
Export non-secret test values before starting Astro (real credentials are needed only to get past resolution):
CF_ACCOUNT_ID=test-account \
CF_IMAGES_ACCOUNT_HASH=test-hash \
CF_IMAGES_TOKEN=test-token \
astro dev
Open the media library or invoke a provider operation. Expected: the provider resolves the exported Node environment variables and reaches the API (which may reject these test values). Actual: resolution stops locally with Cloudflare Images: Missing CF_ACCOUNT_ID.
Cloudflare Stream has the same behavior for accountIdEnvVar and apiTokenEnvVar.
Current implementation
Both runtimes import env directly from cloudflare:workers and resolveEnvValue() reads only:
(env as Record<string, string | undefined>)[envVar]
packages/cloudflare/src/media/images-runtime.ts
packages/cloudflare/src/media/stream-runtime.ts
Direct config values work because they are checked first, but inlining an API token into the generated virtual:emdash/media-providers module is not an appropriate general workaround for secrets.
Expected behavior / candidate shape
Please preserve the existing precedence and Workers behavior:
- A direct config value remains authoritative when explicitly supplied.
- If a
cloudflare:workers binding exists for the requested name, use it.
- Only when that Workers binding is absent, fall back to
process.env[envVar] in runtimes where process is available.
- Otherwise throw the existing missing-variable error.
Conceptually:
const workersValue = (env as Record<string, string | undefined>)[envVar];
const nodeValue =
typeof process !== 'undefined' ? process.env?.[envVar] : undefined;
const value = workersValue ?? nodeValue;
An adapter-generated env virtual module would also be reasonable if it avoids a direct Node-global reference. The key contract is that Workers bindings retain precedence and secrets remain runtime values rather than being serialized into generated source.
Documentation should explicitly distinguish Workers bindings from Node process.env and state the precedence.
Summary
The Cloudflare Images and Cloudflare Stream media-provider runtimes document
*EnvVaroptions as runtime environment-variable names, but they resolve those names only fromcloudflare:workersenv. Under a Node adapter, exportedprocess.envvalues are ignored and provider operations fail as missing credentials.This is both an implementation defect for an advertised adapter-neutral config shape and a documentation ambiguity: the error says to set an environment variable, and the provider examples say the default/custom names are read at runtime, without saying that only Workers bindings are consulted.
Minimal reproduction (Node adapter)
Export non-secret test values before starting Astro (real credentials are needed only to get past resolution):
Open the media library or invoke a provider operation. Expected: the provider resolves the exported Node environment variables and reaches the API (which may reject these test values). Actual: resolution stops locally with
Cloudflare Images: Missing CF_ACCOUNT_ID.Cloudflare Stream has the same behavior for
accountIdEnvVarandapiTokenEnvVar.Current implementation
Both runtimes import
envdirectly fromcloudflare:workersandresolveEnvValue()reads only:packages/cloudflare/src/media/images-runtime.tspackages/cloudflare/src/media/stream-runtime.tsDirect config values work because they are checked first, but inlining an API token into the generated
virtual:emdash/media-providersmodule is not an appropriate general workaround for secrets.Expected behavior / candidate shape
Please preserve the existing precedence and Workers behavior:
cloudflare:workersbinding exists for the requested name, use it.process.env[envVar]in runtimes whereprocessis available.Conceptually:
An adapter-generated env virtual module would also be reasonable if it avoids a direct Node-global reference. The key contract is that Workers bindings retain precedence and secrets remain runtime values rather than being serialized into generated source.
Documentation should explicitly distinguish Workers bindings from Node
process.envand state the precedence.