student life social platform
at main 5.1 kB view raw
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}