Skip to content

fix(env): keep local values out of Astro's Worker bundle - #729

Open
morinokami wants to merge 5 commits into
voidzero-dev:mainfrom
morinokami:fix/astro-private-env-inlining
Open

morinokami wants to merge 5 commits into
voidzero-dev:mainfrom
morinokami:fix/astro-private-env-inlining

Conversation

@morinokami

@morinokami morinokami commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

In an Astro app whose server code imports void/env, astro build copies local values into the Worker:

# .env
STRIPE_KEY=sk_local_value
// dist/server/chunks/env_*.mjs
function getClientEnvValues() {
  const values = Object.assign({ /* ... */ }, { STRIPE_KEY: "sk_local_value" });
  // ...
}

The value also reaches the deploy upload. It stays out of dist/client, and the Worker can't read it at runtime because void/env keeps only VITE_* keys, but .env is meant to stay local.

When a module reads import.meta.env as a whole, Astro adds every private variable whose name occurs anywhere in that module, identifiers and comments included. void/env reads it that way to pick the VITE_* keys, and its source contains names such as STRIPE_KEY (in its docs), registry and getClientEnvValues.

Fix

In Astro builds, a pre plugin replaces import.meta.env in the void/env module with an object of the build's VITE_* values, built the way Vite's define builds import.meta.env: the values from the final config.env, then each define of an import.meta.env.VITE_* key, which wins and is kept as code. Astro's plugin then finds no read in the module, so no name in it can match. Development is unchanged.

Astro's own actions/runtime modules read import.meta.env in the same way, so a variable named in their source, such as PORT, still lands in the output of any Astro app. The common secret names I checked, such as API_KEY, SECRET, TOKEN and DATABASE_URL, don't occur there.

Tests

packages/void/test/integration/astro.test.ts builds the playground with STRIPE_KEY, registry and getClientEnvValues in .env, VITE_ added to envPrefix, and define entries for import.meta.env.VITE_DEFINED and import.meta.env.VITE_OVERRIDDEN:

  • None of the three .env values is in dist. Without this fix, all three are in server/chunks/env_*.mjs.
  • A preview serves STRIPE_KEY from the Worker env, VITE_ORIGIN from the build shell, VITE_DEFINED from define, and VITE_OVERRIDDEN from define over its build-shell value. Without this fix, the preview serves the same values.

Verification

  • vp test run: 776 files pass, 1 skipped (16,003 tests pass, 29 skipped).
  • vp run check: 33/33 pass.
  • By hand on a copy of the Astro playground: build, generateWrapper, then the upload preparation, run in Miniflare with nodejs_als. The .env value is in neither the build output nor the upload. A route reads STRIPE_KEY from a binding and, with VITE_ in envPrefix, VITE_ORIGIN from the build shell.
  • By hand: .env variables named values and env still land in server/entry.mjs, through Astro's own actions/runtime modules.
  • Not run: void deploy against Cloudflare, and an installed package layout.

Maintainer polish (4d2783b)

void/env module id ──realpath──► match? ──yes──► scan for import.meta.env ──► swap for VITE_* object
                       (symlink / Windows short name no longer slips past and leaks again)
  • The module is matched by real path, so a symlinked or Windows short-name id still gets the transform.
  • import.meta.env is found with a small scan instead of a regex; it skips longer names like $import.meta.env and a.import.meta.env.
  • The plugin is now createAstroPublicEnvPlugin(), with fast unit tests (test/unit/astro-public-env.test.ts) for the scan, define values, the client skip and the path match.
  • The integration test also checks a build-shell canary: Astro treats every shell variable as private, not only .env.
  • Comments corrected: Void's env plugin adds schema defaults to the shared env object; it does not remove .env values there.
  • Merged the latest main.

Known gap, not changed here (older than this PR): in Astro builds a .env VITE_* value still reaches the Worker bundle, because the Cloudflare adapter copies .env into process.env before Void's strip step runs. Left for a follow-up.

Verification: vp run check 33/33; vp run test --base origin/main 542 files passed; the unit test and astro.test.ts pass (15/15).

🤖 Generated with Claude Code

