import fs from 'node:fs';
import { ClientRequest } from 'node:http';
import ghauth from 'ghauth';
import { clearCachedConfig, encryptValue, getMergedConfig, getNcurcPath } from './config.js';
export default lazy(auth);
function check(username, token, format = /^[A-Za-z0-9\-_.]+$/) {
if (typeof username !== 'string') {
throw new Error(`username must be a string, received ${typeof username}`);
}
if (!/^[a-zA-Z0-9-]+$/.test(username)) {
throw new Error(
'username may only contain alphanumeric characters or hyphens, ' +
`received ${username}`
);
}
if (typeof token !== 'string') {
throw new Error(`token must be a string, received ${typeof token}`);
}
if (!format.test(token)) {
throw new Error(`token is misformatted: ${token}`);
}
}
function lazy(fn) {
let cachedValue;
return function(...args) {
if (cachedValue !== undefined) {
return cachedValue;
}
cachedValue = fn(...args);
return cachedValue;
};
}
async function tryCreateGitHubToken(githubAuth) {
let credentials;
try {
credentials = await githubAuth({
noSave: true,
scopes: ['user:email', 'read:org'],
note: 'node-core-utils CLI tools',
noDeviceFlow: true
});
} catch (e) {
throw new Error(`Could not get token: ${e.message}`, { cause: e });
}
return credentials;
}
function encode(name, token) {
return Buffer.from(`${name}:${token}`).toString('base64');
}
function setOwnProperty(target, key, value) {
return Object.defineProperty(target, key, {
__proto__: null,
configurable: true,
enumerable: true,
value
});
}
// TODO: support jenkins only...or not necessary?
// TODO: make this a class with dependency (CLI) injectable for testing
async function auth(
options = { github: true },
githubAuth = ghauth) {
const result = {
get github() {
let username;
let token;
try {
({ username, token } = getMergedConfig());
} catch {
// Ignore error and prompt
}
check(username, token);
const github = encode(username, token);
setOwnProperty(result, 'github', github);
return github;
},
get jenkins() {
const { username, jenkins_token } = getMergedConfig();
if (!username || !jenkins_token) {
throw new Error(
'Get your Jenkins API token in https://ci.nodejs.org/me/security ' +
'and run the following command to add it to your ncu config: ' +
'ncu-config --global set -x jenkins_token'
);
}
check(username, jenkins_token);
const jenkins = encode(username, jenkins_token);
setOwnProperty(result, 'jenkins', jenkins);
return jenkins;
},
get h1() {
const { h1_username, h1_token } = getMergedConfig();
check(h1_username, h1_token, /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/);
const h1 = encode(h1_username, h1_token);
setOwnProperty(result, 'h1', h1);
return h1;
},
// OpenJS Foundation CNA bucket token and Worker URL. Only used when
// `cve_source` is `openjs-cna`; HackerOne stays the bug-bounty source.
get cna() {
const { openjs_cna_token, openjs_cna_worker_url } = getMergedConfig();
if (!openjs_cna_token || !openjs_cna_worker_url) {
throw new Error(
'OpenJS CNA credentials are not configured. Run:\n' +
' ncu-config --global set -x openjs_cna_token <bucket:secret>\n' +
' ncu-config --global set openjs_cna_worker_url <https://...workers.dev>'
);
}
if (!/^[a-z0-9_]+:[0-9a-f]+$/i.test(openjs_cna_token)) {
throw new Error(
'openjs_cna_token is misformatted; expected `<bucket>:<hex-secret>`'
);
}
if (!/^https:\/\//.test(openjs_cna_worker_url)) {
throw new Error(
'openjs_cna_worker_url is misformatted; expected an https:// URL'
);
}
const cna = {
token: openjs_cna_token,
worker_url: openjs_cna_worker_url.replace(/\/+$/, '')
};
setOwnProperty(result, 'cna', cna);
return cna;
}
};
if (options.github) {
let config;
try {
config = getMergedConfig();
} catch {
config = {};
}
if (!Object.hasOwn(config, 'token') || !Object.hasOwn(config, 'username')) {
process.stdout.write(
'If this is your first time running this command, ' +
'follow the instructions to create an access token' +
'. If you prefer to create it yourself on Github, ' +
'see https://github.com/nodejs/node-core-utils/blob/main/README.md.\n');
const credentials = await tryCreateGitHubToken(githubAuth);
const username = credentials.user;
let token;
try {
token = await encryptValue(credentials.token);
} catch {
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 */
});
// Try again reading the file
clearCachedConfig();
}
}
return result;
}
// This is an ugly hack to get around a bug in hyperquest & ghauth
// which are not currently maintained
const originalSetTimeout = ClientRequest.prototype.setTimeout;
ClientRequest.prototype.setTimeout = function(msecs, ...args) {
msecs = Math.min(msecs, Math.pow(2, 31) - 1);
return originalSetTimeout.call(this, msecs, ...args);
};