Skip to content

Commit f072456

Browse files
authored
Merge pull request #45 from SocketDev/ENG-1496/cli-login
CLI login/logout
2 parents e6280b3 + 21e4377 commit f072456

File tree

7 files changed

+169
-9
lines changed

7 files changed

+169
-9
lines changed

lib/commands/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ export * from './info/index.js'
22
export * from './report/index.js'
33
export * from './npm/index.js'
44
export * from './npx/index.js'
5+
export * from './login/index.js'
6+
export * from './logout/index.js'

lib/commands/login/index.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import isInteractive from 'is-interactive'
2+
import meow from 'meow'
3+
import ora from 'ora'
4+
import prompts from 'prompts'
5+
6+
import { ChalkOrMarkdown } from '../../utils/chalk-markdown.js'
7+
import { AuthError, InputError } from '../../utils/errors.js'
8+
import { setupSdk } from '../../utils/sdk.js'
9+
import { getSetting, updateSetting } from '../../utils/settings.js'
10+
11+
const description = 'Socket API login'
12+
13+
/** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
14+
export const login = {
15+
description,
16+
run: async (argv, importMeta, { parentName }) => {
17+
const name = parentName + ' login'
18+
const cli = meow(`
19+
Usage
20+
$ ${name}
21+
22+
Logs into the Socket API by prompting for an API key
23+
24+
Examples
25+
$ ${name}
26+
`, {
27+
argv,
28+
description,
29+
importMeta,
30+
})
31+
32+
if (cli.input.length) cli.showHelp()
33+
34+
if (!isInteractive()) {
35+
throw new InputError('cannot prompt for credentials in a non-interactive shell')
36+
}
37+
const format = new ChalkOrMarkdown(false)
38+
const { apiKey } = await prompts({
39+
type: 'password',
40+
name: 'apiKey',
41+
message: `Enter your ${format.hyperlink(
42+
'Socket.dev API key',
43+
'https://docs.socket.dev/docs/api-keys'
44+
)}`,
45+
})
46+
47+
if (!apiKey) {
48+
ora('API key not updated').warn()
49+
return
50+
}
51+
52+
const spinner = ora('Verifying API key...').start()
53+
54+
const oldKey = getSetting('apiKey')
55+
updateSetting('apiKey', apiKey)
56+
try {
57+
const sdk = await setupSdk()
58+
const quota = await sdk.getQuota()
59+
if (!quota.success) throw new AuthError()
60+
spinner.succeed(`API key ${oldKey ? 'updated' : 'set'}`)
61+
} catch (e) {
62+
updateSetting('apiKey', oldKey)
63+
spinner.fail('Invalid API key')
64+
}
65+
}
66+
}

lib/commands/logout/index.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import meow from 'meow'
2+
import ora from 'ora'
3+
4+
import { updateSetting } from '../../utils/settings.js'
5+
6+
const description = 'Socket API logout'
7+
8+
/** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
9+
export const logout = {
10+
description,
11+
run: async (argv, importMeta, { parentName }) => {
12+
const name = parentName + ' logout'
13+
const cli = meow(`
14+
Usage
15+
$ ${name}
16+
17+
Logs out of the Socket API and clears all Socket credentials from disk
18+
19+
Examples
20+
$ ${name}
21+
`, {
22+
argv,
23+
description,
24+
importMeta,
25+
})
26+
27+
if (cli.input.length) cli.showHelp()
28+
29+
updateSetting('apiKey', null)
30+
ora('Successfully logged out').succeed()
31+
}
32+
}

lib/shadow/npm-injection.cjs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const oraPromise = import('ora')
1212
const isInteractivePromise = import('is-interactive')
1313
const chalkPromise = import('chalk')
1414
const chalkMarkdownPromise = import('../utils/chalk-markdown.js')
15+
const settingsPromise = import('../utils/settings.js')
1516
const ipc_version = require('../../package.json').version
1617

1718
try {
@@ -32,7 +33,9 @@ try {
3233
* @typedef {import('stream').Writable} Writable
3334
*/
3435

35-
const pubToken = 'sktsec_t_--RAN5U4ivauy4w37-6aoKyYPDt5ZbaT5JBVMqiwKo_api'
36+
const pubTokenPromise = settingsPromise.then(({ getSetting }) =>
37+
getSetting('apiKey') || 'sktsec_t_--RAN5U4ivauy4w37-6aoKyYPDt5ZbaT5JBVMqiwKo_api'
38+
);
3639

