Skip to content
Merged
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: 22 additions & 7 deletions lib/utils/error-message.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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=<code> 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=<code> 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
Expand Down Expand Up @@ -369,6 +382,7 @@ const errorMessage = (er, npm) => {
summary,
detail,
files,
json,
}
}

Expand Down Expand Up @@ -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,
Expand All @@ -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])]),
Expand Down
1 change: 1 addition & 0 deletions lib/utils/output-error.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
}
Expand Down
37 changes: 37 additions & 0 deletions tap-snapshots/test/lib/utils/error-message.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 [
Expand Down
29 changes: 29 additions & 0 deletions test/lib/cli/exit-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
11 changes: 11 additions & 0 deletions test/lib/utils/error-message.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"',
Expand Down
Loading