Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .bumpy/cascade-resolved-env-cloudflare-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@varlock/astro-integration": minor
"@varlock/cloudflare-integration": minor
---

Cascaded from @varlock/vite-integration: no longer defaults ssrInjectMode to resolved-env for the Cloudflare adapter (redundant with the native runtime binding loader)
5 changes: 5 additions & 0 deletions .bumpy/guard-resolved-env-cloudflare-vercel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@varlock/vite-integration": minor
---

Warn when deploying to Vercel with resolved env injection and no encryption enabled; throw if resolved-env is combined with a detected Cloudflare target (redundant with native runtime binding injection)
5 changes: 5 additions & 0 deletions .bumpy/nextjs-vercel-encryption-warning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@varlock/nextjs-integration": patch
---

Warn when deploying to Vercel without encryption enabled for the injected env blob
10 changes: 8 additions & 2 deletions packages/integrations/astro/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function varlockAstroIntegration(
...integrationOptions,
};

if (['@astrojs/netlify', '@astrojs/vercel', '@astrojs/cloudflare'].includes(adapterName || '')) {
if (['@astrojs/netlify', '@astrojs/vercel'].includes(adapterName || '')) {
ssrInjectMode ??= 'resolved-env';
} else if (adapterName === '@astrojs/node') {
ssrInjectMode ??= 'auto-load';
Expand All @@ -36,7 +36,12 @@ function varlockAstroIntegration(
if (adapterName === '@astrojs/cloudflare') {
// @astrojs/cloudflare runs SSR in workerd via @cloudflare/vite-plugin.
// Inject varlock init into the CF worker entry and load env from bindings
// at runtime in production (via varlock-wrangler deploy).
// at runtime in production (via varlock-wrangler deploy). We deliberately
// do NOT default ssrInjectMode to 'resolved-env' here (leaving it
// 'init-only' unless the user overrides it): the runtime loader below
// already hydrates env from Cloudflare bindings, so baking the resolved
// env into the bundle as well would be redundant and ship secrets in
// the worker artifact that are never read.
let cloudflareSsrEntryCode: string;
let logVarlockEnvInjectionNotice: () => void;
try {
Expand All @@ -62,6 +67,7 @@ function varlockAstroIntegration(
'\0virtual:cloudflare/worker-entry',
],
ssrEntryCode: vitePluginOptions.ssrEntryCode ?? [cloudflareSsrEntryCode],
isCloudflareTarget: true,
};

// Print a varlock notice at server start, in place of wrangler's
Expand Down
1 change: 1 addition & 0 deletions packages/integrations/cloudflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ export function varlockCloudflareVitePlugin(
ssrEdgeRuntime: true,
ssrEntryModuleIds: ['\0virtual:cloudflare/worker-entry'],
ssrEntryCode: [CLOUDFLARE_SSR_ENTRY_CODE],
isCloudflareTarget: true,
integrationTelemetry: {
name: __VARLOCK_INTEGRATION_NAME__,
version: __VARLOCK_INTEGRATION_VERSION__,
Expand Down
1 change: 1 addition & 0 deletions packages/integrations/cloudflare/src/sveltekit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function varlockSvelteKitCloudflarePlugin(): Array<any> {
varlockVitePlugin({
ssrEdgeRuntime: true,
ssrEntryCode: [CLOUDFLARE_SSR_ENTRY_CODE],
isCloudflareTarget: true,
}),
];
}
14 changes: 14 additions & 0 deletions packages/integrations/nextjs/src/turbopack-runtime-inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,20 @@ export function injectVarlockInitIntoTurbopackRuntime(nextDirPath: string) {
// Mark as done so we don't retry
injectedTurbopackRuntime = true;

// Vercel has no native runtime-binding mechanism, so baking the resolved env
// into the build is the correct approach there — but plaintext means secrets
// sit as JSON in the build artifact. Nudge (don't block) users who haven't
// opted into `@encryptInjectedEnv`. Placed after the guard above so it only
// fires once (this function is called repeatedly until the runtime files
// are found, per the early return above).
if (process.env.VERCEL === '1' && !encryptionRequired) {
// eslint-disable-next-line no-console
console.warn(
'[varlock] ⚠️ Deploying to Vercel ships your resolved env as plaintext JSON in the build artifact. '
+ 'Consider enabling `@encryptInjectedEnv` — see https://varlock.dev/guides/encrypted-deployments/',
);
}

// Load both init bundles — server (full, node:zlib/node:http) and edge (no node builtins)
const initServerSrc = fs.readFileSync(require.resolve('varlock/init-server'), 'utf8');
const initEdgeSrc = fs.readFileSync(require.resolve('varlock/init-edge'), 'utf8');
Expand Down
15 changes: 15 additions & 0 deletions packages/integrations/nextjs/src/webpack-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import type { NextConfig } from 'next';

const WEBPACK_PLUGIN_NAME = 'VarlockNextWebpackPlugin';

// one-time guard for the Vercel unencrypted resolved-env warning
let vercelUnencryptedWarningLogged = false;

// We use a proxy object for the static replacements that we pass to webpack.DefinePlugin
// because this plugin/webpack code is not invoked again when env files change
// instead next is direcly messing with the existing plugins for their own changes
Expand Down Expand Up @@ -183,6 +186,18 @@ export function createWebpackConfigFn(
+ 'See https://varlock.dev/guides/encrypted-deployments/ for details.',
);
}
// Vercel has no native runtime-binding mechanism, so baking the resolved
// env into the build is the correct approach there — but plaintext means
// secrets sit as JSON in the build artifact. Nudge (don't block) users
// who haven't opted into `@encryptInjectedEnv`.
if (process.env.VERCEL === '1' && !encryptionRequired && !dev && !vercelUnencryptedWarningLogged) {
vercelUnencryptedWarningLogged = true;
// eslint-disable-next-line no-console
console.warn(
'[varlock] ⚠️ Deploying to Vercel ships your resolved env as plaintext JSON in the build artifact. '
+ 'Consider enabling `@encryptInjectedEnv` — see https://varlock.dev/guides/encrypted-deployments/',
);
}
let envPayload = rawEnv;
if (rawEnv && encryptionKey) {
envPayload = encryptEnvBlobSync(rawEnv, encryptionKey);
Expand Down
34 changes: 34 additions & 0 deletions packages/integrations/vite/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ let lastErrorAt = 0;
let configHookCalled = false;
// one-time guard for the SvelteKit+Cloudflare auto-detection notice
let cfDetectNoticeLogged = false;
// one-time guard for the Vercel unencrypted resolved-env warning
let vercelUnencryptedWarningLogged = false;
let staticReplacements: Record<string, any> = {};
let replacerFn: ReturnType<typeof createReplacerTransformFn>;

Expand Down Expand Up @@ -153,6 +155,13 @@ export interface VarlockVitePluginOptions {
ssrEntryModuleIds?: Array<string>,
/** override integration identity for CLI telemetry (used by composed integrations like Astro) */
integrationTelemetry?: { name: string, version: string },
/**
* set by composed integrations (e.g. `@varlock/cloudflare-integration`, the
* Astro Cloudflare adapter branch) when the CF runtime env loader has been
* injected via `ssrEntryCode`. Used to reject `ssrInjectMode: 'resolved-env'`
* as redundant — the loader already hydrates env from Cloudflare bindings.
*/
isCloudflareTarget?: boolean,
}

// Return type is `any` instead of `Plugin` to avoid symlink type conflicts.
Expand All @@ -179,6 +188,7 @@ export function varlockVitePlugin(
// when the virtual init module is loaded — always after `configResolved`.
let resolvedSsrEdgeRuntime = vitePluginOptions?.ssrEdgeRuntime ?? false;
let resolvedSsrEntryCode = vitePluginOptions?.ssrEntryCode;
let resolvedIsCloudflareTarget = vitePluginOptions?.isCloudflareTarget ?? false;

// Build the virtual init module content once. This module is imported
// by SSR entry points and evaluates before any user code because it
Expand All @@ -199,6 +209,29 @@ export function varlockVitePlugin(
lines.push("import 'varlock/auto-load';");
} else {
if (ssrInjectMode === 'resolved-env') {
if (resolvedIsCloudflareTarget) {
throw new Error(
"[varlock] ssrInjectMode: 'resolved-env' is redundant on Cloudflare Workers and ships resolved "
+ '(possibly sensitive) values into the worker bundle unnecessarily. Cloudflare deploys get their '
+ 'env injected at runtime from bindings via `varlock-wrangler` — remove the `ssrInjectMode` override '
+ "(or set it to 'init-only') and let the Cloudflare integration handle it.\n"
+ 'See https://varlock.dev/integrations/cloudflare/ for details.',
);
}
// Vercel has no native runtime-binding mechanism like Cloudflare's, so
// `resolved-env` is the correct approach there — but plaintext means
// secrets sit as JSON in the build artifact. Nudge (don't block) users
// who haven't opted into `@encryptInjectedEnv`.
if (process.env.VERCEL === '1' && !encryptionRequired && !isDevCommand) {
if (!vercelUnencryptedWarningLogged) {
vercelUnencryptedWarningLogged = true;
console.warn(
"\x1b[33m[varlock] ⚠️ ssrInjectMode: 'resolved-env' on Vercel ships your resolved env as plaintext JSON "
+ 'in the build artifact. Consider enabling `@encryptInjectedEnv` — '
+ 'see https://varlock.dev/guides/encrypted-deployments/\x1b[0m',
);
}
}
if (encryptionRequired && !encryptionKey) {
if (isDevCommand) {
// auto-generate a temporary key for local dev
Expand Down Expand Up @@ -296,6 +329,7 @@ export function varlockVitePlugin(
const { CLOUDFLARE_SSR_ENTRY_CODE } = await import(cfEntryCodeModule) as { CLOUDFLARE_SSR_ENTRY_CODE: string };
resolvedSsrEntryCode = [CLOUDFLARE_SSR_ENTRY_CODE];
resolvedSsrEdgeRuntime = true;
resolvedIsCloudflareTarget = true;
debug('detected SvelteKit + Cloudflare adapter — injecting edge env loader');
// Surface the auto-detection so it isn't silent magic in the build output.
if (!cfDetectNoticeLogged) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ description: Encrypt the env blob in your build output so secrets are never stor
import { Steps, Tabs, TabItem } from '@astrojs/starlight/components';
import ExecCommandWidget from '@/components/ExecCommandWidget.astro';

When deploying SSR applications to **serverless platforms** (like Vercel, Netlify, or Cloudflare Workers), varlock injects the fully resolved env data into your server-side build output so it's available at runtime without needing the CLI or filesystem access. This is necessary because serverless environments don't give you control over how the application boots.
When deploying SSR applications to **serverless platforms** (like Vercel or Netlify) that don't give you control over how the application boots, varlock injects the fully resolved env data into your server-side build output (`ssrInjectMode: 'resolved-env'`) so it's available at runtime without needing the CLI or filesystem access. Cloudflare Workers is different: `varlock-wrangler deploy` uploads the blob as a runtime secret binding instead of baking it into the build output, so it never appears in a build artifact or sourcemap in the first place. See the [Cloudflare Workers](#cloudflare-workers) section below.

By default, this blob is **plaintext JSON**. Since it only appears in server-side code, it's generally safe: your secrets are not exposed to the client. However, encrypting the blob adds protection against secrets leaking via **sourcemaps** that may be uploaded to error tracking services.
By default, this blob is **plaintext JSON**. Since it only appears in server-side code, it's generally safe: your secrets are not exposed to the client. However, encrypting the blob adds protection against secrets leaking via **sourcemaps** that may be uploaded to error tracking services. If you deploy to Vercel with `ssrInjectMode: 'resolved-env'` and haven't enabled encryption, varlock warns you about this at build time.

## Enabling encryption

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,15 @@ varlockVitePlugin({ ssrInjectMode: 'auto-load' })

- `init-only` - injects varlock initialization code, but does not load the env vars. You must still boot your app via `varlock run` in this mode.
- `auto-load` - injects `import 'varlock/auto-load';` to load your resolved env via the varlock CLI
- `resolved-env` - injects the fully resolved env data into your built code. This is useful in environments like Vercel/Cloudflare/etc where you have no control over your build command, and limited access to use CLI commands or the filesystem. See the [encrypting the env blob](#encrypting-the-env-blob) section for more information.
- `resolved-env` - injects the fully resolved env data into your built code. This is useful in environments like Vercel where you have no control over your build command, and limited access to use CLI commands or the filesystem. See the [encrypting the env blob](#encrypting-the-env-blob) section for more information.

**If not specified, we will attempt to infer the correct mode based on the presence of other vite plugins and environment variables, which give us hints about how your application will be run.**
Otherwise defaulting to `init-only`.

:::caution[Don't use `resolved-env` on Cloudflare]
On Cloudflare Workers, env is injected at runtime from bindings via [`varlock-wrangler`](/integrations/cloudflare/) — not baked into the bundle. If Cloudflare is detected (via `@varlock/cloudflare-integration` or SvelteKit's `@sveltejs/adapter-cloudflare`) and `ssrInjectMode` is explicitly set to `'resolved-env'`, the plugin throws at build time rather than silently shipping resolved (and possibly sensitive) values into the worker artifact for no reason.
:::


## Accessing environment variables

Expand Down
Loading