Skip to content

Commit 8afd57b

Browse files
committed
feat(ncu-config): add support for partially encrypted config files
1 parent 2589b9c commit 8afd57b

File tree

2 files changed

+58
-5
lines changed

2 files changed

+58
-5
lines changed

bin/ncu-config.js

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,31 @@
11
#!/usr/bin/env node
22

3+
import * as readline from 'node:readline/promises';
4+
import { stdin as input, stdout as output } from 'node:process';
5+
36
import yargs from 'yargs';
47
import { hideBin } from 'yargs/helpers';
58

69
import {
710
getConfig, updateConfig, GLOBAL_CONFIG, PROJECT_CONFIG, LOCAL_CONFIG
811
} from '../lib/config.js';
912
import { setVerbosityFromEnv } from '../lib/verbosity.js';
13+
import { runSync } from '../lib/run.js';
1014

1115
setVerbosityFromEnv();
1216

1317
const args = yargs(hideBin(process.argv))
1418
.completion('completion')
1519
.command({
16-
command: 'set <key> <value>',
20+
command: 'set <key> [<value>]',
1721
desc: 'Set a config variable',
1822
builder: (yargs) => {
1923
yargs
24+
.option('encrypt', {
25+
describe: 'Store the value encrypted using gpg',
26+
alias: 'x',
27+
type: 'boolean'
28+
})
2029
.positional('key', {
2130
describe: 'key of the configuration',
2231
type: 'string'
@@ -61,8 +70,6 @@ const args = yargs(hideBin(process.argv))
6170
.conflicts('global', 'project')
6271
.help();
6372

64-
const argv = args.parse();
65-
6673
function getConfigType(argv) {
6774
if (argv.global) {
6875
return { configName: 'global', configType: GLOBAL_CONFIG };
@@ -73,9 +80,21 @@ function getConfigType(argv) {
7380
return { configName: 'local', configType: LOCAL_CONFIG };
7481
}
7582

76-
function setHandler(argv) {
83+
async function setHandler(argv) {
7784
const { configName, configType } = getConfigType(argv);
7885
const config = getConfig(configType);
86+
if (!argv.value) {
87+
const rl = readline.createInterface({ input, output });
88+
argv.value = await rl.question('What value do you want to set? ');
89+
rl.close();
90+
} else if (argv.encrypt) {
91+
console.warn('Passing sensitive config value via the shell is discouraged');
92+
}
93+
if (argv.encrypt) {
94+
argv.value = runSync('gpg', ['--default-recipient-self', '--encrypt', '--armor'], {
95+
input: argv.value
96+
});
97+
}
7998
console.log(
8099
`Updating ${configName} configuration ` +
81100
`[${argv.key}]: ${config[argv.key]} -> ${argv.value}`);
@@ -96,6 +115,8 @@ function listHandler(argv) {
96115
}
97116
}
98117

118+
const argv = await args.parse();
119+
99120
if (!['get', 'set', 'list'].includes(argv._[0])) {
100121
args.showHelp();
101122
}

lib/config.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import os from 'node:os';
44
import { readJson, writeJson } from './file.js';
55
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
66
import { spawnSync } from 'node:child_process';
7+
import { runSync } from './run.js';
78

89
export const GLOBAL_CONFIG = Symbol('globalConfig');
910
export const PROJECT_CONFIG = Symbol('projectConfig');
@@ -32,6 +33,33 @@ export function clearCachedConfig() {
3233
mergedConfig = null;
3334
}
3435

36+
function setOwnProperty(target, key, value) {
37+
return Object.defineProperty(target, key, {
38+
__proto__: null,
39+
configurable: true,
40+
enumerable: true,
41+
value
42+
});
43+
}
44+
function addEncryptedPropertyGetter(target, key, input) {
45+
if (input.startsWith('-----BEGIN PGP MESSAGE-----\n')) {
46+
return Object.defineProperty(target, key, {
47+
__proto__: null,
48+
configurable: true,
49+
get() {
50+
console.warn(`The config value for ${key} is encrypted, spawning gpg to decrypt it...`);
51+
const value = runSync('gpg', ['--decrypt'], { input });
52+
setOwnProperty(target, key, value);
53+
return value;
54+
},
55+
set(newValue) {
56+
addEncryptedPropertyGetter(target, key, newValue) ||
57+
setOwnProperty(target, key, newValue);
58+
}
59+
});
60+
}
61+
}
62+
3563
export function getConfig(configType, dir) {
3664
const configPath = getConfigPath(configType, dir);
3765
const encryptedConfigPath = configPath + '.gpg';
@@ -44,7 +72,11 @@ export function getConfig(configType, dir) {
4472
}
4573
}
4674
try {
47-
return readJson(configPath);
75+
const json = readJson(configPath);
76+
for (const [key, val] of Object.entries(json)) {
77+
addEncryptedPropertyGetter(json, key, val);
78+
}
79+
return json;
4880
} catch (cause) {
4981
throw new Error('Unable to parse config file ' + configPath, { cause });
5082
}

0 commit comments

Comments
 (0)