diff --git a/src/index.ts b/src/index.ts index 552898a..a954267 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,45 +1,9 @@ import Bun from 'bun' -import path from 'node:path' -import os from 'node:os' import authenticated from './modes/authenticated' import unauthenticated from './modes/unauthenticated' +import { resolveApiKey } from './resolve-api-key' -let SOCKET_API_KEY = process.env.SOCKET_API_KEY - -if (typeof SOCKET_API_KEY !== 'string') { - // get OS app data directory - let dataHome = process.platform === 'win32' - ? Bun.env.LOCALAPPDATA - : Bun.env.XDG_DATA_HOME - - // fallback - if (!dataHome) { - if (process.platform === 'win32') throw new Error('missing %LOCALAPPDATA%') - - const home = os.homedir() - - dataHome = path.join(home, ...(process.platform === 'darwin' - ? ['Library', 'Application Support'] - : ['.local', 'share'] - )) - } - - // append `socket/settings` - const defaultSettingsPath = path.join(dataHome, 'socket', 'settings') - const file = Bun.file(defaultSettingsPath) - - // attempt to read token from socket settings - if (await file.exists()) { - const rawContent = await file.text() - // rawContent is base64, must decode - - try { - SOCKET_API_KEY = JSON.parse(Buffer.from(rawContent, 'base64').toString().trim()).apiToken - } catch { - throw new Error('error reading Socket settings') - } - } -} +const SOCKET_API_KEY = await resolveApiKey() if (!SOCKET_API_KEY) { console.log(`⚠ Socket Security Scanner free mode. Set SOCKET_API_KEY to use your Socket org settings.`) diff --git a/src/resolve-api-key.ts b/src/resolve-api-key.ts new file mode 100644 index 0000000..99f94f7 --- /dev/null +++ b/src/resolve-api-key.ts @@ -0,0 +1,49 @@ +import Bun from 'bun' +import path from 'node:path' +import os from 'node:os' + +export async function resolveApiKey (): Promise { + if (typeof process.env.SOCKET_API_KEY === 'string') { + return process.env.SOCKET_API_KEY + } + + // get OS app data directory + let dataHome = process.platform === 'win32' + ? Bun.env.LOCALAPPDATA + : Bun.env.XDG_DATA_HOME + + // fallback + if (!dataHome) { + if (process.platform === 'win32') throw new Error('missing %LOCALAPPDATA%') + + const home = os.homedir() + + dataHome = path.join(home, ...(process.platform === 'darwin' + ? ['Library', 'Application Support'] + : ['.local', 'share'] + )) + } + + // attempt to read token from socket settings + // supports both the legacy flat file and the CLI v2 directory layout + const settingsPath = path.join(dataHome, 'socket', 'settings') + const candidates = [ + Bun.file(settingsPath), + Bun.file(path.join(settingsPath, 'config.json')) + ] + + for (const file of candidates) { + if (await file.exists()) { + const rawContent = await file.text() + // rawContent is base64, must decode + + try { + return JSON.parse(Buffer.from(rawContent, 'base64').toString().trim()).apiToken + } catch { + throw new Error('error reading Socket settings') + } + } + } + + return undefined +} diff --git a/test/resolve-api-key.test.ts b/test/resolve-api-key.test.ts new file mode 100644 index 0000000..a0ff2d1 --- /dev/null +++ b/test/resolve-api-key.test.ts @@ -0,0 +1,129 @@ +import { expect, test, describe, beforeEach, afterEach } from 'bun:test' +import { resolveApiKey } from '../src/resolve-api-key' +import path from 'node:path' +import fs from 'node:fs' +import os from 'node:os' + +describe('resolveApiKey', () => { + let tmpDir: string + let originalEnv: Record + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'socket-test-')) + originalEnv = { + SOCKET_API_KEY: process.env.SOCKET_API_KEY, + XDG_DATA_HOME: process.env.XDG_DATA_HOME, + } + }) + + afterEach(() => { + // restore env + for (const [key, value] of Object.entries(originalEnv)) { + if (value === undefined) { + delete process.env[key] + } else { + process.env[key] = value + } + } + + fs.rmSync(tmpDir, { recursive: true, force: true }) + }) + + test('should return SOCKET_API_KEY from environment variable', async () => { + process.env.SOCKET_API_KEY = 'env-test-token' + + const result = await resolveApiKey() + + expect(result).toBe('env-test-token') + }) + + test('should read token from legacy flat settings file', async () => { + delete process.env.SOCKET_API_KEY + process.env.XDG_DATA_HOME = tmpDir + + const settingsDir = path.join(tmpDir, 'socket') + fs.mkdirSync(settingsDir, { recursive: true }) + + const token = 'legacy-flat-file-token' + const content = Buffer.from(JSON.stringify({ apiToken: token })).toString('base64') + fs.writeFileSync(path.join(settingsDir, 'settings'), content) + + const result = await resolveApiKey() + + expect(result).toBe(token) + }) + + test('should read token from CLI v2 settings/config.json', async () => { + delete process.env.SOCKET_API_KEY + process.env.XDG_DATA_HOME = tmpDir + + const settingsDir = path.join(tmpDir, 'socket', 'settings') + fs.mkdirSync(settingsDir, { recursive: true }) + + const token = 'cli-v2-directory-token' + const content = Buffer.from(JSON.stringify({ apiToken: token })).toString('base64') + fs.writeFileSync(path.join(settingsDir, 'config.json'), content) + + const result = await resolveApiKey() + + expect(result).toBe(token) + }) + + test('should prefer legacy flat file over CLI v2 directory', async () => { + delete process.env.SOCKET_API_KEY + process.env.XDG_DATA_HOME = tmpDir + + const socketDir = path.join(tmpDir, 'socket') + + // create legacy flat file + fs.mkdirSync(socketDir, { recursive: true }) + const legacyToken = 'legacy-token' + fs.writeFileSync( + path.join(socketDir, 'settings'), + Buffer.from(JSON.stringify({ apiToken: legacyToken })).toString('base64') + ) + + // Note: can't have both a file and directory named 'settings', + // so this test just verifies the flat file is read when it exists + + const result = await resolveApiKey() + + expect(result).toBe(legacyToken) + }) + + test('should return undefined when no settings exist', async () => { + delete process.env.SOCKET_API_KEY + process.env.XDG_DATA_HOME = tmpDir + + const result = await resolveApiKey() + + expect(result).toBeUndefined() + }) + + test('should throw on malformed settings file', async () => { + delete process.env.SOCKET_API_KEY + process.env.XDG_DATA_HOME = tmpDir + + const settingsDir = path.join(tmpDir, 'socket') + fs.mkdirSync(settingsDir, { recursive: true }) + fs.writeFileSync(path.join(settingsDir, 'settings'), 'not-valid-base64-json!!!') + + await expect(resolveApiKey()).rejects.toThrow('error reading Socket settings') + }) + + test('should prefer env variable over settings file', async () => { + process.env.SOCKET_API_KEY = 'env-takes-priority' + process.env.XDG_DATA_HOME = tmpDir + + const settingsDir = path.join(tmpDir, 'socket', 'settings') + fs.mkdirSync(settingsDir, { recursive: true }) + fs.writeFileSync( + path.join(settingsDir, 'config.json'), + Buffer.from(JSON.stringify({ apiToken: 'file-token' })).toString('base64') + ) + + const result = await resolveApiKey() + + expect(result).toBe('env-takes-priority') + }) +})