Skip to content

Commit 4877a4d

Browse files
Brooooooklynclaude
andcommitted
Stop the dev server from serving database URLs left in .void
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent b73b3ca commit 4877a4d

4 files changed

Lines changed: 28 additions & 8 deletions

File tree

‎packages/void/AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ production functions and real generated entries rather than copied code.
198198
- **Workers AI has no local emulator.** For a saved direct Cloudflare destination, `shouldBindWorkersAi()` injects the owner's remote AI binding during development and preview, using the same Cloudflare credentials as the CLI. Production builds use the normal AI binding. The root `ai.binding` takes precedence over `inference.bindings.ai`, with `AI` as the default name; `__VOID_AI_BINDING` records a custom runtime name. Managed and unsaved projects keep the Void proxy path during development. Apps without inferred AI usage must perform no AI authentication or entitlement probe.
199199
- **`mergeBindings` may remove a generated Cloudflare entry only under the name Void itself writes, gated on the matching `needs*` flag.** The Cloudflare plugin `defu`-merges the returned patch over `resolved` (arrays concatenate, `undefined` is skipped), so the only way to drop a key is to mutate `resolved` — and a mutation there is a change to the user's own config, which the CF-plugin-API rule above says takes precedence. The `send_email` serve-strip once deleted the whole field, so an app that drove Cloudflare's email API through its own `send_email: [{ name: "MAILER" }]` without ever importing `void/email` started `void dev` with `env.MAILER` undefined. It now filters out only the reserved `SEND_EMAIL` entry (`SEND_EMAIL_BINDING` from `cli/email-cloudflare.ts`, the name `void deploy --platform cloudflare` writes and the runtime reads), only when `bindings.needsEmail` — the same signal that arms the dev inbox — and drops the key only when nothing is left. `test/unit/plugin-email-config.test.ts` runs the real closure through a mocked `cloudflare()`.
200200
- **Every `/__void/inbox` route requires the dev-trigger token; the worker's capture POST carries it via a var.** Vite's host validation (`server.allowedHosts`, runs before plugin middlewares) only rejects DNS-rebinding hostnames — any IP-literal `Host` passes — and its default CORS reflects every `localhost` origin, so an unauthenticated inbox middleware hands every peer under `--host` (and every other local dev server) the raw MIME with Bcc and attachments, a DELETE, and a capture endpoint to plant messages. `plugin-email-dev-inbox.ts` therefore checks the same per-project token the cron/queue dev routes and the pages dev-CSS loopback use (`getOrCreateDevTriggerToken`, `.void/dev-trigger-token`), accepted as the `x-void-dev-trigger` header (worker, curl) or `?token=` (browser — the plugin prints the tokenized URL on `listening` and renders its own links with it; responses already send `Referrer-Policy: no-referrer`). Missing or wrong → `401` with a one-line body naming both forms and no ack header. The compiled router embeds the token as a module-local const that `runtime/email.ts` cannot import, so `mergeBindings` injects `__VOID_EMAIL_DEV_INBOX_TOKEN` next to `__VOID_EMAIL_DEV_INBOX` under the one `shouldInjectEmailDevInboxVar` gate (dev only — never in a build's `wrangler.json`); `sendEmail` sends it and, on a non-2xx, echoes the first line of the dev server's body so a missing token is diagnosable. Nothing on the worker side ever skips the inbox for a real transport because the token is absent. Because that file is now the inbox's whole credential and loopback needs no `--host`, `getOrCreateDevTriggerToken` creates it `0600` (`openSync(.., 'wx', 0o600)`) and best-effort `chmod`s a pre-existing file whose group/other bits are set, never rotating it (skipped on Windows, which has no such bits) — a default-umask `0644` file would hand every other account on a shared host the raw MIME, the DELETE, and the capture endpoint. `.void/dev-triggers.ts` (`plugin-framework-triggers.ts`) embeds the same token for Class A frameworks and is written `0600` the same way. `/__void/ping` stays open: it exposes no data. `/__void/restart` answers `403` unless the POST is same-origin (`Sec-Fetch-Site`, else `Origin`) and carries this server instance's random token in `x-void-restart` — browsers send no Fetch Metadata to plain-HTTP LAN origins (`--host`), so the token, embedded only in the `virtual:void/binding-hint` toast (which navigates with `&t=`; Vite's default CORS exposes it to localhost origins only, like the HMR token), is what stops a foreign page from opening `/__void/restarting` and riding its auto-POST. `/__void/restarting` refuses a navigation whose `Sec-Fetch-Site` is not `same-origin` or `none`, sends `Referrer-Policy: no-referrer`, and only navigates to a same-origin path from `?return=`, falling back to `/`.
201-
- **The dev server never serves the dev-trigger token.** Vite serves any root file not in `server.fs.deny`, and its CORS lets every localhost origin read the reply. `devTriggerSecretGuardPlugin` (`plugin-dev-config.ts`, registered in every serve plugin list) therefore answers 404 to `.void/dev-trigger*`, to Nitro dev bundles (`.nuxt/dev/`, `.nitro/dev/`, which inline the dispatch) and to Astro's `.astro/dev.log` (background `astro dev` output, which includes the startup hint) in any URL form, and `void:routing` refuses `virtual:void-routes` in the client environment. Do not switch to `server.fs.deny`: a plugin's list replaces Vite's defaults, and denying `.void/dev-triggers.ts` breaks the worker import.
201+
- **The dev server never serves the dev-trigger token or old database URLs.** Vite serves any root file not in `server.fs.deny`, and its CORS lets every localhost origin read the reply. `devTriggerSecretGuardPlugin` (`plugin-dev-config.ts`, registered in every serve plugin list) therefore answers 404 to `.void/dev-trigger*`, to Nitro dev bundles (`.nuxt/dev/`, `.nitro/dev/`, which inline the dispatch), to Astro's `.astro/dev.log` (background `astro dev` output, which includes the startup hint) and to `.void/drizzle.config.json` (older CLI versions left `db push` / `db studio` database URLs there; only drizzle-kit reads it) in any URL form, and `void:routing` refuses `virtual:void-routes` in the client environment. Do not switch to `server.fs.deny`: a plugin's list replaces Vite's defaults, and denying `.void/dev-triggers.ts` breaks the worker import.
202202

203203
- **Sandbox SDK 1.0**: Keep the packaged Dockerfile helper version and the managed controller’s verified helper-layer digest in sync with `@cloudflare/sandbox`. Native execution uses Durable Object scheduling with named images; managed controller protocol 2 preserves admission, metering, and cleanup, including retained protocol 1 deployments. Cancellation crosses Workers RPC as a stream, not an AbortSignal. Run the Sandbox RPC and lifecycle tests when changing this boundary.
204204

‎packages/void/src/plugin-dev-config.ts‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,13 @@ export function getOrCreateDevTriggerToken(root: string): string {
120120
// the generated `.void/dev-triggers.ts`, Nitro's dev bundle (`.nuxt/dev/` for
121121
// Nuxt, `dist/.nitro/dev/` for Analog), which inlines that module, and Astro's
122122
// `.astro/dev.log`, which records the startup hint when `astro dev` runs in the
123-
// background (`--background`, or started by a coding agent). Matched anywhere
124-
// in the decoded, lower-cased path so `/@fs/<abs>/`, `/@id/`, a base such as
125-
// `/_nuxt/`, case-insensitive file systems, dot segments, `//`, `%2e`, `\` and
126-
// Windows trailing dots/spaces all hit.
123+
// background (`--background`, or started by a coding agent). Also
124+
// `.void/drizzle.config.json`, where older CLI versions wrote database URLs.
125+
// Matched anywhere in the decoded, lower-cased path so `/@fs/<abs>/`, `/@id/`,
126+
// a base such as `/_nuxt/`, case-insensitive file systems, dot segments, `//`,
127+
// `%2e`, `\` and Windows trailing dots/spaces all hit.
127128
const DEV_TRIGGER_SECRET_PATH_RE =
128-
/(?:^|\/)\.void[. ]*\/(?:.*\/)?dev-trigger|(?:^|\/)\.(?:nuxt|nitro)[. ]*\/(?:.*\/)?dev\/|(?:^|\/)\.astro[. ]*\/(?:.*\/)?dev\.log/;
129+
/(?:^|\/)\.void[. ]*\/(?:.*\/)?(?:dev-trigger|drizzle\.config\.json)|(?:^|\/)\.(?:nuxt|nitro)[. ]*\/(?:.*\/)?dev\/|(?:^|\/)\.astro[. ]*\/(?:.*\/)?dev\.log/;
129130

130131
export function isDevTriggerSecretRequest(url: string | undefined): boolean {
131132
if (!url) {
@@ -149,8 +150,8 @@ export function isDevTriggerSecretRequest(url: string | undefined): boolean {
149150

150151
/**
151152
* Vite serves every root file that `server.fs.deny` does not list, and its default
152-
* CORS lets any localhost origin read the reply, so the files above would hand the
153-
* token to `--host` peers, other local accounts and other local dev servers.
153+
* CORS lets any localhost origin read the reply, so the files above would hand their
154+
* secrets to `--host` peers, other local accounts and other local dev servers.
154155
* `server.fs.deny` is not used: a plugin's list replaces Vite's defaults, and the
155156
* Cloudflare worker environment applies the same check when it imports
156157
* `.void/dev-triggers.ts`. The middleware is added in configureServer, so it runs

‎packages/void/test/integration/dev-trigger-secret-guard.test.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vite-plus/test';
88
import { devTriggerSecretGuardPlugin } from '../../src/plugin-dev-config.ts';
99

1010
const TOKEN = 'dev-trigger-secret-0f3c9a';
11+
const DATABASE_URL = 'postgresql://app:db-secret-7e21b4@db.example.com/app';
1112

1213
// node:http sends the path exactly as written; fetch would normalize dot segments.
1314
function get(port: number, path: string): Promise<{ status: number; body: string }> {
@@ -40,6 +41,11 @@ describe('dev server with the dev-trigger secret guard', () => {
4041
// Astro writes its background dev server output, startup hint included, here.
4142
mkdirSync(join(root, '.astro'), { recursive: true });
4243
writeFileSync(join(root, '.astro', 'dev.log'), `x-void-dev-trigger: ${TOKEN}\n`);
44+
// Older CLI versions left the database URL of `void db push` / `db studio` here.
45+
writeFileSync(
46+
join(root, '.void', 'drizzle.config.json'),
47+
JSON.stringify({ dialect: 'postgresql', dbCredentials: { url: DATABASE_URL } }),
48+
);
4349

4450
server = await createServer({
4551
root,
@@ -84,6 +90,12 @@ describe('dev server with the dev-trigger secret guard', () => {
8490
expect(response.body).not.toContain(TOKEN);
8591
});
8692

93+
it('does not serve a database URL left in .void/drizzle.config.json', async () => {
94+
const response = await get(port, '/.void/drizzle.config.json');
95+
expect(response.status).toBe(404);
96+
expect(response.body).not.toContain('db-secret-7e21b4');
97+
});
98+
8799
it('still serves other files under .void', async () => {
88100
const response = await get(port, '/.void/routes.d.ts');
89101
expect(response.status).toBe(200);

‎packages/void/test/unit/plugin-dev-config.test.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ describe('isDevTriggerSecretRequest', () => {
3131
'/.astro/dev.log',
3232
'/.ASTRO/Dev.Log?raw',
3333
'/@fs/Users/me/app/.astro/dev.log',
34+
'/.void/drizzle.config.json',
35+
'/.VOID/Drizzle.Config.json',
36+
'/%2evoid/drizzle.config.json',
37+
'/@fs/Users/me/app/.void/drizzle.config.json',
38+
'/.void/drizzle.config.json?raw',
3439
])('refuses %s', (url) => {
3540
expect(isDevTriggerSecretRequest(url)).toBe(true);
3641
});
@@ -52,6 +57,8 @@ describe('isDevTriggerSecretRequest', () => {
5257
'/dist/.nitro/types/nitro.d.ts',
5358
'/.astro/dev.json',
5459
'/.astro/content.d.ts',
60+
'/drizzle.config.json',
61+
'/.void/drizzle.config.ts',
5562
'/?q=/.void/dev-trigger-token',
5663
'/@vite/client',
5764
])('allows %s', (url) => {

0 commit comments

Comments
 (0)