@@ -2,69 +2,123 @@ import vscode from 'vscode'
22import os from 'os'
33import path from 'path'
44import { DIAGNOSTIC_SOURCE_STR , EXTENSION_PREFIX } from './util'
5- import { getQuota } from './api'
5+ import constants from '@socketsecurity/registry/lib/constants'
6+ import https from 'node:https'
7+ import { once } from 'node:events'
8+ import { IncomingMessage } from 'node:http'
9+ import { text } from 'node:stream/consumers'
10+ import fs from 'fs'
11+ import { randomUUID } from 'node:crypto'
12+ const { SOCKET_PUBLIC_API_TOKEN } = constants
13+ export type APIConfig = {
14+ apiKey : string
15+ }
16+
17+ type OrgInfo = {
18+ id : string
19+ name : string
20+ image : string | null
21+ plan : 'opensource' | 'team' | 'enterprise'
22+ }
23+
24+ type OrganizationsRecord = {
25+ organizations : Record < string , OrgInfo >
26+ }
27+
28+ async function getOrganizations ( apiKey : string ) : Promise < OrganizationsRecord | null > {
29+ const authHeader = getAuthHeader ( apiKey )
30+ const orgReq = https . get ( 'https://api.socket.dev/v0/organizations' , {
31+ method : 'GET' ,
32+ headers : {
33+ Authorization : authHeader ,
34+ 'Content-Type' : 'application/json'
35+ }
36+ } )
37+ const [ orgRes ] = await once ( orgReq , 'response' ) as [ IncomingMessage ]
38+ if ( orgRes . statusCode !== 200 ) {
39+ return null
40+ }
41+ const orgs : OrganizationsRecord = JSON . parse ( await text ( orgRes ) )
42+ return orgs
43+ }
644
7- export async function activate ( context : vscode . ExtensionContext , disposables ? : Array < vscode . Disposable > ) {
45+ export async function activate ( context : vscode . ExtensionContext , disposables : Array < vscode . Disposable > ) {
846 //#region file path/watching
947 // responsible for watching files to know when to sync from disk
1048 let dataHome = process . platform === 'win32'
11- ? process . env [ 'LOCALAPPDATA' ]
12- : process . env [ 'XDG_DATA_HOME' ]
13-
49+ ? process . env [ 'LOCALAPPDATA' ]
50+ : process . env [ 'XDG_DATA_HOME' ]
51+
1452 if ( ! dataHome ) {
15- if ( process . platform === 'win32' ) throw new Error ( 'missing %LOCALAPPDATA%' )
16- const home = os . homedir ( )
17- dataHome = path . join ( home , ...( process . platform === 'darwin'
18- ? [ 'Library' , 'Application Support' ]
19- : [ '.local' , 'share' ]
20- ) )
53+ if ( process . platform === 'win32' ) throw new Error ( 'missing %LOCALAPPDATA%' )
54+ const home = os . homedir ( )
55+ dataHome = path . join ( home , ...( process . platform === 'darwin'
56+ ? [ 'Library' , 'Application Support' ]
57+ : [ '.local' , 'share' ]
58+ ) )
2159 }
22-
60+ let pleaseLoginStatusBar = vscode . window . createStatusBarItem ( vscode . StatusBarAlignment . Left , 100 )
61+ pleaseLoginStatusBar . hide ( )
62+ pleaseLoginStatusBar . text = `$(warning) Socket Security: Login`
63+ pleaseLoginStatusBar . tooltip = 'Socket Security needs to login for full functionality'
64+ pleaseLoginStatusBar . command = `${ EXTENSION_PREFIX } .login`
65+
2366 let defaultSettingsPath = path . join ( dataHome , 'socket' , 'settings' )
2467 let settingsPath = vscode . workspace . getConfiguration ( EXTENSION_PREFIX )
2568 . get ( 'settingsFile' , defaultSettingsPath )
2669 //#endregion
2770 //#region session sync
2871 // responsible for keeping disk an mem in sync
29- const PUBLIC_TOKEN = 'sktsec_t_--RAN5U4ivauy4w37-6aoKyYPDt5ZbaT5JBVMqiwKo_api'
3072 let liveSessions : Map < vscode . AuthenticationSession [ 'accessToken' ] , vscode . AuthenticationSession > = new Map ( )
31- const emitter = new vscode . EventEmitter < vscode . AuthenticationProviderAuthenticationSessionsChangeEvent > ( )
32- async function syncLiveSessionsFromDisk ( ) {
33- const settings_on_disk = JSON . parse ( Buffer . from (
34- new TextDecoder ( ) . decode ( await vscode . workspace . fs . readFile ( vscode . Uri . file ( settingsPath ) ) ) ,
35- 'base64'
36- ) . toString ( 'utf8' ) )
73+ const diskSessionsChanges = new vscode . EventEmitter < vscode . AuthenticationProviderAuthenticationSessionsChangeEvent > ( )
74+
75+ const watcher = vscode . workspace . createFileSystemWatcher (
76+ new vscode . RelativePattern (
77+ path . dirname ( settingsPath ) ,
78+ path . basename ( settingsPath )
79+ )
80+ )
81+ disposables ?. push (
82+ watcher ,
83+ watcher . onDidChange ( ( ) => syncLiveSessionFromDisk ( ) ) ,
84+ watcher . onDidCreate ( ( ) => syncLiveSessionFromDisk ( ) ) ,
85+ watcher . onDidDelete ( ( ) => { syncLiveSessionFromDisk ( ) } )
86+ )
87+ async function syncLiveSessionFromDisk ( ) {
88+ let settings_on_disk : { apiKey ?: string | null } = {
89+ apiKey : null
90+ }
91+ try {
92+ let fromDisk = JSON . parse ( Buffer . from (
93+ new TextDecoder ( ) . decode ( await vscode . workspace . fs . readFile ( vscode . Uri . file ( settingsPath ) ) ) ,
94+ 'base64'
95+ ) . toString ( 'utf8' ) )
96+ if ( fromDisk && typeof fromDisk === 'object' && fromDisk !== null ) {
97+ settings_on_disk = fromDisk
98+ }
99+ } catch { }
37100 const {
38101 apiKey
39102 } = settings_on_disk
40103 const sessionOnDisk : typeof liveSessions = new Map < vscode . AuthenticationSession [ 'accessToken' ] , vscode . AuthenticationSession > ( )
41- if ( apiKey ) {
42- sessionOnDisk . set (
43- apiKey ,
44- {
45- accessToken : apiKey ,
46- id : apiKey ,
47- account : {
48- id : apiKey ,
49- label : `API Key for ${ DIAGNOSTIC_SOURCE_STR } `
50- } ,
51- scopes : [ ] ,
52- }
53- )
104+ if ( typeof apiKey === 'string' && apiKey . length > 0 && apiKey !== SOCKET_PUBLIC_API_TOKEN ) {
105+ const organizations = await getOrganizations ( apiKey )
106+ const org = Object . values ( organizations ! . organizations ) [ 0 ]
107+ if ( org ) {
108+ sessionOnDisk . set (
109+ apiKey ,
110+ sessionFromAPIKey ( apiKey , org )
111+ )
112+ }
54113 }
55114 let added : Array < vscode . AuthenticationSession > = [ ]
56115 let changed : Array < vscode . AuthenticationSession > = [ ]
57116 let removed : Array < vscode . AuthenticationSession > = [ ]
58117 for ( const diskSession of sessionOnDisk . values ( ) ) {
59118 // already have this access token in mem session
119+ // remove from live sessions that haven't been sorted
60120 if ( liveSessions . has ( diskSession . accessToken ) ) {
61- const liveSession = liveSessions . get ( diskSession . accessToken )
62121 liveSessions . delete ( diskSession . accessToken )
63- // mem has same as what is on disk
64- if ( JSON . stringify ( liveSession ) !== JSON . stringify ( diskSession ) ) {
65- continue
66- }
67- changed . push ( diskSession )
68122 } else {
69123 added . push ( diskSession )
70124 }
@@ -74,110 +128,132 @@ export async function activate(context: vscode.ExtensionContext, disposables?: A
74128 }
75129 liveSessions = sessionOnDisk
76130 if ( added . length + changed . length + removed . length > 0 ) {
77- emitter . fire ( {
131+ diskSessionsChanges . fire ( {
78132 added,
79133 changed,
80134 removed
81135 } )
82136 }
83137 }
84- async function syncLiveSessionsToDisk ( ) {
138+ async function syncLiveSessionToDisk ( session : vscode . AuthenticationSession ) {
139+ if ( ! session || ! session . accessToken || session . accessToken === SOCKET_PUBLIC_API_TOKEN ) {
140+ return
141+ }
85142 const contents = Buffer . from (
86- JSON . stringify (
87- Array . from ( liveSessions . values ( ) , s => ( {
88- apiKey : s . accessToken
89- } ) ) ,
90- null ,
91- 2
92- )
143+ JSON . stringify ( {
144+ apiKey : session . accessToken
145+ } )
93146 ) . toString ( 'base64' )
94147 return vscode . workspace . fs . writeFile ( vscode . Uri . file ( settingsPath ) , new TextEncoder ( ) . encode ( contents ) )
95148 }
96- await syncLiveSessionsFromDisk ( )
97149 //#endregion
98150 //#region service glue
99151 const service = vscode . authentication . registerAuthenticationProvider ( `${ EXTENSION_PREFIX } ` , `${ DIAGNOSTIC_SOURCE_STR } ` , {
100152 onDidChangeSessions ( fn ) {
101- return emitter . event ( fn )
153+ return diskSessionsChanges . event ( fn ) ;
102154 } ,
103155 async getSessions ( scopes : readonly string [ ] | undefined , options : vscode . AuthenticationProviderSessionOptions ) : Promise < vscode . AuthenticationSession [ ] > {
104156 return Array . from ( liveSessions . values ( ) )
105157 } ,
106158 async createSession ( scopes : readonly string [ ] , options : vscode . AuthenticationProviderSessionOptions ) : Promise < vscode . AuthenticationSession > {
107- const realLogin = `Log in to ${ DIAGNOSTIC_SOURCE_STR } `
108- const publicLogin = `Use public token for ${ DIAGNOSTIC_SOURCE_STR } `
109- const res = await vscode . window . showQuickPick ( [
110- realLogin ,
111- publicLogin ,
112- ] )
113- if ( ! res ) {
114- throw new Error ( `Cancelled creation of session for ${ DIAGNOSTIC_SOURCE_STR } ` )
115- }
116- let apiKey : string
117- if ( res === publicLogin ) {
118- apiKey = ''
119- } else {
120- let keyInfo : string
121- let maybeApiKey = await vscode . window . showInputBox ( {
122- title : 'Socket Security API Token' ,
123- placeHolder : 'Leave this blank to use public demo token' ,
124- prompt : 'Enter your API token from https://socket.dev/' ,
125- async validateInput ( value ) {
126- if ( ! value ) return
127- keyInfo = ( await getQuota ( value ) ) !
128- if ( ! keyInfo ) return 'Unable to validate API key'
129- }
130- } )
131- // cancelled
132- if ( maybeApiKey === undefined ) {
133- throw new Error ( `Cancelled creation of session for ${ DIAGNOSTIC_SOURCE_STR } ` )
159+ let organizations : OrganizationsRecord
160+ let apiKey : string = await vscode . window . showInputBox ( {
161+ title : 'Socket Security API Token' ,
162+ placeHolder : 'Leave this blank to stay logged out' ,
163+ ignoreFocusOut : true ,
164+ prompt : 'Enter your API token from https://socket.dev/' ,
165+ async validateInput ( value ) {
166+ if ( ! value ) return
167+ organizations = ( await getOrganizations ( value ) ) !
168+ if ( ! organizations ) return 'Invalid API key'
134169 }
135- apiKey = maybeApiKey
136- }
137- if ( apiKey === '' ) {
138- apiKey = PUBLIC_TOKEN
139- }
140- const session = {
141- accessToken : apiKey ,
142- id : apiKey ,
143- account : {
144- id : apiKey ,
145- label : `API Key for ${ DIAGNOSTIC_SOURCE_STR } `
146- } ,
147- scopes : [ ] ,
170+ } ) ?? ''
171+ if ( ! apiKey ) {
172+ throw new Error ( 'User did not want to provide an API key' )
148173 }
174+ const org = Object . values ( organizations ! . organizations ) [ 0 ]
175+ const session = sessionFromAPIKey ( apiKey , org )
149176 let oldSessions = Array . from ( liveSessions . values ( ) )
177+ await syncLiveSessionToDisk ( session )
150178 liveSessions = new Map ( [
151179 [ apiKey , session ]
152180 ] )
153- emitter . fire ( {
181+ pleaseLoginStatusBar . hide ( )
182+ diskSessionsChanges . fire ( {
154183 added : [ session ] ,
155184 changed : [ ] ,
156185 removed : oldSessions
157186 } )
158- await syncLiveSessionsToDisk ( )
159187 return session
160188 } ,
161189 async removeSession ( sessionId : string ) : Promise < void > {
162190 const session = liveSessions . get ( sessionId )
191+ try {
192+ pleaseLoginStatusBar . show ( )
193+ } catch { }
194+ try {
195+ fs . unlinkSync ( settingsPath )
196+ } catch { }
163197 if ( session ) {
164- emitter . fire ( {
198+ diskSessionsChanges . fire ( {
165199 added : [ ] ,
166200 changed : [ ] ,
167201 removed : [ session ]
168202 } )
169- await syncLiveSessionsToDisk ( )
170203 }
171204 }
172205 } )
173206 context . subscriptions . push ( service )
174- vscode . commands . registerCommand ( `${ EXTENSION_PREFIX } .login` , ( ) => {
175- vscode . authentication . getSession ( `${ EXTENSION_PREFIX } ` , [ ] , {
207+ vscode . commands . registerCommand ( `${ EXTENSION_PREFIX } .login` , async ( ) => {
208+ let session = await vscode . authentication . getSession ( `${ EXTENSION_PREFIX } ` , [ ] , {
176209 createIfNone : true ,
177210 } )
178211 } )
212+ try {
213+ await syncLiveSessionFromDisk ( )
214+ } catch { }
215+ let session
216+ try {
217+ session = await vscode . authentication . getSession ( `${ EXTENSION_PREFIX } ` , [ ] , {
218+ createIfNone : false
219+ } )
220+ } catch { }
221+ if ( ! session ) {
222+ pleaseLoginStatusBar . show ( )
223+ }
179224 //#endregion
180225 return {
181226
182227 }
183228}
229+
230+ export async function getAPIKey ( ) {
231+ const session = await vscode . authentication . getSession ( `${ EXTENSION_PREFIX } ` , [ ] , {
232+ createIfNone : false ,
233+ } )
234+ if ( session ) {
235+ return session ?. accessToken
236+ } else {
237+ return SOCKET_PUBLIC_API_TOKEN
238+ }
239+ }
240+
241+ export function getAuthHeader ( apiKey : string ) {
242+ return `Bearer ${ apiKey } `
243+ }
244+ function sessionFromAPIKey ( apiKey : string , org : OrgInfo ) {
245+ // vscode auth does weird caching based upon ids
246+ // if we don't change the id various things stop working
247+ // like logging in and out with same account/api token
248+ const uniqueId = `${ apiKey } -${ randomUUID ( ) } `
249+ return {
250+ accessToken : apiKey ,
251+ id : `${ uniqueId } .session` ,
252+ account : {
253+ id : `${ apiKey } .account` ,
254+ label : `${ org . name } (${ org . plan } )`
255+ } ,
256+ scopes : [ ] ,
257+ }
258+ }
259+
0 commit comments