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
9 changes: 9 additions & 0 deletions .bumpy/encrypted-env-blob.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"varlock": minor
"@varlock/nextjs-integration": patch
"@varlock/vite-integration": patch
"@varlock/cloudflare-integration": patch
"@varlock/expo-integration": patch
---

add @encryptInjectedEnv and @disableProcessEnvInjection root decorators for encrypted deployments
141 changes: 128 additions & 13 deletions bun.lock

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions framework-tests/frameworks/cloudflare/cloudflare-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Shared Cloudflare Workers test definitions, parameterized by Vite version.
Covers basic worker dev, leak detection, build + preview, and large env chunking.
*/
import { randomBytes } from 'node:crypto';
import {
describe, beforeAll, afterAll,
} from 'vitest';
Expand Down Expand Up @@ -163,6 +164,32 @@ export function defineCloudflareTests(
],
});

cfEnv.describeDevScenario('encrypted env blob with _VARLOCK_ENV_KEY', {
command: `vite dev --port ${basePort + 5}`,
readyPattern: /Local:.*http/,
readyTimeout: 30_000,
env: { _VARLOCK_ENV_KEY: randomBytes(32).toString('hex') },
templateFiles: {
'src/index.ts': 'workers/basic-worker.ts',
'vite.config.ts': 'vite-configs/vite.config.ts',
'wrangler.jsonc': '_base-wrangler/wrangler.jsonc',
'tsconfig.json': '_base-wrangler/tsconfig.json',
},
requests: [
{
path: '/',
bodyAssertions: {
shouldContain: [
'public_var::public-test-value',
'api_url::https://api.example.com',
'has_sensitive::yes',
],
shouldNotContain: ['super-secret-value'],
},
},
],
});

cfEnv.describeDevScenario('large env (chunking)', {
command: `vite dev --port ${basePort + 4}`,
readyPattern: /Local:.*http/,
Expand Down
26 changes: 26 additions & 0 deletions framework-tests/frameworks/expo/expo.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { randomBytes } from 'node:crypto';
import {
describe, beforeAll, afterAll,
} from 'vitest';
Expand Down Expand Up @@ -146,6 +147,31 @@ describe('Expo Integration', () => {
});
});

describe('encrypted env blob', () => {
expoEnv.describeScenario('build succeeds with _VARLOCK_ENV_KEY', {
command: 'node build.mjs',
env: { _VARLOCK_ENV_KEY: randomBytes(32).toString('hex') },
templateFiles: {
'app/page.tsx': 'pages/basic-page.tsx',
},
fileAssertions: [
{
description: 'public env vars are still statically replaced',
fileGlob: 'dist/**/*.js',
shouldContain: [
'"Varlock Expo Test"',
'"https://api.example.com"',
],
},
{
description: 'sensitive var is NOT inlined',
fileGlob: 'dist/**/*.js',
shouldNotContain: ['super-secret-key-12345'],
},
],
});
});

