Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions bin/ncu-config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
#!/usr/bin/env node

import * as readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';

import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';

import {
getConfig, updateConfig, GLOBAL_CONFIG, PROJECT_CONFIG, LOCAL_CONFIG
getConfig, updateConfig, GLOBAL_CONFIG, PROJECT_CONFIG, LOCAL_CONFIG,
encryptValue
} from '../lib/config.js';
import { setVerbosityFromEnv } from '../lib/verbosity.js';

Expand All @@ -13,10 +17,15 @@ setVerbosityFromEnv();
const args = yargs(hideBin(process.argv))
.completion('completion')
.command({
command: 'set <key> <value>',
command: 'set <key> [<value>]',
desc: 'Set a config variable',
builder: (yargs) => {
yargs
.option('encrypt', {
describe: 'Store the value encrypted using gpg',
alias: 'x',
type: 'boolean'
})
.positional('key', {
describe: 'key of the configuration',
type: 'string'
Expand Down Expand Up @@ -61,8 +70,6 @@ const args = yargs(hideBin(process.argv))
.conflicts('global', 'project')
.help();

const argv = args.parse();

function getConfigType(argv) {
if (argv.global) {
return { configName: 'global', configType: GLOBAL_CONFIG };
Expand All @@ -73,9 +80,19 @@ function getConfigType(argv) {
return { configName: 'local', configType: LOCAL_CONFIG };
}

function setHandler(argv) {
async function setHandler(argv) {
const { configName, configType } = getConfigType(argv);
const config = getConfig(configType);
if (!argv.value) {
const rl = readline.createInterface({ input, output });
argv.value = await rl.question('What value do you want to set? ');
rl.close();
} else if (argv.encrypt) {
console.warn('Passing sensitive config value via the shell is discouraged');
}
if (argv.encrypt) {
argv.value = await encryptValue(argv.value);
}
console.log(
`Updating ${configName} configuration ` +
`[${argv.key}]: ${config[argv.key]} -> ${argv.value}`);
Expand All @@ -96,6 +113,8 @@ function listHandler(argv) {
}
}

const argv = await args.parse();

if (!['get', 'set', 'list'].includes(argv._[0])) {
args.showHelp();
}
9 changes: 7 additions & 2 deletions lib/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ClientRequest } from 'node:http';

import ghauth from 'ghauth';

import { clearCachedConfig, getMergedConfig, getNcurcPath } from './config.js';
import { clearCachedConfig, encryptValue, getMergedConfig, getNcurcPath } from './config.js';

export default lazy(auth);

Expand Down Expand Up @@ -83,7 +83,12 @@ async function auth(
'see https://github.com/nodejs/node-core-utils/blob/main/README.md.\n');
const credentials = await tryCreateGitHubToken(githubAuth);
username = credentials.user;
token = credentials.token;
try {
token = await encryptValue(credentials.token);
} catch (err) {
console.warn('Failed encrypt token, storing unencrypted instead');
token = credentials.token;
}
const json = JSON.stringify({ username, token }, null, 2);
fs.writeFileSync(getNcurcPath(), json, {
mode: 0o600 /* owner read/write */
Expand Down
47 changes: 46 additions & 1 deletion lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import os from 'node:os';
import { readJson, writeJson } from './file.js';
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { forceRunAsync, runSync } from './run.js';

export const GLOBAL_CONFIG = Symbol('globalConfig');
export const PROJECT_CONFIG = Symbol('projectConfig');
Expand Down Expand Up @@ -32,6 +33,46 @@ export function clearCachedConfig() {
mergedConfig = null;
}

export async function encryptValue(input) {
console.warn('Spawning gpg to encrypt the config value');
return forceRunAsync(
process.env.GPG_BIN || 'gpg',
['--default-recipient-self', '--encrypt', '--armor'],
{
captureStdout: true,
ignoreFailure: false,
spawnArgs: { input }
}
);
}

function setOwnProperty(target, key, value) {
return Object.defineProperty(target, key, {
__proto__: null,
configurable: true,
enumerable: true,
value
});
}
function addEncryptedPropertyGetter(target, key, input) {
if (input?.startsWith?.('-----BEGIN PGP MESSAGE-----\n')) {
return Object.defineProperty(target, key, {
__proto__: null,
configurable: true,
get() {
console.warn(`The config value for ${key} is encrypted, spawning gpg to decrypt it...`);
const value = runSync(process.env.GPG_BIN || 'gpg', ['--decrypt'], { input });
setOwnProperty(target, key, value);
return value;
},
set(newValue) {
addEncryptedPropertyGetter(target, key, newValue) ||
setOwnProperty(target, key, newValue);
}
});
}
}

export function getConfig(configType, dir) {
const configPath = getConfigPath(configType, dir);
const encryptedConfigPath = configPath + '.gpg';
Expand All @@ -44,7 +85,11 @@ export function getConfig(configType, dir) {
}
}
try {
return readJson(configPath);
const json = readJson(configPath);
for (const [key, val] of Object.entries(json)) {
addEncryptedPropertyGetter(json, key, val);
}
return json;
} catch (cause) {
throw new Error('Unable to parse config file ' + configPath, { cause });
}
Expand Down
13 changes: 8 additions & 5 deletions test/unit/auth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ describe('auth', async function() {
it('asks for auth data if no ncurc is found', async function() {
await runAuthScript(
undefined,
[FIRST_TIME_MSG, MOCKED_TOKEN]
[FIRST_TIME_MSG, MOCKED_TOKEN],
/^Spawning gpg to encrypt the config value\r?\nError: spawn do-not-exist ENOENT(?:.*\n)+Failed encrypt token, storing unencrypted instead\r?\n$/
);
});

it('asks for auth data if ncurc is invalid json', async function() {
await runAuthScript(
{ HOME: 'this is not json' },
[FIRST_TIME_MSG, MOCKED_TOKEN]
[FIRST_TIME_MSG, MOCKED_TOKEN],
/^Spawning gpg to encrypt the config value\r?\nError: spawn do-not-exist ENOENT(?:.*\n)+Failed encrypt token, storing unencrypted instead\r?\n$/
);
});

Expand Down Expand Up @@ -117,7 +119,7 @@ describe('auth', async function() {
function runAuthScript(
ncurc = {}, expect = [], error = '', fixture = 'run-auth-github') {
return new Promise((resolve, reject) => {
const newEnv = { HOME: undefined, XDG_CONFIG_HOME: undefined };
const newEnv = { HOME: undefined, XDG_CONFIG_HOME: undefined, GPG_BIN: 'do-not-exist' };
if (ncurc.HOME === undefined) ncurc.HOME = ''; // HOME must always be set.
for (const envVar in ncurc) {
if (ncurc[envVar] === undefined) continue;
Expand Down Expand Up @@ -154,8 +156,9 @@ function runAuthScript(
});
proc.on('close', () => {
try {
assert.strictEqual(stderr, error);
assert.strictEqual(expect.length, 0);
if (typeof error === 'string') assert.strictEqual(stderr, error);
else assert.match(stderr, error);
assert.deepStrictEqual(expect, []);
if (newEnv.HOME) {
fs.rmSync(newEnv.HOME, { recursive: true, force: true });
}
Expand Down
Loading