diff --git a/docs/integrations/cloudflare.md b/docs/integrations/cloudflare.md
index 3acb16822..5b7a85030 100644
--- a/docs/integrations/cloudflare.md
+++ b/docs/integrations/cloudflare.md
@@ -52,6 +52,7 @@ This provides binding types and Workers runtime types.
| `KV` | KV | `c.env.KV` or `void/kv` |
| `STORAGE` | R2 | `c.env.STORAGE` or `void/storage` |
| `AI` | Workers AI | `c.env.AI` or `void/ai` |
+| `ASSETS` | Static assets | `c.env.ASSETS.fetch(request)` |
| `QUEUE_*` | Queues | `defineQueue()` or `void/queues` |
| Filename-derived | Durable Objects | A module in `durable-objects/` |
diff --git a/docs/reference/api/env.md b/docs/reference/api/env.md
index a329354eb..ba26e9f3d 100644
--- a/docs/reference/api/env.md
+++ b/docs/reference/api/env.md
@@ -45,6 +45,6 @@ Non-client keys are server values and use remote secret storage in production. `
///
```
-Declares the `Cloudflare.Env` namespace with `DB`, `KV`, `STORAGE`, `AI`, and `SANDBOX` binding types.
+Declares the `Cloudflare.Env` namespace with `DB`, `KV`, `STORAGE`, `AI`, `SANDBOX`, and `ASSETS` binding types.
For handler context variables such as `c.set()` and `c.get()`, augment [`CloudContextVariables`](./types.md#cloudcontextvariables). This is separate from `Cloudflare.Env`, which types worker bindings. Framework adapters such as `@void/vue`, `@void/react`, and `@void/svelte` also augment `CloudContextVariables` to add the `shared` key used by `useShared()`.
diff --git a/docs/reference/api/types.md b/docs/reference/api/types.md
index b20f7691d..6444104f9 100644
--- a/docs/reference/api/types.md
+++ b/docs/reference/api/types.md
@@ -24,6 +24,7 @@ interface CloudEnv extends Env {
STORAGE: R2Bucket;
AI: Ai;
SANDBOX: DurableObjectNamespace;
+ ASSETS: { fetch(request: Request): Promise };
[key: string]: unknown;
};
Variables: CloudContextVariables;
diff --git a/packages/void/src/runtime/env-public.ts b/packages/void/src/runtime/env-public.ts
index 058c88380..dcb57b1f8 100644
--- a/packages/void/src/runtime/env-public.ts
+++ b/packages/void/src/runtime/env-public.ts
@@ -400,6 +400,7 @@ declare global {
STORAGE: R2Bucket;
AI: Ai;
SANDBOX: DurableObjectNamespace;
+ ASSETS: Fetcher;
/** @internal Service binding to proxy worker (AI, revalidation, future features) */
__VOID_PROXY?: Fetcher;
/** @internal Custom proxy URL override */
diff --git a/packages/void/src/runtime/handler.ts b/packages/void/src/runtime/handler.ts
index 9804e091b..01006ffc7 100644
--- a/packages/void/src/runtime/handler.ts
+++ b/packages/void/src/runtime/handler.ts
@@ -4,6 +4,7 @@ import type { HeadDescriptor } from '../pages/head.ts';
import type { AuthSession, AuthUser } from './auth.ts';
import { base64ToBytes } from './base64.ts';
import { convertReturnValue } from './response.ts';
+import type { AssetBinding } from './routing-worker.ts';
import { runValidation, type ValidatorSlots, type HandlerInput } from './validator.ts';
/**
@@ -55,6 +56,8 @@ export interface CloudBindings extends VoidGeneratedEnvBindings {
STORAGE: R2Bucket;
AI: Ai;
SANDBOX: DurableObjectNamespace;
+ /** Node, Bun and Deno targets serve static files through a provider that takes only a `Request`. */
+ ASSETS: AssetBinding;
[key: string]: unknown;
}
diff --git a/packages/void/test/integration/env-types-typecheck.test.ts b/packages/void/test/integration/env-types-typecheck.test.ts
index 298b12b76..ddfa411b2 100644
--- a/packages/void/test/integration/env-types-typecheck.test.ts
+++ b/packages/void/test/integration/env-types-typecheck.test.ts
@@ -14,7 +14,14 @@ afterEach(() => {
});
describe('generated env types in a consumer project', () => {
- it('types schema keys on both env and handler c.env', () => {
+ // `wrangler types` declares an assets binding as `ASSETS: Fetcher`, which must merge with Void's.
+ it.each([
+ ['', ''],
+ [
+ ' next to wrangler types output',
+ 'declare global { namespace Cloudflare { interface Env { ASSETS: Fetcher } } }\n',
+ ],
+ ])('types schema keys and ASSETS on both env and handler c.env%s', (_, wranglerTypes) => {
// This nested fixture resolves the workspace's installed void package just
// as a consumer does, while keeping its source out of the playground routes.
const root = mkdtempSync(join(workspaceRoot, 'playground/kitchen-sink/.tmp-env-types-'));
@@ -33,7 +40,12 @@ describe('generated env types in a consumer project', () => {
'import { defineHandler } from "void";\n' +
'import { env } from "void/env";\n' +
'const direct: string = env.API_SECRET;\n' +
- 'export const GET = defineHandler((c) => c.text(direct + c.env.API_SECRET.toUpperCase()));\n',
+ 'export const GET = defineHandler((c) => c.text(direct + c.env.API_SECRET.toUpperCase()));\n' +
+ 'export const POST = defineHandler((c) => c.env.ASSETS.fetch(c.req.raw));\n' +
+ '// @ts-expect-error Node, Bun and Deno targets fetch assets only by Request.\n' +
+ 'export const PUT = defineHandler((c) => c.env.ASSETS.fetch("/logo.png"));\n' +
+ 'export const fetchAsset = (bindings: Cloudflare.Env) => bindings.ASSETS.fetch("https://app.test/");\n' +
+ wranglerTypes,
);
writeFileSync(
join(root, 'tsconfig.json'),