describe('invalid schema', () => {
expoEnv.describeScenario('build fails on invalid config', {
command: 'node build.mjs',
Expand Down
18 changes: 18 additions & 0 deletions framework-tests/frameworks/nextjs/nextjs-shared.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { randomBytes } from 'node:crypto';
import {
describe, beforeAll, afterAll,
} from 'vitest';
Expand Down Expand Up @@ -346,6 +347,23 @@ export function defineNextjsTests(nextVersion: number, testDir: string) {
],
});

nextEnv.describeScenario('encrypted env blob with _VARLOCK_ENV_KEY', {
command: buildCommand,
env: { _VARLOCK_ENV_KEY: randomBytes(32).toString('hex') },
templateFiles: {
'app/page.tsx': 'pages/basic-page.tsx',
},
expectSuccess: true,
fileAssertions: [
{
description: 'server JS files contain encrypted blob (varlock:v1: prefix) instead of plaintext',
fileGlob: '.next/server/**/*.js',
shouldContain: ['varlock:v1:'],
shouldNotContain: ['super-secret-var'],
},
],
});

nextEnv.describeScenario('leaky edge page', {
command: buildCommand,
templateFiles: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from 'vite';
import { varlockVitePlugin } from '@varlock/vite-integration';

export default defineConfig({
plugins: [varlockVitePlugin({ ssrInjectMode: 'resolved-env' })],
});
33 changes: 33 additions & 0 deletions framework-tests/frameworks/vite/vite-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ Shared Vite test definitions, parameterized by Vite version.
Covers static builds, HTML constant replacement, leak detection,
log redaction, sourcemap scrubbing, SSR init injection, and dev server.
*/
import { randomBytes } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import {
Expand Down Expand Up @@ -256,6 +257,38 @@ export function defineViteTests(
});
});

// ---- Encrypted env blob ----

describe('encrypted env blob', () => {
viteEnv.describeScenario('SSR build with _VARLOCK_ENV_KEY encrypts the blob', {
command: 'vite build --ssr src/ssr-entry.ts',
env: { _VARLOCK_ENV_KEY: randomBytes(32).toString('hex') },
templateFiles: {
'vite.config.ts': 'vite-configs/vite.config.resolved-env.ts',
'index.html': 'html/basic.html',
'src/ssr-entry.ts': 'pages/ssr-entry.ts',
},
expectSuccess: true,
fileAssertions: [
{
description: 'SSR output contains encrypted blob (varlock:v1: prefix)',
fileGlob: 'dist/*.js',
shouldContain: ['varlock:v1:'],
},
{
description: 'SSR output does not contain plaintext secret',
fileGlob: 'dist/*.js',
shouldNotContain: ['super-secret-value'],
},
{
description: 'public vars are still statically replaced',
fileGlob: 'dist/*.js',
shouldContain: ['public-test-value'],
},
],
});
});

// ---- Dev server ----

describe('dev server', () => {
Expand Down
25 changes: 18 additions & 7 deletions framework-tests/harness/pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,31 @@ const PACKAGE_DIRS: Record<string, string> = {
/**
* Finds an existing packed .tgz file for the given package name.
*/
function findPackedFile(packageName: string): string | undefined {
if (!existsSync(PACKED_DIR)) return undefined;
function findPackedFiles(packageName: string): Array<string> {
if (!existsSync(PACKED_DIR)) return [];

const files = readdirSync(PACKED_DIR).filter((f) => f.endsWith('.tgz'));

// Package names like @varlock/nextjs-integration produce tgz files like
// varlock-nextjs-integration-0.2.3.tgz (scoped @ and / are stripped/replaced)
// Match "name-VERSION.tgz" where VERSION starts with a digit to avoid
// "varlock-" matching "varlock-vite-integration-..."
const normalizedName = packageName
.replace(/^@/, '')
.replace(/\//g, '-');

const match = files.find((f) => f.startsWith(`${normalizedName}-`));
return match ? join(PACKED_DIR, match) : undefined;
return files
.filter((f) => {
if (!f.startsWith(`${normalizedName}-`)) return false;
const rest = f.slice(normalizedName.length + 1);
return /^\d/.test(rest);
})
.map((f) => join(PACKED_DIR, f));
}

function findPackedFile(packageName: string): string | undefined {
const matches = findPackedFiles(packageName);
return matches[0];
}

/**
Expand Down Expand Up @@ -111,9 +123,8 @@ export function packPackages(
if (existing) return existing;
}

// Remove old tarball if it exists (version may not have changed)
const oldPacked = findPackedFile(name);
if (oldPacked) {
// Remove all old tarballs for this package (handles version bumps leaving stale files)
for (const oldPacked of findPackedFiles(name)) {
rmSync(oldPacked);
}

Expand Down
5 changes: 3 additions & 2 deletions framework-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
},
"devDependencies": {
"@types/node": "^24.0.0",
"vitest": "^3.2.4",
"typescript": "^5.9.3"
"@types/react": "19.2.15",
"typescript": "^5.9.3",
"vitest": "^3.2.4"
}
}
Loading
Loading