student life social platform
1/**
2 * Update .env to add missing variables, taking their declarations from .env.example
3 * Supports multiline values (single-quoted strings)
4 */
5
6import dotenv from 'dotenv-parser-serializer';
7import fs from 'fs';
8import kleur from 'kleur';
9import path from 'path';
10const { blue, bold, dim, yellow } = kleur;
11
12const here = path.dirname(new URL(import.meta.url).pathname);
13const envPath = path.join(here, '../.env');
14const examplePath = path.join(here, '../.env.example');
15
16type DotEnv = Record<string, { value: string; description: string | null }>;
17const env: DotEnv = dotenv.parse(fs.readFileSync(envPath, 'utf-8'), {
18 extractDescriptions: true,
19});
20const example: DotEnv = dotenv.parse(fs.readFileSync(examplePath, 'utf-8'), {
21 extractDescriptions: true,
22});
23
24const keysNotInExample = Object.keys(env).filter((key) => !(key in example));
25if (keysNotInExample.length > 0) {
26 const toAddToExample = Object.fromEntries(
27 keysNotInExample.map((key) => {
28 let value = '';
29 if (key.startsWith('PUBLIC_') || !isSensitive(env[key].value)) {
30 value = env[key].value;
31 }
32
33 return [
34 key,
35 {
36 value,
37 description:
38 env[key].description || (value ? null : 'TODO: document this environment variable'),
39 },
40 ];
41 }),
42 );
43 console.warn(yellow('Local .env file contains keys that are not in the example file:'));
44 keysNotInExample.forEach((key) => {
45 console.warn(`- ${key}`);
46 });
47
48 console.info('Adding the following to .env.example:');
49 console.info(quoteblock(dotenv.serialize(toAddToExample).trim()));
50 fs.writeFileSync(examplePath, dotenv.serialize({ ...example, ...toAddToExample }));
51 console.info(bold('Remember to also update packages/api/src/env.ts, add the following:'));
52 console.info(
53 Object.entries(toAddToExample)
54 .map(
55 ([key, { description }]) =>
56 `${key}: z.string()/* refine the schema here if relevant */.describe('${description}'),`,
57 )
58 .join('\n'),
59 );
60}
61
62const keysOnlyInExample = Object.keys(example).filter((key) => !(key in env));
63for (const key of keysOnlyInExample) {
64 console.info(`${blue(bold(key))} was added to .env.example, adding this to your .env file:`);
65 console.info(quoteblock(dotenv.serialize({ [key]: example[key] }).trim()));
66}
67const result = { ...example, ...env };
68
69// back up the old .env file
70fs.copyFileSync(envPath, `${envPath}.bak`);
71
72fs.writeFileSync(envPath, dotenv.serialize(result));
73
74function isSensitive(value: string): boolean {
75 // see https://github.com/mvhenten/string-entropy/blob/HEAD/src/index.ts
76 const LOWERCASE_ALPHA = 'abcdefghijklmnopqrstuvwxyz',
77 UPPERCASE_ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
78 DIGITS = '0123456789',
79 PUNCT1 = '!@#$%^&*()',
80 PUNCT2 = '~`-_=+[]{}\\|;:\'",.<>?/';
81
82 // Calculate the size of the alphabet.
83 //
84 // This is a mostly back-of-the hand calculation of the alphabet.
85 // We group the a-z, A-Z and 0-9 together with the leftovers of the keys on an US keyboard.
86 // Characters outside ascii add one more to the alphabet. Meaning that the alphabet size of the word:
87 // "ümlout" will yield 27 characters. There is no scientific reasoning behind this, besides to
88 // err on the save side.
89 /**
90 * @param {Str} str String to calculate the alphabet from
91 * @returns {Number} n Size of the alphabet
92 */
93 const alphabetSize = (str: string): number => {
94 let c: string;
95 let size = 0;
96
97 const collect: Record<string, number> = {
98 alcaps: 0,
99 punct1: 0,
100 digits: 0,
101 alpha: 0,
102 unicode: 0,
103 size: 0,
104 };
105
106 let seen = '';
107
108 for (var i = 0; i < str.length; i++) {
109 c = str[i];
110
111 // we only need to look at each character once
112 if (str.indexOf(c) !== i) continue;
113 if (LOWERCASE_ALPHA.indexOf(c) !== -1) collect.alpha = LOWERCASE_ALPHA.length;
114 else if (UPPERCASE_ALPHA.indexOf(c) !== -1) collect.alcaps = UPPERCASE_ALPHA.length;
115 else if (DIGITS.indexOf(c) !== -1) collect.digits = DIGITS.length;
116 else if (PUNCT1.indexOf(c) !== -1) collect.punct1 = PUNCT1.length;
117 else if (PUNCT2.indexOf(c) !== -1) collect.size = PUNCT2.length;
118 // I can only guess the size of a non-western alphabet.
119 // The choice here is to grant the size of the western alphabet, together
120 // with an additional bonus for the character itself.
121 //
122 // Someone correct me if I'm wrong here.
123 else if (c.charCodeAt(0) > 127) {
124 collect.alpha = 26;
125 collect.unicode += 1;
126 }
127
128 seen += c;
129 }
130
131 for (var k in collect) {
132 size += collect[k];
133 }
134
135 return size;
136 };
137
138 // Calculate [information entropy](https://en.wikipedia.org/wiki/Password_strength#Entropy_as_a_measure_of_password_strength)
139 /**
140 * @param {String} str String to calculate entropy for
141 * @returns {Number} entropy
142 */
143 const entropy = (str: string): number => {
144 if (!str) return 0;
145 return Math.round(str.length * (Math.log(alphabetSize(str)) / Math.log(2)));
146 };
147
148 return entropy(value) > 200;
149}
150
151function quoteblock(s: string): string {
152 return dim(s.replace(/^/gm, dim(bold('│ '))));
153}