Skip to content

Commit 47d8048

Browse files
feat: Add privatePrefix for private environment variables (#9996)
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]>
1 parent 316a4af commit 47d8048

File tree

20 files changed

+124
-35
lines changed

20 files changed

+124
-35
lines changed

.changeset/stupid-mayflies-suffer.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': minor
3+
---
4+
5+
feat: add `privatePrefix` to `config.kit.env`

packages/kit/src/core/config/index.spec.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ const get_defaults = (prefix = '') => ({
7575
embedded: false,
7676
env: {
7777
dir: process.cwd(),
78-
publicPrefix: 'PUBLIC_'
78+
publicPrefix: 'PUBLIC_',
79+
privatePrefix: ''
7980
},
8081
files: {
8182
assets: join(prefix, 'static'),

packages/kit/src/core/config/options.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ const options = object(
120120

121121
env: object({
122122
dir: string(process.cwd()),
123-
publicPrefix: string('PUBLIC_')
123+
publicPrefix: string('PUBLIC_'),
124+
privatePrefix: string('')
124125
}),
125126

126127
files: object({

packages/kit/src/core/env.js

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,21 +63,30 @@ export function create_static_types(id, env) {
6363
/**
6464
* @param {EnvType} id
6565
* @param {import('types').Env} env
66-
* @param {string} prefix
66+
* @param {{
67+
* public_prefix: string;
68+
* private_prefix: string;
69+
* }} prefixes
6770
* @returns {string}
6871
*/
69-
export function create_dynamic_types(id, env, prefix) {
72+
export function create_dynamic_types(id, env, { public_prefix, private_prefix }) {
7073
const properties = Object.keys(env[id])
7174
.filter((k) => valid_identifier.test(k))
7275
.map((k) => `${k}: string;`);
7376

74-
const prefixed = `[key: \`${prefix}\${string}\`]`;
77+
const public_prefixed = `[key: \`${public_prefix}\${string}\`]`;
78+
const private_prefixed = `[key: \`${private_prefix}\${string}\`]`;
7579

7680
if (id === 'private') {
77-
properties.push(`${prefixed}: undefined;`);
78-
properties.push('[key: string]: string | undefined;');
81+
if (public_prefix) {
82+
properties.push(`${public_prefixed}: undefined;`);
83+
}
84+
properties.push(`${private_prefixed}: string | undefined;`);
7985
} else {
80-
properties.push(`${prefixed}: string | undefined;`);
86+
if (private_prefix) {
87+
properties.push(`${private_prefixed}: undefined;`);
88+
}
89+
properties.push(`${public_prefixed}: string | undefined;`);
8190
}
8291

8392
return dedent`

packages/kit/src/core/postbuild/analyse.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { forked } from '../../utils/fork.js';
1313
import { should_polyfill } from '../../utils/platform.js';
1414
import { installPolyfills } from '../../exports/node/polyfills.js';
1515
import { resolvePath } from '../../exports/index.js';
16+
import { filter_private_env, filter_public_env } from '../../utils/env.js';
1617

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

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

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

5151
/** @type {import('types').ServerMetadata} */
5252
const metadata = {

packages/kit/src/core/sync/write_ambient.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ function read_description(filename) {
2222

2323
/**
2424
* @param {import('types').Env} env
25-
* @param {string} prefix
25+
* @param {{
26+
* public_prefix: string;
27+
* private_prefix: string;
28+
* }} prefixes
2629
*/
27-
const template = (env, prefix) => `
30+
const template = (env, prefixes) => `
2831
${GENERATED_COMMENT}
2932
3033
/// <reference types="@sveltejs/kit" />
@@ -36,10 +39,10 @@ ${read_description('$env+static+public.md')}
3639
${create_static_types('public', env)}
3740
3841
${read_description('$env+dynamic+private.md')}
39-
${create_dynamic_types('private', env, prefix)}
42+
${create_dynamic_types('private', env, prefixes)}
4043
4144
${read_description('$env+dynamic+public.md')}
42-
${create_dynamic_types('public', env, prefix)}
45+
${create_dynamic_types('public', env, prefixes)}
4346
`;
4447

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

5559
write_if_changed(
5660
path.join(config.outDir, 'ambient.d.ts'),
57-
template(env, config.env.publicPrefix)
61+
template(env, { public_prefix, private_prefix })
5862
);
5963
}

packages/kit/src/core/sync/write_server.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export const options = {
3737
track_server_fetches: ${s(config.kit.dangerZone.trackServerFetches)},
3838
embedded: ${config.kit.embedded},
3939
env_public_prefix: '${config.kit.env.publicPrefix}',
40+
env_private_prefix: '${config.kit.env.privatePrefix}',
4041
hooks: null, // added lazily, via \`get_hooks\`
4142
preload_strategy: ${s(config.kit.output.preloadStrategy)},
4243
root,

packages/kit/src/exports/public.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,11 @@ export interface KitConfig {
372372
* @default "PUBLIC_"
373373
*/
374374
publicPrefix?: string;
375+
/**
376+
* 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).
377+
* @default ""
378+
*/
379+
privatePrefix?: string;
375380
};
376381
/**
377382
* Where to find various files within your project.

packages/kit/src/exports/vite/utils.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import path from 'node:path';
22
import { loadEnv } from 'vite';
33
import { posixify } from '../../utils/filesystem.js';
44
import { negotiate } from '../../utils/http.js';
5+
import { filter_private_env, filter_public_env } from '../../utils/env.js';
56

67
/**
78
* Transforms kit.alias to a valid vite.resolve.alias array.
@@ -56,11 +57,12 @@ function escape_for_regexp(str) {
5657
* @param {string} mode
5758
*/
5859
export function get_env(env_config, mode) {
59-
const entries = Object.entries(loadEnv(mode, env_config.dir, ''));
60+
const { publicPrefix: public_prefix, privatePrefix: private_prefix } = env_config;
61+
const env = loadEnv(mode, env_config.dir, '');
6062

6163
return {
62-
public: Object.fromEntries(entries.filter(([k]) => k.startsWith(env_config.publicPrefix))),
63-
private: Object.fromEntries(entries.filter(([k]) => !k.startsWith(env_config.publicPrefix)))
64+
public: filter_public_env(env, { public_prefix, private_prefix }),
65+
private: filter_private_env(env, { public_prefix, private_prefix })
6466
};
6567
}
6668

packages/kit/src/runtime/server/index.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { respond } from './respond.js';
22
import { set_private_env, set_public_env } from '../shared-server.js';
33
import { options, get_hooks } from '__SERVER__/internal.js';
44
import { DEV } from 'esm-env';
5+
import { filter_private_env, filter_public_env } from '../../utils/env.js';
56

67
export class Server {
78
/** @type {import('types').SSROptions} */
@@ -26,14 +27,19 @@ export class Server {
2627
// Take care: Some adapters may have to call `Server.init` per-request to set env vars,
2728
// so anything that shouldn't be rerun should be wrapped in an `if` block to make sure it hasn't
2829
// been done already.
29-
const entries = Object.entries(env);
30-
31-
const prefix = this.#options.env_public_prefix;
32-
const prv = Object.fromEntries(entries.filter(([k]) => !k.startsWith(prefix)));
33-
const pub = Object.fromEntries(entries.filter(([k]) => k.startsWith(prefix)));
34-
35-
set_private_env(prv);
36-
set_public_env(pub);
30+
// set env, in case it's used in initialisation
31+
set_private_env(
32+
filter_private_env(env, {
33+
public_prefix: this.#options.env_public_prefix,
34+
private_prefix: this.#options.env_private_prefix
35+
})
36+
);
37+
set_public_env(
38+
filter_public_env(env, {
39+
public_prefix: this.#options.env_public_prefix,
40+
private_prefix: this.#options.env_private_prefix
41+
})
42+
);
3743

3844
if (!this.#options.hooks) {
3945
try {

packages/kit/src/types/internal.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ export interface SSROptions {
336336
track_server_fetches: boolean;
337337
embedded: boolean;
338338
env_public_prefix: string;
339+
env_private_prefix: string;
339340
hooks: ServerHooks;
340341
preload_strategy: ValidatedConfig['kit']['output']['preloadStrategy'];
341342
root: SSRComponent['default'];

packages/kit/src/types/synthetic/$env+dynamic+private.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
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).
1+
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).
22

33
This module cannot be imported into client-side code.
44

packages/kit/src/types/synthetic/$env+static+private.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
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).
1+
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).
22

33
_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.
44

packages/kit/src/utils/env.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* @param {Record<string, string>} env
3+
* @param {{
4+
* public_prefix: string;
5+
* private_prefix: string;
6+
* }} prefixes
7+
* @returns {Record<string, string>}
8+
*/
9+
export function filter_private_env(env, { public_prefix, private_prefix }) {
10+
return Object.fromEntries(
11+
Object.entries(env).filter(
12+
([k]) =>
13+
k.startsWith(private_prefix) && (public_prefix === '' || !k.startsWith(public_prefix))
14+
)
15+
);
16+
}
17+
18+
/**
19+
* @param {Record<string, string>} env
20+
* @param {{
21+
* public_prefix: string;
22+
* private_prefix: string;
23+
* }} prefixes
24+
* @returns {Record<string, string>}
25+
*/
26+
export function filter_public_env(env, { public_prefix, private_prefix }) {
27+
return Object.fromEntries(
28+
Object.entries(env).filter(
29+
([k]) =>
30+
k.startsWith(public_prefix) && (private_prefix === '' || !k.startsWith(private_prefix))
31+
)
32+
);
33+
}

packages/kit/test/apps/embed/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "test-options",
2+
"name": "test-embed",
33
"private": true,
44
"version": "0.0.1",
55
"scripts": {
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
GO_AWAY_PLEASE=and thank you
1+
GO_AWAY_PLEASE=and thank you
2+
TOP_SECRET_SHH_PLS=shhhh
3+
MATCHES_NEITHER=should be discarded
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { TOP_SECRET_SHH_PLS } from '$env/static/private';
2+
import { env } from '$env/dynamic/private';
3+
4+
export function load() {
5+
return {
6+
TOP_SECRET_SHH_PLS,
7+
// @ts-expect-error
8+
MATCHES_NEITHER: env.MATCHES_NEITHER || ''
9+
};
10+
}
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
<script>
22
import { GO_AWAY_PLEASE } from '$env/static/public';
3+
export let data;
34
</script>
45

5-
<p>{GO_AWAY_PLEASE}</p>
6+
<p id="public">{GO_AWAY_PLEASE}</p>
7+
<p id="private">{data.TOP_SECRET_SHH_PLS}</p>
8+
<p id="neither">{data.MATCHES_NEITHER}</p>

packages/kit/test/apps/options/svelte.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ const config = {
3636
},
3737
env: {
3838
dir: './env-dir',
39-
publicPrefix: 'GO_AWAY_'
39+
publicPrefix: 'GO_AWAY_',
40+
privatePrefix: 'TOP_SECRET_SHH'
4041
}
4142
}
4243
};

packages/kit/test/apps/options/test/test.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,12 @@ test.describe('Custom extensions', () => {
157157
test.describe('env', () => {
158158
test('resolves downwards', async ({ page }) => {
159159
await page.goto('/path-base/env');
160-
expect(await page.textContent('p')).toBe('and thank you');
160+
expect(await page.textContent('#public')).toBe('and thank you');
161+
});
162+
test('respects private prefix', async ({ page }) => {
163+
await page.goto('/path-base/env');
164+
expect(await page.textContent('#private')).toBe('shhhh');
165+
expect(await page.textContent('#neither')).toBe('');
161166
});
162167
});
163168

0 commit comments

Comments
 (0)