diff --git a/lib/utils/error-message.js b/lib/utils/error-message.js index 50d15d7bc9a37..9faf4c339b1a5 100644 --- a/lib/utils/error-message.js +++ b/lib/utils/error-message.js @@ -7,6 +7,7 @@ const errorMessage = (er, npm) => { const summary = [] const detail = [] const files = [] + let json er.message &&= replaceInfo(er.message) er.stack &&= replaceInfo(er.stack) @@ -123,12 +124,24 @@ const errorMessage = (er, npm) => { case 'E401': // E401 is for places where we accidentally neglect OTP stuff if (er.code === 'EOTP' || /one-time pass/.test(er.message)) { - summary.push(['', 'This operation requires a one-time password from your authenticator.']) - detail.push(['', [ - 'You can provide a one-time password by passing --otp= to the command you ran.', - 'If you already provided a one-time password then it is likely that you either typoed', - 'it, or it timed out. Please try again.', - ].join('\n')]) + const authUrl = er.body?.authUrl + const doneUrl = er.body?.doneUrl + if (authUrl && doneUrl) { + json = { authUrl, doneUrl } + summary.push(['', 'This operation requires a one-time password.']) + detail.push(['', `Open this URL in your browser to authenticate:`]) + detail.push(['', ` ${authUrl}`]) + detail.push(['', '']) + detail.push(['', `After authenticating, your token can be retrieved from:`]) + detail.push(['', ` ${doneUrl}`]) + } else { + summary.push(['', 'This operation requires a one-time password from your authenticator.']) + detail.push(['', [ + 'You can provide a one-time password by passing --otp= to the command you ran.', + 'If you already provided a one-time password then it is likely that you either typoed', + 'it, or it timed out. Please try again.', + ].join('\n')]) + } } else { // npm ERR! code E401 // npm ERR! Unable to authenticate, need: Basic @@ -369,6 +382,7 @@ const errorMessage = (er, npm) => { summary, detail, files, + json, } } @@ -430,7 +444,7 @@ const getError = (err, { npm, command, pkg }) => { // so they have redacted information err.code ??= err.message.match(/^(?:Error: )?(E[A-Z]+)/)?.[1] // this mutates the error and redacts stack/message - const { summary, detail, files } = errorMessage(err, npm) + const { summary, detail, files, json } = errorMessage(err, npm) return { err, @@ -440,6 +454,7 @@ const getError = (err, { npm, command, pkg }) => { summary, detail, files, + json, verbose: ['type', 'stack', 'statusCode', 'pkgid'] .filter(k => err[k]) .map(k => [k, replaceInfo(err[k])]), diff --git a/lib/utils/output-error.js b/lib/utils/output-error.js index 27128e9f03a8c..07a86c9718b90 100644 --- a/lib/utils/output-error.js +++ b/lib/utils/output-error.js @@ -19,6 +19,7 @@ const jsonError = (error, npm) => { code: error.code, summary: (error.summary || []).map(l => l.slice(1).join(' ')).join('\n').trim(), detail: (error.detail || []).map(l => l.slice(1).join(' ')).join('\n').trim(), + ...error.json, } } } diff --git a/tap-snapshots/test/lib/utils/error-message.js.test.cjs b/tap-snapshots/test/lib/utils/error-message.js.test.cjs index af91a3ce0427c..c180cb0f28377 100644 --- a/tap-snapshots/test/lib/utils/error-message.js.test.cjs +++ b/tap-snapshots/test/lib/utils/error-message.js.test.cjs @@ -1015,6 +1015,43 @@ Object { } ` +exports[`test/lib/utils/error-message.js TAP eotp/e401 one-time pass webauth challenge > must match snapshot 1`] = ` +Object { + "detail": Array [ + Array [ + "", + "Open this URL in your browser to authenticate:", + ], + Array [ + "", + " https://registry.npmjs.org/-/auth/login/abc123", + ], + Array [ + "", + "", + ], + Array [ + "", + "After authenticating, your token can be retrieved from:", + ], + Array [ + "", + " https://registry.npmjs.org/-/auth/done/abc123", + ], + ], + "json": Object { + "authUrl": "https://registry.npmjs.org/-/auth/login/abc123", + "doneUrl": "https://registry.npmjs.org/-/auth/done/abc123", + }, + "summary": Array [ + Array [ + "", + "This operation requires a one-time password.", + ], + ], +} +` + exports[`test/lib/utils/error-message.js TAP eotp/e401 www-authenticate challenges Basic realm=by, charset="UTF-8", challenge="your friends" > must match snapshot 1`] = ` Object { "detail": Array [ diff --git a/test/lib/cli/exit-handler.js b/test/lib/cli/exit-handler.js index c63141286a3f0..0deae15428ff5 100644 --- a/test/lib/cli/exit-handler.js +++ b/test/lib/cli/exit-handler.js @@ -296,6 +296,35 @@ t.test('merges output buffers errors with --json', async (t) => { ) }) +t.test('json output includes authUrl and doneUrl for webauth EOTP errors', async (t) => { + const { exitHandler, outputs } = await mockExitHandler(t, { + config: { json: true }, + error: Object.assign(new Error('one-time pass required'), { + code: 'EOTP', + body: { + authUrl: 'https://registry.npmjs.org/-/auth/login/abc123', + doneUrl: 'https://registry.npmjs.org/-/auth/done/abc123', + }, + }), + }) + + await exitHandler() + + t.equal(process.exitCode, 1) + const jsonOutput = JSON.parse(outputs[0]) + t.same(jsonOutput.error, { + code: 'EOTP', + summary: 'This operation requires a one-time password.', + detail: 'Open this URL in your browser to authenticate:\n' + + ' https://registry.npmjs.org/-/auth/login/abc123\n' + + '\n' + + 'After authenticating, your token can be retrieved from:\n' + + ' https://registry.npmjs.org/-/auth/done/abc123', + authUrl: 'https://registry.npmjs.org/-/auth/login/abc123', + doneUrl: 'https://registry.npmjs.org/-/auth/done/abc123', + }) +}) + t.test('output buffer without json', async (t) => { const { exitHandler, outputs, logs } = await mockExitHandler(t, { error: err('Error: EBADTHING Something happened'), diff --git a/test/lib/utils/error-message.js b/test/lib/utils/error-message.js index 380e8611645e9..44a57eca645d7 100644 --- a/test/lib/utils/error-message.js +++ b/test/lib/utils/error-message.js @@ -281,6 +281,17 @@ t.test('eotp/e401', async t => { t.end() }) + t.test('one-time pass webauth challenge', t => { + t.matchSnapshot(errorMessage(Object.assign(new Error('nope'), { + code: 'EOTP', + body: { + authUrl: 'https://registry.npmjs.org/-/auth/login/abc123', + doneUrl: 'https://registry.npmjs.org/-/auth/done/abc123', + }, + }))) + t.end() + }) + t.test('www-authenticate challenges', t => { const auths = [ 'Bearer realm=do, charset="UTF-8", challenge="yourself"',