Skip to content
Merged
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/varlock-config-var-aliases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
varlock: patch
"@varlock/nextjs-integration": patch
---

Accept `VARLOCK_ENV_KEY` (and the other `_VARLOCK_*` config vars without the leading underscore) for platforms like AWS Lambda that reject env var names starting with `_`
3 changes: 2 additions & 1 deletion framework-tests/frameworks/nextjs/nextjs-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,8 @@ export function defineNextjsTests(versionOrCanary: number | 'canary', testDir: s
[
'env -i PATH=/usr/bin:/bin NODE_ENV=production',
`HOSTNAME=127.0.0.1 PORT=${standalonePort}`,
'_VARLOCK_ENV_KEY=$_VARLOCK_ENV_KEY',
// the underscore-free alias, as on AWS Lambda (which rejects names starting with `_`)
'VARLOCK_ENV_KEY=$_VARLOCK_ENV_KEY',
'RUNTIME_BOOT_VAR=runtime-boot-value INSTANCE_ID=i-123',
'"$NODE_BIN" server.js',
].join(' '),
Expand Down
1 change: 1 addition & 0 deletions packages/integrations/nextjs/src/next-env-compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,7 @@ function bootFromFrozenBuildEnv(frozenEnv: string): LoadedEnvConfig {
'',
'🚨 [varlock] this build\'s env is frozen and encrypted, but _VARLOCK_ENV_KEY is not set 🚨',
'Set _VARLOCK_ENV_KEY at runtime to the key that was set when building.',
'(On platforms that reject env var names starting with `_`, like AWS Lambda, set VARLOCK_ENV_KEY instead.)',
'',
].join('\n'));
process.exit(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ No setup needed. `varlock-wrangler deploy` **auto-generates a key** and uploads

You must set `_VARLOCK_ENV_KEY` as an environment variable on your platform. The key must be available at both **build time** (for encryption) and **runtime** (for decryption). It must be a real environment variable in both places: it's a reserved `_VARLOCK_*` name that varlock never populates from `.env` files, and it can't be baked in via vite's `define` (the value has to come from the actual process environment at runtime, not a string substituted at build time).

Some platforms reject environment variable names that start with `_` (AWS Lambda requires a leading letter). On those, set the key as `VARLOCK_ENV_KEY` instead. varlock reads it as `_VARLOCK_ENV_KEY` when that is not set. The same works for the other [`_VARLOCK_*` configuration variables](/reference/reserved-variables/#configuration-variables-_varlock_).

<Steps>

1. **Generate and set the key**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ services:

Your app boots exactly as it does with a file: `import 'varlock/auto-load'` and read `ENV`.

:::caution[Not for AWS Lambda]
This route does not work on AWS Lambda. Lambda rejects env var names that start with `_`, so `__VARLOCK_ENV` cannot be set, and it caps all of a function's env vars at 4 KB combined, which an encrypted payload usually exceeds. Ship a frozen file in the deploy package instead (or [freeze into the build output](#freezing-in-build-output)), and set `VARLOCK_USE_FROZEN_ENV=1` and `VARLOCK_ENV_KEY`. The `_VARLOCK_*` configuration variables can all be set without the leading underscore; see [Reserved variables](/reference/reserved-variables/#configuration-variables-_varlock_).
:::

The summary goes to stderr in this mode, so stdout carries the payload and nothing else. Everything else is identical to writing a file: the same resolution, the same encryption, the same refusal to emit a payload built from a failed resolution.

### What you give up
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ The decision is made at build time and never at boot, so a value never has two p

How a frozen build behaves:

- **It must be encrypted.** Generate a key with `varlock generate-key` and set `_VARLOCK_ENV_KEY` both in the build environment and at runtime. See the [encrypted deployments guide](/guides/encrypted-deployments/). A build without the key fails. Set `_VARLOCK_ALLOW_PLAINTEXT_FROZEN_ENV=1` only if you accept every resolved secret sitting unencrypted in your build output.
- **It must be encrypted.** Generate a key with `varlock generate-key` and set `_VARLOCK_ENV_KEY` both in the build environment and at runtime. On platforms that reject env var names starting with `_` (such as AWS Lambda), set it as `VARLOCK_ENV_KEY` instead. See the [encrypted deployments guide](/guides/encrypted-deployments/). A build without the key fails. Set `_VARLOCK_ALLOW_PLAINTEXT_FROZEN_ENV=1` only if you accept every resolved secret sitting unencrypted in your build output.
- **Frozen values win over the environment at boot.** If the environment sets a different value for a frozen key, varlock uses the frozen value and prints a warning. A `__VARLOCK_ENV` blob in the environment (for example from `varlock run`) is ignored, also with a warning.
- **Values that must vary per instance** go in the schema as [`@dynamic=boot`](/guides/frozen-env/#keys-supplied-at-boot). The frozen value becomes a default, and a value set in the environment at boot replaces it after being checked against the item's type.
- Rotating a secret takes effect on your next build, not on the next restart.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ Config items whose key starts with `_VARLOCK_` are reserved for varlock. They ar

Set these in your shell, CI, or deploy environment to configure varlock's behavior.

:::note[Platforms that reject a leading underscore]
Some platforms don't allow environment variable names that start with `_`. AWS Lambda, for example, requires names to start with a letter. On those, set any of these variables without the leading underscore instead: `VARLOCK_ENV_KEY` for `_VARLOCK_ENV_KEY`, `VARLOCK_USE_FROZEN_ENV` for `_VARLOCK_USE_FROZEN_ENV`, and so on. If both forms are set, the `_VARLOCK_*` one wins. This applies only to the variables listed in this section, not to `__VARLOCK_*` internals. These alias names are reserved the same way as the `_VARLOCK_` prefix, so don't define items with them in your schema.
:::

### `_VARLOCK_ENV_KEY`

Encryption key used to decrypt the injected env blob at runtime. Typically set in deploy environments. See [Encrypted deployments](/guides/encrypted-deployments/).
Expand Down
3 changes: 3 additions & 0 deletions packages/varlock/src/auto-load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { createDebug } from './lib/debug';
import { isVarlockCliChild } from './lib/cli-child-marker';

import { initVarlockEnv, getPreInjectionProcessEnv } from './runtime/env';
import { applyVarlockConfigEnvAliases } from './runtime/lib/config-env-aliases';
import { patchGlobalConsole } from './runtime/patch-console';
import { patchProcessStreams } from './runtime/patch-process-streams';
import { patchGlobalServerResponse } from './runtime/patch-server-response';
Expand Down Expand Up @@ -39,6 +40,8 @@ function getPartialResolvedEnv(err: unknown): Record<string, unknown> {
}

function autoLoad() {
// `VARLOCK_ENV_KEY` etc. for platforms that reject a leading underscore (see config-env-aliases)
applyVarlockConfigEnvAliases();
// `@internal` keys stripped from a reused blob - their ambient env values are scrubbed
// after init (see the bottom of this module)
let strippedInternalKeys: Array<string> = [];
Expand Down
4 changes: 4 additions & 0 deletions packages/varlock/src/cli/helpers/clear-cli-child-marker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CLI_CHILD_MARKER } from '../../lib/cli-child-marker';
import { applyVarlockConfigEnvAliases } from '../../runtime/lib/config-env-aliases';

/**
* Side-effect module: must be the FIRST import of the CLI entrypoint.
Expand All @@ -20,3 +21,6 @@ export function clearCliChildMarker() {
}

clearCliChildMarker();
// also accept `VARLOCK_ENV_KEY` etc. for `_VARLOCK_*` config vars (see config-env-aliases),
// before any command reads them or snapshots process.env for a child
applyVarlockConfigEnvAliases();
6 changes: 4 additions & 2 deletions packages/varlock/src/env-graph/lib/env-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,10 +639,12 @@ export class EnvGraph {
for (const itemKey of Object.keys(source.configItemDefs)) {
if (isVarlockReservedKey(itemKey)) {
source._errors.push(new SchemaError(
`"${itemKey}" uses varlock's reserved _VARLOCK_ prefix`,
itemKey.startsWith('_')
? `"${itemKey}" uses varlock's reserved _VARLOCK_ prefix`
: `"${itemKey}" is reserved: it is an alias for varlock's _${itemKey} setting`,
{
isWarning: true,
tip: 'Keys starting with _VARLOCK_ are reserved for configuring varlock itself and are excluded from the injected env and generated types. Rename this item unless that exclusion is intended.',
tip: 'Keys starting with _VARLOCK_ (and their underscore-free aliases like VARLOCK_ENV_KEY) are reserved for configuring varlock itself and are excluded from the injected env and generated types. Rename this item unless that exclusion is intended.',
},
));
}
Expand Down
17 changes: 13 additions & 4 deletions packages/varlock/src/env-graph/lib/reserved-vars.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { VARLOCK_CONFIG_ENV_VAR_ALIAS_NAMES } from '../../runtime/lib/config-env-aliases';

/**
* Single source of truth for varlock's reserved environment variables.
*
Expand Down Expand Up @@ -29,6 +31,10 @@ export const VARLOCK_CONFIG_ENV_VARS: Array<ReservedVarInfo> = [
name: '_VARLOCK_ENV_KEY',
description: 'Encryption key used to decrypt the injected env blob at runtime. Typically set in deploy environments.',
},
{
name: '_VARLOCK_ALLOW_PLAINTEXT_FROZEN_ENV',
description: 'Set to `1` in the build environment to let a Next.js build freeze the env into its output without encrypting it. Without it, a frozen build fails when `_VARLOCK_ENV_KEY` is not set.',
},
{
name: '_VARLOCK_CACHE_KEY',
description: 'Encryption key for the on-disk resolved-value cache. When set (e.g. as a CI secret), it enables the disk cache in environments without OS keychain access.',
Expand Down Expand Up @@ -98,10 +104,13 @@ export const VARLOCK_INTERNAL_ENV_VARS: Array<ReservedVarInfo> = [
];

/**
* Check if a config item key is reserved for varlock infrastructure. These keys are never
* exposed via the ENV proxy, serialized into the injected env blob, included in generated
* types, or recorded as override provenance — even if a user defines one in their schema.
* Check if a config item key is reserved for varlock infrastructure: any `_VARLOCK_*` key, plus
* the underscore-free aliases of the config vars (`VARLOCK_ENV_KEY`, ...; see config-env-aliases),
* which configure varlock the same way. Computed builtins like `VARLOCK_ENV` are not reserved.
* These keys are never exposed via the ENV proxy, serialized into the injected env blob,
* included in generated types, or recorded as override provenance, even if a user defines one
* in their schema.
*/
export function isVarlockReservedKey(key: string): boolean {
return key.startsWith(VARLOCK_RESERVED_KEY_PREFIX);
return key.startsWith(VARLOCK_RESERVED_KEY_PREFIX) || VARLOCK_CONFIG_ENV_VAR_ALIAS_NAMES.has(key);
}
31 changes: 31 additions & 0 deletions packages/varlock/src/env-graph/test/varlock-reserved-keys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ describe('isVarlockReservedKey', () => {
expect(isVarlockReservedKey(`${VARLOCK_RESERVED_KEY_PREFIX}ANYTHING_NEW`)).toBe(true);
});

it('matches the underscore-free aliases of config vars', () => {
expect(isVarlockReservedKey('VARLOCK_ENV_KEY')).toBe(true);
expect(isVarlockReservedKey('VARLOCK_CACHE_KEY')).toBe(true);
expect(isVarlockReservedKey('VARLOCK_USE_FROZEN_ENV')).toBe(true);
});

it('does not reserve computed builtins', () => {
for (const name of ['VARLOCK_ENV', 'VARLOCK_IS_CI', 'VARLOCK_BRANCH', 'VARLOCK_PLATFORM']) {
expect(isVarlockReservedKey(name), name).toBe(false);
}
});

it('does not match normal config keys', () => {
expect(isVarlockReservedKey('FOO')).toBe(false);
expect(isVarlockReservedKey('VARLOCK_ENV')).toBe(false); // no leading underscore
Expand Down Expand Up @@ -69,6 +81,25 @@ describe('serialized graph excludes reserved _VARLOCK_ keys', () => {
expect(Object.keys(config)).not.toContain('_VARLOCK_REDACT_STDOUT');
});

it('drops schema-defined aliases too, even with an ambient key value', async () => {
const g = await buildGraph(
outdent`
# @defaultSensitive=false @defaultRequired=false
# ---
FOO=bar
VARLOCK_ENV_KEY=
VARLOCK_CACHE_KEY=
_VARLOCK_ENV_KEY=
`,
{ VARLOCK_ENV_KEY: 'ambient-key', VARLOCK_CACHE_KEY: 'ambient-cache-key' },
);
const serialized = g.getSerializedGraph();
expect(Object.keys(serialized.config)).toEqual(['FOO']);
expect(JSON.stringify(serialized)).not.toContain('ambient-key');
expect(JSON.stringify(serialized)).not.toContain('ambient-cache-key');
expect(serialized.overrideKeys ?? []).not.toContain('VARLOCK_ENV_KEY');
});

it('only records schema config keys in override provenance (not arbitrary env vars or reserved keys)', async () => {
const g = await buildGraph(
outdent`
Expand Down
7 changes: 7 additions & 0 deletions packages/varlock/src/runtime/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { isBrowser } from '../lib/detect-runtime';
import { debug } from './lib/debug';
import { isStreamRedactionPatched } from './lib/stream-patch-key';
import { envValueMatchesBlobItem } from '../lib/injected-env-provenance';
import { applyVarlockConfigEnvAliases } from './lib/config-env-aliases';

export { applyVarlockConfigEnvAliases };

// TODO: would like to move all of the redaction utils out of this file
// but its complicated since it is imported by code that may be run in the backend and frontend
Expand Down Expand Up @@ -635,6 +638,10 @@ type EnvState = {

const processExists = !!globalThis.process;

// `VARLOCK_ENV_KEY` etc. stand in for `_VARLOCK_*` config vars on platforms that reject a
// leading underscore (AWS Lambda). Applied before the snapshot below so it sees them too.
applyVarlockConfigEnvAliases();

const ENV_STATE_KEY = '__varlockEnvState';
function getEnvState(): EnvState {
if (!(globalThis as any)[ENV_STATE_KEY]) {
Expand Down
7 changes: 5 additions & 2 deletions packages/varlock/src/runtime/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*/

import { initVarlockEnv } from './env';
import { applyVarlockConfigEnvAliases, getVarlockConfigEnvVar } from './lib/config-env-aliases';
import { patchGlobalConsole } from './patch-console';
import { patchProcessStreams } from './patch-process-streams';
import { patchGlobalServerResponse } from './patch-server-response';
Expand Down Expand Up @@ -139,10 +140,12 @@ function storeDecryptedEnv(decrypted: string, source: 'global' | 'process') {
* as `globalThis.__varlockEnvReady`, and Next.js edge handler invocation is gated on it.
*/
export function initVarlockRuntime() {
applyVarlockConfigEnvAliases();
Comment thread
pullfrog[bot] marked this conversation as resolved.
Resolved
const encrypted = getEncryptedEnvBlob();
if (encrypted) {
const key = (globalThis as any).process?.env?._VARLOCK_ENV_KEY;
if (!key) throw new Error('[varlock] env blob is encrypted but _VARLOCK_ENV_KEY is not set');
// read directly (not just via the alias copy above), which is a no-op on a read-only process.env
const key = getVarlockConfigEnvVar('_VARLOCK_ENV_KEY');
if (!key) throw new Error('[varlock] env blob is encrypted but _VARLOCK_ENV_KEY (or VARLOCK_ENV_KEY) is not set');
try {
storeDecryptedEnv(decryptEnvBlobSync(encrypted.blob, key), encrypted.source);
initNow();
Expand Down
62 changes: 62 additions & 0 deletions packages/varlock/src/runtime/lib/config-env-aliases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Some platforms reject env var names that start with an underscore (AWS Lambda requires
* `[a-zA-Z][a-zA-Z0-9_]+`), so the `_VARLOCK_*` config vars cannot be set there directly.
* Each one can also be set without the leading underscore (`VARLOCK_ENV_KEY` for
* `_VARLOCK_ENV_KEY`); the underscore form wins when both are set.
*
* Kept as a plain list of names (rather than importing `VARLOCK_CONFIG_ENV_VARS`) because this
* runs inside the edge-safe init bundles. A test keeps it in sync with reserved-vars.ts.
*/
export const VARLOCK_CONFIG_ENV_VAR_NAMES = [
'_VARLOCK_ENV_KEY',
'_VARLOCK_ALLOW_PLAINTEXT_FROZEN_ENV',
'_VARLOCK_CACHE_KEY',
'_VARLOCK_REDACT_STDOUT',
'_VARLOCK_FILTER',
'_VARLOCK_THROW_ON_LOAD_ERROR',
'_VARLOCK_DYNAMIC_BUILD_ACCESS_MODE',
'_VARLOCK_USE_INJECTED_ENV',
'_VARLOCK_ALLOW_EMPTY_CONFIG',
'_VARLOCK_USE_FROZEN_ENV',
] as const;

/** The underscore-free alias names themselves (`VARLOCK_ENV_KEY`, ...) */
export const VARLOCK_CONFIG_ENV_VAR_ALIAS_NAMES: ReadonlySet<string> = new Set(
VARLOCK_CONFIG_ENV_VAR_NAMES.map((name) => name.slice(1)),
);

type ConfigEnvVarName = typeof VARLOCK_CONFIG_ENV_VAR_NAMES[number];

/**
* Read a `_VARLOCK_*` config var, falling back to its underscore-free alias. Use where the
* value matters even if process.env is read-only (so `applyVarlockConfigEnvAliases` could
* not copy the alias over).
*/
export function getVarlockConfigEnvVar(
name: ConfigEnvVarName,
env: Record<string, string | undefined> | undefined = (globalThis as any).process?.env,
): string | undefined {
return env?.[name] || env?.[name.slice(1)] || undefined;
}

/**
* Copy each underscore-free alias (`VARLOCK_ENV_KEY`) onto its `_VARLOCK_*` name when that
* name is not already set, so every reader of the `_VARLOCK_*` name sees it.
* Mutates `env` (process.env by default). Safe to call more than once, and a no-op where
* process.env is missing or read-only (some edge runtimes).
*/
export function applyVarlockConfigEnvAliases(
env: Record<string, string | undefined> | undefined = (globalThis as any).process?.env,
) {
if (!env) return;
for (const name of VARLOCK_CONFIG_ENV_VAR_NAMES) {
if (env[name] !== undefined && env[name] !== '') continue;
const aliasValue = env[name.slice(1)];
Comment thread
pullfrog[bot] marked this conversation as resolved.
Resolved
if (aliasValue === undefined || aliasValue === '') continue;
try {
env[name] = aliasValue;
} catch {
// read-only process.env
}
}
}
Loading
Loading