Skip to content
Open
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
40 changes: 2 additions & 38 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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.`)
Expand Down
49 changes: 49 additions & 0 deletions src/resolve-api-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Bun from 'bun'
import path from 'node:path'
import os from 'node:os'

export async function resolveApiKey (): Promise<string | undefined> {
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
}
129 changes: 129 additions & 0 deletions test/resolve-api-key.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string | undefined>

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')
})
})