morinokami and others added 5 commits November 6, 2026 11:44
When a module reads `import.meta.env` as a whole, Astro's env plugin adds
every local variable whose name occurs anywhere in that module,
identifiers and comments included. `void/env` does that read, so
`astro build` copied values such as STRIPE_KEY from `.env` or the build
shell into the server output and the deploy upload.

In Astro builds, Void now replaces that read in `void/env` with the
object Vite's define would give it, limited to the public `VITE_*` keys,
before Astro's plugin sees the module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alues

- Match the void/env module by real path, so a symlinked or Windows short-name id
  still gets the transform instead of silently leaking again.
- Find `import.meta.env` with a small scan instead of a regex; it also skips
  longer names such as `$import.meta.env` and `a.import.meta.env`.
- Move the plugin into createAstroPublicEnvPlugin() with fast unit tests for
  the scan, define values, the client skip and the path match.
- The integration test also checks a build-shell canary: Astro treats every
  shell variable as private too.
- Fix comments: Astro copies values from .env and the build shell, and Void's
  env plugin adds schema defaults to the shared env object (it does not
  remove .env values there).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Brooooooklyn

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-06T07:40:52.890896Z 4d2783b Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: 4d2783b1a4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@cpojer
cpojer requested a review from Brooooooklyn November 6, 2026 10:06
cpojer pushed a commit that referenced this pull request Nov 6, 2026
…ndles (#732)

In a SvelteKit app on D1 with `void` installed from npm, `void deploy
--platform cloudflare` puts the `VITE_*` values from the local `.env`
into the uploaded Worker:

```sh
# .env
VITE_API_ORIGIN=http://localhost:8787
VITE_LOCAL_NOTE=only-on-my-laptop
```

```js
// uploaded Worker (minified)
function m() {
  return Object.fromEntries(Object.entries({
    BASE_URL: "/", DEV: !1, MODE: "production", PROD: !0, SSR: !1,
    VITE_API_ORIGIN: "http://localhost:8787",
    VITE_LOCAL_NOTE: "only-on-my-laptop"
  }).filter(([e]) => e.startsWith("VITE_")));
}
```

With `VITE_API_ORIGIN: url().default('https://api.example.com')` in
`env.ts` and no value in the build shell, the deployed Worker answers
`env.VITE_API_ORIGIN` with `http://localhost:8787`, while the browser
bundle has `https://api.example.com`. `VITE_LOCAL_NOTE` is not declared
in `env.ts` and is readable too. The docs say builds ignore client
values from `.env`.

Void's env plugin removes `.env` client values during Kit's build, but
Kit's server build leaves an installed `void/env` as an import, so that
module is not compiled there. Before the upload,
`prepareBundledBuildOutput` bundles the adapter's Worker once more with
Vite, with the project root as `root` and no `envDir`. Vite reads `.env`
there and replaces `import.meta.env` in `void/env` with its values.
Managed deploys bundle the same entry from `.void/bundle`, which has no
`.env`.

## Fix

The bundle sets `envDir: false`. Vite reads no env files there; values
from the build shell still reach `import.meta.env`.

## Tests

`packages/void/test/unit/cf-build-output.test.ts` bundles a framework
Worker that imports a package reading `import.meta.env` as a whole, with
`VITE_ORIGIN` in `.env` and `VITE_SHELL_ORIGIN` in the environment. The
bundle contains the shell value and not the `.env` value. Without this
fix, it contains both.

The SvelteKit playground does not show the problem: it links `void` from
the workspace, so Kit bundles it in its own build and the second bundle
finds no `import.meta.env` to replace.

## Verification

- `vp test run`: 781 files pass, 1 skipped (16,159 tests pass, 29
skipped).
- `vp run check`: 33/33 pass.
- By hand on a copy of the SvelteKit playground, with `void` copied into
`node_modules` as a real directory: build, then
`prepareCfFrameworkBuildOutput` and `bundleEntry`, each run in
Miniflare. Without this fix, the native Worker returns the `.env` values
from an API route and renders `http://localhost:8787` in a page. With
it, the native and managed Workers return the same values: none without
a build-shell value, and the build-shell value when `VITE_API_ORIGIN` is
set.
- Not run: `void deploy` against Cloudflare, and Nuxt, Analog and Astro,
which go through the same bundle.

Related: #729 keeps local values out of Astro's Worker through a
different path (Astro's env plugin). The two changes touch different
files and don't depend on each other.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants