diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js
index b50c8de6a..2f1d05f63 100644
--- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js
+++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js
@@ -47,6 +47,10 @@ const EnvironmentVariables = ({ environment, collection }) => {
variable.enabled = e.target.checked;
break;
}
+ case 'secret': {
+ variable.secret = e.target.checked;
+ break;
+ }
}
reducerDispatch({
type: 'UPDATE_VAR',
@@ -66,8 +70,10 @@ const EnvironmentVariables = ({ environment, collection }) => {
+ Enabled |
Name |
Value |
+ Secret |
|
@@ -76,6 +82,14 @@ const EnvironmentVariables = ({ environment, collection }) => {
? variables.map((variable, index) => {
return (
+
+ handleVarChange(e, variable, 'enabled')}
+ />
+ |
{
/>
|
-
- handleVarChange(e, variable, 'enabled')}
- />
-
-
+ handleVarChange(e, variable, 'secret')}
+ />
+ |
+
+
|
);
diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/reducer.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/reducer.js
index c72bf7b24..a5aa3e0c1 100644
--- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/reducer.js
+++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/reducer.js
@@ -12,6 +12,7 @@ const reducer = (state, action) => {
name: '',
value: '',
type: 'text',
+ secret: false,
enabled: true
});
draft.hasChanges = true;
@@ -24,6 +25,7 @@ const reducer = (state, action) => {
variable.name = action.variable.name;
variable.value = action.variable.value;
variable.enabled = action.variable.enabled;
+ variable.secret = action.variable.secret;
draft.hasChanges = true;
});
}
diff --git a/packages/bruno-app/src/utils/collections/export.js b/packages/bruno-app/src/utils/collections/export.js
index b3c7780ad..64fc0da91 100644
--- a/packages/bruno-app/src/utils/collections/export.js
+++ b/packages/bruno-app/src/utils/collections/export.js
@@ -54,11 +54,22 @@ const deleteUidsInEnvs = (envs) => {
});
};
+const deleteSecretsInEnvs = (envs) => {
+ each(envs, (env) => {
+ each(env.variables, (variable) => {
+ if (variable.secret) {
+ variable.value = '';
+ }
+ });
+ });
+};
+
const exportCollection = (collection) => {
// delete uids
delete collection.uid;
deleteUidsInItems(collection.items);
deleteUidsInEnvs(collection.environments);
+ deleteSecretsInEnvs(collection.environments);
transformItem(collection.items);
const fileName = `${collection.name}.json`;
diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json
index bb5314957..1334f3f5d 100644
--- a/packages/bruno-electron/package.json
+++ b/packages/bruno-electron/package.json
@@ -10,7 +10,8 @@
"clean": "rimraf dist",
"dev": "electron .",
"dist": "electron-builder --mac --config electron-builder-config.js",
- "pack": "electron-builder --dir"
+ "pack": "electron-builder --dir",
+ "test": "jest"
},
"dependencies": {
"@usebruno/js": "0.4.0",
@@ -32,6 +33,7 @@
"lodash": "^4.17.21",
"mustache": "^4.2.0",
"nanoid": "3.3.4",
+ "node-machine-id": "^1.1.12",
"qs": "^6.11.0",
"uuid": "^9.0.0",
"vm2": "^3.9.13",
diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js
index 5e6b27206..3fd2b08a2 100644
--- a/packages/bruno-electron/src/app/watcher.js
+++ b/packages/bruno-electron/src/app/watcher.js
@@ -10,7 +10,11 @@ const { isLegacyEnvFile, migrateLegacyEnvFile, isLegacyBruFile, migrateLegacyBru
const { itemSchema } = require('@usebruno/schema');
const { uuid } = require('../utils/common');
const { getRequestUid } = require('../cache/requestUids');
+const { decryptString } = require('../utils/encryption');
const { setDotEnvVars } = require('../store/process-env');
+const EnvironmentSecretsStore = require('../store/env-secrets');
+
+const environmentSecretsStore = new EnvironmentSecretsStore();
const isJsonEnvironmentConfig = (pathname, collectionPath) => {
const dirname = path.dirname(pathname);
@@ -56,7 +60,13 @@ const hydrateRequestWithUuid = (request, pathname) => {
return request;
};
-const addEnvironmentFile = async (win, pathname, collectionUid) => {
+const envHasSecrets = (environment = {}) => {
+ const secrets = _.filter(environment.variables, (v) => v.secret);
+
+ return secrets && secrets.length > 0;
+};
+
+const addEnvironmentFile = async (win, pathname, collectionUid, collectionPath) => {
try {
const basename = path.basename(pathname);
const file = {
@@ -79,13 +89,25 @@ const addEnvironmentFile = async (win, pathname, collectionUid) => {
file.data.uid = getRequestUid(pathname);
_.each(_.get(file, 'data.variables', []), (variable) => (variable.uid = uuid()));
+
+ // hydrate environment variables with secrets
+ if (envHasSecrets(file.data)) {
+ const envSecrets = environmentSecretsStore.getEnvSecrets(collectionPath, file.data);
+ _.each(envSecrets, (secret) => {
+ const variable = _.find(file.data.variables, (v) => v.name === secret.name);
+ if (variable) {
+ variable.value = decryptString(secret.value);
+ }
+ });
+ }
+
win.webContents.send('main:collection-tree-updated', 'addEnvironmentFile', file);
} catch (err) {
console.error(err);
}
};
-const changeEnvironmentFile = async (win, pathname, collectionUid) => {
+const changeEnvironmentFile = async (win, pathname, collectionUid, collectionPath) => {
try {
const basename = path.basename(pathname);
const file = {
@@ -102,6 +124,17 @@ const changeEnvironmentFile = async (win, pathname, collectionUid) => {
file.data.uid = getRequestUid(pathname);
_.each(_.get(file, 'data.variables', []), (variable) => (variable.uid = uuid()));
+ // hydrate environment variables with secrets
+ if (envHasSecrets(file.data)) {
+ const envSecrets = environmentSecretsStore.getEnvSecrets(collectionPath, file.data);
+ _.each(envSecrets, (secret) => {
+ const variable = _.find(file.data.variables, (v) => v.name === secret.name);
+ if (variable) {
+ variable.value = decryptString(secret.value);
+ }
+ });
+ }
+
// we are reusing the addEnvironmentFile event itself
// this is because the uid of the pathname remains the same
// and the collection tree will be able to update the existing environment
@@ -180,7 +213,7 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
}
if (isBruEnvironmentConfig(pathname, collectionPath)) {
- return addEnvironmentFile(win, pathname, collectionUid);
+ return addEnvironmentFile(win, pathname, collectionUid, collectionPath);
}
// migrate old json files to bru
@@ -268,7 +301,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
}
if (isBruEnvironmentConfig(pathname, collectionPath)) {
- return changeEnvironmentFile(win, pathname, collectionUid);
+ return changeEnvironmentFile(win, pathname, collectionUid, collectionPath);
}
if (hasBruExtension(pathname)) {
diff --git a/packages/bruno-electron/src/bru/index.js b/packages/bruno-electron/src/bru/index.js
index 6b1d73cdf..2a0d21da5 100644
--- a/packages/bruno-electron/src/bru/index.js
+++ b/packages/bruno-electron/src/bru/index.js
@@ -15,7 +15,7 @@ const bruToEnvJson = (bru) => {
return json;
} catch (error) {
- return Promise.reject(e);
+ return Promise.reject(error);
}
};
@@ -24,7 +24,7 @@ const envJsonToBru = (json) => {
const bru = envJsonToBruV2(json);
return bru;
} catch (error) {
- return Promise.reject(e);
+ return Promise.reject(error);
}
};
@@ -32,7 +32,7 @@ const envJsonToBru = (json) => {
* The transformer function for converting a BRU file to JSON.
*
* We map the json response from the bru lang and transform it into the DSL
- * format that the app users
+ * format that the app uses
*
* @param {string} bru The BRU file content.
* @returns {object} The JSON representation of the BRU file.
diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js
index 42eef5a21..2a62dd969 100644
--- a/packages/bruno-electron/src/index.js
+++ b/packages/bruno-electron/src/index.js
@@ -18,7 +18,7 @@ setContentSecurityPolicy(`
connect-src * 'unsafe-inline';
base-uri 'none';
form-action 'none';
- img-src 'self' data:image/svg+xml
+ img-src 'self' data:image/svg+xml;
`);
const menu = Menu.buildFromTemplate(menuTemplate);
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index cf931a072..7431d4fed 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -18,6 +18,15 @@ const { openCollectionDialog, openCollection } = require('../app/collections');
const { generateUidBasedOnHash } = require('../utils/common');
const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids');
const { setPreferences } = require('../store/preferences');
+const EnvironmentSecretsStore = require('../store/env-secrets');
+
+const environmentSecretsStore = new EnvironmentSecretsStore();
+
+const envHasSecrets = (environment = {}) => {
+ const secrets = _.filter(environment.variables, (v) => v.secret);
+
+ return secrets && secrets.length > 0;
+};
const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
// browse directory
@@ -153,6 +162,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
throw new Error(`environment: ${envFilePath} does not exist`);
}
+ if (envHasSecrets(environment)) {
+ environmentSecretsStore.storeEnvSecrets(collectionPathname, environment);
+ }
+
const content = envJsonToBru(environment);
await writeFile(envFilePath, content);
} catch (error) {
@@ -175,6 +188,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
fs.renameSync(envFilePath, newEnvFilePath);
+
+ environmentSecretsStore.renameEnvironment(collectionPathname, environmentName, newName);
} catch (error) {
return Promise.reject(error);
}
@@ -190,6 +205,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
fs.unlinkSync(envFilePath);
+
+ environmentSecretsStore.deleteEnvironment(collectionPathname, environmentName);
} catch (error) {
return Promise.reject(error);
}
diff --git a/packages/bruno-electron/src/store/env-secrets.js b/packages/bruno-electron/src/store/env-secrets.js
new file mode 100644
index 000000000..b3d26c723
--- /dev/null
+++ b/packages/bruno-electron/src/store/env-secrets.js
@@ -0,0 +1,126 @@
+const _ = require('lodash');
+const Store = require('electron-store');
+const { encryptString } = require('../utils/encryption');
+
+/**
+ * Sample secrets store file
+ *
+ * {
+ * "collections": [{
+ * "path": "/Users/anoop/Code/acme-acpi-collection",
+ * "environments" : [{
+ * "name": "Local",
+ * "secrets": [{
+ * "name": "token",
+ * "value": "abracadabra"
+ * }]
+ * }]
+ * }]
+ * }
+ */
+
+class EnvironmentSecretsStore {
+ constructor() {
+ this.store = new Store({
+ name: 'secrets',
+ clearInvalidConfig: true
+ });
+ }
+
+ isValidValue(val) {
+ return val && typeof val === 'string' && val.length > 0;
+ }
+
+ storeEnvSecrets(collectionPathname, environment) {
+ const envVars = [];
+ _.each(environment.variables, (v) => {
+ if (v.secret) {
+ envVars.push({
+ name: v.name,
+ value: this.isValidValue(v.value) ? encryptString(v.value) : ''
+ });
+ }
+ });
+
+ const collections = this.store.get('collections') || [];
+ const collection = _.find(collections, (c) => c.path === collectionPathname);
+
+ // if collection doesn't exist, create it, add the environment and save
+ if (!collection) {
+ collections.push({
+ path: collectionPathname,
+ environments: [
+ {
+ name: environment.name,
+ secrets: envVars
+ }
+ ]
+ });
+
+ this.store.set('collections', collections);
+ return;
+ }
+
+ // if collection exists, check if environment exists
+ // if environment doesn't exist, add the environment and save
+ collection.environments = collection.environments || [];
+ const env = _.find(collection.environments, (e) => e.name === environment.name);
+ if (!env) {
+ collection.environments.push({
+ name: environment.name,
+ secrets: envVars
+ });
+
+ this.store.set('collections', collections);
+ return;
+ }
+
+ // if environment exists, update the secrets and save
+ env.secrets = envVars;
+ this.store.set('collections', collections);
+ }
+
+ getEnvSecrets(collectionPathname, environment) {
+ const collections = this.store.get('collections') || [];
+ const collection = _.find(collections, (c) => c.path === collectionPathname);
+ if (!collection) {
+ return [];
+ }
+
+ const env = _.find(collection.environments, (e) => e.name === environment.name);
+ if (!env) {
+ return [];
+ }
+
+ return env.secrets || [];
+ }
+
+ renameEnvironment(collectionPathname, oldName, newName) {
+ const collections = this.store.get('collections') || [];
+ const collection = _.find(collections, (c) => c.path === collectionPathname);
+ if (!collection) {
+ return;
+ }
+
+ const env = _.find(collection.environments, (e) => e.name === oldName);
+ if (!env) {
+ return;
+ }
+
+ env.name = newName;
+ this.store.set('collections', collections);
+ }
+
+ deleteEnvironment(collectionPathname, environmentName) {
+ const collections = this.store.get('collections') || [];
+ const collection = _.find(collections, (c) => c.path === collectionPathname);
+ if (!collection) {
+ return;
+ }
+
+ _.remove(collection.environments, (e) => e.name === environmentName);
+ this.store.set('collections', collections);
+ }
+}
+
+module.exports = EnvironmentSecretsStore;
diff --git a/packages/bruno-electron/src/utils/encryption.js b/packages/bruno-electron/src/utils/encryption.js
new file mode 100644
index 000000000..980311ff9
--- /dev/null
+++ b/packages/bruno-electron/src/utils/encryption.js
@@ -0,0 +1,100 @@
+const crypto = require('crypto');
+const { machineIdSync } = require('node-machine-id');
+const { safeStorage } = require('electron');
+
+// Constants for algorithm identification
+const ELECTRONSAFESTORAGE_ALGO = '00';
+const AES256_ALGO = '01';
+
+// AES-256 encryption and decryption functions
+function aes256Encrypt(data) {
+ const key = machineIdSync();
+ const cipher = crypto.createCipher('aes-256-cbc', key);
+ let encrypted = cipher.update(data, 'utf8', 'hex');
+ encrypted += cipher.final('hex');
+
+ return encrypted;
+}
+
+function aes256Decrypt(data) {
+ const key = machineIdSync();
+ const decipher = crypto.createDecipher('aes-256-cbc', key);
+ let decrypted = decipher.update(data, 'hex', 'utf8');
+ decrypted += decipher.final('utf8');
+
+ return decrypted;
+}
+
+// electron safe storage encryption and decryption functions
+function safeStorageEncrypt(str) {
+ let encryptedStringBuffer = safeStorage.encryptString(str);
+
+ // Convert the encrypted buffer to a hexadecimal string
+ const encryptedString = encryptedStringBuffer.toString('hex');
+
+ return encryptedString;
+}
+function safeStorageDecrypt(str) {
+ // Convert the hexadecimal string to a buffer
+ const encryptedStringBuffer = Buffer.from(str, 'hex');
+
+ // Decrypt the buffer
+ const decryptedStringBuffer = safeStorage.decryptString(encryptedStringBuffer);
+
+ // Convert the decrypted buffer to a string
+ const decryptedString = decryptedStringBuffer.toString();
+
+ return decryptedString;
+}
+
+function encryptString(str) {
+ if (!str || typeof str !== 'string' || str.length === 0) {
+ throw new Error('Encrypt failed: invalid string');
+ }
+
+ let encryptedString = '';
+
+ if (safeStorage && safeStorage.isEncryptionAvailable()) {
+ encryptedString = safeStorageEncrypt(str);
+ return `$${ELECTRONSAFESTORAGE_ALGO}:${encryptedString}`;
+ }
+
+ // fallback to aes256
+ encryptedString = aes256Encrypt(str);
+
+ return `$${AES256_ALGO}:${encryptedString}`;
+}
+
+function decryptString(str) {
+ if (!str) {
+ throw new Error('Decrypt failed: unrecognized string format');
+ }
+
+ // Find the index of the first colon
+ const colonIndex = str.indexOf(':');
+
+ if (colonIndex === -1) {
+ throw new Error('Decrypt failed: unrecognized string format');
+ }
+
+ // Extract algo and encryptedString based on the colon index
+ const algo = str.substring(1, colonIndex);
+ const encryptedString = str.substring(colonIndex + 1);
+
+ if ([ELECTRONSAFESTORAGE_ALGO, AES256_ALGO].indexOf(algo) === -1) {
+ throw new Error('Decrypt failed: Invalid algo');
+ }
+
+ if (algo === ELECTRONSAFESTORAGE_ALGO) {
+ return safeStorageDecrypt(encryptedString);
+ }
+
+ if (algo === AES256_ALGO) {
+ return aes256Decrypt(encryptedString);
+ }
+}
+
+module.exports = {
+ encryptString,
+ decryptString
+};
diff --git a/packages/bruno-electron/tests/utils/encryption.spec.js b/packages/bruno-electron/tests/utils/encryption.spec.js
new file mode 100644
index 000000000..b00772284
--- /dev/null
+++ b/packages/bruno-electron/tests/utils/encryption.spec.js
@@ -0,0 +1,31 @@
+const { encryptString, decryptString } = require('../../src/utils/encryption');
+
+// We can only unit test aes 256 fallback as safeStorage is only available
+// in the main process
+
+describe('Encryption and Decryption Tests', () => {
+ it('should encrypt and decrypt using AES-256', () => {
+ const plaintext = 'bruno is awesome';
+ const encrypted = encryptString(plaintext);
+ const decrypted = decryptString(encrypted);
+
+ expect(decrypted).toBe(plaintext);
+ });
+
+ it('encrypt should throw an error for invalid string', () => {
+ expect(() => encryptString(null)).toThrow('Encrypt failed: invalid string');
+ expect(() => encryptString('')).toThrow('Encrypt failed: invalid string');
+ });
+
+ it('decrypt should throw an error for invalid string', () => {
+ expect(() => decryptString(null)).toThrow('Decrypt failed: unrecognized string format');
+ expect(() => decryptString('')).toThrow('Decrypt failed: unrecognized string format');
+ expect(() => decryptString('garbage')).toThrow('Decrypt failed: unrecognized string format');
+ });
+
+ it('decrypt should throw an error for invalid algorithm', () => {
+ const invalidAlgo = '$99:abcdefg';
+
+ expect(() => decryptString(invalidAlgo)).toThrow('Decrypt failed: Invalid algo');
+ });
+});
diff --git a/packages/bruno-lang/v2/src/envToJson.js b/packages/bruno-lang/v2/src/envToJson.js
index 63bd7f2bf..eef4de375 100644
--- a/packages/bruno-lang/v2/src/envToJson.js
+++ b/packages/bruno-lang/v2/src/envToJson.js
@@ -2,7 +2,7 @@ const ohm = require('ohm-js');
const _ = require('lodash');
const grammar = ohm.grammar(`Bru {
- BruEnvFile = (vars)*
+ BruEnvFile = (vars | secretvars)*
nl = "\\r"? "\\n"
st = " " | "\\t"
@@ -19,6 +19,13 @@ const grammar = ohm.grammar(`Bru {
key = keychar*
value = valuechar*
+ // Array Blocks
+ array = st* "[" stnl* valuelist stnl* "]"
+ valuelist = stnl* arrayvalue stnl* ("," stnl* arrayvalue)*
+ arrayvalue = arrayvaluechar*
+ arrayvaluechar = ~(nl | st | "[" | "]" | ",") any
+
+ secretvars = "vars:secret" array
vars = "vars" dictionary
}`);
@@ -44,6 +51,29 @@ const mapPairListToKeyValPairs = (pairList = []) => {
});
};
+const mapArrayListToKeyValPairs = (arrayList = []) => {
+ arrayList = arrayList.filter((v) => v && v.length);
+
+ if (!arrayList.length) {
+ return [];
+ }
+
+ return _.map(arrayList, (value) => {
+ let name = value;
+ let enabled = true;
+ if (name && name.length && name.charAt(0) === '~') {
+ name = name.slice(1);
+ enabled = false;
+ }
+
+ return {
+ name,
+ value: null,
+ enabled
+ };
+ });
+};
+
const concatArrays = (objValue, srcValue) => {
if (_.isArray(objValue) && _.isArray(srcValue)) {
return objValue.concat(srcValue);
@@ -66,6 +96,15 @@ const sem = grammar.createSemantics().addAttribute('ast', {
{}
);
},
+ array(_1, _2, _3, valuelist, _4, _5) {
+ return valuelist.ast;
+ },
+ arrayvalue(chars) {
+ return chars.sourceString ? chars.sourceString.trim() : '';
+ },
+ valuelist(_1, value, _2, _3, _4, rest) {
+ return [value.ast, ...rest.ast];
+ },
dictionary(_1, _2, pairlist, _3) {
return pairlist.ast;
},
@@ -97,6 +136,18 @@ const sem = grammar.createSemantics().addAttribute('ast', {
},
vars(_1, dictionary) {
const vars = mapPairListToKeyValPairs(dictionary.ast);
+ _.each(vars, (v) => {
+ v.secret = false;
+ });
+ return {
+ variables: vars
+ };
+ },
+ secretvars: (_1, array) => {
+ const vars = mapArrayListToKeyValPairs(array.ast);
+ _.each(vars, (v) => {
+ v.secret = true;
+ });
return {
variables: vars
};
diff --git a/packages/bruno-lang/v2/src/jsonToEnv.js b/packages/bruno-lang/v2/src/jsonToEnv.js
index 46106f336..42d0a4281 100644
--- a/packages/bruno-lang/v2/src/jsonToEnv.js
+++ b/packages/bruno-lang/v2/src/jsonToEnv.js
@@ -2,22 +2,42 @@ const _ = require('lodash');
const envToJson = (json) => {
const variables = _.get(json, 'variables', []);
- const vars = variables.map((variable) => {
- const { name, value, enabled } = variable;
- const prefix = enabled ? '' : '~';
- return ` ${prefix}${name}: ${value}`;
- });
+ const vars = variables
+ .filter((variable) => !variable.secret)
+ .map((variable) => {
+ const { name, value, enabled } = variable;
+ const prefix = enabled ? '' : '~';
+ return ` ${prefix}${name}: ${value}`;
+ });
- if (!vars || !vars.length) {
+ const secretVars = variables
+ .filter((variable) => variable.secret)
+ .map((variable) => {
+ const { name, enabled } = variable;
+ const prefix = enabled ? '' : '~';
+ return ` ${prefix}${name}`;
+ });
+
+ if (!variables || !variables.length) {
return `vars {
}
`;
}
- const output = `vars {
+ let output = '';
+ if (vars.length) {
+ output += `vars {
${vars.join('\n')}
}
`;
+ }
+
+ if (secretVars.length) {
+ output += `vars:secret [
+${secretVars.join(',\n')}
+]
+`;
+ }
return output;
};
diff --git a/packages/bruno-lang/v2/tests/envToJson.spec.js b/packages/bruno-lang/v2/tests/envToJson.spec.js
index a082e2cf4..fbb74f2b9 100644
--- a/packages/bruno-lang/v2/tests/envToJson.spec.js
+++ b/packages/bruno-lang/v2/tests/envToJson.spec.js
@@ -26,7 +26,8 @@ vars {
{
name: 'url',
value: 'http://localhost:3000',
- enabled: true
+ enabled: true,
+ secret: false
}
]
};
@@ -48,17 +49,20 @@ vars {
{
name: 'url',
value: 'http://localhost:3000',
- enabled: true
+ enabled: true,
+ secret: false
},
{
name: 'port',
value: '3000',
- enabled: true
+ enabled: true,
+ secret: false
},
{
name: 'token',
value: 'secret',
- enabled: false
+ enabled: false,
+ secret: false
}
]
};
@@ -82,12 +86,14 @@ vars {
{
name: 'url',
value: 'http://localhost:3000',
- enabled: true
+ enabled: true,
+ secret: false
},
{
name: 'port',
value: '3000',
- enabled: true
+ enabled: true,
+ secret: false
}
]
};
@@ -110,17 +116,197 @@ vars {
{
name: 'url',
value: '',
- enabled: true
+ enabled: true,
+ secret: false
},
{
name: 'phone',
value: '',
- enabled: true
+ enabled: true,
+ secret: false
},
{
name: 'api-key',
value: '',
- enabled: true
+ enabled: true,
+ secret: false
+ }
+ ]
+ };
+
+ expect(output).toEqual(expected);
+ });
+
+ it('should parse empty secret vars', () => {
+ const input = `
+vars {
+ url: http://localhost:3000
+}
+
+vars:secret [
+
+]
+`;
+
+ const output = parser(input);
+ const expected = {
+ variables: [
+ {
+ name: 'url',
+ value: 'http://localhost:3000',
+ enabled: true,
+ secret: false
+ }
+ ]
+ };
+
+ expect(output).toEqual(expected);
+ });
+
+ it('should parse secret vars', () => {
+ const input = `
+vars {
+ url: http://localhost:3000
+}
+
+vars:secret [
+ token
+]
+`;
+
+ const output = parser(input);
+ const expected = {
+ variables: [
+ {
+ name: 'url',
+ value: 'http://localhost:3000',
+ enabled: true,
+ secret: false
+ },
+ {
+ name: 'token',
+ value: null,
+ enabled: true,
+ secret: true
+ }
+ ]
+ };
+
+ expect(output).toEqual(expected);
+ });
+
+ it('should parse multiline secret vars', () => {
+ const input = `
+vars {
+ url: http://localhost:3000
+}
+
+vars:secret [
+ access_token,
+ access_secret,
+
+ ~access_password
+]
+`;
+
+ const output = parser(input);
+ const expected = {
+ variables: [
+ {
+ name: 'url',
+ value: 'http://localhost:3000',
+ enabled: true,
+ secret: false
+ },
+ {
+ name: 'access_token',
+ value: null,
+ enabled: true,
+ secret: true
+ },
+ {
+ name: 'access_secret',
+ value: null,
+ enabled: true,
+ secret: true
+ },
+ {
+ name: 'access_password',
+ value: null,
+ enabled: false,
+ secret: true
+ }
+ ]
+ };
+
+ expect(output).toEqual(expected);
+ });
+
+ it('should parse inline secret vars', () => {
+ const input = `
+vars {
+ url: http://localhost:3000
+}
+
+vars:secret [access_key]
+`;
+
+ const output = parser(input);
+ const expected = {
+ variables: [
+ {
+ name: 'url',
+ value: 'http://localhost:3000',
+ enabled: true,
+ secret: false
+ },
+ {
+ name: 'access_key',
+ value: null,
+ enabled: true,
+ secret: true
+ }
+ ]
+ };
+
+ expect(output).toEqual(expected);
+ });
+
+ it('should parse inline multiple secret vars', () => {
+ const input = `
+vars {
+ url: http://localhost:3000
+}
+
+vars:secret [access_key,access_secret, access_password ]
+`;
+
+ const output = parser(input);
+ const expected = {
+ variables: [
+ {
+ name: 'url',
+ value: 'http://localhost:3000',
+ enabled: true,
+ secret: false
+ },
+ {
+ name: 'access_key',
+ value: null,
+ enabled: true,
+ secret: true
+ },
+ {
+ name: 'access_secret',
+ value: null,
+ enabled: true,
+ secret: true
+ },
+ {
+ name: 'access_password',
+ value: null,
+ enabled: true,
+ secret: true
}
]
};
diff --git a/packages/bruno-lang/v2/tests/jsonToEnv.spec.js b/packages/bruno-lang/v2/tests/jsonToEnv.spec.js
index 7aee11428..62b7aa269 100644
--- a/packages/bruno-lang/v2/tests/jsonToEnv.spec.js
+++ b/packages/bruno-lang/v2/tests/jsonToEnv.spec.js
@@ -57,4 +57,87 @@ describe('env parser', () => {
const output = parser(input);
expect(output).toEqual(expected);
});
+
+ it('should parse secret vars', () => {
+ const input = {
+ variables: [
+ {
+ name: 'url',
+ value: 'http://localhost:3000',
+ enabled: true
+ },
+ {
+ name: 'token',
+ value: 'abracadabra',
+ enabled: true,
+ secret: true
+ }
+ ]
+ };
+
+ const output = parser(input);
+ const expected = `vars {
+ url: http://localhost:3000
+}
+vars:secret [
+ token
+]
+`;
+ expect(output).toEqual(expected);
+ });
+
+ it('should parse multiple secret vars', () => {
+ const input = {
+ variables: [
+ {
+ name: 'url',
+ value: 'http://localhost:3000',
+ enabled: true
+ },
+ {
+ name: 'access_token',
+ value: 'abracadabra',
+ enabled: true,
+ secret: true
+ },
+ {
+ name: 'access_secret',
+ value: 'abracadabra',
+ enabled: false,
+ secret: true
+ }
+ ]
+ };
+
+ const output = parser(input);
+ const expected = `vars {
+ url: http://localhost:3000
+}
+vars:secret [
+ access_token,
+ ~access_secret
+]
+`;
+ expect(output).toEqual(expected);
+ });
+
+ it('should parse even if the only secret vars are present', () => {
+ const input = {
+ variables: [
+ {
+ name: 'token',
+ value: 'abracadabra',
+ enabled: true,
+ secret: true
+ }
+ ]
+ };
+
+ const output = parser(input);
+ const expected = `vars:secret [
+ token
+]
+`;
+ expect(output).toEqual(expected);
+ });
});
diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js
index fb5195488..ba2256a53 100644
--- a/packages/bruno-schema/src/collections/index.js
+++ b/packages/bruno-schema/src/collections/index.js
@@ -6,7 +6,8 @@ const environmentVariablesSchema = Yup.object({
name: Yup.string().nullable(),
value: Yup.string().nullable(),
type: Yup.string().oneOf(['text']).required('type is required'),
- enabled: Yup.boolean().defined()
+ enabled: Yup.boolean().defined(),
+ secret: Yup.boolean()
})
.noUnknown(true)
.strict();