3740
// shadow `npm` and `npx` to mitigate subshells
3841
require('./link.cjs')(fs.realpathSync(path.join(__dirname, 'bin')), 'npm')
@@ -64,6 +67,7 @@ const pkgidParts = (pkgid) => {
6467
async function * batchScan (
6568
pkgids
6669
) {
70+
const pubToken = await pubTokenPromise
6771
const query = {
6872
packages: pkgids.map(pkgid => {
6973
const { name, version } = pkgidParts(pkgid)

lib/utils/sdk.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,27 @@ import isInteractive from 'is-interactive'
77
import prompts from 'prompts'
88

99
import { AuthError } from './errors.js'
10+
import { getSetting } from './settings.js'
1011

1112
/**
12-
* The API key should be stored globally for the duration of the CLI execution
13+
* This API key should be stored globally for the duration of the CLI execution
1314
*
1415
* @type {string | undefined}
1516
*/
16-
let apiKey
17+
let sessionAPIKey
1718

1819
/** @returns {Promise<import('@socketsecurity/sdk').SocketSdk>} */
1920
export async function setupSdk () {
20-
if (!apiKey) {
21-
apiKey = process.env['SOCKET_SECURITY_API_KEY']
22-
}
21+
let apiKey = getSetting('apiKey') || process.env['SOCKET_SECURITY_API_KEY'] || sessionAPIKey
2322

2423
if (!apiKey && isInteractive()) {
2524
const input = await prompts({
2625
type: 'password',
2726
name: 'apiKey',
28-
message: 'Enter your Socket.dev API key',
27+
message: 'Enter your Socket.dev API key (not saved)',
2928
})
3029

31-
apiKey = input.apiKey
30+
apiKey = sessionAPIKey = input.apiKey
3231
}
3332

3433
if (!apiKey) {

lib/utils/settings.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import * as fs from 'fs'
2+
import * as os from 'os'
3+
import * as path from 'path'
4+
5+
import ora from 'ora'
6+
7+
let dataHome = process.platform === 'win32'
8+
? process.env['LOCALAPPDATA']
9+
: process.env['XDG_DATA_HOME']
10+
11+
if (!dataHome) {
12+
if (process.platform === 'win32') throw new Error('missing %LOCALAPPDATA%')
13+
const home = os.homedir()
14+
dataHome = path.join(home, ...(process.platform === 'darwin'
15+
? ['Library', 'Application Support']
16+
: ['.local', 'share']
17+
))
18+
}
19+
20+
const settingsPath = path.join(dataHome, 'socket', 'settings')
21+
22+
/** @type {{apiKey?: string | null}} */
23+
let settings = {}
24+
25+
if (fs.existsSync(settingsPath)) {
26+
const raw = fs.readFileSync(settingsPath, 'utf-8')
27+
try {
28+
settings = JSON.parse(Buffer.from(raw, 'base64').toString())
29+
} catch (e) {
30+
ora(`Failed to parse settings at ${settingsPath}`).warn()
31+
}
32+
} else {
33+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
34+
}
35+
36+
/**
37+
* @template {keyof typeof settings} Key
38+
* @param {Key} key
39+
* @returns {typeof settings[Key]}
40+
*/
41+
export function getSetting (key) {
42+
return settings[key]
43+
}
44+
45+
/**
46+
* @template {keyof typeof settings} Key
47+
* @param {Key} key
48+
* @param {typeof settings[Key]} value
49+
* @returns {void}
50+
*/
51+
export function updateSetting (key, value) {
52+
settings[key] = value
53+
fs.writeFileSync(
54+
settingsPath,
55+
Buffer.from(JSON.stringify(settings)).toString('base64')
56+
)
57+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"test": "run-s check test:*"
4444
},
4545
"devDependencies": {
46-
"@socketsecurity/eslint-config": "^2.0.0",
46+
"@socketsecurity/eslint-config": "^3.0.1",
4747
"@tsconfig/node14": "^1.0.3",
4848
"@types/chai": "^4.3.3",
4949
"@types/chai-as-promised": "^7.1.5",

0 commit comments

Comments
 (0)