@@ -2,10 +2,10 @@ import isInteractive from 'is-interactive'
22import meow from 'meow'
33import ora from 'ora'
44import prompts from 'prompts'
5+ import terminalLink from 'terminal-link'
56
6- import { ChalkOrMarkdown } from '../../utils/chalk-markdown.js'
77import { AuthError , InputError } from '../../utils/errors.js'
8- import { setupSdk } from '../../utils/sdk.js'
8+ import { FREE_API_KEY , setupSdk } from '../../utils/sdk.js'
99import { getSetting , updateSetting } from '../../utils/settings.js'
1010
1111const description = 'Socket API login'
@@ -29,38 +29,96 @@ export const login = {
2929 importMeta,
3030 } )
3131
32+ /**
33+ * @param {{aborted: boolean} } state
34+ */
35+ const promptAbortHandler = ( state ) => {
36+ if ( state . aborted ) {
37+ process . nextTick ( ( ) => process . exit ( 1 ) )
38+ }
39+ }
40+
3241 if ( cli . input . length ) cli . showHelp ( )
3342
3443 if ( ! isInteractive ( ) ) {
3544 throw new InputError ( 'cannot prompt for credentials in a non-interactive shell' )
3645 }
37- const format = new ChalkOrMarkdown ( false )
38- const { apiKey } = await prompts ( {
46+ const result = await prompts ( {
3947 type : 'password' ,
4048 name : 'apiKey' ,
41- message : `Enter your ${ format . hyperlink (
49+ message : `Enter your ${ terminalLink (
4250 'Socket.dev API key' ,
4351 'https://docs.socket.dev/docs/api-keys'
44- ) } `,
52+ ) } (leave blank for a public key)`,
53+ onState : promptAbortHandler
4554 } )
4655
47- if ( ! apiKey ) {
48- ora ( 'API key not updated' ) . warn ( )
49- return
50- }
56+ const apiKey = result . apiKey || FREE_API_KEY
5157
5258 const spinner = ora ( 'Verifying API key...' ) . start ( )
5359
54- const oldKey = getSetting ( 'apiKey' )
55- updateSetting ( 'apiKey' , apiKey )
60+ /** @type {import('@socketsecurity/sdk').SocketSdkReturnType<'getOrganizations'>['data'] } */
61+ let orgs
62+
5663 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' } ` )
64+ const sdk = await setupSdk ( apiKey )
65+ const result = await sdk . getOrganizations ( )
66+ if ( ! result . success ) throw new AuthError ( )
67+ orgs = result . data
68+ spinner . succeed ( 'API key verified\n' )
6169 } catch ( e ) {
62- updateSetting ( 'apiKey' , oldKey )
6370 spinner . fail ( 'Invalid API key' )
71+ return
6472 }
73+
74+ /**
75+ * @template T
76+ * @param {T | null | undefined } value
77+ * @returns {value is T }
78+ */
79+ const nonNullish = value => value != null
80+
81+ /** @type {prompts.Choice[] } */
82+ const enforcedChoices = Object . values ( orgs . organizations )
83+ . filter ( nonNullish )
84+ . filter ( org => org . plan === 'enterprise' )
85+ . map ( org => ( {
86+ title : org . name ,
87+ value : org . id
88+ } ) )
89+
90+ /** @type {string[] } */
91+ let enforcedOrgs = [ ]
92+
93+ if ( enforcedChoices . length > 1 ) {
94+ const { id } = await prompts ( {
95+ type : 'select' ,
96+ name : 'id' ,
97+ hint : '\n Pick "None" if this is a personal device' ,
98+ message : 'Which organization\'s policies should Socket enforce system-wide?' ,
99+ choices : enforcedChoices . concat ( {
100+ title : 'None' ,
101+ value : null
102+ } ) ,
103+ onState : promptAbortHandler
104+ } )
105+ if ( id ) enforcedOrgs = [ id ]
106+ } else if ( enforcedChoices . length ) {
107+ const { confirmOrg } = await prompts ( {
108+ type : 'confirm' ,
109+ name : 'confirmOrg' ,
110+ message : `Should Socket enforce ${ enforcedChoices [ 0 ] ?. title } 's security policies system-wide?` ,
111+ initial : true ,
112+ onState : promptAbortHandler
113+ } )
114+ if ( confirmOrg ) {
115+ enforcedOrgs = [ enforcedChoices [ 0 ] ?. value ]
116+ }
117+ }
118+ // MUST DO all updateSetting ON SAME TICK TO AVOID PARTIAL WRITE
119+ updateSetting ( 'enforcedOrgs' , enforcedOrgs )
120+ const oldKey = getSetting ( 'apiKey' )
121+ updateSetting ( 'apiKey' , apiKey )
122+ spinner . succeed ( `API credentials ${ oldKey ? 'updated' : 'set' } ` )
65123 }
66124}
0 commit comments