Skip to content

Commit

Permalink
feat: Add privatePrefix for private environment variables (#9996)
Browse files Browse the repository at this point in the history
Closes #9776.

Adds a new config option, config.kit.env.privatePrefix, for setting a private prefix on environment variables. This defaults to ''.

To prevent super-weird and unexpected behaviors, the logic is:

- private: does not begin with public prefix, does begin with private prefix
- public: does begin with public prefix, does not begin with private prefix
This has the side benefit of not allowing the two prefixes to be the same.

Note: Had to create env.js utils file so that the utils could be imported into server/index -- putting them pretty much anywhere else ended up causing a transitive dependency on some node package somewhere that wasn't compatible with Node 16.

---------

Co-authored-by: Simon H <[email protected]>
  • Loading branch information
1 parent 316a4af commit 47d8048
Show file tree
Hide file tree
Showing 20 changed files with 124 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .changeset/stupid-mayflies-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: add `privatePrefix` to `config.kit.env`
3 changes: 2 additions & 1 deletion packages/kit/src/core/config/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ const get_defaults = (prefix = '') => ({
embedded: false,
env: {
dir: process.cwd(),
publicPrefix: 'PUBLIC_'
publicPrefix: 'PUBLIC_',
privatePrefix: ''
},
files: {
assets: join(prefix, 'static'),
Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/core/config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ const options = object(

env: object({
dir: string(process.cwd()),
publicPrefix: string('PUBLIC_')
publicPrefix: string('PUBLIC_'),
privatePrefix: string('')
}),

files: object({
Expand Down
21 changes: 15 additions & 6 deletions packages/kit/src/core/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,21 +63,30 @@ export function create_static_types(id, env) {
/**
* @param {EnvType} id
* @param {import('types').Env} env
* @param {string} prefix
* @param {{
* public_prefix: string;
* private_prefix: string;
* }} prefixes
* @returns {string}
*/
export function create_dynamic_types(id, env, prefix) {
export function create_dynamic_types(id, env, { public_prefix, private_prefix }) {
const properties = Object.keys(env[id])
.filter((k) => valid_identifier.test(k))
.map((k) => `${k}: string;`);

const prefixed = `[key: \`${prefix}\${string}\`]`;
const public_prefixed = `[key: \`${public_prefix}\${string}\`]`;
const private_prefixed = `[key: \`${private_prefix}\${string}\`]`;

if (id === 'private') {
properties.push(`${prefixed}: undefined;`);
properties.push('[key: string]: string | undefined;');
if (public_prefix) {
properties.push(`${public_prefixed}: undefined;`);
}
properties.push(`${private_prefixed}: string | undefined;`);
} else {
properties.push(`${prefixed}: string | undefined;`);
if (private_prefix) {
properties.push(`${private_prefixed}: undefined;`);
}
properties.push(`${public_prefixed}: string | undefined;`);
}

return dedent`
Expand Down
8 changes: 4 additions & 4 deletions packages/kit/src/core/postbuild/analyse.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { forked } from '../../utils/fork.js';
import { should_polyfill } from '../../utils/platform.js';
import { installPolyfills } from '../../exports/node/polyfills.js';
import { resolvePath } from '../../exports/index.js';
import { filter_private_env, filter_public_env } from '../../utils/env.js';

export default forked(import.meta.url, analyse);

Expand Down Expand Up @@ -43,10 +44,9 @@ async function analyse({ manifest_path, env }) {
internal.set_building(true);

// set env, in case it's used in initialisation
const entries = Object.entries(env);
const prefix = config.env.publicPrefix;
internal.set_private_env(Object.fromEntries(entries.filter(([k]) => !k.startsWith(prefix))));
internal.set_public_env(Object.fromEntries(entries.filter(([k]) => k.startsWith(prefix))));
const { publicPrefix: public_prefix, privatePrefix: private_prefix } = config.env;
internal.set_private_env(filter_private_env(env, { public_prefix, private_prefix }));
internal.set_public_env(filter_public_env(env, { public_prefix, private_prefix }));

/** @type {import('types').ServerMetadata} */
const metadata = {
Expand Down
14 changes: 9 additions & 5 deletions packages/kit/src/core/sync/write_ambient.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ function read_description(filename) {

/**
* @param {import('types').Env} env
* @param {string} prefix
* @param {{
* public_prefix: string;
* private_prefix: string;
* }} prefixes
*/
const template = (env, prefix) => `
const template = (env, prefixes) => `
${GENERATED_COMMENT}
/// <reference types="@sveltejs/kit" />
Expand All @@ -36,10 +39,10 @@ ${read_description('$env+static+public.md')}
${create_static_types('public', env)}
${read_description('$env+dynamic+private.md')}
${create_dynamic_types('private', env, prefix)}
${create_dynamic_types('private', env, prefixes)}
${read_description('$env+dynamic+public.md')}
${create_dynamic_types('public', env, prefix)}
${create_dynamic_types('public', env, prefixes)}
`;

/**
Expand All @@ -51,9 +54,10 @@ ${create_dynamic_types('public', env, prefix)}
*/
export function write_ambient(config, mode) {
const env = get_env(config.env, mode);
const { publicPrefix: public_prefix, privatePrefix: private_prefix } = config.env;

write_if_changed(
path.join(config.outDir, 'ambient.d.ts'),
template(env, config.env.publicPrefix)
template(env, { public_prefix, private_prefix })
);
}
1 change: 1 addition & 0 deletions packages/kit/src/core/sync/write_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const options = {
track_server_fetches: ${s(config.kit.dangerZone.trackServerFetches)},
embedded: ${config.kit.embedded},
env_public_prefix: '${config.kit.env.publicPrefix}',
env_private_prefix: '${config.kit.env.privatePrefix}',
hooks: null, // added lazily, via \`get_hooks\`
preload_strategy: ${s(config.kit.output.preloadStrategy)},
root,
Expand Down
5 changes: 5 additions & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,11 @@ export interface KitConfig {
* @default "PUBLIC_"
*/
publicPrefix?: string;
/**
* A prefix that signals that an environment variable is unsafe to expose to client-side code. Environment variables matching neither the public nor the private prefix will be discarded completely. See [`$env/static/private`](/docs/modules#$env-static-private) and [`$env/dynamic/private`](/docs/modules#$env-dynamic-private).
* @default ""
*/
privatePrefix?: string;
};
/**
* Where to find various files within your project.
Expand Down
8 changes: 5 additions & 3 deletions packages/kit/src/exports/vite/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import path from 'node:path';
import { loadEnv } from 'vite';
import { posixify } from '../../utils/filesystem.js';
import { negotiate } from '../../utils/http.js';
import { filter_private_env, filter_public_env } from '../../utils/env.js';

/**
* Transforms kit.alias to a valid vite.resolve.alias array.
Expand Down Expand Up @@ -56,11 +57,12 @@ function escape_for_regexp(str) {
* @param {string} mode
*/
export function get_env(env_config, mode) {
const entries = Object.entries(loadEnv(mode, env_config.dir, ''));
const { publicPrefix: public_prefix, privatePrefix: private_prefix } = env_config;
const env = loadEnv(mode, env_config.dir, '');

return {
public: Object.fromEntries(entries.filter(([k]) => k.startsWith(env_config.publicPrefix))),
private: Object.fromEntries(entries.filter(([k]) => !k.startsWith(env_config.publicPrefix)))
public: filter_public_env(env, { public_prefix, private_prefix }),
private: filter_private_env(env, { public_prefix, private_prefix })
};
}

Expand Down
22 changes: 14 additions & 8 deletions packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { respond } from './respond.js';
import { set_private_env, set_public_env } from '../shared-server.js';
import { options, get_hooks } from '__SERVER__/internal.js';
import { DEV } from 'esm-env';
import { filter_private_env, filter_public_env } from '../../utils/env.js';

export class Server {
/** @type {import('types').SSROptions} */
Expand All @@ -26,14 +27,19 @@ export class Server {
// Take care: Some adapters may have to call `Server.init` per-request to set env vars,
// so anything that shouldn't be rerun should be wrapped in an `if` block to make sure it hasn't
// been done already.
const entries = Object.entries(env);

const prefix = this.#options.env_public_prefix;
const prv = Object.fromEntries(entries.filter(([k]) => !k.startsWith(prefix)));
const pub = Object.fromEntries(entries.filter(([k]) => k.startsWith(prefix)));

set_private_env(prv);
set_public_env(pub);
// set env, in case it's used in initialisation
set_private_env(
filter_private_env(env, {
public_prefix: this.#options.env_public_prefix,
private_prefix: this.#options.env_private_prefix
})
);
set_public_env(
filter_public_env(env, {
public_prefix: this.#options.env_public_prefix,
private_prefix: this.#options.env_private_prefix
})
);

if (!this.#options.hooks) {
try {
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ export interface SSROptions {
track_server_fetches: boolean;
embedded: boolean;
env_public_prefix: string;
env_private_prefix: string;
hooks: ServerHooks;
preload_strategy: ValidatedConfig['kit']['output']['preloadStrategy'];
root: SSRComponent['default'];
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/types/synthetic/$env+dynamic+private.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
This module provides access to runtime environment variables, as defined by the platform you're running on. For example if you're using [`adapter-node`](https://github.com/sveltejs/kit/tree/master/packages/adapter-node) (or running [`vite preview`](https://kit.svelte.dev/docs/cli)), this is equivalent to `process.env`. This module only includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#env).
This module provides access to runtime environment variables, as defined by the platform you're running on. For example if you're using [`adapter-node`](https://github.com/sveltejs/kit/tree/master/packages/adapter-node) (or running [`vite preview`](https://kit.svelte.dev/docs/cli)), this is equivalent to `process.env`. This module only includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#env) _and do_ start with [`config.kit.env.privatePrefix`](https://kit.svelte.dev/docs/configuration#env) (if configured).

This module cannot be imported into client-side code.

Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/types/synthetic/$env+static+private.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Environment variables [loaded by Vite](https://vitejs.dev/guide/env-and-mode.html#env-files) from `.env` files and `process.env`. Like [`$env/dynamic/private`](https://kit.svelte.dev/docs/modules#$env-dynamic-private), this module cannot be imported into client-side code. This module only includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#env).
Environment variables [loaded by Vite](https://vitejs.dev/guide/env-and-mode.html#env-files) from `.env` files and `process.env`. Like [`$env/dynamic/private`](https://kit.svelte.dev/docs/modules#$env-dynamic-private), this module cannot be imported into client-side code. This module only includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#env) _and do_ start with [`config.kit.env.privatePrefix`](https://kit.svelte.dev/docs/configuration#env) (if configured).

_Unlike_ [`$env/dynamic/private`](https://kit.svelte.dev/docs/modules#$env-dynamic-private), the values exported from this module are statically injected into your bundle at build time, enabling optimisations like dead code elimination.

Expand Down
33 changes: 33 additions & 0 deletions packages/kit/src/utils/env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* @param {Record<string, string>} env
* @param {{
* public_prefix: string;
* private_prefix: string;
* }} prefixes
* @returns {Record<string, string>}
*/
export function filter_private_env(env, { public_prefix, private_prefix }) {
return Object.fromEntries(
Object.entries(env).filter(
([k]) =>
k.startsWith(private_prefix) && (public_prefix === '' || !k.startsWith(public_prefix))
)
);
}

/**
* @param {Record<string, string>} env
* @param {{
* public_prefix: string;
* private_prefix: string;
* }} prefixes
* @returns {Record<string, string>}
*/
export function filter_public_env(env, { public_prefix, private_prefix }) {
return Object.fromEntries(
Object.entries(env).filter(
([k]) =>
k.startsWith(public_prefix) && (private_prefix === '' || !k.startsWith(private_prefix))
)
);
}
2 changes: 1 addition & 1 deletion packages/kit/test/apps/embed/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "test-options",
"name": "test-embed",
"private": true,
"version": "0.0.1",
"scripts": {
Expand Down
4 changes: 3 additions & 1 deletion packages/kit/test/apps/options/env-dir/.env
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
GO_AWAY_PLEASE=and thank you
GO_AWAY_PLEASE=and thank you
TOP_SECRET_SHH_PLS=shhhh
MATCHES_NEITHER=should be discarded
10 changes: 10 additions & 0 deletions packages/kit/test/apps/options/source/pages/env/+page.server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { TOP_SECRET_SHH_PLS } from '$env/static/private';
import { env } from '$env/dynamic/private';

export function load() {
return {
TOP_SECRET_SHH_PLS,
// @ts-expect-error
MATCHES_NEITHER: env.MATCHES_NEITHER || ''
};
}
5 changes: 4 additions & 1 deletion packages/kit/test/apps/options/source/pages/env/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<script>
import { GO_AWAY_PLEASE } from '$env/static/public';
export let data;
</script>

<p>{GO_AWAY_PLEASE}</p>
<p id="public">{GO_AWAY_PLEASE}</p>
<p id="private">{data.TOP_SECRET_SHH_PLS}</p>
<p id="neither">{data.MATCHES_NEITHER}</p>
3 changes: 2 additions & 1 deletion packages/kit/test/apps/options/svelte.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ const config = {
},
env: {
dir: './env-dir',
publicPrefix: 'GO_AWAY_'
publicPrefix: 'GO_AWAY_',
privatePrefix: 'TOP_SECRET_SHH'
}
}
};
Expand Down
7 changes: 6 additions & 1 deletion packages/kit/test/apps/options/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,12 @@ test.describe('Custom extensions', () => {
test.describe('env', () => {
test('resolves downwards', async ({ page }) => {
await page.goto('/path-base/env');
expect(await page.textContent('p')).toBe('and thank you');
expect(await page.textContent('#public')).toBe('and thank you');
});
test('respects private prefix', async ({ page }) => {
await page.goto('/path-base/env');
expect(await page.textContent('#private')).toBe('shhhh');
expect(await page.textContent('#neither')).toBe('');
});
});

Expand Down

0 comments on commit 47d8048

Please sign in to comment.